feat(web): app logs

This commit is contained in:
zhaoying
2026-03-24 16:31:32 +08:00
parent 3dcf901043
commit d62b484d71
10 changed files with 215 additions and 12 deletions

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 13:59:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 20:42:23
* @Last Modified time: 2026-03-24 15:48:30
*/
import { request } from '@/utils/request'
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
@@ -169,4 +169,9 @@ export const cancelShare = (app_id: string, target_workspace_id?: string) => {
export const cancelSpaceShare = (target_workspace_id?: string) => {
return request.delete(`/apps/share/${target_workspace_id}`)
}
// Application conversation logs
export const getAppLogsUrl = (app_id: string) => `/apps/${app_id}/logs`
// Get full conversation message history
export const getAppLogDetail = (app_id: string, conversation_id: string) => {
return request.get(`/apps/${app_id}/logs/${conversation_id}`)
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-02 15:29:46
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-23 12:11:18
* @Last Modified time: 2026-03-24 16:12:56
*/
/**
* RbTable Component
@@ -102,7 +102,7 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
const [loading, setLoading] = useState(false)
const [currentPagination, setCurrentPagination] = useState({
page: 1,
pagesize: 10,
pagesize: 20,
});
const [total, setTotal] = useState(0);

View File

@@ -1309,7 +1309,7 @@ export const en = {
apiKeyRequestTotal: 'Total Requests',
qps: 'Average QPS',
qpsLimit: 'QPS Limit',
qpsLimitTip: '(Requests per second)',
qpsLimitTip: 'Requests per second',
apiLimitConfig: 'Rate Limiting Configuration',
qpsLimitDesc: 'Limit the maximum number of requests this Key can make per second',
dailyUsageLimit: 'Daily Usage Limit',

View File

@@ -687,7 +687,7 @@ export const zh = {
apiKeyRequestTotal: '总请求数',
qps: '平均 QPS',
qpsLimit: 'QPS 限制',
qpsLimitTip: '每秒请求数',
qpsLimitTip: '每秒请求数',
apiLimitConfig: '限流配置',
qpsLimitDesc: '限制此 Key 每秒最多可以发起的请求数',
dailyUsageLimit: '日调用量限制',

View File

@@ -0,0 +1,79 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-24 15:41:20
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-24 16:06:44
*/
import { type FC, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { Flex, Button } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getAppLogsUrl } from '@/api/application';
import Table from '@/components/Table'
import { formatDateTime } from '@/utils/format';
import type { LogItem, LogDetailModalRef } from './types'
import LogDetailModal from './components/LogDetailModal'
const Statistics: FC = () => {
const { t } = useTranslation();
const { id } = useParams();
const logDetailRef = useRef<LogDetailModalRef>(null);
const handleViewDetail = (item: LogItem) => {
logDetailRef.current?.handleOpen(item);
}
/** Table column configuration */
const columns: ColumnsType = [
{
title: t('application.logTitle'),
dataIndex: 'title',
key: 'title',
className: 'rb:text-[#212332]'
},
{
title: t('user.createTime'),
dataIndex: 'created_at',
key: 'created_at',
render: (createdAt: string) => formatDateTime(createdAt, 'YYYY-MM-DD HH:mm:ss'),
},
{
title: t('user.lastLoginTime'),
dataIndex: 'last_login_at',
key: 'last_login_at',
render: (lastLoginAt: string) => lastLoginAt ? formatDateTime(lastLoginAt, 'YYYY-MM-DD HH:mm:ss') : '-',
},
{
title: t('common.operation'),
key: 'action',
render: (_, record) => (
<Flex wrap>
<Button
type="link"
onClick={() => handleViewDetail(record as LogItem)}
>
{t('common.view')}
</Button>
</Flex>
),
},
];
return (
<div className="rb:bg-white rb:rounded-lg rb:pt-3 rb:px-3">
<Table
apiUrl={getAppLogsUrl(id || '')}
apiParams={{
is_draft: false,
}}
columns={columns}
rowKey="id"
isScroll={true}
scrollY="calc(100vh - 214px)"
/>
<LogDetailModal ref={logDetailRef} />
</div>
);
}
export default Statistics;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 21:21:28
* @Last Modified time: 2026-03-24 15:58:23
*/
import { type FC, useRef, useMemo, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
@@ -27,10 +27,10 @@ import FeaturesConfig from './FeaturesConfig'
/**
* Tab keys for application configuration
*/
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
const tabKeys = ['arrangement', 'api', 'release', 'log', 'statistics']
const sharingTabKeys = [
'test',
// 'log',
'log',
'api'
]

View File

@@ -0,0 +1,103 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-24 16:31:24
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-24 16:31:24
*/
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import { Flex, Button, Empty, Skeleton } from 'antd';
import { useTranslation } from 'react-i18next';
import type { LogDetailModalRef, LogItem } from '../types'
import RbModal from '@/components/RbModal'
import { getAppLogDetail } from '@/api/application'
import ChatContent from '@/components/Chat/ChatContent'
import { formatDateTime } from '@/utils/format'
import type { ChatItem } from '@/components/Chat/types'
/** Log detail data with conversation messages */
type Data = LogItem & {
messages: ChatItem[];
}
/** Modal component for displaying conversation log details */
const LogDetailModal = forwardRef<LogDetailModalRef>((_props, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false)
const [vo, setVo] = useState<LogItem | null>(null)
const [data, setData] = useState<Data>({} as Data)
/** Close modal and reset form */
const handleClose = () => {
setVisible(false);
setLoading(false)
setVo(null)
setData({} as Data)
};
/** Open modal */
const handleOpen = (item: LogItem) => {
setVisible(true);
setVo(item)
};
/** Fetch detail when modal opens */
useEffect(() => {
if (visible && vo) {
getDetail()
}
}, [visible, vo])
/** Fetch conversation log detail from API */
const getDetail = () => {
if (!vo) return
setLoading(true)
getAppLogDetail(vo.app_id, vo.id).then(res => {
setData(res as Data)
})
.finally(() => {
setLoading(false)
})
}
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={<>
{data.title}
<div className="rb:text-[#5B6167] rb:leading-4.5 rb:text-[12px]">{formatDateTime(data.created_at, 'YYYY.MM')} - {formatDateTime(data.updated_at, 'YYYY.MM')}</div>
</>}
open={visible}
onCancel={handleClose}
footer={null}
width={1000}
>
<Flex justify="space-between" align="center" className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-2.5! rb:pr-2.5! rb:pl-3.25!">
{t('workingDetail.conversationStream')}
<Button className="rb:h-6!" onClick={getDetail}>{t('workingDetail.refresh')}</Button>
</Flex>
<div className="rb-border rb:p-3 rb:rounded-xl rb:mt-3 rb:h-116.5 rb:overflow-y-auto">
{loading
? <Skeleton active />
: data.messages?.length === 0
? <Empty className="rb:my-20" />
: (
<ChatContent
contentClassNames="rb:max-w-110!"
data={data.messages || []}
streamLoading={false}
labelFormat={(item) => formatDateTime(item.created_at)}
/>
)
}
</div>
</RbModal>
);
});
export default LogDetailModal;

View File

@@ -14,7 +14,7 @@
font-weight: 500;
}
.tabs :global(.ant-tabs-tab+.ant-tabs-tab) {
margin-left: 78px;
margin-left: 48px;
}
.tabs:global(.ant-tabs-top>.ant-tabs-nav .ant-tabs-ink-bar),
.tabs:global(.ant-tabs-top>div>.ant-tabs-nav .ant-tabs-ink-bar) {

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 21:09:32
* @Last Modified time: 2026-03-24 15:59:47
*/
import React, { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
@@ -19,6 +19,7 @@ import Workflow from '@/views/Workflow';
import Statistics from './Statistics'
import TestChat from './TestChat'
import type { WorkflowConfig } from '@/views/Workflow/types';
import Logs from './Logs';
/**
* Application configuration page component
@@ -126,6 +127,7 @@ const ApplicationConfig: React.FC = () => {
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
{activeTab === 'statistics' && <Statistics application={application} />}
{activeTab === 'test' && <TestChat application={application} config={config} />}
{activeTab === 'log' && <Logs />}
</div>
</>
);

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-24 10:59:18
* @Last Modified time: 2026-03-24 15:44:33
*/
import type { KnowledgeConfig } from './components/Knowledge/types'
import type { Variable } from './components/VariableList/types'
@@ -481,4 +481,18 @@ export interface AppSharingModalRef {
export interface AppSharingForm {
target_workspace_ids: string[];
permission: 'readonly' | 'editable'
}
export interface LogItem {
id: string;
app_id: string;
user_id: string;
title: string;
message_count: number;
is_draft: boolean;
created_at: number;
updated_at: number;
}
export interface LogDetailModalRef {
handleOpen: (vo: LogItem) => void;
}