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;
@@ -36,6 +36,7 @@ import { elasticsearchClearIndexPreview, isElasticsearchProtocolIndex, isPartial
import { supportsMongoAllDriverMutations, supportsMongoIndexMutations, supportsNativeMongoDriverMutations } from "@/lib/mongo/mongoCapabilities";
import { runMongoSidebarMutation } from "@/lib/sidebar/runMongoSidebarMutation";
import { executeWithProductionContextGuard } from "@/lib/database/productionExecutionGuard";
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
import { uuid } from "@/lib/common/utils";
import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata";
import type { NacosAdminConfig } from "@/types/nacos";
@@ -77,6 +78,11 @@ import {
cloneMongoCollectionError,
cloneMongoCollectionLoading,
showCreateMongoIndexDialog,
showCreateMeilisearchIndexDialog,
meilisearchCreateIndexUid,
meilisearchCreateIndexPrimaryKey,
meilisearchCreateIndexError,
meilisearchCreateIndexLoading,
mongoCreateIndexForm,
mongoCreateIndexFieldOptions,
mongoCreateIndexError,
@@ -368,6 +374,48 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
});
}
const canCreateMeilisearchIndex = computed(() => {
const node = activeNode.value;
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
return node.type === "connection" && config?.db_type === "meilisearch" && !connectionIsEffectivelyReadOnly(config);
});
function prepareCreateMeilisearchIndexDialog() {
if (!canCreateMeilisearchIndex.value) return;
meilisearchCreateIndexUid.value = "";
meilisearchCreateIndexPrimaryKey.value = "";
meilisearchCreateIndexError.value = "";
meilisearchCreateIndexLoading.value = false;
showCreateMeilisearchIndexDialog.value = true;
}
async function confirmCreateMeilisearchIndex() {
const node = sidebarFormTarget.value ?? activeNode.value;
const connectionId = node.connectionId;
const uid = meilisearchCreateIndexUid.value.trim();
if (!canCreateMeilisearchIndex.value || node.type !== "connection" || !connectionId) return;
if (!uid) {
meilisearchCreateIndexError.value = t("meilisearch.createIndexUidRequired");
return;
}
meilisearchCreateIndexLoading.value = true;
meilisearchCreateIndexError.value = "";
try {
await connectionStore.ensureConnected(connectionId);
await api.meilisearchCreateIndex(connectionId, {
uid,
primaryKey: meilisearchCreateIndexPrimaryKey.value.trim() || undefined,
});
showCreateMeilisearchIndexDialog.value = false;
toast(t("meilisearch.indexCreated"), 3000);
await connectionStore.loadElasticsearchIndices(connectionId);
} catch (error) {
meilisearchCreateIndexError.value = translateBackendError(t, errorMessage(error));
} finally {
meilisearchCreateIndexLoading.value = false;
}
}
async function loadMongoIndexManagerRows() {
const node = sidebarFormTarget.value ?? activeNode.value;
const connectionId = node.connectionId;
@@ -1219,6 +1267,9 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
mongoCreateIndexCanSubmit,
mongoCreateIndexCanAddField,
prepareCreateMongoIndexDialog,
canCreateMeilisearchIndex,
prepareCreateMeilisearchIndexDialog,
confirmCreateMeilisearchIndex,
addMongoCreateIndexField,
removeMongoCreateIndexField,
confirmCreateMongoIndex,
@@ -106,6 +106,14 @@ const en = {
taskOperationFinished: "Task operation finished with status {status}",
taskPollingTimeout: "Task #{uid} is still pending after 60 seconds. Refresh manually to check it.",
taskOutsideIndex: "This task does not belong to the current index.",
createIndex: "Create index",
createIndexUid: "Index UID",
createIndexPrimaryKey: "Primary key",
indexCreated: "Index created",
createIndexUidRequired: "Index UID is required.",
createIndexUidPlaceholder: "Enter index UID",
createIndexPrimaryKeyPlaceholder: "Optional primary key",
createIndexPrimaryKeyHelp: "Leave empty to set the primary key when adding the first document.",
};
const zhCN = {
@@ -217,6 +225,14 @@ const zhCN = {
taskOperationFinished: "任务操作结束,状态为 {status}",
taskPollingTimeout: "任务 #{uid} 在 60 秒后仍未完成,请手动刷新查看。",
taskOutsideIndex: "该任务不属于当前索引。",
createIndex: "创建索引",
createIndexUid: "索引 UID",
createIndexPrimaryKey: "主键",
indexCreated: "索引已创建",
createIndexUidRequired: "索引 UID 不能为空。",
createIndexUidPlaceholder: "输入索引 UID",
createIndexPrimaryKeyPlaceholder: "可选,输入主键字段",
createIndexPrimaryKeyHelp: "留空时可在添加第一条文档时设置主键。",
};
export const meilisearchManagementMessages = {
+1
View File
@@ -838,6 +838,7 @@ export const meilisearchGetIndexSettings = forward("meilisearchGetIndexSettings"
export const meilisearchUpdateIndexSettings = forward("meilisearchUpdateIndexSettings");
export const meilisearchGetIndexStats = forward("meilisearchGetIndexStats");
export const meilisearchGetIndexOverview = forward("meilisearchGetIndexOverview");
export const meilisearchCreateIndex = forward("meilisearchCreateIndex");
export const meilisearchDeleteIndex = forward("meilisearchDeleteIndex");
export const meilisearchDeleteAllDocuments = forward("meilisearchDeleteAllDocuments");
export const meilisearchGetSystemOverview = forward("meilisearchGetSystemOverview");
+5 -1
View File
@@ -57,7 +57,7 @@ import { normalizeRustMongoCommand, type MongoCommand } from "@/lib/mongo/mongoS
import type { MongoBulkWriteResult } from "@/lib/mongo/mongoShellCommand";
import { BackendErrorException, type BackendError } from "@/lib/backend/errorUtils";
import { decodeMeilisearchDocumentPage, decodeMeilisearchSearchResult, type MeilisearchDocumentPage, type MeilisearchDocumentPageWire, type MeilisearchSearchResult, type MeilisearchSearchWireResult } from "@/lib/backend/meilisearchTransport";
import type { CreatedKey, EnqueuedTaskSummary, KeyCreateInput, KeyListItem, KeyPage, KeyUpdateInput, MeilisearchSystemOverview, MeilisearchTask, TaskListInput, TaskPage, TaskSelector } from "@/types/meilisearchManagement";
import type { CreatedKey, EnqueuedTaskSummary, KeyCreateInput, KeyListItem, KeyPage, KeyUpdateInput, MeilisearchCreateIndexInput, MeilisearchSystemOverview, MeilisearchTask, TaskListInput, TaskPage, TaskSelector } from "@/types/meilisearchManagement";
import type { CollectionInfo } from "@/types/database";
import type { SchemaDiffPreparation, SchemaDiffPreparationOptions, SchemaSyncSqlPlan, SelectedSchemaDiffInput, GenerateSchemaSyncPlanOptions, TableDiff, FunctionDiff, SequenceDiff, RuleDiff, OwnerDiff } from "@/lib/schema/schemaDiff";
import type { SidebarObjectKind } from "@/lib/database/databaseObjectCapabilities";
@@ -4862,6 +4862,10 @@ export async function meilisearchGetIndexOverview(connectionId: string, index: s
});
}
export async function meilisearchCreateIndex(connectionId: string, input: MeilisearchCreateIndexInput): Promise<void> {
return post("/api/document-store/meilisearch/index/create", { connectionId, input });
}
export async function meilisearchDeleteIndex(connectionId: string, index: string): Promise<void> {
return post("/api/document-store/meilisearch/index/delete", {
connectionId,
+5 -1
View File
@@ -17,7 +17,7 @@ import { ExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { decodeMeilisearchDocumentPage, decodeMeilisearchSearchResult, type MeilisearchDocumentPage, type MeilisearchDocumentPageWire, type MeilisearchSearchResult, type MeilisearchSearchWireResult } from "@/lib/backend/meilisearchTransport";
import type { XuguTablespaceInfo } from "@/types/database";
import type { CreatedKey, EnqueuedTaskSummary, KeyCreateInput, KeyListItem, KeyPage, KeyUpdateInput, MeilisearchSystemOverview, MeilisearchTask, TaskListInput, TaskPage, TaskSelector } from "@/types/meilisearchManagement";
import type { CreatedKey, EnqueuedTaskSummary, KeyCreateInput, KeyListItem, KeyPage, KeyUpdateInput, MeilisearchCreateIndexInput, MeilisearchSystemOverview, MeilisearchTask, TaskListInput, TaskPage, TaskSelector } from "@/types/meilisearchManagement";
import type { CsvQuoteMode } from "@/lib/export/csvQuoteMode";
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
@@ -4665,6 +4665,10 @@ export async function meilisearchGetIndexOverview(connectionId: string, index: s
});
}
export async function meilisearchCreateIndex(connectionId: string, input: MeilisearchCreateIndexInput): Promise<void> {
return invoke("meilisearch_create_index", { connectionId, input });
}
export async function meilisearchDeleteIndex(connectionId: string, index: string): Promise<void> {
return invoke("meilisearch_delete_index", {
connectionId,
@@ -125,6 +125,11 @@ export interface EnqueuedTaskSummary {
enqueuedAt?: string | null;
}
export interface MeilisearchCreateIndexInput {
uid: string;
primaryKey?: string;
}
export interface TaskListInput {
selector: TaskSelector;
from?: number | null;