Files
dbx/apps/desktop/src/components/meilisearch/MeilisearchIndexView.vue
T

151 lines
5.9 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import "dayjs/locale/zh-cn";
import "dayjs/locale/zh-tw";
import "dayjs/locale/es";
import "dayjs/locale/it";
import "dayjs/locale/ja";
import "dayjs/locale/ko";
import "dayjs/locale/pt-br";
import "dayjs/locale/tr";
import { Copy, FileText, ListChecks, Settings } from "@lucide/vue";
import * as api from "@/lib/backend/api";
import type { MeilisearchIndexOverview } from "@/lib/backend/tauri";
import { formatBytes } from "@/lib/database/serverMetrics";
import { useToast } from "@/composables/useToast";
import MeilisearchDocumentsPage from "./MeilisearchDocumentsPage.vue";
import MeilisearchSettingsPage from "./MeilisearchSettingsPage.vue";
import MeilisearchTasksPage from "./MeilisearchTasksPage.vue";
dayjs.extend(relativeTime);
const DAYJS_LOCALES: Record<string, string> = {
"zh-CN": "zh-cn",
"zh-TW": "zh-tw",
"pt-BR": "pt-br",
es: "es",
it: "it",
ja: "ja",
ko: "ko",
tr: "tr",
};
const props = defineProps<{
connectionId: string;
index: string;
}>();
type ActiveSection = "documents" | "tasks" | "settings";
const { t, locale } = useI18n();
const { toast } = useToast();
const activeSection = ref<ActiveSection>("documents");
const overview = ref<MeilisearchIndexOverview | null>(null);
const navSections = computed<Array<{ value: ActiveSection; label: string; icon: typeof FileText }>>(() => [
{ value: "documents", label: t("meilisearch.documents"), icon: FileText },
{ value: "tasks", label: t("meilisearch.tasks"), icon: ListChecks },
{ value: "settings", label: t("meilisearch.settings"), icon: Settings },
]);
const updatedAtLabel = computed(() => {
const value = overview.value?.updatedAt;
if (!value) return "-";
return dayjs(value)
.locale(DAYJS_LOCALES[locale.value] ?? "en")
.fromNow();
});
async function refreshStats() {
try {
overview.value = await api.meilisearchGetIndexOverview(props.connectionId, props.index);
} catch {
// Overview is best-effort; the tab still works without it.
}
}
async function copyText(value: string | null | undefined) {
if (!value) return;
try {
await navigator.clipboard.writeText(value);
toast(t("meilisearch.copied"));
} catch {
// Clipboard may be unavailable (e.g. permission denied); stay silent.
}
}
onMounted(() => {
void refreshStats();
});
</script>
<template>
<div class="h-full flex overflow-hidden">
<!-- 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 v-if="overview?.isIndexing" class="mt-0.5 text-xs text-primary">{{ t("meilisearch.isIndexing") }}</div>
</div>
<div class="space-y-2.5 px-1 pb-2 text-xs">
<div>
<div class="text-muted-foreground">{{ t("meilisearch.uid") }}</div>
<div class="mt-0.5 flex items-center gap-1">
<span class="min-w-0 truncate font-mono text-foreground/80" :title="overview?.uid ?? index">{{ overview?.uid ?? index }}</span>
<button type="button" class="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground" :title="t('common.copy')" @click="copyText(overview?.uid ?? index)">
<Copy class="h-3 w-3" />
</button>
</div>
</div>
<div>
<div class="text-muted-foreground">{{ t("meilisearch.updatedAt") }}</div>
<div class="mt-0.5 text-foreground/80" :title="overview?.updatedAt ?? undefined">{{ updatedAtLabel }}</div>
</div>
<div>
<div class="text-muted-foreground">{{ t("meilisearch.primaryKey") }}</div>
<div class="mt-0.5 flex items-center gap-1">
<span class="min-w-0 truncate font-mono text-foreground/80" :title="overview?.primaryKey ?? undefined">{{ overview?.primaryKey ?? "-" }}</span>
<button v-if="overview?.primaryKey" type="button" class="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground" :title="t('common.copy')" @click="copyText(overview.primaryKey)">
<Copy class="h-3 w-3" />
</button>
</div>
</div>
<div>
<div class="text-muted-foreground">{{ t("meilisearch.documentCountLabel") }}</div>
<div class="mt-0.5 tabular-nums text-foreground/80">{{ overview ? overview.numberOfDocuments : "-" }}</div>
</div>
<div v-if="overview?.databaseSize != null">
<div class="text-muted-foreground">{{ t("meilisearch.databaseSize") }}</div>
<div class="mt-0.5 tabular-nums text-foreground/80">{{ formatBytes(overview.databaseSize) }}</div>
</div>
</div>
<div class="mx-1 border-t border-border/60" />
<button
v-for="section in navSections"
:key="section.value"
type="button"
class="mt-1 flex items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs transition-colors"
:class="activeSection === section.value ? 'bg-accent text-foreground font-medium' : 'text-muted-foreground hover:bg-muted/70 hover:text-foreground'"
@click="activeSection = section.value"
>
<component :is="section.icon" class="h-3.5 w-3.5 shrink-0" />
<span>{{ section.label }}</span>
</button>
</nav>
<!-- Content -->
<div class="flex-1 min-h-0 min-w-0 overflow-hidden">
<MeilisearchDocumentsPage v-if="activeSection === 'documents'" :connection-id="connectionId" :index="index" @refresh-stats="refreshStats" />
<MeilisearchTasksPage v-else-if="activeSection === 'tasks'" :connection-id="connectionId" :fixed-index-uid="index" />
<MeilisearchSettingsPage v-else :connection-id="connectionId" :index="index" @refresh-stats="refreshStats" />
</div>
</div>
</template>