Files
dbx/apps/desktop/src/components/sidebar/MongoIndexManagerDialog.vue
T

263 lines
16 KiB
Vue

<script setup lang="ts">
import { toRefs, watch } from "vue";
import { Loader2, Pencil, Plus, RefreshCw, Trash2 } from "@lucide/vue";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
const props = defineProps<{ controller: Record<string, any> }>();
const emit = defineEmits<{ closed: [] }>();
const {
node,
t,
showMongoIndexManagerDialog,
mongoIndexManagerRows,
mongoIndexManagerLoading,
mongoIndexManagerError,
mongoIndexManagerSelectedName,
mongoIndexManagerMode,
mongoIndexManagerSelected,
mongoIndexManagerCollectionName,
loadMongoIndexManagerRows,
selectMongoIndexRow,
startCreateMongoIndexDraft,
startEditMongoIndexDraft,
cancelMongoIndexDraft,
dropSelectedMongoIndexRow,
canDropSelectedMongoIndexRow,
canEditSelectedMongoIndexRow,
confirmEditMongoIndex,
mongoCreateIndexForm,
mongoCreateIndexFieldOptions,
mongoCreateIndexError,
mongoCreateIndexLoading,
mongoIndexKeyTypes,
mongoCreateIndexCanSubmit,
mongoCreateIndexCanAddField,
addMongoCreateIndexField,
removeMongoCreateIndexField,
confirmCreateMongoIndex,
} = toRefs(props.controller);
/** Render the transport values (1 / -1) with their human-readable direction. */
function mongoIndexTypeLabel(type: string): string {
if (type === "1") return t.value("contextMenu.mongoIndexAscending");
if (type === "-1") return t.value("contextMenu.mongoIndexDescending");
return type;
}
watch(showMongoIndexManagerDialog, (open) => {
if (!open) emit("closed");
});
</script>
<template>
<Dialog v-model:open="showMongoIndexManagerDialog">
<DialogContent class="flex min-w-0 flex-col gap-0 p-0 sm:max-w-[760px]">
<DialogHeader class="border-b px-5 py-4">
<DialogTitle>{{ t("contextMenu.manageMongoIndexesTitle", { collection: mongoIndexManagerCollectionName }) }}</DialogTitle>
<p class="font-mono text-xs text-muted-foreground">{{ node.database }} / {{ mongoIndexManagerCollectionName }}</p>
</DialogHeader>
<div class="flex items-center gap-2 border-b px-5 py-2">
<Button type="button" variant="outline" size="sm" :disabled="mongoIndexManagerLoading || mongoCreateIndexLoading || mongoIndexManagerMode === 'create' || mongoIndexManagerMode === 'edit'" @click="startCreateMongoIndexDraft">
<Plus class="mr-1 h-4 w-4" />
{{ t("contextMenu.createMongoIndex") }}
</Button>
<Button type="button" variant="outline" size="sm" :disabled="mongoIndexManagerLoading || mongoCreateIndexLoading || !canEditSelectedMongoIndexRow" @click="startEditMongoIndexDraft">
<Pencil class="mr-1 h-4 w-4" />
{{ t("contextMenu.editMongoIndex") }}
</Button>
<Button type="button" variant="outline" size="sm" class="text-destructive hover:text-destructive" :disabled="mongoIndexManagerLoading || mongoCreateIndexLoading || !canDropSelectedMongoIndexRow" @click="dropSelectedMongoIndexRow">
<Trash2 class="mr-1 h-4 w-4" />
{{ t("contextMenu.dropIndex") }}
</Button>
<Button
type="button"
variant="ghost"
size="sm"
class="ml-auto"
:disabled="mongoIndexManagerLoading || mongoCreateIndexLoading || mongoIndexManagerMode === 'create' || mongoIndexManagerMode === 'edit'"
:title="t('contextMenu.refreshChildren')"
:aria-label="t('contextMenu.refreshChildren')"
@click="loadMongoIndexManagerRows"
>
<RefreshCw class="h-4 w-4" :class="mongoIndexManagerLoading ? 'animate-spin' : ''" />
</Button>
</div>
<section class="min-w-0">
<div class="flex min-w-0 gap-2 border-b bg-muted/40 px-5 py-1.5 text-xs text-muted-foreground">
<span class="min-w-0 flex-1">{{ t("structureEditor.indexName") }}</span>
<span class="min-w-0 flex-1">{{ t("contextMenu.mongoIndexKeys") }}</span>
</div>
<div class="h-[200px] min-w-0 overflow-auto">
<div v-if="mongoIndexManagerLoading" class="flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("common.loading") }}
</div>
<p v-else-if="mongoIndexManagerError" class="min-w-0 whitespace-pre-wrap break-all px-5 py-6 text-sm text-destructive">{{ mongoIndexManagerError }}</p>
<p v-else-if="mongoIndexManagerRows.length === 0" class="px-5 py-10 text-center text-sm text-muted-foreground">{{ t("contextMenu.mongoIndexEmpty") }}</p>
<button
v-for="row in mongoIndexManagerRows"
v-else
:key="row.name"
type="button"
class="flex w-full min-w-0 gap-2 px-5 py-1.5 text-left text-sm hover:bg-muted/60"
:class="row.name === mongoIndexManagerSelectedName ? 'bg-primary/10' : ''"
:disabled="mongoIndexManagerMode === 'create' || mongoIndexManagerMode === 'edit'"
@click="selectMongoIndexRow(row.name)"
>
<span class="min-w-0 flex-1 truncate font-mono">{{ row.name }}</span>
<span class="min-w-0 flex-1 truncate font-mono text-muted-foreground">{{ row.keys }}</span>
</button>
</div>
</section>
<section class="min-w-0 border-t bg-muted/20 px-5 py-4">
<div v-if="mongoIndexManagerMode === 'create' || mongoIndexManagerMode === 'edit'" class="grid min-w-0 gap-4">
<div class="grid min-w-0 gap-2">
<div class="flex items-center justify-between gap-3">
<span class="text-sm font-medium">{{ t("contextMenu.createMongoIndexFields") }}</span>
<Button type="button" variant="outline" size="sm" :disabled="mongoCreateIndexLoading || !mongoCreateIndexCanAddField" @click="addMongoCreateIndexField">
<Plus class="mr-1 h-4 w-4" />
{{ t("mongo.addField") }}
</Button>
</div>
<div v-for="field in mongoCreateIndexForm.fields" :key="field.id" class="flex min-w-0 items-center gap-2">
<Input v-model="field.path" :list="`mongo-index-manager-fields-${field.id}`" :disabled="mongoCreateIndexLoading" :placeholder="t('mongo.fieldPlaceholder')" :aria-label="t('mongo.field')" class="h-8 min-w-0 flex-1" autocomplete="off" />
<datalist :id="`mongo-index-manager-fields-${field.id}`">
<option v-for="option in mongoCreateIndexFieldOptions" :key="option" :value="option"></option>
</datalist>
<Select v-model="field.type" :disabled="mongoCreateIndexLoading">
<SelectTrigger class="h-8 w-36 shrink-0" :aria-label="t('structureEditor.indexType')">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="type in mongoIndexKeyTypes" :key="type" :value="type">{{ mongoIndexTypeLabel(type) }}</SelectItem>
</SelectContent>
</Select>
<Button type="button" variant="ghost" size="icon" class="h-8 w-8" :disabled="mongoCreateIndexLoading || mongoCreateIndexForm.fields.length === 1" :title="t('structureEditor.remove')" :aria-label="t('structureEditor.remove')" @click="removeMongoCreateIndexField(field.id)">
<Trash2 class="h-4 w-4" />
</Button>
</div>
</div>
<label class="grid min-w-0 gap-1.5 text-sm font-medium">
{{ t("structureEditor.indexName") }}
<Input v-model="mongoCreateIndexForm.name" :disabled="mongoCreateIndexLoading" :placeholder="t('contextMenu.createMongoIndexNamePlaceholder')" :aria-label="t('structureEditor.indexName')" class="h-8" />
</label>
<div class="grid min-w-0 gap-2.5">
<label class="flex w-fit cursor-pointer items-center gap-2 text-sm">
<Switch v-model="mongoCreateIndexForm.unique" :disabled="mongoCreateIndexLoading" :aria-label="t('structureEditor.unique')" />
{{ t("structureEditor.unique") }}
</label>
<label class="flex w-fit cursor-pointer items-center gap-2 text-sm">
<Switch v-model="mongoCreateIndexForm.sparse" :disabled="mongoCreateIndexLoading" :aria-label="t('contextMenu.createMongoIndexSparse')" />
{{ t("contextMenu.createMongoIndexSparse") }}
</label>
<label v-if="mongoIndexManagerMode === 'edit' || mongoCreateIndexForm.hidden" class="flex w-fit cursor-pointer items-center gap-2 text-sm">
<Switch v-model="mongoCreateIndexForm.hidden" :disabled="mongoCreateIndexLoading" :aria-label="t('mongo.indexHidden')" />
{{ t("mongo.indexHidden") }}
</label>
<label class="grid min-w-0 gap-1.5 text-sm">
{{ t("mongo.indexExpireAfterSeconds") }}
<Input v-model="mongoCreateIndexForm.expireAfterSeconds" :disabled="mongoCreateIndexLoading" inputmode="numeric" :placeholder="t('mongo.indexExpireAfterSecondsPlaceholder')" :aria-label="t('mongo.indexExpireAfterSeconds')" class="h-8 max-w-[260px]" />
<span class="text-xs text-muted-foreground">{{ t("mongo.indexExpireAfterSecondsHint") }}</span>
</label>
<label class="grid min-w-0 gap-1.5 text-sm">
{{ t("mongo.indexPartialFilter") }}
<Input v-model="mongoCreateIndexForm.partialFilterExpression" :disabled="mongoCreateIndexLoading" placeholder='{ "status": "active" }' :aria-label="t('mongo.indexPartialFilter')" class="h-8 font-mono" />
</label>
</div>
<details class="min-w-0 rounded-md border bg-background px-3 py-2">
<summary class="cursor-pointer text-sm font-medium">{{ t("mongo.indexLegacyOptions") }}</summary>
<div class="grid min-w-0 gap-2.5 pt-3">
<p class="text-xs text-muted-foreground">{{ t("mongo.indexLegacyOptionsHint") }}</p>
<label class="flex w-fit cursor-pointer items-center gap-2 text-sm">
<Switch v-model="mongoCreateIndexForm.background" :disabled="mongoCreateIndexLoading" :aria-label="t('mongo.indexBackground')" />
{{ t("mongo.indexBackground") }}
</label>
<label class="grid min-w-0 gap-1.5 text-sm">
{{ t("mongo.indexBucketSize") }}
<Input v-model="mongoCreateIndexForm.bucketSize" :disabled="mongoCreateIndexLoading" inputmode="numeric" :aria-label="t('mongo.indexBucketSize')" class="h-8 max-w-[260px]" />
</label>
</div>
</details>
<p v-if="mongoIndexManagerMode === 'edit' && mongoIndexManagerSelected?.extraOptions" class="text-xs text-muted-foreground">{{ t("contextMenu.mongoIndexExtraOptionsPreserved") }}</p>
<p v-if="mongoIndexManagerMode === 'edit'" class="text-xs text-muted-foreground">{{ t("contextMenu.mongoIndexRebuildHint") }}</p>
<p v-if="mongoCreateIndexError" class="min-w-0 max-w-full whitespace-pre-wrap break-all text-sm text-destructive">{{ mongoCreateIndexError }}</p>
<div class="flex items-center justify-end gap-2">
<Button variant="outline" size="sm" :disabled="mongoCreateIndexLoading" @click="cancelMongoIndexDraft">{{ t("dangerDialog.cancel") }}</Button>
<Button size="sm" :disabled="mongoCreateIndexLoading || !mongoCreateIndexCanSubmit" @click="mongoIndexManagerMode === 'edit' ? confirmEditMongoIndex() : confirmCreateMongoIndex()">
<Loader2 v-if="mongoCreateIndexLoading" class="mr-2 h-4 w-4 animate-spin" />
{{ mongoIndexManagerMode === "edit" ? t("contextMenu.saveMongoIndex") : t("contextMenu.createMongoIndex") }}
</Button>
</div>
</div>
<div v-else class="grid min-w-0 gap-3">
<p v-if="!mongoIndexManagerSelected" class="text-sm text-muted-foreground">{{ t("contextMenu.mongoIndexSelectHint") }}</p>
<template v-else>
<div class="grid min-w-0 gap-1.5">
<span class="text-xs text-muted-foreground">{{ t("structureEditor.indexName") }}</span>
<span class="min-w-0 break-all font-mono text-sm">{{ mongoIndexManagerSelected.name }}</span>
</div>
<div class="grid min-w-0 gap-1.5">
<span class="text-xs text-muted-foreground">{{ t("contextMenu.mongoIndexKeys") }}</span>
<span class="min-w-0 break-all font-mono text-sm">{{ mongoIndexManagerSelected.keys }}</span>
</div>
<!-- MongoDB has no ALTER INDEX, so existing properties stay read-only. -->
<div v-if="mongoIndexManagerMode !== 'edit'" class="flex min-w-0 flex-wrap gap-x-6 gap-y-2">
<label class="flex w-fit items-center gap-2 text-sm text-muted-foreground">
<Switch :model-value="mongoIndexManagerSelected.isUnique" disabled :aria-label="t('structureEditor.unique')" />
{{ t("structureEditor.unique") }}
</label>
<template v-if="mongoIndexManagerSelected.propertiesComplete">
<label class="flex w-fit items-center gap-2 text-sm text-muted-foreground">
<Switch :model-value="mongoIndexManagerSelected.isSparse" disabled :aria-label="t('mongo.indexSparse')" />
{{ t("mongo.indexSparse") }}
</label>
<label class="flex w-fit items-center gap-2 text-sm text-muted-foreground">
<Switch :model-value="mongoIndexManagerSelected.background" disabled :aria-label="t('mongo.indexBackground')" />
{{ t("mongo.indexBackground") }}
</label>
<label v-if="mongoIndexManagerSelected.hidden" class="flex w-fit items-center gap-2 text-sm text-muted-foreground">
<Switch :model-value="true" disabled :aria-label="t('mongo.indexHidden')" />
{{ t("mongo.indexHidden") }}
</label>
</template>
</div>
<template v-if="mongoIndexManagerSelected.propertiesComplete">
<div class="grid min-w-0 gap-1.5">
<span class="text-xs text-muted-foreground">{{ t("mongo.indexExpireAfterSeconds") }}</span>
<span class="min-w-0 break-all font-mono text-sm">{{ mongoIndexManagerSelected.expireAfterSeconds ?? t("mongo.indexNoExpiry") }}</span>
</div>
<div v-if="mongoIndexManagerSelected.bucketSize !== undefined" class="grid min-w-0 gap-1.5">
<span class="text-xs text-muted-foreground">{{ t("mongo.indexBucketSize") }}</span>
<span class="min-w-0 break-all font-mono text-sm">{{ mongoIndexManagerSelected.bucketSize }}</span>
</div>
</template>
<div v-if="mongoIndexManagerSelected.partialFilterExpression" class="grid min-w-0 gap-1.5">
<span class="text-xs text-muted-foreground">{{ t("mongo.indexPartialFilter") }}</span>
<span class="min-w-0 whitespace-pre-wrap break-all font-mono text-xs">{{ mongoIndexManagerSelected.partialFilterExpression }}</span>
</div>
<div v-if="mongoIndexManagerSelected.extraOptions" class="grid min-w-0 gap-1.5">
<span class="text-xs text-muted-foreground">{{ t("mongo.indexOtherOptions") }}</span>
<span class="min-w-0 whitespace-pre-wrap break-all font-mono text-xs">{{ mongoIndexManagerSelected.extraOptions }}</span>
</div>
<!-- The Legacy driver cannot report sparse/TTL/background, so say so
instead of rendering the defaults as if the server had sent them. -->
<p v-if="!mongoIndexManagerSelected.propertiesComplete" class="text-xs text-muted-foreground">{{ t("mongo.indexPropertiesUnavailable") }}</p>
<p v-if="mongoIndexManagerMode !== 'edit'" class="text-xs text-muted-foreground">{{ t("contextMenu.mongoIndexReadOnlyHint") }}</p>
</template>
</div>
</section>
</DialogContent>
</Dialog>
</template>