@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.
- package/LICENSE +190 -0
- package/README.md +53 -0
- package/dist/automation/index.d.ts +2 -0
- package/dist/automation/index.js +13 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/tableWatch.d.ts +17 -0
- package/dist/automation/tableWatch.js +189 -0
- package/dist/automation/tableWatch.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +19 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/diagram_layouts.model.d.ts +25 -0
- package/dist/db/models/diagram_layouts.model.js +29 -0
- package/dist/db/models/diagram_layouts.model.js.map +1 -0
- package/dist/db/models/diagram_notes.model.d.ts +23 -0
- package/dist/db/models/diagram_notes.model.js +15 -0
- package/dist/db/models/diagram_notes.model.js.map +1 -0
- package/dist/db/models/index.d.ts +4 -0
- package/dist/db/models/index.js +21 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/query_history.model.d.ts +25 -0
- package/dist/db/models/query_history.model.js +33 -0
- package/dist/db/models/query_history.model.js.map +1 -0
- package/dist/db/models/saved_queries.model.d.ts +24 -0
- package/dist/db/models/saved_queries.model.js +21 -0
- package/dist/db/models/saved_queries.model.js.map +1 -0
- package/dist/db/tables/diagram_layouts.table.d.ts +7 -0
- package/dist/db/tables/diagram_layouts.table.js +35 -0
- package/dist/db/tables/diagram_layouts.table.js.map +1 -0
- package/dist/db/tables/diagram_notes.table.d.ts +13 -0
- package/dist/db/tables/diagram_notes.table.js +59 -0
- package/dist/db/tables/diagram_notes.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +4 -0
- package/dist/db/tables/index.js +21 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/query_history.table.d.ts +11 -0
- package/dist/db/tables/query_history.table.js +54 -0
- package/dist/db/tables/query_history.table.js.map +1 -0
- package/dist/db/tables/saved_queries.table.d.ts +12 -0
- package/dist/db/tables/saved_queries.table.js +58 -0
- package/dist/db/tables/saved_queries.table.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/data.d.ts +9 -0
- package/dist/pages/data.js +29 -0
- package/dist/pages/data.js.map +1 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.js +21 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/overview.d.ts +9 -0
- package/dist/pages/overview.js +29 -0
- package/dist/pages/overview.js.map +1 -0
- package/dist/pages/query.d.ts +9 -0
- package/dist/pages/query.js +29 -0
- package/dist/pages/query.js.map +1 -0
- package/dist/pages/schemas.d.ts +9 -0
- package/dist/pages/schemas.js +29 -0
- package/dist/pages/schemas.js.map +1 -0
- package/dist/routes/data.d.ts +25 -0
- package/dist/routes/data.js +324 -0
- package/dist/routes/data.js.map +1 -0
- package/dist/routes/diagram.d.ts +47 -0
- package/dist/routes/diagram.js +200 -0
- package/dist/routes/diagram.js.map +1 -0
- package/dist/routes/health.d.ts +6 -0
- package/dist/routes/health.js +35 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +6 -0
- package/dist/routes/index.js +23 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/query.d.ts +26 -0
- package/dist/routes/query.js +245 -0
- package/dist/routes/query.js.map +1 -0
- package/dist/routes/schemas.d.ts +8 -0
- package/dist/routes/schemas.js +35 -0
- package/dist/routes/schemas.js.map +1 -0
- package/dist/routes/tables.d.ts +23 -0
- package/dist/routes/tables.js +94 -0
- package/dist/routes/tables.js.map +1 -0
- package/dist/service/fieldDescriptor.d.ts +66 -0
- package/dist/service/fieldDescriptor.js +156 -0
- package/dist/service/fieldDescriptor.js.map +1 -0
- package/dist/service/health.d.ts +20 -0
- package/dist/service/health.js +58 -0
- package/dist/service/health.js.map +1 -0
- package/dist/service/introspect.d.ts +6 -0
- package/dist/service/introspect.js +312 -0
- package/dist/service/introspect.js.map +1 -0
- package/dist/service/schemaLabels.d.ts +7 -0
- package/dist/service/schemaLabels.js +39 -0
- package/dist/service/schemaLabels.js.map +1 -0
- package/dist/service/schemaRegistry.d.ts +2 -0
- package/dist/service/schemaRegistry.js +8 -0
- package/dist/service/schemaRegistry.js.map +1 -0
- package/dist/service/stagedSerialization.d.ts +6 -0
- package/dist/service/stagedSerialization.js +54 -0
- package/dist/service/stagedSerialization.js.map +1 -0
- package/dist/service/types.d.ts +67 -0
- package/dist/service/types.js +3 -0
- package/dist/service/types.js.map +1 -0
- package/dist/types/constants.d.ts +3 -0
- package/dist/types/constants.js +7 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/responses.d.ts +14 -0
- package/dist/types/responses.js +3 -0
- package/dist/types/responses.js.map +1 -0
- package/dist/utils/query.d.ts +17 -0
- package/dist/utils/query.js +116 -0
- package/dist/utils/query.js.map +1 -0
- package/dist/utils/requestValidation.d.ts +2 -0
- package/dist/utils/requestValidation.js +14 -0
- package/dist/utils/requestValidation.js.map +1 -0
- package/frontend-vue/.editorconfig +12 -0
- package/frontend-vue/.prettierrc +11 -0
- package/frontend-vue/app/components/DataBrowserSidebar.vue +254 -0
- package/frontend-vue/app/components/DataBrowserTabBar.vue +166 -0
- package/frontend-vue/app/components/DataGrid.vue +735 -0
- package/frontend-vue/app/components/DataGridCell.vue +147 -0
- package/frontend-vue/app/components/DataGridHeaderCell.vue +175 -0
- package/frontend-vue/app/components/DiagramNoteNode.vue +100 -0
- package/frontend-vue/app/components/DiagramRelationEdge.vue +62 -0
- package/frontend-vue/app/components/DiagramStubNode.vue +37 -0
- package/frontend-vue/app/components/DiagramTableNode.vue +94 -0
- package/frontend-vue/app/components/JsonCellPanel.vue +93 -0
- package/frontend-vue/app/components/QueryBuilderPanel.vue +501 -0
- package/frontend-vue/app/components/QueryFavoritesPanel.vue +127 -0
- package/frontend-vue/app/components/QueryHistoryPanel.vue +134 -0
- package/frontend-vue/app/components/QueryResultPanel.vue +204 -0
- package/frontend-vue/app/components/SchemaDiagram.vue +1150 -0
- package/frontend-vue/app/components/SchemaTableInspector.vue +278 -0
- package/frontend-vue/app/composables/useDataBrowserGrid.ts +162 -0
- package/frontend-vue/app/composables/useDataBrowserTabs.ts +255 -0
- package/frontend-vue/app/composables/useDatabaseHealth.ts +50 -0
- package/frontend-vue/app/composables/useDatabaseSchemas.ts +92 -0
- package/frontend-vue/app/composables/useDiagramPersistence.ts +99 -0
- package/frontend-vue/app/composables/useQueryRuntime.ts +118 -0
- package/frontend-vue/app/composables/useQueryStore.ts +117 -0
- package/frontend-vue/app/composables/useTableViewRowClick.ts +22 -0
- package/frontend-vue/app/custom-pages/database/data.vue +42 -0
- package/frontend-vue/app/custom-pages/database/overview.vue +367 -0
- package/frontend-vue/app/custom-pages/database/query.vue +432 -0
- package/frontend-vue/app/custom-pages/database/schemas.vue +216 -0
- package/frontend-vue/app/utils/databaseFieldModifiers.ts +33 -0
- package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +224 -0
- package/frontend-vue/app/utils/dmsTableView.ts +83 -0
- package/frontend-vue/app/utils/relativeTime.ts +15 -0
- package/frontend-vue/app/utils/stagedSerialization.ts +91 -0
- package/frontend-vue/dms.frontend.ts +38 -0
- package/frontend-vue/eslint.config.mjs +16 -0
- package/frontend-vue/i18n/locales/database-en-GB.json +239 -0
- package/frontend-vue/i18n/locales/database-fr-FR.json +239 -0
- package/frontend-vue/package.json +42 -0
- package/frontend-vue/pnpm-lock.yaml +5819 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/frontend-vue/tests/persistence.test.ts +70 -0
- package/frontend-vue/tests/queryRuntime.test.ts +53 -0
- package/frontend-vue/tests/registration.test.ts +33 -0
- package/frontend-vue/tsconfig.json +13 -0
- package/frontend-vue/vitest.config.ts +5 -0
- 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,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,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>
|