@sfxcode/nuxt-ui-mongocamp 1.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.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/module.d.mts +8 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +35 -0
- package/dist/runtime/components/MongocampAccount.d.vue.ts +3 -0
- package/dist/runtime/components/MongocampAccount.vue +197 -0
- package/dist/runtime/components/MongocampAccount.vue.d.ts +3 -0
- package/dist/runtime/components/MongocampCollectionData.d.vue.ts +6 -0
- package/dist/runtime/components/MongocampCollectionData.vue +511 -0
- package/dist/runtime/components/MongocampCollectionData.vue.d.ts +6 -0
- package/dist/runtime/components/MongocampCollectionInfos.d.vue.ts +9 -0
- package/dist/runtime/components/MongocampCollectionInfos.vue +393 -0
- package/dist/runtime/components/MongocampCollectionInfos.vue.d.ts +9 -0
- package/dist/runtime/components/MongocampCollections.d.vue.ts +7 -0
- package/dist/runtime/components/MongocampCollections.vue +231 -0
- package/dist/runtime/components/MongocampCollections.vue.d.ts +7 -0
- package/dist/runtime/components/MongocampDatabases.d.vue.ts +3 -0
- package/dist/runtime/components/MongocampDatabases.vue +112 -0
- package/dist/runtime/components/MongocampDatabases.vue.d.ts +3 -0
- package/dist/runtime/components/MongocampJobs.d.vue.ts +3 -0
- package/dist/runtime/components/MongocampJobs.vue +354 -0
- package/dist/runtime/components/MongocampJobs.vue.d.ts +3 -0
- package/dist/runtime/components/MongocampLogin.d.vue.ts +3 -0
- package/dist/runtime/components/MongocampLogin.vue +63 -0
- package/dist/runtime/components/MongocampLogin.vue.d.ts +3 -0
- package/dist/runtime/components/MongocampRoleGrants.d.vue.ts +6 -0
- package/dist/runtime/components/MongocampRoleGrants.vue +276 -0
- package/dist/runtime/components/MongocampRoleGrants.vue.d.ts +6 -0
- package/dist/runtime/components/MongocampRoles.d.vue.ts +6 -0
- package/dist/runtime/components/MongocampRoles.vue +272 -0
- package/dist/runtime/components/MongocampRoles.vue.d.ts +6 -0
- package/dist/runtime/components/MongocampUsers.d.vue.ts +3 -0
- package/dist/runtime/components/MongocampUsers.vue +320 -0
- package/dist/runtime/components/MongocampUsers.vue.d.ts +3 -0
- package/dist/runtime/components/MongocampVersion.d.vue.ts +3 -0
- package/dist/runtime/components/MongocampVersion.vue +25 -0
- package/dist/runtime/components/MongocampVersion.vue.d.ts +3 -0
- package/dist/runtime/composables/useMongocampAccount.d.ts +7 -0
- package/dist/runtime/composables/useMongocampAccount.js +28 -0
- package/dist/runtime/composables/useMongocampAdmin.d.ts +14 -0
- package/dist/runtime/composables/useMongocampAdmin.js +48 -0
- package/dist/runtime/composables/useMongocampBucket.d.ts +15 -0
- package/dist/runtime/composables/useMongocampBucket.js +109 -0
- package/dist/runtime/composables/useMongocampCollection.d.ts +17 -0
- package/dist/runtime/composables/useMongocampCollection.js +12 -0
- package/dist/runtime/composables/useMongocampDocument.d.ts +13 -0
- package/dist/runtime/composables/useMongocampDocument.js +18 -0
- package/dist/runtime/composables/useMongocampDynamicForm.d.ts +14 -0
- package/dist/runtime/composables/useMongocampDynamicForm.js +231 -0
- package/dist/runtime/composables/useMongocampIndex.d.ts +9 -0
- package/dist/runtime/composables/useMongocampIndex.js +23 -0
- package/dist/runtime/composables/useMongocampJobs.d.ts +9 -0
- package/dist/runtime/composables/useMongocampJobs.js +25 -0
- package/dist/runtime/composables/useMongocampQuery.d.ts +99 -0
- package/dist/runtime/composables/useMongocampQuery.js +108 -0
- package/dist/runtime/composables/useMongocampQueryBuilder.d.ts +30 -0
- package/dist/runtime/composables/useMongocampQueryBuilder.js +112 -0
- package/dist/runtime/composables/useMongocampSchema.d.ts +14 -0
- package/dist/runtime/composables/useMongocampSchema.js +142 -0
- package/dist/runtime/composables/useMongocampSystem.d.ts +12 -0
- package/dist/runtime/composables/useMongocampSystem.js +21 -0
- package/dist/runtime/plugin.d.ts +6 -0
- package/dist/runtime/plugin.js +23 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/types.d.mts +3 -0
- package/package.json +99 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, h, reactive, ref, resolveComponent, watch } from "vue";
|
|
3
|
+
import { useMongocampApi, useMongocampIndex, useMongocampSchema, useToast } from "#imports";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
collectionName: { type: String, required: true },
|
|
6
|
+
sampleSize: { type: Number, required: false, default: 500 }
|
|
7
|
+
});
|
|
8
|
+
const { collectionApi, documentApi } = useMongocampApi();
|
|
9
|
+
const { schemaFromSamples, schemaToTsInterface } = useMongocampSchema();
|
|
10
|
+
const { listIndexes, createIndex, createUniqueIndex, createTextIndex, createExpiringIndex, deleteIndex } = useMongocampIndex();
|
|
11
|
+
const UBadge = resolveComponent("UBadge");
|
|
12
|
+
const UIcon = resolveComponent("UIcon");
|
|
13
|
+
const UButton = resolveComponent("UButton");
|
|
14
|
+
const UTooltip = resolveComponent("UTooltip");
|
|
15
|
+
function withTooltip(text, node) {
|
|
16
|
+
return h(UTooltip, { text }, { default: () => node });
|
|
17
|
+
}
|
|
18
|
+
const info = ref(null);
|
|
19
|
+
const columnDefinitions = ref([]);
|
|
20
|
+
const indexes = ref([]);
|
|
21
|
+
const loading = ref(false);
|
|
22
|
+
async function fetchIndexes() {
|
|
23
|
+
if (!props.collectionName) return;
|
|
24
|
+
indexes.value = await listIndexes(props.collectionName);
|
|
25
|
+
}
|
|
26
|
+
async function fetchInfo() {
|
|
27
|
+
if (!props.collectionName) return;
|
|
28
|
+
loading.value = true;
|
|
29
|
+
try {
|
|
30
|
+
info.value = await collectionApi.getCollectionInformation({ collectionName: props.collectionName });
|
|
31
|
+
const samples = await documentApi.listDocuments({
|
|
32
|
+
collectionName: props.collectionName,
|
|
33
|
+
rowsPerPage: props.sampleSize,
|
|
34
|
+
page: 1
|
|
35
|
+
});
|
|
36
|
+
columnDefinitions.value = schemaFromSamples(samples);
|
|
37
|
+
await fetchIndexes();
|
|
38
|
+
} finally {
|
|
39
|
+
loading.value = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
watch(() => [props.collectionName, props.sampleSize], fetchInfo, { immediate: true });
|
|
43
|
+
const TYPE_COLORS = {
|
|
44
|
+
"string": "neutral",
|
|
45
|
+
"number": "warning",
|
|
46
|
+
"integer": "warning",
|
|
47
|
+
"date-time": "info",
|
|
48
|
+
"boolean": "success",
|
|
49
|
+
"object": "secondary",
|
|
50
|
+
"array": "primary"
|
|
51
|
+
};
|
|
52
|
+
const schemaRows = computed(
|
|
53
|
+
() => columnDefinitions.value.map((col) => ({
|
|
54
|
+
field: col.columnKey,
|
|
55
|
+
type: col.columnType,
|
|
56
|
+
required: col.required
|
|
57
|
+
}))
|
|
58
|
+
);
|
|
59
|
+
const schemaTitle = computed(() => props.collectionName);
|
|
60
|
+
const tsInterface = computed(() => schemaToTsInterface(columnDefinitions.value, props.collectionName));
|
|
61
|
+
const toast = useToast();
|
|
62
|
+
async function copySchemaAsTs() {
|
|
63
|
+
await navigator.clipboard.writeText(tsInterface.value);
|
|
64
|
+
toast.add({ title: "Copied to clipboard", description: "TypeScript interface copied.", color: "success" });
|
|
65
|
+
}
|
|
66
|
+
const schemaTableColumns = [
|
|
67
|
+
{
|
|
68
|
+
accessorKey: "field",
|
|
69
|
+
header: "Field"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
accessorKey: "type",
|
|
73
|
+
header: "Type",
|
|
74
|
+
cell: ({ row }) => h(UBadge, {
|
|
75
|
+
label: row.original.type,
|
|
76
|
+
color: TYPE_COLORS[row.original.type] ?? "neutral",
|
|
77
|
+
variant: "subtle"
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
accessorKey: "required",
|
|
82
|
+
header: "In all samples",
|
|
83
|
+
cell: ({ row }) => row.original.required ? h(UIcon, { name: "i-lucide-check", class: "size-4 text-success" }) : h("span", { class: "text-dimmed" }, "\u2014")
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
const indexRows = computed(
|
|
87
|
+
() => indexes.value.map((idx) => ({
|
|
88
|
+
name: idx.name,
|
|
89
|
+
keys: Object.entries(idx.keys ?? {}).map(([field, direction]) => `${field}: ${direction}`).join(", "),
|
|
90
|
+
unique: idx.unique,
|
|
91
|
+
text: idx.text,
|
|
92
|
+
expire: idx.expire,
|
|
93
|
+
sizeKb: Math.round((info.value?.indexSizes?.[idx.name] ?? 0) / 1024)
|
|
94
|
+
}))
|
|
95
|
+
);
|
|
96
|
+
const isCreateIndexModalOpen = ref(false);
|
|
97
|
+
const createIndexError = ref("");
|
|
98
|
+
const newIndex = ref({ fieldName: "", sortAscending: true, indexType: "standard", duration: "3600s" });
|
|
99
|
+
const createIndexSchema = reactive([
|
|
100
|
+
{
|
|
101
|
+
$formkit: "nuxtUIInput",
|
|
102
|
+
name: "fieldName",
|
|
103
|
+
label: "Field Name",
|
|
104
|
+
validation: "required"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
$formkit: "nuxtUISwitch",
|
|
108
|
+
name: "sortAscending",
|
|
109
|
+
label: "Ascending"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
$formkit: "nuxtUISelectMenu",
|
|
113
|
+
id: "indexType",
|
|
114
|
+
name: "indexType",
|
|
115
|
+
label: "Index Type",
|
|
116
|
+
options: ["standard", "unique", "text", "expiring"],
|
|
117
|
+
validation: "required"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
$formkit: "nuxtUIInput",
|
|
121
|
+
name: "duration",
|
|
122
|
+
label: "Expire After (e.g. 3600s)",
|
|
123
|
+
help: "Only used for the expiring index type.",
|
|
124
|
+
if: "$get(indexType).value === 'expiring'"
|
|
125
|
+
}
|
|
126
|
+
]);
|
|
127
|
+
function extractSelectValue(raw) {
|
|
128
|
+
if (typeof raw === "object" && raw !== null) {
|
|
129
|
+
const obj = raw;
|
|
130
|
+
return obj.value ?? obj.label ?? "";
|
|
131
|
+
}
|
|
132
|
+
return String(raw ?? "");
|
|
133
|
+
}
|
|
134
|
+
function openCreateIndex() {
|
|
135
|
+
createIndexError.value = "";
|
|
136
|
+
newIndex.value = { fieldName: "", sortAscending: true, indexType: "standard", duration: "3600s" };
|
|
137
|
+
isCreateIndexModalOpen.value = true;
|
|
138
|
+
}
|
|
139
|
+
async function handleCreateIndex() {
|
|
140
|
+
createIndexError.value = "";
|
|
141
|
+
const { fieldName, sortAscending } = newIndex.value;
|
|
142
|
+
const indexType = extractSelectValue(newIndex.value.indexType);
|
|
143
|
+
try {
|
|
144
|
+
if (indexType === "unique") {
|
|
145
|
+
await createUniqueIndex(props.collectionName, fieldName, sortAscending);
|
|
146
|
+
} else if (indexType === "text") {
|
|
147
|
+
await createTextIndex(props.collectionName, fieldName);
|
|
148
|
+
} else if (indexType === "expiring") {
|
|
149
|
+
await createExpiringIndex(props.collectionName, fieldName, newIndex.value.duration);
|
|
150
|
+
} else {
|
|
151
|
+
await createIndex(props.collectionName, { [fieldName]: sortAscending ? 1 : -1 });
|
|
152
|
+
}
|
|
153
|
+
isCreateIndexModalOpen.value = false;
|
|
154
|
+
await fetchIndexes();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
createIndexError.value = e instanceof Error ? e.message : "Error creating index";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const isDeleteIndexModalOpen = ref(false);
|
|
160
|
+
const deleteIndexError = ref("");
|
|
161
|
+
const indexToDelete = ref(void 0);
|
|
162
|
+
function confirmDeleteIndex(indexName) {
|
|
163
|
+
indexToDelete.value = indexName;
|
|
164
|
+
deleteIndexError.value = "";
|
|
165
|
+
isDeleteIndexModalOpen.value = true;
|
|
166
|
+
}
|
|
167
|
+
async function handleDeleteIndex() {
|
|
168
|
+
deleteIndexError.value = "";
|
|
169
|
+
if (!indexToDelete.value) return;
|
|
170
|
+
try {
|
|
171
|
+
await deleteIndex(props.collectionName, indexToDelete.value);
|
|
172
|
+
isDeleteIndexModalOpen.value = false;
|
|
173
|
+
await fetchIndexes();
|
|
174
|
+
} catch (e) {
|
|
175
|
+
deleteIndexError.value = e instanceof Error ? e.message : "Error deleting index";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const INDEX_FLAG_BADGES = [
|
|
179
|
+
{ key: "unique", label: "unique", color: "warning" },
|
|
180
|
+
{ key: "text", label: "text", color: "info" },
|
|
181
|
+
{ key: "expire", label: "expire", color: "secondary" }
|
|
182
|
+
];
|
|
183
|
+
const indexTableColumns = [
|
|
184
|
+
{
|
|
185
|
+
accessorKey: "name",
|
|
186
|
+
header: "Index"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
accessorKey: "keys",
|
|
190
|
+
header: "Keys",
|
|
191
|
+
cell: ({ row }) => h("span", { class: "font-mono text-xs" }, row.original.keys)
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "flags",
|
|
195
|
+
header: "Type",
|
|
196
|
+
cell: ({ row }) => h("div", { class: "flex gap-1" }, INDEX_FLAG_BADGES.filter((flag) => row.original[flag.key]).map((flag) => h(UBadge, { label: flag.label, color: flag.color, variant: "subtle", size: "sm" })))
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
accessorKey: "sizeKb",
|
|
200
|
+
header: "Size (KB)"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "actions",
|
|
204
|
+
header: "",
|
|
205
|
+
cell: ({ row }) => row.original.name === "_id_" ? null : h("div", { class: "flex justify-end" }, [
|
|
206
|
+
withTooltip("Delete index", h(UButton, {
|
|
207
|
+
"icon": "i-lucide-trash-2",
|
|
208
|
+
"color": "error",
|
|
209
|
+
"variant": "ghost",
|
|
210
|
+
"size": "sm",
|
|
211
|
+
"aria-label": "Delete index",
|
|
212
|
+
"onClick": () => confirmDeleteIndex(row.original.name)
|
|
213
|
+
}))
|
|
214
|
+
])
|
|
215
|
+
}
|
|
216
|
+
];
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<template>
|
|
220
|
+
<div class="flex flex-col gap-4">
|
|
221
|
+
<USkeleton
|
|
222
|
+
v-if="loading"
|
|
223
|
+
class="h-32 w-full"
|
|
224
|
+
/>
|
|
225
|
+
<div
|
|
226
|
+
v-else-if="info"
|
|
227
|
+
class="grid grid-cols-2 gap-3 sm:grid-cols-3"
|
|
228
|
+
>
|
|
229
|
+
<UCard>
|
|
230
|
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
231
|
+
Documents
|
|
232
|
+
</div>
|
|
233
|
+
<div class="text-2xl font-semibold">
|
|
234
|
+
{{ info.count }}
|
|
235
|
+
</div>
|
|
236
|
+
</UCard>
|
|
237
|
+
<UCard>
|
|
238
|
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
239
|
+
Data Size
|
|
240
|
+
</div>
|
|
241
|
+
<div class="text-2xl font-semibold">
|
|
242
|
+
{{ Math.round(info.size / 1024) }} KB
|
|
243
|
+
</div>
|
|
244
|
+
</UCard>
|
|
245
|
+
<UCard>
|
|
246
|
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
247
|
+
Storage Size
|
|
248
|
+
</div>
|
|
249
|
+
<div class="text-2xl font-semibold">
|
|
250
|
+
{{ Math.round(info.storageSize / 1024) }} KB
|
|
251
|
+
</div>
|
|
252
|
+
</UCard>
|
|
253
|
+
<UCard>
|
|
254
|
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
255
|
+
Avg Doc Size
|
|
256
|
+
</div>
|
|
257
|
+
<div class="text-2xl font-semibold">
|
|
258
|
+
{{ Math.round(info.avgObjSize) }} B
|
|
259
|
+
</div>
|
|
260
|
+
</UCard>
|
|
261
|
+
<UCard>
|
|
262
|
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
263
|
+
Indexes
|
|
264
|
+
</div>
|
|
265
|
+
<div class="text-2xl font-semibold">
|
|
266
|
+
{{ info.nindexes }}
|
|
267
|
+
</div>
|
|
268
|
+
</UCard>
|
|
269
|
+
<UCard>
|
|
270
|
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
271
|
+
Index Size
|
|
272
|
+
</div>
|
|
273
|
+
<div class="text-2xl font-semibold">
|
|
274
|
+
{{ Math.round(info.totalIndexSize / 1024) }} KB
|
|
275
|
+
</div>
|
|
276
|
+
</UCard>
|
|
277
|
+
</div>
|
|
278
|
+
<UCard v-if="indexRows.length">
|
|
279
|
+
<template #header>
|
|
280
|
+
<div class="flex items-center justify-between gap-2">
|
|
281
|
+
<div class="flex items-center gap-2">
|
|
282
|
+
<UIcon
|
|
283
|
+
name="i-lucide-list-tree"
|
|
284
|
+
class="size-4 text-(--ui-primary)"
|
|
285
|
+
/>
|
|
286
|
+
<span class="font-semibold">Indexes</span>
|
|
287
|
+
<UBadge
|
|
288
|
+
:label="`${indexRows.length} indexes`"
|
|
289
|
+
variant="subtle"
|
|
290
|
+
color="neutral"
|
|
291
|
+
size="sm"
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
<UButton
|
|
295
|
+
icon="i-lucide-plus"
|
|
296
|
+
label="Create Index"
|
|
297
|
+
color="neutral"
|
|
298
|
+
variant="ghost"
|
|
299
|
+
size="sm"
|
|
300
|
+
@click="openCreateIndex"
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
</template>
|
|
304
|
+
<UTable
|
|
305
|
+
:data="indexRows"
|
|
306
|
+
:columns="indexTableColumns"
|
|
307
|
+
/>
|
|
308
|
+
</UCard>
|
|
309
|
+
|
|
310
|
+
<UCard v-if="schemaRows.length">
|
|
311
|
+
<template #header>
|
|
312
|
+
<div class="flex items-center justify-between gap-2">
|
|
313
|
+
<div class="flex items-center gap-2">
|
|
314
|
+
<UIcon
|
|
315
|
+
name="i-lucide-braces"
|
|
316
|
+
class="size-4 text-(--ui-primary)"
|
|
317
|
+
/>
|
|
318
|
+
<span class="font-semibold">{{ schemaTitle }}</span>
|
|
319
|
+
<UBadge
|
|
320
|
+
:label="`${schemaRows.length} fields`"
|
|
321
|
+
variant="subtle"
|
|
322
|
+
color="neutral"
|
|
323
|
+
size="sm"
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
<UButton
|
|
327
|
+
icon="i-lucide-copy"
|
|
328
|
+
label="Copy as TS Interface"
|
|
329
|
+
color="neutral"
|
|
330
|
+
variant="ghost"
|
|
331
|
+
size="sm"
|
|
332
|
+
@click="copySchemaAsTs"
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
</template>
|
|
336
|
+
<UTable
|
|
337
|
+
:data="schemaRows"
|
|
338
|
+
:columns="schemaTableColumns"
|
|
339
|
+
/>
|
|
340
|
+
</UCard>
|
|
341
|
+
|
|
342
|
+
<UModal
|
|
343
|
+
v-model:open="isCreateIndexModalOpen"
|
|
344
|
+
title="Create Index"
|
|
345
|
+
>
|
|
346
|
+
<template #body>
|
|
347
|
+
<FUDataEdit
|
|
348
|
+
:data="newIndex"
|
|
349
|
+
:schema="createIndexSchema"
|
|
350
|
+
submit-label="Create Index"
|
|
351
|
+
submit-icon="i-lucide-plus"
|
|
352
|
+
@data-saved="handleCreateIndex"
|
|
353
|
+
/>
|
|
354
|
+
<p
|
|
355
|
+
v-if="createIndexError"
|
|
356
|
+
class="mt-2 text-sm text-error-500"
|
|
357
|
+
>
|
|
358
|
+
{{ createIndexError }}
|
|
359
|
+
</p>
|
|
360
|
+
</template>
|
|
361
|
+
</UModal>
|
|
362
|
+
|
|
363
|
+
<UModal
|
|
364
|
+
v-model:open="isDeleteIndexModalOpen"
|
|
365
|
+
title="Delete Index"
|
|
366
|
+
:ui="{ footer: 'justify-end' }"
|
|
367
|
+
>
|
|
368
|
+
<template #body>
|
|
369
|
+
<p>Are you sure you want to delete the index "{{ indexToDelete }}"? This action cannot be undone.</p>
|
|
370
|
+
<p
|
|
371
|
+
v-if="deleteIndexError"
|
|
372
|
+
class="mt-2 text-sm text-error-500"
|
|
373
|
+
>
|
|
374
|
+
{{ deleteIndexError }}
|
|
375
|
+
</p>
|
|
376
|
+
</template>
|
|
377
|
+
<template #footer>
|
|
378
|
+
<UButton
|
|
379
|
+
label="Cancel"
|
|
380
|
+
color="neutral"
|
|
381
|
+
variant="ghost"
|
|
382
|
+
@click="isDeleteIndexModalOpen = false"
|
|
383
|
+
/>
|
|
384
|
+
<UButton
|
|
385
|
+
label="Delete"
|
|
386
|
+
color="error"
|
|
387
|
+
icon="i-lucide-trash-2"
|
|
388
|
+
@click="handleDeleteIndex"
|
|
389
|
+
/>
|
|
390
|
+
</template>
|
|
391
|
+
</UModal>
|
|
392
|
+
</div>
|
|
393
|
+
</template>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
collectionName: string;
|
|
3
|
+
sampleSize?: number;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
6
|
+
sampleSize: number;
|
|
7
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
infoPath?: string;
|
|
3
|
+
dataPath?: string;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { h, ref, resolveComponent } from "vue";
|
|
3
|
+
import { useMongocampApi, useMongocampBucket } from "#imports";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
infoPath: { type: String, required: false },
|
|
6
|
+
dataPath: { type: String, required: false }
|
|
7
|
+
});
|
|
8
|
+
const { collectionApi } = useMongocampApi();
|
|
9
|
+
const { isBucketCollection, bucketNameFor, bucketActionInFlight, clearBucket, deleteBucket } = useMongocampBucket();
|
|
10
|
+
const collections = ref([]);
|
|
11
|
+
const loading = ref(false);
|
|
12
|
+
async function fetchCollections() {
|
|
13
|
+
loading.value = true;
|
|
14
|
+
try {
|
|
15
|
+
const names = await collectionApi.listCollections();
|
|
16
|
+
const rows = await Promise.all(
|
|
17
|
+
names.filter((name) => !name.startsWith("mc")).map(async (name) => {
|
|
18
|
+
const info = await collectionApi.getCollectionInformation({ collectionName: name });
|
|
19
|
+
return {
|
|
20
|
+
name,
|
|
21
|
+
count: info.count,
|
|
22
|
+
sizekb: Math.round(info.storageSize / 1024),
|
|
23
|
+
indexCount: info.nindexes
|
|
24
|
+
};
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
collections.value = rows;
|
|
28
|
+
} finally {
|
|
29
|
+
loading.value = false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const UButton = resolveComponent("UButton");
|
|
33
|
+
const UBadge = resolveComponent("UBadge");
|
|
34
|
+
const UTooltip = resolveComponent("UTooltip");
|
|
35
|
+
function withTooltip(text, node) {
|
|
36
|
+
return h(UTooltip, { text }, { default: () => node });
|
|
37
|
+
}
|
|
38
|
+
const globalFilter = ref("");
|
|
39
|
+
const sorting = ref([]);
|
|
40
|
+
const isClearBucketModalOpen = ref(false);
|
|
41
|
+
const bucketToClear = ref(void 0);
|
|
42
|
+
function confirmClearBucket(bucketName) {
|
|
43
|
+
bucketToClear.value = bucketName;
|
|
44
|
+
isClearBucketModalOpen.value = true;
|
|
45
|
+
}
|
|
46
|
+
async function handleClearBucket() {
|
|
47
|
+
if (!bucketToClear.value) return;
|
|
48
|
+
const success = await clearBucket(bucketToClear.value);
|
|
49
|
+
isClearBucketModalOpen.value = false;
|
|
50
|
+
if (success) await fetchCollections();
|
|
51
|
+
}
|
|
52
|
+
const isDeleteBucketModalOpen = ref(false);
|
|
53
|
+
const bucketToDelete = ref(void 0);
|
|
54
|
+
function confirmDeleteBucket(bucketName) {
|
|
55
|
+
bucketToDelete.value = bucketName;
|
|
56
|
+
isDeleteBucketModalOpen.value = true;
|
|
57
|
+
}
|
|
58
|
+
async function handleDeleteBucket() {
|
|
59
|
+
if (!bucketToDelete.value) return;
|
|
60
|
+
const success = await deleteBucket(bucketToDelete.value);
|
|
61
|
+
isDeleteBucketModalOpen.value = false;
|
|
62
|
+
if (success) await fetchCollections();
|
|
63
|
+
}
|
|
64
|
+
function sortHeader(column, label) {
|
|
65
|
+
const isSorted = column.getIsSorted();
|
|
66
|
+
return h(UButton, {
|
|
67
|
+
color: "neutral",
|
|
68
|
+
variant: "ghost",
|
|
69
|
+
label,
|
|
70
|
+
icon: isSorted === "asc" ? "i-lucide-arrow-up-narrow-wide" : isSorted === "desc" ? "i-lucide-arrow-down-wide-narrow" : "i-lucide-arrow-up-down",
|
|
71
|
+
class: "-mx-2.5",
|
|
72
|
+
onClick: () => column.toggleSorting(isSorted === "asc")
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const columns = [
|
|
76
|
+
{
|
|
77
|
+
accessorKey: "name",
|
|
78
|
+
header: ({ column }) => sortHeader(column, "Collection"),
|
|
79
|
+
cell: ({ row }) => h("div", { class: "flex items-center gap-2" }, [
|
|
80
|
+
h("span", row.original.name),
|
|
81
|
+
isBucketCollection(row.original.name) ? h(UBadge, { label: "bucket", color: "neutral", variant: "subtle", size: "sm" }) : null
|
|
82
|
+
])
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
accessorKey: "count",
|
|
86
|
+
header: ({ column }) => sortHeader(column, "Documents")
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
accessorKey: "sizekb",
|
|
90
|
+
header: ({ column }) => sortHeader(column, "Size (KB)")
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
accessorKey: "indexCount",
|
|
94
|
+
header: ({ column }) => sortHeader(column, "Indexes")
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "actions",
|
|
98
|
+
header: "",
|
|
99
|
+
cell: ({ row }) => {
|
|
100
|
+
const buttons = [
|
|
101
|
+
withTooltip("Collection info", h(UButton, {
|
|
102
|
+
"icon": "i-lucide-info",
|
|
103
|
+
"color": "neutral",
|
|
104
|
+
"variant": "ghost",
|
|
105
|
+
"size": "sm",
|
|
106
|
+
"aria-label": "Collection info",
|
|
107
|
+
"to": `${props.infoPath ?? "/secured/admin/collections"}/${row.original.name}`
|
|
108
|
+
})),
|
|
109
|
+
withTooltip("Collection data", h(UButton, {
|
|
110
|
+
"icon": "i-lucide-table",
|
|
111
|
+
"color": "neutral",
|
|
112
|
+
"variant": "ghost",
|
|
113
|
+
"size": "sm",
|
|
114
|
+
"aria-label": "Collection data",
|
|
115
|
+
"to": `${props.dataPath ?? "/secured/admin/collections"}/${row.original.name}/data`
|
|
116
|
+
}))
|
|
117
|
+
];
|
|
118
|
+
if (isBucketCollection(row.original.name)) {
|
|
119
|
+
const bucketName = bucketNameFor(row.original.name);
|
|
120
|
+
buttons.push(
|
|
121
|
+
withTooltip("Clear bucket", h(UButton, {
|
|
122
|
+
"icon": "i-lucide-eraser",
|
|
123
|
+
"color": "warning",
|
|
124
|
+
"variant": "ghost",
|
|
125
|
+
"size": "sm",
|
|
126
|
+
"aria-label": "Clear bucket",
|
|
127
|
+
"loading": bucketActionInFlight.value.has(bucketName),
|
|
128
|
+
"onClick": () => confirmClearBucket(bucketName)
|
|
129
|
+
})),
|
|
130
|
+
withTooltip("Delete bucket", h(UButton, {
|
|
131
|
+
"icon": "i-lucide-trash-2",
|
|
132
|
+
"color": "error",
|
|
133
|
+
"variant": "ghost",
|
|
134
|
+
"size": "sm",
|
|
135
|
+
"aria-label": "Delete bucket",
|
|
136
|
+
"loading": bucketActionInFlight.value.has(bucketName),
|
|
137
|
+
"onClick": () => confirmDeleteBucket(bucketName)
|
|
138
|
+
}))
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return h("div", { class: "flex gap-1 justify-end" }, buttons);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
];
|
|
145
|
+
fetchCollections();
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<template>
|
|
149
|
+
<div class="flex flex-col gap-4">
|
|
150
|
+
<div class="flex items-center justify-between gap-4">
|
|
151
|
+
<h2 class="text-xl font-semibold">
|
|
152
|
+
Collections
|
|
153
|
+
</h2>
|
|
154
|
+
<div class="flex flex-1 items-center gap-2">
|
|
155
|
+
<UInput
|
|
156
|
+
v-model="globalFilter"
|
|
157
|
+
icon="i-lucide-search"
|
|
158
|
+
placeholder="Filter collections..."
|
|
159
|
+
size="sm"
|
|
160
|
+
class="flex-1 max-w-xs"
|
|
161
|
+
/>
|
|
162
|
+
<UTooltip text="Refresh">
|
|
163
|
+
<UButton
|
|
164
|
+
icon="i-lucide-refresh-cw"
|
|
165
|
+
color="neutral"
|
|
166
|
+
variant="ghost"
|
|
167
|
+
:loading="loading"
|
|
168
|
+
aria-label="Refresh"
|
|
169
|
+
@click="fetchCollections"
|
|
170
|
+
/>
|
|
171
|
+
</UTooltip>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<UTable
|
|
176
|
+
v-model:global-filter="globalFilter"
|
|
177
|
+
v-model:sorting="sorting"
|
|
178
|
+
:data="collections"
|
|
179
|
+
:columns="columns"
|
|
180
|
+
:loading="loading"
|
|
181
|
+
/>
|
|
182
|
+
|
|
183
|
+
<UModal
|
|
184
|
+
v-model:open="isClearBucketModalOpen"
|
|
185
|
+
title="Clear Bucket"
|
|
186
|
+
:ui="{ footer: 'justify-end' }"
|
|
187
|
+
>
|
|
188
|
+
<template #body>
|
|
189
|
+
<p>Are you sure you want to delete every file in the "{{ bucketToClear }}" bucket? The bucket itself is kept, but all files in it are permanently removed. This action cannot be undone.</p>
|
|
190
|
+
</template>
|
|
191
|
+
<template #footer>
|
|
192
|
+
<UButton
|
|
193
|
+
label="Cancel"
|
|
194
|
+
color="neutral"
|
|
195
|
+
variant="ghost"
|
|
196
|
+
@click="isClearBucketModalOpen = false"
|
|
197
|
+
/>
|
|
198
|
+
<UButton
|
|
199
|
+
label="Clear Bucket"
|
|
200
|
+
color="warning"
|
|
201
|
+
icon="i-lucide-eraser"
|
|
202
|
+
@click="handleClearBucket"
|
|
203
|
+
/>
|
|
204
|
+
</template>
|
|
205
|
+
</UModal>
|
|
206
|
+
|
|
207
|
+
<UModal
|
|
208
|
+
v-model:open="isDeleteBucketModalOpen"
|
|
209
|
+
title="Delete Bucket"
|
|
210
|
+
:ui="{ footer: 'justify-end' }"
|
|
211
|
+
>
|
|
212
|
+
<template #body>
|
|
213
|
+
<p>Are you sure you want to delete the "{{ bucketToDelete }}" bucket? This permanently removes both the "{{ bucketToDelete }}.files" and "{{ bucketToDelete }}.chunks" collections. This action cannot be undone.</p>
|
|
214
|
+
</template>
|
|
215
|
+
<template #footer>
|
|
216
|
+
<UButton
|
|
217
|
+
label="Cancel"
|
|
218
|
+
color="neutral"
|
|
219
|
+
variant="ghost"
|
|
220
|
+
@click="isDeleteBucketModalOpen = false"
|
|
221
|
+
/>
|
|
222
|
+
<UButton
|
|
223
|
+
label="Delete Bucket"
|
|
224
|
+
color="error"
|
|
225
|
+
icon="i-lucide-trash-2"
|
|
226
|
+
@click="handleDeleteBucket"
|
|
227
|
+
/>
|
|
228
|
+
</template>
|
|
229
|
+
</UModal>
|
|
230
|
+
</div>
|
|
231
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
infoPath?: string;
|
|
3
|
+
dataPath?: string;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|