fix(meilisearch): add index stats refresh with manual reload

This commit is contained in:
二丫讲梵
2026-09-23 15:24:24 +08:00
committed by GitHub
parent 517ddbfa05
commit d13bc37bea
2 changed files with 67 additions and 7 deletions
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import { Copy, FileText, ListChecks, Settings } from "@lucide/vue";
import { Copy, FileText, ListChecks, RefreshCcw, Settings } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import * as api from "@/lib/backend/api";
import type { MeilisearchIndexOverview } from "@/lib/backend/tauri";
import { formatBytes } from "@/lib/database/serverMetrics";
@@ -26,6 +27,8 @@ const { toast } = useToast();
const activeSection = ref<ActiveSection>(restoredUiState.activeSection ?? "documents");
const overview = ref<MeilisearchIndexOverview | null>(null);
const refreshing = ref(false);
let refreshRequestId = 0;
trackUiState(() => ({ activeSection: activeSection.value }));
@@ -41,11 +44,17 @@ const updatedAtLabel = computed(() => {
return formatMeilisearchTaskDateTime(value, locale.value);
});
async function refreshStats() {
async function refreshStats(notifyOnError = false) {
const requestId = ++refreshRequestId;
refreshing.value = true;
try {
overview.value = await api.meilisearchGetIndexOverview(props.connectionId, props.index);
} catch {
const nextOverview = await api.meilisearchGetIndexOverview(props.connectionId, props.index);
if (requestId === refreshRequestId) overview.value = nextOverview;
} catch (cause: any) {
// Overview is best-effort; the tab still works without it.
if (requestId === refreshRequestId && notifyOnError) toast(cause?.message || String(cause), 5000);
} finally {
if (requestId === refreshRequestId) refreshing.value = false;
}
}
@@ -69,7 +78,12 @@ onMounted(() => {
<!-- Left column: index meta + navigation -->
<nav class="w-44 shrink-0 border-r flex flex-col gap-1 overflow-y-auto p-2">
<div class="px-1 pb-2">
<div class="truncate text-sm font-semibold text-foreground" :title="index">{{ index }}</div>
<div class="flex items-center gap-1">
<div class="min-w-0 flex-1 truncate text-sm font-semibold text-foreground" :title="index">{{ index }}</div>
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" :disabled="refreshing" :title="t('meilisearch.refresh')" :aria-label="t('meilisearch.refresh')" @click="refreshStats(true)">
<RefreshCcw class="h-3.5 w-3.5" :class="{ 'animate-spin': refreshing }" />
</Button>
</div>
<div v-if="overview?.isIndexing" class="mt-0.5 text-xs text-primary">{{ t("meilisearch.isIndexing") }}</div>
</div>
@@ -3,7 +3,7 @@
import { createApp, defineComponent, h, nextTick, ref, type Component } from "vue";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({ getIndexOverview: vi.fn() }));
const mocks = vi.hoisted(() => ({ getIndexOverview: vi.fn(), toast: vi.fn() }));
function passthrough(tag: string): Component {
return defineComponent({
@@ -21,10 +21,11 @@ vi.mock("@lucide/vue", () => ({
Copy: passthrough("span"),
FileText: passthrough("span"),
ListChecks: passthrough("span"),
RefreshCcw: passthrough("span"),
Settings: passthrough("span"),
}));
vi.mock("@/lib/backend/api", () => ({ meilisearchGetIndexOverview: mocks.getIndexOverview }));
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) }));
vi.mock("@/lib/tabs/tabUiState", () => ({ useTabUiState: () => ({ initialState: {}, track: () => {} }) }));
vi.mock("@/components/meilisearch/MeilisearchDocumentsPage.vue", () => ({ default: passthrough("div") }));
vi.mock("@/components/meilisearch/MeilisearchSettingsPage.vue", () => ({ default: passthrough("div") }));
@@ -105,3 +106,48 @@ describe("MeilisearchIndexView storage rows", () => {
expect(text).toContain("25.7 MB");
});
});
describe("MeilisearchIndexView refresh", () => {
const overview = {
uid: "report_template",
primaryKey: "id",
createdAt: "2026-09-21T06:30:00Z",
updatedAt: "2026-09-21T06:30:46Z",
numberOfDocuments: 256,
isIndexing: false,
documentSize: 309_329_920,
avgDocumentSize: 1_208_320,
databaseSize: 4_194_304_000,
};
function refreshButton(mounted: HTMLElement): HTMLButtonElement {
const button = mounted.querySelector<HTMLButtonElement>('button[aria-label="meilisearch.refresh"]');
expect(button).toBeTruthy();
return button!;
}
it("refetches the overview and renders the fresh numbers when clicked", async () => {
mocks.getIndexOverview.mockResolvedValueOnce(overview).mockResolvedValueOnce({ ...overview, numberOfDocuments: 512 });
const mounted = await mountIndexView();
expect(mounted.textContent).toContain("256");
refreshButton(mounted).click();
await vi.waitFor(() => expect(mocks.getIndexOverview).toHaveBeenCalledTimes(2));
expect(mocks.getIndexOverview).toHaveBeenNthCalledWith(2, "c1", "report_template");
await vi.waitFor(() => expect(mounted.textContent).toContain("512"));
expect(mounted.textContent).not.toContain("256");
});
it("toasts when a manual refresh fails", async () => {
mocks.getIndexOverview.mockResolvedValueOnce(overview).mockRejectedValueOnce(new Error("index unavailable"));
const mounted = await mountIndexView();
expect(mocks.toast).not.toHaveBeenCalled();
refreshButton(mounted).click();
await vi.waitFor(() => expect(mocks.toast).toHaveBeenCalledWith("index unavailable", 5000));
// The mounted overview stays visible; the failure only notifies.
expect(mounted.textContent).toContain("256");
});
});