@antelopejs/dms-database 0.0.14 → 0.1.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/README.md +30 -5
- package/dist/db/models/query_history.model.d.ts +1 -0
- package/dist/db/models/query_history.model.js +6 -0
- package/dist/db/models/query_history.model.js.map +1 -1
- package/dist/db/tables/query_history.table.d.ts +3 -0
- package/dist/db/tables/query_history.table.js +12 -0
- package/dist/db/tables/query_history.table.js.map +1 -1
- package/dist/db/tables/saved_queries.table.d.ts +1 -0
- package/dist/db/tables/saved_queries.table.js +4 -0
- package/dist/db/tables/saved_queries.table.js.map +1 -1
- package/dist/i18n/messages.d.ts +39 -0
- package/dist/i18n/messages.js +64 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -7
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +11 -0
- package/dist/module.js +55 -0
- package/dist/module.js.map +1 -0
- package/dist/pages/data.d.ts +1 -1
- package/dist/pages/data.js +9 -6
- package/dist/pages/data.js.map +1 -1
- package/dist/pages/diagram.d.ts +9 -0
- package/dist/pages/diagram.js +45 -0
- package/dist/pages/diagram.js.map +1 -0
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/overview.d.ts +4 -1
- package/dist/pages/overview.js +113 -6
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/query.d.ts +1 -1
- package/dist/pages/query.js +22 -6
- package/dist/pages/query.js.map +1 -1
- package/dist/pages/schemas.d.ts +2 -1
- package/dist/pages/schemas.js +143 -6
- package/dist/pages/schemas.js.map +1 -1
- package/dist/routes/data.d.ts +8 -0
- package/dist/routes/data.js +84 -21
- package/dist/routes/data.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/overview.d.ts +12 -0
- package/dist/routes/overview.js +55 -0
- package/dist/routes/overview.js.map +1 -0
- package/dist/routes/query.d.ts +5 -1
- package/dist/routes/query.js +162 -27
- package/dist/routes/query.js.map +1 -1
- package/dist/routes/tables.d.ts +15 -12
- package/dist/routes/tables.js +95 -55
- package/dist/routes/tables.js.map +1 -1
- package/dist/service/catalog.d.ts +3 -0
- package/dist/service/catalog.js +26 -0
- package/dist/service/catalog.js.map +1 -0
- package/dist/service/health.d.ts +3 -3
- package/dist/service/health.js +17 -14
- package/dist/service/health.js.map +1 -1
- package/dist/service/introspect.d.ts +3 -1
- package/dist/service/introspect.js +131 -43
- package/dist/service/introspect.js.map +1 -1
- package/dist/service/overview.d.ts +9 -0
- package/dist/service/overview.js +113 -0
- package/dist/service/overview.js.map +1 -0
- package/dist/service/queryInspection.d.ts +15 -0
- package/dist/service/queryInspection.js +107 -0
- package/dist/service/queryInspection.js.map +1 -0
- package/dist/service/references.d.ts +9 -0
- package/dist/service/references.js +52 -0
- package/dist/service/references.js.map +1 -0
- package/dist/service/schemaFilters.d.ts +26 -0
- package/dist/service/schemaFilters.js +42 -0
- package/dist/service/schemaFilters.js.map +1 -0
- package/dist/service/stagedSerialization.d.ts +2 -0
- package/dist/service/stagedSerialization.js +38 -10
- package/dist/service/stagedSerialization.js.map +1 -1
- package/dist/service/types.d.ts +11 -1
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/constants.js +3 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/utils/query.d.ts +0 -2
- package/dist/utils/query.js +0 -20
- package/dist/utils/query.js.map +1 -1
- package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
- package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
- package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
- package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
- package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
- package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
- package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
- package/frontend-vue/app/build/data/cellValues.ts +132 -0
- package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
- package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
- package/frontend-vue/app/build/data/recentTables.ts +48 -0
- package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
- package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
- package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
- package/frontend-vue/app/build/diagram/autosave.ts +72 -0
- package/frontend-vue/app/build/diagram/graph.ts +585 -0
- package/frontend-vue/app/build/diagram/history.ts +178 -0
- package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
- package/frontend-vue/app/build/diagram/sync.ts +137 -0
- package/frontend-vue/app/build/query/completion.ts +100 -0
- package/frontend-vue/app/build/query/csv.ts +32 -0
- package/frontend-vue/app/build/query/examples.ts +38 -0
- package/frontend-vue/app/build/query/runs.ts +168 -0
- package/frontend-vue/app/build/query/tableColumns.ts +35 -0
- package/frontend-vue/app/build/query/target.ts +143 -0
- package/frontend-vue/app/build/schemas/filterState.ts +128 -0
- package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
- package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
- package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
- package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
- package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
- package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
- package/frontend-vue/app/components/DataBrowser.vue +126 -0
- package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
- package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
- package/frontend-vue/app/components/DataFilterBar.vue +163 -0
- package/frontend-vue/app/components/DataGrid.vue +737 -499
- package/frontend-vue/app/components/DataGridCell.vue +125 -77
- package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
- package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
- package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
- package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
- package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
- package/frontend-vue/app/components/InstancePicker.vue +195 -0
- package/frontend-vue/app/components/JsonCellPanel.vue +55 -41
- package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
- package/frontend-vue/app/components/QueryConsole.vue +726 -0
- package/frontend-vue/app/components/QueryLibrary.vue +314 -0
- package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
- package/frontend-vue/app/components/RowDrawer.vue +520 -0
- package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
- package/frontend-vue/app/components/SchemaPicker.vue +86 -0
- package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
- package/frontend-vue/app/components/TableInspector.vue +741 -0
- package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
- package/frontend-vue/dms.frontend.ts +5 -14
- package/frontend-vue/i18n/locales/database-en-GB.json +527 -193
- package/frontend-vue/i18n/locales/database-fr-FR.json +529 -195
- package/frontend-vue/package.json +2 -1
- package/frontend-vue/pnpm-lock.yaml +2601 -858
- package/frontend-vue/pnpm-workspace.yaml +1 -1
- package/frontend-vue/tests/dataBrowser.test.ts +206 -0
- package/frontend-vue/tests/diagram.test.ts +516 -0
- package/frontend-vue/tests/helpers.test.ts +205 -0
- package/frontend-vue/tests/instanceOptions.test.ts +53 -0
- package/frontend-vue/tests/locales.test.ts +56 -0
- package/frontend-vue/tests/persistence.test.ts +106 -5
- package/frontend-vue/tests/pickerMenus.test.ts +254 -0
- package/frontend-vue/tests/queryConsole.test.ts +282 -0
- package/frontend-vue/tests/queryRuntime.test.ts +2 -2
- package/frontend-vue/tests/registration.test.ts +36 -25
- package/frontend-vue/tests/schemasFilters.test.ts +115 -0
- package/package.json +7 -7
- package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
- package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
- package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
- package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
- package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
- package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
- package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
- package/frontend-vue/app/composables/useQueryStore.ts +0 -117
- package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
- package/frontend-vue/app/custom-pages/database/data.vue +0 -41
- package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
- package/frontend-vue/app/custom-pages/database/query.vue +0 -436
- package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
- package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
- package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
- package/frontend-vue/app/utils/dmsTableView.ts +0 -83
- package/frontend-vue/app/utils/relativeTime.ts +0 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/types/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,WAAW,GAAG,cAAc,CAAC;AAE7B,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAE3B,QAAA,uBAAuB,GAAG,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/types/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,WAAW,GAAG,cAAc,CAAC;AAE7B,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAE3B,QAAA,uBAAuB,GAAG,oBAAoB,CAAC;AAC/C,QAAA,mBAAmB,GAAG,CAAC,CAAC;AAGxB,QAAA,aAAa,GAAG,GAAG,CAAC"}
|
package/dist/utils/query.d.ts
CHANGED
|
@@ -10,8 +10,6 @@ export declare function parseFilter(raw: unknown): ParsedFilter | undefined;
|
|
|
10
10
|
export declare function unwrapIsFilter(raw: unknown): string | undefined;
|
|
11
11
|
export declare function decodeInstanceFilter(raw: unknown): InstanceId | undefined;
|
|
12
12
|
export declare function applyFilterToList<T>(rows: T[], field: keyof T, rawFilter: unknown): T[];
|
|
13
|
-
export declare function applySearchToList<T>(rows: T[], field: keyof T, search: unknown): T[];
|
|
14
13
|
export declare function cellToText(value: unknown): string;
|
|
15
|
-
export declare function applySortToList<T>(rows: T[], sortKey: unknown, sortDirection: unknown): T[];
|
|
16
14
|
export type SortDirection = "asc" | "desc";
|
|
17
15
|
export declare function normalizeSortDirection(value: unknown): SortDirection;
|
package/dist/utils/query.js
CHANGED
|
@@ -7,9 +7,7 @@ exports.parseFilter = parseFilter;
|
|
|
7
7
|
exports.unwrapIsFilter = unwrapIsFilter;
|
|
8
8
|
exports.decodeInstanceFilter = decodeInstanceFilter;
|
|
9
9
|
exports.applyFilterToList = applyFilterToList;
|
|
10
|
-
exports.applySearchToList = applySearchToList;
|
|
11
10
|
exports.cellToText = cellToText;
|
|
12
|
-
exports.applySortToList = applySortToList;
|
|
13
11
|
exports.normalizeSortDirection = normalizeSortDirection;
|
|
14
12
|
const interface_database_1 = require("@antelopejs/interface-database");
|
|
15
13
|
const constants_1 = require("../types/constants");
|
|
@@ -69,12 +67,6 @@ function applyFilterToList(rows, field, rawFilter) {
|
|
|
69
67
|
return rows;
|
|
70
68
|
return rows.filter((row) => predicate(String(row[field] ?? ""), parsed.value));
|
|
71
69
|
}
|
|
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
70
|
function cellToText(value) {
|
|
79
71
|
if (value === null || value === undefined)
|
|
80
72
|
return "";
|
|
@@ -97,18 +89,6 @@ function cellToText(value) {
|
|
|
97
89
|
return "";
|
|
98
90
|
}
|
|
99
91
|
}
|
|
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
92
|
const SORT_DIRECTION_DESC = "desc";
|
|
113
93
|
function normalizeSortDirection(value) {
|
|
114
94
|
return asNonEmptyString(value) === SORT_DIRECTION_DESC ? "desc" : "asc";
|
package/dist/utils/query.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/utils/query.ts"],"names":[],"mappings":";;AAeA,4CAIC;AAED,oCAKC;AAED,sBAEC;AAED,kCAMC;AAED,wCAIC;AAKD,oDAKC;AAWD,8CAYC;
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/utils/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,88 +1,103 @@
|
|
|
1
|
+
import { type BrowserTab, DEFAULT_INSTANCE_VALUE } from './useDataBrowserTabs'
|
|
1
2
|
// Per-tab grid state (paging, sort, filters, column widths) and a data cache
|
|
2
3
|
// so switching tabs restores instantly without a refetch. Keyed by
|
|
3
4
|
// BrowserTab.id.
|
|
4
5
|
|
|
5
6
|
export interface ColumnFilterState {
|
|
6
|
-
mode:
|
|
7
|
-
value: string
|
|
7
|
+
mode: 'is' | 'contains'
|
|
8
|
+
value: string
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
page: number
|
|
12
|
-
pageSize: number
|
|
13
|
-
sortKey: string | null
|
|
14
|
-
sortDirection:
|
|
15
|
-
search: string
|
|
16
|
-
filters: Record<string, ColumnFilterState
|
|
17
|
-
columnWidths: Record<string, number
|
|
11
|
+
interface GridState {
|
|
12
|
+
page: number
|
|
13
|
+
pageSize: number
|
|
14
|
+
sortKey: string | null
|
|
15
|
+
sortDirection: 'asc' | 'desc'
|
|
16
|
+
search: string
|
|
17
|
+
filters: Record<string, ColumnFilterState>
|
|
18
|
+
columnWidths: Record<string, number>
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface GridColumn {
|
|
21
|
-
name: string
|
|
22
|
+
name: string
|
|
22
23
|
// Coarse runtime type sampled by the backend: string | number | boolean |
|
|
23
24
|
// object | array | null | undefined.
|
|
24
|
-
type: string
|
|
25
|
-
isPrimaryKey: boolean
|
|
25
|
+
type: string
|
|
26
|
+
isPrimaryKey: boolean
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface GridData {
|
|
29
|
-
columns: GridColumn[]
|
|
30
|
-
rows: Record<string, unknown>[]
|
|
31
|
-
total: number
|
|
32
|
-
primaryKey: string | null
|
|
30
|
+
columns: GridColumn[]
|
|
31
|
+
rows: Record<string, unknown>[]
|
|
32
|
+
total: number
|
|
33
|
+
primaryKey: string | null
|
|
33
34
|
// Serialised query parameters that produced this payload; lets the grid
|
|
34
35
|
// skip refetching when nothing changed.
|
|
35
|
-
paramsKey: string
|
|
36
|
+
paramsKey: string
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export const GRID_PAGE_SIZES = [25, 50, 100, 200]
|
|
39
|
+
export const GRID_PAGE_SIZES = [25, 50, 100, 200]
|
|
39
40
|
|
|
40
41
|
function defaultGridState(): GridState {
|
|
41
42
|
return {
|
|
42
43
|
page: 0,
|
|
43
|
-
pageSize:
|
|
44
|
+
pageSize: 50,
|
|
44
45
|
sortKey: null,
|
|
45
|
-
sortDirection:
|
|
46
|
-
search:
|
|
46
|
+
sortDirection: 'asc',
|
|
47
|
+
search: '',
|
|
47
48
|
filters: {},
|
|
48
49
|
columnWidths: {},
|
|
49
|
-
}
|
|
50
|
+
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export function useDataBrowserGrid() {
|
|
53
54
|
const states = useDmsState<Record<string, GridState>>(
|
|
54
|
-
|
|
55
|
+
'dms-database-grid-states',
|
|
55
56
|
() => ({}),
|
|
56
|
-
)
|
|
57
|
+
)
|
|
57
58
|
const cache = useDmsState<Record<string, GridData>>(
|
|
58
|
-
|
|
59
|
+
'dms-database-grid-cache',
|
|
59
60
|
() => ({}),
|
|
60
|
-
)
|
|
61
|
+
)
|
|
61
62
|
|
|
62
63
|
function getState(tabId: string): GridState {
|
|
63
|
-
if (!states.value[tabId]) states.value[tabId] = defaultGridState()
|
|
64
|
-
return states.value[tabId]
|
|
64
|
+
if (!states.value[tabId]) states.value[tabId] = defaultGridState()
|
|
65
|
+
return states.value[tabId]
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
function getCache(tabId: string): GridData | null {
|
|
68
|
-
return cache.value[tabId] ?? null
|
|
69
|
+
return cache.value[tabId] ?? null
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
function setCache(tabId: string, data: GridData) {
|
|
72
|
-
cache.value[tabId] = data
|
|
73
|
+
cache.value[tabId] = data
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
function invalidate(tabId: string) {
|
|
76
|
-
Reflect.deleteProperty(cache.value, tabId)
|
|
77
|
+
Reflect.deleteProperty(cache.value, tabId)
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// Forget everything about a closed tab.
|
|
80
81
|
function drop(tabId: string) {
|
|
81
|
-
Reflect.deleteProperty(states.value, tabId)
|
|
82
|
-
Reflect.deleteProperty(cache.value, tabId)
|
|
82
|
+
Reflect.deleteProperty(states.value, tabId)
|
|
83
|
+
Reflect.deleteProperty(cache.value, tabId)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A tab switched to another instance keeps what the user narrowed the
|
|
87
|
+
// rows to (search, filters, sort, page size, widths), from its first page.
|
|
88
|
+
function carry(fromId: string, toId: string) {
|
|
89
|
+
const from = states.value[fromId]
|
|
90
|
+
if (!from) return
|
|
91
|
+
states.value[toId] = {
|
|
92
|
+
...from,
|
|
93
|
+
filters: { ...from.filters },
|
|
94
|
+
columnWidths: { ...from.columnWidths },
|
|
95
|
+
page: 0,
|
|
96
|
+
}
|
|
97
|
+
Reflect.deleteProperty(cache.value, toId)
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
return { getState, getCache, setCache, invalidate, drop }
|
|
100
|
+
return { getState, getCache, setCache, invalidate, drop, carry }
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
// --- query helpers shared by the grid ---
|
|
@@ -91,10 +106,10 @@ export function useDataBrowserGrid() {
|
|
|
91
106
|
// SELECTION_FILTER_KEYS): a data filter on a column literally named `schema`,
|
|
92
107
|
// `table` or `instance` would clobber the selection parameters, so it is never
|
|
93
108
|
// emitted and the filter UI is disabled for these columns.
|
|
94
|
-
const RESERVED_FILTER_FIELDS = new Set([
|
|
109
|
+
const RESERVED_FILTER_FIELDS = new Set(['schema', 'table', 'instance'])
|
|
95
110
|
|
|
96
111
|
export function isReservedFilterField(field: string): boolean {
|
|
97
|
-
return RESERVED_FILTER_FIELDS.has(field)
|
|
112
|
+
return RESERVED_FILTER_FIELDS.has(field)
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
// Normalized (empty-pruned, reserved-pruned, name-sorted) filter entries: the
|
|
@@ -105,18 +120,18 @@ function activeFilterEntries(
|
|
|
105
120
|
): Array<[string, ColumnFilterState]> {
|
|
106
121
|
return Object.entries(state.filters)
|
|
107
122
|
.filter(([field, filter]) => filter.value && !isReservedFilterField(field))
|
|
108
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
123
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
// Single encoder for the `<mode>:<value>` wire tokens parseFilter decodes
|
|
112
127
|
// server-side (src/utils/query.ts) — the grammar is defined here once instead
|
|
113
128
|
// of scattered string literals.
|
|
114
|
-
|
|
115
|
-
return `${mode}:${value}
|
|
129
|
+
function filterToken(mode: 'is' | 'contains', value: string): string {
|
|
130
|
+
return `${mode}:${value}`
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
export function isFilterToken(value: string): string {
|
|
119
|
-
return filterToken(
|
|
134
|
+
return filterToken('is', value)
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
// Selection filters identifying the table, in the wire format the browse
|
|
@@ -125,11 +140,11 @@ export function browseSelectionQuery(tab: BrowserTab): Record<string, string> {
|
|
|
125
140
|
const query: Record<string, string> = {
|
|
126
141
|
filter_schema: isFilterToken(tab.schema),
|
|
127
142
|
filter_table: isFilterToken(tab.table),
|
|
128
|
-
}
|
|
143
|
+
}
|
|
129
144
|
if (tab.instance !== DEFAULT_INSTANCE_VALUE) {
|
|
130
|
-
query.filter_instance = isFilterToken(tab.instance)
|
|
145
|
+
query.filter_instance = isFilterToken(tab.instance)
|
|
131
146
|
}
|
|
132
|
-
return query
|
|
147
|
+
return query
|
|
133
148
|
}
|
|
134
149
|
|
|
135
150
|
export function browseListQuery(
|
|
@@ -139,13 +154,13 @@ export function browseListQuery(
|
|
|
139
154
|
offset: state.page * state.pageSize,
|
|
140
155
|
limit: state.pageSize,
|
|
141
156
|
sortDirection: state.sortDirection,
|
|
142
|
-
}
|
|
143
|
-
if (state.sortKey) query.sortKey = state.sortKey
|
|
144
|
-
if (state.search) query.search = state.search
|
|
157
|
+
}
|
|
158
|
+
if (state.sortKey) query.sortKey = state.sortKey
|
|
159
|
+
if (state.search) query.search = state.search
|
|
145
160
|
for (const [field, filter] of activeFilterEntries(state)) {
|
|
146
|
-
query[`filter_${field}`] = filterToken(filter.mode, filter.value)
|
|
161
|
+
query[`filter_${field}`] = filterToken(filter.mode, filter.value)
|
|
147
162
|
}
|
|
148
|
-
return query
|
|
163
|
+
return query
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
// Everything that changes the server payload (column widths deliberately not).
|
|
@@ -158,5 +173,5 @@ export function browseParamsKey(tab: BrowserTab, state: GridState): string {
|
|
|
158
173
|
state.sortDirection,
|
|
159
174
|
state.search,
|
|
160
175
|
activeFilterEntries(state),
|
|
161
|
-
])
|
|
176
|
+
])
|
|
162
177
|
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { CROSS_INSTANCE_SENTINEL } from '../utils/stagedSerialization'
|
|
2
|
+
import { useDataBrowserGrid } from './useDataBrowserGrid'
|
|
3
|
+
import type { InstanceChoice } from '../data/instanceOptions'
|
|
4
|
+
import { rememberTable } from '../data/recentTables'
|
|
5
|
+
import { useStagedEdits } from '../data/stagedEdits'
|
|
6
|
+
import {
|
|
7
|
+
decodeMatch,
|
|
8
|
+
instanceFromUrl,
|
|
9
|
+
instanceToUrl,
|
|
10
|
+
} from '../utils/databaseLinks'
|
|
11
|
+
|
|
12
|
+
// Shared state for the data-browser tabs: each tab pins a (schema, instance,
|
|
13
|
+
// table) triplet. The active tab is mirrored to the URL query (deep-links) and
|
|
14
|
+
// the whole tab set is persisted to sessionStorage (stays per browser tab); a
|
|
15
|
+
// page refresh brings every tab back, the preview one still a preview.
|
|
16
|
+
|
|
17
|
+
// A tab's instance as its id and the browse routes' `filter_instance` carry
|
|
18
|
+
// it: these two values, or a named instance id.
|
|
19
|
+
export const DEFAULT_INSTANCE_VALUE = '__DEFAULT__'
|
|
20
|
+
export const CROSS_INSTANCE_VALUE = CROSS_INSTANCE_SENTINEL
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A tab's instance as a choice ("all": read-only), as pickers and the URL
|
|
24
|
+
* (see instanceFromUrl) carry it.
|
|
25
|
+
*/
|
|
26
|
+
export function instanceChoiceOf(instance: string): InstanceChoice {
|
|
27
|
+
if (instance === DEFAULT_INSTANCE_VALUE) return { kind: 'default' }
|
|
28
|
+
if (instance === CROSS_INSTANCE_VALUE) return { kind: 'all' }
|
|
29
|
+
return { kind: 'named', id: instance }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The tab instance a choice stands for. */
|
|
33
|
+
export function tabInstanceOf(choice: InstanceChoice): string {
|
|
34
|
+
if (choice.kind === 'default') return DEFAULT_INSTANCE_VALUE
|
|
35
|
+
if (choice.kind === 'all') return CROSS_INSTANCE_VALUE
|
|
36
|
+
return choice.id
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** How a tab names its instance: null for the default one, "@eu", "@all". */
|
|
40
|
+
export function instanceBadge(
|
|
41
|
+
instance: string,
|
|
42
|
+
allLabel: string,
|
|
43
|
+
): string | null {
|
|
44
|
+
if (instance === DEFAULT_INSTANCE_VALUE) return null
|
|
45
|
+
return `@${instance === CROSS_INSTANCE_VALUE ? allLabel : instance}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BrowserTab {
|
|
49
|
+
id: string
|
|
50
|
+
schema: string
|
|
51
|
+
// DEFAULT_INSTANCE_VALUE, CROSS_INSTANCE_VALUE or a named instance id.
|
|
52
|
+
instance: string
|
|
53
|
+
table: string
|
|
54
|
+
// VS Code-style preview tab: the next openTab reuses it instead of adding
|
|
55
|
+
// a new tab. At most one per session; pinning (double-click, pin icon,
|
|
56
|
+
// drag-reorder, editing a cell) clears the flag.
|
|
57
|
+
preview?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const STORAGE_KEY = 'dms-database:data-browser:tabs'
|
|
61
|
+
const ACTIVATIONS_STATE = 'dms-database-browser-activations'
|
|
62
|
+
|
|
63
|
+
function browserTabId(schema: string, instance: string, table: string): string {
|
|
64
|
+
return `${schema}::${instance}::${table}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface PersistedTabs {
|
|
68
|
+
tabs: BrowserTab[]
|
|
69
|
+
activeId: string | null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readPersisted(): PersistedTabs | null {
|
|
73
|
+
if (typeof window === 'undefined') return null
|
|
74
|
+
try {
|
|
75
|
+
const raw = window.sessionStorage.getItem(STORAGE_KEY)
|
|
76
|
+
if (!raw) return null
|
|
77
|
+
const parsed = JSON.parse(raw) as PersistedTabs
|
|
78
|
+
if (!Array.isArray(parsed.tabs)) return null
|
|
79
|
+
const tabs = parsed.tabs
|
|
80
|
+
.filter(
|
|
81
|
+
(tab) =>
|
|
82
|
+
tab &&
|
|
83
|
+
typeof tab.schema === 'string' &&
|
|
84
|
+
typeof tab.instance === 'string' &&
|
|
85
|
+
typeof tab.table === 'string',
|
|
86
|
+
)
|
|
87
|
+
.map((tab) => ({
|
|
88
|
+
...tab,
|
|
89
|
+
id: browserTabId(tab.schema, tab.instance, tab.table),
|
|
90
|
+
preview: tab.preview === true,
|
|
91
|
+
}))
|
|
92
|
+
// Recomputed ids can collide on a legacy/hand-edited payload; duplicate
|
|
93
|
+
// ids would break the tab bar's v-for keys and closeTab.
|
|
94
|
+
.filter(
|
|
95
|
+
(tab, index, all) => all.findIndex((t) => t.id === tab.id) === index,
|
|
96
|
+
)
|
|
97
|
+
// The preview slot is a singleton; a hand-edited payload could carry
|
|
98
|
+
// several — honour only the first.
|
|
99
|
+
const firstPreview = tabs.findIndex((tab) => tab.preview)
|
|
100
|
+
for (const [index, tab] of tabs.entries()) {
|
|
101
|
+
if (index !== firstPreview) tab.preview = false
|
|
102
|
+
}
|
|
103
|
+
const activeId = tabs.some((tab) => tab.id === parsed.activeId)
|
|
104
|
+
? parsed.activeId
|
|
105
|
+
: (tabs[0]?.id ?? null)
|
|
106
|
+
return { tabs, activeId }
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function useDataBrowserTabs() {
|
|
113
|
+
const tabs = useDmsState<BrowserTab[]>('dms-database-browser-tabs', () => [])
|
|
114
|
+
const activeId = useDmsState<string | null>(
|
|
115
|
+
'dms-database-browser-active',
|
|
116
|
+
() => null,
|
|
117
|
+
)
|
|
118
|
+
// Counts the activations, a tab already active included: the sidebar
|
|
119
|
+
// follows the tab the user goes to, even back to the same one.
|
|
120
|
+
const activations = useDmsState<number>(ACTIVATIONS_STATE, () => 0)
|
|
121
|
+
const route = useDmsRoute()
|
|
122
|
+
const router = useDmsRouter()
|
|
123
|
+
// Captured at setup so closeTab can drop the closed tab's
|
|
124
|
+
// grid state itself instead of relying on every caller to remember to.
|
|
125
|
+
const grid = useDataBrowserGrid()
|
|
126
|
+
const staged = useStagedEdits()
|
|
127
|
+
|
|
128
|
+
const activeTab = computed(
|
|
129
|
+
() => tabs.value.find((tab) => tab.id === activeId.value) ?? null,
|
|
130
|
+
)
|
|
131
|
+
// When tabs span several schemas the tab bar disambiguates its labels.
|
|
132
|
+
const hasMultipleSchemas = computed(
|
|
133
|
+
() => new Set(tabs.value.map((tab) => tab.schema)).size > 1,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
function persist() {
|
|
137
|
+
if (typeof window === 'undefined') return
|
|
138
|
+
try {
|
|
139
|
+
window.sessionStorage.setItem(
|
|
140
|
+
STORAGE_KEY,
|
|
141
|
+
JSON.stringify({ tabs: tabs.value, activeId: activeId.value }),
|
|
142
|
+
)
|
|
143
|
+
} catch {
|
|
144
|
+
// Quota/private-mode failures only cost tab restoration.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The URL carries the ACTIVE tab only (deep-links); per-tab grid state
|
|
149
|
+
// deliberately stays out of it.
|
|
150
|
+
function syncUrl() {
|
|
151
|
+
const tab = activeTab.value
|
|
152
|
+
const query = { ...route.query }
|
|
153
|
+
delete query.schema
|
|
154
|
+
delete query.instance
|
|
155
|
+
delete query.table
|
|
156
|
+
delete query.match
|
|
157
|
+
if (tab) {
|
|
158
|
+
query.schema = tab.schema
|
|
159
|
+
query.table = tab.table
|
|
160
|
+
const instance = instanceToUrl(instanceChoiceOf(tab.instance))
|
|
161
|
+
if (instance) query.instance = instance
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
Object.keys(query).length === Object.keys(route.query).length &&
|
|
165
|
+
Object.entries(query).every(([key, value]) => route.query[key] === value)
|
|
166
|
+
)
|
|
167
|
+
return
|
|
168
|
+
void router.replace({ query })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function activateTab(id: string) {
|
|
172
|
+
const tab = tabs.value.find((candidate) => candidate.id === id)
|
|
173
|
+
if (!tab) return
|
|
174
|
+
rememberTable({
|
|
175
|
+
schema: tab.schema,
|
|
176
|
+
instance: tab.instance,
|
|
177
|
+
table: tab.table,
|
|
178
|
+
})
|
|
179
|
+
activeId.value = id
|
|
180
|
+
activations.value += 1
|
|
181
|
+
syncUrl()
|
|
182
|
+
persist()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Promote a preview tab to a permanent one (double-click, pin icon, drag).
|
|
186
|
+
function pinTab(id: string) {
|
|
187
|
+
const tab = tabs.value.find((t) => t.id === id)
|
|
188
|
+
if (!tab?.preview) return
|
|
189
|
+
tabs.value = tabs.value.map((t) =>
|
|
190
|
+
t.id === id ? { ...t, preview: false } : t,
|
|
191
|
+
)
|
|
192
|
+
persist()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Opens as a preview by default (VS Code-style: single-click reuses the
|
|
196
|
+
// preview slot). Pass preview = false for an explicitly permanent open
|
|
197
|
+
// (sidebar double-click, deep-links) — it also pins the tab if already
|
|
198
|
+
// open as preview.
|
|
199
|
+
function openTab(
|
|
200
|
+
schema: string,
|
|
201
|
+
instance: string,
|
|
202
|
+
table: string,
|
|
203
|
+
preview = true,
|
|
204
|
+
) {
|
|
205
|
+
const id = browserTabId(schema, instance, table)
|
|
206
|
+
const existing = tabs.value.find((tab) => tab.id === id)
|
|
207
|
+
if (existing) {
|
|
208
|
+
if (!preview) pinTab(id)
|
|
209
|
+
activateTab(id)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
const tab: BrowserTab = { id, schema, instance, table, preview }
|
|
213
|
+
let previewIndex = tabs.value.findIndex((t) => t.preview)
|
|
214
|
+
// A tab holding staged edits is never replaced: it is kept instead
|
|
215
|
+
// (staging a cell already keeps it; this guards any other path).
|
|
216
|
+
if (previewIndex >= 0 && staged.count(tabs.value[previewIndex]!.id) > 0) {
|
|
217
|
+
pinTab(tabs.value[previewIndex]!.id)
|
|
218
|
+
previewIndex = -1
|
|
219
|
+
}
|
|
220
|
+
if (preview && previewIndex >= 0) {
|
|
221
|
+
// Reuse the preview slot in place: swap its content, drop the replaced
|
|
222
|
+
// tab's grid state (its id dies with it).
|
|
223
|
+
const replaced = tabs.value[previewIndex]!
|
|
224
|
+
const next = [...tabs.value]
|
|
225
|
+
next[previewIndex] = tab
|
|
226
|
+
tabs.value = next
|
|
227
|
+
grid.drop(replaced.id)
|
|
228
|
+
} else {
|
|
229
|
+
tabs.value = [...tabs.value, tab]
|
|
230
|
+
}
|
|
231
|
+
activateTab(id)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Puts another table in a tab's place (an instance switch): same position,
|
|
236
|
+
* same preview state, and the grid's search, filters and sort carried
|
|
237
|
+
* over. A tab already open on that table is activated instead, and the
|
|
238
|
+
* replaced one closed.
|
|
239
|
+
*/
|
|
240
|
+
function replaceTab(
|
|
241
|
+
oldId: string,
|
|
242
|
+
schema: string,
|
|
243
|
+
instance: string,
|
|
244
|
+
table: string,
|
|
245
|
+
) {
|
|
246
|
+
const index = tabs.value.findIndex((tab) => tab.id === oldId)
|
|
247
|
+
const old = tabs.value[index]
|
|
248
|
+
if (!old) return openTab(schema, instance, table, false)
|
|
249
|
+
const id = browserTabId(schema, instance, table)
|
|
250
|
+
if (id === oldId) return activateTab(id)
|
|
251
|
+
if (tabs.value.some((tab) => tab.id === id)) {
|
|
252
|
+
tabs.value = tabs.value.filter((tab) => tab.id !== oldId)
|
|
253
|
+
grid.drop(oldId)
|
|
254
|
+
activateTab(id)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
const next = [...tabs.value]
|
|
258
|
+
next[index] = { id, schema, instance, table, preview: old.preview }
|
|
259
|
+
tabs.value = next
|
|
260
|
+
grid.carry(oldId, id)
|
|
261
|
+
grid.drop(oldId)
|
|
262
|
+
activateTab(id)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function closeTab(id: string) {
|
|
266
|
+
const index = tabs.value.findIndex((tab) => tab.id === id)
|
|
267
|
+
if (index < 0) return
|
|
268
|
+
const next = tabs.value.filter((tab) => tab.id !== id)
|
|
269
|
+
tabs.value = next
|
|
270
|
+
if (activeId.value === id) {
|
|
271
|
+
// Right neighbour first, then left — mirrors browser tab behaviour.
|
|
272
|
+
activeId.value = (next[index] ?? next[index - 1])?.id ?? null
|
|
273
|
+
}
|
|
274
|
+
// The closed tab's grid state and cached rows go with it — otherwise they
|
|
275
|
+
// leak for the rest of the session.
|
|
276
|
+
grid.drop(id)
|
|
277
|
+
syncUrl()
|
|
278
|
+
persist()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Reorder (drag & drop): move the dragged tab to the target tab's position.
|
|
282
|
+
// Pure reorder — pin-on-drag is a gesture policy and lives at the drop site
|
|
283
|
+
// (the tab bar's onDrop), not in this primitive.
|
|
284
|
+
function moveTab(draggedId: string, targetId: string) {
|
|
285
|
+
if (draggedId === targetId) return
|
|
286
|
+
const next = [...tabs.value]
|
|
287
|
+
const from = next.findIndex((tab) => tab.id === draggedId)
|
|
288
|
+
const to = next.findIndex((tab) => tab.id === targetId)
|
|
289
|
+
if (from < 0 || to < 0) return
|
|
290
|
+
const [moved] = next.splice(from, 1)
|
|
291
|
+
if (!moved) return
|
|
292
|
+
next.splice(to, 0, moved)
|
|
293
|
+
tabs.value = next
|
|
294
|
+
persist()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Opens the table the URL names (overview links, the inspector's "Browse
|
|
298
|
+
// data", relation links, shared URLs): permanently, since the preview
|
|
299
|
+
// slot is for casual browsing from the list.
|
|
300
|
+
// The table the URL names, if any.
|
|
301
|
+
function routeTable() {
|
|
302
|
+
const schema = route.query.schema
|
|
303
|
+
const table = route.query.table
|
|
304
|
+
if (
|
|
305
|
+
typeof schema !== 'string' ||
|
|
306
|
+
!schema ||
|
|
307
|
+
typeof table !== 'string' ||
|
|
308
|
+
!table
|
|
309
|
+
)
|
|
310
|
+
return null
|
|
311
|
+
const instance = tabInstanceOf(instanceFromUrl(route.query.instance))
|
|
312
|
+
return {
|
|
313
|
+
schema,
|
|
314
|
+
instance,
|
|
315
|
+
table,
|
|
316
|
+
id: browserTabId(schema, instance, table),
|
|
317
|
+
// A link to one row (a relation, a reference) opens the table filtered
|
|
318
|
+
// on it; the filter shows as a chip the user removes.
|
|
319
|
+
match: decodeMatch(route.query.match),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function openFromRoute() {
|
|
324
|
+
const target = routeTable()
|
|
325
|
+
if (!target) return false
|
|
326
|
+
const { schema, instance, table, id, match } = target
|
|
327
|
+
// The route names the active tab: the browser wrote it itself (a tab
|
|
328
|
+
// activated, a reload), so it opens nothing and a preview tab stays one.
|
|
329
|
+
if (!match && id === activeId.value) return true
|
|
330
|
+
if (match) {
|
|
331
|
+
const state = grid.getState(id)
|
|
332
|
+
state.filters = { [match.field]: { mode: 'is', value: match.value } }
|
|
333
|
+
state.page = 0
|
|
334
|
+
}
|
|
335
|
+
openTab(schema, instance, table, false)
|
|
336
|
+
return true
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Restore persisted tabs, preview included, then let the URL win for the
|
|
340
|
+
// active selection. Call once, from the page's onMounted.
|
|
341
|
+
function restore() {
|
|
342
|
+
const persisted = readPersisted()
|
|
343
|
+
if (persisted) {
|
|
344
|
+
tabs.value = persisted.tabs
|
|
345
|
+
activeId.value = persisted.activeId
|
|
346
|
+
}
|
|
347
|
+
// A schema-only link (?schema=X) is the sidebar's to honour.
|
|
348
|
+
if (!openFromRoute() && !route.query.schema && activeTab.value) syncUrl()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
tabs,
|
|
353
|
+
activeId,
|
|
354
|
+
activeTab,
|
|
355
|
+
activations,
|
|
356
|
+
hasMultipleSchemas,
|
|
357
|
+
openTab,
|
|
358
|
+
replaceTab,
|
|
359
|
+
closeTab,
|
|
360
|
+
activateTab,
|
|
361
|
+
pinTab,
|
|
362
|
+
moveTab,
|
|
363
|
+
restore,
|
|
364
|
+
openFromRoute,
|
|
365
|
+
}
|
|
366
|
+
}
|