@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,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
|
+
}
|