feat: agent log details

This commit is contained in:
yyh
2026-05-27 16:02:01 +08:00
parent 14e7fc87e4
commit cb2e404eb6
8 changed files with 333 additions and 22 deletions

View File

@ -117,27 +117,30 @@ const MainNav = ({
},
], [isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t])
const renderLogo = () => (
<h1 className="min-w-0">
<Link href="/" className="flex h-8 shrink-0 items-center overflow-hidden px-2 indent-[-9999px] whitespace-nowrap">
{systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-[22px] w-auto object-contain"
alt="logo"
/>
)
: <DifyLogo />}
</Link>
</h1>
)
const renderLogo = () => {
const appTitle = systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'
return (
<h1 className="min-w-0">
<Link href="/" className="flex h-8 shrink-0 items-center overflow-hidden px-2">
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-5.5 w-auto object-contain"
alt={appTitle}
/>
)
: <DifyLogo alt={appTitle} />}
</Link>
</h1>
)
}
return (
<aside
className={cn(
'flex h-full w-[240px] shrink-0 flex-col',
'flex h-full w-60 shrink-0 flex-col',
showDetailNavigation ? 'bg-components-panel-bg-blur' : 'bg-background-body',
className,
)}
@ -174,7 +177,7 @@ const MainNav = ({
</div>
)}
</div>
<div className="flex w-[240px] items-center justify-between bg-gradient-to-b from-background-body-transparent to-background-body to-50% py-3 pr-1 pl-3 backdrop-blur-[2px]">
<div className="flex w-60 items-center justify-between bg-linear-to-b from-background-body-transparent to-background-body to-50% py-3 pr-1 pl-3 backdrop-blur-[2px]">
<div className="flex min-w-0 items-center gap-1">
<AccountSection />
</div>

View File

@ -2,6 +2,25 @@ import { render, screen } from '@testing-library/react'
import { AgentDetailPage } from '../pages/agent-detail-page'
describe('AgentDetailPage', () => {
it('renders the logs skeleton with filters and table rows', () => {
render(<AgentDetailPage section="logs" />)
expect(screen.getByRole('region', { name: 'agentV2.agentDetail.sections.logs' })).toBeInTheDocument()
expect(screen.getByRole('heading', { name: 'agentV2.agentDetail.logs.title' })).toBeInTheDocument()
expect(screen.getByRole('combobox', {
name: 'agentV2.agentDetail.logs.filters.status.label',
})).toHaveTextContent('agentV2.agentDetail.logs.filters.status.all')
expect(screen.getByRole('combobox', {
name: 'agentV2.agentDetail.logs.filters.period.label',
})).toHaveTextContent('agentV2.agentDetail.logs.filters.period.last7days')
expect(screen.getByRole('textbox', {
name: 'agentV2.agentDetail.logs.filters.search.label',
})).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: 'agentV2.agentDetail.logs.table.startTime' })).toBeInTheDocument()
expect(screen.getByText('run_8f4e21')).toBeInTheDocument()
expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument()
})
it('renders the monitoring layout with metric cards', () => {
render(<AgentDetailPage section="monitoring" />)

View File

@ -0,0 +1,243 @@
'use client'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { Input } from '@langgenius/dify-ui/input'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
type StatusKey = 'all' | 'succeeded' | 'failed' | 'running'
type PeriodKey = 'last7days' | 'last30days' | 'allTime'
type AgentLogStatus = Exclude<StatusKey, 'all'>
type FilterOption<T extends string> = {
value: T
labelKey: I18nKeysWithPrefix<'agentV2', 'agentDetail.logs.'>
}
type AgentLogRow = {
id: string
startedAt: string
status: AgentLogStatus
runtime: string
tokens: string
user: string
triggerKey: I18nKeysWithPrefix<'agentV2', 'agentDetail.logs.triggers.'>
}
const statusOptions: Array<FilterOption<StatusKey>> = [
{ value: 'all', labelKey: 'agentDetail.logs.filters.status.all' },
{ value: 'succeeded', labelKey: 'agentDetail.logs.filters.status.succeeded' },
{ value: 'failed', labelKey: 'agentDetail.logs.filters.status.failed' },
{ value: 'running', labelKey: 'agentDetail.logs.filters.status.running' },
]
const periodOptions: Array<FilterOption<PeriodKey>> = [
{ value: 'last7days', labelKey: 'agentDetail.logs.filters.period.last7days' },
{ value: 'last30days', labelKey: 'agentDetail.logs.filters.period.last30days' },
{ value: 'allTime', labelKey: 'agentDetail.logs.filters.period.allTime' },
]
const logRows: AgentLogRow[] = [
{
id: 'run_8f4e21',
startedAt: '2026-05-27 14:28',
status: 'succeeded',
runtime: '2.431s',
tokens: '1,284',
user: 'tender-reviewer',
triggerKey: 'agentDetail.logs.triggers.workflowNode',
},
{
id: 'run_7a19c0',
startedAt: '2026-05-27 13:46',
status: 'running',
runtime: '0.842s',
tokens: '624',
user: 'pricing-team',
triggerKey: 'agentDetail.logs.triggers.debugRun',
},
{
id: 'run_62bd95',
startedAt: '2026-05-26 18:12',
status: 'failed',
runtime: '1.209s',
tokens: '416',
user: 'compliance-reviewer',
triggerKey: 'agentDetail.logs.triggers.workflowNode',
},
]
const statusTone: Record<AgentLogStatus, 'success' | 'error' | 'normal'> = {
succeeded: 'success',
failed: 'error',
running: 'normal',
}
export function AgentLogsPage() {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const [status, setStatus] = useState<StatusKey>('all')
const [period, setPeriod] = useState<PeriodKey>('last7days')
const [keyword, setKeyword] = useState('')
const [page, setPage] = useState(1)
const [limit, setLimit] = useState(10)
const selectedStatus = statusOptions.find(option => option.value === status) ?? statusOptions[0]!
const selectedPeriod = periodOptions.find(option => option.value === period) ?? periodOptions[0]!
return (
<section
aria-label={t('agentDetail.sections.logs')}
className="h-full min-w-0 flex-1 overflow-auto bg-components-panel-bg-blur px-4 py-6 sm:px-12"
>
<div className="mx-auto flex h-full max-w-6xl flex-col">
<header>
<h2 className="system-xl-semibold text-text-primary">
{t('agentDetail.logs.title')}
</h2>
<p className="mt-1 system-sm-regular text-text-tertiary">
{t('agentDetail.logs.description')}
</p>
</header>
<div className="flex max-h-[calc(100%-16px)] min-h-0 flex-1 flex-col py-4">
<div className="mb-2 flex flex-wrap items-center gap-2">
<Select
value={status}
onValueChange={(nextValue) => {
if (nextValue)
setStatus(nextValue as StatusKey)
}}
>
<SelectTrigger
aria-label={t('agentDetail.logs.filters.status.label')}
className="mt-0 w-fit max-w-full min-w-36"
>
{t(selectedStatus.labelKey)}
</SelectTrigger>
<SelectContent>
{statusOptions.map(option => (
<SelectItem key={option.value} value={option.value}>
<SelectItemText>{t(option.labelKey)}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={period}
onValueChange={(nextValue) => {
if (nextValue)
setPeriod(nextValue as PeriodKey)
}}
>
<SelectTrigger
aria-label={t('agentDetail.logs.filters.period.label')}
className="mt-0 w-fit max-w-full min-w-36"
>
{t(selectedPeriod.labelKey)}
</SelectTrigger>
<SelectContent>
{periodOptions.map(option => (
<SelectItem key={option.value} value={option.value}>
<SelectItemText>{t(option.labelKey)}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative w-62 max-w-full">
<span aria-hidden className="pointer-events-none absolute top-1/2 left-3 i-ri-search-line size-4 -translate-y-1/2 text-text-tertiary" />
<Input
aria-label={t('agentDetail.logs.filters.search.label')}
name="agent-log-search"
autoComplete="off"
value={keyword}
placeholder={t('agentDetail.logs.filters.search.placeholder')}
className="pl-9"
onChange={(event) => {
setKeyword(event.target.value)
}}
/>
</div>
</div>
<div className="min-h-0 overflow-x-auto">
<table className="mt-2 w-full min-w-180 border-collapse border-0">
<thead className="system-xs-medium-uppercase text-text-tertiary">
<tr>
<th scope="col" className="rounded-l-lg bg-background-section-burn py-1.5 pr-2 pl-3 text-left whitespace-nowrap">
{t('agentDetail.logs.table.startTime')}
</th>
<th scope="col" className="bg-background-section-burn py-1.5 pr-2 pl-3 text-left whitespace-nowrap">
{t('agentDetail.logs.table.status')}
</th>
<th scope="col" className="bg-background-section-burn py-1.5 pr-2 pl-3 text-left whitespace-nowrap">
{t('agentDetail.logs.table.runtime')}
</th>
<th scope="col" className="bg-background-section-burn py-1.5 pr-2 pl-3 text-left whitespace-nowrap">
{t('agentDetail.logs.table.tokens')}
</th>
<th scope="col" className="bg-background-section-burn py-1.5 pr-2 pl-3 text-left whitespace-nowrap">
{t('agentDetail.logs.table.user')}
</th>
<th scope="col" className="rounded-r-lg bg-background-section-burn py-1.5 pr-2 pl-3 text-left whitespace-nowrap">
{t('agentDetail.logs.table.trigger')}
</th>
</tr>
</thead>
<tbody className="system-sm-regular text-text-secondary">
{logRows.map(log => (
<tr key={log.id} className="border-b border-divider-subtle hover:bg-background-default-hover">
<td className="p-3 pr-2 whitespace-nowrap">
<div className="system-sm-medium text-text-secondary">{log.startedAt}</div>
<div className="mt-0.5 code-xs-regular text-text-tertiary" translate="no">{log.id}</div>
</td>
<td className="p-3 pr-2 whitespace-nowrap">
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
<StatusDot status={statusTone[log.status]} />
<span>{t(`agentDetail.logs.filters.status.${log.status}`)}</span>
</div>
</td>
<td className="p-3 pr-2 whitespace-nowrap">{log.runtime}</td>
<td className="p-3 pr-2 whitespace-nowrap">{log.tokens}</td>
<td className="p-3 pr-2">
<div className="max-w-48 truncate">{log.user}</div>
</td>
<td className="p-3 pr-2">
<div className="max-w-48 truncate">{t(log.triggerKey)}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Pagination
page={page}
totalPages={3}
onPageChange={setPage}
labels={{
previous: tCommon('pagination.previous'),
next: tCommon('pagination.next'),
editPageNumber: (page, totalPages) => tCommon('pagination.editPageNumber', { page, totalPages }),
pageNumberInput: tCommon('pagination.pageNumber'),
}}
pageSize={{
value: limit,
options: [10, 25, 50],
onValueChange: setLimit,
label: tCommon('pagination.perPage'),
ariaLabel: tCommon('pagination.perPage'),
}}
/>
</div>
</div>
</section>
)
}

View File

@ -64,7 +64,7 @@ export function AgentMonitoringPage() {
return (
<section
aria-label={t('agentDetail.sections.monitoring')}
className="h-full min-w-0 flex-1 overflow-auto bg-chatbot-bg px-4 py-6 sm:px-12"
className="h-full min-w-0 flex-1 overflow-auto bg-components-panel-bg-blur px-4 py-6 sm:px-12"
>
<div className="mx-auto max-w-6xl">
<header className="mb-4">

View File

@ -19,8 +19,8 @@ export function AgentDetailLayout({
useDocumentTitle(t('agentDetail.documentTitle'))
return (
<main className="flex h-full min-w-0 flex-col overflow-hidden bg-background-section">
<header className="flex h-20 shrink-0 items-center justify-between border-b border-divider-subtle bg-background-body px-6">
<main className="flex h-full min-w-0 flex-col overflow-hidden bg-components-panel-bg-blur">
<header className="flex h-20 shrink-0 items-center justify-between border-b border-divider-subtle bg-components-panel-bg-blur px-6">
<div className="flex min-w-0 items-center gap-3">
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-text-accent text-text-primary-on-surface shadow-xs">
<span aria-hidden className="i-custom-vender-solid-mediaAndDevices-robot size-5" />

View File

@ -1,6 +1,7 @@
'use client'
import { useTranslation } from 'react-i18next'
import { AgentLogsPage } from '../components/logs/logs-page'
import { AgentMonitoringPage } from '../components/monitoring/monitoring-page'
type AgentDetailPageProps = {
@ -15,10 +16,13 @@ export function AgentDetailPage({
if (section === 'monitoring')
return <AgentMonitoringPage />
if (section === 'logs')
return <AgentLogsPage />
return (
<section
aria-label={t(`agentDetail.sections.${section}`)}
className="h-full min-w-0 flex-1 bg-background-section"
className="h-full min-w-0 flex-1 bg-components-panel-bg-blur"
/>
)
}

View File

@ -1,6 +1,27 @@
{
"agentDetail.documentTitle": "Agent",
"agentDetail.history": "History",
"agentDetail.logs.description": "Review recent runs that invoked this reusable agent across workflows.",
"agentDetail.logs.filters.period.allTime": "All time",
"agentDetail.logs.filters.period.label": "Log period",
"agentDetail.logs.filters.period.last30days": "Last 30 days",
"agentDetail.logs.filters.period.last7days": "Last 7 days",
"agentDetail.logs.filters.search.label": "Search agent logs",
"agentDetail.logs.filters.search.placeholder": "Search run ID, trigger, or user…",
"agentDetail.logs.filters.status.all": "All",
"agentDetail.logs.filters.status.failed": "Fail",
"agentDetail.logs.filters.status.label": "Log status",
"agentDetail.logs.filters.status.running": "Running",
"agentDetail.logs.filters.status.succeeded": "Success",
"agentDetail.logs.table.runtime": "Runtime",
"agentDetail.logs.table.startTime": "Start Time",
"agentDetail.logs.table.status": "Status",
"agentDetail.logs.table.tokens": "Tokens",
"agentDetail.logs.table.trigger": "Triggered From",
"agentDetail.logs.table.user": "User",
"agentDetail.logs.title": "Agent Logs",
"agentDetail.logs.triggers.debugRun": "Debug Run",
"agentDetail.logs.triggers.workflowNode": "Workflow Node",
"agentDetail.monitoring.change": "{{value}} from previous period",
"agentDetail.monitoring.description": "Track the reusable agent activity, cost, and interaction quality across workflows.",
"agentDetail.monitoring.metrics.activeUsers.explanation": "Unique end users who triggered workflows using this agent.",

View File

@ -1,6 +1,27 @@
{
"agentDetail.documentTitle": "智能体",
"agentDetail.history": "历史记录",
"agentDetail.logs.description": "查看近期调用此可复用智能体的工作流运行记录。",
"agentDetail.logs.filters.period.allTime": "全部时间",
"agentDetail.logs.filters.period.label": "日志时间范围",
"agentDetail.logs.filters.period.last30days": "最近 30 天",
"agentDetail.logs.filters.period.last7days": "最近 7 天",
"agentDetail.logs.filters.search.label": "搜索智能体日志",
"agentDetail.logs.filters.search.placeholder": "搜索运行 ID、触发方式或用户…",
"agentDetail.logs.filters.status.all": "全部",
"agentDetail.logs.filters.status.failed": "失败",
"agentDetail.logs.filters.status.label": "日志状态",
"agentDetail.logs.filters.status.running": "运行中",
"agentDetail.logs.filters.status.succeeded": "成功",
"agentDetail.logs.table.runtime": "运行时长",
"agentDetail.logs.table.startTime": "开始时间",
"agentDetail.logs.table.status": "状态",
"agentDetail.logs.table.tokens": "Tokens",
"agentDetail.logs.table.trigger": "触发来源",
"agentDetail.logs.table.user": "用户",
"agentDetail.logs.title": "智能体日志",
"agentDetail.logs.triggers.debugRun": "调试运行",
"agentDetail.logs.triggers.workflowNode": "工作流节点",
"agentDetail.monitoring.change": "较上一周期 {{value}}",
"agentDetail.monitoring.description": "跟踪可复用智能体在工作流中的活跃度、成本和交互质量。",
"agentDetail.monitoring.metrics.activeUsers.explanation": "触发过使用此智能体的工作流的独立终端用户数。",