@antelopejs/dms-database 0.0.13 → 0.1.0

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 (173) hide show
  1. package/README.md +30 -5
  2. package/dist/db/models/query_history.model.d.ts +1 -0
  3. package/dist/db/models/query_history.model.js +6 -0
  4. package/dist/db/models/query_history.model.js.map +1 -1
  5. package/dist/db/tables/query_history.table.d.ts +3 -0
  6. package/dist/db/tables/query_history.table.js +12 -0
  7. package/dist/db/tables/query_history.table.js.map +1 -1
  8. package/dist/db/tables/saved_queries.table.d.ts +1 -0
  9. package/dist/db/tables/saved_queries.table.js +4 -0
  10. package/dist/db/tables/saved_queries.table.js.map +1 -1
  11. package/dist/i18n/messages.d.ts +39 -0
  12. package/dist/i18n/messages.js +64 -0
  13. package/dist/i18n/messages.js.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.d.ts +11 -0
  18. package/dist/module.js +55 -0
  19. package/dist/module.js.map +1 -0
  20. package/dist/pages/data.d.ts +1 -1
  21. package/dist/pages/data.js +9 -6
  22. package/dist/pages/data.js.map +1 -1
  23. package/dist/pages/diagram.d.ts +9 -0
  24. package/dist/pages/diagram.js +45 -0
  25. package/dist/pages/diagram.js.map +1 -0
  26. package/dist/pages/index.d.ts +1 -0
  27. package/dist/pages/index.js +1 -0
  28. package/dist/pages/index.js.map +1 -1
  29. package/dist/pages/overview.d.ts +4 -1
  30. package/dist/pages/overview.js +113 -6
  31. package/dist/pages/overview.js.map +1 -1
  32. package/dist/pages/query.d.ts +1 -1
  33. package/dist/pages/query.js +22 -6
  34. package/dist/pages/query.js.map +1 -1
  35. package/dist/pages/schemas.d.ts +2 -1
  36. package/dist/pages/schemas.js +143 -6
  37. package/dist/pages/schemas.js.map +1 -1
  38. package/dist/routes/data.d.ts +8 -0
  39. package/dist/routes/data.js +84 -21
  40. package/dist/routes/data.js.map +1 -1
  41. package/dist/routes/index.d.ts +1 -0
  42. package/dist/routes/index.js +1 -0
  43. package/dist/routes/index.js.map +1 -1
  44. package/dist/routes/overview.d.ts +12 -0
  45. package/dist/routes/overview.js +55 -0
  46. package/dist/routes/overview.js.map +1 -0
  47. package/dist/routes/query.d.ts +5 -1
  48. package/dist/routes/query.js +162 -27
  49. package/dist/routes/query.js.map +1 -1
  50. package/dist/routes/tables.d.ts +15 -12
  51. package/dist/routes/tables.js +95 -55
  52. package/dist/routes/tables.js.map +1 -1
  53. package/dist/service/catalog.d.ts +3 -0
  54. package/dist/service/catalog.js +26 -0
  55. package/dist/service/catalog.js.map +1 -0
  56. package/dist/service/health.d.ts +3 -3
  57. package/dist/service/health.js +17 -14
  58. package/dist/service/health.js.map +1 -1
  59. package/dist/service/introspect.d.ts +3 -1
  60. package/dist/service/introspect.js +131 -43
  61. package/dist/service/introspect.js.map +1 -1
  62. package/dist/service/overview.d.ts +9 -0
  63. package/dist/service/overview.js +113 -0
  64. package/dist/service/overview.js.map +1 -0
  65. package/dist/service/queryInspection.d.ts +15 -0
  66. package/dist/service/queryInspection.js +107 -0
  67. package/dist/service/queryInspection.js.map +1 -0
  68. package/dist/service/references.d.ts +9 -0
  69. package/dist/service/references.js +52 -0
  70. package/dist/service/references.js.map +1 -0
  71. package/dist/service/schemaFilters.d.ts +26 -0
  72. package/dist/service/schemaFilters.js +42 -0
  73. package/dist/service/schemaFilters.js.map +1 -0
  74. package/dist/service/stagedSerialization.d.ts +2 -0
  75. package/dist/service/stagedSerialization.js +38 -10
  76. package/dist/service/stagedSerialization.js.map +1 -1
  77. package/dist/service/types.d.ts +11 -1
  78. package/dist/types/constants.d.ts +2 -0
  79. package/dist/types/constants.js +3 -1
  80. package/dist/types/constants.js.map +1 -1
  81. package/dist/utils/query.d.ts +0 -2
  82. package/dist/utils/query.js +0 -20
  83. package/dist/utils/query.js.map +1 -1
  84. package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
  85. package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
  86. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
  87. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
  88. package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
  89. package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
  90. package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
  91. package/frontend-vue/app/build/data/cellValues.ts +132 -0
  92. package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
  93. package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
  94. package/frontend-vue/app/build/data/recentTables.ts +48 -0
  95. package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
  96. package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
  97. package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
  98. package/frontend-vue/app/build/diagram/autosave.ts +72 -0
  99. package/frontend-vue/app/build/diagram/graph.ts +585 -0
  100. package/frontend-vue/app/build/diagram/history.ts +178 -0
  101. package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
  102. package/frontend-vue/app/build/diagram/sync.ts +137 -0
  103. package/frontend-vue/app/build/query/completion.ts +100 -0
  104. package/frontend-vue/app/build/query/csv.ts +32 -0
  105. package/frontend-vue/app/build/query/examples.ts +38 -0
  106. package/frontend-vue/app/build/query/runs.ts +168 -0
  107. package/frontend-vue/app/build/query/tableColumns.ts +35 -0
  108. package/frontend-vue/app/build/query/target.ts +143 -0
  109. package/frontend-vue/app/build/schemas/filterState.ts +128 -0
  110. package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
  111. package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
  112. package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
  113. package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
  114. package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
  115. package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
  116. package/frontend-vue/app/components/DataBrowser.vue +126 -0
  117. package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
  118. package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
  119. package/frontend-vue/app/components/DataFilterBar.vue +163 -0
  120. package/frontend-vue/app/components/DataGrid.vue +737 -499
  121. package/frontend-vue/app/components/DataGridCell.vue +125 -77
  122. package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
  123. package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
  124. package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
  125. package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
  126. package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
  127. package/frontend-vue/app/components/InstancePicker.vue +195 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +30 -29
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
  130. package/frontend-vue/app/components/QueryConsole.vue +726 -0
  131. package/frontend-vue/app/components/QueryLibrary.vue +314 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
  133. package/frontend-vue/app/components/RowDrawer.vue +520 -0
  134. package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
  135. package/frontend-vue/app/components/SchemaPicker.vue +86 -0
  136. package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
  137. package/frontend-vue/app/components/TableInspector.vue +741 -0
  138. package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
  139. package/frontend-vue/dms.frontend.ts +5 -14
  140. package/frontend-vue/i18n/locales/database-en-GB.json +526 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +528 -195
  142. package/frontend-vue/package.json +2 -1
  143. package/frontend-vue/pnpm-lock.yaml +2601 -858
  144. package/frontend-vue/pnpm-workspace.yaml +1 -1
  145. package/frontend-vue/tests/dataBrowser.test.ts +206 -0
  146. package/frontend-vue/tests/diagram.test.ts +516 -0
  147. package/frontend-vue/tests/helpers.test.ts +205 -0
  148. package/frontend-vue/tests/instanceOptions.test.ts +53 -0
  149. package/frontend-vue/tests/locales.test.ts +56 -0
  150. package/frontend-vue/tests/persistence.test.ts +106 -5
  151. package/frontend-vue/tests/pickerMenus.test.ts +254 -0
  152. package/frontend-vue/tests/queryConsole.test.ts +282 -0
  153. package/frontend-vue/tests/queryRuntime.test.ts +2 -2
  154. package/frontend-vue/tests/registration.test.ts +36 -25
  155. package/frontend-vue/tests/schemasFilters.test.ts +115 -0
  156. package/package.json +9 -6
  157. package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
  158. package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
  159. package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
  160. package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
  161. package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
  162. package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
  163. package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
  164. package/frontend-vue/app/composables/useQueryStore.ts +0 -117
  165. package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
  166. package/frontend-vue/app/custom-pages/database/data.vue +0 -41
  167. package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
  168. package/frontend-vue/app/custom-pages/database/query.vue +0 -436
  169. package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
  170. package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
  171. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
  172. package/frontend-vue/app/utils/dmsTableView.ts +0 -83
  173. package/frontend-vue/app/utils/relativeTime.ts +0 -18
@@ -1,134 +0,0 @@
1
- <script setup lang="ts">
2
- import type { HistoryEntry } from "../composables/useQueryStore";
3
- import { formatDatabaseRelativeTime } from "../utils/relativeTime";
4
-
5
- interface Props {
6
- items: HistoryEntry[];
7
- total: number;
8
- page: number;
9
- pageSize: number;
10
- }
11
-
12
- const props = defineProps<Props>();
13
-
14
- const emit = defineEmits<{
15
- "update:page": [value: number];
16
- load: [entry: HistoryEntry];
17
- save: [entry: HistoryEntry];
18
- }>();
19
-
20
- const totalPages = computed(() =>
21
- Math.max(1, Math.ceil(props.total / props.pageSize)),
22
- );
23
-
24
- const canPrev = computed(() => props.page > 0);
25
- const canNext = computed(() => props.page + 1 < totalPages.value);
26
-
27
- const QUERY_PREVIEW_LIMIT = 64;
28
-
29
- function preview(query: string): string {
30
- const flat = query.replace(/\s+/g, " ").trim();
31
- return flat.length > QUERY_PREVIEW_LIMIT
32
- ? `${flat.slice(0, QUERY_PREVIEW_LIMIT)}…`
33
- : flat;
34
- }
35
-
36
- const { t } = useI18n();
37
-
38
- function meta(item: HistoryEntry): string {
39
- const count = t(
40
- "dms_database.query.rowCount",
41
- { count: item.rowCount },
42
- item.rowCount,
43
- );
44
- return `${formatDatabaseRelativeTime(item.executedAt)} · ${count}`;
45
- }
46
-
47
- function prev() {
48
- if (canPrev.value) emit("update:page", props.page - 1);
49
- }
50
-
51
- function next() {
52
- if (canNext.value) emit("update:page", props.page + 1);
53
- }
54
- </script>
55
-
56
- <template>
57
- <DmsCard :padded="false" class="flex flex-col">
58
- <header class="flex items-center gap-2.5 px-4 pt-4 pb-2.5">
59
- <UIcon name="i-ph-clock" class="text-primary shrink-0" />
60
- <h3 class="text-highlighted text-sm font-semibold">
61
- {{ $t("dms_database.query.history") }}
62
- </h3>
63
- </header>
64
-
65
- <ul
66
- v-if="items.length > 0"
67
- class="flex-1 overflow-y-auto px-1.5 pb-2 space-y-0.5"
68
- >
69
- <li
70
- v-for="item in items"
71
- :key="item.id"
72
- class="group flex items-center gap-2.5 rounded-md px-2.5 py-2.5 cursor-pointer hover:bg-elevated transition-colors"
73
- @click="emit('load', item)"
74
- @keydown.enter.prevent="emit('load', item)"
75
- >
76
- <UIcon
77
- name="i-ph-clock"
78
- class="text-dimmed shrink-0 size-3.5"
79
- />
80
- <div class="min-w-0 flex-1">
81
- <div class="text-toned font-mono text-[11px] truncate">
82
- {{ preview(item.source) }}
83
- </div>
84
- <div class="text-dimmed text-[10px] mt-0.5">
85
- {{ meta(item) }}
86
- </div>
87
- </div>
88
- <UButton
89
- icon="i-ph-star"
90
- variant="ghost"
91
- color="neutral"
92
- size="xs"
93
- class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity hover:text-warning"
94
- :aria-label="$t('dms_database.query.saveAsFavorite')"
95
- @click.stop="emit('save', item)"
96
- />
97
- </li>
98
- </ul>
99
-
100
- <div
101
- v-else
102
- class="flex-1 flex items-center justify-center px-4 py-6 text-xs text-dimmed text-center"
103
- >
104
- {{ $t("dms_database.query.emptyHistory") }}
105
- </div>
106
-
107
- <footer
108
- v-if="items.length > 0"
109
- class="flex items-center justify-between gap-2 px-3 py-2 border-t border-default"
110
- >
111
- <UButton
112
- size="xs"
113
- variant="ghost"
114
- color="neutral"
115
- icon="i-ph-caret-left"
116
- :disabled="!canPrev"
117
- :label="$t('dms_database.query.previous')"
118
- @click="prev"
119
- />
120
- <span class="text-xs text-dimmed tabular-nums">
121
- {{ page + 1 }} / {{ totalPages }}
122
- </span>
123
- <UButton
124
- size="xs"
125
- variant="ghost"
126
- color="neutral"
127
- trailing-icon="i-ph-caret-right"
128
- :disabled="!canNext"
129
- :label="$t('dms_database.query.next')"
130
- @click="next"
131
- />
132
- </footer>
133
- </DmsCard>
134
- </template>
@@ -1,278 +0,0 @@
1
- <script setup lang="ts">
2
- import type {
3
- FieldDescriptor,
4
- TableSummary,
5
- } from "../composables/useDatabaseSchemas";
6
- import { inferFieldTypeIcon, isPrimaryKey } from "../utils/databaseFieldTypeIcons";
7
-
8
- // Body panel for the shared dms drawer (useDrawer). The drawer owns the chrome
9
- // (themed surface, header with the table name + subtitle, close button); this
10
- // component only renders the inspector content: columns / indexes / preview
11
- // tabs and the footer. It is opened from schemas.vue via `useDrawer().open()`.
12
-
13
- // Descriptor kinds that admit null/undefined on their own: an explicit
14
- // null/undefined, or an untyped any/unknown.
15
- const NULLISH_KINDS = new Set<FieldDescriptor["kind"]>([
16
- "null",
17
- "undefined",
18
- "any",
19
- "unknown",
20
- ]);
21
-
22
- // A field is nullable when its descriptor admits null/undefined — directly, as
23
- // an untyped any/unknown, or as a member of a union. Anything else is required.
24
- function isNullable(descriptor: FieldDescriptor): boolean {
25
- if (NULLISH_KINDS.has(descriptor.kind)) return true;
26
- if (descriptor.kind === "union") {
27
- return descriptor.members.some(
28
- (member) => member.kind === "null" || member.kind === "undefined",
29
- );
30
- }
31
- return false;
32
- }
33
-
34
- interface Props {
35
- schemaId: string | null;
36
- table: TableSummary | null;
37
- // Injected by the shared DynamicDrawer; unused here (read-only panel).
38
- containerId?: string;
39
- }
40
-
41
- const props = defineProps<Props>();
42
-
43
- // The shared DynamicDrawer dismisses the drawer when the body emits `success`.
44
- const emit = defineEmits<{ success: [] }>();
45
-
46
- const { t } = useI18n();
47
- const router = useDmsRouter();
48
- const { $authFetch } = useAuthFetch();
49
-
50
- const DATA_PATH = "/modules/database/data";
51
- const BROWSE_LIST = "/api/database/browse/list";
52
- const FILTER_MODE_IS = "is";
53
-
54
- type Tab = "columns" | "indexes" | "preview";
55
- const tab = ref<Tab>("columns");
56
-
57
- const tabItems = computed(() => [
58
- { label: t("dms_database.schemas.inspector.columns"), value: "columns" as const, icon: "i-ph-layout" },
59
- { label: t("dms_database.schemas.inspector.indexes"), value: "indexes" as const, icon: "i-ph-key" },
60
- { label: t("dms_database.schemas.inspector.preview"), value: "preview" as const, icon: "i-ph-table" },
61
- ]);
62
-
63
- const fieldEntries = computed(() =>
64
- Object.entries(props.table?.fields ?? {}).map(([name, descriptor]) => {
65
- const meta = inferFieldTypeIcon(descriptor, name);
66
- const pk = isPrimaryKey(name, props.table?.indexes ?? {});
67
- // Primary keys are never nullable; otherwise read it off the descriptor.
68
- const nullable = pk ? false : isNullable(descriptor);
69
- return { name, meta, pk, nullable };
70
- }),
71
- );
72
-
73
- const indexEntries = computed(() =>
74
- Object.entries(props.table?.indexes ?? {}).map(([name, def]) => ({
75
- name,
76
- fields: def.fields ?? [],
77
- multi: Boolean(def.multi),
78
- })),
79
- );
80
-
81
- // --- preview (single sample row) ---
82
- const previewRow = ref<Record<string, unknown> | null>(null);
83
- const previewLoading = ref(false);
84
- const previewError = ref(false);
85
- const previewLoadedFor = ref<string | null>(null);
86
-
87
- async function loadPreview() {
88
- if (!props.schemaId || !props.table) return;
89
- const key = `${props.schemaId}::${props.table.name}`;
90
- if (previewLoadedFor.value === key) return;
91
- previewLoading.value = true;
92
- previewError.value = false;
93
- try {
94
- const res = await $authFetch<{ results: Record<string, unknown>[] }>(
95
- BROWSE_LIST,
96
- {
97
- query: {
98
- filter_schema: `${FILTER_MODE_IS}:${props.schemaId}`,
99
- filter_table: `${FILTER_MODE_IS}:${props.table.name}`,
100
- limit: 1,
101
- },
102
- },
103
- );
104
- previewRow.value = res.results?.[0] ?? null;
105
- previewLoadedFor.value = key;
106
- } catch {
107
- previewError.value = true;
108
- previewRow.value = null;
109
- } finally {
110
- previewLoading.value = false;
111
- }
112
- }
113
-
114
- const previewJson = computed(() =>
115
- previewRow.value ? JSON.stringify(previewRow.value, null, 2) : "",
116
- );
117
-
118
- // The panel is mounted only while the drawer is open, so the preview loads as
119
- // soon as its tab is selected (no `open` prop to gate on anymore).
120
- watch(
121
- () => [tab.value, props.table?.name] as const,
122
- () => {
123
- if (tab.value === "preview") loadPreview();
124
- },
125
- );
126
-
127
- const browseTo = computed(() => {
128
- if (!props.schemaId || !props.table) return DATA_PATH;
129
- return `${DATA_PATH}?schema=${encodeURIComponent(props.schemaId)}&table=${encodeURIComponent(props.table.name)}`;
130
- });
131
-
132
- // "Browse data" navigates to the data browser and closes the drawer so the user
133
- // lands on the table without the panel lingering over it.
134
- function browseData() {
135
- router.push(browseTo.value);
136
- emit("success");
137
- }
138
- </script>
139
-
140
- <template>
141
- <!-- Fixed width so the right-side drawer keeps a stable, comfortable size
142
- instead of shrinking/growing with the active tab's content (the shared
143
- DynamicDrawer sizes a right drawer to its content and exposes no width). -->
144
- <div class="flex h-full w-[36rem] max-w-[calc(100vw-3rem)] flex-col">
145
- <!-- tabs -->
146
- <div class="border-b border-default pb-3">
147
- <DmsSegmented v-model="tab" :items="tabItems" />
148
- </div>
149
-
150
- <!-- body -->
151
- <div class="flex-1 overflow-y-auto py-4">
152
- <!-- columns -->
153
- <table v-if="tab === 'columns'" class="w-full text-sm">
154
- <thead>
155
- <tr class="border-b border-default text-left">
156
- <th class="text-dimmed py-2 pr-3 text-xs font-semibold">
157
- {{ $t("dms_database.schemas.inspector.colName") }}
158
- </th>
159
- <th class="text-dimmed py-2 px-3 text-xs font-semibold">
160
- {{ $t("dms_database.schemas.inspector.colType") }}
161
- </th>
162
- <th class="text-dimmed py-2 pl-3 text-xs font-semibold">
163
- {{ $t("dms_database.schemas.inspector.colNullable") }}
164
- </th>
165
- </tr>
166
- </thead>
167
- <tbody>
168
- <tr
169
- v-for="field in fieldEntries"
170
- :key="field.name"
171
- class="border-b border-default/60 last:border-0"
172
- >
173
- <td class="py-2 pr-3">
174
- <span class="flex items-center gap-2 font-mono text-toned">
175
- <UIcon
176
- :name="field.pk ? 'i-ph-key' : field.meta.icon"
177
- class="size-3.5 shrink-0"
178
- :class="field.pk ? 'text-warning' : field.meta.color"
179
- />
180
- {{ field.name }}
181
- </span>
182
- </td>
183
- <td class="py-2 px-3">
184
- <span
185
- class="font-mono text-[11px]"
186
- :class="field.meta.color"
187
- >
188
- {{ field.pk ? "pk" : field.meta.label }}
189
- </span>
190
- </td>
191
- <td class="py-2 pl-3 text-dimmed">
192
- {{
193
- field.nullable
194
- ? $t("dms_database.schemas.inspector.yes")
195
- : $t("dms_database.schemas.inspector.no")
196
- }}
197
- </td>
198
- </tr>
199
- </tbody>
200
- </table>
201
-
202
- <!-- indexes -->
203
- <div v-else-if="tab === 'indexes'" class="space-y-2.5">
204
- <div
205
- v-for="idx in indexEntries"
206
- :key="idx.name"
207
- class="flex items-center gap-3 rounded-md border border-default bg-default px-3 py-2.5"
208
- >
209
- <UBadge
210
- :color="idx.multi ? 'info' : 'neutral'"
211
- variant="soft"
212
- size="sm"
213
- >
214
- {{
215
- idx.multi
216
- ? $t("dms_database.schemas.inspector.multi")
217
- : $t("dms_database.schemas.inspector.single")
218
- }}
219
- </UBadge>
220
- <span class="font-mono text-[12.5px] text-toned">{{ idx.name }}</span>
221
- <span
222
- v-if="idx.fields.length"
223
- class="font-mono text-[11px] text-dimmed ml-auto truncate"
224
- >
225
- {{ idx.fields.join(", ") }}
226
- </span>
227
- </div>
228
- <p
229
- v-if="indexEntries.length === 0"
230
- class="text-dimmed text-sm py-6 text-center"
231
- >
232
- {{ $t("dms_database.schemas.inspector.noIndexes") }}
233
- </p>
234
- </div>
235
-
236
- <!-- preview -->
237
- <div v-else>
238
- <div v-if="previewLoading" class="py-10 text-center">
239
- <UProgress size="sm" />
240
- </div>
241
- <div
242
- v-else-if="previewError"
243
- class="text-dimmed text-sm py-10 text-center"
244
- >
245
- {{ $t("dms_database.schemas.inspector.previewError") }}
246
- </div>
247
- <div
248
- v-else-if="!previewRow"
249
- class="text-dimmed text-sm py-10 text-center"
250
- >
251
- {{ $t("dms_database.schemas.inspector.previewEmpty") }}
252
- </div>
253
- <pre
254
- v-else
255
- class="text-xs rounded-lg border border-default bg-default p-4 overflow-auto whitespace-pre text-toned"
256
- >{{ previewJson }}</pre
257
- >
258
- </div>
259
- </div>
260
-
261
- <!-- footer -->
262
- <div class="flex items-center gap-2 border-t border-default pt-4">
263
- <span class="flex items-center gap-1.5 text-xs text-success">
264
- <UIcon name="i-ph-check-circle" class="size-3.5" />
265
- {{ $t("dms_database.schemas.inspector.healthy") }}
266
- </span>
267
- <UButton
268
- color="neutral"
269
- variant="outline"
270
- size="sm"
271
- icon="i-ph-rows"
272
- :label="$t('dms_database.schemas.browseData')"
273
- class="ml-auto"
274
- @click="browseData"
275
- />
276
- </div>
277
- </div>
278
- </template>
@@ -1,255 +0,0 @@
1
- // Shared state for the data-browser tabs: each tab pins a (schema, instance,
2
- // table) triplet. The active tab is mirrored to the URL query (deep-links) and
3
- // the whole tab set is persisted to sessionStorage (survives a page refresh,
4
- // stays per browser tab).
5
-
6
- export const DEFAULT_INSTANCE_VALUE = "__DEFAULT__";
7
- export const CROSS_INSTANCE_VALUE = "__CROSS_INSTANCE__";
8
-
9
- export interface BrowserTab {
10
- id: string;
11
- schema: string;
12
- // DEFAULT_INSTANCE_VALUE, CROSS_INSTANCE_VALUE or a named instance id.
13
- instance: string;
14
- table: string;
15
- // VS Code-style preview tab: the next openTab reuses it instead of adding
16
- // a new tab. At most one per session; pinning (double-click, pin icon,
17
- // drag-reorder, editing a cell) clears the flag.
18
- preview?: boolean;
19
- }
20
-
21
- const STORAGE_KEY = "dms-database:data-browser:tabs";
22
-
23
- function browserTabId(
24
- schema: string,
25
- instance: string,
26
- table: string,
27
- ): string {
28
- return `${schema}::${instance}::${table}`;
29
- }
30
-
31
- interface PersistedTabs {
32
- tabs: BrowserTab[];
33
- activeId: string | null;
34
- }
35
-
36
- function readPersisted(): PersistedTabs | null {
37
- if (typeof window === "undefined") return null;
38
- try {
39
- const raw = window.sessionStorage.getItem(STORAGE_KEY);
40
- if (!raw) return null;
41
- const parsed = JSON.parse(raw) as PersistedTabs;
42
- if (!Array.isArray(parsed.tabs)) return null;
43
- const tabs = parsed.tabs
44
- .filter(
45
- (tab) =>
46
- tab &&
47
- typeof tab.schema === "string" &&
48
- typeof tab.instance === "string" &&
49
- typeof tab.table === "string",
50
- )
51
- .map((tab) => ({
52
- ...tab,
53
- id: browserTabId(tab.schema, tab.instance, tab.table),
54
- preview: tab.preview === true,
55
- }))
56
- // Recomputed ids can collide on a legacy/hand-edited payload; duplicate
57
- // ids would break the tab bar's v-for keys and closeTab.
58
- .filter(
59
- (tab, index, all) => all.findIndex((t) => t.id === tab.id) === index,
60
- );
61
- // The preview slot is a singleton; a hand-edited payload could carry
62
- // several — honour only the first.
63
- const firstPreview = tabs.findIndex((tab) => tab.preview);
64
- for (const [index, tab] of tabs.entries()) {
65
- if (index !== firstPreview) tab.preview = false;
66
- }
67
- const activeId = tabs.some((tab) => tab.id === parsed.activeId)
68
- ? parsed.activeId
69
- : (tabs[0]?.id ?? null);
70
- return { tabs, activeId };
71
- } catch {
72
- return null;
73
- }
74
- }
75
-
76
- export function useDataBrowserTabs() {
77
- const tabs = useDmsState<BrowserTab[]>("dms-database-browser-tabs", () => []);
78
- const activeId = useDmsState<string | null>(
79
- "dms-database-browser-active",
80
- () => null,
81
- );
82
- const route = useDmsRoute();
83
- const router = useDmsRouter();
84
- // Captured at setup so closeTab can drop the closed tab's
85
- // grid state itself instead of relying on every caller to remember to.
86
- const grid = useDataBrowserGrid();
87
-
88
- const activeTab = computed(
89
- () => tabs.value.find((tab) => tab.id === activeId.value) ?? null,
90
- );
91
- // When tabs span several schemas the tab bar disambiguates its labels.
92
- const hasMultipleSchemas = computed(
93
- () => new Set(tabs.value.map((tab) => tab.schema)).size > 1,
94
- );
95
-
96
- function persist() {
97
- if (typeof window === "undefined") return;
98
- try {
99
- window.sessionStorage.setItem(
100
- STORAGE_KEY,
101
- JSON.stringify({ tabs: tabs.value, activeId: activeId.value }),
102
- );
103
- } catch {
104
- // Quota/private-mode failures only cost tab restoration.
105
- }
106
- }
107
-
108
- // The URL carries the ACTIVE tab only (deep-links); per-tab grid state
109
- // deliberately stays out of it.
110
- function syncUrl() {
111
- const tab = activeTab.value;
112
- const query = { ...route.query };
113
- delete query.schema;
114
- delete query.instance;
115
- delete query.table;
116
- if (tab) {
117
- query.schema = tab.schema;
118
- query.table = tab.table;
119
- if (tab.instance !== DEFAULT_INSTANCE_VALUE) query.instance = tab.instance;
120
- }
121
- if (
122
- Object.keys(query).length === Object.keys(route.query).length &&
123
- Object.entries(query).every(([key, value]) => route.query[key] === value)
124
- ) return;
125
- router.replace({ query });
126
- }
127
-
128
- function activateTab(id: string) {
129
- if (!tabs.value.some((tab) => tab.id === id)) return;
130
- activeId.value = id;
131
- syncUrl();
132
- persist();
133
- }
134
-
135
- // Promote a preview tab to a permanent one (double-click, pin icon, drag).
136
- function pinTab(id: string) {
137
- const tab = tabs.value.find((t) => t.id === id);
138
- if (!tab?.preview) return;
139
- tabs.value = tabs.value.map((t) =>
140
- t.id === id ? { ...t, preview: false } : t,
141
- );
142
- persist();
143
- }
144
-
145
- // Opens as a preview by default (VS Code-style: single-click reuses the
146
- // preview slot). Pass preview = false for an explicitly permanent open
147
- // (sidebar double-click, deep-links) — it also pins the tab if already
148
- // open as preview.
149
- function openTab(
150
- schema: string,
151
- instance: string,
152
- table: string,
153
- preview = true,
154
- ) {
155
- const id = browserTabId(schema, instance, table);
156
- const existing = tabs.value.find((tab) => tab.id === id);
157
- if (existing) {
158
- if (!preview) pinTab(id);
159
- activateTab(id);
160
- return;
161
- }
162
- const tab: BrowserTab = { id, schema, instance, table, preview };
163
- const previewIndex = tabs.value.findIndex((t) => t.preview);
164
- if (preview && previewIndex >= 0) {
165
- // Reuse the preview slot in place: swap its content, drop the replaced
166
- // tab's grid state (its id dies with it).
167
- const replaced = tabs.value[previewIndex]!;
168
- const next = [...tabs.value];
169
- next[previewIndex] = tab;
170
- tabs.value = next;
171
- grid.drop(replaced.id);
172
- } else {
173
- tabs.value = [...tabs.value, tab];
174
- }
175
- activateTab(id);
176
- }
177
-
178
- function closeTab(id: string) {
179
- const index = tabs.value.findIndex((tab) => tab.id === id);
180
- if (index < 0) return;
181
- const next = tabs.value.filter((tab) => tab.id !== id);
182
- tabs.value = next;
183
- if (activeId.value === id) {
184
- // Right neighbour first, then left — mirrors browser tab behaviour.
185
- activeId.value = (next[index] ?? next[index - 1])?.id ?? null;
186
- }
187
- // The closed tab's grid state and cached rows go with it — otherwise they
188
- // leak for the rest of the session.
189
- grid.drop(id);
190
- syncUrl();
191
- persist();
192
- }
193
-
194
- // Reorder (drag & drop): move the dragged tab to the target tab's position.
195
- // Pure reorder — pin-on-drag is a gesture policy and lives at the drop site
196
- // (the tab bar's onDrop), not in this primitive.
197
- function moveTab(draggedId: string, targetId: string) {
198
- if (draggedId === targetId) return;
199
- const next = [...tabs.value];
200
- const from = next.findIndex((tab) => tab.id === draggedId);
201
- const to = next.findIndex((tab) => tab.id === targetId);
202
- if (from < 0 || to < 0) return;
203
- const [moved] = next.splice(from, 1);
204
- if (!moved) return;
205
- next.splice(to, 0, moved);
206
- tabs.value = next;
207
- persist();
208
- }
209
-
210
- // Restore persisted tabs, then let the URL win for the active selection so
211
- // existing deep-links keep working. Call exactly once, from the page's
212
- // onMounted.
213
- function restore() {
214
- const persisted = readPersisted();
215
- if (persisted) {
216
- tabs.value = persisted.tabs;
217
- activeId.value = persisted.activeId;
218
- }
219
- const schema = route.query.schema;
220
- const table = route.query.table;
221
- if (
222
- typeof schema === "string" &&
223
- schema &&
224
- typeof table === "string" &&
225
- table
226
- ) {
227
- const instance =
228
- typeof route.query.instance === "string" && route.query.instance
229
- ? route.query.instance
230
- : DEFAULT_INSTANCE_VALUE;
231
- // Deep-links carry explicit intent (overview row double-click, the
232
- // inspector's "browse data" button, shared URLs): open permanently, the
233
- // preview slot is for casual sidebar browsing only.
234
- openTab(schema, instance, table, false);
235
- } else if (!schema && activeTab.value) {
236
- // Only reclaim the URL when it carries no selection at all: a
237
- // schema-only deep-link (?schema=X) is the sidebar's to honour and
238
- // must not be clobbered by the restored tab.
239
- syncUrl();
240
- }
241
- }
242
-
243
- return {
244
- tabs,
245
- activeId,
246
- activeTab,
247
- hasMultipleSchemas,
248
- openTab,
249
- closeTab,
250
- activateTab,
251
- pinTab,
252
- moveTab,
253
- restore,
254
- };
255
- }