@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,432 @@
1
+ <script setup lang="ts">
2
+ import { prepareQueryPayload } from "../../composables/useQueryRuntime";
3
+ import type {
4
+ ExecuteResult,
5
+ HistoryEntry,
6
+ QueryLanguage,
7
+ QueryScope,
8
+ SavedQuery,
9
+ } from "../../composables/useQueryStore";
10
+
11
+ const HISTORY_PAGE_SIZE = 5;
12
+ const MUTATION_SKIP_KEY = "dms-db-query.skipMutationWarning";
13
+
14
+ const route = useDmsRoute();
15
+ const router = useDmsRouter();
16
+
17
+ const {
18
+ savedQueries,
19
+ historyItems,
20
+ historyTotal,
21
+ fetchSaved,
22
+ fetchHistory,
23
+ executeQuery,
24
+ saveQuery,
25
+ updateSaved,
26
+ deleteSaved,
27
+ } = useQueryStore();
28
+
29
+ const { schemas: dbSchemas, refresh: refreshSchemas } = useDatabaseSchemas();
30
+
31
+ const DATA_PATH = "/modules/database/data";
32
+
33
+ const queryText = ref<string>("");
34
+ const language = ref<QueryLanguage>("aql");
35
+ const lastResult = ref<ExecuteResult | null>(null);
36
+ const resultMode = ref<"table" | "json" | "chart">("table");
37
+ const executing = ref(false);
38
+
39
+ const savedScope = ref<QueryScope>("me");
40
+ const historyPage = ref(0);
41
+
42
+ const saveModalOpen = ref(false);
43
+ const saveName = ref("");
44
+ const saveDescription = ref("");
45
+ const saveShared = ref(false);
46
+ const saving = ref(false);
47
+ // Non-null while the save modal edits an existing saved query instead of
48
+ // creating a new one. The modal works on its own snapshot of the source and
49
+ // language, so it never clobbers (or depends on) the main editor.
50
+ const editingSaved = ref<SavedQuery | null>(null);
51
+ const saveSource = ref("");
52
+ const saveLanguage = ref<QueryLanguage>("aql");
53
+ const saveError = ref<string | null>(null);
54
+
55
+ const mutationModalOpen = ref(false);
56
+ const mutationDontRemind = ref(false);
57
+ let pendingPayload: { query: Record<string, unknown>; source: string } | null =
58
+ null;
59
+
60
+ async function refreshSaved() {
61
+ try {
62
+ await fetchSaved(savedScope.value);
63
+ } catch {
64
+ savedQueries.value = [];
65
+ }
66
+ }
67
+
68
+ async function refreshHistory() {
69
+ try {
70
+ await fetchHistory(
71
+ HISTORY_PAGE_SIZE,
72
+ historyPage.value * HISTORY_PAGE_SIZE,
73
+ );
74
+ } catch {
75
+ historyItems.value = [];
76
+ historyTotal.value = 0;
77
+ }
78
+ }
79
+
80
+ watch(savedScope, refreshSaved);
81
+ watch(historyPage, refreshHistory);
82
+
83
+ function loadFromSaved(saved: SavedQuery) {
84
+ queryText.value = saved.source;
85
+ language.value = saved.language;
86
+ }
87
+
88
+ function loadFromHistory(entry: HistoryEntry) {
89
+ queryText.value = entry.source;
90
+ language.value = entry.language;
91
+ }
92
+
93
+ function saveFromHistory(entry: HistoryEntry) {
94
+ loadFromHistory(entry);
95
+ openSaveModal();
96
+ }
97
+
98
+ function setError(message: string) {
99
+ lastResult.value = {
100
+ rows: [],
101
+ executedAt: new Date().toISOString(),
102
+ durationMs: 0,
103
+ error: message,
104
+ };
105
+ }
106
+
107
+ function extractFetchError(err: unknown): string {
108
+ const { data, message } = (err ?? {}) as { data?: unknown; message?: unknown };
109
+ if (typeof data === "string" && data.length > 0) return data;
110
+ if (data && typeof data === "object") {
111
+ const inner = (data as { message?: unknown; error?: unknown }).message
112
+ ?? (data as { message?: unknown; error?: unknown }).error;
113
+ if (typeof inner === "string" && inner.length > 0) return inner;
114
+ }
115
+ if (typeof message === "string" && message.length > 0) return message;
116
+ return "Query execution failed";
117
+ }
118
+
119
+ async function runPrepared(query: Record<string, unknown>, source: string) {
120
+ executing.value = true;
121
+ try {
122
+ const result = await executeQuery(query, source, language.value);
123
+ lastResult.value = result;
124
+ await refreshHistory();
125
+ } catch (err) {
126
+ setError(extractFetchError(err));
127
+ } finally {
128
+ executing.value = false;
129
+ }
130
+ }
131
+
132
+ async function onExecute() {
133
+ if (!queryText.value.trim() || executing.value) return;
134
+
135
+ let prepared: ReturnType<typeof prepareQueryPayload>;
136
+ try {
137
+ prepared = prepareQueryPayload(queryText.value, dbSchemas.value);
138
+ } catch (err) {
139
+ setError((err as Error).message);
140
+ return;
141
+ }
142
+
143
+ const skip =
144
+ typeof window !== "undefined" &&
145
+ window.localStorage?.getItem(MUTATION_SKIP_KEY) === "true";
146
+ if (prepared.hasMutations && !skip) {
147
+ pendingPayload = { query: prepared.query, source: prepared.source };
148
+ mutationDontRemind.value = false;
149
+ mutationModalOpen.value = true;
150
+ return;
151
+ }
152
+
153
+ await runPrepared(prepared.query, prepared.source);
154
+ }
155
+
156
+ async function confirmMutation() {
157
+ const payload = pendingPayload;
158
+ pendingPayload = null;
159
+ mutationModalOpen.value = false;
160
+ if (mutationDontRemind.value && typeof window !== "undefined") {
161
+ window.localStorage?.setItem(MUTATION_SKIP_KEY, "true");
162
+ }
163
+ if (payload) {
164
+ await runPrepared(payload.query, payload.source);
165
+ }
166
+ }
167
+
168
+ function cancelMutation() {
169
+ pendingPayload = null;
170
+ mutationModalOpen.value = false;
171
+ }
172
+
173
+ function openSaveModal() {
174
+ editingSaved.value = null;
175
+ saveName.value = "";
176
+ saveDescription.value = "";
177
+ saveShared.value = false;
178
+ // Snapshot the editor state; confirmSave only reads modal refs.
179
+ saveSource.value = queryText.value;
180
+ saveLanguage.value = language.value;
181
+ saveError.value = null;
182
+ saveModalOpen.value = true;
183
+ }
184
+
185
+ // Editing reuses the save modal: open it blank, then overlay the item.
186
+ function openEditModal(item: SavedQuery) {
187
+ openSaveModal();
188
+ editingSaved.value = item;
189
+ saveName.value = item.name;
190
+ saveDescription.value = item.description;
191
+ saveShared.value = item.shared;
192
+ saveSource.value = item.source;
193
+ saveLanguage.value = item.language;
194
+ }
195
+
196
+ async function confirmSave() {
197
+ const name = saveName.value.trim();
198
+ if (!name) return;
199
+
200
+ // A metadata-only edit reuses the stored payload — re-preparing would
201
+ // needlessly re-evaluate the source and fail when the query's schema is
202
+ // no longer registered.
203
+ const editing = editingSaved.value;
204
+ let payload: { query: Record<string, unknown>; source: string };
205
+ if (editing && saveSource.value === editing.source) {
206
+ payload = { query: editing.query, source: editing.source };
207
+ } else {
208
+ try {
209
+ payload = prepareQueryPayload(saveSource.value, dbSchemas.value);
210
+ } catch (err) {
211
+ saveError.value = (err as Error).message;
212
+ return;
213
+ }
214
+ }
215
+
216
+ saving.value = true;
217
+ saveError.value = null;
218
+ try {
219
+ const input = {
220
+ name,
221
+ description: saveDescription.value.trim(),
222
+ query: payload.query,
223
+ source: payload.source,
224
+ language: saveLanguage.value,
225
+ shared: saveShared.value,
226
+ };
227
+ if (editing) {
228
+ await updateSaved(editing.id, input);
229
+ } else {
230
+ await saveQuery(input);
231
+ }
232
+ saveModalOpen.value = false;
233
+ await refreshSaved();
234
+ } catch (err) {
235
+ saveError.value = extractFetchError(err);
236
+ } finally {
237
+ saving.value = false;
238
+ }
239
+ }
240
+
241
+ async function onDeleteSaved(id: string) {
242
+ try {
243
+ await deleteSaved(id);
244
+ await refreshSaved();
245
+ } catch {
246
+ // swallow — re-fetch ensures the UI is consistent
247
+ await refreshSaved();
248
+ }
249
+ }
250
+
251
+ onMounted(async () => {
252
+ await Promise.all([refreshSaved(), refreshHistory(), refreshSchemas()]);
253
+ const queryParam = route.query.q;
254
+ const targetId = typeof queryParam === "string" ? queryParam : null;
255
+ if (targetId) {
256
+ const match = savedQueries.value.find((q) => q.id === targetId);
257
+ if (match) loadFromSaved(match);
258
+ }
259
+ // A raw DSL snippet forwarded from the data browser ("Open in query console")
260
+ // prefills the editor directly.
261
+ const sourceParam = route.query.source;
262
+ const prefill = typeof sourceParam === "string" ? sourceParam : null;
263
+ if (prefill) queryText.value = prefill;
264
+ if (targetId || prefill) {
265
+ router.replace({ query: { ...route.query, q: undefined, source: undefined } });
266
+ }
267
+ });
268
+ </script>
269
+
270
+ <template>
271
+ <div class="space-y-6 pb-6">
272
+ <section class="flex flex-wrap items-center gap-4 pt-6 pb-2">
273
+ <div
274
+ class="rounded-lg bg-primary/10 shadow-sm shrink-0 ring ring-primary/20 flex items-center justify-center size-12"
275
+ >
276
+ <UIcon name="i-ph-code" class="text-primary" size="1.75rem" />
277
+ </div>
278
+ <div class="flex-1 min-w-0">
279
+ <h1 class="text-highlighted font-semibold">
280
+ {{ $t("dms_database.query.title") }}
281
+ </h1>
282
+ <p class="text-muted text-sm">
283
+ {{ $t("dms_database.query.description") }}
284
+ </p>
285
+ </div>
286
+ <UButton
287
+ :to="DATA_PATH"
288
+ color="neutral"
289
+ variant="ghost"
290
+ icon="i-ph-table"
291
+ :label="$t('dms_database.query.backToData')"
292
+ class="ml-auto"
293
+ />
294
+ </section>
295
+
296
+ <div class="grid grid-cols-1 lg:grid-cols-[18rem_1fr] gap-5 items-start">
297
+ <aside class="flex flex-col gap-5 lg:sticky lg:top-6 lg:max-h-[calc(100vh-6rem)]">
298
+ <DmsDatabaseQueryHistoryPanel
299
+ v-model:page="historyPage"
300
+ :items="historyItems"
301
+ :total="historyTotal"
302
+ :page-size="HISTORY_PAGE_SIZE"
303
+ class="flex-1 min-h-0 max-h-96"
304
+ @load="loadFromHistory"
305
+ @save="saveFromHistory"
306
+ />
307
+ <DmsDatabaseQueryFavoritesPanel
308
+ v-model:scope="savedScope"
309
+ :items="savedQueries"
310
+ class="flex-1 min-h-0 max-h-80"
311
+ @load="loadFromSaved"
312
+ @edit="openEditModal"
313
+ @delete="onDeleteSaved"
314
+ />
315
+ </aside>
316
+
317
+ <div class="flex flex-col gap-5 min-w-0">
318
+ <DmsDatabaseQueryBuilderPanel
319
+ v-model="queryText"
320
+ v-model:language="language"
321
+ :executing="executing"
322
+ :schemas="dbSchemas"
323
+ @execute="onExecute"
324
+ @save="openSaveModal"
325
+ />
326
+ <DmsDatabaseQueryResultPanel
327
+ v-model:mode="resultMode"
328
+ :result="lastResult"
329
+ />
330
+ </div>
331
+ </div>
332
+
333
+ <UModal
334
+ v-model:open="saveModalOpen"
335
+ :title="
336
+ editingSaved
337
+ ? $t('dms_database.query.editModal.title')
338
+ : $t('dms_database.query.saveModal.title')
339
+ "
340
+ >
341
+ <template #body>
342
+ <div class="space-y-4">
343
+ <UFormField :label="$t('dms_database.query.saveModal.name')" required>
344
+ <UInput
345
+ v-model="saveName"
346
+ autofocus
347
+ class="w-full"
348
+ :placeholder="$t('dms_database.query.saveModal.namePlaceholder')"
349
+ />
350
+ </UFormField>
351
+ <UFormField :label="$t('dms_database.query.saveModal.description')">
352
+ <UTextarea
353
+ v-model="saveDescription"
354
+ class="w-full"
355
+ :placeholder="$t('dms_database.query.saveModal.descriptionPlaceholder')"
356
+ :rows="3"
357
+ />
358
+ </UFormField>
359
+ <UFormField
360
+ v-if="editingSaved"
361
+ :label="$t('dms_database.query.editModal.query')"
362
+ >
363
+ <UTextarea
364
+ v-model="saveSource"
365
+ class="w-full font-mono"
366
+ :rows="4"
367
+ />
368
+ </UFormField>
369
+ <UCheckbox
370
+ v-model="saveShared"
371
+ :label="$t('dms_database.query.saveModal.share')"
372
+ :description="$t('dms_database.query.saveModal.shareDescription')"
373
+ />
374
+ <UAlert
375
+ v-if="saveError"
376
+ color="error"
377
+ variant="subtle"
378
+ :title="$t('dms_database.query.errorTitle')"
379
+ :description="saveError"
380
+ />
381
+ </div>
382
+ </template>
383
+ <template #footer>
384
+ <div class="flex w-full justify-end gap-2">
385
+ <UButton
386
+ variant="outline"
387
+ color="neutral"
388
+ :disabled="saving"
389
+ @click="saveModalOpen = false"
390
+ >
391
+ {{ $t("dms_database.query.saveModal.cancel") }}
392
+ </UButton>
393
+ <UButton
394
+ color="primary"
395
+ :loading="saving"
396
+ :disabled="!saveName.trim() || saving"
397
+ @click="confirmSave"
398
+ >
399
+ {{ $t("dms_database.query.saveModal.save") }}
400
+ </UButton>
401
+ </div>
402
+ </template>
403
+ </UModal>
404
+
405
+ <UModal
406
+ v-model:open="mutationModalOpen"
407
+ :title="$t('dms_database.query.mutationModal.title')"
408
+ >
409
+ <template #body>
410
+ <div class="space-y-4">
411
+ <p class="text-sm text-muted">
412
+ {{ $t("dms_database.query.mutationModal.body") }}
413
+ </p>
414
+ <UCheckbox
415
+ v-model="mutationDontRemind"
416
+ :label="$t('dms_database.query.mutationModal.dontRemind')"
417
+ />
418
+ </div>
419
+ </template>
420
+ <template #footer>
421
+ <div class="flex w-full justify-end gap-2">
422
+ <UButton variant="outline" color="neutral" @click="cancelMutation">
423
+ {{ $t("dms_database.query.mutationModal.cancel") }}
424
+ </UButton>
425
+ <UButton color="warning" @click="confirmMutation">
426
+ {{ $t("dms_database.query.mutationModal.confirm") }}
427
+ </UButton>
428
+ </div>
429
+ </template>
430
+ </UModal>
431
+ </div>
432
+ </template>
@@ -0,0 +1,216 @@
1
+ <script setup lang="ts">
2
+ import type { TableSummary } from "../../composables/useDatabaseSchemas";
3
+ import SchemaTableInspector from "../../components/SchemaTableInspector.vue";
4
+ import { buildReadonlyColumns } from "../../utils/dmsTableView";
5
+
6
+ const TABLES_LOCATION = "/api/database/tables";
7
+ const COMPONENT_ID = "database-schemas-tables";
8
+ const PAGE_ID = "database.schemas";
9
+
10
+ // One table summary row, as returned by /api/database/tables/list.
11
+ interface TableListRow {
12
+ id: string;
13
+ schema: string;
14
+ name: string;
15
+ columnCount: number;
16
+ indexCount: number;
17
+ relationCount: number;
18
+ elementCount: number;
19
+ }
20
+
21
+ const route = useDmsRoute();
22
+ const { t } = useI18n();
23
+ const { schemas } = useDatabaseSchemas();
24
+
25
+ const fullFormatter = new Intl.NumberFormat("en-US");
26
+
27
+ // --- schema selection (pill) ---
28
+ // Local state (not URL-bound) to avoid re-triggering the global module-routing
29
+ // guard on every selection, which can loop. Deep links via ?schema are honoured
30
+ // once on load; the diagram link carries the schema explicitly.
31
+ const initialSchema = route.query.schema;
32
+ const selectedSchemaId = ref<string | null>(
33
+ typeof initialSchema === "string" && initialSchema.length > 0
34
+ ? initialSchema
35
+ : null,
36
+ );
37
+
38
+ const currentSchema = computed(
39
+ () => schemas.value.find((s) => s.id === selectedSchemaId.value) ?? null,
40
+ );
41
+
42
+ const schemaOptions = computed(() =>
43
+ schemas.value.map((s) => ({ label: s.id, value: s.id })),
44
+ );
45
+
46
+ watch(
47
+ schemas,
48
+ (list) => {
49
+ if (selectedSchemaId.value) return;
50
+ if (list[0]) selectedSchemaId.value = list[0].id;
51
+ },
52
+ { immediate: true },
53
+ );
54
+
55
+ // --- list / diagram view toggle (inline alternate view, no navigation) ---
56
+ // View is local state, diagram by default; ?view=list deep links are honoured
57
+ // once on load without writing back to the URL.
58
+ type SchemaView = "list" | "diagram";
59
+ const view = ref<SchemaView>(route.query.view === "list" ? "list" : "diagram");
60
+ const viewItems = computed(() => [
61
+ { label: t("dms_database.schemas.view.diagram"), value: "diagram" as const, icon: "i-ph-graph" },
62
+ { label: t("dms_database.schemas.view.list"), value: "list" as const, icon: "i-ph-table" },
63
+ ]);
64
+
65
+ // --- tables list (shared DmsTableView, read-only) ---
66
+ const columns = computed(() =>
67
+ buildReadonlyColumns([
68
+ { key: "name", header: t("dms_database.overview.tables.cols.table") },
69
+ { key: "elementCount", header: t("dms_database.overview.tables.cols.rows") },
70
+ { key: "columnCount", header: t("dms_database.overview.tables.cols.columns") },
71
+ { key: "indexCount", header: t("dms_database.overview.tables.cols.indexes") },
72
+ { key: "relationCount", header: t("dms_database.schemas.cols.relations") },
73
+ ]),
74
+ );
75
+
76
+ // Pin the table list to the selected schema. The table is remounted (`:key`) when
77
+ // the schema changes so the filter is rebuilt cleanly.
78
+ const defaultFilters = computed(() =>
79
+ selectedSchemaId.value
80
+ ? [{ accessorKey: "schema", value: selectedSchemaId.value, mode: "is" }]
81
+ : [],
82
+ );
83
+
84
+ const collectionsLabel = computed(() =>
85
+ t(
86
+ "dms_database.schemas.collections",
87
+ { count: currentSchema.value?.tables.length ?? 0 },
88
+ currentSchema.value?.tables.length ?? 0,
89
+ ),
90
+ );
91
+
92
+ // --- inspector (shared dms drawer) ---
93
+ // Opened imperatively through the dms-ui `useDrawer` composable: the shared
94
+ // DynamicDrawer owns the themed surface, the header (table name + subtitle) and
95
+ // the close button; SchemaTableInspector only renders the body (tabs + content).
96
+ const { open: openDrawer } = useDrawer();
97
+
98
+ function inspectorSubtitle(table: TableSummary, rows: number | null): string {
99
+ const parts: string[] = [];
100
+ if (rows != null) {
101
+ parts.push(
102
+ `${fullFormatter.format(rows)} ${t("dms_database.overview.storage.rowsUnit")}`,
103
+ );
104
+ }
105
+ parts.push(
106
+ t("dms_database.data.colCount", {
107
+ count: Object.keys(table.fields ?? {}).length,
108
+ }),
109
+ );
110
+ return parts.join(" · ");
111
+ }
112
+
113
+ function openInspectorForTable(table: TableSummary, rows: number | null) {
114
+ // `useDrawer` (dms-ui ≥ 0.1.3) defers the actual open until any closing
115
+ // row-actions menu has left the DOM, so opening from the "View details"
116
+ // dropdown no longer flashes the drawer in and out — no local wait needed.
117
+ // The schema id is captured here (at call time) into componentOptions rather
118
+ // than read lazily: the open may still be deferred while the menu closes, and
119
+ // the schema pill could change in that window.
120
+ openDrawer({
121
+ title: table.name,
122
+ description: inspectorSubtitle(table, rows),
123
+ direction: "right",
124
+ component: SchemaTableInspector,
125
+ componentOptions: {
126
+ schemaId: selectedSchemaId.value,
127
+ table,
128
+ },
129
+ });
130
+ }
131
+
132
+ function openInspector(row: TableListRow) {
133
+ // The list row carries display counts; the full descriptor (fields/indexes/
134
+ // relations) comes from the schema introspection the inspector needs.
135
+ const table = currentSchema.value?.tables.find((tbl) => tbl.name === row.name);
136
+ if (!table) return;
137
+ openInspectorForTable(table, row.elementCount ?? null);
138
+ }
139
+
140
+ // Double-clicking a table node in the diagram opens the same inspector drawer.
141
+ function openInspectorByName(tableName: string) {
142
+ const table = currentSchema.value?.tables.find((tbl) => tbl.name === tableName);
143
+ if (!table) return;
144
+ openInspectorForTable(table, null);
145
+ }
146
+
147
+ // A row (double-)click opens the table inspector drawer.
148
+ useTableViewRowClick<TableListRow>(COMPONENT_ID, openInspector);
149
+ </script>
150
+
151
+ <template>
152
+ <div class="space-y-5 pb-6">
153
+ <section class="flex flex-wrap items-center gap-4 pt-6 pb-2">
154
+ <div
155
+ class="rounded-lg bg-primary/10 shadow-sm shrink-0 ring ring-primary/20 flex items-center justify-center size-12"
156
+ >
157
+ <UIcon name="i-ph-tree-structure" class="text-primary" size="1.75rem" />
158
+ </div>
159
+ <div class="flex-1 min-w-0">
160
+ <h1 class="text-highlighted font-semibold">
161
+ {{ $t("dms_database.schemas.title") }}
162
+ </h1>
163
+ <p class="text-muted text-sm">
164
+ {{ $t("dms_database.schemas.description") }}
165
+ </p>
166
+ </div>
167
+ <div class="flex items-center gap-3 ml-auto">
168
+ <USelect
169
+ v-model="selectedSchemaId"
170
+ :items="schemaOptions"
171
+ icon="i-ph-database"
172
+ :placeholder="$t('dms_database.data.schema')"
173
+ class="min-w-40"
174
+ />
175
+ <DmsSegmented
176
+ v-model="view"
177
+ :items="viewItems"
178
+ :aria-label="$t('dms_database.schemas.view.list')"
179
+ />
180
+ </div>
181
+ </section>
182
+
183
+ <div v-show="view === 'list'" class="space-y-2">
184
+ <p class="text-dimmed px-1 text-xs">
185
+ <span class="font-mono">{{ selectedSchemaId }}</span>
186
+ · {{ collectionsLabel }}
187
+ </p>
188
+ <DmsTableView
189
+ v-if="selectedSchemaId"
190
+ :key="selectedSchemaId"
191
+ :location="TABLES_LOCATION"
192
+ :columns="columns"
193
+ :form-components="{}"
194
+ :form-container="{ type: 'drawer' }"
195
+ :row-actions="{ details: true }"
196
+ :default-filters="defaultFilters"
197
+ :default-sort="{ field: 'elementCount', desc: true }"
198
+ :component-id="COMPONENT_ID"
199
+ :page-id="PAGE_ID"
200
+ row-id-key="id"
201
+ />
202
+ </div>
203
+
204
+ <!-- ClientOnly: vue-flow is not SSR-safe. Kept mounted (v-show) so toggling
205
+ List/Diagram preserves un-applied diagram edits and avoids a blocking
206
+ confirm dialog on every switch. -->
207
+ <ClientOnly>
208
+ <DmsDatabaseSchemaDiagram
209
+ v-show="view === 'diagram'"
210
+ :schema-id="selectedSchemaId"
211
+ :active="view === 'diagram'"
212
+ @inspect-table="openInspectorByName"
213
+ />
214
+ </ClientOnly>
215
+ </div>
216
+ </template>
@@ -0,0 +1,33 @@
1
+ export interface ModifierIcon {
2
+ icon: string;
3
+ color: string;
4
+ label: string;
5
+ }
6
+
7
+ const MODIFIER_ICONS: Record<string, ModifierIcon> = {
8
+ HashModifier: {
9
+ icon: "i-ph-fingerprint",
10
+ color: "text-rose-500",
11
+ label: "Hashed",
12
+ },
13
+ LocalizationModifier: {
14
+ icon: "i-ph-translate",
15
+ color: "text-cyan-500",
16
+ label: "Localized",
17
+ },
18
+ EncryptionModifier: {
19
+ icon: "i-ph-lock-key",
20
+ color: "text-indigo-500",
21
+ label: "Encrypted",
22
+ },
23
+ };
24
+
25
+ const fallbackModifierIcon = (id: string): ModifierIcon => ({
26
+ icon: "i-ph-question",
27
+ color: "text-muted",
28
+ label: id,
29
+ });
30
+
31
+ export function resolveModifierIcon(modifierId: string): ModifierIcon {
32
+ return MODIFIER_ICONS[modifierId] ?? fallbackModifierIcon(modifierId);
33
+ }