@antelopejs/dms-database 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +53 -0
- package/dist/automation/index.d.ts +2 -0
- package/dist/automation/index.js +13 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/tableWatch.d.ts +17 -0
- package/dist/automation/tableWatch.js +189 -0
- package/dist/automation/tableWatch.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +19 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/diagram_layouts.model.d.ts +25 -0
- package/dist/db/models/diagram_layouts.model.js +29 -0
- package/dist/db/models/diagram_layouts.model.js.map +1 -0
- package/dist/db/models/diagram_notes.model.d.ts +23 -0
- package/dist/db/models/diagram_notes.model.js +15 -0
- package/dist/db/models/diagram_notes.model.js.map +1 -0
- package/dist/db/models/index.d.ts +4 -0
- package/dist/db/models/index.js +21 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/query_history.model.d.ts +25 -0
- package/dist/db/models/query_history.model.js +33 -0
- package/dist/db/models/query_history.model.js.map +1 -0
- package/dist/db/models/saved_queries.model.d.ts +24 -0
- package/dist/db/models/saved_queries.model.js +21 -0
- package/dist/db/models/saved_queries.model.js.map +1 -0
- package/dist/db/tables/diagram_layouts.table.d.ts +7 -0
- package/dist/db/tables/diagram_layouts.table.js +35 -0
- package/dist/db/tables/diagram_layouts.table.js.map +1 -0
- package/dist/db/tables/diagram_notes.table.d.ts +13 -0
- package/dist/db/tables/diagram_notes.table.js +59 -0
- package/dist/db/tables/diagram_notes.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +4 -0
- package/dist/db/tables/index.js +21 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/query_history.table.d.ts +11 -0
- package/dist/db/tables/query_history.table.js +54 -0
- package/dist/db/tables/query_history.table.js.map +1 -0
- package/dist/db/tables/saved_queries.table.d.ts +12 -0
- package/dist/db/tables/saved_queries.table.js +58 -0
- package/dist/db/tables/saved_queries.table.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/data.d.ts +9 -0
- package/dist/pages/data.js +29 -0
- package/dist/pages/data.js.map +1 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.js +21 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/overview.d.ts +9 -0
- package/dist/pages/overview.js +29 -0
- package/dist/pages/overview.js.map +1 -0
- package/dist/pages/query.d.ts +9 -0
- package/dist/pages/query.js +29 -0
- package/dist/pages/query.js.map +1 -0
- package/dist/pages/schemas.d.ts +9 -0
- package/dist/pages/schemas.js +29 -0
- package/dist/pages/schemas.js.map +1 -0
- package/dist/routes/data.d.ts +25 -0
- package/dist/routes/data.js +324 -0
- package/dist/routes/data.js.map +1 -0
- package/dist/routes/diagram.d.ts +47 -0
- package/dist/routes/diagram.js +200 -0
- package/dist/routes/diagram.js.map +1 -0
- package/dist/routes/health.d.ts +6 -0
- package/dist/routes/health.js +35 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +6 -0
- package/dist/routes/index.js +23 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/query.d.ts +26 -0
- package/dist/routes/query.js +245 -0
- package/dist/routes/query.js.map +1 -0
- package/dist/routes/schemas.d.ts +8 -0
- package/dist/routes/schemas.js +35 -0
- package/dist/routes/schemas.js.map +1 -0
- package/dist/routes/tables.d.ts +23 -0
- package/dist/routes/tables.js +94 -0
- package/dist/routes/tables.js.map +1 -0
- package/dist/service/fieldDescriptor.d.ts +66 -0
- package/dist/service/fieldDescriptor.js +156 -0
- package/dist/service/fieldDescriptor.js.map +1 -0
- package/dist/service/health.d.ts +20 -0
- package/dist/service/health.js +58 -0
- package/dist/service/health.js.map +1 -0
- package/dist/service/introspect.d.ts +6 -0
- package/dist/service/introspect.js +312 -0
- package/dist/service/introspect.js.map +1 -0
- package/dist/service/schemaLabels.d.ts +7 -0
- package/dist/service/schemaLabels.js +39 -0
- package/dist/service/schemaLabels.js.map +1 -0
- package/dist/service/schemaRegistry.d.ts +2 -0
- package/dist/service/schemaRegistry.js +8 -0
- package/dist/service/schemaRegistry.js.map +1 -0
- package/dist/service/stagedSerialization.d.ts +6 -0
- package/dist/service/stagedSerialization.js +54 -0
- package/dist/service/stagedSerialization.js.map +1 -0
- package/dist/service/types.d.ts +67 -0
- package/dist/service/types.js +3 -0
- package/dist/service/types.js.map +1 -0
- package/dist/types/constants.d.ts +3 -0
- package/dist/types/constants.js +7 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/responses.d.ts +14 -0
- package/dist/types/responses.js +3 -0
- package/dist/types/responses.js.map +1 -0
- package/dist/utils/query.d.ts +17 -0
- package/dist/utils/query.js +116 -0
- package/dist/utils/query.js.map +1 -0
- package/dist/utils/requestValidation.d.ts +2 -0
- package/dist/utils/requestValidation.js +14 -0
- package/dist/utils/requestValidation.js.map +1 -0
- package/frontend-vue/.editorconfig +12 -0
- package/frontend-vue/.prettierrc +11 -0
- package/frontend-vue/app/components/DataBrowserSidebar.vue +254 -0
- package/frontend-vue/app/components/DataBrowserTabBar.vue +166 -0
- package/frontend-vue/app/components/DataGrid.vue +735 -0
- package/frontend-vue/app/components/DataGridCell.vue +147 -0
- package/frontend-vue/app/components/DataGridHeaderCell.vue +175 -0
- package/frontend-vue/app/components/DiagramNoteNode.vue +100 -0
- package/frontend-vue/app/components/DiagramRelationEdge.vue +62 -0
- package/frontend-vue/app/components/DiagramStubNode.vue +37 -0
- package/frontend-vue/app/components/DiagramTableNode.vue +94 -0
- package/frontend-vue/app/components/JsonCellPanel.vue +93 -0
- package/frontend-vue/app/components/QueryBuilderPanel.vue +501 -0
- package/frontend-vue/app/components/QueryFavoritesPanel.vue +127 -0
- package/frontend-vue/app/components/QueryHistoryPanel.vue +134 -0
- package/frontend-vue/app/components/QueryResultPanel.vue +204 -0
- package/frontend-vue/app/components/SchemaDiagram.vue +1150 -0
- package/frontend-vue/app/components/SchemaTableInspector.vue +278 -0
- package/frontend-vue/app/composables/useDataBrowserGrid.ts +162 -0
- package/frontend-vue/app/composables/useDataBrowserTabs.ts +255 -0
- package/frontend-vue/app/composables/useDatabaseHealth.ts +50 -0
- package/frontend-vue/app/composables/useDatabaseSchemas.ts +92 -0
- package/frontend-vue/app/composables/useDiagramPersistence.ts +99 -0
- package/frontend-vue/app/composables/useQueryRuntime.ts +118 -0
- package/frontend-vue/app/composables/useQueryStore.ts +117 -0
- package/frontend-vue/app/composables/useTableViewRowClick.ts +22 -0
- package/frontend-vue/app/custom-pages/database/data.vue +42 -0
- package/frontend-vue/app/custom-pages/database/overview.vue +367 -0
- package/frontend-vue/app/custom-pages/database/query.vue +432 -0
- package/frontend-vue/app/custom-pages/database/schemas.vue +216 -0
- package/frontend-vue/app/utils/databaseFieldModifiers.ts +33 -0
- package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +224 -0
- package/frontend-vue/app/utils/dmsTableView.ts +83 -0
- package/frontend-vue/app/utils/relativeTime.ts +15 -0
- package/frontend-vue/app/utils/stagedSerialization.ts +91 -0
- package/frontend-vue/dms.frontend.ts +38 -0
- package/frontend-vue/eslint.config.mjs +16 -0
- package/frontend-vue/i18n/locales/database-en-GB.json +239 -0
- package/frontend-vue/i18n/locales/database-fr-FR.json +239 -0
- package/frontend-vue/package.json +42 -0
- package/frontend-vue/pnpm-lock.yaml +5819 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/frontend-vue/tests/persistence.test.ts +70 -0
- package/frontend-vue/tests/queryRuntime.test.ts +53 -0
- package/frontend-vue/tests/registration.test.ts +33 -0
- package/frontend-vue/tsconfig.json +13 -0
- package/frontend-vue/vitest.config.ts +5 -0
- package/package.json +78 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DropdownMenuItem } from "@nuxt/ui";
|
|
3
|
+
import type { QueryScope, SavedQuery } from "../composables/useQueryStore";
|
|
4
|
+
import { formatRelativeTime } from "../utils/relativeTime";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
items: SavedQuery[];
|
|
8
|
+
scope: QueryScope;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = defineProps<Props>();
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
"update:scope": [value: QueryScope];
|
|
15
|
+
load: [query: SavedQuery];
|
|
16
|
+
edit: [query: SavedQuery];
|
|
17
|
+
delete: [id: string];
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const { t } = useI18n();
|
|
21
|
+
const { user } = useUserSession();
|
|
22
|
+
|
|
23
|
+
const internalScope = computed<QueryScope>({
|
|
24
|
+
get: () => props.scope,
|
|
25
|
+
set: (value) => emit("update:scope", value),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const scopeItems = computed(() => [
|
|
29
|
+
{ label: t("dms_database.query.scopeMe"), value: "me" as const },
|
|
30
|
+
{ label: t("dms_database.query.scopeShared"), value: "shared" as const },
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function menuItems(item: SavedQuery): DropdownMenuItem[][] {
|
|
34
|
+
const load: DropdownMenuItem = {
|
|
35
|
+
label: t("dms_database.query.itemMenu.load"),
|
|
36
|
+
icon: "i-ph-arrow-square-out",
|
|
37
|
+
onSelect: () => emit("load", item),
|
|
38
|
+
};
|
|
39
|
+
// The Team tab also lists other users' queries; only the owner may edit
|
|
40
|
+
// or delete them (the API enforces the same rule).
|
|
41
|
+
if (item.userId !== user.value?._id) return [[load]];
|
|
42
|
+
return [
|
|
43
|
+
[
|
|
44
|
+
load,
|
|
45
|
+
{
|
|
46
|
+
label: t("dms_database.query.itemMenu.edit"),
|
|
47
|
+
icon: "i-ph-pencil-simple",
|
|
48
|
+
onSelect: () => emit("edit", item),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
{
|
|
53
|
+
label: t("dms_database.query.itemMenu.delete"),
|
|
54
|
+
icon: "i-ph-trash",
|
|
55
|
+
color: "error",
|
|
56
|
+
onSelect: () => emit("delete", item.id),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<DmsCard :padded="false" class="flex flex-col">
|
|
65
|
+
<header class="flex items-center gap-2.5 px-4 pt-4 pb-2.5">
|
|
66
|
+
<UIcon name="i-ph-star" class="text-primary shrink-0" />
|
|
67
|
+
<h3 class="text-highlighted text-sm font-semibold">
|
|
68
|
+
{{ $t("dms_database.query.favorites") }}
|
|
69
|
+
</h3>
|
|
70
|
+
<DmsSegmented
|
|
71
|
+
v-model="internalScope"
|
|
72
|
+
:items="scopeItems"
|
|
73
|
+
:aria-label="$t('dms_database.query.scopeMe')"
|
|
74
|
+
class="ml-auto"
|
|
75
|
+
/>
|
|
76
|
+
</header>
|
|
77
|
+
|
|
78
|
+
<ul
|
|
79
|
+
v-if="items.length > 0"
|
|
80
|
+
class="flex-1 overflow-y-auto px-1.5 pb-2 space-y-0.5"
|
|
81
|
+
>
|
|
82
|
+
<li
|
|
83
|
+
v-for="item in items"
|
|
84
|
+
:key="item.id"
|
|
85
|
+
class="group flex items-center gap-2.5 rounded-md px-2.5 py-2.5 cursor-pointer hover:bg-elevated transition-colors"
|
|
86
|
+
@click="emit('load', item)"
|
|
87
|
+
@keydown.enter.prevent="emit('load', item)"
|
|
88
|
+
>
|
|
89
|
+
<UIcon name="i-ph-star-fill" class="text-primary shrink-0 size-3.5" />
|
|
90
|
+
<div class="min-w-0 flex-1">
|
|
91
|
+
<div
|
|
92
|
+
class="text-toned text-[12.5px] font-semibold truncate flex items-center gap-1.5"
|
|
93
|
+
>
|
|
94
|
+
<span class="truncate">{{ item.name }}</span>
|
|
95
|
+
<UIcon
|
|
96
|
+
v-if="item.shared"
|
|
97
|
+
name="i-ph-users"
|
|
98
|
+
class="text-info shrink-0 size-3.5"
|
|
99
|
+
:aria-label="$t('dms_database.query.sharedBadge')"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<span class="text-dimmed font-mono text-[10.5px] shrink-0">
|
|
104
|
+
{{ formatRelativeTime(item.createdAt) }}
|
|
105
|
+
</span>
|
|
106
|
+
<UDropdownMenu :items="menuItems(item)" :content="{ align: 'end' }">
|
|
107
|
+
<UButton
|
|
108
|
+
icon="i-ph-dots-three-vertical"
|
|
109
|
+
variant="ghost"
|
|
110
|
+
color="neutral"
|
|
111
|
+
size="xs"
|
|
112
|
+
class="opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
|
113
|
+
:aria-label="item.name"
|
|
114
|
+
@click.stop
|
|
115
|
+
/>
|
|
116
|
+
</UDropdownMenu>
|
|
117
|
+
</li>
|
|
118
|
+
</ul>
|
|
119
|
+
|
|
120
|
+
<div
|
|
121
|
+
v-else
|
|
122
|
+
class="flex-1 flex items-center justify-center px-4 py-6 text-xs text-dimmed text-center"
|
|
123
|
+
>
|
|
124
|
+
{{ $t("dms_database.query.emptyFavorites") }}
|
|
125
|
+
</div>
|
|
126
|
+
</DmsCard>
|
|
127
|
+
</template>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HistoryEntry } from "../composables/useQueryStore";
|
|
3
|
+
import { formatRelativeTime } from "../utils/relativeTime";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
items: HistoryEntry[];
|
|
7
|
+
total: number;
|
|
8
|
+
page: number;
|
|
9
|
+
pageSize: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = defineProps<Props>();
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
"update:page": [value: number];
|
|
16
|
+
load: [entry: HistoryEntry];
|
|
17
|
+
save: [entry: HistoryEntry];
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const totalPages = computed(() =>
|
|
21
|
+
Math.max(1, Math.ceil(props.total / props.pageSize)),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const canPrev = computed(() => props.page > 0);
|
|
25
|
+
const canNext = computed(() => props.page + 1 < totalPages.value);
|
|
26
|
+
|
|
27
|
+
const QUERY_PREVIEW_LIMIT = 64;
|
|
28
|
+
|
|
29
|
+
function preview(query: string): string {
|
|
30
|
+
const flat = query.replace(/\s+/g, " ").trim();
|
|
31
|
+
return flat.length > QUERY_PREVIEW_LIMIT
|
|
32
|
+
? `${flat.slice(0, QUERY_PREVIEW_LIMIT)}…`
|
|
33
|
+
: flat;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { t } = useI18n();
|
|
37
|
+
|
|
38
|
+
function meta(item: HistoryEntry): string {
|
|
39
|
+
const count = t(
|
|
40
|
+
"dms_database.query.rowCount",
|
|
41
|
+
{ count: item.rowCount },
|
|
42
|
+
item.rowCount,
|
|
43
|
+
);
|
|
44
|
+
return `${formatRelativeTime(item.executedAt)} · ${count}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function prev() {
|
|
48
|
+
if (canPrev.value) emit("update:page", props.page - 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function next() {
|
|
52
|
+
if (canNext.value) emit("update:page", props.page + 1);
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<DmsCard :padded="false" class="flex flex-col">
|
|
58
|
+
<header class="flex items-center gap-2.5 px-4 pt-4 pb-2.5">
|
|
59
|
+
<UIcon name="i-ph-clock" class="text-primary shrink-0" />
|
|
60
|
+
<h3 class="text-highlighted text-sm font-semibold">
|
|
61
|
+
{{ $t("dms_database.query.history") }}
|
|
62
|
+
</h3>
|
|
63
|
+
</header>
|
|
64
|
+
|
|
65
|
+
<ul
|
|
66
|
+
v-if="items.length > 0"
|
|
67
|
+
class="flex-1 overflow-y-auto px-1.5 pb-2 space-y-0.5"
|
|
68
|
+
>
|
|
69
|
+
<li
|
|
70
|
+
v-for="item in items"
|
|
71
|
+
:key="item.id"
|
|
72
|
+
class="group flex items-center gap-2.5 rounded-md px-2.5 py-2.5 cursor-pointer hover:bg-elevated transition-colors"
|
|
73
|
+
@click="emit('load', item)"
|
|
74
|
+
@keydown.enter.prevent="emit('load', item)"
|
|
75
|
+
>
|
|
76
|
+
<UIcon
|
|
77
|
+
name="i-ph-clock"
|
|
78
|
+
class="text-dimmed shrink-0 size-3.5"
|
|
79
|
+
/>
|
|
80
|
+
<div class="min-w-0 flex-1">
|
|
81
|
+
<div class="text-toned font-mono text-[11px] truncate">
|
|
82
|
+
{{ preview(item.source) }}
|
|
83
|
+
</div>
|
|
84
|
+
<div class="text-dimmed text-[10px] mt-0.5">
|
|
85
|
+
{{ meta(item) }}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<UButton
|
|
89
|
+
icon="i-ph-star"
|
|
90
|
+
variant="ghost"
|
|
91
|
+
color="neutral"
|
|
92
|
+
size="xs"
|
|
93
|
+
class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity hover:text-warning"
|
|
94
|
+
:aria-label="$t('dms_database.query.saveAsFavorite')"
|
|
95
|
+
@click.stop="emit('save', item)"
|
|
96
|
+
/>
|
|
97
|
+
</li>
|
|
98
|
+
</ul>
|
|
99
|
+
|
|
100
|
+
<div
|
|
101
|
+
v-else
|
|
102
|
+
class="flex-1 flex items-center justify-center px-4 py-6 text-xs text-dimmed text-center"
|
|
103
|
+
>
|
|
104
|
+
{{ $t("dms_database.query.emptyHistory") }}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<footer
|
|
108
|
+
v-if="items.length > 0"
|
|
109
|
+
class="flex items-center justify-between gap-2 px-3 py-2 border-t border-default"
|
|
110
|
+
>
|
|
111
|
+
<UButton
|
|
112
|
+
size="xs"
|
|
113
|
+
variant="ghost"
|
|
114
|
+
color="neutral"
|
|
115
|
+
icon="i-ph-caret-left"
|
|
116
|
+
:disabled="!canPrev"
|
|
117
|
+
:label="$t('dms_database.query.previous')"
|
|
118
|
+
@click="prev"
|
|
119
|
+
/>
|
|
120
|
+
<span class="text-xs text-dimmed tabular-nums">
|
|
121
|
+
{{ page + 1 }} / {{ totalPages }}
|
|
122
|
+
</span>
|
|
123
|
+
<UButton
|
|
124
|
+
size="xs"
|
|
125
|
+
variant="ghost"
|
|
126
|
+
color="neutral"
|
|
127
|
+
trailing-icon="i-ph-caret-right"
|
|
128
|
+
:disabled="!canNext"
|
|
129
|
+
:label="$t('dms_database.query.next')"
|
|
130
|
+
@click="next"
|
|
131
|
+
/>
|
|
132
|
+
</footer>
|
|
133
|
+
</DmsCard>
|
|
134
|
+
</template>
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ExecuteResult } from "../composables/useQueryStore";
|
|
3
|
+
|
|
4
|
+
type ResultMode = "table" | "json" | "chart";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
result: ExecuteResult | null;
|
|
8
|
+
mode: ResultMode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = defineProps<Props>();
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
"update:mode": [value: ResultMode];
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const { t } = useI18n();
|
|
18
|
+
|
|
19
|
+
const internalMode = computed<ResultMode>({
|
|
20
|
+
get: () => props.mode,
|
|
21
|
+
set: (value) => emit("update:mode", value),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const modeItems = computed(() => [
|
|
25
|
+
{ label: t("dms_database.query.table"), value: "table" as const },
|
|
26
|
+
{ label: t("dms_database.query.json"), value: "json" as const },
|
|
27
|
+
{ label: t("dms_database.query.chart"), value: "chart" as const },
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const columns = computed<string[]>(() => {
|
|
31
|
+
const rows = props.result?.rows ?? [];
|
|
32
|
+
const first = rows[0];
|
|
33
|
+
if (!first || typeof first !== "object") return [];
|
|
34
|
+
return Object.keys(first as Record<string, unknown>);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const rows = computed(() => props.result?.rows ?? []);
|
|
38
|
+
|
|
39
|
+
const jsonText = computed(() =>
|
|
40
|
+
props.result ? JSON.stringify(props.result.rows, null, 2) : "",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
function renderCell(value: unknown): string {
|
|
44
|
+
if (value === null || value === undefined) return "—";
|
|
45
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
46
|
+
return String(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isNumeric(value: unknown): value is number {
|
|
50
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- chart derivation (first numeric column, labelled by first string column) ---
|
|
54
|
+
const numericColumn = computed<string | null>(() => {
|
|
55
|
+
const first = rows.value[0] as Record<string, unknown> | undefined;
|
|
56
|
+
if (!first) return null;
|
|
57
|
+
return columns.value.find((c) => isNumeric(first[c])) ?? null;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const labelColumn = computed<string | null>(() => {
|
|
61
|
+
const first = rows.value[0] as Record<string, unknown> | undefined;
|
|
62
|
+
if (!first) return null;
|
|
63
|
+
return (
|
|
64
|
+
columns.value.find(
|
|
65
|
+
(c) => c !== numericColumn.value && typeof first[c] === "string",
|
|
66
|
+
) ?? null
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const chartAvailable = computed(
|
|
71
|
+
() => rows.value.length > 0 && numericColumn.value !== null,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const chartDataset = computed(() => {
|
|
75
|
+
const col = numericColumn.value;
|
|
76
|
+
if (!col) return [];
|
|
77
|
+
const label = labelColumn.value;
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
name: col,
|
|
81
|
+
data: rows.value.map((row, index) => {
|
|
82
|
+
const record = row as Record<string, unknown>;
|
|
83
|
+
return {
|
|
84
|
+
x: label ? String(record[label]) : index + 1,
|
|
85
|
+
y: isNumeric(record[col]) ? (record[col] as number) : 0,
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
});
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<template>
|
|
94
|
+
<DmsCard>
|
|
95
|
+
<!-- status bar -->
|
|
96
|
+
<div class="flex flex-wrap items-center gap-2.5 mb-4">
|
|
97
|
+
<UIcon
|
|
98
|
+
:name="
|
|
99
|
+
result?.error ? 'i-ph-x-circle-fill' : 'i-ph-check-circle-fill'
|
|
100
|
+
"
|
|
101
|
+
:class="result?.error ? 'text-error' : 'text-success'"
|
|
102
|
+
class="size-4"
|
|
103
|
+
/>
|
|
104
|
+
<b class="text-highlighted text-[15px]">
|
|
105
|
+
{{ $t("dms_database.query.result") }}
|
|
106
|
+
</b>
|
|
107
|
+
<template v-if="result && !result.error">
|
|
108
|
+
<UBadge color="success" variant="soft" size="sm" class="font-mono">
|
|
109
|
+
{{ $t("dms_database.query.rowCount", { count: rows.length }, rows.length) }}
|
|
110
|
+
</UBadge>
|
|
111
|
+
<span class="text-dimmed font-mono text-xs">
|
|
112
|
+
{{ $t("dms_database.query.runtime", { ms: result.durationMs }) }}
|
|
113
|
+
</span>
|
|
114
|
+
</template>
|
|
115
|
+
<DmsSegmented
|
|
116
|
+
v-model="internalMode"
|
|
117
|
+
:items="modeItems"
|
|
118
|
+
:aria-label="$t('dms_database.query.result')"
|
|
119
|
+
class="ml-auto"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- body -->
|
|
124
|
+
<div class="min-h-64">
|
|
125
|
+
<div
|
|
126
|
+
v-if="!result"
|
|
127
|
+
class="flex flex-col items-center justify-center gap-3 py-16 text-dimmed"
|
|
128
|
+
>
|
|
129
|
+
<UIcon name="i-ph-code" class="text-4xl text-dimmed/60" />
|
|
130
|
+
<p class="text-sm">{{ $t("dms_database.query.emptyResult") }}</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div
|
|
134
|
+
v-else-if="result.error"
|
|
135
|
+
class="rounded-lg border border-error/40 bg-error/10 px-4 py-3 text-sm text-error whitespace-pre-wrap"
|
|
136
|
+
>
|
|
137
|
+
<p class="font-semibold mb-1">
|
|
138
|
+
{{ $t("dms_database.query.errorTitle") }}
|
|
139
|
+
</p>
|
|
140
|
+
<p>{{ result.error }}</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- table -->
|
|
144
|
+
<div v-else-if="internalMode === 'table'" class="overflow-x-auto -mx-1">
|
|
145
|
+
<table class="w-full text-sm">
|
|
146
|
+
<thead>
|
|
147
|
+
<tr class="border-b border-default text-left">
|
|
148
|
+
<th
|
|
149
|
+
v-for="column in columns"
|
|
150
|
+
:key="column"
|
|
151
|
+
class="px-3 py-2 font-mono text-xs font-semibold text-dimmed"
|
|
152
|
+
>
|
|
153
|
+
{{ column }}
|
|
154
|
+
</th>
|
|
155
|
+
</tr>
|
|
156
|
+
</thead>
|
|
157
|
+
<tbody>
|
|
158
|
+
<tr
|
|
159
|
+
v-for="(row, rowIndex) in rows"
|
|
160
|
+
:key="rowIndex"
|
|
161
|
+
class="border-b border-default/60 last:border-0 hover:bg-elevated/40 transition-colors"
|
|
162
|
+
>
|
|
163
|
+
<td
|
|
164
|
+
v-for="(column, colIndex) in columns"
|
|
165
|
+
:key="column"
|
|
166
|
+
class="px-3 py-2 font-mono whitespace-nowrap"
|
|
167
|
+
:class="colIndex === 0 ? 'text-primary' : 'text-toned'"
|
|
168
|
+
>
|
|
169
|
+
{{ renderCell((row as Record<string, unknown>)[column]) }}
|
|
170
|
+
</td>
|
|
171
|
+
</tr>
|
|
172
|
+
</tbody>
|
|
173
|
+
</table>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<!-- json -->
|
|
177
|
+
<pre
|
|
178
|
+
v-else-if="internalMode === 'json'"
|
|
179
|
+
class="text-xs rounded-lg border border-default bg-default p-4 overflow-auto max-h-80 whitespace-pre text-toned"
|
|
180
|
+
>{{ jsonText }}</pre
|
|
181
|
+
>
|
|
182
|
+
|
|
183
|
+
<!-- chart -->
|
|
184
|
+
<div v-else>
|
|
185
|
+
<DmsChart
|
|
186
|
+
v-if="chartAvailable"
|
|
187
|
+
type="area"
|
|
188
|
+
:static-dataset="chartDataset"
|
|
189
|
+
color="primary"
|
|
190
|
+
height="280px"
|
|
191
|
+
:show-legend="false"
|
|
192
|
+
xaxis-type="category"
|
|
193
|
+
/>
|
|
194
|
+
<div
|
|
195
|
+
v-else
|
|
196
|
+
class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-center"
|
|
197
|
+
>
|
|
198
|
+
<UIcon name="i-ph-chart-line" class="text-4xl text-dimmed/60" />
|
|
199
|
+
<p class="text-sm">{{ $t("dms_database.query.chartUnavailable") }}</p>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</DmsCard>
|
|
204
|
+
</template>
|