@antelopejs/dms-database 0.0.13 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +30 -29
- 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 +526 -193
- package/frontend-vue/i18n/locales/database-fr-FR.json +528 -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 +9 -6
- 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
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SchemaSummary } from '../build/composables/useDatabaseSchemas'
|
|
3
|
+
|
|
4
|
+
type InspectorTab = 'columns' | 'indexes' | 'relations' | 'sample'
|
|
5
|
+
|
|
6
|
+
// The drawer mounts the inspector anew on each table J / K step to: the
|
|
7
|
+
// last schemas and tab carry over, so the next table shows at once, on the
|
|
8
|
+
// same tab, while the schemas load again. Written in the browser only.
|
|
9
|
+
let lastSchemas: SchemaSummary[] | undefined
|
|
10
|
+
let lastTab: InspectorTab = 'columns'
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { useClipboard } from '@vueuse/core'
|
|
15
|
+
import {
|
|
16
|
+
findTableIn,
|
|
17
|
+
inboundRelationsIn,
|
|
18
|
+
useDatabaseSchemas,
|
|
19
|
+
type FieldDescriptor,
|
|
20
|
+
} from '../build/composables/useDatabaseSchemas'
|
|
21
|
+
import {
|
|
22
|
+
COLUMN_ROLE_CLASSES,
|
|
23
|
+
type ColumnRole,
|
|
24
|
+
describeField,
|
|
25
|
+
describeModifier,
|
|
26
|
+
isPrimaryKey,
|
|
27
|
+
} from '../build/utils/fieldTypes'
|
|
28
|
+
import { CROSS_INSTANCE_VALUE } from '../build/composables/useDataBrowserTabs'
|
|
29
|
+
import { ALL_INSTANCES_PARAM, tableLink } from '../build/utils/databaseLinks'
|
|
30
|
+
|
|
31
|
+
// The Schemas page's row drawer (D-07), also opened from the diagram: the
|
|
32
|
+
// facts of one table, its columns, indexes and relations both ways, and a
|
|
33
|
+
// sample row. The table view hands it the row and the means to step to the
|
|
34
|
+
// next one (J / K); a relation opens the table it names in its place.
|
|
35
|
+
|
|
36
|
+
interface TableRow {
|
|
37
|
+
schema: string
|
|
38
|
+
name: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface RowNavigation {
|
|
42
|
+
index: number
|
|
43
|
+
total: number
|
|
44
|
+
hasPrev: boolean
|
|
45
|
+
hasNext: boolean
|
|
46
|
+
prev: () => void
|
|
47
|
+
next: () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const props = defineProps<{
|
|
51
|
+
rowData?: TableRow
|
|
52
|
+
navigation?: RowNavigation
|
|
53
|
+
}>()
|
|
54
|
+
|
|
55
|
+
// A link out closes the drawer.
|
|
56
|
+
const emit = defineEmits<{ success: [] }>()
|
|
57
|
+
|
|
58
|
+
// Descriptor kinds that admit an empty value on their own.
|
|
59
|
+
const NULLISH_KINDS = new Set<FieldDescriptor['kind']>([
|
|
60
|
+
'null',
|
|
61
|
+
'undefined',
|
|
62
|
+
'any',
|
|
63
|
+
'unknown',
|
|
64
|
+
])
|
|
65
|
+
const BROWSE_LIST = '/api/database/browse/list'
|
|
66
|
+
const BROWSE_COUNT = '/api/database/browse/count'
|
|
67
|
+
|
|
68
|
+
const { t, n } = useI18n()
|
|
69
|
+
const { $authFetch } = useAuthFetch()
|
|
70
|
+
const { copy } = useClipboard()
|
|
71
|
+
const toast = useToast()
|
|
72
|
+
|
|
73
|
+
const isBrowser = typeof window !== 'undefined'
|
|
74
|
+
|
|
75
|
+
const tab = ref<InspectorTab>(lastTab)
|
|
76
|
+
watch(tab, (value) => {
|
|
77
|
+
if (isBrowser) lastTab = value
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const {
|
|
81
|
+
schemas: loadedSchemas,
|
|
82
|
+
isSettled: schemasSettled,
|
|
83
|
+
error: schemasError,
|
|
84
|
+
refresh: refreshSchemas,
|
|
85
|
+
} = useDatabaseSchemas()
|
|
86
|
+
watch(schemasSettled, (settled) => {
|
|
87
|
+
if (settled && !schemasError.value && isBrowser)
|
|
88
|
+
lastSchemas = loadedSchemas.value
|
|
89
|
+
})
|
|
90
|
+
// Every schema: the last ones until these load, and should these fail.
|
|
91
|
+
const schemas = computed(() =>
|
|
92
|
+
schemasSettled.value && !schemasError.value
|
|
93
|
+
? loadedSchemas.value
|
|
94
|
+
: lastSchemas,
|
|
95
|
+
)
|
|
96
|
+
const loading = computed(
|
|
97
|
+
() => !schemasSettled.value && schemas.value === undefined,
|
|
98
|
+
)
|
|
99
|
+
const failed = computed(
|
|
100
|
+
() => schemasSettled.value && schemas.value === undefined,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// The table a relation was followed to, in place of the row's.
|
|
104
|
+
const followed = ref<TableRow | null>(null)
|
|
105
|
+
const shown = computed(() => followed.value ?? props.rowData)
|
|
106
|
+
// J / K step from the row the table view opened: its position is not the
|
|
107
|
+
// followed table's.
|
|
108
|
+
const rowNavigation = computed(() =>
|
|
109
|
+
followed.value ? undefined : props.navigation,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const schemaId = computed(() => shown.value?.schema ?? '')
|
|
113
|
+
const tableName = computed(() => shown.value?.name ?? '')
|
|
114
|
+
const tableKey = computed(() => `${schemaId.value}::${tableName.value}`)
|
|
115
|
+
const table = computed(() =>
|
|
116
|
+
findTableIn(schemas.value ?? [], schemaId.value, tableName.value),
|
|
117
|
+
)
|
|
118
|
+
// Renamed, dropped or mistyped: once the schemas are in, nothing names it.
|
|
119
|
+
const missing = computed(
|
|
120
|
+
() => !loading.value && !failed.value && table.value === undefined,
|
|
121
|
+
)
|
|
122
|
+
const schema = computed(() =>
|
|
123
|
+
schemas.value?.find((s) => s.id === schemaId.value),
|
|
124
|
+
)
|
|
125
|
+
const instanceCount = computed(() => schema.value?.stats.instanceCount ?? 1)
|
|
126
|
+
const address = computed(() => ({
|
|
127
|
+
schema: schemaId.value,
|
|
128
|
+
table: tableName.value,
|
|
129
|
+
}))
|
|
130
|
+
|
|
131
|
+
function isNullable(descriptor: FieldDescriptor): boolean {
|
|
132
|
+
if (NULLISH_KINDS.has(descriptor.kind)) return true
|
|
133
|
+
return (
|
|
134
|
+
descriptor.kind === 'union' &&
|
|
135
|
+
descriptor.members.some((m) => m.kind === 'null' || m.kind === 'undefined')
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const relationByField = computed(
|
|
140
|
+
() => new Map((table.value?.relations ?? []).map((r) => [r.fromField, r])),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const columns = computed(() => {
|
|
144
|
+
const current = table.value
|
|
145
|
+
if (!current) return []
|
|
146
|
+
return Object.entries(current.fields).map(([name, descriptor]) => {
|
|
147
|
+
const relation = relationByField.value.get(name)
|
|
148
|
+
const key = isPrimaryKey(name, current.indexes ?? {})
|
|
149
|
+
const role: ColumnRole = key ? 'key' : relation ? 'relation' : 'plain'
|
|
150
|
+
const type = describeField(descriptor, name)
|
|
151
|
+
return {
|
|
152
|
+
name,
|
|
153
|
+
role,
|
|
154
|
+
icon: key ? 'i-ph-key' : relation ? 'i-ph-arrow-right' : type.icon,
|
|
155
|
+
label: key
|
|
156
|
+
? t('dms_database.inspector.primary_key')
|
|
157
|
+
: relation
|
|
158
|
+
? relation.toTable
|
|
159
|
+
: type.label,
|
|
160
|
+
nullable: !key && isNullable(descriptor),
|
|
161
|
+
modifiers: (current.modifiers?.[name] ?? []).map(describeModifier),
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const indexes = computed(() =>
|
|
167
|
+
Object.entries(table.value?.indexes ?? {}).map(([name, index]) => ({
|
|
168
|
+
name,
|
|
169
|
+
fields: index.fields ?? [],
|
|
170
|
+
compound: (index.fields?.length ?? 0) > 1,
|
|
171
|
+
multi: Boolean(index.multi),
|
|
172
|
+
})),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
// A table outside the inspected one's schema is named with its schema.
|
|
176
|
+
function qualified(schemaOf: string, name: string): string {
|
|
177
|
+
return schemaOf === schemaId.value ? name : `${schemaOf}.${name}`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const outgoing = computed(() =>
|
|
181
|
+
(table.value?.relations ?? []).map((r) => ({
|
|
182
|
+
key: `out-${r.fromField}`,
|
|
183
|
+
from: `${tableName.value}.${r.fromField}`,
|
|
184
|
+
to: `${qualified(r.toSchema, r.toTable)}.${r.toField}`,
|
|
185
|
+
cardinality: r.many ? 'N : N' : 'N : 1',
|
|
186
|
+
target: { schema: r.toSchema, name: r.toTable },
|
|
187
|
+
})),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const incoming = computed(() =>
|
|
191
|
+
inboundRelationsIn(schemas.value ?? [], schemaId.value, tableName.value).map(
|
|
192
|
+
(r) => ({
|
|
193
|
+
key: `in-${r.fromSchema}-${r.fromTable}-${r.fromField}`,
|
|
194
|
+
from: `${qualified(r.fromSchema, r.fromTable)}.${r.fromField}`,
|
|
195
|
+
to: `${tableName.value}.${r.toField}`,
|
|
196
|
+
cardinality: r.many ? 'N : N' : 'N : 1',
|
|
197
|
+
target: { schema: r.fromSchema, name: r.fromTable },
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// The row's own table again closes the detour.
|
|
203
|
+
function follow(target: TableRow) {
|
|
204
|
+
const own =
|
|
205
|
+
target.schema === props.rowData?.schema &&
|
|
206
|
+
target.name === props.rowData?.name
|
|
207
|
+
followed.value = own ? null : target
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --- rows: in every instance at once; the data browser shows each one's ---
|
|
211
|
+
const total = ref<number | null>(null)
|
|
212
|
+
|
|
213
|
+
async function loadTotal(key: string) {
|
|
214
|
+
total.value = null
|
|
215
|
+
if (!schemaId.value || !tableName.value) return
|
|
216
|
+
try {
|
|
217
|
+
const result = await $authFetch<{ total: number }>(BROWSE_COUNT, {
|
|
218
|
+
query: {
|
|
219
|
+
filter_schema: `is:${schemaId.value}`,
|
|
220
|
+
filter_table: `is:${tableName.value}`,
|
|
221
|
+
filter_instance: `is:${CROSS_INSTANCE_VALUE}`,
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
if (key === tableKey.value) total.value = result.total
|
|
225
|
+
} catch {
|
|
226
|
+
// The total the list gave, if any, stays.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
watch(tableKey, loadTotal, { immediate: true })
|
|
231
|
+
|
|
232
|
+
const rowsFact = computed(() => {
|
|
233
|
+
const rows = total.value
|
|
234
|
+
return rows === null
|
|
235
|
+
? ''
|
|
236
|
+
: t('dms_database.inspector.facts.rows', { count: n(rows) }, rows)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const structureFacts = computed(() =>
|
|
240
|
+
[
|
|
241
|
+
t('dms_database.inspector.facts.columns', columns.value.length),
|
|
242
|
+
t('dms_database.inspector.facts.indexes', indexes.value.length),
|
|
243
|
+
t(
|
|
244
|
+
'dms_database.inspector.facts.relations',
|
|
245
|
+
outgoing.value.length + incoming.value.length,
|
|
246
|
+
),
|
|
247
|
+
].join(' · '),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const tabs = computed(() => [
|
|
251
|
+
{
|
|
252
|
+
value: 'columns',
|
|
253
|
+
label: t('dms_database.inspector.tabs.columns'),
|
|
254
|
+
badge: loading.value ? undefined : columns.value.length,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
value: 'indexes',
|
|
258
|
+
label: t('dms_database.inspector.tabs.indexes'),
|
|
259
|
+
badge: loading.value ? undefined : indexes.value.length,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
value: 'relations',
|
|
263
|
+
label: t('dms_database.inspector.tabs.relations'),
|
|
264
|
+
badge: loading.value
|
|
265
|
+
? undefined
|
|
266
|
+
: outgoing.value.length + incoming.value.length,
|
|
267
|
+
},
|
|
268
|
+
{ value: 'sample', label: t('dms_database.inspector.tabs.sample') },
|
|
269
|
+
])
|
|
270
|
+
|
|
271
|
+
// --- sample row: the latest row, or the next one on demand ---
|
|
272
|
+
const sample = ref<Record<string, unknown> | null>(null)
|
|
273
|
+
const sampleOffset = ref(0)
|
|
274
|
+
const sampleTotal = ref(0)
|
|
275
|
+
const sampleLoading = ref(false)
|
|
276
|
+
const sampleFailed = ref(false)
|
|
277
|
+
|
|
278
|
+
interface SamplePage {
|
|
279
|
+
results: Record<string, unknown>[]
|
|
280
|
+
total: number
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// The instance the sample is read from: the default one, or the first named
|
|
284
|
+
// instance holding rows when the default one holds none.
|
|
285
|
+
const sampleInstance = ref<string | null>(null)
|
|
286
|
+
|
|
287
|
+
const sampleInstanceLabel = computed(() =>
|
|
288
|
+
sampleInstance.value === null
|
|
289
|
+
? t('dms_database.inspector.sample.default_instance')
|
|
290
|
+
: t('dms_database.inspector.sample.named_instance', {
|
|
291
|
+
name: sampleInstance.value,
|
|
292
|
+
}),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
function fetchSample(instance: string | null) {
|
|
296
|
+
const query: Record<string, string | number> = {
|
|
297
|
+
filter_schema: `is:${schemaId.value}`,
|
|
298
|
+
filter_table: `is:${tableName.value}`,
|
|
299
|
+
offset: sampleOffset.value,
|
|
300
|
+
limit: 1,
|
|
301
|
+
}
|
|
302
|
+
if (instance) query.filter_instance = `is:${instance}`
|
|
303
|
+
return $authFetch<SamplePage>(BROWSE_LIST, { query })
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function firstFilledInstance(): Promise<SamplePage> {
|
|
307
|
+
const page = await fetchSample(null)
|
|
308
|
+
if (page.total > 0) return page
|
|
309
|
+
for (const instance of schema.value?.instances ?? []) {
|
|
310
|
+
const named = await fetchSample(instance)
|
|
311
|
+
if (named.total > 0) {
|
|
312
|
+
sampleInstance.value = instance
|
|
313
|
+
return named
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return page
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function loadSample() {
|
|
320
|
+
if (!schemaId.value || !tableName.value) return
|
|
321
|
+
sampleLoading.value = true
|
|
322
|
+
sampleFailed.value = false
|
|
323
|
+
try {
|
|
324
|
+
const res =
|
|
325
|
+
sampleInstance.value === null && sampleOffset.value === 0
|
|
326
|
+
? await firstFilledInstance()
|
|
327
|
+
: await fetchSample(sampleInstance.value)
|
|
328
|
+
sample.value = res.results?.[0] ?? null
|
|
329
|
+
sampleTotal.value = res.total ?? 0
|
|
330
|
+
} catch {
|
|
331
|
+
sampleFailed.value = true
|
|
332
|
+
sample.value = null
|
|
333
|
+
} finally {
|
|
334
|
+
sampleLoading.value = false
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function nextSample() {
|
|
339
|
+
sampleOffset.value =
|
|
340
|
+
sampleTotal.value > 0 ? (sampleOffset.value + 1) % sampleTotal.value : 0
|
|
341
|
+
loadSample()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Another table (a relation followed) starts over from its latest row.
|
|
345
|
+
watch(tableKey, () => {
|
|
346
|
+
sample.value = null
|
|
347
|
+
sampleOffset.value = 0
|
|
348
|
+
sampleTotal.value = 0
|
|
349
|
+
sampleInstance.value = null
|
|
350
|
+
sampleFailed.value = false
|
|
351
|
+
if (tab.value === 'sample' && !loading.value) loadSample()
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// The sample waits for the schemas: they name the instances to look in.
|
|
355
|
+
watch(
|
|
356
|
+
() => tab.value === 'sample' && !loading.value,
|
|
357
|
+
(ready) => {
|
|
358
|
+
if (ready && !sample.value && !sampleLoading.value) loadSample()
|
|
359
|
+
},
|
|
360
|
+
{ immediate: true },
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
// The `_instance` tag the store adds to rows of named instances is not a
|
|
364
|
+
// column of the table.
|
|
365
|
+
const sampleJson = computed(() => {
|
|
366
|
+
if (!sample.value) return ''
|
|
367
|
+
const { _instance: _tag, ...row } = sample.value
|
|
368
|
+
return JSON.stringify(row, null, 2)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
async function copyName() {
|
|
372
|
+
await copy(tableName.value)
|
|
373
|
+
toast.add({
|
|
374
|
+
title: t('dms_database.inspector.copied', { name: tableName.value }),
|
|
375
|
+
color: 'success',
|
|
376
|
+
icon: 'i-ph-check',
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
</script>
|
|
380
|
+
|
|
381
|
+
<template>
|
|
382
|
+
<!-- As wide as the side drawer, whatever the open tab holds. -->
|
|
383
|
+
<div class="flex h-full w-full min-w-0 flex-col gap-4">
|
|
384
|
+
<header class="grid grid-cols-[minmax(0,1fr)] gap-3">
|
|
385
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
386
|
+
<DmsEyebrow
|
|
387
|
+
class="min-w-0 truncate"
|
|
388
|
+
:label="t('dms_database.inspector.eyebrow', { schema: schemaId })"
|
|
389
|
+
/>
|
|
390
|
+
<span class="flex-1" />
|
|
391
|
+
<template v-if="rowNavigation">
|
|
392
|
+
<span class="text-dimmed shrink-0 font-mono text-[11px] tabular-nums">
|
|
393
|
+
{{ rowNavigation.index + 1 }} / {{ rowNavigation.total }}
|
|
394
|
+
</span>
|
|
395
|
+
<UButton
|
|
396
|
+
icon="i-ph-caret-up"
|
|
397
|
+
color="neutral"
|
|
398
|
+
variant="ghost"
|
|
399
|
+
size="xs"
|
|
400
|
+
:disabled="!rowNavigation.hasPrev"
|
|
401
|
+
:aria-label="t('dms_database.inspector.previous')"
|
|
402
|
+
:title="t('dms_database.inspector.previous')"
|
|
403
|
+
@click="rowNavigation.prev()"
|
|
404
|
+
/>
|
|
405
|
+
<UButton
|
|
406
|
+
icon="i-ph-caret-down"
|
|
407
|
+
color="neutral"
|
|
408
|
+
variant="ghost"
|
|
409
|
+
size="xs"
|
|
410
|
+
:disabled="!rowNavigation.hasNext"
|
|
411
|
+
:aria-label="t('dms_database.inspector.next')"
|
|
412
|
+
:title="t('dms_database.inspector.next')"
|
|
413
|
+
@click="rowNavigation.next()"
|
|
414
|
+
/>
|
|
415
|
+
</template>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
|
418
|
+
<h3
|
|
419
|
+
class="text-highlighted min-w-0 break-all font-mono text-lg font-semibold"
|
|
420
|
+
>
|
|
421
|
+
{{ tableName }}
|
|
422
|
+
</h3>
|
|
423
|
+
<UBadge
|
|
424
|
+
v-if="!loading"
|
|
425
|
+
color="neutral"
|
|
426
|
+
variant="outline"
|
|
427
|
+
size="sm"
|
|
428
|
+
class="font-mono"
|
|
429
|
+
>
|
|
430
|
+
{{ t('dms_database.inspector.instances', instanceCount) }}
|
|
431
|
+
</UBadge>
|
|
432
|
+
<UButton
|
|
433
|
+
icon="i-ph-copy"
|
|
434
|
+
color="neutral"
|
|
435
|
+
variant="ghost"
|
|
436
|
+
size="xs"
|
|
437
|
+
:aria-label="t('dms_database.inspector.copy_name')"
|
|
438
|
+
:title="t('dms_database.inspector.copy_name')"
|
|
439
|
+
@click="copyName"
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
<div v-if="loading" class="grid gap-2" aria-busy="true">
|
|
443
|
+
<USkeleton class="h-4 w-40" />
|
|
444
|
+
<USkeleton class="h-4 w-64 max-w-full" />
|
|
445
|
+
</div>
|
|
446
|
+
<div v-else class="text-muted grid gap-0.5 text-sm tabular-nums">
|
|
447
|
+
<p v-if="rowsFact">
|
|
448
|
+
{{ rowsFact }}
|
|
449
|
+
<template v-if="instanceCount > 1">
|
|
450
|
+
·
|
|
451
|
+
<DmsAutoLink
|
|
452
|
+
@click="emit('success')"
|
|
453
|
+
:to="
|
|
454
|
+
tableLink('data', { ...address, instance: ALL_INSTANCES_PARAM })
|
|
455
|
+
"
|
|
456
|
+
class="text-primary hover:underline"
|
|
457
|
+
>
|
|
458
|
+
{{ t('dms_database.inspector.facts.by_instance') }}
|
|
459
|
+
</DmsAutoLink>
|
|
460
|
+
</template>
|
|
461
|
+
</p>
|
|
462
|
+
<p>{{ structureFacts }}</p>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="flex flex-wrap gap-2">
|
|
465
|
+
<UButton
|
|
466
|
+
:to="tableLink('data', address)"
|
|
467
|
+
@click="emit('success')"
|
|
468
|
+
icon="i-ph-rows"
|
|
469
|
+
size="sm"
|
|
470
|
+
:label="t('dms_database.inspector.actions.browse')"
|
|
471
|
+
/>
|
|
472
|
+
<UButton
|
|
473
|
+
:to="tableLink('query', address)"
|
|
474
|
+
@click="emit('success')"
|
|
475
|
+
icon="i-ph-code"
|
|
476
|
+
size="sm"
|
|
477
|
+
color="neutral"
|
|
478
|
+
variant="outline"
|
|
479
|
+
:label="t('dms_database.inspector.actions.query')"
|
|
480
|
+
/>
|
|
481
|
+
<UButton
|
|
482
|
+
:to="tableLink('diagram', address)"
|
|
483
|
+
@click="emit('success')"
|
|
484
|
+
icon="i-ph-graph"
|
|
485
|
+
size="sm"
|
|
486
|
+
color="neutral"
|
|
487
|
+
variant="ghost"
|
|
488
|
+
:label="t('dms_database.inspector.actions.diagram')"
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
<!-- The tab list scrolls sideways on its own: held to the header's
|
|
492
|
+
width, the four tabs scroll on a phone rather than overflow. -->
|
|
493
|
+
<UTabs
|
|
494
|
+
v-model="tab"
|
|
495
|
+
:items="tabs"
|
|
496
|
+
variant="link"
|
|
497
|
+
size="sm"
|
|
498
|
+
:content="false"
|
|
499
|
+
class="min-w-0"
|
|
500
|
+
/>
|
|
501
|
+
</header>
|
|
502
|
+
|
|
503
|
+
<div class="min-h-0 min-w-0 flex-1 overflow-auto">
|
|
504
|
+
<div v-if="loading" class="grid gap-3" aria-busy="true">
|
|
505
|
+
<USkeleton v-for="line in 6" :key="line" class="h-7 w-full" />
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<DmsEmptyState
|
|
509
|
+
v-else-if="failed"
|
|
510
|
+
size="sm"
|
|
511
|
+
variant="error"
|
|
512
|
+
:title="t('dms_database.inspector.load_error')"
|
|
513
|
+
:actions="[
|
|
514
|
+
{
|
|
515
|
+
label: t('dms_database.common.retry'),
|
|
516
|
+
icon: 'i-ph-arrows-clockwise',
|
|
517
|
+
onClick: refreshSchemas,
|
|
518
|
+
},
|
|
519
|
+
]"
|
|
520
|
+
/>
|
|
521
|
+
|
|
522
|
+
<DmsEmptyState
|
|
523
|
+
v-else-if="missing"
|
|
524
|
+
size="sm"
|
|
525
|
+
variant="no-result"
|
|
526
|
+
icon="i-ph-magnifying-glass"
|
|
527
|
+
:title="t('dms_database.schemas.inspector.not_found')"
|
|
528
|
+
:description="`${schemaId}.${tableName}`"
|
|
529
|
+
/>
|
|
530
|
+
|
|
531
|
+
<table v-else-if="tab === 'columns'" class="w-full text-sm">
|
|
532
|
+
<thead>
|
|
533
|
+
<tr class="border-default border-b text-left">
|
|
534
|
+
<th class="text-dimmed py-2 pr-3 text-xs font-semibold">
|
|
535
|
+
{{ t('dms_database.inspector.cols.column') }}
|
|
536
|
+
</th>
|
|
537
|
+
<th class="text-dimmed px-3 py-2 text-xs font-semibold">
|
|
538
|
+
{{ t('dms_database.inspector.cols.type') }}
|
|
539
|
+
</th>
|
|
540
|
+
<th class="text-dimmed px-3 py-2 text-xs font-semibold">
|
|
541
|
+
{{ t('dms_database.inspector.cols.null') }}
|
|
542
|
+
</th>
|
|
543
|
+
<th class="text-dimmed py-2 pl-3 text-xs font-semibold">
|
|
544
|
+
{{ t('dms_database.inspector.cols.notes') }}
|
|
545
|
+
</th>
|
|
546
|
+
</tr>
|
|
547
|
+
</thead>
|
|
548
|
+
<tbody>
|
|
549
|
+
<tr
|
|
550
|
+
v-for="column in columns"
|
|
551
|
+
:key="column.name"
|
|
552
|
+
class="border-default/60 border-b last:border-0"
|
|
553
|
+
>
|
|
554
|
+
<td
|
|
555
|
+
class="text-highlighted break-all py-2 pr-3 font-mono text-[12.5px]"
|
|
556
|
+
>
|
|
557
|
+
{{ column.name }}
|
|
558
|
+
</td>
|
|
559
|
+
<td class="px-3 py-2">
|
|
560
|
+
<span
|
|
561
|
+
class="inline-flex items-center gap-1.5 font-mono text-[11.5px]"
|
|
562
|
+
:class="COLUMN_ROLE_CLASSES[column.role]"
|
|
563
|
+
>
|
|
564
|
+
<UIcon :name="column.icon" class="size-3.5 shrink-0" />
|
|
565
|
+
{{ column.label }}
|
|
566
|
+
</span>
|
|
567
|
+
</td>
|
|
568
|
+
<td class="px-3 py-2">
|
|
569
|
+
<span
|
|
570
|
+
v-if="column.nullable"
|
|
571
|
+
class="text-dimmed rounded border border-dashed border-current px-1 font-mono text-[10.5px]"
|
|
572
|
+
>
|
|
573
|
+
null
|
|
574
|
+
</span>
|
|
575
|
+
<span v-else class="text-dimmed">—</span>
|
|
576
|
+
</td>
|
|
577
|
+
<td class="text-muted py-2 pl-3 text-xs">
|
|
578
|
+
<span
|
|
579
|
+
v-for="modifier in column.modifiers"
|
|
580
|
+
:key="modifier.id"
|
|
581
|
+
class="mr-2 inline-flex items-center gap-1"
|
|
582
|
+
>
|
|
583
|
+
<UIcon :name="modifier.icon" class="size-3.5" />
|
|
584
|
+
{{ modifier.labelKey ? t(modifier.labelKey) : modifier.id }}
|
|
585
|
+
</span>
|
|
586
|
+
</td>
|
|
587
|
+
</tr>
|
|
588
|
+
</tbody>
|
|
589
|
+
</table>
|
|
590
|
+
|
|
591
|
+
<div v-else-if="tab === 'indexes'" class="grid gap-2">
|
|
592
|
+
<div
|
|
593
|
+
v-for="index in indexes"
|
|
594
|
+
:key="index.name"
|
|
595
|
+
class="border-default flex min-w-0 items-center gap-3 rounded-md border px-3 py-2.5"
|
|
596
|
+
>
|
|
597
|
+
<UBadge
|
|
598
|
+
:color="index.compound ? 'primary' : 'neutral'"
|
|
599
|
+
variant="subtle"
|
|
600
|
+
size="sm"
|
|
601
|
+
class="font-mono"
|
|
602
|
+
>
|
|
603
|
+
{{
|
|
604
|
+
index.compound
|
|
605
|
+
? t('dms_database.inspector.index.compound')
|
|
606
|
+
: t('dms_database.inspector.index.single')
|
|
607
|
+
}}
|
|
608
|
+
</UBadge>
|
|
609
|
+
<span class="text-toned min-w-0 truncate font-mono text-[12.5px]">
|
|
610
|
+
{{ index.name }}
|
|
611
|
+
</span>
|
|
612
|
+
<span class="ml-auto flex flex-wrap justify-end gap-1">
|
|
613
|
+
<span
|
|
614
|
+
v-for="field in index.fields"
|
|
615
|
+
:key="field"
|
|
616
|
+
class="bg-elevated text-muted rounded px-1.5 py-0.5 font-mono text-[11px]"
|
|
617
|
+
>
|
|
618
|
+
{{ field }}
|
|
619
|
+
</span>
|
|
620
|
+
</span>
|
|
621
|
+
</div>
|
|
622
|
+
<DmsEmptyState
|
|
623
|
+
v-if="indexes.length === 0"
|
|
624
|
+
size="sm"
|
|
625
|
+
icon="i-ph-key"
|
|
626
|
+
:title="t('dms_database.inspector.index.empty')"
|
|
627
|
+
/>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
<div v-else-if="tab === 'relations'" class="grid gap-5">
|
|
631
|
+
<section class="grid gap-2">
|
|
632
|
+
<DmsEyebrow
|
|
633
|
+
:label="
|
|
634
|
+
t('dms_database.inspector.relations.out', {
|
|
635
|
+
count: outgoing.length,
|
|
636
|
+
})
|
|
637
|
+
"
|
|
638
|
+
/>
|
|
639
|
+
<button
|
|
640
|
+
v-for="relation in outgoing"
|
|
641
|
+
:key="relation.key"
|
|
642
|
+
type="button"
|
|
643
|
+
class="border-default hover:bg-elevated/50 grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)_auto] items-center gap-2 rounded-md border px-3 py-2 text-left font-mono text-[12px]"
|
|
644
|
+
@click="follow(relation.target)"
|
|
645
|
+
>
|
|
646
|
+
<span class="text-toned truncate">{{ relation.from }}</span>
|
|
647
|
+
<UIcon name="i-ph-arrow-right" class="text-info size-3.5" />
|
|
648
|
+
<span class="text-info truncate">{{ relation.to }}</span>
|
|
649
|
+
<span class="text-dimmed text-[10.5px]">
|
|
650
|
+
{{ relation.cardinality }}
|
|
651
|
+
</span>
|
|
652
|
+
</button>
|
|
653
|
+
<p v-if="outgoing.length === 0" class="text-dimmed text-sm">
|
|
654
|
+
{{ t('dms_database.inspector.relations.none_out') }}
|
|
655
|
+
</p>
|
|
656
|
+
</section>
|
|
657
|
+
<section class="grid gap-2">
|
|
658
|
+
<DmsEyebrow
|
|
659
|
+
:label="
|
|
660
|
+
t('dms_database.inspector.relations.in', {
|
|
661
|
+
count: incoming.length,
|
|
662
|
+
})
|
|
663
|
+
"
|
|
664
|
+
/>
|
|
665
|
+
<button
|
|
666
|
+
v-for="relation in incoming"
|
|
667
|
+
:key="relation.key"
|
|
668
|
+
type="button"
|
|
669
|
+
class="border-default hover:bg-elevated/50 grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)_auto] items-center gap-2 rounded-md border px-3 py-2 text-left font-mono text-[12px]"
|
|
670
|
+
@click="follow(relation.target)"
|
|
671
|
+
>
|
|
672
|
+
<span class="text-info truncate">{{ relation.from }}</span>
|
|
673
|
+
<UIcon name="i-ph-arrow-right" class="text-info size-3.5" />
|
|
674
|
+
<span class="text-toned truncate">{{ relation.to }}</span>
|
|
675
|
+
<span class="text-dimmed text-[10.5px]">
|
|
676
|
+
{{ relation.cardinality }}
|
|
677
|
+
</span>
|
|
678
|
+
</button>
|
|
679
|
+
<p v-if="incoming.length === 0" class="text-dimmed text-sm">
|
|
680
|
+
{{ t('dms_database.inspector.relations.none_in') }}
|
|
681
|
+
</p>
|
|
682
|
+
</section>
|
|
683
|
+
</div>
|
|
684
|
+
|
|
685
|
+
<div v-else class="grid gap-3">
|
|
686
|
+
<!-- Nothing to page through in an empty table. -->
|
|
687
|
+
<div
|
|
688
|
+
v-if="sampleTotal > 0"
|
|
689
|
+
class="flex min-w-0 items-center justify-between gap-2"
|
|
690
|
+
>
|
|
691
|
+
<DmsEyebrow
|
|
692
|
+
class="min-w-0 truncate"
|
|
693
|
+
:label="
|
|
694
|
+
t('dms_database.inspector.sample.position', {
|
|
695
|
+
position: sampleOffset + 1,
|
|
696
|
+
total: sampleTotal,
|
|
697
|
+
instance: sampleInstanceLabel,
|
|
698
|
+
})
|
|
699
|
+
"
|
|
700
|
+
/>
|
|
701
|
+
<UButton
|
|
702
|
+
icon="i-ph-arrows-clockwise"
|
|
703
|
+
color="neutral"
|
|
704
|
+
variant="ghost"
|
|
705
|
+
size="xs"
|
|
706
|
+
:disabled="sampleTotal < 2"
|
|
707
|
+
:loading="sampleLoading"
|
|
708
|
+
:label="t('dms_database.inspector.sample.another')"
|
|
709
|
+
@click="nextSample"
|
|
710
|
+
/>
|
|
711
|
+
</div>
|
|
712
|
+
<DmsEmptyState
|
|
713
|
+
v-if="sampleFailed"
|
|
714
|
+
size="sm"
|
|
715
|
+
variant="error"
|
|
716
|
+
:title="t('dms_database.inspector.sample.error')"
|
|
717
|
+
:actions="[
|
|
718
|
+
{
|
|
719
|
+
label: t('dms_database.common.retry'),
|
|
720
|
+
icon: 'i-ph-arrows-clockwise',
|
|
721
|
+
onClick: loadSample,
|
|
722
|
+
},
|
|
723
|
+
]"
|
|
724
|
+
/>
|
|
725
|
+
<div
|
|
726
|
+
v-else-if="sampleLoading && !sample"
|
|
727
|
+
class="grid gap-2"
|
|
728
|
+
aria-busy="true"
|
|
729
|
+
>
|
|
730
|
+
<USkeleton v-for="line in 5" :key="line" class="h-4 w-full" />
|
|
731
|
+
</div>
|
|
732
|
+
<DmsEmptyState
|
|
733
|
+
v-else-if="!sample"
|
|
734
|
+
size="sm"
|
|
735
|
+
:title="t('dms_database.inspector.sample.empty')"
|
|
736
|
+
/>
|
|
737
|
+
<DmsCodeSnippet v-else :code="sampleJson" language="json" />
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
</template>
|