@antelopejs/dms-database 0.0.14 → 0.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 (173) hide show
  1. package/README.md +30 -5
  2. package/dist/db/models/query_history.model.d.ts +1 -0
  3. package/dist/db/models/query_history.model.js +6 -0
  4. package/dist/db/models/query_history.model.js.map +1 -1
  5. package/dist/db/tables/query_history.table.d.ts +3 -0
  6. package/dist/db/tables/query_history.table.js +12 -0
  7. package/dist/db/tables/query_history.table.js.map +1 -1
  8. package/dist/db/tables/saved_queries.table.d.ts +1 -0
  9. package/dist/db/tables/saved_queries.table.js +4 -0
  10. package/dist/db/tables/saved_queries.table.js.map +1 -1
  11. package/dist/i18n/messages.d.ts +39 -0
  12. package/dist/i18n/messages.js +64 -0
  13. package/dist/i18n/messages.js.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.d.ts +11 -0
  18. package/dist/module.js +55 -0
  19. package/dist/module.js.map +1 -0
  20. package/dist/pages/data.d.ts +1 -1
  21. package/dist/pages/data.js +9 -6
  22. package/dist/pages/data.js.map +1 -1
  23. package/dist/pages/diagram.d.ts +9 -0
  24. package/dist/pages/diagram.js +45 -0
  25. package/dist/pages/diagram.js.map +1 -0
  26. package/dist/pages/index.d.ts +1 -0
  27. package/dist/pages/index.js +1 -0
  28. package/dist/pages/index.js.map +1 -1
  29. package/dist/pages/overview.d.ts +4 -1
  30. package/dist/pages/overview.js +113 -6
  31. package/dist/pages/overview.js.map +1 -1
  32. package/dist/pages/query.d.ts +1 -1
  33. package/dist/pages/query.js +22 -6
  34. package/dist/pages/query.js.map +1 -1
  35. package/dist/pages/schemas.d.ts +2 -1
  36. package/dist/pages/schemas.js +143 -6
  37. package/dist/pages/schemas.js.map +1 -1
  38. package/dist/routes/data.d.ts +8 -0
  39. package/dist/routes/data.js +84 -21
  40. package/dist/routes/data.js.map +1 -1
  41. package/dist/routes/index.d.ts +1 -0
  42. package/dist/routes/index.js +1 -0
  43. package/dist/routes/index.js.map +1 -1
  44. package/dist/routes/overview.d.ts +12 -0
  45. package/dist/routes/overview.js +55 -0
  46. package/dist/routes/overview.js.map +1 -0
  47. package/dist/routes/query.d.ts +5 -1
  48. package/dist/routes/query.js +162 -27
  49. package/dist/routes/query.js.map +1 -1
  50. package/dist/routes/tables.d.ts +15 -12
  51. package/dist/routes/tables.js +95 -55
  52. package/dist/routes/tables.js.map +1 -1
  53. package/dist/service/catalog.d.ts +3 -0
  54. package/dist/service/catalog.js +26 -0
  55. package/dist/service/catalog.js.map +1 -0
  56. package/dist/service/health.d.ts +3 -3
  57. package/dist/service/health.js +17 -14
  58. package/dist/service/health.js.map +1 -1
  59. package/dist/service/introspect.d.ts +3 -1
  60. package/dist/service/introspect.js +131 -43
  61. package/dist/service/introspect.js.map +1 -1
  62. package/dist/service/overview.d.ts +9 -0
  63. package/dist/service/overview.js +113 -0
  64. package/dist/service/overview.js.map +1 -0
  65. package/dist/service/queryInspection.d.ts +15 -0
  66. package/dist/service/queryInspection.js +107 -0
  67. package/dist/service/queryInspection.js.map +1 -0
  68. package/dist/service/references.d.ts +9 -0
  69. package/dist/service/references.js +52 -0
  70. package/dist/service/references.js.map +1 -0
  71. package/dist/service/schemaFilters.d.ts +26 -0
  72. package/dist/service/schemaFilters.js +42 -0
  73. package/dist/service/schemaFilters.js.map +1 -0
  74. package/dist/service/stagedSerialization.d.ts +2 -0
  75. package/dist/service/stagedSerialization.js +38 -10
  76. package/dist/service/stagedSerialization.js.map +1 -1
  77. package/dist/service/types.d.ts +11 -1
  78. package/dist/types/constants.d.ts +2 -0
  79. package/dist/types/constants.js +3 -1
  80. package/dist/types/constants.js.map +1 -1
  81. package/dist/utils/query.d.ts +0 -2
  82. package/dist/utils/query.js +0 -20
  83. package/dist/utils/query.js.map +1 -1
  84. package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
  85. package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
  86. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
  87. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
  88. package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
  89. package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
  90. package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
  91. package/frontend-vue/app/build/data/cellValues.ts +132 -0
  92. package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
  93. package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
  94. package/frontend-vue/app/build/data/recentTables.ts +48 -0
  95. package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
  96. package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
  97. package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
  98. package/frontend-vue/app/build/diagram/autosave.ts +72 -0
  99. package/frontend-vue/app/build/diagram/graph.ts +585 -0
  100. package/frontend-vue/app/build/diagram/history.ts +178 -0
  101. package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
  102. package/frontend-vue/app/build/diagram/sync.ts +137 -0
  103. package/frontend-vue/app/build/query/completion.ts +100 -0
  104. package/frontend-vue/app/build/query/csv.ts +32 -0
  105. package/frontend-vue/app/build/query/examples.ts +38 -0
  106. package/frontend-vue/app/build/query/runs.ts +168 -0
  107. package/frontend-vue/app/build/query/tableColumns.ts +35 -0
  108. package/frontend-vue/app/build/query/target.ts +143 -0
  109. package/frontend-vue/app/build/schemas/filterState.ts +128 -0
  110. package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
  111. package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
  112. package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
  113. package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
  114. package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
  115. package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
  116. package/frontend-vue/app/components/DataBrowser.vue +126 -0
  117. package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
  118. package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
  119. package/frontend-vue/app/components/DataFilterBar.vue +163 -0
  120. package/frontend-vue/app/components/DataGrid.vue +737 -499
  121. package/frontend-vue/app/components/DataGridCell.vue +125 -77
  122. package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
  123. package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
  124. package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
  125. package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
  126. package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
  127. package/frontend-vue/app/components/InstancePicker.vue +195 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +30 -29
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
  130. package/frontend-vue/app/components/QueryConsole.vue +726 -0
  131. package/frontend-vue/app/components/QueryLibrary.vue +314 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
  133. package/frontend-vue/app/components/RowDrawer.vue +520 -0
  134. package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
  135. package/frontend-vue/app/components/SchemaPicker.vue +86 -0
  136. package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
  137. package/frontend-vue/app/components/TableInspector.vue +741 -0
  138. package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
  139. package/frontend-vue/dms.frontend.ts +5 -14
  140. package/frontend-vue/i18n/locales/database-en-GB.json +526 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +528 -195
  142. package/frontend-vue/package.json +2 -1
  143. package/frontend-vue/pnpm-lock.yaml +2601 -858
  144. package/frontend-vue/pnpm-workspace.yaml +1 -1
  145. package/frontend-vue/tests/dataBrowser.test.ts +206 -0
  146. package/frontend-vue/tests/diagram.test.ts +516 -0
  147. package/frontend-vue/tests/helpers.test.ts +205 -0
  148. package/frontend-vue/tests/instanceOptions.test.ts +53 -0
  149. package/frontend-vue/tests/locales.test.ts +56 -0
  150. package/frontend-vue/tests/persistence.test.ts +106 -5
  151. package/frontend-vue/tests/pickerMenus.test.ts +254 -0
  152. package/frontend-vue/tests/queryConsole.test.ts +282 -0
  153. package/frontend-vue/tests/queryRuntime.test.ts +2 -2
  154. package/frontend-vue/tests/registration.test.ts +36 -25
  155. package/frontend-vue/tests/schemasFilters.test.ts +115 -0
  156. package/package.json +7 -7
  157. package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
  158. package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
  159. package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
  160. package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
  161. package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
  162. package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
  163. package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
  164. package/frontend-vue/app/composables/useQueryStore.ts +0 -117
  165. package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
  166. package/frontend-vue/app/custom-pages/database/data.vue +0 -41
  167. package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
  168. package/frontend-vue/app/custom-pages/database/query.vue +0 -436
  169. package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
  170. package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
  171. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
  172. package/frontend-vue/app/utils/dmsTableView.ts +0 -83
  173. package/frontend-vue/app/utils/relativeTime.ts +0 -18
@@ -1,100 +1,118 @@
1
1
  <script setup lang="ts">
2
- interface NoteNodeData {
3
- noteId: string;
4
- text: string;
5
- color: string | null;
6
- width: number;
7
- height: number;
8
- onTextChange: (noteId: string, text: string) => void;
9
- onDelete: (noteId: string) => void;
10
- }
11
-
12
- const props = defineProps<{ data: NoteNodeData; selected: boolean }>();
2
+ import { onClickOutside } from '@vueuse/core'
3
+ import type { NoteNodeData } from '../build/diagram/graph'
13
4
 
14
- // Sticky-note palette (amber). Centralised so the note frame, header and ink
15
- // stay in sync instead of being repeated as literals across the template.
16
- const NOTE_DEFAULT_BG = "#fef9c3";
17
- const NOTE_BORDER = "rgba(202, 138, 4, 0.45)";
18
- const NOTE_HEADER_BG = "rgba(250, 204, 21, 0.5)";
19
- const NOTE_HEADER_BORDER = "rgba(202, 138, 4, 0.35)";
20
- const NOTE_ACCENT = "#92400e";
21
- const NOTE_INK = "#422006";
22
- const NOTE_PLACEHOLDER_INK = "rgba(66, 32, 6, 0.45)";
5
+ // A sticky note on the diagram: double-click to write, saved with the layout.
6
+ // A click anywhere else, or Escape, ends the edit.
7
+ const props = defineProps<{
8
+ data: NoteNodeData
9
+ selected?: boolean
10
+ /** Set by Vue Flow once it has measured the node. */
11
+ dimensions?: { width: number; height: number }
12
+ }>()
23
13
 
24
- const editing = ref(false);
25
- const textBuffer = ref(props.data.text);
14
+ const root = useTemplateRef<HTMLElement>('root')
15
+ const textarea = useTemplateRef<HTMLTextAreaElement>('textarea')
16
+ const editing = ref(false)
17
+ const textBuffer = ref(props.data.text)
26
18
 
27
19
  watch(
28
20
  () => props.data.text,
29
- (v) => {
30
- if (!editing.value) textBuffer.value = v;
21
+ (value) => {
22
+ if (!editing.value) textBuffer.value = value
31
23
  },
32
- );
24
+ )
25
+
26
+ // Vue Flow shows a new node only once it has measured it: until then the
27
+ // field cannot take the focus. It is tried again when the node's size comes in.
28
+ let focusPending = false
29
+
30
+ function focusField() {
31
+ const field = textarea.value
32
+ if (!focusPending || !field || !editing.value) return
33
+ field.focus()
34
+ if (document.activeElement !== field) return
35
+ focusPending = false
36
+ field.setSelectionRange(field.value.length, field.value.length)
37
+ }
38
+
39
+ watch(() => props.dimensions?.width, focusField, { flush: 'post' })
40
+
41
+ async function startEdit() {
42
+ if (editing.value) return
43
+ textBuffer.value = props.data.text
44
+ editing.value = true
45
+ focusPending = true
46
+ await nextTick()
47
+ focusField()
48
+ }
33
49
 
34
50
  function commitText() {
35
- editing.value = false;
36
- const next = textBuffer.value;
37
- if (next !== props.data.text) {
38
- props.data.onTextChange(props.data.noteId, next);
51
+ if (!editing.value) return
52
+ editing.value = false
53
+ focusPending = false
54
+ if (textBuffer.value !== props.data.text) {
55
+ props.data.onTextChange(props.data.noteId, textBuffer.value)
39
56
  }
57
+ props.data.onEditEnd(props.data.noteId)
40
58
  }
59
+
60
+ // The canvas swallows the pointer down that would blur the field: a click on
61
+ // the pane or a table has to end the edit itself.
62
+ onClickOutside(root, () => {
63
+ if (editing.value) commitText()
64
+ })
65
+
66
+ // A note just created opens ready to write.
67
+ onMounted(() => {
68
+ if (props.data.autoEdit) startEdit()
69
+ })
41
70
  </script>
42
71
 
43
72
  <template>
44
73
  <div
45
- class="rounded-lg shadow-md border relative overflow-hidden flex flex-col"
46
- :class="{ 'ring-2 ring-primary': selected }"
47
- :style="{
48
- background: data.color ?? NOTE_DEFAULT_BG,
49
- borderColor: NOTE_BORDER,
50
- width: `${data.width}px`,
51
- height: `${data.height}px`,
52
- }"
53
- @dblclick="editing = true"
74
+ ref="root"
75
+ class="bg-warning/10 border-warning/40 text-toned relative flex flex-col overflow-hidden rounded-sm border shadow-sm backdrop-blur-sm"
76
+ :class="{ 'ring-warning ring-2': selected || editing }"
77
+ :style="{ width: `${data.width}px`, height: `${data.height}px` }"
78
+ @dblclick="startEdit"
54
79
  >
55
- <!-- Header strip: gives the note a recognisable sticky-note frame and
56
- hosts the delete affordance without overlapping the text. -->
57
80
  <div
58
- class="flex items-center justify-between h-6 px-2 shrink-0 cursor-grab"
59
- :style="{
60
- background: NOTE_HEADER_BG,
61
- borderBottom: `1px solid ${NOTE_HEADER_BORDER}`,
62
- }"
81
+ class="text-warning flex h-7 shrink-0 cursor-grab items-center gap-1.5 px-3"
63
82
  >
64
- <UIcon
65
- name="i-ph-note-pencil"
66
- class="size-3.5"
67
- :style="{ color: NOTE_ACCENT }"
68
- />
83
+ <UIcon name="i-ph-note-pencil" class="size-3.5" />
84
+ <span
85
+ class="font-mono text-[10px] font-semibold uppercase tracking-[0.12em]"
86
+ >
87
+ {{ $t('dms_database.diagram.notes.label') }}
88
+ </span>
69
89
  <button
70
- v-if="selected"
90
+ v-if="selected || editing"
71
91
  type="button"
72
- class="p-0.5 rounded hover:bg-black/10"
73
- :style="{ color: NOTE_ACCENT }"
92
+ class="nodrag hover:bg-warning/20 ml-auto rounded p-0.5"
74
93
  :aria-label="$t('dms_database.diagram.notes.delete')"
94
+ :title="$t('dms_database.diagram.notes.delete')"
95
+ @pointerdown.stop
75
96
  @click.stop="data.onDelete(data.noteId)"
76
97
  >
77
- <UIcon name="i-ph-x" class="size-3.5" />
98
+ <UIcon name="i-ph-trash" class="size-3.5" />
78
99
  </button>
79
100
  </div>
80
- <!-- Body. Fixed dark ink so the text stays readable on the light note in
81
- any app theme (the previous theme-aware text colour washed out). -->
82
101
  <textarea
83
102
  v-if="editing"
103
+ ref="textarea"
84
104
  v-model="textBuffer"
85
- class="flex-1 w-full bg-transparent text-sm p-3 resize-none outline-none"
86
- :style="{ color: NOTE_INK }"
105
+ class="nodrag nowheel w-full flex-1 resize-none bg-transparent px-3 pb-3 text-[12.5px] leading-normal outline-none"
87
106
  :placeholder="$t('dms_database.diagram.notes.placeholder')"
88
- autofocus
89
107
  @blur="commitText"
90
- @keydown.escape.prevent="commitText"
108
+ @keydown.escape.prevent.stop="commitText"
91
109
  />
92
110
  <div
93
111
  v-else
94
- class="flex-1 w-full p-3 text-sm whitespace-pre-wrap break-words cursor-text overflow-auto"
95
- :style="{ color: data.text ? NOTE_INK : NOTE_PLACEHOLDER_INK }"
112
+ class="w-full flex-1 cursor-text overflow-auto whitespace-pre-wrap break-words px-3 pb-3 text-[12.5px] leading-normal"
113
+ :class="data.text ? '' : 'text-dimmed'"
96
114
  >
97
- {{ data.text || $t("dms_database.diagram.notes.placeholder") }}
115
+ {{ data.text || $t('dms_database.diagram.notes.placeholder') }}
98
116
  </div>
99
117
  </div>
100
118
  </template>
@@ -1,34 +1,46 @@
1
1
  <script setup lang="ts">
2
- import { BaseEdge, type EdgeProps, Position, getBezierPath } from "@vue-flow/core";
2
+ import {
3
+ BaseEdge,
4
+ type EdgeProps,
5
+ Position,
6
+ getBezierPath,
7
+ } from '@vue-flow/core'
8
+ import { EDGE_ACTIVE_COLOR, type EdgeEnds } from '../build/diagram/graph'
3
9
 
4
- const props = defineProps<EdgeProps>();
10
+ // A relation column pointing at a table. The relations of the selected table
11
+ // are drawn in the primary colour; the others stay quiet.
12
+ const props = defineProps<EdgeProps<EdgeEnds>>()
5
13
 
6
- const ARC_MIN_OFFSET = 60;
7
- const ARC_DY_FACTOR = 0.35;
14
+ const edgeStyle = computed(() =>
15
+ props.data?.active
16
+ ? { ...(props.style as object), stroke: EDGE_ACTIVE_COLOR, strokeWidth: 2 }
17
+ : props.style,
18
+ )
19
+
20
+ const ARC_MIN_OFFSET = 60
21
+ const ARC_DY_FACTOR = 0.35
8
22
 
9
23
  function sideSign(position: Position): 1 | -1 | 0 {
10
- if (position === Position.Right) return 1;
11
- if (position === Position.Left) return -1;
12
- return 0;
24
+ if (position === Position.Right) return 1
25
+ if (position === Position.Left) return -1
26
+ return 0
13
27
  }
14
28
 
15
29
  const computedPath = computed<[string, number, number]>(() => {
16
- const sourceSign = sideSign(props.sourcePosition);
17
- const targetSign = sideSign(props.targetPosition);
30
+ const sourceSign = sideSign(props.sourcePosition)
31
+ const targetSign = sideSign(props.targetPosition)
18
32
  if (sourceSign !== 0 && targetSign !== 0) {
19
- const dy = Math.abs(props.targetY - props.sourceY);
20
- const offset = Math.max(ARC_MIN_OFFSET, dy * ARC_DY_FACTOR);
21
- const cx1 = props.sourceX + sourceSign * offset;
22
- const cy1 = props.sourceY;
23
- const cx2 = props.targetX + targetSign * offset;
24
- const cy2 = props.targetY;
25
- const d = `M${props.sourceX},${props.sourceY} C${cx1},${cy1} ${cx2},${cy2} ${props.targetX},${props.targetY}`;
33
+ const dy = Math.abs(props.targetY - props.sourceY)
34
+ const offset = Math.max(ARC_MIN_OFFSET, dy * ARC_DY_FACTOR)
35
+ const cx1 = props.sourceX + sourceSign * offset
36
+ const cy1 = props.sourceY
37
+ const cx2 = props.targetX + targetSign * offset
38
+ const cy2 = props.targetY
39
+ const d = `M${props.sourceX},${props.sourceY} C${cx1},${cy1} ${cx2},${cy2} ${props.targetX},${props.targetY}`
26
40
  // Label sits at the bezier midpoint (t = 0.5).
27
- const labelX =
28
- (props.sourceX + 3 * cx1 + 3 * cx2 + props.targetX) / 8;
29
- const labelY =
30
- (props.sourceY + 3 * cy1 + 3 * cy2 + props.targetY) / 8;
31
- return [d, labelX, labelY];
41
+ const labelX = (props.sourceX + 3 * cx1 + 3 * cx2 + props.targetX) / 8
42
+ const labelY = (props.sourceY + 3 * cy1 + 3 * cy2 + props.targetY) / 8
43
+ return [d, labelX, labelY]
32
44
  }
33
45
  const [d, lx, ly] = getBezierPath({
34
46
  sourceX: props.sourceX,
@@ -37,9 +49,9 @@ const computedPath = computed<[string, number, number]>(() => {
37
49
  targetX: props.targetX,
38
50
  targetY: props.targetY,
39
51
  targetPosition: props.targetPosition,
40
- });
41
- return [d, lx, ly];
42
- });
52
+ })
53
+ return [d, lx, ly]
54
+ })
43
55
  </script>
44
56
 
45
57
  <template>
@@ -48,7 +60,7 @@ const computedPath = computed<[string, number, number]>(() => {
48
60
  :path="computedPath[0]"
49
61
  :marker-end="markerEnd"
50
62
  :marker-start="markerStart"
51
- :style="style"
63
+ :style="edgeStyle"
52
64
  :label-x="computedPath[1]"
53
65
  :label-y="computedPath[2]"
54
66
  :label="undefined"
@@ -56,13 +68,31 @@ const computedPath = computed<[string, number, number]>(() => {
56
68
  />
57
69
  <text
58
70
  v-if="label"
59
- class="vue-flow__edge-text"
71
+ class="diagram-edge-label"
72
+ :class="{ 'diagram-edge-label--active': data?.active }"
60
73
  :x="computedPath[1]"
61
74
  :y="computedPath[2]"
62
75
  text-anchor="middle"
63
76
  dominant-baseline="middle"
64
- :style="labelStyle"
65
77
  >
66
78
  {{ label }}
67
79
  </text>
68
80
  </template>
81
+
82
+ <style scoped>
83
+ /* Theme tokens, with a halo of the canvas colour so the line that crosses
84
+ the label does not run through its letters. */
85
+ .diagram-edge-label {
86
+ fill: var(--ui-text-muted);
87
+ stroke: var(--ui-bg-muted);
88
+ stroke-width: 4px;
89
+ stroke-linejoin: round;
90
+ paint-order: stroke;
91
+ font: 500 10.5px var(--font-mono, ui-monospace, monospace);
92
+ pointer-events: none;
93
+ user-select: none;
94
+ }
95
+ .diagram-edge-label--active {
96
+ fill: var(--ui-primary);
97
+ }
98
+ </style>
@@ -1,31 +1,30 @@
1
1
  <script setup lang="ts">
2
- import { Handle, Position } from "@vue-flow/core";
2
+ import { Handle, Position } from '@vue-flow/core'
3
3
 
4
- interface StubNodeData {
5
- targetSchema: string;
6
- targetTable: string;
7
- }
8
-
9
- defineProps<{ data: StubNodeData }>();
4
+ import type { StubNodeData } from '../build/diagram/graph'
10
5
 
11
- const route = useDmsRoute();
12
- const router = useDmsRouter();
13
-
14
- function jumpToSchema(schemaId: string) {
15
- router.replace({ query: { ...route.query, schema: schemaId } });
16
- }
6
+ // A table a relation points to that is not drawn: one of another schema, or
7
+ // one the focus leaves out. The dashed chip opens it: in its own schema's
8
+ // diagram, or here, out of the focus.
9
+ defineProps<{ data: StubNodeData }>()
17
10
  </script>
18
11
 
19
12
  <template>
20
13
  <button
21
14
  type="button"
22
- class="diagram-stub-node flex items-center gap-1 px-2 py-1 rounded border border-dashed border-default bg-elevated text-xs text-muted hover:text-highlighted hover:border-primary transition-colors opacity-70 relative"
23
- @click="jumpToSchema(data.targetSchema)"
15
+ class="diagram-stub-node border-accented bg-muted text-muted hover:border-primary hover:text-highlighted relative flex h-7 items-center gap-1.5 rounded-sm border border-dashed px-2.5 font-mono text-[11.5px] transition-colors"
16
+ :title="
17
+ $t('dms_database.diagram.stub_title', {
18
+ schema: data.targetSchema,
19
+ table: data.targetTable,
20
+ })
21
+ "
22
+ @click="data.onOpenSchema(data.targetSchema, data.targetTable)"
24
23
  >
25
24
  <Handle id="stub::left::target" type="target" :position="Position.Left" />
26
25
  <Handle id="stub::right::target" type="target" :position="Position.Right" />
27
- <UIcon name="i-ph-arrow-right" />
28
- <span class="font-mono">{{ data.targetSchema }}.{{ data.targetTable }}</span>
26
+ <UIcon name="i-ph-arrow-square-out" class="text-dimmed size-3.5" />
27
+ <span>{{ data.targetSchema }}.{{ data.targetTable }}</span>
29
28
  </button>
30
29
  </template>
31
30
 
@@ -1,86 +1,124 @@
1
1
  <script setup lang="ts">
2
- import { Handle, Position } from "@vue-flow/core";
2
+ import { Handle, Position } from '@vue-flow/core'
3
+ import type { TableNodeData } from '../build/diagram/graph'
4
+ import {
5
+ COLUMN_ROLE_CLASSES,
6
+ type ColumnRole,
7
+ describeField,
8
+ describeModifier,
9
+ isPrimaryKey,
10
+ } from '../build/utils/fieldTypes'
3
11
 
4
- interface NodeData {
5
- tableName: string;
6
- fields: Record<string, unknown>;
7
- indexes: Record<string, { fields?: string[]; multi?: boolean }>;
8
- modifiers?: Record<string, string[]>;
9
- }
12
+ // One table of the diagram (D-09): each column with its type as mono text and
13
+ // a neutral icon. Colour only marks structure: the primary key, and the
14
+ // relation columns, which light up with the selected table's relations.
15
+
16
+ // The canvas keeps the selection in `data.selected`: Vue Flow's own one is
17
+ // off for tables, so one table at most is drawn selected.
18
+ const props = defineProps<{ data: TableNodeData }>()
19
+
20
+ const SIDES = ['left', 'right'] as const
21
+ const KINDS = ['source', 'target'] as const
22
+ const HANDLE_POSITIONS = { left: Position.Left, right: Position.Right } as const
10
23
 
11
- defineProps<{ data: NodeData }>();
24
+ const relationTargets = computed(
25
+ () =>
26
+ new Map(
27
+ props.data.table.relations.map((relation) => [
28
+ relation.fromField,
29
+ relation,
30
+ ]),
31
+ ),
32
+ )
33
+
34
+ const rows = computed(() =>
35
+ Object.entries(props.data.table.fields).map(([name, descriptor]) => {
36
+ const key = isPrimaryKey(name, props.data.table.indexes ?? {})
37
+ const relation = relationTargets.value.get(name)
38
+ const role: ColumnRole = key ? 'key' : relation ? 'relation' : 'plain'
39
+ const type = describeField(descriptor, name)
40
+ return {
41
+ name,
42
+ role,
43
+ icon: key ? 'i-ph-key' : relation ? 'i-ph-arrow-right' : type.icon,
44
+ type: relation ? relation.toTable : type.label,
45
+ modifiers: (props.data.table.modifiers?.[name] ?? []).map(
46
+ describeModifier,
47
+ ),
48
+ highlighted: props.data.highlighted.includes(name),
49
+ }
50
+ }),
51
+ )
12
52
  </script>
13
53
 
14
54
  <template>
15
- <div class="diagram-table-node bg-elevated border border-default rounded-lg w-60 shadow-md">
16
- <!-- Fallback handles on the node body for edges that reference a non-existent field -->
17
- <Handle id="__node__::left::source" type="source" :position="Position.Left" />
18
- <Handle id="__node__::left::target" type="target" :position="Position.Left" />
19
- <Handle id="__node__::right::source" type="source" :position="Position.Right" />
20
- <Handle id="__node__::right::target" type="target" :position="Position.Right" />
55
+ <div
56
+ class="diagram-table-node bg-default w-[232px] cursor-grab rounded-md border text-xs shadow-sm transition-[opacity,border-color,box-shadow]"
57
+ :class="[
58
+ data.selected
59
+ ? 'border-primary shadow-[0_0_0_3px_var(--ui-color-primary-500)]/20 ring-primary/30 ring-2'
60
+ : 'border-accented',
61
+ data.dimmed ? 'opacity-45' : '',
62
+ ]"
63
+ >
64
+ <template v-for="side in SIDES" :key="side">
65
+ <Handle
66
+ v-for="kind in KINDS"
67
+ :id="`__node__::${side}::${kind}`"
68
+ :key="kind"
69
+ :type="kind"
70
+ :position="HANDLE_POSITIONS[side]"
71
+ />
72
+ </template>
21
73
 
22
- <div class="px-3 py-2 border-b border-default flex items-center gap-2">
23
- <UIcon name="i-ph-table" />
24
- <span class="font-mono text-sm">{{ data.tableName }}</span>
74
+ <div
75
+ class="border-default flex h-9 items-center gap-2 border-b pl-3 pr-2.5"
76
+ >
77
+ <UIcon name="i-ph-table" class="text-primary size-[15px] shrink-0" />
78
+ <span
79
+ class="text-highlighted truncate font-mono text-[12.5px] font-semibold"
80
+ >
81
+ {{ data.tableName }}
82
+ </span>
25
83
  </div>
26
84
 
27
85
  <ul class="py-1">
28
86
  <li
29
- v-for="(fieldType, fieldName) in data.fields"
30
- :key="fieldName"
31
- class="relative flex items-center justify-between px-3 py-1 text-sm"
87
+ v-for="row in rows"
88
+ :key="row.name"
89
+ class="relative flex h-6 items-center gap-1.5 pl-3 pr-2.5 font-mono"
90
+ :class="[
91
+ row.role === 'relation' ? 'text-primary' : 'text-toned',
92
+ row.highlighted ? 'bg-primary/10' : '',
93
+ ]"
32
94
  >
33
- <Handle
34
- :id="`${fieldName}::left::source`"
35
- type="source"
36
- :position="Position.Left"
37
- />
38
- <Handle
39
- :id="`${fieldName}::left::target`"
40
- type="target"
41
- :position="Position.Left"
42
- />
43
- <Handle
44
- :id="`${fieldName}::right::source`"
45
- type="source"
46
- :position="Position.Right"
95
+ <template v-for="side in SIDES" :key="side">
96
+ <Handle
97
+ v-for="kind in KINDS"
98
+ :id="`${row.name}::${side}::${kind}`"
99
+ :key="kind"
100
+ :type="kind"
101
+ :position="HANDLE_POSITIONS[side]"
102
+ />
103
+ </template>
104
+ <UIcon
105
+ :name="row.icon"
106
+ class="size-3 shrink-0"
107
+ :class="
108
+ row.role === 'plain' ? 'text-dimmed' : COLUMN_ROLE_CLASSES[row.role]
109
+ "
47
110
  />
48
- <Handle
49
- :id="`${fieldName}::right::target`"
50
- type="target"
51
- :position="Position.Right"
111
+ <span class="truncate">{{ row.name }}</span>
112
+ <UIcon
113
+ v-for="modifier in row.modifiers"
114
+ :key="modifier.id"
115
+ :name="modifier.icon"
116
+ class="text-dimmed size-3 shrink-0"
117
+ :title="modifier.labelKey ? $t(modifier.labelKey) : modifier.id"
52
118
  />
53
-
54
- <span class="flex items-center gap-0.5 font-mono min-w-0">
55
- <UTooltip
56
- v-if="isPrimaryKey(String(fieldName), data.indexes)"
57
- text="Primary key"
58
- >
59
- <UIcon name="i-ph-key" class="text-amber-500 size-4 shrink-0" />
60
- </UTooltip>
61
- <template
62
- v-for="modifierId in data.modifiers?.[String(fieldName)] ?? []"
63
- :key="modifierId"
64
- >
65
- <UTooltip :text="resolveModifierIcon(modifierId).label">
66
- <UIcon
67
- :name="resolveModifierIcon(modifierId).icon"
68
- :class="[resolveModifierIcon(modifierId).color, 'size-4 shrink-0']"
69
- />
70
- </UTooltip>
71
- </template>
72
- <span class="truncate ml-1">{{ fieldName }}</span>
119
+ <span class="text-dimmed ml-auto max-w-24 truncate pl-2 text-[10.5px]">
120
+ {{ row.type }}
73
121
  </span>
74
-
75
- <UTooltip :text="inferFieldTypeIcon(fieldType, String(fieldName)).label">
76
- <UIcon
77
- :name="inferFieldTypeIcon(fieldType, String(fieldName)).icon"
78
- :class="[
79
- inferFieldTypeIcon(fieldType, String(fieldName)).color,
80
- 'size-4 shrink-0',
81
- ]"
82
- />
83
- </UTooltip>
84
122
  </li>
85
123
  </ul>
86
124
  </div>