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