@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,147 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
value: unknown;
|
|
4
|
+
editable: boolean;
|
|
5
|
+
editing: boolean;
|
|
6
|
+
pending: boolean;
|
|
7
|
+
invalid: boolean;
|
|
8
|
+
focused: boolean;
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
select: [];
|
|
13
|
+
"start-edit": [];
|
|
14
|
+
commit: [draft: string | boolean, source: "enter" | "blur"];
|
|
15
|
+
cancel: [];
|
|
16
|
+
"open-json": [];
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const isJson = computed(
|
|
20
|
+
() => props.value !== null && props.value !== undefined && typeof props.value === "object",
|
|
21
|
+
);
|
|
22
|
+
const isNull = computed(() => props.value === null || props.value === undefined);
|
|
23
|
+
|
|
24
|
+
const display = computed(() => {
|
|
25
|
+
if (isNull.value) return "";
|
|
26
|
+
if (isJson.value) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(props.value);
|
|
29
|
+
} catch {
|
|
30
|
+
return String(props.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return String(props.value);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// --- inline editing ---
|
|
37
|
+
const draft = ref("");
|
|
38
|
+
const boolDraft = ref(false);
|
|
39
|
+
const cellInput = useTemplateRef<HTMLInputElement | HTMLSelectElement>("cellInput");
|
|
40
|
+
// Set before blurring on Escape so the blur-commit does not fire after a cancel.
|
|
41
|
+
const cancelling = ref(false);
|
|
42
|
+
|
|
43
|
+
watch(
|
|
44
|
+
() => props.editing,
|
|
45
|
+
(editing) => {
|
|
46
|
+
if (!editing) return;
|
|
47
|
+
cancelling.value = false;
|
|
48
|
+
if (typeof props.value === "boolean") {
|
|
49
|
+
boolDraft.value = props.value;
|
|
50
|
+
} else {
|
|
51
|
+
draft.value = isNull.value ? "" : String(props.value);
|
|
52
|
+
}
|
|
53
|
+
nextTick(() => {
|
|
54
|
+
cellInput.value?.focus();
|
|
55
|
+
if (cellInput.value instanceof HTMLInputElement) cellInput.value.select();
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// The parent needs the trigger: Enter keeps an invalid draft open for fixing,
|
|
61
|
+
// while a blur (click-away) discards it so the grid never holds an unfocused
|
|
62
|
+
// editor hostage.
|
|
63
|
+
function commit(source: "enter" | "blur") {
|
|
64
|
+
if (cancelling.value) return;
|
|
65
|
+
emit(
|
|
66
|
+
"commit",
|
|
67
|
+
typeof props.value === "boolean" ? boolDraft.value : draft.value,
|
|
68
|
+
source,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cancel() {
|
|
73
|
+
cancelling.value = true;
|
|
74
|
+
emit("cancel");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onDoubleClick() {
|
|
78
|
+
if (isJson.value) emit("open-json");
|
|
79
|
+
else if (props.editable) emit("start-edit");
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<td
|
|
85
|
+
class="relative cursor-default border-r border-b border-default p-0 align-middle"
|
|
86
|
+
:class="[
|
|
87
|
+
invalid
|
|
88
|
+
? 'ring-2 ring-error ring-inset'
|
|
89
|
+
: focused
|
|
90
|
+
? 'ring-2 ring-primary ring-inset'
|
|
91
|
+
: '',
|
|
92
|
+
editing ? 'bg-primary/5' : '',
|
|
93
|
+
]"
|
|
94
|
+
@click="emit('select')"
|
|
95
|
+
@dblclick="onDoubleClick"
|
|
96
|
+
>
|
|
97
|
+
<template v-if="editing">
|
|
98
|
+
<select
|
|
99
|
+
v-if="typeof value === 'boolean'"
|
|
100
|
+
ref="cellInput"
|
|
101
|
+
v-model="boolDraft"
|
|
102
|
+
class="w-full bg-default px-2 py-1 font-mono text-[12.5px] outline-none"
|
|
103
|
+
@change="commit('enter')"
|
|
104
|
+
@keydown.esc.stop="cancel"
|
|
105
|
+
@keydown.stop
|
|
106
|
+
@blur="commit('blur')"
|
|
107
|
+
>
|
|
108
|
+
<option :value="true">true</option>
|
|
109
|
+
<option :value="false">false</option>
|
|
110
|
+
</select>
|
|
111
|
+
<input
|
|
112
|
+
v-else
|
|
113
|
+
ref="cellInput"
|
|
114
|
+
v-model="draft"
|
|
115
|
+
type="text"
|
|
116
|
+
class="w-full bg-default px-2 py-1 font-mono text-[12.5px] outline-none"
|
|
117
|
+
@keydown.enter.prevent="commit('enter')"
|
|
118
|
+
@keydown.esc.stop="cancel"
|
|
119
|
+
@keydown.stop
|
|
120
|
+
@blur="commit('blur')"
|
|
121
|
+
>
|
|
122
|
+
</template>
|
|
123
|
+
<div
|
|
124
|
+
v-else
|
|
125
|
+
class="flex items-center gap-1 overflow-hidden px-2.5 py-1 whitespace-nowrap"
|
|
126
|
+
>
|
|
127
|
+
<span
|
|
128
|
+
v-if="isNull"
|
|
129
|
+
class="rounded bg-elevated px-1 font-mono text-[10px] tracking-wide text-dimmed uppercase"
|
|
130
|
+
>
|
|
131
|
+
null
|
|
132
|
+
</span>
|
|
133
|
+
<span
|
|
134
|
+
v-else
|
|
135
|
+
class="overflow-hidden font-mono text-[12.5px] text-ellipsis"
|
|
136
|
+
:class="isJson ? 'cursor-pointer text-info' : 'text-toned'"
|
|
137
|
+
>
|
|
138
|
+
{{ display }}
|
|
139
|
+
</span>
|
|
140
|
+
<UIcon
|
|
141
|
+
v-if="pending"
|
|
142
|
+
name="i-ph-circle-notch"
|
|
143
|
+
class="size-3 shrink-0 animate-spin text-primary"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</td>
|
|
147
|
+
</template>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ColumnFilterState, GridColumn } from "../composables/useDataBrowserGrid";
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
column: GridColumn;
|
|
6
|
+
sortKey: string | null;
|
|
7
|
+
sortDirection: "asc" | "desc";
|
|
8
|
+
filter: ColumnFilterState | null;
|
|
9
|
+
width: number;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
sort: [];
|
|
14
|
+
filter: [value: ColumnFilterState | null];
|
|
15
|
+
resize: [width: number];
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
|
|
20
|
+
// Shared icon set (utils/databaseFieldTypeIcons) so runtime-sampled columns
|
|
21
|
+
// carry the same glyph, color and named-field overrides as the inspector.
|
|
22
|
+
const typeIcon = computed(() =>
|
|
23
|
+
runtimeTypeIcon(props.column.type, props.column.name),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const sortIcon = computed(() => {
|
|
27
|
+
if (props.sortKey !== props.column.name) return null;
|
|
28
|
+
return props.sortDirection === "asc" ? "i-ph-caret-up" : "i-ph-caret-down";
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// --- filter popover ---
|
|
32
|
+
const filterOpen = ref(false);
|
|
33
|
+
const draftMode = ref<"is" | "contains">("contains");
|
|
34
|
+
const draftValue = ref("");
|
|
35
|
+
|
|
36
|
+
watch(filterOpen, (open) => {
|
|
37
|
+
if (!open) return;
|
|
38
|
+
draftMode.value = props.filter?.mode ?? "contains";
|
|
39
|
+
draftValue.value = props.filter?.value ?? "";
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const modeOptions = computed(() => [
|
|
43
|
+
{ label: t("dms_database.data.filterModeContains"), value: "contains" },
|
|
44
|
+
{ label: t("dms_database.data.filterModeIs"), value: "is" },
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
function applyFilter() {
|
|
48
|
+
const value = draftValue.value.trim();
|
|
49
|
+
emit("filter", value ? { mode: draftMode.value, value } : null);
|
|
50
|
+
filterOpen.value = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearFilter() {
|
|
54
|
+
emit("filter", null);
|
|
55
|
+
filterOpen.value = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- column resize (drag the right edge) ---
|
|
59
|
+
const MIN_WIDTH = 80;
|
|
60
|
+
const MAX_WIDTH = 640;
|
|
61
|
+
|
|
62
|
+
// Emits are rAF-throttled (one width update per frame, not per mousemove) and
|
|
63
|
+
// the window listeners are detached if the header unmounts mid-drag.
|
|
64
|
+
let resizeFrame = 0;
|
|
65
|
+
let detachResize: (() => void) | null = null;
|
|
66
|
+
|
|
67
|
+
function startResize(event: MouseEvent) {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
const startX = event.clientX;
|
|
70
|
+
const startWidth = props.width;
|
|
71
|
+
function onMove(move: MouseEvent) {
|
|
72
|
+
cancelAnimationFrame(resizeFrame);
|
|
73
|
+
resizeFrame = requestAnimationFrame(() => {
|
|
74
|
+
const next = Math.min(
|
|
75
|
+
MAX_WIDTH,
|
|
76
|
+
Math.max(MIN_WIDTH, startWidth + (move.clientX - startX)),
|
|
77
|
+
);
|
|
78
|
+
emit("resize", next);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function onUp() {
|
|
82
|
+
detachResize?.();
|
|
83
|
+
}
|
|
84
|
+
detachResize = () => {
|
|
85
|
+
cancelAnimationFrame(resizeFrame);
|
|
86
|
+
window.removeEventListener("mousemove", onMove);
|
|
87
|
+
window.removeEventListener("mouseup", onUp);
|
|
88
|
+
detachResize = null;
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener("mousemove", onMove);
|
|
91
|
+
window.addEventListener("mouseup", onUp);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onBeforeUnmount(() => detachResize?.());
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<template>
|
|
98
|
+
<th
|
|
99
|
+
class="group relative select-none border-r border-b border-default bg-elevated p-0 text-left font-medium"
|
|
100
|
+
scope="col"
|
|
101
|
+
>
|
|
102
|
+
<div class="flex items-center gap-1.5 px-2.5 py-1.5">
|
|
103
|
+
<UIcon
|
|
104
|
+
v-if="column.isPrimaryKey"
|
|
105
|
+
name="i-ph-key"
|
|
106
|
+
class="size-3 shrink-0 text-amber-500"
|
|
107
|
+
/>
|
|
108
|
+
<UIcon
|
|
109
|
+
v-else
|
|
110
|
+
:name="typeIcon.icon"
|
|
111
|
+
class="size-3 shrink-0"
|
|
112
|
+
:class="typeIcon.color"
|
|
113
|
+
:title="typeIcon.label"
|
|
114
|
+
/>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class="min-w-0 flex-1 truncate text-left font-mono text-xs text-toned hover:text-highlighted"
|
|
118
|
+
@click="emit('sort')"
|
|
119
|
+
>
|
|
120
|
+
{{ column.name }}
|
|
121
|
+
</button>
|
|
122
|
+
<UIcon v-if="sortIcon" :name="sortIcon" class="size-3 shrink-0 text-primary" />
|
|
123
|
+
<!--
|
|
124
|
+
Data filters on columns named after the wire protocol's selection
|
|
125
|
+
params (schema / table / instance) are silently dropped server-side,
|
|
126
|
+
so don't offer the filter button on those columns at all.
|
|
127
|
+
-->
|
|
128
|
+
<UPopover v-if="!isReservedFilterField(column.name)" v-model:open="filterOpen">
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
class="rounded p-0.5 transition-opacity"
|
|
132
|
+
:class="
|
|
133
|
+
filter
|
|
134
|
+
? 'text-primary opacity-100'
|
|
135
|
+
: 'text-dimmed opacity-0 group-hover:opacity-100'
|
|
136
|
+
"
|
|
137
|
+
:aria-label="t('dms_database.data.filterTitle', { column: column.name })"
|
|
138
|
+
>
|
|
139
|
+
<UIcon name="i-ph-funnel" class="size-3" />
|
|
140
|
+
</button>
|
|
141
|
+
<template #content>
|
|
142
|
+
<div class="w-56 space-y-2 p-3">
|
|
143
|
+
<p class="font-mono text-xs text-muted">{{ column.name }}</p>
|
|
144
|
+
<USelect v-model="draftMode" :items="modeOptions" size="xs" class="w-full" />
|
|
145
|
+
<UInput
|
|
146
|
+
v-model="draftValue"
|
|
147
|
+
size="xs"
|
|
148
|
+
class="w-full"
|
|
149
|
+
autofocus
|
|
150
|
+
@keydown.enter="applyFilter"
|
|
151
|
+
/>
|
|
152
|
+
<div class="flex justify-end gap-1.5">
|
|
153
|
+
<UButton
|
|
154
|
+
size="xs"
|
|
155
|
+
variant="ghost"
|
|
156
|
+
color="neutral"
|
|
157
|
+
:label="t('dms_database.data.filterClear')"
|
|
158
|
+
@click="clearFilter"
|
|
159
|
+
/>
|
|
160
|
+
<UButton
|
|
161
|
+
size="xs"
|
|
162
|
+
:label="t('dms_database.data.filterApply')"
|
|
163
|
+
@click="applyFilter"
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
</UPopover>
|
|
169
|
+
</div>
|
|
170
|
+
<span
|
|
171
|
+
class="absolute top-0 right-0 z-10 h-full w-1 cursor-col-resize hover:bg-primary/40"
|
|
172
|
+
@mousedown="startResize"
|
|
173
|
+
/>
|
|
174
|
+
</th>
|
|
175
|
+
</template>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface NoteNodeData {
|
|
3
|
+
noteId: string;
|
|
4
|
+
text: string;
|
|
5
|
+
color: string | null;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
onTextChange: (noteId: string, text: string) => void;
|
|
9
|
+
onDelete: (noteId: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{ data: NoteNodeData; selected: boolean }>();
|
|
13
|
+
|
|
14
|
+
// Sticky-note palette (amber). Centralised so the note frame, header and ink
|
|
15
|
+
// stay in sync instead of being repeated as literals across the template.
|
|
16
|
+
const NOTE_DEFAULT_BG = "#fef9c3";
|
|
17
|
+
const NOTE_BORDER = "rgba(202, 138, 4, 0.45)";
|
|
18
|
+
const NOTE_HEADER_BG = "rgba(250, 204, 21, 0.5)";
|
|
19
|
+
const NOTE_HEADER_BORDER = "rgba(202, 138, 4, 0.35)";
|
|
20
|
+
const NOTE_ACCENT = "#92400e";
|
|
21
|
+
const NOTE_INK = "#422006";
|
|
22
|
+
const NOTE_PLACEHOLDER_INK = "rgba(66, 32, 6, 0.45)";
|
|
23
|
+
|
|
24
|
+
const editing = ref(false);
|
|
25
|
+
const textBuffer = ref(props.data.text);
|
|
26
|
+
|
|
27
|
+
watch(
|
|
28
|
+
() => props.data.text,
|
|
29
|
+
(v) => {
|
|
30
|
+
if (!editing.value) textBuffer.value = v;
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
function commitText() {
|
|
35
|
+
editing.value = false;
|
|
36
|
+
const next = textBuffer.value;
|
|
37
|
+
if (next !== props.data.text) {
|
|
38
|
+
props.data.onTextChange(props.data.noteId, next);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<div
|
|
45
|
+
class="rounded-lg shadow-md border relative overflow-hidden flex flex-col"
|
|
46
|
+
:class="{ 'ring-2 ring-primary': selected }"
|
|
47
|
+
:style="{
|
|
48
|
+
background: data.color ?? NOTE_DEFAULT_BG,
|
|
49
|
+
borderColor: NOTE_BORDER,
|
|
50
|
+
width: `${data.width}px`,
|
|
51
|
+
height: `${data.height}px`,
|
|
52
|
+
}"
|
|
53
|
+
@dblclick="editing = true"
|
|
54
|
+
>
|
|
55
|
+
<!-- Header strip: gives the note a recognisable sticky-note frame and
|
|
56
|
+
hosts the delete affordance without overlapping the text. -->
|
|
57
|
+
<div
|
|
58
|
+
class="flex items-center justify-between h-6 px-2 shrink-0 cursor-grab"
|
|
59
|
+
:style="{
|
|
60
|
+
background: NOTE_HEADER_BG,
|
|
61
|
+
borderBottom: `1px solid ${NOTE_HEADER_BORDER}`,
|
|
62
|
+
}"
|
|
63
|
+
>
|
|
64
|
+
<UIcon
|
|
65
|
+
name="i-ph-note-pencil"
|
|
66
|
+
class="size-3.5"
|
|
67
|
+
:style="{ color: NOTE_ACCENT }"
|
|
68
|
+
/>
|
|
69
|
+
<button
|
|
70
|
+
v-if="selected"
|
|
71
|
+
type="button"
|
|
72
|
+
class="p-0.5 rounded hover:bg-black/10"
|
|
73
|
+
:style="{ color: NOTE_ACCENT }"
|
|
74
|
+
:aria-label="$t('dms_database.diagram.notes.delete')"
|
|
75
|
+
@click.stop="data.onDelete(data.noteId)"
|
|
76
|
+
>
|
|
77
|
+
<UIcon name="i-ph-x" class="size-3.5" />
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
<!-- Body. Fixed dark ink so the text stays readable on the light note in
|
|
81
|
+
any app theme (the previous theme-aware text colour washed out). -->
|
|
82
|
+
<textarea
|
|
83
|
+
v-if="editing"
|
|
84
|
+
v-model="textBuffer"
|
|
85
|
+
class="flex-1 w-full bg-transparent text-sm p-3 resize-none outline-none"
|
|
86
|
+
:style="{ color: NOTE_INK }"
|
|
87
|
+
:placeholder="$t('dms_database.diagram.notes.placeholder')"
|
|
88
|
+
autofocus
|
|
89
|
+
@blur="commitText"
|
|
90
|
+
@keydown.escape.prevent="commitText"
|
|
91
|
+
/>
|
|
92
|
+
<div
|
|
93
|
+
v-else
|
|
94
|
+
class="flex-1 w-full p-3 text-sm whitespace-pre-wrap break-words cursor-text overflow-auto"
|
|
95
|
+
:style="{ color: data.text ? NOTE_INK : NOTE_PLACEHOLDER_INK }"
|
|
96
|
+
>
|
|
97
|
+
{{ data.text || $t("dms_database.diagram.notes.placeholder") }}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { BaseEdge, type EdgeProps, Position, getBezierPath } from "@vue-flow/core";
|
|
3
|
+
|
|
4
|
+
const props = defineProps<EdgeProps>();
|
|
5
|
+
|
|
6
|
+
const ARC_MIN_OFFSET = 60;
|
|
7
|
+
const ARC_DY_FACTOR = 0.35;
|
|
8
|
+
|
|
9
|
+
function sideSign(position: Position): 1 | -1 | 0 {
|
|
10
|
+
if (position === Position.Right) return 1;
|
|
11
|
+
if (position === Position.Left) return -1;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const computedPath = computed<[string, number, number]>(() => {
|
|
16
|
+
const sourceSign = sideSign(props.sourcePosition);
|
|
17
|
+
const targetSign = sideSign(props.targetPosition);
|
|
18
|
+
if (sourceSign !== 0 && targetSign !== 0) {
|
|
19
|
+
const dy = Math.abs(props.targetY - props.sourceY);
|
|
20
|
+
const offset = Math.max(ARC_MIN_OFFSET, dy * ARC_DY_FACTOR);
|
|
21
|
+
const cx1 = props.sourceX + sourceSign * offset;
|
|
22
|
+
const cy1 = props.sourceY;
|
|
23
|
+
const cx2 = props.targetX + targetSign * offset;
|
|
24
|
+
const cy2 = props.targetY;
|
|
25
|
+
const d = `M${props.sourceX},${props.sourceY} C${cx1},${cy1} ${cx2},${cy2} ${props.targetX},${props.targetY}`;
|
|
26
|
+
// Label sits at the bezier midpoint (t = 0.5).
|
|
27
|
+
const labelX =
|
|
28
|
+
(props.sourceX + 3 * cx1 + 3 * cx2 + props.targetX) / 8;
|
|
29
|
+
const labelY =
|
|
30
|
+
(props.sourceY + 3 * cy1 + 3 * cy2 + props.targetY) / 8;
|
|
31
|
+
return [d, labelX, labelY];
|
|
32
|
+
}
|
|
33
|
+
const [d, lx, ly] = getBezierPath({
|
|
34
|
+
sourceX: props.sourceX,
|
|
35
|
+
sourceY: props.sourceY,
|
|
36
|
+
sourcePosition: props.sourcePosition,
|
|
37
|
+
targetX: props.targetX,
|
|
38
|
+
targetY: props.targetY,
|
|
39
|
+
targetPosition: props.targetPosition,
|
|
40
|
+
});
|
|
41
|
+
return [d, lx, ly];
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<BaseEdge
|
|
47
|
+
:id="id"
|
|
48
|
+
:path="computedPath[0]"
|
|
49
|
+
:marker-end="markerEnd"
|
|
50
|
+
:marker-start="markerStart"
|
|
51
|
+
:style="style"
|
|
52
|
+
:label="label"
|
|
53
|
+
:label-x="computedPath[1]"
|
|
54
|
+
:label-y="computedPath[2]"
|
|
55
|
+
:label-style="labelStyle"
|
|
56
|
+
:label-show-bg="labelShowBg"
|
|
57
|
+
:label-bg-style="labelBgStyle"
|
|
58
|
+
:label-bg-padding="labelBgPadding"
|
|
59
|
+
:label-bg-border-radius="labelBgBorderRadius"
|
|
60
|
+
:interaction-width="interactionWidth"
|
|
61
|
+
/>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Handle, Position } from "@vue-flow/core";
|
|
3
|
+
|
|
4
|
+
interface StubNodeData {
|
|
5
|
+
targetSchema: string;
|
|
6
|
+
targetTable: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
defineProps<{ data: StubNodeData }>();
|
|
10
|
+
|
|
11
|
+
const route = useDmsRoute();
|
|
12
|
+
const router = useDmsRouter();
|
|
13
|
+
|
|
14
|
+
function jumpToSchema(schemaId: string) {
|
|
15
|
+
router.replace({ query: { ...route.query, schema: schemaId } });
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="diagram-stub-node flex items-center gap-1 px-2 py-1 rounded border border-dashed border-default bg-elevated text-xs text-muted hover:text-highlighted hover:border-primary transition-colors opacity-70 relative"
|
|
23
|
+
@click="jumpToSchema(data.targetSchema)"
|
|
24
|
+
>
|
|
25
|
+
<Handle id="stub::left::target" type="target" :position="Position.Left" />
|
|
26
|
+
<Handle id="stub::right::target" type="target" :position="Position.Right" />
|
|
27
|
+
<UIcon name="i-ph-arrow-right" />
|
|
28
|
+
<span class="font-mono">{{ data.targetSchema }}.{{ data.targetTable }}</span>
|
|
29
|
+
</button>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
.diagram-stub-node :deep(.vue-flow__handle) {
|
|
34
|
+
opacity: 0;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Handle, Position } from "@vue-flow/core";
|
|
3
|
+
|
|
4
|
+
interface NodeData {
|
|
5
|
+
tableName: string;
|
|
6
|
+
fields: Record<string, unknown>;
|
|
7
|
+
indexes: Record<string, { fields?: string[]; multi?: boolean }>;
|
|
8
|
+
modifiers: Record<string, string[]>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
defineProps<{ data: NodeData }>();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="diagram-table-node bg-elevated border border-default rounded-lg w-60 shadow-md">
|
|
16
|
+
<!-- Fallback handles on the node body for edges that reference a non-existent field -->
|
|
17
|
+
<Handle id="__node__::left::source" type="source" :position="Position.Left" />
|
|
18
|
+
<Handle id="__node__::left::target" type="target" :position="Position.Left" />
|
|
19
|
+
<Handle id="__node__::right::source" type="source" :position="Position.Right" />
|
|
20
|
+
<Handle id="__node__::right::target" type="target" :position="Position.Right" />
|
|
21
|
+
|
|
22
|
+
<div class="px-3 py-2 border-b border-default flex items-center gap-2">
|
|
23
|
+
<UIcon name="i-ph-table" />
|
|
24
|
+
<span class="font-mono text-sm">{{ data.tableName }}</span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<ul class="py-1">
|
|
28
|
+
<li
|
|
29
|
+
v-for="(fieldType, fieldName) in data.fields"
|
|
30
|
+
:key="fieldName"
|
|
31
|
+
class="relative flex items-center justify-between px-3 py-1 text-sm"
|
|
32
|
+
>
|
|
33
|
+
<Handle
|
|
34
|
+
:id="`${fieldName}::left::source`"
|
|
35
|
+
type="source"
|
|
36
|
+
:position="Position.Left"
|
|
37
|
+
/>
|
|
38
|
+
<Handle
|
|
39
|
+
:id="`${fieldName}::left::target`"
|
|
40
|
+
type="target"
|
|
41
|
+
:position="Position.Left"
|
|
42
|
+
/>
|
|
43
|
+
<Handle
|
|
44
|
+
:id="`${fieldName}::right::source`"
|
|
45
|
+
type="source"
|
|
46
|
+
:position="Position.Right"
|
|
47
|
+
/>
|
|
48
|
+
<Handle
|
|
49
|
+
:id="`${fieldName}::right::target`"
|
|
50
|
+
type="target"
|
|
51
|
+
:position="Position.Right"
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<span class="flex items-center gap-0.5 font-mono min-w-0">
|
|
55
|
+
<UTooltip
|
|
56
|
+
v-if="isPrimaryKey(String(fieldName), data.indexes)"
|
|
57
|
+
text="Primary key"
|
|
58
|
+
>
|
|
59
|
+
<UIcon name="i-ph-key" class="text-amber-500 size-4 shrink-0" />
|
|
60
|
+
</UTooltip>
|
|
61
|
+
<template
|
|
62
|
+
v-for="modifierId in data.modifiers?.[String(fieldName)] ?? []"
|
|
63
|
+
:key="modifierId"
|
|
64
|
+
>
|
|
65
|
+
<UTooltip :text="resolveModifierIcon(modifierId).label">
|
|
66
|
+
<UIcon
|
|
67
|
+
:name="resolveModifierIcon(modifierId).icon"
|
|
68
|
+
:class="[resolveModifierIcon(modifierId).color, 'size-4 shrink-0']"
|
|
69
|
+
/>
|
|
70
|
+
</UTooltip>
|
|
71
|
+
</template>
|
|
72
|
+
<span class="truncate ml-1">{{ fieldName }}</span>
|
|
73
|
+
</span>
|
|
74
|
+
|
|
75
|
+
<UTooltip :text="inferFieldTypeIcon(fieldType, String(fieldName)).label">
|
|
76
|
+
<UIcon
|
|
77
|
+
:name="inferFieldTypeIcon(fieldType, String(fieldName)).icon"
|
|
78
|
+
:class="[
|
|
79
|
+
inferFieldTypeIcon(fieldType, String(fieldName)).color,
|
|
80
|
+
'size-4 shrink-0',
|
|
81
|
+
]"
|
|
82
|
+
/>
|
|
83
|
+
</UTooltip>
|
|
84
|
+
</li>
|
|
85
|
+
</ul>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<style scoped>
|
|
90
|
+
.diagram-table-node :deep(.vue-flow__handle) {
|
|
91
|
+
opacity: 0;
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
}
|
|
94
|
+
</style>
|