refactor: replace DashboardPage and DashboardTableShell with DashboardListPage for notifications and permissions
- Updated DashboardNotificationsPage to utilize DashboardListPage for improved structure and functionality. - Simplified state management and data fetching in DashboardNotificationsPage. - Refactored DashboardPermissionsPage to use DashboardListPage, enhancing filter and pagination handling. - Introduced DashboardListPage component to standardize list rendering with filters and pagination. - Added utility functions for managing filters and pagination in the new DashboardListPage component. - Improved code readability and maintainability by consolidating common logic into reusable components.
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { RefreshCwIcon, SearchIcon } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { useMemo, useState } from "react"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
fetchKnowledgeRetrieveLogs,
|
||||
type KnowledgeRetrieveLog,
|
||||
type PageResult,
|
||||
} from "@/lib/api/admin"
|
||||
import {
|
||||
KnowledgeAnswerStatus,
|
||||
@@ -22,19 +20,8 @@ import {
|
||||
} from "@/lib/knowledge-i18n"
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import { useI18n } from "@/i18n/provider"
|
||||
import { ListPagination } from "@/components/list-pagination"
|
||||
import { OptionCombobox } from "@/components/option-combobox"
|
||||
import { DashboardListPage } from "@/components/dashboard/list"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { RetrieveLogDetailDrawer } from "./retrieve-log-detail"
|
||||
|
||||
type RetrieveLogListProps = {
|
||||
@@ -125,158 +112,108 @@ export function RetrieveLogList({
|
||||
knowledgeBaseId,
|
||||
}: RetrieveLogListProps) {
|
||||
const t = useI18n()
|
||||
const [questionInput, setQuestionInput] = useState("")
|
||||
const [question, setQuestion] = useState("")
|
||||
const [channel, setChannel] = useState("all")
|
||||
const [scene, setScene] = useState("all")
|
||||
const [answerStatus, setAnswerStatus] = useState("all")
|
||||
const [chunkProvider, setChunkProvider] = useState("all")
|
||||
const [rerankEnabled, setRerankEnabled] = useState("all")
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
const [selectedLogId, setSelectedLogId] = useState<number | null>(null)
|
||||
const [result, setResult] = useState<PageResult<KnowledgeRetrieveLog>>({
|
||||
results: [],
|
||||
page: { page: 1, limit: 20, total: 0 },
|
||||
})
|
||||
const [selectedKnowledgeBaseId, setSelectedKnowledgeBaseId] = useState<number | null>(null)
|
||||
const channelOptions = useMemo(() => getChannelOptions(t), [t])
|
||||
const sceneOptions = useMemo(() => getSceneOptions(t), [t])
|
||||
const answerStatusOptions = useMemo(() => getAnswerStatusOptions(t), [t])
|
||||
const providerOptions = useMemo(() => getProviderOptions(t), [t])
|
||||
const rerankOptions = useMemo(() => getRerankOptions(t), [t])
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!knowledgeBaseId) {
|
||||
setResult({ results: [], page: { page: 1, limit: 20, total: 0 } })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await fetchKnowledgeRetrieveLogs({
|
||||
knowledgeBaseId,
|
||||
question: question.trim() || undefined,
|
||||
channel: channel === "all" ? undefined : channel,
|
||||
scene: scene === "all" ? undefined : scene,
|
||||
answerStatus: answerStatus === "all" ? undefined : Number(answerStatus),
|
||||
chunkProvider: chunkProvider === "all" ? undefined : chunkProvider,
|
||||
rerankEnabled: rerankEnabled === "all" ? undefined : Number(rerankEnabled),
|
||||
page,
|
||||
limit,
|
||||
})
|
||||
setResult(data)
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t("knowledge.loadRetrieveLogsFailed"))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [answerStatus, channel, chunkProvider, knowledgeBaseId, limit, page, question, rerankEnabled, scene, t])
|
||||
|
||||
useEffect(() => {
|
||||
void loadData()
|
||||
}, [loadData])
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
setSelectedLogId(null)
|
||||
setDetailOpen(false)
|
||||
}, [knowledgeBaseId])
|
||||
|
||||
const emptyStateText = useMemo(() => {
|
||||
if (!knowledgeBaseId) {
|
||||
return t("knowledge.selectBaseForLogs")
|
||||
}
|
||||
if (loading) {
|
||||
return t("knowledge.loadingRetrieveLogs")
|
||||
}
|
||||
return t("knowledge.emptyRetrieveLogs")
|
||||
}, [knowledgeBaseId, loading, t])
|
||||
|
||||
function applyFilters() {
|
||||
setQuestion(questionInput)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
function handleQuestionKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key !== "Enter") {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
function handleOpenDetail(logId: number) {
|
||||
setSelectedLogId(logId)
|
||||
setSelectedKnowledgeBaseId(knowledgeBaseId)
|
||||
setDetailOpen(true)
|
||||
}
|
||||
|
||||
if (!knowledgeBaseId) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
{emptyStateText}
|
||||
{t("knowledge.selectBaseForLogs")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex flex-col gap-3 border-b bg-background px-6 py-2">
|
||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.8fr)_repeat(5,minmax(0,0.8fr))_auto]">
|
||||
<div className="relative">
|
||||
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={questionInput}
|
||||
onChange={(event) => setQuestionInput(event.target.value)}
|
||||
onKeyDown={handleQuestionKeyDown}
|
||||
placeholder={t("knowledge.filterQuestion")}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<OptionCombobox value={channel} options={channelOptions} placeholder={t("knowledge.selectChannel")} onChange={setChannel} />
|
||||
<OptionCombobox value={scene} options={sceneOptions} placeholder={t("knowledge.selectScene")} onChange={setScene} />
|
||||
<OptionCombobox value={answerStatus} options={answerStatusOptions} placeholder={t("knowledge.answerStatus")} onChange={setAnswerStatus} />
|
||||
<OptionCombobox value={chunkProvider} options={providerOptions} placeholder={t("knowledge.chunkStrategy")} onChange={setChunkProvider} />
|
||||
<OptionCombobox value={rerankEnabled} options={rerankOptions} placeholder="Rerank" onChange={setRerankEnabled} />
|
||||
<Button onClick={applyFilters}>{t("knowledge.filter")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-auto px-6 py-4">
|
||||
<div className="overflow-hidden rounded-xl border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-42">{t("knowledge.time")}</TableHead>
|
||||
<TableHead>{t("knowledge.question")}</TableHead>
|
||||
<TableHead className="w-28">{t("knowledge.answerStatus")}</TableHead>
|
||||
<TableHead className="w-24 text-right">{t("knowledge.hitCount")}</TableHead>
|
||||
<TableHead className="w-24 text-right">TopScore</TableHead>
|
||||
<TableHead className="w-28">Provider</TableHead>
|
||||
<TableHead className="w-24">Rerank</TableHead>
|
||||
<TableHead className="w-24 text-right">{t("knowledge.citations")}</TableHead>
|
||||
<TableHead className="w-28 text-right">{t("knowledge.duration")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.results.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="h-32 text-center text-muted-foreground">
|
||||
{emptyStateText}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
result.results.map((item) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleOpenDetail(item.id)}
|
||||
>
|
||||
<TableCell className="text-xs text-muted-foreground">{formatDateTime(item.createdAt)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex h-full flex-col px-6 py-4">
|
||||
<DashboardListPage<KnowledgeRetrieveLog>
|
||||
layout="fragment"
|
||||
filters={[
|
||||
{
|
||||
name: "question",
|
||||
label: t("knowledge.filterQuestion"),
|
||||
placeholder: t("knowledge.filterQuestion"),
|
||||
defaultValue: "",
|
||||
trim: true,
|
||||
className: "min-w-0 xl:min-w-[280px]",
|
||||
inputClassName: "pl-9",
|
||||
icon: <SearchIcon className="size-4" />,
|
||||
},
|
||||
{
|
||||
name: "channel",
|
||||
label: t("knowledge.selectChannel"),
|
||||
type: "select",
|
||||
defaultValue: "all",
|
||||
allValue: "all",
|
||||
options: channelOptions,
|
||||
placeholder: t("knowledge.selectChannel"),
|
||||
},
|
||||
{
|
||||
name: "scene",
|
||||
label: t("knowledge.selectScene"),
|
||||
type: "select",
|
||||
defaultValue: "all",
|
||||
allValue: "all",
|
||||
options: sceneOptions,
|
||||
placeholder: t("knowledge.selectScene"),
|
||||
},
|
||||
{
|
||||
name: "answerStatus",
|
||||
label: t("knowledge.answerStatus"),
|
||||
type: "select",
|
||||
defaultValue: "all",
|
||||
allValue: "all",
|
||||
valueType: "number",
|
||||
options: answerStatusOptions,
|
||||
placeholder: t("knowledge.answerStatus"),
|
||||
},
|
||||
{
|
||||
name: "chunkProvider",
|
||||
label: t("knowledge.chunkStrategy"),
|
||||
type: "select",
|
||||
defaultValue: "all",
|
||||
allValue: "all",
|
||||
options: providerOptions,
|
||||
placeholder: t("knowledge.chunkStrategy"),
|
||||
},
|
||||
{
|
||||
name: "rerankEnabled",
|
||||
label: "Rerank",
|
||||
type: "select",
|
||||
defaultValue: "all",
|
||||
allValue: "all",
|
||||
valueType: "number",
|
||||
options: rerankOptions,
|
||||
placeholder: "Rerank",
|
||||
},
|
||||
]}
|
||||
fetchList={(query) => fetchKnowledgeRetrieveLogs({ knowledgeBaseId, ...query })}
|
||||
getItemId={(item) => item.id}
|
||||
getRowClassName={() => "cursor-pointer"}
|
||||
onRowClick={(item) => handleOpenDetail(item.id)}
|
||||
columns={[
|
||||
{
|
||||
key: "time",
|
||||
label: t("knowledge.time"),
|
||||
className: "w-42 text-xs text-muted-foreground",
|
||||
render: (item) => formatDateTime(item.createdAt),
|
||||
},
|
||||
{
|
||||
key: "question",
|
||||
label: t("knowledge.question"),
|
||||
render: (item) => (
|
||||
<div className="space-y-1">
|
||||
<div className="line-clamp-2 font-medium">{item.question || "-"}</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
@@ -285,45 +222,78 @@ export function RetrieveLogList({
|
||||
{item.knowledgeBaseName ? <span>{item.knowledgeBaseName}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "answerStatus",
|
||||
label: t("knowledge.answerStatus"),
|
||||
className: "w-28",
|
||||
render: (item) => (
|
||||
<Badge variant={getAnswerStatusVariant(item.answerStatus)}>
|
||||
{answerStatusLabel(item.answerStatus, item.answerStatusName, t)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{item.hitCount}</TableCell>
|
||||
<TableCell className="text-right font-mono text-xs">{item.topScore.toFixed(4)}</TableCell>
|
||||
<TableCell>{item.chunkProvider ? providerLabel(item.chunkProvider, t) : "-"}</TableCell>
|
||||
<TableCell>{item.rerankEnabled ? `${t("knowledge.yes")} (${item.rerankLimit})` : t("knowledge.no")}</TableCell>
|
||||
<TableCell className="text-right">{item.citationCount}</TableCell>
|
||||
<TableCell className="text-right">{item.latencyMs} ms</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t px-6 py-4">
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={result.page.limit}
|
||||
loading={loading}
|
||||
onPageChange={setPage}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "hitCount",
|
||||
label: t("knowledge.hitCount"),
|
||||
className: "w-24 text-right",
|
||||
render: (item) => item.hitCount,
|
||||
},
|
||||
{
|
||||
key: "topScore",
|
||||
label: "TopScore",
|
||||
className: "w-24 text-right font-mono text-xs",
|
||||
render: (item) => item.topScore.toFixed(4),
|
||||
},
|
||||
{
|
||||
key: "provider",
|
||||
label: "Provider",
|
||||
className: "w-28",
|
||||
render: (item) =>
|
||||
item.chunkProvider ? providerLabel(item.chunkProvider, t) : "-",
|
||||
},
|
||||
{
|
||||
key: "rerank",
|
||||
label: "Rerank",
|
||||
className: "w-24",
|
||||
render: (item) =>
|
||||
item.rerankEnabled
|
||||
? `${t("knowledge.yes")} (${item.rerankLimit})`
|
||||
: t("knowledge.no"),
|
||||
},
|
||||
{
|
||||
key: "citations",
|
||||
label: t("knowledge.citations"),
|
||||
className: "w-24 text-right",
|
||||
render: (item) => item.citationCount,
|
||||
},
|
||||
{
|
||||
key: "duration",
|
||||
label: t("knowledge.duration"),
|
||||
className: "w-28 text-right",
|
||||
render: (item) => `${item.latencyMs} ms`,
|
||||
},
|
||||
]}
|
||||
labels={{
|
||||
query: t("knowledge.filter"),
|
||||
loading: t("knowledge.loadingRetrieveLogs"),
|
||||
empty: t("knowledge.emptyRetrieveLogs"),
|
||||
loadFailed: t("knowledge.loadRetrieveLogsFailed"),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RetrieveLogDetailDrawer
|
||||
open={detailOpen}
|
||||
open={detailOpen && selectedKnowledgeBaseId === knowledgeBaseId}
|
||||
retrieveLogId={selectedLogId}
|
||||
onOpenChange={setDetailOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDetailOpen(open)
|
||||
if (!open) {
|
||||
setSelectedLogId(null)
|
||||
setSelectedKnowledgeBaseId(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user