From 8ecad5010a8724ffef314451502f6a539fbc4ab7 Mon Sep 17 00:00:00 2001 From: Peyton Nowlin Date: Tue, 7 Apr 2026 08:46:54 -0400 Subject: [PATCH 1/2] feat(index-list): display last task time for each index with internationalization support --- src/components/biz/IndexConfigEditor.tsx | 30 ++++++--- src/components/biz/IndexList.tsx | 13 ++++ src/hooks/useIndexLastTaskAt.ts | 79 ++++++++++++++++++++++++ src/locales/en/index.json | 4 +- src/locales/zh/index.json | 4 +- 5 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 src/hooks/useIndexLastTaskAt.ts diff --git a/src/components/biz/IndexConfigEditor.tsx b/src/components/biz/IndexConfigEditor.tsx index 1b775cf..d520a15 100644 --- a/src/components/biz/IndexConfigEditor.tsx +++ b/src/components/biz/IndexConfigEditor.tsx @@ -2,7 +2,7 @@ import { useCurrentIndex } from "@/hooks/useCurrentIndex"; import { useMeiliClient } from "@/hooks/useMeiliClient"; import { cn } from "@/lib/cn"; import { hiddenRequestLoader, showRequestLoader } from "@/lib/loader"; -import { showTaskSubmitNotification } from "@/lib/toast"; +import { showTaskSubmitNotification, toast } from "@/lib/toast"; import { Button } from "@nextui-org/react"; import { useMutation, useQuery } from "@tanstack/react-query"; import type { Settings } from "meilisearch"; @@ -33,11 +33,9 @@ export const IndexConfigEditor: FC<{ (data: Settings = {}) => { setIsSettingsEditing(false); setIndexSettingInitialData(data); - if (!isSettingsEditing) { - setIndexSettingEditorData(JSON.stringify(data, null, 2)); - } + setIndexSettingEditorData(JSON.stringify(data, null, 2)); }, - [isSettingsEditing], + [], ); const querySettings = useQuery({ @@ -75,19 +73,33 @@ export const IndexConfigEditor: FC<{ return await currentIndex.index.updateSettings(variables); }, - onSuccess: (t) => { - showTaskSubmitNotification(t); + onSuccess: (task) => { + setIsSettingsEditing(false); + showTaskSubmitNotification(task); setTimeout(() => querySettings.refetch(), 450); }, + onError: (error) => { + const errorMessage = + error instanceof Error ? error.message : String(error); + toast.error(errorMessage); + }, onSettled: () => { hiddenRequestLoader(); }, }); const onSaveSettings = useCallback(() => { - setIsSettingsEditing(false); - indexSettingEditorData && + if (!indexSettingEditorData) { + return; + } + + try { settingsMutation.mutate(JSON.parse(indexSettingEditorData)); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + toast.error(errorMessage); + } }, [indexSettingEditorData, settingsMutation]); const isLoading = useMemo(() => { diff --git a/src/components/biz/IndexList.tsx b/src/components/biz/IndexList.tsx index 9fb0bb0..bf12e62 100644 --- a/src/components/biz/IndexList.tsx +++ b/src/components/biz/IndexList.tsx @@ -1,5 +1,6 @@ "use client"; import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useIndexLastTaskAt } from "@/hooks/useIndexLastTaskAt"; import { useIndexTaskCounts } from "@/hooks/useIndexTaskCounts"; import { useInstanceStats } from "@/hooks/useInstanceStats"; import { cn } from "@/lib/cn"; @@ -16,6 +17,7 @@ import { useTranslation } from "react-i18next"; import { useImmer } from "use-immer"; import { EmptyArea } from "../common/empty"; import { LoaderPage } from "../common/Loader"; +import { TimeAgo } from "../common/TimeAgo"; import { CreateIndexButton } from "./CreateIndex"; interface Props { @@ -61,6 +63,7 @@ export const IndexList: FC = ({ className = "", client }) => { [indexList], ); const [taskCounts] = useIndexTaskCounts(client, indexUids); + const [lastTaskAt] = useIndexLastTaskAt(client, indexUids); useEffect(() => { // load index list into fuse collection @@ -223,6 +226,15 @@ export const IndexList: FC = ({ className = "", client }) => { + +
+ {t("last_task")} + +
+
); @@ -260,6 +272,7 @@ export const IndexList: FC = ({ className = "", client }) => { statsQuery.refetch, isLoading, taskCounts, + lastTaskAt, ], ); }; diff --git a/src/hooks/useIndexLastTaskAt.ts b/src/hooks/useIndexLastTaskAt.ts new file mode 100644 index 0000000..a6b1fca --- /dev/null +++ b/src/hooks/useIndexLastTaskAt.ts @@ -0,0 +1,79 @@ +import { useQuery, type UseQueryResult } from "@tanstack/react-query"; +import type { MeiliSearch, Task } from "meilisearch"; +import { useMemo } from "react"; +import { useCurrentInstance } from "./useCurrentInstance"; + +const PAGE_LIMIT = 1000; +const MAX_PAGES = 100; + +function pickTaskActivityTimestamp( + task: Task, +): string | Date | undefined { + return task.finishedAt ?? task.startedAt ?? task.enqueuedAt ?? undefined; +} + +export const useIndexLastTaskAt = ( + client: MeiliSearch, + indexUids: string[], +) => { + const currentInstance = useCurrentInstance(); + const host = currentInstance?.host; + const enabled = indexUids.length > 0; + + const query = useQuery({ + queryKey: ["indexLastTaskAt", host, [...indexUids].sort().join(",")], + queryFn: async () => { + const lastByIndex = new Map(); + const target = new Set(indexUids); + let from: number | undefined; + let pages = 0; + + while (pages < MAX_PAGES) { + const page = await client.getTasks({ + indexUids, + limit: PAGE_LIMIT, + ...(from !== undefined ? { from } : {}), + }); + + for (const task of page.results) { + const uid = task.indexUid; + if (!uid || lastByIndex.has(uid)) { + continue; + } + lastByIndex.set(uid, pickTaskActivityTimestamp(task)); + } + + const allFound = [...target].every((id) => lastByIndex.has(id)); + if (allFound) { + break; + } + + if (page.next == null) { + break; + } + from = page.next; + pages++; + } + + return lastByIndex; + }, + enabled, + refetchInterval: 30_000, + }); + + const lastTaskAt = useMemo(() => { + if (!query.data) { + return {} as Record; + } + const result: Record = {}; + for (const [indexUid, ts] of query.data.entries()) { + result[indexUid] = ts; + } + return result; + }, [query.data]); + + return [lastTaskAt, query] as [ + Record, + UseQueryResult>, + ]; +}; diff --git a/src/locales/en/index.json b/src/locales/en/index.json index 9991ee7..63a0100 100644 --- a/src/locales/en/index.json +++ b/src/locales/en/index.json @@ -39,5 +39,7 @@ }, "tasks_count": "{{count}} pending/processing task(s)", "tasks_count_tooltip": "{{count}} incomplete task(s)", - "count_tooltip": "View documents" + "count_tooltip": "View documents", + "last_task": "Last task", + "last_task_tooltip": "Time of the latest task for this index (finished, in progress, or enqueued)" } diff --git a/src/locales/zh/index.json b/src/locales/zh/index.json index bfc1d34..d886658 100644 --- a/src/locales/zh/index.json +++ b/src/locales/zh/index.json @@ -39,5 +39,7 @@ }, "tasks_count": "{{count}} 个待处理/处理中的任务", "tasks_count_tooltip": "{{count}} 个未完成任务", - "count_tooltip": "查看文档" + "count_tooltip": "查看文档", + "last_task": "最近任务", + "last_task_tooltip": "该索引最新任务的时间(已完成、进行中或排队中)" } From 9a13d81fa218e2f74236d002d64b34a321c4e1b1 Mon Sep 17 00:00:00 2001 From: Peyton Nowlin Date: Tue, 21 Apr 2026 22:35:39 -0400 Subject: [PATCH 2/2] fix(IndexList): correct case in Timeago import path Vercel's case-sensitive Linux build failed to resolve "../common/TimeAgo" since the file is Timeago.tsx. macOS masked this locally. --- src/components/biz/IndexList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/biz/IndexList.tsx b/src/components/biz/IndexList.tsx index bf12e62..c40299c 100644 --- a/src/components/biz/IndexList.tsx +++ b/src/components/biz/IndexList.tsx @@ -17,7 +17,7 @@ import { useTranslation } from "react-i18next"; import { useImmer } from "use-immer"; import { EmptyArea } from "../common/empty"; import { LoaderPage } from "../common/Loader"; -import { TimeAgo } from "../common/TimeAgo"; +import { TimeAgo } from "../common/Timeago"; import { CreateIndexButton } from "./CreateIndex"; interface Props {