@antelopejs/dms-database 0.0.13 → 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 +9 -6
  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
@@ -0,0 +1,198 @@
1
+ import type { FieldDescriptor } from '../composables/useDatabaseSchemas'
2
+
3
+ // How a column's type reads in the inspector, the diagram and the grid: an
4
+ // icon and a mono label. Colour never encodes the type (D-09): it is kept for
5
+ // structure, primary keys and relations, which `columnRole` names.
6
+
7
+ interface FieldTypeView {
8
+ icon: string
9
+ label: string
10
+ }
11
+
12
+ type DescriptorOf<K extends FieldDescriptor['kind']> = Extract<
13
+ FieldDescriptor,
14
+ { kind: K }
15
+ >
16
+ type DescriptorViews = {
17
+ [K in FieldDescriptor['kind']]: (descriptor: DescriptorOf<K>) => FieldTypeView
18
+ }
19
+
20
+ const UNKNOWN: FieldTypeView = { icon: 'i-ph-question', label: 'unknown' }
21
+ const STRING_ICON = 'i-ph-text-aa'
22
+ const NUMBER_ICON = 'i-ph-hash'
23
+ const BOOLEAN_ICON = 'i-ph-toggle-right'
24
+ const LIST_ICON = 'i-ph-brackets-square'
25
+ const OBJECT_ICON = 'i-ph-brackets-curly'
26
+ const ENUM_ICON = 'i-ph-list-checks'
27
+ const UNION_ICON = 'i-ph-split'
28
+
29
+ const NAMED_FIELDS: Record<string, FieldTypeView> = {
30
+ email: { icon: 'i-ph-at', label: 'email' },
31
+ password: { icon: 'i-ph-lock', label: 'password' },
32
+ secret: { icon: 'i-ph-lock', label: 'secret' },
33
+ }
34
+
35
+ const LITERAL_ICONS: Record<string, string> = {
36
+ string: STRING_ICON,
37
+ number: NUMBER_ICON,
38
+ boolean: BOOLEAN_ICON,
39
+ }
40
+
41
+ function literalLabel(value: string | number | boolean): string {
42
+ return typeof value === 'string' ? `"${value}"` : String(value)
43
+ }
44
+
45
+ function label(descriptor: FieldDescriptor): string {
46
+ return describeField(descriptor).label
47
+ }
48
+
49
+ const VIEWS: DescriptorViews = {
50
+ string: () => ({ icon: STRING_ICON, label: 'string' }),
51
+ number: () => ({ icon: NUMBER_ICON, label: 'number' }),
52
+ boolean: () => ({ icon: BOOLEAN_ICON, label: 'boolean' }),
53
+ date: () => ({ icon: 'i-ph-calendar-blank', label: 'date' }),
54
+ any: () => ({ icon: 'i-ph-asterisk', label: 'any' }),
55
+ unknown: () => UNKNOWN,
56
+ null: () => ({ icon: 'i-ph-circle-dashed', label: 'null' }),
57
+ undefined: () => ({ icon: 'i-ph-circle-dashed', label: 'undefined' }),
58
+ literal: (d) => ({
59
+ icon: LITERAL_ICONS[typeof d.value] ?? STRING_ICON,
60
+ label: literalLabel(d.value),
61
+ }),
62
+ array: (d) => ({
63
+ icon: LIST_ICON,
64
+ label: `${d.element ? label(d.element) : 'any'}[]`,
65
+ }),
66
+ tuple: (d) => ({
67
+ icon: LIST_ICON,
68
+ label: `[${d.elements.map(label).join(', ')}]`,
69
+ }),
70
+ object: (d) => ({
71
+ icon: OBJECT_ICON,
72
+ label: d.partial ? 'partial object' : 'object',
73
+ }),
74
+ record: (d) => ({
75
+ icon: OBJECT_ICON,
76
+ label: `Record<${d.key ? label(d.key) : 'string'}, ${label(d.value)}>`,
77
+ }),
78
+ union: (d) => ({
79
+ icon:
80
+ d.members.length > 0 && d.members.every((m) => m.kind === 'literal')
81
+ ? ENUM_ICON
82
+ : UNION_ICON,
83
+ label: d.members.map(label).join(' | '),
84
+ }),
85
+ intersection: (d) => ({
86
+ icon: UNION_ICON,
87
+ label: d.members.map(label).join(' & '),
88
+ }),
89
+ keyof: (d) => ({
90
+ icon: ENUM_ICON,
91
+ label:
92
+ d.keys.length > 0 ? d.keys.map((k) => `"${k}"`).join(' | ') : 'keyof',
93
+ }),
94
+ refinement: (d) => ({ icon: describeField(d.base).icon, label: d.name }),
95
+ recursive: (d) => ({ icon: UNKNOWN.icon, label: d.name }),
96
+ }
97
+
98
+ function isDescriptor(value: unknown): value is FieldDescriptor {
99
+ return (
100
+ value !== null &&
101
+ typeof value === 'object' &&
102
+ typeof (value as { kind?: unknown }).kind === 'string' &&
103
+ (value as { kind: string }).kind in VIEWS
104
+ )
105
+ }
106
+
107
+ /** Icon and label of a schema-declared type. */
108
+ export function describeField(
109
+ descriptor: unknown,
110
+ fieldName = '',
111
+ ): FieldTypeView {
112
+ const named = NAMED_FIELDS[fieldName.toLowerCase()]
113
+ if (named) return named
114
+ if (!isDescriptor(descriptor)) return UNKNOWN
115
+ const view = VIEWS[descriptor.kind] as (d: FieldDescriptor) => FieldTypeView
116
+ return view(descriptor)
117
+ }
118
+
119
+ const RUNTIME_KINDS = new Set([
120
+ 'string',
121
+ 'number',
122
+ 'boolean',
123
+ 'array',
124
+ 'object',
125
+ 'null',
126
+ 'undefined',
127
+ ])
128
+
129
+ /**
130
+ * Icon and label of a type sampled at run time by the browse `/columns`
131
+ * route (string, number, boolean, object, array, null), so sampled and
132
+ * declared columns share one vocabulary.
133
+ */
134
+ export function describeRuntimeType(
135
+ type: string,
136
+ fieldName: string,
137
+ ): FieldTypeView {
138
+ if (type === 'date') return describeField({ kind: 'date' }, fieldName)
139
+ if (type === 'object')
140
+ return describeField({ kind: 'object', fields: {} }, fieldName)
141
+ if (type === 'array') return describeField({ kind: 'array' }, fieldName)
142
+ return describeField(
143
+ RUNTIME_KINDS.has(type)
144
+ ? ({ kind: type } as FieldDescriptor)
145
+ : { kind: 'unknown' },
146
+ fieldName,
147
+ )
148
+ }
149
+
150
+ const PRIMARY_KEY_NAMES = new Set(['id', '_id'])
151
+ const PRIMARY_KEY_INDEXES = ['pk', 'primary'] as const
152
+
153
+ export function isPrimaryKey(
154
+ fieldName: string,
155
+ indexes: Record<string, { fields?: string[] }>,
156
+ ): boolean {
157
+ if (PRIMARY_KEY_NAMES.has(fieldName)) return true
158
+ return PRIMARY_KEY_INDEXES.some((name) =>
159
+ indexes[name]?.fields?.includes(fieldName),
160
+ )
161
+ }
162
+
163
+ /** What a column is for the structure: its colour role in every view. */
164
+ export type ColumnRole = 'key' | 'relation' | 'plain'
165
+
166
+ /** The CSS colour class of each role: warning for keys, primary for relations. */
167
+ export const COLUMN_ROLE_CLASSES: Record<ColumnRole, string> = {
168
+ key: 'text-warning',
169
+ relation: 'text-primary',
170
+ plain: 'text-muted',
171
+ }
172
+
173
+ interface ModifierView {
174
+ icon: string
175
+ /** i18n key of the modifier's name. */
176
+ labelKey?: string
177
+ /** The modifier id, shown when it has no name of its own. */
178
+ id: string
179
+ }
180
+
181
+ const MODIFIERS: Record<string, Omit<ModifierView, 'id'>> = {
182
+ HashModifier: {
183
+ icon: 'i-ph-fingerprint',
184
+ labelKey: 'dms_database.modifiers.hashed',
185
+ },
186
+ LocalizationModifier: {
187
+ icon: 'i-ph-translate',
188
+ labelKey: 'dms_database.modifiers.localized',
189
+ },
190
+ EncryptionModifier: {
191
+ icon: 'i-ph-lock-key',
192
+ labelKey: 'dms_database.modifiers.encrypted',
193
+ },
194
+ }
195
+
196
+ export function describeModifier(id: string): ModifierView {
197
+ return { id, ...(MODIFIERS[id] ?? { icon: 'i-ph-sliders' }) }
198
+ }
@@ -0,0 +1,19 @@
1
+ const MINUTE_MS = 60_000
2
+ const MINUTES_PER_HOUR = 60
3
+ const HOURS_PER_DAY = 24
4
+
5
+ // Human "3 minutes ago" style label for an ISO timestamp, relative to now, in
6
+ // the interface language. Not named `formatRelativeTime`: the DMS core layer
7
+ // auto-imports a helper of that name, and the two would shadow each other.
8
+ export function formatDatabaseRelativeTime(
9
+ iso: string,
10
+ locale?: string,
11
+ ): string {
12
+ const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
13
+ const minutes = Math.round((new Date(iso).getTime() - Date.now()) / MINUTE_MS)
14
+ if (Math.abs(minutes) < MINUTES_PER_HOUR)
15
+ return formatter.format(minutes, 'minute')
16
+ const hours = Math.round(minutes / MINUTES_PER_HOUR)
17
+ if (Math.abs(hours) < HOURS_PER_DAY) return formatter.format(hours, 'hour')
18
+ return formatter.format(Math.round(hours / HOURS_PER_DAY), 'day')
19
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ CROSS_INSTANCE,
2
3
  Datum,
3
4
  Query,
4
5
  Schema,
@@ -10,7 +11,7 @@ import {
10
11
  ValueProxy,
11
12
  } from '@antelopejs/interface-database/staged-query'
12
13
 
13
- export interface QueryStage {
14
+ interface QueryStage {
14
15
  stage: string
15
16
  options?: unknown
16
17
  args: unknown[]
@@ -37,7 +38,12 @@ function classNameOf(obj: object): string | undefined {
37
38
  return undefined
38
39
  }
39
40
 
41
+ // CROSS_INSTANCE is a symbol, which JSON drops: it travels as this sentinel,
42
+ // which the server turns back into the symbol.
43
+ export const CROSS_INSTANCE_SENTINEL = '__CROSS_INSTANCE__'
44
+
40
45
  export function encodeStaged(node: unknown): unknown {
46
+ if (node === CROSS_INSTANCE) return CROSS_INSTANCE_SENTINEL
41
47
  if (node === null || node === undefined) return node
42
48
  if (typeof node !== 'object') return node
43
49
  if (Array.isArray(node)) return node.map(encodeStaged)
@@ -0,0 +1,122 @@
1
+ <script setup lang="ts">
2
+ import { useDatabaseHealth } from '../build/composables/useDatabaseHealth'
3
+ import { useIntervalFn, useNow } from '@vueuse/core'
4
+
5
+ // The overview's health hero: whether the database answers, how fast, and
6
+ // what it holds. The probe runs again every PROBE_INTERVAL_MS while the page
7
+ // is open, so a lost connection shows up without a reload.
8
+ const PROBE_INTERVAL_MS = 30_000
9
+ const SECOND_MS = 1000
10
+ const MINUTE_S = 60
11
+
12
+ const { t, n, locale } = useI18n()
13
+ const { health, isLoading, error, refresh } = useDatabaseHealth()
14
+ const now = useNow({ interval: SECOND_MS })
15
+
16
+ useIntervalFn(() => refresh(), PROBE_INTERVAL_MS)
17
+
18
+ // A failed health request is a lost connection too: the route answers
19
+ // whenever the backend is up.
20
+ const isDown = computed(
21
+ () => Boolean(error.value) || health.value.status === 'down',
22
+ )
23
+
24
+ const checkedAgo = computed(() => {
25
+ const checkedAt = health.value.checkedAt
26
+ if (!checkedAt) return ''
27
+ const seconds = Math.max(
28
+ 0,
29
+ Math.round(
30
+ (now.value.getTime() - new Date(checkedAt).getTime()) / SECOND_MS,
31
+ ),
32
+ )
33
+ return seconds < MINUTE_S
34
+ ? t('dms_database.overview.connection.checked_seconds', { count: seconds })
35
+ : t('dms_database.overview.connection.checked_minutes', {
36
+ count: Math.round(seconds / MINUTE_S),
37
+ })
38
+ })
39
+
40
+ const since = computed(() => {
41
+ const parts = [health.value.driver, checkedAgo.value].filter(Boolean)
42
+ return parts.join(' · ')
43
+ })
44
+
45
+ function compact(value: number): string {
46
+ return new Intl.NumberFormat(locale.value, {
47
+ notation: 'compact',
48
+ maximumFractionDigits: 1,
49
+ }).format(value)
50
+ }
51
+
52
+ const healthyMetrics = computed(() => {
53
+ const h = health.value
54
+ return [
55
+ {
56
+ label: t('dms_database.overview.connection.latency'),
57
+ value: h.latencyMs ?? '—',
58
+ unit: h.latencyMs === null ? undefined : 'ms',
59
+ sub: t('dms_database.overview.connection.latency_sub'),
60
+ tone: 'success' as const,
61
+ },
62
+ {
63
+ label: t('dms_database.overview.connection.tables'),
64
+ value: n(h.tableCount),
65
+ sub: t('dms_database.overview.connection.tables_sub', h.schemaCount),
66
+ },
67
+ {
68
+ label: t('dms_database.overview.connection.rows'),
69
+ value: compact(h.totalRows),
70
+ sub: t('dms_database.overview.connection.rows_sub', h.relationCount),
71
+ },
72
+ {
73
+ label: t('dms_database.overview.connection.indexes'),
74
+ value: n(h.indexCount),
75
+ sub: t('dms_database.overview.connection.indexes_sub'),
76
+ },
77
+ ]
78
+ })
79
+
80
+ const downMetrics = computed(() => [
81
+ {
82
+ label: t('dms_database.overview.connection.last_error'),
83
+ value:
84
+ health.value.error ?? t('dms_database.overview.connection.no_answer'),
85
+ tone: 'error' as const,
86
+ },
87
+ {
88
+ label: t('dms_database.overview.connection.next_probe'),
89
+ value: Math.round(PROBE_INTERVAL_MS / SECOND_MS),
90
+ unit: 's',
91
+ sub: t('dms_database.overview.connection.next_probe_sub'),
92
+ },
93
+ ])
94
+ </script>
95
+
96
+ <template>
97
+ <div class="grid gap-2">
98
+ <DmsStatusSummary
99
+ :status="isDown ? 'down' : 'ok'"
100
+ :status-label="t('dms_database.overview.connection.label')"
101
+ :status-value="
102
+ isDown
103
+ ? t('dms_database.overview.connection.down')
104
+ : t('dms_database.overview.connection.healthy')
105
+ "
106
+ :since="since"
107
+ :metrics="isDown ? downMetrics : healthyMetrics"
108
+ :class="isLoading && !health.checkedAt ? 'animate-pulse' : undefined"
109
+ />
110
+ <div v-if="isDown" class="flex justify-end">
111
+ <UButton
112
+ color="neutral"
113
+ variant="outline"
114
+ size="sm"
115
+ icon="i-ph-arrows-clockwise"
116
+ :loading="isLoading"
117
+ :label="t('dms_database.overview.connection.retry')"
118
+ @click="refresh()"
119
+ />
120
+ </div>
121
+ </div>
122
+ </template>
@@ -0,0 +1,126 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ instanceBadge,
4
+ useDataBrowserTabs,
5
+ } from '../build/composables/useDataBrowserTabs'
6
+ import { useDatabaseSchemas } from '../build/composables/useDatabaseSchemas'
7
+ import { formatDatabaseRelativeTime } from '../build/utils/relativeTime'
8
+ import { type RecentTable, readRecentTables } from '../build/data/recentTables'
9
+ import { useStagedEdits } from '../build/data/stagedEdits'
10
+ import DataBrowserSidebar from './DataBrowserSidebar.vue'
11
+ import DataBrowserTabBar from './DataBrowserTabBar.vue'
12
+ import DataGrid from './DataGrid.vue'
13
+
14
+ // The data browser: a table list scoped to one schema and instance, the open
15
+ // tables as tabs, and the grid of the active one. Edits are staged per tab
16
+ // and written only when saved; leaving with staged edits asks first.
17
+
18
+ const { t, locale } = useI18n()
19
+ const {
20
+ schemas,
21
+ findTable,
22
+ isLoading: schemasLoading,
23
+ error: schemasError,
24
+ refresh: refreshSchemas,
25
+ } = useDatabaseSchemas()
26
+ const { tabs, activeTab, restore, openTab, openFromRoute } =
27
+ useDataBrowserTabs()
28
+ const staged = useStagedEdits()
29
+ const route = useDmsRoute()
30
+
31
+ onMounted(() => restore())
32
+ // A link from this page to another table (a relation) keeps the page mounted.
33
+ watch(
34
+ () => route.query,
35
+ () => openFromRoute(),
36
+ )
37
+
38
+ const hasStagedEdits = computed(() =>
39
+ tabs.value.some((tab) => staged.count(tab.id) > 0),
40
+ )
41
+ useUnsavedChanges({ dirty: hasStagedEdits })
42
+
43
+ // Schema-introspected columns of the active table: the grid falls back to
44
+ // them when sampling finds no row, and reads its relation columns from them.
45
+ const activeTableSummary = computed(() =>
46
+ activeTab.value
47
+ ? findTable(activeTab.value.schema, activeTab.value.table)
48
+ : undefined,
49
+ )
50
+
51
+ // Read once mounted: the server renders without the browser's storage, and
52
+ // a list only the client has would not match its markup.
53
+ const recent = ref<RecentTable[]>([])
54
+ onMounted(() => {
55
+ recent.value = readRecentTables()
56
+ })
57
+ watch(activeTab, () => {
58
+ recent.value = readRecentTables()
59
+ })
60
+
61
+ function instanceLabel(instance: string): string {
62
+ return instanceBadge(instance, t('dms_database.data.scope.all')) ?? ''
63
+ }
64
+ </script>
65
+
66
+ <template>
67
+ <div
68
+ class="border-default bg-default flex min-h-0 flex-1 overflow-hidden rounded-xl border"
69
+ >
70
+ <DataBrowserSidebar
71
+ :schemas="schemas"
72
+ :loading="schemasLoading"
73
+ :error="Boolean(schemasError)"
74
+ @retry="refreshSchemas()"
75
+ />
76
+ <section class="flex min-w-0 flex-1 flex-col">
77
+ <DataBrowserTabBar />
78
+ <DataGrid
79
+ v-if="activeTab"
80
+ :key="activeTab.id"
81
+ :tab="activeTab"
82
+ :table="activeTableSummary"
83
+ />
84
+ <div v-else class="grid flex-1 place-items-center p-6">
85
+ <DmsEmptyState
86
+ icon="i-ph-rows"
87
+ :title="t('dms_database.data.empty.pick_title')"
88
+ :description="
89
+ recent.length
90
+ ? t('dms_database.data.empty.pick_description')
91
+ : t('dms_database.data.empty.pick_description_first')
92
+ "
93
+ >
94
+ <template v-if="recent.length" #actions>
95
+ <div class="grid w-full max-w-sm gap-1.5">
96
+ <DmsEyebrow :label="t('dms_database.data.empty.recent')" />
97
+ <button
98
+ v-for="entry in recent"
99
+ :key="`${entry.schema}.${entry.instance}.${entry.table}`"
100
+ type="button"
101
+ class="border-default hover:bg-elevated flex items-center gap-2 rounded-md border px-3 py-2 text-left font-mono text-[12.5px]"
102
+ @click="
103
+ openTab(entry.schema, entry.instance, entry.table, false)
104
+ "
105
+ >
106
+ <UIcon name="i-ph-table" class="text-primary size-3.5" />
107
+ <span class="text-toned">
108
+ {{ entry.schema }}.{{ entry.table }}
109
+ </span>
110
+ <span
111
+ v-if="instanceLabel(entry.instance)"
112
+ class="text-primary text-[11px]"
113
+ >
114
+ {{ instanceLabel(entry.instance) }}
115
+ </span>
116
+ <span class="text-dimmed ml-auto font-sans text-xs">
117
+ {{ formatDatabaseRelativeTime(entry.openedAt, locale) }}
118
+ </span>
119
+ </button>
120
+ </div>
121
+ </template>
122
+ </DmsEmptyState>
123
+ </div>
124
+ </section>
125
+ </div>
126
+ </template>