@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.
Files changed (164) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +53 -0
  3. package/dist/automation/index.d.ts +2 -0
  4. package/dist/automation/index.js +13 -0
  5. package/dist/automation/index.js.map +1 -0
  6. package/dist/automation/tableWatch.d.ts +17 -0
  7. package/dist/automation/tableWatch.js +189 -0
  8. package/dist/automation/tableWatch.js.map +1 -0
  9. package/dist/config.d.ts +7 -0
  10. package/dist/config.js +12 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/db/index.d.ts +2 -0
  13. package/dist/db/index.js +19 -0
  14. package/dist/db/index.js.map +1 -0
  15. package/dist/db/models/diagram_layouts.model.d.ts +25 -0
  16. package/dist/db/models/diagram_layouts.model.js +29 -0
  17. package/dist/db/models/diagram_layouts.model.js.map +1 -0
  18. package/dist/db/models/diagram_notes.model.d.ts +23 -0
  19. package/dist/db/models/diagram_notes.model.js +15 -0
  20. package/dist/db/models/diagram_notes.model.js.map +1 -0
  21. package/dist/db/models/index.d.ts +4 -0
  22. package/dist/db/models/index.js +21 -0
  23. package/dist/db/models/index.js.map +1 -0
  24. package/dist/db/models/query_history.model.d.ts +25 -0
  25. package/dist/db/models/query_history.model.js +33 -0
  26. package/dist/db/models/query_history.model.js.map +1 -0
  27. package/dist/db/models/saved_queries.model.d.ts +24 -0
  28. package/dist/db/models/saved_queries.model.js +21 -0
  29. package/dist/db/models/saved_queries.model.js.map +1 -0
  30. package/dist/db/tables/diagram_layouts.table.d.ts +7 -0
  31. package/dist/db/tables/diagram_layouts.table.js +35 -0
  32. package/dist/db/tables/diagram_layouts.table.js.map +1 -0
  33. package/dist/db/tables/diagram_notes.table.d.ts +13 -0
  34. package/dist/db/tables/diagram_notes.table.js +59 -0
  35. package/dist/db/tables/diagram_notes.table.js.map +1 -0
  36. package/dist/db/tables/index.d.ts +4 -0
  37. package/dist/db/tables/index.js +21 -0
  38. package/dist/db/tables/index.js.map +1 -0
  39. package/dist/db/tables/query_history.table.d.ts +11 -0
  40. package/dist/db/tables/query_history.table.js +54 -0
  41. package/dist/db/tables/query_history.table.js.map +1 -0
  42. package/dist/db/tables/saved_queries.table.d.ts +12 -0
  43. package/dist/db/tables/saved_queries.table.js +58 -0
  44. package/dist/db/tables/saved_queries.table.js.map +1 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +56 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/pages/data.d.ts +9 -0
  49. package/dist/pages/data.js +29 -0
  50. package/dist/pages/data.js.map +1 -0
  51. package/dist/pages/index.d.ts +4 -0
  52. package/dist/pages/index.js +21 -0
  53. package/dist/pages/index.js.map +1 -0
  54. package/dist/pages/overview.d.ts +9 -0
  55. package/dist/pages/overview.js +29 -0
  56. package/dist/pages/overview.js.map +1 -0
  57. package/dist/pages/query.d.ts +9 -0
  58. package/dist/pages/query.js +29 -0
  59. package/dist/pages/query.js.map +1 -0
  60. package/dist/pages/schemas.d.ts +9 -0
  61. package/dist/pages/schemas.js +29 -0
  62. package/dist/pages/schemas.js.map +1 -0
  63. package/dist/routes/data.d.ts +25 -0
  64. package/dist/routes/data.js +324 -0
  65. package/dist/routes/data.js.map +1 -0
  66. package/dist/routes/diagram.d.ts +47 -0
  67. package/dist/routes/diagram.js +200 -0
  68. package/dist/routes/diagram.js.map +1 -0
  69. package/dist/routes/health.d.ts +6 -0
  70. package/dist/routes/health.js +35 -0
  71. package/dist/routes/health.js.map +1 -0
  72. package/dist/routes/index.d.ts +6 -0
  73. package/dist/routes/index.js +23 -0
  74. package/dist/routes/index.js.map +1 -0
  75. package/dist/routes/query.d.ts +26 -0
  76. package/dist/routes/query.js +245 -0
  77. package/dist/routes/query.js.map +1 -0
  78. package/dist/routes/schemas.d.ts +8 -0
  79. package/dist/routes/schemas.js +35 -0
  80. package/dist/routes/schemas.js.map +1 -0
  81. package/dist/routes/tables.d.ts +23 -0
  82. package/dist/routes/tables.js +94 -0
  83. package/dist/routes/tables.js.map +1 -0
  84. package/dist/service/fieldDescriptor.d.ts +66 -0
  85. package/dist/service/fieldDescriptor.js +156 -0
  86. package/dist/service/fieldDescriptor.js.map +1 -0
  87. package/dist/service/health.d.ts +20 -0
  88. package/dist/service/health.js +58 -0
  89. package/dist/service/health.js.map +1 -0
  90. package/dist/service/introspect.d.ts +6 -0
  91. package/dist/service/introspect.js +312 -0
  92. package/dist/service/introspect.js.map +1 -0
  93. package/dist/service/schemaLabels.d.ts +7 -0
  94. package/dist/service/schemaLabels.js +39 -0
  95. package/dist/service/schemaLabels.js.map +1 -0
  96. package/dist/service/schemaRegistry.d.ts +2 -0
  97. package/dist/service/schemaRegistry.js +8 -0
  98. package/dist/service/schemaRegistry.js.map +1 -0
  99. package/dist/service/stagedSerialization.d.ts +6 -0
  100. package/dist/service/stagedSerialization.js +54 -0
  101. package/dist/service/stagedSerialization.js.map +1 -0
  102. package/dist/service/types.d.ts +67 -0
  103. package/dist/service/types.js +3 -0
  104. package/dist/service/types.js.map +1 -0
  105. package/dist/types/constants.d.ts +3 -0
  106. package/dist/types/constants.js +7 -0
  107. package/dist/types/constants.js.map +1 -0
  108. package/dist/types/responses.d.ts +14 -0
  109. package/dist/types/responses.js +3 -0
  110. package/dist/types/responses.js.map +1 -0
  111. package/dist/utils/query.d.ts +17 -0
  112. package/dist/utils/query.js +116 -0
  113. package/dist/utils/query.js.map +1 -0
  114. package/dist/utils/requestValidation.d.ts +2 -0
  115. package/dist/utils/requestValidation.js +14 -0
  116. package/dist/utils/requestValidation.js.map +1 -0
  117. package/frontend-vue/.editorconfig +12 -0
  118. package/frontend-vue/.prettierrc +11 -0
  119. package/frontend-vue/app/components/DataBrowserSidebar.vue +254 -0
  120. package/frontend-vue/app/components/DataBrowserTabBar.vue +166 -0
  121. package/frontend-vue/app/components/DataGrid.vue +735 -0
  122. package/frontend-vue/app/components/DataGridCell.vue +147 -0
  123. package/frontend-vue/app/components/DataGridHeaderCell.vue +175 -0
  124. package/frontend-vue/app/components/DiagramNoteNode.vue +100 -0
  125. package/frontend-vue/app/components/DiagramRelationEdge.vue +62 -0
  126. package/frontend-vue/app/components/DiagramStubNode.vue +37 -0
  127. package/frontend-vue/app/components/DiagramTableNode.vue +94 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +93 -0
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +501 -0
  130. package/frontend-vue/app/components/QueryFavoritesPanel.vue +127 -0
  131. package/frontend-vue/app/components/QueryHistoryPanel.vue +134 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +204 -0
  133. package/frontend-vue/app/components/SchemaDiagram.vue +1150 -0
  134. package/frontend-vue/app/components/SchemaTableInspector.vue +278 -0
  135. package/frontend-vue/app/composables/useDataBrowserGrid.ts +162 -0
  136. package/frontend-vue/app/composables/useDataBrowserTabs.ts +255 -0
  137. package/frontend-vue/app/composables/useDatabaseHealth.ts +50 -0
  138. package/frontend-vue/app/composables/useDatabaseSchemas.ts +92 -0
  139. package/frontend-vue/app/composables/useDiagramPersistence.ts +99 -0
  140. package/frontend-vue/app/composables/useQueryRuntime.ts +118 -0
  141. package/frontend-vue/app/composables/useQueryStore.ts +117 -0
  142. package/frontend-vue/app/composables/useTableViewRowClick.ts +22 -0
  143. package/frontend-vue/app/custom-pages/database/data.vue +42 -0
  144. package/frontend-vue/app/custom-pages/database/overview.vue +367 -0
  145. package/frontend-vue/app/custom-pages/database/query.vue +432 -0
  146. package/frontend-vue/app/custom-pages/database/schemas.vue +216 -0
  147. package/frontend-vue/app/utils/databaseFieldModifiers.ts +33 -0
  148. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +224 -0
  149. package/frontend-vue/app/utils/dmsTableView.ts +83 -0
  150. package/frontend-vue/app/utils/relativeTime.ts +15 -0
  151. package/frontend-vue/app/utils/stagedSerialization.ts +91 -0
  152. package/frontend-vue/dms.frontend.ts +38 -0
  153. package/frontend-vue/eslint.config.mjs +16 -0
  154. package/frontend-vue/i18n/locales/database-en-GB.json +239 -0
  155. package/frontend-vue/i18n/locales/database-fr-FR.json +239 -0
  156. package/frontend-vue/package.json +42 -0
  157. package/frontend-vue/pnpm-lock.yaml +5819 -0
  158. package/frontend-vue/pnpm-workspace.yaml +2 -0
  159. package/frontend-vue/tests/persistence.test.ts +70 -0
  160. package/frontend-vue/tests/queryRuntime.test.ts +53 -0
  161. package/frontend-vue/tests/registration.test.ts +33 -0
  162. package/frontend-vue/tsconfig.json +13 -0
  163. package/frontend-vue/vitest.config.ts +5 -0
  164. 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>