dify/web/features/agent-v2/agent-detail/logs/components/logs-table.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

311 lines
10 KiB
TypeScript

import type { AgentLogConversationItemResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useTranslation } from 'react-i18next'
import useTimestamp from '@/hooks/use-timestamp'
import { LogSourceCell } from './source-cell'
export function AgentLogsTable({
logs,
isPending,
isError,
isSuccess,
selectedLogId,
onOpenLog,
onRetry,
}: {
logs: AgentLogConversationItemResponse[]
isPending: boolean
isError: boolean
isSuccess: boolean
selectedLogId?: string
onOpenLog: (log: AgentLogConversationItemResponse) => void
onRetry: () => void
}) {
const { t } = useTranslation('agentV2')
const tableHeaderLabels = {
unread: t(($) => $['agentDetail.logs.table.unread']),
title: t(($) => $['agentDetail.logs.table.title']),
source: t(($) => $['agentDetail.logs.table.source']),
endUser: t(($) => $['agentDetail.logs.table.endUser']),
messageCount: t(($) => $['agentDetail.logs.table.messageCount']),
userRate: t(($) => $['agentDetail.logs.table.userRate']),
operationRate: t(($) => $['agentDetail.logs.table.operationRate']),
updatedTime: t(($) => $['agentDetail.logs.table.updatedTime']),
createdTime: t(($) => $['agentDetail.logs.table.createdTime']),
}
return (
<div className="flex h-full min-w-0 flex-col overflow-hidden">
<div className="shrink-0 pr-3">
<table aria-hidden="true" className="w-full table-fixed border-collapse">
<LogsTableColGroup />
<LogsTableHeader labels={tableHeaderLabels} />
</table>
</div>
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={t(($) => $['agentDetail.logs.title'])}
role="region"
tabIndex={-1}
className="overscroll-contain"
>
<ScrollAreaContent className="pr-3">
<table className="w-full table-fixed border-collapse">
<LogsTableColGroup />
<LogsTableHeader labels={tableHeaderLabels} rowClassName="sr-only" />
<AgentLogsTableBody
logs={logs}
isPending={isPending}
isError={isError}
isSuccess={isSuccess}
selectedLogId={selectedLogId}
onOpenLog={onOpenLog}
onRetry={onRetry}
/>
</table>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</div>
)
}
function AgentLogsTableBody({
logs,
isPending,
isError,
isSuccess,
selectedLogId,
onOpenLog,
onRetry,
}: {
logs: AgentLogConversationItemResponse[]
isPending: boolean
isError: boolean
isSuccess: boolean
selectedLogId?: string
onOpenLog: (log: AgentLogConversationItemResponse) => void
onRetry: () => void
}) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const { formatTime } = useTimestamp()
const notAvailable = t(($) => $['agentDetail.logs.notAvailable'])
const formatLogTime = (value?: number | null) =>
value == null
? notAvailable
: formatTime(
value,
t(($) => $['roster.dateTimeFormat']),
)
return (
<tbody className="system-sm-regular text-text-secondary">
{isPending && <LogsSkeletonRows />}
{isError && (
<LogsStateRow>
<div className="flex items-center justify-center gap-2">
<span>{t(($) => $['agentDetail.logs.loadFailed'])}</span>
<Button variant="secondary" size="small" onClick={onRetry}>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
</LogsStateRow>
)}
{isSuccess && logs.length === 0 && (
<LogsStateRow>{t(($) => $['agentDetail.logs.empty'])}</LogsStateRow>
)}
{isSuccess &&
logs.map((log) => {
const logTitle = log.title || log.conversation_id
const isSelected = selectedLogId === log.id
return (
<tr
key={log.id}
className={cn(
'h-10 cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover',
isSelected && 'bg-background-default-hover',
)}
onClick={() => onOpenLog(log)}
>
<td className="px-0">
<span
className={cn(
'mx-auto block size-1.5 rounded-full',
log.unread ? 'bg-util-colors-blue-blue-500' : 'bg-transparent',
)}
/>
</td>
<TableCell>
<button
type="button"
aria-label={logTitle}
className="block w-full truncate rounded-sm text-left system-sm-medium text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={(event) => {
event.stopPropagation()
onOpenLog(log)
}}
>
{log.title || notAvailable}
</button>
</TableCell>
<td className="px-3">
<LogSourceCell source={log.source} />
</td>
<TableCell translate="no">{log.end_user_id || notAvailable}</TableCell>
<TableCell className="tabular-nums">{log.message_count}</TableCell>
<TableCell className="text-text-quaternary">
{formatRate(log.user_rate, notAvailable)}
</TableCell>
<TableCell className="text-text-quaternary">
{formatRate(log.operation_rate, notAvailable)}
</TableCell>
<TableCell>{formatLogTime(log.updated_at)}</TableCell>
<TableCell>{formatLogTime(log.created_at)}</TableCell>
</tr>
)
})}
</tbody>
)
}
function formatRate(value: number | null | undefined, fallback: string) {
return value == null ? fallback : `${Math.round(value * 100)}%`
}
type LogsTableHeaderLabels = {
unread: string
title: string
source: string
endUser: string
messageCount: string
userRate: string
operationRate: string
updatedTime: string
createdTime: string
}
function LogsTableHeader({
labels,
rowClassName,
}: {
labels: LogsTableHeaderLabels
rowClassName?: string
}) {
return (
<thead>
<tr
className={cn(
'h-7 bg-background-section-burn text-left system-xs-medium-uppercase text-text-tertiary',
rowClassName,
)}
>
<th scope="col" className="rounded-l-lg px-0">
<span className="sr-only">{labels.unread}</span>
</th>
<TableHead>{labels.title}</TableHead>
<TableHead>{labels.source}</TableHead>
<TableHead>{labels.endUser}</TableHead>
<TableHead>{labels.messageCount}</TableHead>
<TableHead>{labels.userRate}</TableHead>
<TableHead>{labels.operationRate}</TableHead>
<TableHead>{labels.updatedTime}</TableHead>
<TableHead className="rounded-r-lg">{labels.createdTime}</TableHead>
</tr>
</thead>
)
}
function LogsTableColGroup() {
return (
<colgroup>
<col className="w-5" />
<col className="w-[28%]" />
<col className="w-[16%]" />
<col className="w-[15%]" />
<col className="w-[8%]" />
<col className="w-[7%]" />
<col className="w-[7%]" />
<col className="w-[10%]" />
<col className="w-[9%]" />
</colgroup>
)
}
function LogsStateRow({ children }: { children: ReactNode }) {
return (
<tr className="h-20 border-b border-divider-subtle">
<td colSpan={9} className="px-3 text-center text-text-tertiary">
{children}
</td>
</tr>
)
}
function LogsSkeletonRows() {
return (
<>
{Array.from({ length: 10 }, (_, index) => (
<tr key={index} className="h-10 border-b border-divider-subtle">
<td className="px-0">
<span className="mx-auto block size-1.5 rounded-full bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-3/4 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="flex items-center gap-2">
<div className="size-5 rounded-full bg-text-quaternary opacity-20" />
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
</div>
</td>
<td className="px-3">
<div className="h-3 w-28 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
</td>
</tr>
))}
</>
)
}
function TableHead({ className, ...props }: ThHTMLAttributes<HTMLTableCellElement>) {
return <th scope="col" className={cn('px-3 text-left whitespace-nowrap', className)} {...props} />
}
function TableCell({ children, className, ...props }: TdHTMLAttributes<HTMLTableCellElement>) {
return (
<td className={cn('min-w-0 px-3 whitespace-nowrap', className)} {...props}>
<div className="truncate">{children}</div>
</td>
)
}