feat(web): app logs
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 13:59:45
|
* @Date: 2026-02-03 13:59:45
|
||||||
* @Last Modified by: ZhaoYing
|
* @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 { request } from '@/utils/request'
|
||||||
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
|
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) => {
|
export const cancelSpaceShare = (target_workspace_id?: string) => {
|
||||||
return request.delete(`/apps/share/${target_workspace_id}`)
|
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}`)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 15:29:46
|
* @Date: 2026-02-02 15:29:46
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-23 12:11:18
|
* @Last Modified time: 2026-03-24 16:12:56
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* RbTable Component
|
* RbTable Component
|
||||||
@@ -102,7 +102,7 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [currentPagination, setCurrentPagination] = useState({
|
const [currentPagination, setCurrentPagination] = useState({
|
||||||
page: 1,
|
page: 1,
|
||||||
pagesize: 10,
|
pagesize: 20,
|
||||||
});
|
});
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
|
|||||||
@@ -1309,7 +1309,7 @@ export const en = {
|
|||||||
apiKeyRequestTotal: 'Total Requests',
|
apiKeyRequestTotal: 'Total Requests',
|
||||||
qps: 'Average QPS',
|
qps: 'Average QPS',
|
||||||
qpsLimit: 'QPS Limit',
|
qpsLimit: 'QPS Limit',
|
||||||
qpsLimitTip: '(Requests per second)',
|
qpsLimitTip: 'Requests per second',
|
||||||
apiLimitConfig: 'Rate Limiting Configuration',
|
apiLimitConfig: 'Rate Limiting Configuration',
|
||||||
qpsLimitDesc: 'Limit the maximum number of requests this Key can make per second',
|
qpsLimitDesc: 'Limit the maximum number of requests this Key can make per second',
|
||||||
dailyUsageLimit: 'Daily Usage Limit',
|
dailyUsageLimit: 'Daily Usage Limit',
|
||||||
|
|||||||
@@ -687,7 +687,7 @@ export const zh = {
|
|||||||
apiKeyRequestTotal: '总请求数',
|
apiKeyRequestTotal: '总请求数',
|
||||||
qps: '平均 QPS',
|
qps: '平均 QPS',
|
||||||
qpsLimit: 'QPS 限制',
|
qpsLimit: 'QPS 限制',
|
||||||
qpsLimitTip: '(每秒请求数)',
|
qpsLimitTip: '每秒请求数',
|
||||||
apiLimitConfig: '限流配置',
|
apiLimitConfig: '限流配置',
|
||||||
qpsLimitDesc: '限制此 Key 每秒最多可以发起的请求数',
|
qpsLimitDesc: '限制此 Key 每秒最多可以发起的请求数',
|
||||||
dailyUsageLimit: '日调用量限制',
|
dailyUsageLimit: '日调用量限制',
|
||||||
|
|||||||
79
web/src/views/ApplicationConfig/Logs.tsx
Normal file
79
web/src/views/ApplicationConfig/Logs.tsx
Normal 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;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:27:52
|
* @Date: 2026-02-03 16:27:52
|
||||||
* @Last Modified by: ZhaoYing
|
* @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 { type FC, useRef, useMemo, useCallback } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
@@ -27,10 +27,10 @@ import FeaturesConfig from './FeaturesConfig'
|
|||||||
/**
|
/**
|
||||||
* Tab keys for application configuration
|
* Tab keys for application configuration
|
||||||
*/
|
*/
|
||||||
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
|
const tabKeys = ['arrangement', 'api', 'release', 'log', 'statistics']
|
||||||
const sharingTabKeys = [
|
const sharingTabKeys = [
|
||||||
'test',
|
'test',
|
||||||
// 'log',
|
'log',
|
||||||
'api'
|
'api'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
103
web/src/views/ApplicationConfig/components/LogDetailModal.tsx
Normal file
103
web/src/views/ApplicationConfig/components/LogDetailModal.tsx
Normal 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;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.tabs :global(.ant-tabs-tab+.ant-tabs-tab) {
|
.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>.ant-tabs-nav .ant-tabs-ink-bar),
|
||||||
.tabs:global(.ant-tabs-top>div>.ant-tabs-nav .ant-tabs-ink-bar) {
|
.tabs:global(.ant-tabs-top>div>.ant-tabs-nav .ant-tabs-ink-bar) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:29:37
|
* @Date: 2026-02-03 16:29:37
|
||||||
* @Last Modified by: ZhaoYing
|
* @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 React, { useEffect, useState, useRef } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -19,6 +19,7 @@ import Workflow from '@/views/Workflow';
|
|||||||
import Statistics from './Statistics'
|
import Statistics from './Statistics'
|
||||||
import TestChat from './TestChat'
|
import TestChat from './TestChat'
|
||||||
import type { WorkflowConfig } from '@/views/Workflow/types';
|
import type { WorkflowConfig } from '@/views/Workflow/types';
|
||||||
|
import Logs from './Logs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application configuration page component
|
* Application configuration page component
|
||||||
@@ -126,6 +127,7 @@ const ApplicationConfig: React.FC = () => {
|
|||||||
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
|
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
|
||||||
{activeTab === 'statistics' && <Statistics application={application} />}
|
{activeTab === 'statistics' && <Statistics application={application} />}
|
||||||
{activeTab === 'test' && <TestChat application={application} config={config} />}
|
{activeTab === 'test' && <TestChat application={application} config={config} />}
|
||||||
|
{activeTab === 'log' && <Logs />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:29:49
|
* @Date: 2026-02-03 16:29:49
|
||||||
* @Last Modified by: ZhaoYing
|
* @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 { KnowledgeConfig } from './components/Knowledge/types'
|
||||||
import type { Variable } from './components/VariableList/types'
|
import type { Variable } from './components/VariableList/types'
|
||||||
@@ -481,4 +481,18 @@ export interface AppSharingModalRef {
|
|||||||
export interface AppSharingForm {
|
export interface AppSharingForm {
|
||||||
target_workspace_ids: string[];
|
target_workspace_ids: string[];
|
||||||
permission: 'readonly' | 'editable'
|
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;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user