mirror of
https://github.com/langgenius/dify.git
synced 2026-05-27 20:36:18 +08:00
feat: agent log details
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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" />)
|
||||
|
||||
|
||||
243
web/features/agent-v2/components/logs/logs-page.tsx
Normal file
243
web/features/agent-v2/components/logs/logs-page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "触发过使用此智能体的工作流的独立终端用户数。",
|
||||
|
||||
Reference in New Issue
Block a user