@antelopejs/dms-database 0.0.1

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 (164) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +53 -0
  3. package/dist/automation/index.d.ts +2 -0
  4. package/dist/automation/index.js +13 -0
  5. package/dist/automation/index.js.map +1 -0
  6. package/dist/automation/tableWatch.d.ts +17 -0
  7. package/dist/automation/tableWatch.js +189 -0
  8. package/dist/automation/tableWatch.js.map +1 -0
  9. package/dist/config.d.ts +7 -0
  10. package/dist/config.js +12 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/db/index.d.ts +2 -0
  13. package/dist/db/index.js +19 -0
  14. package/dist/db/index.js.map +1 -0
  15. package/dist/db/models/diagram_layouts.model.d.ts +25 -0
  16. package/dist/db/models/diagram_layouts.model.js +29 -0
  17. package/dist/db/models/diagram_layouts.model.js.map +1 -0
  18. package/dist/db/models/diagram_notes.model.d.ts +23 -0
  19. package/dist/db/models/diagram_notes.model.js +15 -0
  20. package/dist/db/models/diagram_notes.model.js.map +1 -0
  21. package/dist/db/models/index.d.ts +4 -0
  22. package/dist/db/models/index.js +21 -0
  23. package/dist/db/models/index.js.map +1 -0
  24. package/dist/db/models/query_history.model.d.ts +25 -0
  25. package/dist/db/models/query_history.model.js +33 -0
  26. package/dist/db/models/query_history.model.js.map +1 -0
  27. package/dist/db/models/saved_queries.model.d.ts +24 -0
  28. package/dist/db/models/saved_queries.model.js +21 -0
  29. package/dist/db/models/saved_queries.model.js.map +1 -0
  30. package/dist/db/tables/diagram_layouts.table.d.ts +7 -0
  31. package/dist/db/tables/diagram_layouts.table.js +35 -0
  32. package/dist/db/tables/diagram_layouts.table.js.map +1 -0
  33. package/dist/db/tables/diagram_notes.table.d.ts +13 -0
  34. package/dist/db/tables/diagram_notes.table.js +59 -0
  35. package/dist/db/tables/diagram_notes.table.js.map +1 -0
  36. package/dist/db/tables/index.d.ts +4 -0
  37. package/dist/db/tables/index.js +21 -0
  38. package/dist/db/tables/index.js.map +1 -0
  39. package/dist/db/tables/query_history.table.d.ts +11 -0
  40. package/dist/db/tables/query_history.table.js +54 -0
  41. package/dist/db/tables/query_history.table.js.map +1 -0
  42. package/dist/db/tables/saved_queries.table.d.ts +12 -0
  43. package/dist/db/tables/saved_queries.table.js +58 -0
  44. package/dist/db/tables/saved_queries.table.js.map +1 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +56 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/pages/data.d.ts +9 -0
  49. package/dist/pages/data.js +29 -0
  50. package/dist/pages/data.js.map +1 -0
  51. package/dist/pages/index.d.ts +4 -0
  52. package/dist/pages/index.js +21 -0
  53. package/dist/pages/index.js.map +1 -0
  54. package/dist/pages/overview.d.ts +9 -0
  55. package/dist/pages/overview.js +29 -0
  56. package/dist/pages/overview.js.map +1 -0
  57. package/dist/pages/query.d.ts +9 -0
  58. package/dist/pages/query.js +29 -0
  59. package/dist/pages/query.js.map +1 -0
  60. package/dist/pages/schemas.d.ts +9 -0
  61. package/dist/pages/schemas.js +29 -0
  62. package/dist/pages/schemas.js.map +1 -0
  63. package/dist/routes/data.d.ts +25 -0
  64. package/dist/routes/data.js +324 -0
  65. package/dist/routes/data.js.map +1 -0
  66. package/dist/routes/diagram.d.ts +47 -0
  67. package/dist/routes/diagram.js +200 -0
  68. package/dist/routes/diagram.js.map +1 -0
  69. package/dist/routes/health.d.ts +6 -0
  70. package/dist/routes/health.js +35 -0
  71. package/dist/routes/health.js.map +1 -0
  72. package/dist/routes/index.d.ts +6 -0
  73. package/dist/routes/index.js +23 -0
  74. package/dist/routes/index.js.map +1 -0
  75. package/dist/routes/query.d.ts +26 -0
  76. package/dist/routes/query.js +245 -0
  77. package/dist/routes/query.js.map +1 -0
  78. package/dist/routes/schemas.d.ts +8 -0
  79. package/dist/routes/schemas.js +35 -0
  80. package/dist/routes/schemas.js.map +1 -0
  81. package/dist/routes/tables.d.ts +23 -0
  82. package/dist/routes/tables.js +94 -0
  83. package/dist/routes/tables.js.map +1 -0
  84. package/dist/service/fieldDescriptor.d.ts +66 -0
  85. package/dist/service/fieldDescriptor.js +156 -0
  86. package/dist/service/fieldDescriptor.js.map +1 -0
  87. package/dist/service/health.d.ts +20 -0
  88. package/dist/service/health.js +58 -0
  89. package/dist/service/health.js.map +1 -0
  90. package/dist/service/introspect.d.ts +6 -0
  91. package/dist/service/introspect.js +312 -0
  92. package/dist/service/introspect.js.map +1 -0
  93. package/dist/service/schemaLabels.d.ts +7 -0
  94. package/dist/service/schemaLabels.js +39 -0
  95. package/dist/service/schemaLabels.js.map +1 -0
  96. package/dist/service/schemaRegistry.d.ts +2 -0
  97. package/dist/service/schemaRegistry.js +8 -0
  98. package/dist/service/schemaRegistry.js.map +1 -0
  99. package/dist/service/stagedSerialization.d.ts +6 -0
  100. package/dist/service/stagedSerialization.js +54 -0
  101. package/dist/service/stagedSerialization.js.map +1 -0
  102. package/dist/service/types.d.ts +67 -0
  103. package/dist/service/types.js +3 -0
  104. package/dist/service/types.js.map +1 -0
  105. package/dist/types/constants.d.ts +3 -0
  106. package/dist/types/constants.js +7 -0
  107. package/dist/types/constants.js.map +1 -0
  108. package/dist/types/responses.d.ts +14 -0
  109. package/dist/types/responses.js +3 -0
  110. package/dist/types/responses.js.map +1 -0
  111. package/dist/utils/query.d.ts +17 -0
  112. package/dist/utils/query.js +116 -0
  113. package/dist/utils/query.js.map +1 -0
  114. package/dist/utils/requestValidation.d.ts +2 -0
  115. package/dist/utils/requestValidation.js +14 -0
  116. package/dist/utils/requestValidation.js.map +1 -0
  117. package/frontend-vue/.editorconfig +12 -0
  118. package/frontend-vue/.prettierrc +11 -0
  119. package/frontend-vue/app/components/DataBrowserSidebar.vue +254 -0
  120. package/frontend-vue/app/components/DataBrowserTabBar.vue +166 -0
  121. package/frontend-vue/app/components/DataGrid.vue +735 -0
  122. package/frontend-vue/app/components/DataGridCell.vue +147 -0
  123. package/frontend-vue/app/components/DataGridHeaderCell.vue +175 -0
  124. package/frontend-vue/app/components/DiagramNoteNode.vue +100 -0
  125. package/frontend-vue/app/components/DiagramRelationEdge.vue +62 -0
  126. package/frontend-vue/app/components/DiagramStubNode.vue +37 -0
  127. package/frontend-vue/app/components/DiagramTableNode.vue +94 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +93 -0
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +501 -0
  130. package/frontend-vue/app/components/QueryFavoritesPanel.vue +127 -0
  131. package/frontend-vue/app/components/QueryHistoryPanel.vue +134 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +204 -0
  133. package/frontend-vue/app/components/SchemaDiagram.vue +1150 -0
  134. package/frontend-vue/app/components/SchemaTableInspector.vue +278 -0
  135. package/frontend-vue/app/composables/useDataBrowserGrid.ts +162 -0
  136. package/frontend-vue/app/composables/useDataBrowserTabs.ts +255 -0
  137. package/frontend-vue/app/composables/useDatabaseHealth.ts +50 -0
  138. package/frontend-vue/app/composables/useDatabaseSchemas.ts +92 -0
  139. package/frontend-vue/app/composables/useDiagramPersistence.ts +99 -0
  140. package/frontend-vue/app/composables/useQueryRuntime.ts +118 -0
  141. package/frontend-vue/app/composables/useQueryStore.ts +117 -0
  142. package/frontend-vue/app/composables/useTableViewRowClick.ts +22 -0
  143. package/frontend-vue/app/custom-pages/database/data.vue +42 -0
  144. package/frontend-vue/app/custom-pages/database/overview.vue +367 -0
  145. package/frontend-vue/app/custom-pages/database/query.vue +432 -0
  146. package/frontend-vue/app/custom-pages/database/schemas.vue +216 -0
  147. package/frontend-vue/app/utils/databaseFieldModifiers.ts +33 -0
  148. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +224 -0
  149. package/frontend-vue/app/utils/dmsTableView.ts +83 -0
  150. package/frontend-vue/app/utils/relativeTime.ts +15 -0
  151. package/frontend-vue/app/utils/stagedSerialization.ts +91 -0
  152. package/frontend-vue/dms.frontend.ts +38 -0
  153. package/frontend-vue/eslint.config.mjs +16 -0
  154. package/frontend-vue/i18n/locales/database-en-GB.json +239 -0
  155. package/frontend-vue/i18n/locales/database-fr-FR.json +239 -0
  156. package/frontend-vue/package.json +42 -0
  157. package/frontend-vue/pnpm-lock.yaml +5819 -0
  158. package/frontend-vue/pnpm-workspace.yaml +2 -0
  159. package/frontend-vue/tests/persistence.test.ts +70 -0
  160. package/frontend-vue/tests/queryRuntime.test.ts +53 -0
  161. package/frontend-vue/tests/registration.test.ts +33 -0
  162. package/frontend-vue/tsconfig.json +13 -0
  163. package/frontend-vue/vitest.config.ts +5 -0
  164. package/package.json +78 -0
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.asNonEmptyString = asNonEmptyString;
4
+ exports.parseInteger = parseInteger;
5
+ exports.clamp = clamp;
6
+ exports.parseFilter = parseFilter;
7
+ exports.unwrapIsFilter = unwrapIsFilter;
8
+ exports.decodeInstanceFilter = decodeInstanceFilter;
9
+ exports.applyFilterToList = applyFilterToList;
10
+ exports.applySearchToList = applySearchToList;
11
+ exports.cellToText = cellToText;
12
+ exports.applySortToList = applySortToList;
13
+ exports.normalizeSortDirection = normalizeSortDirection;
14
+ const interface_database_1 = require("@antelopejs/interface-database");
15
+ const constants_1 = require("../types/constants");
16
+ const FILTER_MODE_SEPARATOR = ":";
17
+ const FILTER_MODE_IS = "is";
18
+ const FILTER_MODE_CONTAINS = "contains";
19
+ function asNonEmptyString(value) {
20
+ if (typeof value !== "string")
21
+ return undefined;
22
+ if (value.length === 0)
23
+ return undefined;
24
+ return value;
25
+ }
26
+ function parseInteger(value, fallback) {
27
+ const text = asNonEmptyString(value);
28
+ if (!text)
29
+ return fallback;
30
+ const parsed = Number.parseInt(text, 10);
31
+ return Number.isFinite(parsed) ? parsed : fallback;
32
+ }
33
+ function clamp(value, min, max) {
34
+ return Math.min(max, Math.max(min, value));
35
+ }
36
+ function parseFilter(raw) {
37
+ const text = asNonEmptyString(raw);
38
+ if (!text)
39
+ return undefined;
40
+ const idx = text.indexOf(FILTER_MODE_SEPARATOR);
41
+ if (idx < 0)
42
+ return { mode: FILTER_MODE_IS, value: text };
43
+ return { mode: text.slice(0, idx), value: text.slice(idx + 1) };
44
+ }
45
+ function unwrapIsFilter(raw) {
46
+ const parsed = parseFilter(raw);
47
+ if (!parsed || parsed.mode !== FILTER_MODE_IS)
48
+ return undefined;
49
+ return parsed.value;
50
+ }
51
+ function decodeInstanceFilter(raw) {
52
+ const value = unwrapIsFilter(raw);
53
+ if (value === undefined || value === null || value === "")
54
+ return undefined;
55
+ if (value === constants_1.CROSS_INSTANCE_SENTINEL)
56
+ return interface_database_1.CROSS_INSTANCE;
57
+ return value;
58
+ }
59
+ const FILTER_PREDICATES = {
60
+ [FILTER_MODE_IS]: (cell, value) => cell === value,
61
+ [FILTER_MODE_CONTAINS]: (cell, value) => cell.toLowerCase().includes(value.toLowerCase()),
62
+ };
63
+ function applyFilterToList(rows, field, rawFilter) {
64
+ const parsed = parseFilter(rawFilter);
65
+ if (!parsed)
66
+ return rows;
67
+ const predicate = FILTER_PREDICATES[parsed.mode];
68
+ if (!predicate)
69
+ return rows;
70
+ return rows.filter((row) => predicate(String(row[field] ?? ""), parsed.value));
71
+ }
72
+ function applySearchToList(rows, field, search) {
73
+ const needle = asNonEmptyString(search)?.toLowerCase();
74
+ if (!needle)
75
+ return rows;
76
+ return rows.filter((row) => cellToText(row[field]).toLowerCase().includes(needle));
77
+ }
78
+ function cellToText(value) {
79
+ if (value === null || value === undefined)
80
+ return "";
81
+ if (typeof value === "string")
82
+ return value;
83
+ if (typeof value === "number" || typeof value === "boolean") {
84
+ return value.toString();
85
+ }
86
+ try {
87
+ const json = JSON.stringify(value);
88
+ if (json !== undefined)
89
+ return json;
90
+ }
91
+ catch {
92
+ }
93
+ try {
94
+ return String(value);
95
+ }
96
+ catch {
97
+ return "";
98
+ }
99
+ }
100
+ function compareValues(a, b) {
101
+ if (typeof a === "number" && typeof b === "number")
102
+ return a - b;
103
+ return cellToText(a).localeCompare(cellToText(b));
104
+ }
105
+ function applySortToList(rows, sortKey, sortDirection) {
106
+ const key = asNonEmptyString(sortKey);
107
+ if (!key)
108
+ return rows;
109
+ const direction = asNonEmptyString(sortDirection) === "desc" ? -1 : 1;
110
+ return [...rows].sort((a, b) => compareValues(a[key], b[key]) * direction);
111
+ }
112
+ const SORT_DIRECTION_DESC = "desc";
113
+ function normalizeSortDirection(value) {
114
+ return asNonEmptyString(value) === SORT_DIRECTION_DESC ? "desc" : "asc";
115
+ }
116
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/utils/query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function asString(value: unknown): string | undefined;
2
+ export declare function asNonEmptyString(value: unknown): string;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.asString = asString;
4
+ exports.asNonEmptyString = asNonEmptyString;
5
+ const interface_api_util_1 = require("@antelopejs/interface-api-util");
6
+ function asString(value) {
7
+ return typeof value === "string" ? value : undefined;
8
+ }
9
+ function asNonEmptyString(value) {
10
+ const str = asString(value);
11
+ (0, interface_api_util_1.assert)(str !== undefined && str.length > 0, 400, "Expected non-empty string");
12
+ return str;
13
+ }
14
+ //# sourceMappingURL=requestValidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestValidation.js","sourceRoot":"","sources":["../../src/utils/requestValidation.ts"],"names":[],"mappings":";;AAMA,4BAEC;AAED,4CAIC;AAdD,uEAAwD;AAMxD,SAAgB,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED,SAAgB,gBAAgB,CAAC,KAAc;IAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAA,2BAAM,EAAC,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,GAAG,EAAE,2BAA2B,CAAC,CAAC;IAC9E,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_size = 2
5
+ indent_style = space
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
@@ -0,0 +1,11 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "endOfLine": "auto",
6
+ "plugins": ["prettier-plugin-tailwindcss"],
7
+ "useTabs": true,
8
+
9
+ "htmlWhitespaceSensitivity": "ignore",
10
+ "tailwindStylesheet": "~/assets/css/main.css"
11
+ }
@@ -0,0 +1,254 @@
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;
14
+ }
15
+
16
+ const props = defineProps<{ schemas: SchemaSummary[] }>();
17
+
18
+ const { t } = useI18n();
19
+ const { $authFetch } = useAuthFetch();
20
+ const { activeTab, openTab } = useDataBrowserTabs();
21
+ const route = useDmsRoute();
22
+
23
+ const selectedSchemaId = ref<string | null>(null);
24
+ const selectedInstanceId = ref<string>(DEFAULT_INSTANCE_VALUE);
25
+ const tableSearch = ref("");
26
+
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
32
+ ? 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");
48
+
49
+ 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";
56
+ },
57
+ { immediate: true },
58
+ );
59
+ watch(
60
+ () => 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";
77
+ },
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;
87
+ }
88
+ function onUserInstanceChange() {
89
+ selectionSource.value = "user";
90
+ }
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();
117
+
118
+ 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;
135
+ 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;
145
+ } 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 = {};
149
+ }
150
+ },
151
+ { 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
+ );
174
+ }
175
+
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);
183
+ }
184
+ </script>
185
+
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"
195
+ class="w-full"
196
+ @update:model-value="onUserSchemaChange"
197
+ />
198
+ <USelect
199
+ v-model="selectedInstanceId"
200
+ :items="instanceOptions"
201
+ :disabled="!selectedSchemaId"
202
+ icon="i-ph-tree-structure"
203
+ size="sm"
204
+ class="w-full"
205
+ @update:model-value="onUserInstanceChange"
206
+ />
207
+ <UInput
208
+ v-model="tableSearch"
209
+ :placeholder="$t('dms_database.data.findTable')"
210
+ :disabled="!selectedSchemaId"
211
+ icon="i-ph-magnifying-glass"
212
+ size="sm"
213
+ 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)"
229
+ >
230
+ <UIcon
231
+ name="i-ph-table"
232
+ class="size-3.5 shrink-0"
233
+ :class="isActive(table.name) ? 'text-primary' : 'text-dimmed'"
234
+ />
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"
239
+ >
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>
253
+ </aside>
254
+ </template>
@@ -0,0 +1,166 @@
1
+ <script setup lang="ts">
2
+ import type { BrowserTab } from "../composables/useDataBrowserTabs";
3
+
4
+ const {
5
+ tabs,
6
+ activeId,
7
+ hasMultipleSchemas,
8
+ activateTab,
9
+ closeTab,
10
+ pinTab,
11
+ moveTab,
12
+ } = useDataBrowserTabs();
13
+ const { t } = useI18n();
14
+
15
+ // Shared by the pin and close buttons — they are a hover-reveal pair on the
16
+ // same tab and must render identically.
17
+ const tabActionClass =
18
+ "rounded p-0.5 text-dimmed opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 hover:bg-elevated hover:text-default";
19
+
20
+ // The label doubles as the accessible name; a preview tab announces its
21
+ // ephemeral state, which the italic styling alone cannot convey.
22
+ function tabAriaLabel(tab: BrowserTab): string {
23
+ return tab.preview
24
+ ? t("dms_database.data.previewTab", { name: label(tab) })
25
+ : label(tab);
26
+ }
27
+
28
+ // Pinning from the pin button unmounts the button under focus (v-if), which
29
+ // would drop focus to <body> for keyboard users; re-anchor it on the tab's
30
+ // label button — but only when focus actually fell to the body, so a mouse
31
+ // click doesn't get focus stolen elsewhere.
32
+ function pinFromButton(tab: BrowserTab, event: MouseEvent) {
33
+ const tabEl = (event.currentTarget as HTMLElement).closest("[data-tab]");
34
+ pinTab(tab.id);
35
+ nextTick(() => {
36
+ if (document.activeElement === document.body) {
37
+ tabEl?.querySelector("button")?.focus();
38
+ }
39
+ });
40
+ }
41
+
42
+ // --- drag & drop reorder ---
43
+ const draggedId = ref<string | null>(null);
44
+ const dropTargetId = ref<string | null>(null);
45
+
46
+ function onDragStart(tab: BrowserTab, event: DragEvent) {
47
+ draggedId.value = tab.id;
48
+ // Firefox refuses to start a drag without data attached.
49
+ event.dataTransfer?.setData("text/plain", tab.id);
50
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
51
+ }
52
+
53
+ function onDragOver(tab: BrowserTab, event: DragEvent) {
54
+ if (!draggedId.value || draggedId.value === tab.id) return;
55
+ // preventDefault marks the tab as a valid drop target.
56
+ event.preventDefault();
57
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
58
+ dropTargetId.value = tab.id;
59
+ }
60
+
61
+ function onDrop(tab: BrowserTab) {
62
+ // A self-drop is not a reorder: no pin, no move. (onDragOver never marks
63
+ // the dragged tab as a valid target, so this is belt-and-braces.)
64
+ if (draggedId.value && draggedId.value !== tab.id) {
65
+ // An arranged tab is kept (VS Code): a completed drag-reorder pins.
66
+ pinTab(draggedId.value);
67
+ moveTab(draggedId.value, tab.id);
68
+ }
69
+ onDragEnd();
70
+ }
71
+
72
+ function onDragEnd() {
73
+ draggedId.value = null;
74
+ dropTargetId.value = null;
75
+ }
76
+
77
+ function label(tab: BrowserTab): string {
78
+ return hasMultipleSchemas.value ? `${tab.schema}.${tab.table}` : tab.table;
79
+ }
80
+
81
+ function instanceBadge(tab: BrowserTab): string | null {
82
+ if (tab.instance === DEFAULT_INSTANCE_VALUE) return null;
83
+ if (tab.instance === CROSS_INSTANCE_VALUE) return "*";
84
+ return `@${tab.instance}`;
85
+ }
86
+
87
+ function close(tab: BrowserTab) {
88
+ closeTab(tab.id);
89
+ }
90
+ </script>
91
+
92
+ <template>
93
+ <div
94
+ v-if="tabs.length > 0"
95
+ class="flex items-end gap-1 overflow-x-auto border-b border-default bg-elevated/50 px-2 pt-1.5"
96
+ >
97
+ <div
98
+ v-for="tab in tabs"
99
+ :key="tab.id"
100
+ data-tab
101
+ draggable="true"
102
+ class="group flex shrink-0 select-none items-center gap-1.5 rounded-t-md border border-b-0 px-2.5 py-1.5"
103
+ :class="[
104
+ tab.id === activeId
105
+ ? 'border-default bg-default'
106
+ : 'border-transparent bg-transparent hover:bg-default/60',
107
+ tab.id === draggedId ? 'opacity-40' : '',
108
+ tab.id === dropTargetId ? 'ring-1 ring-primary ring-inset' : '',
109
+ ]"
110
+ @dragstart="onDragStart(tab, $event)"
111
+ @dragover="onDragOver(tab, $event)"
112
+ @drop.prevent="onDrop(tab)"
113
+ @dragend="onDragEnd"
114
+ >
115
+ <button
116
+ type="button"
117
+ class="flex items-center gap-1.5"
118
+ :aria-label="tabAriaLabel(tab)"
119
+ @click="activateTab(tab.id)"
120
+ @dblclick="pinTab(tab.id)"
121
+ >
122
+ <UIcon
123
+ name="i-ph-table"
124
+ class="size-3.5"
125
+ :class="tab.id === activeId ? 'text-primary' : 'text-dimmed'"
126
+ />
127
+ <span
128
+ class="max-w-48 truncate font-mono text-xs"
129
+ :class="[
130
+ tab.id === activeId ? 'text-highlighted' : 'text-muted',
131
+ tab.preview ? 'italic' : '',
132
+ ]"
133
+ >
134
+ {{ label(tab) }}
135
+ </span>
136
+ <span
137
+ v-if="instanceBadge(tab)"
138
+ class="rounded bg-primary/10 px-1 font-mono text-[10px] text-primary"
139
+ >
140
+ {{ instanceBadge(tab) }}
141
+ </span>
142
+ </button>
143
+ <button
144
+ type="button"
145
+ :class="[tabActionClass, { 'opacity-100': tab.id === activeId }]"
146
+ :aria-label="$t('dms_database.data.closeTab')"
147
+ @click.stop="close(tab)"
148
+ >
149
+ <UIcon name="i-ph-x" class="size-3" />
150
+ </button>
151
+ <!-- The pin button sits AFTER the close button: pinning unmounts it, and
152
+ nothing may reflow into the pointer's position — a double-click's
153
+ second click would otherwise land on the close button and destroy
154
+ the tab the user just pinned. -->
155
+ <button
156
+ v-if="tab.preview"
157
+ type="button"
158
+ :class="[tabActionClass, { 'opacity-100': tab.id === activeId }]"
159
+ :aria-label="$t('dms_database.data.keepTab')"
160
+ @click.stop="pinFromButton(tab, $event)"
161
+ >
162
+ <UIcon name="i-ph-push-pin" class="size-3" />
163
+ </button>
164
+ </div>
165
+ </div>
166
+ </template>