@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
@@ -1,254 +1,337 @@
1
1
  <script setup lang="ts">
2
- import type { SchemaSummary } from "../composables/useDatabaseSchemas";
3
-
4
- const TABLES_LIST_ENDPOINT = "/api/database/tables/list";
5
- // Row-count fetch cap; mirrors the backend MAX_LIMIT for /api/database/tables.
6
- const ROW_COUNT_FETCH_LIMIT = 200;
7
-
8
- interface TableListRow {
9
- schema: string;
10
- name: string;
11
- columnCount: number;
12
- indexCount: number;
13
- elementCount: number;
2
+ import {
3
+ CROSS_INSTANCE_VALUE,
4
+ DEFAULT_INSTANCE_VALUE,
5
+ instanceChoiceOf,
6
+ tabInstanceOf,
7
+ useDataBrowserTabs,
8
+ } from '../build/composables/useDataBrowserTabs'
9
+ import { isFilterToken } from '../build/composables/useDataBrowserGrid'
10
+ import type { InstanceChoice } from '../build/data/instanceOptions'
11
+ import { onKeyStroke } from '@vueuse/core'
12
+ import type { SchemaSummary } from '../build/composables/useDatabaseSchemas'
13
+ import InstancePicker from './InstancePicker.vue'
14
+ import SchemaPicker from './SchemaPicker.vue'
15
+
16
+ // The data browser's scope (D-04): a labelled schema picker, the instance
17
+ // ("all" is read-only), both the Schemas page's searchable pickers, the table
18
+ // search under them, and the schema's tables with their row count in that
19
+ // instance. A
20
+ // table open in a tab carries a hollow ring, the active one included: the
21
+ // fill of the row says which is shown.
22
+
23
+ const TABLES_ENDPOINT = '/api/database/browse/tables'
24
+
25
+ interface TableEntry {
26
+ name: string
27
+ elementCount: number
14
28
  }
15
29
 
16
- const props = defineProps<{ schemas: SchemaSummary[] }>();
30
+ const props = defineProps<{
31
+ schemas: SchemaSummary[]
32
+ /** The schema list is being read. */
33
+ loading?: boolean
34
+ /** The schema list could not be read. */
35
+ error?: boolean
36
+ }>()
37
+
38
+ const emit = defineEmits<{ retry: [] }>()
17
39
 
18
- const { t } = useI18n();
19
- const { $authFetch } = useAuthFetch();
20
- const { activeTab, openTab } = useDataBrowserTabs();
21
- const route = useDmsRoute();
40
+ const { t, n } = useI18n()
41
+ const { $authFetch } = useAuthFetch()
42
+ const { tabs, activeId, activeTab, activations, openTab } = useDataBrowserTabs()
43
+ const route = useDmsRoute()
22
44
 
23
- const selectedSchemaId = ref<string | null>(null);
24
- const selectedInstanceId = ref<string>(DEFAULT_INSTANCE_VALUE);
25
- const tableSearch = ref("");
45
+ const schemaId = ref<string | null>(null)
46
+ const instance = ref<string>(DEFAULT_INSTANCE_VALUE)
47
+ const filter = ref('')
48
+ const filterInput = useTemplateRef<{ inputRef?: HTMLInputElement }>(
49
+ 'filterInput',
50
+ )
26
51
 
27
- // A schema-only deep-link (?schema=X without table, e.g. "Open in data
28
- // browser" on a schema card) pre-selects the pickers. Captured at setup:
29
- // the tab restore may rewrite the URL before the schemas fetch resolves.
30
- const initialQuerySchema =
31
- typeof route.query.schema === "string" && route.query.schema
52
+ // A schema-only link (?schema=X with no table) picks the schema; otherwise
53
+ // the active tab leads, then the first schema. A schema or instance picked
54
+ // here holds until the user goes to a tab (another one, or the same again).
55
+ const linkedSchema =
56
+ typeof route.query.schema === 'string' && !route.query.table
32
57
  ? route.query.schema
33
- : null;
34
- const initialQueryInstance =
35
- typeof route.query.instance === "string" && route.query.instance
36
- ? route.query.instance
37
- : null;
38
- const initialQueryHasTable =
39
- typeof route.query.table === "string" && route.query.table.length > 0;
40
-
41
- // How the pickers were last set. The active tab (restored tabs, deep-links)
42
- // may override an automatic selection but never a user-driven one — so
43
- // browsing another schema never loses the user's place, while a restore that
44
- // lands AFTER the first-schema fallback still wins (the sidebar setup runs
45
- // before the page's onMounted restore()).
46
- type SelectionSource = "none" | "fallback" | "tab" | "user";
47
- const selectionSource = ref<SelectionSource>("none");
58
+ : null
48
59
 
49
60
  watch(
50
- activeTab,
51
- (tab) => {
52
- if (!tab || selectionSource.value === "user") return;
53
- selectedSchemaId.value = tab.schema;
54
- selectedInstanceId.value = tab.instance;
55
- selectionSource.value = "tab";
61
+ [activeId, activations],
62
+ () => {
63
+ const tab = activeTab.value
64
+ if (!tab) return
65
+ schemaId.value = tab.schema
66
+ instance.value = tab.instance
56
67
  },
57
68
  { immediate: true },
58
- );
69
+ )
70
+
59
71
  watch(
60
72
  () => props.schemas,
61
- (schemas) => {
62
- if (selectionSource.value !== "none" || schemas.length === 0) return;
63
- // A schema-only deep-link wins over the first-schema fallback and, being
64
- // an explicit user request, is not overridden by a restored tab either.
65
- if (
66
- initialQuerySchema &&
67
- !initialQueryHasTable &&
68
- schemas.some((schema) => schema.id === initialQuerySchema)
69
- ) {
70
- selectedSchemaId.value = initialQuerySchema;
71
- selectedInstanceId.value = initialQueryInstance ?? DEFAULT_INSTANCE_VALUE;
72
- selectionSource.value = "user";
73
- return;
74
- }
75
- selectedSchemaId.value = schemas[0]?.id ?? null;
76
- selectionSource.value = "fallback";
73
+ (list) => {
74
+ if (schemaId.value || list.length === 0) return
75
+ const linked = list.find((schema) => schema.id === linkedSchema)
76
+ schemaId.value = linked?.id ?? list[0]?.id ?? null
77
77
  },
78
78
  { immediate: true },
79
- );
80
-
81
- // User-driven picker changes (the USelects emit update:model-value only for
82
- // user interaction, not for the programmatic assignments above).
83
- function onUserSchemaChange() {
84
- selectionSource.value = "user";
85
- // Named instances belong to one schema; reset when the user switches.
86
- selectedInstanceId.value = DEFAULT_INSTANCE_VALUE;
79
+ )
80
+
81
+ function pickSchema(id: string | null) {
82
+ if (!id) return
83
+ schemaId.value = id
84
+ // Named instances belong to one schema.
85
+ instance.value = DEFAULT_INSTANCE_VALUE
87
86
  }
88
- function onUserInstanceChange() {
89
- selectionSource.value = "user";
87
+
88
+ function pickInstance(choice: InstanceChoice) {
89
+ instance.value = tabInstanceOf(choice)
90
90
  }
91
91
 
92
- const currentSchema = computed(
93
- () => props.schemas.find((schema) => schema.id === selectedSchemaId.value) ?? null,
94
- );
95
-
96
- const schemaOptions = computed(() =>
97
- props.schemas.map((schema) => ({ label: schema.id, value: schema.id })),
98
- );
99
- const instanceOptions = computed(() => {
100
- const options = [
101
- { label: t("dms_database.data.instanceDefault"), value: DEFAULT_INSTANCE_VALUE },
102
- ];
103
- for (const id of currentSchema.value?.instances ?? []) {
104
- options.push({ label: id, value: id });
105
- }
106
- options.push({
107
- label: t("dms_database.data.instanceCross"),
108
- value: CROSS_INSTANCE_VALUE,
109
- });
110
- return options;
111
- });
112
-
113
- // --- row counts (best effort, display only) ---
114
- const rowCounts = ref<Record<string, number>>({});
115
- // No explicit locale: let the runtime format with the browser's preference.
116
- const countFormatter = new Intl.NumberFormat();
92
+ const schema = computed(
93
+ () => props.schemas.find((s) => s.id === schemaId.value) ?? null,
94
+ )
95
+ // Nothing to list yet: the list is read, or failed to be.
96
+ const schemasPending = computed(
97
+ () => Boolean(props.loading) && props.schemas.length === 0,
98
+ )
99
+ const schemasFailed = computed(
100
+ () => Boolean(props.error) && !props.loading && props.schemas.length === 0,
101
+ )
102
+ const schemaIds = computed(() => props.schemas.map((s) => s.id))
103
+ const instanceChoice = computed(() => instanceChoiceOf(instance.value))
104
+ // The footer names the instance as a tab's badge does ("all").
105
+ const instanceName = computed(() => {
106
+ if (instance.value === DEFAULT_INSTANCE_VALUE)
107
+ return t('dms_database.pickers.instance.default')
108
+ if (instance.value === CROSS_INSTANCE_VALUE)
109
+ return t('dms_database.data.scope.all')
110
+ return instance.value
111
+ })
112
+
113
+ // --- tables and their counts in the picked instance ---
114
+ const counts = ref<Record<string, number>>({})
115
+ const countsFailed = ref(false)
117
116
 
118
117
  watch(
119
- [selectedSchemaId, selectedInstanceId],
120
- async ([schemaId, instanceId]) => {
121
- rowCounts.value = {};
122
- if (!schemaId) return;
123
- const query: Record<string, string | number> = {
124
- filter_schema: isFilterToken(schemaId),
125
- limit: ROW_COUNT_FETCH_LIMIT,
126
- };
127
- // Counts follow the instance picker (backend defaults to the base
128
- // instance when the filter is absent).
129
- if (instanceId !== DEFAULT_INSTANCE_VALUE) {
130
- query.filter_instance = isFilterToken(instanceId);
131
- }
132
- const isCurrent = () =>
133
- selectedSchemaId.value === schemaId &&
134
- selectedInstanceId.value === instanceId;
118
+ [schemaId, instance],
119
+ async ([id, picked]) => {
120
+ counts.value = {}
121
+ countsFailed.value = false
122
+ if (!id) return
123
+ const query: Record<string, string> = { filter_schema: isFilterToken(id) }
124
+ if (picked !== DEFAULT_INSTANCE_VALUE)
125
+ query.filter_instance = isFilterToken(picked)
135
126
  try {
136
- const res = await $authFetch<{ results: TableListRow[] }>(
137
- TABLES_LIST_ENDPOINT,
138
- { query },
139
- );
140
- // Ignore a response that arrived after the user switched selection.
141
- if (!isCurrent()) return;
142
- const counts: Record<string, number> = {};
143
- for (const row of res.results ?? []) counts[row.name] = row.elementCount;
144
- rowCounts.value = counts;
127
+ const res = await $authFetch<{ items: TableEntry[] }>(TABLES_ENDPOINT, {
128
+ query,
129
+ })
130
+ if (schemaId.value !== id || instance.value !== picked) return
131
+ counts.value = Object.fromEntries(
132
+ res.items.map((item) => [item.name, item.elementCount]),
133
+ )
145
134
  } catch {
146
- // Only clear when the failure concerns the current selection — a late
147
- // failure from a previous one must not wipe fresh counts.
148
- if (isCurrent()) rowCounts.value = {};
135
+ if (schemaId.value === id) countsFailed.value = true
149
136
  }
150
137
  },
151
138
  { immediate: true },
152
- );
153
-
154
- const visibleTables = computed(() => {
155
- const needle = tableSearch.value.trim().toLowerCase();
156
- const tables = currentSchema.value?.tables ?? [];
157
- const filtered = needle
158
- ? tables.filter((table) => table.name.toLowerCase().includes(needle))
159
- : tables;
160
- return filtered.map((table) => ({
161
- name: table.name,
162
- count: rowCounts.value[table.name] ?? null,
163
- }));
164
- });
165
-
166
- function isActive(tableName: string): boolean {
167
- const tab = activeTab.value;
168
- return (
169
- tab !== null &&
170
- tab.schema === selectedSchemaId.value &&
171
- tab.instance === selectedInstanceId.value &&
172
- tab.table === tableName
173
- );
139
+ )
140
+
141
+ const openIds = computed(() => new Set(tabs.value.map((tab) => tab.id)))
142
+
143
+ const tables = computed(() => {
144
+ const needle = filter.value.trim().toLowerCase()
145
+ return (schema.value?.tables ?? [])
146
+ .filter((table) => !needle || table.name.toLowerCase().includes(needle))
147
+ .map((table) => ({
148
+ name: table.name,
149
+ count: counts.value[table.name],
150
+ open: openIds.value.has(
151
+ `${schemaId.value}::${instance.value}::${table.name}`,
152
+ ),
153
+ active:
154
+ activeTab.value?.schema === schemaId.value &&
155
+ activeTab.value?.instance === instance.value &&
156
+ activeTab.value?.table === table.name,
157
+ }))
158
+ })
159
+
160
+ // A click opens a preview tab, a double-click (or a modified click) keeps it.
161
+ function open(name: string, event?: MouseEvent) {
162
+ if (!schemaId.value) return
163
+ const keep = Boolean(
164
+ event && (event.metaKey || event.ctrlKey || event.altKey),
165
+ )
166
+ openTab(schemaId.value, instance.value, name, !keep)
167
+ }
168
+
169
+ function keep(name: string) {
170
+ if (schemaId.value) openTab(schemaId.value, instance.value, name, false)
174
171
  }
175
172
 
176
- // Single click opens as a preview; double-click opens permanently (VS Code
177
- // explorer behaviour — the first click already opened the preview, the second
178
- // pins it). A modified click (cmd/ctrl/alt) also opens permanently: it is the
179
- // keyboard path to a pinned open, since Enter can never produce a dblclick.
180
- function open(tableName: string, preview = true) {
181
- if (!selectedSchemaId.value) return;
182
- openTab(selectedSchemaId.value, selectedInstanceId.value, tableName, preview);
173
+ function clearFilter() {
174
+ filter.value = ''
183
175
  }
176
+
177
+ onKeyStroke('t', (event) => {
178
+ const target = event.target as HTMLElement | null
179
+ if (target?.closest('input, textarea, [contenteditable=true]')) return
180
+ if (event.metaKey || event.ctrlKey || event.altKey) return
181
+ event.preventDefault()
182
+ filterInput.value?.inputRef?.focus()
183
+ })
184
184
  </script>
185
185
 
186
186
  <template>
187
- <aside class="flex h-full w-[270px] shrink-0 flex-col border-r border-default bg-default">
188
- <div class="space-y-2 border-b border-default p-3">
189
- <USelect
190
- v-model="selectedSchemaId"
191
- :items="schemaOptions"
192
- :placeholder="$t('dms_database.data.schema')"
193
- icon="i-ph-stack"
194
- size="sm"
187
+ <aside
188
+ class="border-default bg-default flex h-full w-[264px] shrink-0 flex-col border-r"
189
+ >
190
+ <div class="border-default grid gap-2 border-b p-3">
191
+ <DmsEyebrow :label="t('dms_database.data.scope.schema')" />
192
+ <SchemaPicker
193
+ :model-value="schemaId"
194
+ :schema-ids="schemaIds"
195
+ :placeholder="
196
+ schemasPending
197
+ ? t('dms_database.data.scope.loading_schemas')
198
+ : undefined
199
+ "
200
+ :loading="schemasPending"
201
+ :disabled="schemasPending || schemasFailed"
195
202
  class="w-full"
196
- @update:model-value="onUserSchemaChange"
203
+ @update:model-value="pickSchema"
197
204
  />
198
- <USelect
199
- v-model="selectedInstanceId"
200
- :items="instanceOptions"
201
- :disabled="!selectedSchemaId"
202
- icon="i-ph-tree-structure"
203
- size="sm"
205
+ <DmsEyebrow class="mt-1" :label="t('dms_database.data.scope.instance')" />
206
+ <InstancePicker
207
+ :model-value="instanceChoice"
208
+ :schema="schemaId"
209
+ all-mode="readonly"
210
+ :disabled="!schemaId"
204
211
  class="w-full"
205
- @update:model-value="onUserInstanceChange"
212
+ @update:model-value="pickInstance"
206
213
  />
214
+ <DmsEyebrow class="mt-1" :label="t('dms_database.data.scope.search')" />
207
215
  <UInput
208
- v-model="tableSearch"
209
- :placeholder="$t('dms_database.data.findTable')"
210
- :disabled="!selectedSchemaId"
216
+ ref="filterInput"
217
+ v-model="filter"
211
218
  icon="i-ph-magnifying-glass"
212
219
  size="sm"
213
220
  class="w-full"
214
- />
215
- </div>
216
- <nav class="flex-1 overflow-y-auto p-1.5">
217
- <button
218
- v-for="table in visibleTables"
219
- :key="table.name"
220
- type="button"
221
- class="flex w-full select-none items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors"
222
- :class="
223
- isActive(table.name)
224
- ? 'bg-primary/10 text-primary'
225
- : 'text-toned hover:bg-elevated'
226
- "
227
- @click="open(table.name, !($event.metaKey || $event.ctrlKey || $event.altKey))"
228
- @dblclick="open(table.name, false)"
221
+ :placeholder="t('dms_database.data.scope.find_table')"
222
+ :aria-label="t('dms_database.data.scope.find_table')"
223
+ :disabled="!schemaId"
224
+ @keydown.esc="clearFilter"
229
225
  >
230
- <UIcon
231
- name="i-ph-table"
232
- class="size-3.5 shrink-0"
233
- :class="isActive(table.name) ? 'text-primary' : 'text-dimmed'"
226
+ <template #trailing>
227
+ <UButton
228
+ v-if="filter"
229
+ icon="i-ph-x"
230
+ color="neutral"
231
+ variant="link"
232
+ size="xs"
233
+ :aria-label="t('dms_database.common.clear')"
234
+ @click="clearFilter"
235
+ />
236
+ <UKbd v-else value="T" size="sm" />
237
+ </template>
238
+ </UInput>
239
+ </div>
240
+
241
+ <div class="flex min-h-0 flex-1 flex-col p-2">
242
+ <div class="flex items-center justify-between px-2 py-1">
243
+ <DmsEyebrow
244
+ :label="t('dms_database.data.scope.tables', { count: tables.length })"
234
245
  />
235
- <span class="min-w-0 flex-1 truncate font-mono text-xs">{{ table.name }}</span>
236
- <span
237
- v-if="table.count !== null"
238
- class="shrink-0 text-[10px] tabular-nums text-dimmed"
246
+ <DmsEyebrow :label="t('dms_database.data.scope.rows')" />
247
+ </div>
248
+ <nav class="min-h-0 flex-1 overflow-y-auto">
249
+ <div
250
+ v-if="schemasPending"
251
+ class="grid gap-1.5 px-2 py-1"
252
+ role="status"
253
+ :aria-label="t('dms_database.data.scope.loading_schemas')"
239
254
  >
240
- {{ countFormatter.format(table.count) }}
241
- </span>
242
- </button>
243
- <p
244
- v-if="selectedSchemaId && visibleTables.length === 0"
245
- class="px-2 py-4 text-center text-xs text-muted"
246
- >
247
- {{ $t("dms_database.overview.tables.empty") }}
248
- </p>
249
- <p v-else-if="!selectedSchemaId" class="px-2 py-4 text-center text-xs text-muted">
250
- {{ $t("dms_database.data.pickPrompt") }}
251
- </p>
252
- </nav>
255
+ <USkeleton v-for="index in 6" :key="index" class="h-5 w-full" />
256
+ </div>
257
+ <DmsEmptyState
258
+ v-else-if="schemasFailed"
259
+ variant="error"
260
+ size="sm"
261
+ :title="t('dms_database.data.scope.schemas_failed')"
262
+ :actions="[
263
+ {
264
+ label: t('dms_database.common.retry'),
265
+ icon: 'i-ph-arrows-clockwise',
266
+ color: 'neutral',
267
+ variant: 'outline',
268
+ size: 'xs',
269
+ onClick: () => emit('retry'),
270
+ },
271
+ ]"
272
+ />
273
+ <button
274
+ v-for="table in tables"
275
+ :key="table.name"
276
+ type="button"
277
+ class="flex w-full select-none items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors"
278
+ :class="
279
+ table.active
280
+ ? 'bg-primary/10 text-primary'
281
+ : 'text-toned hover:bg-elevated'
282
+ "
283
+ :title="t('dms_database.data.scope.open_hint')"
284
+ @click="open(table.name, $event)"
285
+ @dblclick="keep(table.name)"
286
+ >
287
+ <UIcon
288
+ name="i-ph-table"
289
+ class="size-3.5 shrink-0"
290
+ :class="table.active ? 'text-primary' : 'text-dimmed'"
291
+ />
292
+ <span class="min-w-0 flex-1 truncate font-mono text-xs">
293
+ {{ table.name }}
294
+ </span>
295
+ <span
296
+ v-if="table.open"
297
+ class="size-2 shrink-0 rounded-full border-[1.5px] border-current"
298
+ :class="table.active ? 'text-primary' : 'text-dimmed'"
299
+ :title="t('dms_database.data.scope.open_tab')"
300
+ >
301
+ <span class="sr-only">
302
+ {{ t('dms_database.data.scope.open_tab') }}
303
+ </span>
304
+ </span>
305
+ <span
306
+ v-if="table.count !== undefined"
307
+ class="text-dimmed shrink-0 font-mono text-[10.5px] tabular-nums"
308
+ >
309
+ {{ n(table.count) }}
310
+ </span>
311
+ </button>
312
+ <p
313
+ v-if="schemaId && tables.length === 0"
314
+ class="text-muted px-2 py-4 text-center text-xs"
315
+ >
316
+ {{ t('dms_database.data.scope.no_table') }}
317
+ </p>
318
+ </nav>
319
+ </div>
320
+ <footer
321
+ class="border-default text-dimmed flex items-center gap-1.5 border-t px-3 py-2 text-[11.5px]"
322
+ >
323
+ <UIcon
324
+ :name="countsFailed ? 'i-ph-warning' : 'i-ph-info'"
325
+ class="size-3.5 shrink-0"
326
+ />
327
+ <span v-if="countsFailed">
328
+ {{ t('dms_database.data.scope.counts_failed') }}
329
+ </span>
330
+ <i18n-t v-else keypath="dms_database.data.scope.counts_for" tag="span">
331
+ <template #instance>
332
+ <span class="text-primary font-mono">{{ instanceName }}</span>
333
+ </template>
334
+ </i18n-t>
335
+ </footer>
253
336
  </aside>
254
337
  </template>