feat(meilisearch): add index creation

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-20 01:18:26 +08:00
1 parent 28cc1b9768
commit 5b937388e5
18 files changed
+304 -4

No files matched your search

@@ -1,19 +1,32 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import { Activity, Database, FileText, KeyRound, RefreshCcw, Tags } from "@lucide/vue";
import { Activity, Database, FileText, KeyRound, Loader2, Plus, RefreshCcw, Tags } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import ErrorBanner from "@/components/ui/ErrorBanner.vue";
import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
import * as api from "@/lib/backend/api";
import { formatBytes } from "@/lib/database/serverMetrics";
import type { MeilisearchSystemOverview, OverviewSection } from "@/types/meilisearchManagement";
import { useConnectionStore } from "@/stores/connectionStore";
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
import { useToast } from "@/composables/useToast";
const props = defineProps<{ connectionId: string }>();
const { t } = useI18n();
const { toast } = useToast();
const connectionStore = useConnectionStore();
const loading = ref(false);
const error = ref("");
const overview = ref<MeilisearchSystemOverview | null>(null);
const createOpen = ref(false);
const createWorking = ref(false);
const createError = ref("");
const createUid = ref("");
const createPrimaryKey = ref("");
const readOnly = computed(() => connectionIsEffectivelyReadOnly(connectionStore.getConfig(props.connectionId)));
const stats = computed(() => overview.value?.stats.data ?? null);
const cards = computed(() => [
@@ -42,6 +55,42 @@ async function load() {
}
}
function openCreate() {
createUid.value = "";
createPrimaryKey.value = "";
createError.value = "";
createOpen.value = true;
}
async function createIndex() {
const uid = createUid.value.trim();
if (!uid) {
createError.value = t("meilisearch.createIndexUidRequired");
return;
}
createWorking.value = true;
createError.value = "";
try {
await api.meilisearchCreateIndex(props.connectionId, {
uid,
...(createPrimaryKey.value.trim() ? { primaryKey: createPrimaryKey.value.trim() } : {}),
});
createOpen.value = false;
toast(t("meilisearch.indexCreated"));
await load();
try {
await connectionStore.loadElasticsearchIndices(props.connectionId);
} catch (refreshError) {
console.warn("[DBX][meilisearch-index-refresh:error]", refreshError);
}
} catch (cause: any) {
createError.value = cause?.message || String(cause);
} finally {
createWorking.value = false;
}
}
onMounted(() => void load());
</script>
@@ -52,7 +101,10 @@ onMounted(() => void load());
<h2 class="text-base font-semibold">{{ t("meilisearch.overview") }}</h2>
<p class="text-xs text-muted-foreground">{{ t("meilisearch.overviewDescription") }}</p>
</div>
<Button size="sm" variant="outline" :disabled="loading" @click="load"><RefreshCcw class="mr-1 h-3.5 w-3.5" />{{ t("meilisearch.refresh") }}</Button>
<div class="flex items-center gap-2">
<Button size="sm" variant="outline" :disabled="readOnly" @click="openCreate"><Plus class="mr-1 h-3.5 w-3.5" />{{ t("meilisearch.createIndex") }}</Button>
<Button size="sm" variant="outline" :disabled="loading" @click="load"><RefreshCcw class="mr-1 h-3.5 w-3.5" />{{ t("meilisearch.refresh") }}</Button>
</div>
</div>
<QueryLoadingState v-if="loading && !overview" class="py-12" />
<ErrorBanner v-else-if="error && !overview" :message="error" />
@@ -120,5 +172,27 @@ onMounted(() => void load());
<p v-else class="p-4 text-xs text-muted-foreground">{{ sectionLabel(overview.topIndexes) }}</p>
</section>
</template>
<Dialog v-model:open="createOpen">
<DialogContent class="max-w-md">
<DialogHeader
><DialogTitle>{{ t("meilisearch.createIndex") }}</DialogTitle></DialogHeader
>
<div class="grid gap-3 text-xs">
<label class="grid gap-1"
><span class="font-medium">{{ t("meilisearch.uid") }}</span
><Input v-model="createUid" :disabled="createWorking" :placeholder="t('meilisearch.createIndexUidPlaceholder')"
/></label>
<label class="grid gap-1"
><span class="font-medium">{{ t("meilisearch.primaryKey") }}</span
><Input v-model="createPrimaryKey" :disabled="createWorking" :placeholder="t('meilisearch.createIndexPrimaryKeyPlaceholder')" /><span class="text-[11px] text-muted-foreground">{{ t("meilisearch.createIndexPrimaryKeyHelp") }}</span></label
>
<ErrorBanner v-if="createError" :message="createError" />
</div>
<DialogFooter
><Button variant="outline" :disabled="createWorking" @click="createOpen = false">{{ t("common.cancel") }}</Button
><Button :disabled="createWorking || readOnly" @click="createIndex"><Loader2 v-if="createWorking" class="mr-1 h-3.5 w-3.5 animate-spin" />{{ t("meilisearch.createIndex") }}</Button></DialogFooter
>
</DialogContent>
</Dialog>
</div>
</template>
@@ -10,6 +10,7 @@ import JsonTree from "@/components/common/JsonTree.vue";
import RedisJsonEditor from "@/components/redis/RedisJsonEditor.vue";
import * as api from "@/lib/backend/api";
import { useToast } from "@/composables/useToast";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
const props = defineProps<{
@@ -23,6 +24,7 @@ const emit = defineEmits<{
const { t } = useI18n();
const { toast } = useToast();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const settingsText = ref("");
@@ -116,6 +118,11 @@ async function confirmDeleteIndex() {
await api.meilisearchDeleteIndex(props.connectionId, props.index);
toast(t("meilisearch.indexDeleted"));
deleteIndexConfirmOpen.value = false;
try {
await connectionStore.loadElasticsearchIndices(props.connectionId);
} catch (refreshError) {
console.warn("[DBX][meilisearch-index-refresh:error]", refreshError);
}
// The tab may have been closed while the delete was in flight.
if (ownerTabId && queryStore.tabs.some((tab) => tab.id === ownerTabId)) queryStore.closeTab(ownerTabId);
} catch (e: any) {
@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({
deleteIndex: vi.fn(),
getIndexSettings: vi.fn(),
closeTab: vi.fn(),
refreshTree: vi.fn(),
toast: vi.fn(),
}));
@@ -51,6 +52,7 @@ vi.mock("@/components/common/QueryLoadingState.vue", () => ({ default: passthrou
vi.mock("@/components/common/JsonTree.vue", () => ({ default: passthrough("div") }));
vi.mock("@/components/redis/RedisJsonEditor.vue", () => ({ default: passthrough("div") }));
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) }));
vi.mock("@/stores/connectionStore", () => ({ useConnectionStore: () => ({ loadElasticsearchIndices: mocks.refreshTree }) }));
vi.mock("@/lib/backend/api", () => ({
meilisearchGetIndexSettings: mocks.getIndexSettings,
meilisearchDeleteIndex: mocks.deleteIndex,
@@ -113,6 +113,12 @@ const {
cloneMongoCollectionLoading,
confirmCloneMongoCollection,
showCreateMongoIndexDialog,
showCreateMeilisearchIndexDialog,
meilisearchCreateIndexUid,
meilisearchCreateIndexPrimaryKey,
meilisearchCreateIndexError,
meilisearchCreateIndexLoading,
confirmCreateMeilisearchIndex,
mongoCreateIndexForm,
mongoCreateIndexFieldOptions,
mongoCreateIndexError,
@@ -208,6 +214,7 @@ watch(
showRenameMongoCollectionDialog,
showCloneMongoCollectionDialog,
showCreateMongoIndexDialog,
showCreateMeilisearchIndexDialog,
showMongoIndexManagerDialog,
showRedisDatabaseAliasDialog,
showCreateSchemaDialog,
@@ -401,6 +408,33 @@ watch(
</DialogContent>
</Dialog>
<Dialog v-model:open="showCreateMeilisearchIndexDialog">
<DialogContent class="sm:max-w-[420px]">
<DialogHeader>
<DialogTitle>{{ t("meilisearch.createIndex") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3">
<label class="grid gap-1.5 text-sm font-medium">
{{ t("meilisearch.createIndexUid") }}
<Input v-model="meilisearchCreateIndexUid" :disabled="meilisearchCreateIndexLoading" :placeholder="t('meilisearch.createIndexUidPlaceholder')" @keydown.enter.prevent="confirmCreateMeilisearchIndex" />
</label>
<label class="grid gap-1.5 text-sm font-medium">
{{ t("meilisearch.createIndexPrimaryKey") }}
<Input v-model="meilisearchCreateIndexPrimaryKey" :disabled="meilisearchCreateIndexLoading" :placeholder="t('meilisearch.createIndexPrimaryKeyPlaceholder')" />
<span class="text-xs font-normal text-muted-foreground">{{ t("meilisearch.createIndexPrimaryKeyHelp") }}</span>
</label>
<p v-if="meilisearchCreateIndexError" class="text-sm text-destructive">{{ meilisearchCreateIndexError }}</p>
</div>
<DialogFooter>
<Button variant="outline" :disabled="meilisearchCreateIndexLoading" @click="showCreateMeilisearchIndexDialog = false">{{ t("dangerDialog.cancel") }}</Button>
<Button :disabled="meilisearchCreateIndexLoading || !meilisearchCreateIndexUid.trim()" @click="confirmCreateMeilisearchIndex">
<Loader2 v-if="meilisearchCreateIndexLoading" class="mr-2 h-4 w-4 animate-spin" />
{{ t("meilisearch.createIndex") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="showRedisDatabaseAliasDialog">
<DialogContent class="sm:max-w-[420px]">
<DialogHeader>
@@ -316,6 +316,11 @@ import {
showDropAllMongoIndexesConfirm,
dropAllMongoIndexesLoading,
showCreateMongoIndexDialog,
showCreateMeilisearchIndexDialog,
meilisearchCreateIndexUid,
meilisearchCreateIndexPrimaryKey,
meilisearchCreateIndexError,
meilisearchCreateIndexLoading,
mongoCreateIndexForm,
mongoCreateIndexFieldOptions,
mongoCreateIndexError,
@@ -566,6 +571,9 @@ const {
mongoCreateIndexCanSubmit,
mongoCreateIndexCanAddField,
prepareCreateMongoIndexDialog,
canCreateMeilisearchIndex,
prepareCreateMeilisearchIndexDialog,
confirmCreateMeilisearchIndex,
addMongoCreateIndexField,
removeMongoCreateIndexField,
confirmCreateMongoIndex,
@@ -1446,6 +1454,12 @@ function openCreateMongoIndexDialog() {
prepareCreateMongoIndexDialog();
}
function openCreateMeilisearchIndexDialog() {
claimTreeItemDialogOwnership();
routeTreeItemDialogController();
prepareCreateMeilisearchIndexDialog();
}
function openMongoIndexManagerDialog() {
claimTreeItemDialogOwnership();
routeTreeItemDialogController();
@@ -5311,6 +5325,12 @@ function databaseSpecificDialogCapabilities() {
cloneMongoCollectionLoading,
confirmCloneMongoCollection,
showCreateMongoIndexDialog,
showCreateMeilisearchIndexDialog,
meilisearchCreateIndexUid,
meilisearchCreateIndexPrimaryKey,
meilisearchCreateIndexError,
meilisearchCreateIndexLoading,
confirmCreateMeilisearchIndex,
mongoCreateIndexForm,
mongoCreateIndexFieldOptions,
mongoCreateIndexError,
@@ -5549,6 +5569,9 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
items.push(addToAiMenuItem(node));
}
}
if (canCreateMeilisearchIndex.value) {
items.push({ label: t("meilisearch.createIndex"), action: openCreateMeilisearchIndexDialog, icon: Plus });
}
const connectionWorkspace = node.connectionId ? driverProfileDatabaseWorkspace(connectionStore.getConfig(node.connectionId)?.driver_profile) : undefined;
if (connectionWorkspace?.entryScopes.includes("connection")) {
items.push({ label: t(connectionWorkspace.menuLabelKey), action: openProfileConnectionWorkspace, icon: GitBranch });
@@ -133,6 +133,11 @@ export const dropMongoIndexLoading = ref(false);
export const showDropAllMongoIndexesConfirm = ref(false);
export const dropAllMongoIndexesLoading = ref(false);
export const showCreateMongoIndexDialog = ref(false);
export const showCreateMeilisearchIndexDialog = ref(false);
export const meilisearchCreateIndexUid = ref("");
export const meilisearchCreateIndexPrimaryKey = ref("");
export const meilisearchCreateIndexError = ref("");
export const meilisearchCreateIndexLoading = ref(false);
function emptyMongoCreateIndexForm(): MongoCreateIndexForm {
return {
@@ -234,6 +239,7 @@ const openFlags = [
showDropMongoIndexConfirm,
showDropAllMongoIndexesConfirm,
showCreateMongoIndexDialog,
showCreateMeilisearchIndexDialog,
showMongoIndexManagerDialog,
showClearElasticsearchIndexConfirm,
showFlushRedisDbConfirm,
@@ -268,6 +274,10 @@ export function resetSidebarTreeDialogState() {
cloneMongoCollectionError.value = "";
cloneMongoCollectionLoading.value = false;
resetMongoCreateIndexForm();
meilisearchCreateIndexUid.value = "";
meilisearchCreateIndexPrimaryKey.value = "";
meilisearchCreateIndexError.value = "";
meilisearchCreateIndexLoading.value = false;
resetMongoIndexManager();
vacuumTableExecuting.value = false;
sidebarTreeDialogOwner.value = null;