mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feature/1.8-Improve system observability
This commit is contained in:
@@ -26,6 +26,7 @@ class InsidePageProps {
|
||||
scrollInsidePage?: boolean = false
|
||||
customPadding?: boolean
|
||||
customBtn?: ReactNode
|
||||
customBanner?: ReactNode
|
||||
}
|
||||
|
||||
const InsidePage: FC<InsidePageProps> = ({
|
||||
@@ -46,7 +47,8 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
scrollPage = true,
|
||||
scrollInsidePage = false,
|
||||
customPadding = false,
|
||||
customBtn
|
||||
customBtn,
|
||||
customBanner
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -61,8 +63,13 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
<div
|
||||
className={`border-[0px] mr-PAGE_INSIDE_X ${showBorder ? 'border-solid border-b-[1px] border-BORDER' : ''} ${headerClassName}`}
|
||||
>
|
||||
{!pageTitle && !description && !backUrl && !customBtn ? (
|
||||
{!pageTitle && !description && !backUrl && !customBtn && !customBanner ? (
|
||||
<></>
|
||||
) : customBanner ? (
|
||||
<div className={customPadding ? '' : 'mb-[15px]'}>
|
||||
{backUrl && <TopBreadcrumb handleBackCallback={() => goBack()} />}
|
||||
{customBanner}
|
||||
</div>
|
||||
) : (
|
||||
<div className={customPadding ? '' : 'mb-[30px]'}>
|
||||
{backUrl && <TopBreadcrumb handleBackCallback={() => goBack()} />}
|
||||
|
||||
@@ -27,6 +27,7 @@ type TimeRangeSelectorProps = {
|
||||
bindRef?: any
|
||||
hideBtns?: TimeRangeButton[]
|
||||
defaultTimeButton?: TimeRangeButton
|
||||
customClassNames?: string
|
||||
}
|
||||
const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
|
||||
const {
|
||||
@@ -38,7 +39,8 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
|
||||
labelSize = 'default',
|
||||
bindRef,
|
||||
hideBtns = [],
|
||||
defaultTimeButton = 'hour'
|
||||
defaultTimeButton = 'hour',
|
||||
customClassNames = 'pt-btnybase'
|
||||
} = props
|
||||
const [timeButton, setTimeButton] = useState(initialTimeButton || '')
|
||||
const [datePickerValue, setDatePickerValue] = useState<RangeValue>(initialDatePickerValue || [null, null])
|
||||
@@ -111,7 +113,7 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
|
||||
<div className={`flex flex-nowrap items-center ${customClassNames} mr-btnybase`}>
|
||||
{!hideTitle && <label className={`whitespace-nowrap `}>{$t('时间')}:</label>}
|
||||
<Radio.Group className="whitespace-nowrap" value={timeButton} onChange={handleRadioChange} buttonStyle="solid">
|
||||
{hideBtns?.length && hideBtns.includes('hour') ? null : (
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
import { Avatar, Button, Card, Tag, Tooltip, App } from 'antd'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { ApiOutlined } from '@ant-design/icons'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { SERVICE_KIND_OPTIONS } from '@core/const/system/const'
|
||||
import { IconButton } from '@common/components/postcat/api/IconButton'
|
||||
import useCopyToClipboard from '@common/hooks/copy'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
|
||||
export type ServiceBasicInfoType = {
|
||||
id?: string
|
||||
logo?: string
|
||||
name: string
|
||||
description: string
|
||||
appNum: number
|
||||
apiNum: number
|
||||
serviceName: string
|
||||
serviceDesc: string
|
||||
invokeCount: number
|
||||
catalogue: {
|
||||
name: string
|
||||
}
|
||||
serviceKind: string
|
||||
service_kind: string
|
||||
enableMcp: boolean
|
||||
enable_mcp: boolean
|
||||
isReleased?: boolean
|
||||
}
|
||||
|
||||
type ServiceInfoCardProps = {
|
||||
actionSlot?: React.ReactNode
|
||||
customClassName?: string
|
||||
serviceId?: string
|
||||
serviceBasicInfo?: ServiceBasicInfoType
|
||||
teamId?: string
|
||||
}
|
||||
const ServiceInfoCard = ({
|
||||
actionSlot,
|
||||
customClassName,
|
||||
serviceId,
|
||||
serviceBasicInfo,
|
||||
teamId
|
||||
}: ServiceInfoCardProps) => {
|
||||
/** 服务指标 */
|
||||
const [serviceMetrics, setServiceMetrics] = useState<{ title: string; icon: React.ReactNode; value: string }[]>([])
|
||||
/** 服务标签 */
|
||||
const [serviceTags, setServiceTags] = useState<
|
||||
{ color: string; textColor: string; title: string; content: React.ReactNode }[]
|
||||
>([])
|
||||
/** 剪切板 */
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
/** 弹窗组件 */
|
||||
const { message } = App.useApp()
|
||||
/** 获取服务信息 */
|
||||
const { fetchData } = useFetch()
|
||||
/** 服务信息 */
|
||||
const [serviceOverview, setServiceOverview] = useState<ServiceBasicInfoType>()
|
||||
|
||||
/**
|
||||
* 复制
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
const handleCopy = async (value: string): Promise<void> => {
|
||||
if (value) {
|
||||
copyToClipboard(value)
|
||||
message.success($t(RESPONSE_TIPS.copySuccess))
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取服务信息 */
|
||||
const getServiceOverview = () => {
|
||||
fetchData<BasicResponse<{ overview: ServiceBasicInfoType }>>('service/overview/basic', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId },
|
||||
eoTransformKeys: [
|
||||
'enable_mcp',
|
||||
'service_kind',
|
||||
'subscriber_num',
|
||||
'invoke_num',
|
||||
'avaliable_monitor',
|
||||
'is_released'
|
||||
],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const serviceOverview = {
|
||||
...data.overview,
|
||||
appNum: data.overview.subscriberNum,
|
||||
invokeCount: data.overview.invokeNum,
|
||||
serviceName: data.overview.name,
|
||||
serviceDesc: data.overview.description
|
||||
}
|
||||
setServiceOverview(serviceOverview)
|
||||
setServiceMetricsList(serviceOverview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开服务详情页面
|
||||
*/
|
||||
const openInPortal = () => {
|
||||
window.open(`/serviceHub/detail/${serviceOverview?.id}`, '_blank')
|
||||
}
|
||||
|
||||
// 格式化调用次数,添加K和M单位
|
||||
const formatInvokeCount = (count: number | null | undefined): string => {
|
||||
if (count === null || count === undefined) return '-'
|
||||
if (count >= 1000000) {
|
||||
const value = Math.floor(count / 100000) / 10
|
||||
return `${value}M`
|
||||
}
|
||||
if (count >= 1000) {
|
||||
const value = Math.floor(count / 100) / 10
|
||||
return `${value}K`
|
||||
}
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
const setServiceMetricsList = (serviceOverview: ServiceBasicInfoType) => {
|
||||
// 设置服务指标数据
|
||||
setServiceMetrics([
|
||||
{
|
||||
title: 'API 数量',
|
||||
icon: <ApiOutlined className="mr-[1px] text-[14px] h-[14px] w-[14px]" />,
|
||||
value: serviceOverview.apiNum?.toString() || '0'
|
||||
},
|
||||
{
|
||||
title: '接入消费者数量',
|
||||
icon: <Icon icon="tabler:api-app" width="14" height="14" />,
|
||||
value: serviceOverview.appNum?.toString() || '0'
|
||||
},
|
||||
{
|
||||
title: '30天内调用次数',
|
||||
icon: <Icon icon="iconoir:graph-up" width="14" height="14" />,
|
||||
value: formatInvokeCount(serviceOverview.invokeCount ?? 0)
|
||||
}
|
||||
])
|
||||
const serviceKind = serviceOverview?.serviceKind || serviceOverview?.service_kind
|
||||
// 设置服务标签数据
|
||||
const tags = [
|
||||
{
|
||||
color: '#7371fc1b',
|
||||
textColor: 'text-theme',
|
||||
title: serviceOverview?.catalogue?.name || '-',
|
||||
content: serviceOverview?.catalogue?.name || '-'
|
||||
},
|
||||
{
|
||||
color: `#${serviceKind === 'ai' ? 'EADEFF' : 'DEFFE7'}`,
|
||||
textColor: 'text-[#000]',
|
||||
title: serviceKind || '-',
|
||||
content: SERVICE_KIND_OPTIONS.find((x) => x.value === serviceKind)?.label || '-'
|
||||
}
|
||||
]
|
||||
|
||||
// 如果启用了MCP,添加MCP标签
|
||||
if (serviceOverview?.enableMcp) {
|
||||
tags.push({
|
||||
color: '#FFF0C1',
|
||||
textColor: 'text-[#000]',
|
||||
title: 'MCP',
|
||||
content: 'MCP'
|
||||
})
|
||||
}
|
||||
|
||||
setServiceTags(tags)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!serviceId && serviceBasicInfo) {
|
||||
setServiceMetricsList(serviceBasicInfo)
|
||||
setServiceOverview(serviceBasicInfo)
|
||||
return
|
||||
}
|
||||
getServiceOverview()
|
||||
}, [serviceId, serviceBasicInfo])
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(35deg, rgb(246, 246, 260) 0%, rgb(255, 255, 255) 40%)'
|
||||
}}
|
||||
className={`w-full ${customClassName}`}
|
||||
classNames={{
|
||||
body: `p-[15px] ${actionSlot ? 'h-[180px]' : 'max-h-[130px]'}`
|
||||
}}
|
||||
>
|
||||
{serviceOverview && (
|
||||
<>
|
||||
<div className="service-info">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size={50}
|
||||
className={`rounded-[12px] border-none rounded-[12px] ${serviceOverview.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`}
|
||||
src={
|
||||
serviceOverview.logo ? (
|
||||
<img
|
||||
src={serviceOverview.logo}
|
||||
alt="Logo"
|
||||
style={{ maxWidth: '200px', width: '45px', height: '45px', objectFit: 'unset' }}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
icon={serviceOverview.logo ? '' : <Icon icon="tabler:api-app" />}
|
||||
>
|
||||
{' '}
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="pl-[20px] w-[calc(100%-50px)] overflow-hidden">
|
||||
<p
|
||||
className={`text-[14px] h-[20px] leading-[20px] truncate font-bold w-full flex items-center gap-[4px]`}
|
||||
>
|
||||
{serviceOverview.serviceName}
|
||||
</p>
|
||||
<div className="mt-[5px] h-[20px] flex items-center font-normal">
|
||||
{serviceTags.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
color={tag.color}
|
||||
className={`${tag.textColor} font-normal border-0 mr-[12px] max-w-[150px] truncate`}
|
||||
bordered={false}
|
||||
title={tag.title}
|
||||
>
|
||||
{tag.content}
|
||||
</Tag>
|
||||
))}
|
||||
{serviceMetrics.map((item, index) => (
|
||||
<Tooltip key={index} title={$t(item.title)}>
|
||||
<span className="mr-[12px] flex items-center">
|
||||
<span className="h-[14px] mr-[4px] flex items-center">{item.icon}</span>
|
||||
<span className="font-normal text-[14px]">{item.value}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{serviceOverview.id && (
|
||||
<>
|
||||
<div className="absolute top-[14px] right-[20px]">
|
||||
<span className="bg-white relative py-[2px] pl-[10px] pr-[30px] inline-block border-solid border-[1px] border-BORDER rounded-lg">
|
||||
{$t('服务 ID')}:{serviceOverview.id || '-'}
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => handleCopy(serviceOverview.id || '')}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
right: '5px',
|
||||
color: '#999',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</span>
|
||||
<Tooltip title={serviceOverview.isReleased ? '' : $t('服务尚未发布')}>
|
||||
<Button
|
||||
disabled={!serviceOverview.isReleased}
|
||||
className="ml-[10px] !max-h-[28px] rounded-[13px]"
|
||||
type="primary"
|
||||
onClick={() => openInPortal()}
|
||||
>
|
||||
{$t('跳转至详情页')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span className="line-clamp-2 mt-[15px] text-[12px] text-[#666]" title={serviceOverview.serviceDesc}>
|
||||
{serviceOverview.serviceDesc || $t('暂无服务描述')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="absolute bottom-[15px]">{actionSlot}</div>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceInfoCard
|
||||
@@ -54,6 +54,10 @@
|
||||
"上游列表": "K54e44357",
|
||||
"备注": "Kb8e8e6f5",
|
||||
"上线情况": "K7e52ffa3",
|
||||
"服务 ID": "K1e84ad04",
|
||||
"服务尚未发布": "Ke1e649cb",
|
||||
"跳转至详情页": "K2e683a7d",
|
||||
"暂无服务描述": "Ka4b45550",
|
||||
"申请原因": "K1ab0ae5b",
|
||||
"审核意见": "K53c00c3c",
|
||||
"暂无(0)权限,请联系管理员分配。": "Kfd50704d",
|
||||
@@ -114,6 +118,7 @@
|
||||
"无需审核:允许任何消费者调用该服务": "K1fc2cc28",
|
||||
"人工审核:仅允许通过人工审核的消费者调用该服务": "K8dabb98e",
|
||||
"开启:AI Agent 等产品能够通过 MCP 方式调用服务": "Ke959f135",
|
||||
"总览": "Kaf9e8011",
|
||||
"永久": "Kbfe02d7f",
|
||||
"否": "K1e9c479e",
|
||||
"是": "Kaddfcb6b",
|
||||
@@ -346,13 +351,12 @@
|
||||
"查询": "Kee8ae330",
|
||||
"请输入 APIURL 搜索": "Kf8187c33",
|
||||
"服务": "Kb58e0c3f",
|
||||
"说明文档": "K6cd677b",
|
||||
"使用说明": "Kdefa9caa",
|
||||
"最近一次更新者": "K617f34f1",
|
||||
"最近一次更新时间": "K6ebca204",
|
||||
"保存": "Kabfe9512",
|
||||
"API 路由": "K51d1eb5d",
|
||||
"API 文档": "Ka2b6d281",
|
||||
"使用说明": "Kdefa9caa",
|
||||
"服务策略": "K52f72551",
|
||||
"发布": "K36856e71",
|
||||
"订阅管理": "K6382bbfd",
|
||||
@@ -361,7 +365,6 @@
|
||||
"管理": "K5974bf24",
|
||||
"调用拓扑图": "K3fa5c4c3",
|
||||
"设置": "Kb5c7b82d",
|
||||
"服务 ID": "K1e84ad04",
|
||||
"新增订阅方": "K39ab0358",
|
||||
"手动添加": "K18307d56",
|
||||
"订阅申请": "K705fe9f5",
|
||||
@@ -559,6 +562,9 @@
|
||||
"MCP 配置": "K6e9c928f",
|
||||
"Open API 文档": "Kb6d0eb39",
|
||||
"AI 代理集成": "Ke6908f16",
|
||||
"请先订阅该服务": "K71ed51fa",
|
||||
"申请": "K4aa9ed2c",
|
||||
"选择 API Key": "K1bec8cbe",
|
||||
"新增 API Key": "Kb0e0aeda",
|
||||
"API 密钥可用于调用系统级 Open API 和 MCP。": "K9d81999c",
|
||||
"MCP 服务": "Kf106bc62",
|
||||
@@ -660,6 +666,25 @@
|
||||
"系统级别角色": "K138facd3",
|
||||
"添加角色": "K6eac768d",
|
||||
"团队级别角色": "Kb9c2cf02",
|
||||
"API / Tools": "K9d526cac",
|
||||
"消费者": "K7acfcfad",
|
||||
"HTTP 状态": "Kc68ba0f4",
|
||||
"IP": "Kb09b747",
|
||||
"通过系统级别的 API Key 来调用": "K2eacb44f",
|
||||
"日志详情": "K764bca7c",
|
||||
"订阅数量": "Ke04bc00d",
|
||||
"已开启": "K1b97ae0a",
|
||||
"开启 MCP": "K19ec733b",
|
||||
"API 使用排名": "Kbee2340",
|
||||
"消费者使用排名": "Kf6af1f40",
|
||||
"请求数": "K318a7519",
|
||||
"Token": "K9ef68e3f",
|
||||
"平均 Token/s 统计": "K6c016898",
|
||||
"平均请求数": "K652843b0",
|
||||
"平均 Token/订阅者统计": "Kf5eeb9c5",
|
||||
"流量": "K53eb7414",
|
||||
"平均响应时间": "K7c8d5c23",
|
||||
"平均流量": "K8158a6e4",
|
||||
"单位:ms,最小值:1": "K2a16c93b",
|
||||
"API 路由设置": "Ka945cfb1",
|
||||
"API 基础信息": "K2e050340",
|
||||
@@ -742,7 +767,6 @@
|
||||
"退出全屏": "Kaf70c3b",
|
||||
"(0)调用详情": "Kd22841a4",
|
||||
"消费者调用统计": "K61cca533",
|
||||
"消费者": "K7acfcfad",
|
||||
"请选择消费者": "Kdfff59d4",
|
||||
"调用趋势": "K8c7f2d2e",
|
||||
"(0)-(1)调用趋势": "K657c3452",
|
||||
@@ -783,14 +807,13 @@
|
||||
"配置集群信息": "Ke5ed9810",
|
||||
"监控设置": "K1a132228",
|
||||
"配置监控信息": "K6af08c3c",
|
||||
"监控总览": "K4a1a14",
|
||||
"服务被调用统计": "K69741ea7",
|
||||
"API 调用统计": "K9c8d9933",
|
||||
"加载数据失败,请重试": "K6c2d93b6",
|
||||
"亿": "K145e4941",
|
||||
"万": "Ke6a935d",
|
||||
"搜索分类或标签": "Kd59290a2",
|
||||
"暂无API数据": "K6b75bdbc",
|
||||
"搜索或选择消费者": "Kb684c806",
|
||||
"该消费者已订阅": "K5611e01e",
|
||||
"申请理由": "K4b15d6f5",
|
||||
"支持把当前服务对接主流的 AI Agent平台,实现在 Agent 平台上快速、安全和合规地使用企业开放的 API 能力。": "K2ec0fa56",
|
||||
"可按以下步骤进行对接:": "K35f23b64",
|
||||
@@ -850,8 +873,6 @@
|
||||
"版本": "K81634069",
|
||||
"更新时间": "Keefda53d",
|
||||
"介绍": "K59cdbec3",
|
||||
"暂无服务描述": "Ka4b45550",
|
||||
"申请": "K4aa9ed2c",
|
||||
"无标签": "K96a2f1c8",
|
||||
"分类": "Kb32f0afe",
|
||||
"服务市场": "K370a3eb2",
|
||||
|
||||
@@ -927,5 +927,34 @@
|
||||
"K71ed51fa": "Please subscribe to the service first",
|
||||
"K1bec8cbe": "Select API Key",
|
||||
"K5611e01e": "This consumer is already subscribed",
|
||||
"Kaf9e8011": "Overview"
|
||||
"Kaf9e8011": "Overview",
|
||||
"Ke1e649cb": "Service not released",
|
||||
"K2e683a7d": "Open in Portal",
|
||||
"Ke04bc00d": "Subscribers",
|
||||
"K1b97ae0a": "Enabled",
|
||||
"K19ec733b": "Enable MCP",
|
||||
"Kbee2340": "Top API",
|
||||
"Kf6af1f40": "Top Consumer",
|
||||
"K318a7519": "Requests",
|
||||
"K9ef68e3f": "Token",
|
||||
"Kfb14ccb0": "Models",
|
||||
"K10a8bee3": "Avg Token per Second",
|
||||
"K2727b76b": "Avg Requests per Subscriber",
|
||||
"K4c7a6704": "Avg Token per Subscriber",
|
||||
"K53eb7414": "Traffic",
|
||||
"K7c8d5c23": "Avg Response Time",
|
||||
"Kf9eb702": "QRS",
|
||||
"K7f0aa740": "Avg Traffic per Subscriber",
|
||||
"K9d526cac": "API / Tools",
|
||||
"Kc68ba0f4": "HTTP Status",
|
||||
"Kb09b747": "IP",
|
||||
"K2eacb44f": "Request the API using a system-level API Key",
|
||||
"K764bca7c": "Log Detail",
|
||||
"K6c016898": "Avg Token per Second",
|
||||
"K652843b0": "Avg Requests per Subscriber",
|
||||
"Kdbf831a0": "Avg Token per Subscriber",
|
||||
"K8158a6e4": "Avg Traffic per Subscriber",
|
||||
"K6b882d4a": "Avg Token per Subscriber",
|
||||
"K6c2d93b6": "Failed to load data, please try again",
|
||||
"Kf5eeb9c5": "Avg Token per Subscriber"
|
||||
}
|
||||
|
||||
@@ -949,5 +949,34 @@
|
||||
"K71ed51fa": "このサービスに先にサブスクリプションしてください",
|
||||
"K1bec8cbe": "APIキーを選択してください",
|
||||
"K5611e01e": "この消費者はすでに購読しています",
|
||||
"Kaf9e8011": "概要"
|
||||
"Kaf9e8011": "概要",
|
||||
"Ke1e649cb": "サービスはまだ公開されていません",
|
||||
"K2e683a7d": "詳細ページへ移動",
|
||||
"Ke04bc00d": "サブスクリプション数",
|
||||
"K1b97ae0a": "有効",
|
||||
"K19ec733b": "MCP を有効にする",
|
||||
"Kbee2340": "API 使用ランキング",
|
||||
"Kf6af1f40": "コンシューマー使用ランキング",
|
||||
"K318a7519": "リクエスト数",
|
||||
"K9ef68e3f": "トークン",
|
||||
"Kfb14ccb0": "モデル使用量",
|
||||
"K10a8bee3": "平均トークン消費量",
|
||||
"K2727b76b": "ユーザーあたり平均リクエスト数",
|
||||
"K4c7a6704": "ユーザーあたり平均トークン消費量",
|
||||
"K53eb7414": "トラフィック",
|
||||
"K7c8d5c23": "平均応答時間",
|
||||
"Kf9eb702": "毎秒リクエスト数",
|
||||
"K7f0aa740": "ユーザーあたり平均トラフィック",
|
||||
"K9d526cac": "API / ツール",
|
||||
"Kc68ba0f4": "HTTP ステータス",
|
||||
"Kb09b747": "IP",
|
||||
"K2eacb44f": "システムレベルの API Key で呼び出し",
|
||||
"K764bca7c": "ログ詳細",
|
||||
"K6c016898": "平均トークン/s 統計",
|
||||
"K652843b0": "平均リクエスト数",
|
||||
"Kdbf831a0": "平均トークン/加入者 統計",
|
||||
"K8158a6e4": "平均トラフィック",
|
||||
"K6b882d4a": "平均トークン/加入者",
|
||||
"K6c2d93b6": "データの読み込みに失敗しました。もう一度お試しください",
|
||||
"Kf5eeb9c5": "平均トークン/加入者 統計"
|
||||
}
|
||||
|
||||
@@ -1,69 +1 @@
|
||||
{
|
||||
"K630c9e6d": "APIPark",
|
||||
"Ka3e9f580": "发布名称",
|
||||
"Kb2480682": "策略列表",
|
||||
"K76036e25": "HTTP 请求头",
|
||||
"K44607e3f": "全等匹配",
|
||||
"Kc287500a": "前缀匹配",
|
||||
"Kfc0b1147": "后缀匹配",
|
||||
"Ka4a92043": "子串匹配",
|
||||
"K30b2e44f": "非等匹配",
|
||||
"Kb1587991": "空值匹配",
|
||||
"K1e97dbd8": "存在匹配",
|
||||
"Kc8ee3e62": "不存在匹配",
|
||||
"K87c5a801": "区分大小写的正则匹配",
|
||||
"K95f062f1": "不区分大小写的正则匹配",
|
||||
"Kfbd230a5": "任意匹配",
|
||||
"Kd85208a3": "驳回",
|
||||
"Kad6aa439": "已订阅",
|
||||
"K9a68443b": "取消申请",
|
||||
"Kaeba0229": "透传客户端请求 Host",
|
||||
"K6d7e2fd0": "使用上游服务 Host",
|
||||
"K31332633": "重写 Host",
|
||||
"K2c2bc64f": "动态服务发现",
|
||||
"K78b1ca25": "地址",
|
||||
"K1644b775": "新增",
|
||||
"Kec91f0db": "申请方消费者",
|
||||
"K118d8d74": "数据格式",
|
||||
"Kfe7c7d2d": "关键字",
|
||||
"K2f57a694": "正则表达式",
|
||||
"K8953e0a6": "手机号",
|
||||
"K6f86a038": "身份证号",
|
||||
"K7954e7c8": "银行卡号",
|
||||
"K320fdb17": "金额",
|
||||
"K7867acda": "日期",
|
||||
"K7d327ae8": "局部显示",
|
||||
"Kfbf38e3c": "局部遮蔽",
|
||||
"Kd8c1fbb0": "截取",
|
||||
"K89829921": "替换",
|
||||
"K480a7165": "乱序",
|
||||
"Kea0d69df": "随机字符串",
|
||||
"Ke7c84d1d": "自定义字符串",
|
||||
"K49731763": "请输入IP地址或CIDR范围,每条以换行分割",
|
||||
"K3a34d49b": "待更新",
|
||||
"Kd2850420": "待删除",
|
||||
"K83237c89": "输入的IP或CIDR不符合格式",
|
||||
"K5ae2c87a": "请正确输入路径,如/usr/*或*/usr/*",
|
||||
"K67f4e9bb": "与外部平台集成时,获取 API 市场中文档信息的域名",
|
||||
"Kc82b8374": "编辑策略",
|
||||
"K4b34a5e5": "策略类型",
|
||||
"K57f0fee8": "匹配条件",
|
||||
"K10650c58": "数据脱敏规则",
|
||||
"K1b34a9ab": "配置脱敏规则",
|
||||
"K26d22405": "匹配值",
|
||||
"K1546e1fe": "脱敏类型",
|
||||
"K9b9b0629": "起始位置",
|
||||
"K52c84fe1": "长度",
|
||||
"Kde84409c": "替换类型",
|
||||
"K338653b4": "替换值",
|
||||
"Kbaeed3b7": "JSON Path",
|
||||
"K4cd91d61": "脱敏规则",
|
||||
"K8dcad979": "自定义字符串; 值:",
|
||||
"K82e3f7b7": "起始位置:(0)位;长度:(1)位",
|
||||
"K49dfc123": "已选择(0)项(1)数据",
|
||||
"K8457ea34": "所有(0)",
|
||||
"K7ca9a795": "属性名称",
|
||||
"Kc4391744": "属性值",
|
||||
"K678e13fc": "配置(0)",
|
||||
"Kf5fd27ed": "输入名称查找用户"
|
||||
}
|
||||
{}
|
||||
@@ -880,5 +880,32 @@
|
||||
"K71ed51fa": "请先订阅该服务",
|
||||
"K1bec8cbe": "选择 API Key",
|
||||
"K5611e01e": "该消费者已订阅",
|
||||
"Kaf9e8011": "总览"
|
||||
"Kaf9e8011": "总览",
|
||||
"Ke1e649cb": "服务尚未发布",
|
||||
"K2e683a7d": "跳转至详情页",
|
||||
"Ke04bc00d": "订阅数量",
|
||||
"K1b97ae0a": "已开启",
|
||||
"K19ec733b": "开启 MCP",
|
||||
"Kbee2340": "API 使用排名",
|
||||
"Kf6af1f40": "消费者使用排名",
|
||||
"K318a7519": "请求数",
|
||||
"K9ef68e3f": "Token",
|
||||
"Kfb14ccb0": "模型使用量",
|
||||
"K10a8bee3": "平均 Token 消耗",
|
||||
"K2727b76b": "人均请求数",
|
||||
"K4c7a6704": "人均 Token 消耗",
|
||||
"K53eb7414": "流量",
|
||||
"K7c8d5c23": "平均响应时间",
|
||||
"Kf9eb702": "每秒请求数量",
|
||||
"K7f0aa740": "人均流量",
|
||||
"K9d526cac": "API / Tools",
|
||||
"Kc68ba0f4": "HTTP 状态",
|
||||
"Kb09b747": "IP",
|
||||
"K2eacb44f": "通过系统级别的 API Key 来调用",
|
||||
"K764bca7c": "日志详情",
|
||||
"K6c016898": "平均 Token/s 统计",
|
||||
"K652843b0": "平均请求数",
|
||||
"K8158a6e4": "平均流量",
|
||||
"K6c2d93b6": "加载数据失败,请重试",
|
||||
"Kf5eeb9c5": "平均 Token/订阅者统计"
|
||||
}
|
||||
|
||||
@@ -949,5 +949,34 @@
|
||||
"K71ed51fa": "請先訂閱該服務",
|
||||
"K1bec8cbe": "選擇 API Key",
|
||||
"K5611e01e": "該消費者已訂閱",
|
||||
"Kaf9e8011": "總覽"
|
||||
"Kaf9e8011": "總覽",
|
||||
"Ke1e649cb": "服務尚未發布",
|
||||
"K2e683a7d": "跳轉至詳情頁",
|
||||
"Ke04bc00d": "訂閱數量",
|
||||
"K1b97ae0a": "已開啟",
|
||||
"K19ec733b": "開啟 MCP",
|
||||
"Kbee2340": "API 使用排名",
|
||||
"Kf6af1f40": "消費者使用排名",
|
||||
"K318a7519": "請求數",
|
||||
"K9ef68e3f": "Token",
|
||||
"Kfb14ccb0": "模型使用量",
|
||||
"K10a8bee3": "平均 Token 消耗",
|
||||
"K2727b76b": "人均請求數",
|
||||
"K4c7a6704": "人均 Token 消耗",
|
||||
"K53eb7414": "流量",
|
||||
"K7c8d5c23": "平均回應時間",
|
||||
"Kf9eb702": "每秒請求數量",
|
||||
"K7f0aa740": "人均流量",
|
||||
"K9d526cac": "API / 工具",
|
||||
"Kc68ba0f4": "HTTP 狀態",
|
||||
"Kb09b747": "IP",
|
||||
"K2eacb44f": "透過系統級 API Key 調用",
|
||||
"K764bca7c": "日誌詳情",
|
||||
"K6c016898": "平均 Token/s 統計",
|
||||
"K652843b0": "平均請求數",
|
||||
"Kdbf831a0": "每位訂閱者平均 Token 統計",
|
||||
"K8158a6e4": "平均流量",
|
||||
"K6b882d4a": "每位訂閱者平均 Token",
|
||||
"K6c2d93b6": "載入資料失敗,請重試",
|
||||
"Kf5eeb9c5": "每位訂閱者平均 Token 統計"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export type AiServiceConfigFieldType = {
|
||||
logoFile?:UploadFile;
|
||||
tags?:Array<string>;
|
||||
description?: string;
|
||||
team?:string;
|
||||
team?:EntityItem;
|
||||
master?:string;
|
||||
serviceType?:'public'|'inner';
|
||||
catalogue?:string | string[];
|
||||
|
||||
@@ -96,6 +96,20 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
key: 'restServiceInside',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsidePage.tsx')),
|
||||
children: [
|
||||
{
|
||||
path: 'overview',
|
||||
key: 'restServiceInsideOverview',
|
||||
lazy: lazy(
|
||||
() => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceOverview/RestServiceContainer')
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
key: 'restServiceInsideLogs',
|
||||
lazy: lazy(
|
||||
() => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceLogs/RestServiceLogsContainer')
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
key: 'restServiceInsideApi',
|
||||
@@ -268,6 +282,20 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsidePage.tsx')
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: 'overview',
|
||||
key: 'aiServiceInsideOverview',
|
||||
lazy: lazy(
|
||||
() => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceOverview/AiServiceContainer')
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
key: 'aiServiceInsideLogs',
|
||||
lazy: lazy(
|
||||
() => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceLogs/AiServiceLogsContainer')
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
key: 'aiServiceInsideApi',
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from './type'
|
||||
|
||||
import { PageProColumns } from '@common/components/aoplatform/PageList'
|
||||
import { LogItem } from '@core/pages/serviceLogs/ServiceLogs'
|
||||
|
||||
export enum SubscribeEnum {
|
||||
Rejected = 0,
|
||||
@@ -500,3 +501,124 @@ export const SYSTEM_PUBLISH_ONLINE_COLUMNS = [
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
/** AI 服务排行 */
|
||||
export const AI_SERVICE_TOP_RANKING_LIST: PageProColumns<any>[] = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '请求总数',
|
||||
dataIndex: 'request',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'Token',
|
||||
dataIndex: 'token',
|
||||
ellipsis: true
|
||||
}
|
||||
]
|
||||
|
||||
/** REST 服务排行 */
|
||||
export const REST_SERVICE_TOP_RANKING_LIST: PageProColumns<any>[] = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '请求总数',
|
||||
dataIndex: 'request',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '流量',
|
||||
dataIndex: 'traffic',
|
||||
ellipsis: true
|
||||
}
|
||||
]
|
||||
|
||||
/** REST 服务日志 */
|
||||
export const REST_SERVICE_LOG_LIST: PageProColumns<LogItem>[] = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'logTime',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'API / Tools',
|
||||
dataIndex: ['api', 'name'],
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '消费者',
|
||||
dataIndex: ['consumers', 'name'],
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'HTTP 状态',
|
||||
dataIndex: 'status',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'IP',
|
||||
dataIndex: 'ip',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
dataIndex: 'responseTime',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '流量',
|
||||
dataIndex: 'traffic',
|
||||
ellipsis: true
|
||||
}
|
||||
]
|
||||
|
||||
/** AI 服务日志 */
|
||||
export const AI_SERVICE_LOG_LIST: PageProColumns<LogItem>[] = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'logTime',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'API / Tools',
|
||||
dataIndex: ['api', 'name'],
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '消费者',
|
||||
dataIndex: ['consumers', 'name'],
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'HTTP 状态',
|
||||
dataIndex: 'status',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '模型',
|
||||
dataIndex: 'model',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'IP',
|
||||
dataIndex: 'ip',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'Token/s',
|
||||
dataIndex: 'tokenPerSecond',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'Token',
|
||||
dataIndex: 'token',
|
||||
ellipsis: true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1156,6 +1156,22 @@ p{
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ranking-list .ant-pro-table{
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
border: none !important;
|
||||
}
|
||||
.ranking-list .ant-table-tbody:not(tbody) .ant-table-cell{
|
||||
padding: 10px 10px !important;
|
||||
}
|
||||
.ranking-list .ant-table-header .ant-table-thead th{
|
||||
background-color: #fff !important;
|
||||
padding: 10px 10px !important;
|
||||
}
|
||||
.ranking-list .ant-table-header .ant-table-thead th::before{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-alert-info{
|
||||
background: #1784FC1A !important;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { AiServiceConfigFieldType } from '@core/const/ai-service/type.ts'
|
||||
import { App, Menu, MenuProps } from 'antd'
|
||||
import { ItemType, MenuItemGroupType, MenuItemType } from 'antd/es/menu/interface'
|
||||
import Paragraph from 'antd/es/typography/Paragraph'
|
||||
import ServiceInfoCard from '@common/components/aoplatform/serviceInfoCard.tsx'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
@@ -67,6 +67,7 @@ const AiServiceInsidePage: FC = () => {
|
||||
'assets',
|
||||
null,
|
||||
[
|
||||
getItem(<Link to="./overview">{$t('总览')}</Link>, 'overview', undefined, undefined, undefined, ''),
|
||||
getItem(
|
||||
<Link to="./route">{$t('API 路由')}</Link>,
|
||||
'route',
|
||||
@@ -149,7 +150,8 @@ const AiServiceInsidePage: FC = () => {
|
||||
'project.myAiService.topology.view'
|
||||
)
|
||||
: null,
|
||||
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting', undefined, undefined, undefined, '')
|
||||
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting', undefined, undefined, undefined, ''),
|
||||
getItem(<Link to="./logs">{$t('日志')}</Link>, 'logs', undefined, undefined, undefined, '')
|
||||
],
|
||||
'group'
|
||||
)
|
||||
@@ -202,7 +204,7 @@ const AiServiceInsidePage: FC = () => {
|
||||
} else if (serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]) {
|
||||
setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1])
|
||||
} else {
|
||||
setActiveMenu('route')
|
||||
setActiveMenu('overview')
|
||||
}
|
||||
}, [currentUrl])
|
||||
|
||||
@@ -231,17 +233,8 @@ const AiServiceInsidePage: FC = () => {
|
||||
{showMenu ? (
|
||||
<InsidePage
|
||||
pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[
|
||||
...(aiServiceInfo?.enable_mcp ? [{ label: 'MCP', color: '#FFF0C1', className: 'text-[#000]' }] : []),
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
{$t('服务 ID')}:{serviceId || '-'}
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
]}
|
||||
backUrl="/service/list"
|
||||
customBanner={<ServiceInfoCard serviceId={serviceId} teamId={teamId} />}
|
||||
>
|
||||
<div className="flex flex-1 h-full">
|
||||
<Menu
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import ServiceLogs from "./ServiceLogs"
|
||||
const AiServiceLogsContainer = () => {
|
||||
return <ServiceLogs serviceType="aiService" />
|
||||
}
|
||||
|
||||
export default AiServiceLogsContainer
|
||||
@@ -0,0 +1,89 @@
|
||||
import { IconButton } from '@common/components/postcat/api/IconButton'
|
||||
import useCopyToClipboard from '@common/hooks/copy'
|
||||
import { RESPONSE_TIPS } from '@common/const/const'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { App } from 'antd'
|
||||
import ReactJson from 'react-json-view'
|
||||
|
||||
const ApiNetWorkDataPreview = ({ configContent = {} }: { configContent?: { [key: string]: string | undefined } }) => {
|
||||
/** 复制组件 */
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
/** 弹窗组件 */
|
||||
const { message } = App.useApp()
|
||||
/**
|
||||
* 复制
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
const handleCopy = async (value: string): Promise<void> => {
|
||||
if (value) {
|
||||
copyToClipboard(value)
|
||||
message.success($t(RESPONSE_TIPS.copySuccess))
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 判断字符串是否是有效的JSON对象字符串
|
||||
*/
|
||||
const isJsonString = (str: string): boolean => {
|
||||
try {
|
||||
const parsed = JSON.parse(str)
|
||||
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.keys(configContent).map((item) => {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<div className="font-semibold text-[16px] mb-[10px]">{item}</div>
|
||||
<div className="bg-[#0a0b21] text-white p-4 rounded-md my-2 font-mono text-sm overflow-auto relative">
|
||||
{!configContent[item] ? (
|
||||
<pre className="whitespace-pre-wrap break-words"></pre>
|
||||
) : isJsonString(configContent[item] || '') ? (
|
||||
// 如果是有效的JSON对象字符串,使用ReactJson渲染
|
||||
<ReactJson
|
||||
src={JSON.parse(configContent[item] || '')}
|
||||
theme="monokai"
|
||||
indentWidth={2}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={false}
|
||||
name={false}
|
||||
collapsed={false}
|
||||
enableClipboard={false}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// 如果是普通字符串,直接用pre渲染
|
||||
<pre className="whitespace-pre-wrap break-words my-[8px]">{configContent[item]}</pre>
|
||||
)}
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => handleCopy(configContent[item] || '')}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
right: '5px',
|
||||
color: '#999',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ApiNetWorkDataPreview
|
||||
@@ -0,0 +1,424 @@
|
||||
import { Descriptions, DescriptionsProps, Spin, Tabs, Tooltip, message } from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import React from 'react'
|
||||
import { ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import ApiNetWorkDataPreview from './ApiNetWorkDataPreview'
|
||||
import { LogItem } from './ServiceLogs'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
|
||||
// 定义状态码颜色映射枚举
|
||||
export enum HttpStatusColor {
|
||||
SUCCESS = '#7EC26A',
|
||||
CLIENT_ERROR = '#F2CF59',
|
||||
SERVER_ERROR = '#f80f34'
|
||||
}
|
||||
|
||||
type LogDetailProps = {
|
||||
selectedRow?: LogItem
|
||||
serviceType: 'aiService' | 'restService'
|
||||
serviceId?: string
|
||||
teamId?: string
|
||||
}
|
||||
|
||||
type AIServiceDetailType = {
|
||||
id: string
|
||||
api: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
logTime: string
|
||||
consumer: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
isSystemConsumer: boolean
|
||||
status: string
|
||||
provider: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
model: string
|
||||
ip: string
|
||||
request: {
|
||||
header: string
|
||||
body: string
|
||||
origin: string
|
||||
token: number
|
||||
}
|
||||
response: {
|
||||
header: string
|
||||
body: string
|
||||
origin: string
|
||||
token: string
|
||||
}
|
||||
}
|
||||
|
||||
type RestServiceDetailType = {
|
||||
id: string
|
||||
api: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
logTime: string
|
||||
consumer: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
isSystemConsumer: boolean
|
||||
status: string
|
||||
ip: string
|
||||
request: {
|
||||
header: string
|
||||
origin: string
|
||||
}
|
||||
response: {
|
||||
header: string
|
||||
origin: string
|
||||
}
|
||||
}
|
||||
|
||||
const LogDetail = ({ selectedRow, serviceType, serviceId, teamId }: LogDetailProps) => {
|
||||
/** 顶部描述 */
|
||||
const [descriptionItems, setDescriptionItems] = useState<DescriptionsProps['items']>()
|
||||
/** 全局状态 */
|
||||
const { state } = useGlobalContext()
|
||||
/** Request 标签页数据 */
|
||||
const [requestInfoData, setRequestInfoData] = useState<{ [key: string]: string | undefined }>()
|
||||
/** Response 标签页数据 */
|
||||
const [responseInfoData, setResponseInfoData] = useState<{ [key: string]: string | undefined }>()
|
||||
/** 面板 loading */
|
||||
const [dashboardLoading, setDashboardLoading] = useState(true)
|
||||
/**
|
||||
* 请求数据
|
||||
*/
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
/**
|
||||
* 根据状态码返回对应颜色的文本
|
||||
* @param status 状态
|
||||
* @returns
|
||||
*/
|
||||
const renderStatusWithColor = (status: string) => {
|
||||
// 获取状态码首位数字
|
||||
const firstDigit = status.charAt(0)
|
||||
let color = ''
|
||||
switch (firstDigit) {
|
||||
case '2':
|
||||
color = HttpStatusColor.SUCCESS
|
||||
break
|
||||
case '4':
|
||||
color = HttpStatusColor.CLIENT_ERROR
|
||||
break
|
||||
case '5':
|
||||
color = HttpStatusColor.SERVER_ERROR
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return color ? <span style={{ color }}>{status}</span> : status
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签页内容
|
||||
*/
|
||||
const tabItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'request',
|
||||
label: 'Request',
|
||||
children: <ApiNetWorkDataPreview configContent={requestInfoData} />
|
||||
},
|
||||
{
|
||||
key: 'response',
|
||||
label: 'Response',
|
||||
children: <ApiNetWorkDataPreview configContent={responseInfoData} />
|
||||
}
|
||||
],
|
||||
[state.language, requestInfoData, responseInfoData]
|
||||
)
|
||||
|
||||
/**
|
||||
* 设置 AI 描述文案
|
||||
*/
|
||||
const getAIServiceDescriptionItemsList = ({
|
||||
time,
|
||||
api,
|
||||
consumer,
|
||||
status,
|
||||
model,
|
||||
ip
|
||||
}: {
|
||||
time: string
|
||||
api: string
|
||||
consumer: string
|
||||
status: string
|
||||
model: string
|
||||
ip: string
|
||||
}) => {
|
||||
setDescriptionItems([
|
||||
{
|
||||
key: 'time',
|
||||
label: $t('时间'),
|
||||
children: time
|
||||
},
|
||||
{
|
||||
key: 'api',
|
||||
label: $t('API / Tools'),
|
||||
children: api
|
||||
},
|
||||
{
|
||||
key: 'consumer',
|
||||
label: $t('消费者'),
|
||||
children: consumer
|
||||
},
|
||||
{
|
||||
key: 'httpStatus',
|
||||
label: $t('HTTP 状态'),
|
||||
children: renderStatusWithColor(status)
|
||||
},
|
||||
{
|
||||
key: 'model',
|
||||
label: $t('模型'),
|
||||
children: model
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
label: $t('IP'),
|
||||
children: ip
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 REST 描述文案
|
||||
*/
|
||||
const getRestServiceDescriptionItemsList = ({
|
||||
time,
|
||||
api,
|
||||
consumer,
|
||||
isSystemConsumer,
|
||||
status,
|
||||
ip
|
||||
}: {
|
||||
time: string
|
||||
api: string
|
||||
consumer: string
|
||||
isSystemConsumer?: boolean
|
||||
status: string
|
||||
ip: string
|
||||
}) => {
|
||||
setDescriptionItems([
|
||||
{
|
||||
key: 'time',
|
||||
label: $t('时间'),
|
||||
children: time
|
||||
},
|
||||
{
|
||||
key: 'api',
|
||||
label: $t('API / Tools'),
|
||||
children: api
|
||||
},
|
||||
{
|
||||
key: 'consumer',
|
||||
label: $t('消费者'),
|
||||
children: (
|
||||
<>
|
||||
<span className="mr-[50px]">{consumer}</span>
|
||||
{isSystemConsumer && (
|
||||
<span>
|
||||
<span>System-level API Key</span>
|
||||
<Tooltip title={$t('通过系统级别的 API Key 来调用')}>
|
||||
<span className="ml-[12px] items-center">
|
||||
<ExclamationCircleOutlined className="text-[14px] h-[14px] w-[14px]" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'httpStatus',
|
||||
label: $t('HTTP 状态'),
|
||||
children: renderStatusWithColor(status)
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
label: $t('IP'),
|
||||
children: ip
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 服务日志详情
|
||||
*/
|
||||
const getAIServiceLogDetail = () => {
|
||||
fetchData<BasicResponse<{ log: AIServiceDetailType }>>('service/log/ai', {
|
||||
method: 'GET',
|
||||
eoParams: { log: selectedRow?.id, service: serviceId, team: teamId },
|
||||
eoTransformKeys: ['is_system_consumer', 'log_time'],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
// const result = data.log
|
||||
const result = {
|
||||
id: '123',
|
||||
api: {
|
||||
id: '222',
|
||||
name: 'api222'
|
||||
},
|
||||
logTime: '2023-01-01 00:00:00',
|
||||
consumer: {
|
||||
id: '333',
|
||||
name: 'consumers333'
|
||||
},
|
||||
isSystemConsumer: false,
|
||||
status: '200',
|
||||
provider: {
|
||||
id: '444',
|
||||
name: 'provider444'
|
||||
},
|
||||
model: 'model1',
|
||||
ip: '1.1.1.1',
|
||||
request: {
|
||||
header:
|
||||
'{\n "mcpServers": {\n "APIPark/test1234": {\n "url": "http://swagger-demo.apinto.com/openapi/v1/mcp/service/c8bc25ca-8855-45cd-8bcc-239195b6c346/sse?apikey={your_api_key}"\n }\n }\n}',
|
||||
body: '{\n "mcpServers": {\n "APIPark/44444": {\n "url": "http://swagger-demo.apinto.com/openapi/v1/mcp/service/c8bc25ca-8855-45cd-8bcc-239195b6c346/sse?apikey={your_api_key}"\n }\n }\n}',
|
||||
origin: '123',
|
||||
token: 0
|
||||
},
|
||||
response: {
|
||||
header:
|
||||
'{\n "mcpServers": {\n "APIPark/44444": {\n "url": "http://swagger-demo.apinto.com/openapi/v1/mcp/service/c8bc25ca-8855-45cd-8bcc-239195b6c346/sse?apikey={your_api_key}"\n }\n }\n}',
|
||||
body: '{\n "mcpServers": {\n "APIPark/44444": {\n "url": "http://swagger-demo.apinto.com/openapi/v1/mcp/service/c8bc25ca-8855-45cd-8bcc-239195b6c346/sse?apikey={your_api_key}"\n }\n }\n}',
|
||||
origin: '312',
|
||||
token: '333'
|
||||
}
|
||||
}
|
||||
getAIServiceDescriptionItemsList({
|
||||
time: result.logTime,
|
||||
api: result.api.name,
|
||||
consumer: result.consumer.name,
|
||||
status: result.status,
|
||||
model: result.model,
|
||||
ip: result.ip
|
||||
})
|
||||
setRequestInfoData({
|
||||
Header: result.request.header,
|
||||
Body: result.request.body,
|
||||
Origin: result.request.origin
|
||||
})
|
||||
setResponseInfoData({
|
||||
Header: result.response.header,
|
||||
Body: result.response.body,
|
||||
Origin: result.response.origin
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取 REST 服务日志详情
|
||||
*/
|
||||
const getRestServiceLogDetail = () => {
|
||||
fetchData<BasicResponse<{ log: RestServiceDetailType }>>('service/log/rest', {
|
||||
method: 'GET',
|
||||
eoParams: { log: selectedRow?.id, service: serviceId, team: teamId },
|
||||
eoTransformKeys: ['is_system_consumer', 'log_time'],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const result = {
|
||||
id: '123',
|
||||
api: {
|
||||
id: '222',
|
||||
name: 'api222'
|
||||
},
|
||||
logTime: '2023-01-01 00:00:00',
|
||||
consumer: {
|
||||
id: '333',
|
||||
name: 'consumers333'
|
||||
},
|
||||
isSystemConsumer: true,
|
||||
status: '200',
|
||||
ip: '1.1.1.1',
|
||||
request: {
|
||||
header:
|
||||
'{\n "mcpServers": {\n "APIPark/test1234": {\n "url": "http://swagger-demo.apinto.com/openapi/v1/mcp/service/c8bc25ca-8855-45cd-8bcc-239195b6c346/sse?apikey={your_api_key}"\n }\n }\n}',
|
||||
origin: '123'
|
||||
},
|
||||
response: {
|
||||
header:
|
||||
'{\n "mcpServers": {\n "APIPark/44444": {\n "url": "http://swagger-demo.apinto.com/openapi/v1/mcp/service/c8bc25ca-8855-45cd-8bcc-239195b6c346/sse?apikey={your_api_key}"\n }\n }\n}',
|
||||
origin: '312'
|
||||
}
|
||||
}
|
||||
getRestServiceDescriptionItemsList({
|
||||
time: result.logTime,
|
||||
api: result.api.name,
|
||||
consumer: result.consumer.name,
|
||||
status: result.status,
|
||||
ip: result.ip,
|
||||
isSystemConsumer: result.isSystemConsumer
|
||||
})
|
||||
setRequestInfoData({
|
||||
Header: result.request.header,
|
||||
Origin: result.request.origin
|
||||
})
|
||||
setResponseInfoData({
|
||||
Header: result.response.header,
|
||||
Origin: result.response.origin
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
setDashboardLoading(true)
|
||||
serviceType === 'aiService' ? getAIServiceLogDetail() : getRestServiceLogDetail()
|
||||
}, [serviceType])
|
||||
|
||||
return (
|
||||
<Spin
|
||||
className="h-full pb-[20px]"
|
||||
wrapperClassName="h-full min-h-[150px]"
|
||||
indicator={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ transform: 'scale(1.5)' }}>
|
||||
<LoadingOutlined style={{ fontSize: 30 }} spin />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
spinning={dashboardLoading}
|
||||
>
|
||||
<Descriptions
|
||||
column={1}
|
||||
className="[&_.ant-descriptions-item]:p-0 [&_.ant-descriptions-item]:py-[5px]"
|
||||
colon={false}
|
||||
items={descriptionItems}
|
||||
classNames={{
|
||||
label: 'w-[250px] text-right pr-[12px]'
|
||||
}}
|
||||
contentStyle={{ fontWeight: '600' }}
|
||||
/>
|
||||
<div className="mt-[5px]">
|
||||
<Tabs
|
||||
className="overflow-hidden h-full [&>.ant-tabs-content-holder]:overflow-auto global-policy-tabs"
|
||||
items={tabItems}
|
||||
/>
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogDetail
|
||||
@@ -0,0 +1,7 @@
|
||||
import ServiceLogs from "./ServiceLogs"
|
||||
|
||||
const RestServiceLogsContainer = () => {
|
||||
return <ServiceLogs serviceType="restService" />
|
||||
}
|
||||
|
||||
export default RestServiceLogsContainer
|
||||
@@ -0,0 +1,282 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { Drawer, Spin, message } from 'antd'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import DateSelectFilter, { TimeOption } from '../serviceOverview/filter/DateSelectFilter'
|
||||
import { TimeRange } from '@common/components/aoplatform/TimeRangeSelector'
|
||||
import PageList from '@common/components/aoplatform/PageList'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { REST_SERVICE_LOG_LIST, AI_SERVICE_LOG_LIST } from '@core/const/system/const'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import LogDetail, { HttpStatusColor } from './LogDetail'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { ActionType } from '@ant-design/pro-components'
|
||||
import { getTime } from '@dashboard/utils/dashboard'
|
||||
|
||||
export type LogItem = {
|
||||
id: string
|
||||
api: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
status: number
|
||||
logTime: string
|
||||
responseTime: string
|
||||
token?: number
|
||||
model?: string
|
||||
tokenPerSecond?: string
|
||||
traffic?: string
|
||||
consumers?: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
provider?: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
const ServiceLogs = ({ serviceType }: { serviceType: 'aiService' | 'restService' }) => {
|
||||
/** 路由参数 */
|
||||
const { serviceId, teamId } = useParams<{ serviceId: string; teamId: string }>()
|
||||
/** 面板 loading */
|
||||
const [dashboardLoading, setDashboardLoading] = useState(true)
|
||||
/** 当前选中的时间范围 */
|
||||
const [timeRange, setTimeRange] = useState<TimeRange | undefined>()
|
||||
/** 默认时间 */
|
||||
const [defaultTime] = useState<TimeOption>('sevenDays')
|
||||
/** 全局状态 */
|
||||
const { state } = useGlobalContext()
|
||||
/**
|
||||
* 请求数据
|
||||
*/
|
||||
const { fetchData } = useFetch()
|
||||
// 打开侧边弹窗
|
||||
const [drawerOpen, setDrawerOpen] = useState<boolean>(false)
|
||||
/** 选中的行 */
|
||||
const [selectedRow, setSelectedRow] = useState<LogItem>()
|
||||
/**
|
||||
* 列表ref
|
||||
*/
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
/** 列 */
|
||||
const columns = useMemo(() => {
|
||||
return [...(serviceType === 'aiService' ? AI_SERVICE_LOG_LIST : REST_SERVICE_LOG_LIST)].map((x) => {
|
||||
if (x.dataIndex === 'status') {
|
||||
x.render = (text: any, record: any) => (
|
||||
<>
|
||||
<div className="w-full">
|
||||
{renderStatusWithColor(record.status)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return {
|
||||
...x,
|
||||
title: typeof x.title === 'string' ? $t(x.title as string) : x.title
|
||||
}
|
||||
})
|
||||
}, [state.language])
|
||||
|
||||
/**
|
||||
* 根据状态码返回对应颜色的文本
|
||||
* @param status 状态
|
||||
* @returns
|
||||
*/
|
||||
const renderStatusWithColor = (status: string | number) => {
|
||||
// 获取状态码首位数字
|
||||
const firstDigit = status.toString().charAt(0)
|
||||
let color = ''
|
||||
switch (firstDigit) {
|
||||
case '2':
|
||||
color = HttpStatusColor.SUCCESS
|
||||
break
|
||||
case '4':
|
||||
color = HttpStatusColor.CLIENT_ERROR
|
||||
break
|
||||
case '5':
|
||||
color = HttpStatusColor.SERVER_ERROR
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return color ? <span style={{ color }}>{status}</span> : status
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 列表数据
|
||||
* @param dataType
|
||||
* @returns
|
||||
*/
|
||||
const getAiServiceLogList = () => {
|
||||
return fetchData<BasicResponse<{ log: LogItem[] }>>(`service/logs/ai`, {
|
||||
method: 'GET',
|
||||
eoParams: {
|
||||
service: serviceId,
|
||||
team: teamId,
|
||||
start: timeRange?.start,
|
||||
end: timeRange?.end
|
||||
},
|
||||
eoTransformKeys: ['log_time', 'response_time', 'token_per_second'],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
// 保存数据
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: '123123',
|
||||
api: {
|
||||
id: '444',
|
||||
name: 'api1'
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
status: 200,
|
||||
logTime: '2023-01-01 00:00:00',
|
||||
token: 123,
|
||||
consumers: {
|
||||
id: '333',
|
||||
name: 'consumers333'
|
||||
},
|
||||
model: 'GPT444',
|
||||
tokenPerSecond: '123m/s'
|
||||
}
|
||||
],
|
||||
total: 1,
|
||||
success: true
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return { data: [], success: false }
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取 REST 列表数据
|
||||
* @param dataType
|
||||
* @returns
|
||||
*/
|
||||
const getRestServiceLogList = () => {
|
||||
return fetchData<BasicResponse<{ log: LogItem[] }>>(`service/logs/rest`, {
|
||||
method: 'GET',
|
||||
eoParams: {
|
||||
service: serviceId,
|
||||
team: teamId,
|
||||
start: timeRange?.start,
|
||||
end: timeRange?.end
|
||||
},
|
||||
eoTransformKeys: ['log_time', 'response_time', 'token_per_second'],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const data = []
|
||||
for (let i = 0; i < 100; i++) {
|
||||
data.push({
|
||||
id: '123123' + i,
|
||||
api: {
|
||||
id: '444' + i,
|
||||
name: 'api1' + i
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
status: 200,
|
||||
logTime: '2023-01-01 00:00:00',
|
||||
responseTime: '1111-01-01 00:00:00',
|
||||
traffic: '123',
|
||||
consumers: {
|
||||
id: '123' + i,
|
||||
name: 'consumers222' + i
|
||||
}
|
||||
})
|
||||
}
|
||||
// 保存数据
|
||||
return {
|
||||
data: data,
|
||||
total: data.length,
|
||||
success: true
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return { data: [], success: false }
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { startTime, endTime } = getTime(defaultTime, [])
|
||||
setTimeRange({
|
||||
start: startTime,
|
||||
end: endTime
|
||||
})
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (timeRange) {
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
}, [timeRange])
|
||||
|
||||
/** 行点击 */
|
||||
const handleRowClick = (record: LogItem) => {
|
||||
setSelectedRow(record)
|
||||
setDrawerOpen(true)
|
||||
}
|
||||
|
||||
/** 时间选择回调 */
|
||||
const selectCallback = (date: TimeRange) => {
|
||||
setTimeRange(date)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setDashboardLoading(false)
|
||||
}, [])
|
||||
return (
|
||||
<Spin
|
||||
className="h-full pb-[20px]"
|
||||
wrapperClassName="h-full min-h-[150px]"
|
||||
indicator={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ transform: 'scale(1.5)' }}>
|
||||
<LoadingOutlined style={{ fontSize: 30 }} spin />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
spinning={dashboardLoading}
|
||||
>
|
||||
<div className="mr-PAGE_INSIDE_X">
|
||||
<DateSelectFilter selectCallback={selectCallback} customClassNames={'pt-[0px]'} defaultTime={defaultTime} />
|
||||
<div className="mt-[20px]">
|
||||
<PageList
|
||||
ref={pageListRef}
|
||||
id={`${serviceType}_logs`}
|
||||
columns={[...columns]}
|
||||
minVirtualHeight={430}
|
||||
request={async () => (serviceType === 'aiService' ? getAiServiceLogList() : getRestServiceLogList())}
|
||||
onRowClick={(row: LogItem) => handleRowClick(row)}
|
||||
/>
|
||||
</div>
|
||||
<Drawer
|
||||
destroyOnClose={true}
|
||||
maskClosable={false}
|
||||
title={$t('日志详情')}
|
||||
width={'40%'}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
open={drawerOpen}
|
||||
>
|
||||
<LogDetail selectedRow={selectedRow} serviceId={serviceId} teamId={teamId} serviceType={serviceType} />
|
||||
</Drawer>
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceLogs
|
||||
@@ -0,0 +1,7 @@
|
||||
import ServiceOverview from "./serviceOverview"
|
||||
|
||||
const AiServiceContainer = () => {
|
||||
return <ServiceOverview serviceType="aiService" />
|
||||
}
|
||||
|
||||
export default AiServiceContainer
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FC } from 'react'
|
||||
import ServiceOverview from './serviceOverview'
|
||||
|
||||
const RestServiceContainer: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ServiceOverview serviceType="restService" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RestServiceContainer
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import ECharts, { EChartsOption } from 'echarts-for-react'
|
||||
import { $t } from '@common/locales'
|
||||
|
||||
type AreaChartInfo = {
|
||||
title: string
|
||||
value: string
|
||||
date: string[]
|
||||
data: number[]
|
||||
}
|
||||
type ServiceAreaCharProps = {
|
||||
customClassNames?: string
|
||||
dataInfo?: AreaChartInfo
|
||||
height?: number
|
||||
}
|
||||
|
||||
const ServiceAreaChart = ({ customClassNames, dataInfo, height }: ServiceAreaCharProps) => {
|
||||
const chartRef = useRef<ECharts>(null)
|
||||
const [option, setOption] = useState<EChartsOption | undefined>({})
|
||||
const setChartOption = (dataInfo: AreaChartInfo) => {
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: function (pt) {
|
||||
return [pt[0], '10%']
|
||||
}
|
||||
},
|
||||
title: {
|
||||
show: false
|
||||
},
|
||||
toolbox: {
|
||||
show: false
|
||||
},
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '3%',
|
||||
bottom: '5%',
|
||||
top: '100px',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dataInfo.date
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: [0, '5%'],
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
dataZoom: [],
|
||||
series: [
|
||||
{
|
||||
name: dataInfo.title,
|
||||
type: 'line',
|
||||
symbol: 'none',
|
||||
sampling: 'lttb',
|
||||
itemStyle: {
|
||||
color: 'rgb(255, 70, 131)'
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(255, 158, 68)'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgb(255, 70, 131)'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
data: dataInfo.data
|
||||
}
|
||||
]
|
||||
}
|
||||
setOption(option)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!dataInfo) return
|
||||
setChartOption(dataInfo)
|
||||
}, [dataInfo])
|
||||
return (
|
||||
<div className={`w-full ${customClassNames}`}>
|
||||
<div className="absolute top-[10px] left-[10px] w-full">
|
||||
<div className="text-[16px] text-[#999]">{$t(dataInfo?.title || '')}</div>
|
||||
<div className="relative top-[-6px]">
|
||||
<span className="text-[30px] font-bold">{dataInfo?.value}</span>
|
||||
<div className="absolute top-[5px] right-[8%] flex flex-col items-end">
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-[#ff4683] text-[9px]">▲</span>
|
||||
<span className="ml-1">381 T/s</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-[#4bdb6a] text-[9px]">▼</span>
|
||||
<span className="ml-1">381 T/s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ECharts ref={chartRef} option={option} style={{ height: height || 400 }} opts={{ renderer: 'svg' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceAreaChart
|
||||
@@ -0,0 +1,174 @@
|
||||
import ECharts, { EChartsOption } from 'echarts-for-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
|
||||
export type BarChartInfo = {
|
||||
title: string
|
||||
value: string
|
||||
date: string[]
|
||||
data: {
|
||||
name: string
|
||||
color: string
|
||||
value: number[]
|
||||
}[]
|
||||
}
|
||||
|
||||
type ServiceBarCharProps = {
|
||||
customClassNames?: string
|
||||
dataInfo?: BarChartInfo
|
||||
height?: number
|
||||
}
|
||||
|
||||
const ServiceBarChar = ({ customClassNames, dataInfo, height }: ServiceBarCharProps) => {
|
||||
const chartRef = useRef<ECharts>(null)
|
||||
const [option, setOption] = useState<EChartsOption | undefined>({})
|
||||
const [detaultColor] = useState('#5470c6')
|
||||
const setChartOption = (dataInfo: BarChartInfo) => {
|
||||
const isNumberArray = typeof dataInfo.data[0] !== 'object'
|
||||
const legendData = isNumberArray ? [dataInfo.title] : dataInfo.data.map((item) => item.name)
|
||||
const tooltipFormatter = (params: { name: string; color: string; seriesIndex?: number }) => {
|
||||
let tooltipContent = `<div style="width:140px;padding:8px;">
|
||||
<div>${isNumberArray ? '' : params.name}</div>`
|
||||
const data = isNumberArray
|
||||
? [
|
||||
{
|
||||
name: params.name,
|
||||
color: detaultColor,
|
||||
value: dataInfo.data
|
||||
}
|
||||
]
|
||||
: dataInfo.data
|
||||
// 为每个数据系列添加一行
|
||||
data.forEach((item, index) => {
|
||||
const color = item.color
|
||||
const name = item.name
|
||||
const value = item.value[dataInfo.date.indexOf(params.name)] || 0
|
||||
|
||||
const marker = `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`
|
||||
tooltipContent += `<div style="margin-top: ${index === 0 ? 8 : 4}px;">
|
||||
${marker} ${name} <span style="float:right;font-weight:bold;">${value}</span>
|
||||
</div>`
|
||||
})
|
||||
|
||||
tooltipContent += '</div>'
|
||||
return tooltipContent
|
||||
}
|
||||
const option: EChartsOption = {
|
||||
title: [
|
||||
{
|
||||
text: '{titleStyle|' + $t(dataInfo.title) + '}\n{valueStyle|' + dataInfo.value + '}',
|
||||
left: '4%',
|
||||
top: '0',
|
||||
textStyle: {
|
||||
rich: {
|
||||
titleStyle: {
|
||||
fontSize: 14,
|
||||
color: '#999',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 20
|
||||
},
|
||||
valueStyle: {
|
||||
fontSize: 25,
|
||||
color: '#000',
|
||||
fontWeight: 500,
|
||||
lineHeight: 40
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '3%',
|
||||
bottom: '5%',
|
||||
top: '100px',
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function (params: { name: string; color: string; seriesIndex?: number }) {
|
||||
return tooltipFormatter(params)
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
data: legendData,
|
||||
right: '10px',
|
||||
top: '30px',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
},
|
||||
icon: 'rect'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dataInfo.date,
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#ccc'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '',
|
||||
min: 0,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#eee'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
}
|
||||
},
|
||||
series: isNumberArray
|
||||
? [
|
||||
{
|
||||
name: dataInfo.title,
|
||||
type: 'bar',
|
||||
stack: '总量',
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
itemStyle: {
|
||||
color: detaultColor
|
||||
},
|
||||
data: dataInfo.data
|
||||
}
|
||||
]
|
||||
: dataInfo.data.map((item) => ({
|
||||
name: item.name,
|
||||
type: 'bar',
|
||||
stack: '总量',
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
itemStyle: {
|
||||
color: item.color
|
||||
},
|
||||
data: item.value
|
||||
}))
|
||||
}
|
||||
setOption(option)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataInfo) return
|
||||
setChartOption(dataInfo)
|
||||
}, [dataInfo])
|
||||
return (
|
||||
<div className={`w-full ${customClassNames}`}>
|
||||
<ECharts ref={chartRef} option={option} style={{ height: height || 400 }} opts={{ renderer: 'svg' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceBarChar
|
||||
@@ -0,0 +1,37 @@
|
||||
import TimeRangeSelector, { RangeValue, TimeRange } from '@common/components/aoplatform/TimeRangeSelector'
|
||||
import { useState } from 'react'
|
||||
|
||||
export type TimeOption = '' | 'hour' | 'day' | 'threeDays' | 'sevenDays'
|
||||
const DateSelectFilter = ({
|
||||
selectCallback,
|
||||
defaultTime,
|
||||
customClassNames
|
||||
}: {
|
||||
selectCallback: (timeRange: TimeRange) => void
|
||||
defaultTime: TimeOption
|
||||
customClassNames?: string
|
||||
}) => {
|
||||
/** 默认时间 */
|
||||
const [timeButton, setTimeButton] = useState<TimeOption>(defaultTime || 'hour')
|
||||
/** 日期选择 */
|
||||
const [datePickerValue, setDatePickerValue] = useState<RangeValue>()
|
||||
/** 时间范围变化 */
|
||||
const handleTimeRangeChange = (timeRange: TimeRange) => {
|
||||
selectCallback(timeRange)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TimeRangeSelector
|
||||
labelSize="small"
|
||||
customClassNames={customClassNames}
|
||||
initialTimeButton={timeButton}
|
||||
onTimeButtonChange={setTimeButton}
|
||||
initialDatePickerValue={datePickerValue}
|
||||
onTimeRangeChange={handleTimeRangeChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateSelectFilter
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Button, Card } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { $t } from '@common/locales'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
|
||||
/** 服务指标 */
|
||||
type IndicatorType = {
|
||||
title: string
|
||||
link?: string
|
||||
content: string | React.ReactNode
|
||||
}
|
||||
const Indicator = ({ indicatorInfo }: { indicatorInfo: any }) => {
|
||||
/** 服务指标 */
|
||||
const [indicatorList, setIndicator] = useState<IndicatorType[]>([])
|
||||
/** 路由跳转 */
|
||||
const navigateTo = useNavigate()
|
||||
|
||||
/** 设置服务指标 */
|
||||
const setIndicatorList = () => {
|
||||
setIndicator([
|
||||
{
|
||||
title: indicatorInfo?.enableMcp ? 'APIs / Tools' : 'APIs',
|
||||
link: `/serviceHub/detail/${indicatorInfo?.serviceId}`,
|
||||
content: indicatorInfo?.apiNum ?? 0
|
||||
},
|
||||
{
|
||||
title: $t('订阅数量'),
|
||||
link: `/consumer/list/${indicatorInfo?.teamId}`,
|
||||
content: indicatorInfo?.subscriberNum ?? 0
|
||||
},
|
||||
{
|
||||
title: 'MCP',
|
||||
content: (
|
||||
<>
|
||||
{/* green */}
|
||||
<Button
|
||||
color={indicatorInfo?.enableMcp ? 'green' : 'primary'}
|
||||
className="w-full rounded-[10px]"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
if (indicatorInfo?.enableMcp) {
|
||||
window.open(`/serviceHub/detail/${indicatorInfo?.serviceId}`, '_blank')
|
||||
} else {
|
||||
navigateTo(`/service/${indicatorInfo?.teamId}/aiInside/${indicatorInfo?.serviceId}/setting`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{indicatorInfo?.enableMcp ? $t('已开启') : $t('开启 MCP')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!indicatorInfo) return
|
||||
setIndicatorList()
|
||||
}, [indicatorInfo])
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{indicatorList.map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 cursor-pointer shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.02] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{
|
||||
body: 'p-[15px]'
|
||||
}}
|
||||
onClick={() => {
|
||||
window.open(item.link)
|
||||
}}
|
||||
>
|
||||
<div className="text-[14px] font-semibold text-gray-400 mb-[15px]">
|
||||
{item.title}
|
||||
{item.link && <Icon icon="uiw:right" width="16" height="16" className="absolute top-[14px] right-[14px]" />}
|
||||
</div>
|
||||
<div className={`${index < 2 ? 'text-[40px] font-semibold' : 'block mt-[30px]'}`}>{item.content}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Indicator
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useMemo, useRef, useEffect } from 'react'
|
||||
import PageList from '@common/components/aoplatform/PageList'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Card } from 'antd'
|
||||
import { AI_SERVICE_TOP_RANKING_LIST, REST_SERVICE_TOP_RANKING_LIST } from '@core/const/system/const'
|
||||
|
||||
interface RankingListData {
|
||||
[key: string]: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
request: number;
|
||||
token?: number;
|
||||
traffic?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface PageListRef {
|
||||
reload: () => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const RankingList = ({ topRankingList, serviceType }: { topRankingList: RankingListData; serviceType: 'aiService' | 'restService' }) => {
|
||||
/** 全局状态 */
|
||||
const { state } = useGlobalContext()
|
||||
/** 表格 ref */
|
||||
const tableRefs = useRef<{ [key: string]: PageListRef | null }>({});
|
||||
/** 列 */
|
||||
const columns = useMemo(() => {
|
||||
return [...(serviceType === 'aiService' ? AI_SERVICE_TOP_RANKING_LIST : REST_SERVICE_TOP_RANKING_LIST)].map((x) => {
|
||||
return {
|
||||
...x,
|
||||
title: typeof x.title === 'string' ? $t(x.title as string) : x.title
|
||||
}
|
||||
})
|
||||
}, [serviceType, state.language])
|
||||
|
||||
/** 监听 serviceType 变化,刷新所有表格 */
|
||||
useEffect(() => {
|
||||
// 重新加载所有表格数据
|
||||
if (Object.keys(tableRefs.current).length > 0) {
|
||||
Object.values(tableRefs.current).forEach(ref => {
|
||||
// 如果组件实例存在并且有reload方法
|
||||
if (ref && typeof ref.reload === 'function') {
|
||||
ref.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [serviceType, topRankingList])
|
||||
|
||||
/**
|
||||
* 获取表格数据
|
||||
* @param item
|
||||
* @returns
|
||||
*/
|
||||
const getTableData = (item: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({ data: topRankingList[item], success: true })
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="flex w-full pb-[10px]">
|
||||
{Object.keys(topRankingList)?.map((item: any, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 h-fit cursor-pointer rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{
|
||||
body: 'p-[15px] pb-[0px]'
|
||||
}}
|
||||
>
|
||||
<div className="mb-[10px]">
|
||||
<span className="text-[14px] text-[#999] font-medium">{item === 'TOP API' ? $t('API 使用排名') : $t('消费者使用排名')}</span>
|
||||
</div>
|
||||
<PageList
|
||||
id={item}
|
||||
columns={[...columns]}
|
||||
minVirtualHeight={430}
|
||||
request={() => getTableData(item)}
|
||||
showPagination={false}
|
||||
tableClass="ranking-list"
|
||||
ref={ref => {
|
||||
if (ref) tableRefs.current[item] = ref;
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RankingList
|
||||
@@ -0,0 +1,541 @@
|
||||
import { Card, Spin } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import Indicator from './indicator/Indicator'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import DateSelectFilter, { TimeOption } from './filter/DateSelectFilter'
|
||||
import { TimeRange } from '@common/components/aoplatform/TimeRangeSelector'
|
||||
import ServiceBarChar, { BarChartInfo } from './charts/ServiceBarChar'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { App } from 'antd'
|
||||
import ServiceAreaChart from './charts/ServiceAreaChart'
|
||||
import RankingList from './rankingList/RankingList'
|
||||
import { getTime } from '@dashboard/utils/dashboard'
|
||||
import { setBarChartInfoData } from './utils'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
|
||||
const ServiceOverview = ({ serviceType }: { serviceType: 'aiService' | 'restService' }) => {
|
||||
/** 路由参数 */
|
||||
const { serviceId, teamId } = useParams<{ serviceId: string; teamId: string }>()
|
||||
/** 面板 loading */
|
||||
const [dashboardLoading, setDashboardLoading] = useState(true)
|
||||
/** 默认时间 */
|
||||
const [defaultTime] = useState<TimeOption>('sevenDays')
|
||||
/** 当前选中的时间范围 */
|
||||
const [timeRange, setTimeRange] = useState<TimeRange | undefined>()
|
||||
/** 总数数据 */
|
||||
const [barChartInfo, setBarChartInfo] = useState<any>()
|
||||
/** 平均值数据 */
|
||||
const [perBarChartInfo, setPerBarChartInfo] = useState<any>()
|
||||
/** 指标数据 */
|
||||
const [indicatorInfo, setIndicatorInfo] = useState<any>([])
|
||||
/** 排名表格数据 */
|
||||
const [topRankingList, setTopRankingList] = useState<any>([])
|
||||
/** 获取服务信息 */
|
||||
const { fetchData } = useFetch()
|
||||
/** 弹窗组件 */
|
||||
const { message } = App.useApp()
|
||||
/** 全局状态 */
|
||||
const { state } = useGlobalContext()
|
||||
/** AI 服务数据 */
|
||||
const [aiServiceOverview, setAiServiceOverview] = useState<any>()
|
||||
/** REST 服务数据 */
|
||||
const [restServiceOverview, setRestServiceOverview] = useState<any>()
|
||||
/** 时间选择回调 */
|
||||
const selectCallback = (date: TimeRange) => {
|
||||
setTimeRange(date)
|
||||
}
|
||||
|
||||
/** 获取 AI 服务信息 */
|
||||
const getAIServiceOverview = () => {
|
||||
fetchData<BasicResponse<{ overview: any }>>('service/overview/monitor/ai', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
|
||||
eoTransformKeys: [
|
||||
'enable_mcp',
|
||||
'subscriber_num',
|
||||
'api_num',
|
||||
'service_kind',
|
||||
'avaliable_monitor',
|
||||
'request_overview',
|
||||
'token_overview',
|
||||
'avg_token_overview',
|
||||
'avg_request_per_subscriber_overview',
|
||||
'avg_token_per_subscriber_overview',
|
||||
'request_total',
|
||||
'token_total',
|
||||
'avg_token',
|
||||
'avg_request_per_subscriber',
|
||||
'avg_token_per_subscriber'
|
||||
],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const serviceOverview = {
|
||||
enableMcp: true,
|
||||
subscriberNum: 11,
|
||||
apiNum: 3,
|
||||
serviceKind: 'ai',
|
||||
avaliableMonitor: false,
|
||||
requestOverview: [
|
||||
{
|
||||
'2xx': 1.0,
|
||||
'4xx': 2.0,
|
||||
'5xx': 3.0,
|
||||
fsdf: 4.0
|
||||
},
|
||||
{
|
||||
'2xx': 2.0,
|
||||
'4xx': 3.0,
|
||||
'5xx': 4.0,
|
||||
fsdf: 5.0
|
||||
},
|
||||
{
|
||||
'2xx': 3.0,
|
||||
'4xx': 4.0,
|
||||
'5xx': 5.0,
|
||||
fsdf: 6.0
|
||||
},
|
||||
{
|
||||
'2xx': 4.0,
|
||||
'4xx': 5.0,
|
||||
'5xx': 6.0,
|
||||
fsdf: 7.0
|
||||
},
|
||||
{
|
||||
'2xx': 5.0,
|
||||
'4xx': 6.0,
|
||||
'5xx': 7.0,
|
||||
fsdf: 8.0
|
||||
},
|
||||
{
|
||||
'2xx': 6.0,
|
||||
'4xx': 7.0,
|
||||
'5xx': 8.0,
|
||||
fsdf: 9.0
|
||||
}
|
||||
],
|
||||
tokenOverview: [
|
||||
{
|
||||
'2xx': 1.0,
|
||||
'4xx': 2.0,
|
||||
'5xx': 3.0
|
||||
},
|
||||
{
|
||||
'2xx': 2.0,
|
||||
'4xx': 3.0,
|
||||
'5xx': 4.0
|
||||
},
|
||||
{
|
||||
'2xx': 3.0,
|
||||
'4xx': 4.0,
|
||||
'5xx': 5.0
|
||||
},
|
||||
{
|
||||
'2xx': 4.0,
|
||||
'4xx': 5.0,
|
||||
'5xx': 6.0
|
||||
},
|
||||
{
|
||||
'2xx': 5.0,
|
||||
'4xx': 6.0,
|
||||
'5xx': 7.0
|
||||
},
|
||||
{
|
||||
'2xx': 6.0,
|
||||
'4xx': 7.0,
|
||||
'5xx': 8.0
|
||||
}
|
||||
],
|
||||
avgTokenOverview: [11, 231, 343, 1414, 25, 362],
|
||||
avgRequestPerSubscriberOverview: [1, 2, 3, 4, 5, 6],
|
||||
avgTokenPerSubscriberOverview: [4, 5, 1, 11, 4, 9],
|
||||
requestTotal: '12 GB',
|
||||
tokenTotal: '14 GB',
|
||||
avgToken: '1 k',
|
||||
avgRequestPerSubscriber: '2 k',
|
||||
avgTokenPerSubscriber: '3 k',
|
||||
date: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
}
|
||||
// 存储 AI 服务数据
|
||||
setAiServiceOverview(serviceOverview)
|
||||
// 设置 AI 报表数据
|
||||
setAiChartInfoData(serviceOverview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 REST 服务数据
|
||||
* */
|
||||
const setRestChartInfoData = (serviceOverview: any) => {
|
||||
// 设置指标数据
|
||||
setIndicatorInfo({
|
||||
apiNum: serviceOverview.apiNum,
|
||||
subscriberNum: serviceOverview.subscriberNum,
|
||||
teamId: teamId,
|
||||
enableMcp: serviceOverview.enableMcp,
|
||||
serviceId: serviceId
|
||||
})
|
||||
// 设置总数数据
|
||||
setBarChartInfo([
|
||||
// 服务请求次数
|
||||
setBarChartInfoData({
|
||||
title: $t('请求数'),
|
||||
data: serviceOverview.requestOverview,
|
||||
value: serviceOverview.requestTotal,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
// 流量消耗总数
|
||||
setBarChartInfoData({
|
||||
title: $t('流量'),
|
||||
data: serviceOverview.trafficOverview,
|
||||
value: serviceOverview.trafficTotal,
|
||||
date: serviceOverview.date
|
||||
})
|
||||
])
|
||||
// 设置平均值数据
|
||||
setPerBarChartInfo([
|
||||
// 各个模型使用量
|
||||
{
|
||||
title: $t('平均响应时间'),
|
||||
data: serviceOverview.avgResponseTimeOverview,
|
||||
value: serviceOverview.avgResponseTime,
|
||||
date: serviceOverview.date,
|
||||
type: 'area'
|
||||
},
|
||||
// 平均请求
|
||||
setBarChartInfoData({
|
||||
title: $t('平均请求数'),
|
||||
data: serviceOverview.avgRequestPerSubscriberOverview,
|
||||
value: serviceOverview.avgRequestPerSubscriber,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
// 平均流量消耗
|
||||
setBarChartInfoData({
|
||||
title: $t('平均流量'),
|
||||
data: serviceOverview.avgTrafficPerSubscriberOverview,
|
||||
value: serviceOverview.avgTrafficPerSubscriber,
|
||||
date: serviceOverview.date
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 AI 服务数据
|
||||
* */
|
||||
const setAiChartInfoData = (serviceOverview: any) => {
|
||||
// 设置指标数据
|
||||
setIndicatorInfo({
|
||||
apiNum: serviceOverview.apiNum,
|
||||
subscriberNum: serviceOverview.subscriberNum,
|
||||
teamId: teamId,
|
||||
enableMcp: serviceOverview.enableMcp,
|
||||
serviceId: serviceId
|
||||
})
|
||||
// 设置总数数据
|
||||
setBarChartInfo([
|
||||
// 服务请求次数
|
||||
setBarChartInfoData({
|
||||
title: $t('请求数'),
|
||||
data: serviceOverview.requestOverview,
|
||||
value: serviceOverview.requestTotal,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
// token 消耗总数
|
||||
setBarChartInfoData({
|
||||
title: $t('Token'),
|
||||
data: serviceOverview.tokenOverview,
|
||||
value: serviceOverview.tokenTotal,
|
||||
date: serviceOverview.date
|
||||
})
|
||||
])
|
||||
// 设置平均值数据
|
||||
setPerBarChartInfo([
|
||||
// 平均 token 消耗
|
||||
{
|
||||
title: $t('平均 Token/s 统计'),
|
||||
data: serviceOverview.avgTokenOverview,
|
||||
value: serviceOverview.avgToken,
|
||||
date: serviceOverview.date,
|
||||
type: 'area'
|
||||
},
|
||||
// 平均请求
|
||||
setBarChartInfoData({
|
||||
title: $t('平均请求数'),
|
||||
data: serviceOverview.avgRequestPerSubscriberOverview,
|
||||
value: serviceOverview.avgRequestPerSubscriber,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
// 评价 token 消耗
|
||||
setBarChartInfoData({
|
||||
title: $t('平均 Token/订阅者统计'),
|
||||
data: serviceOverview.avgTokenPerSubscriberOverview,
|
||||
value: serviceOverview.avgTokenPerSubscriber,
|
||||
date: serviceOverview.date
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
/** 获取 REST 服务信息 */
|
||||
const getRestServiceOverview = () => {
|
||||
fetchData<BasicResponse<{ overview: any }>>('service/overview/monitor/rest', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
|
||||
eoTransformKeys: [
|
||||
'enable_mcp',
|
||||
'subscriber_num',
|
||||
'api_num',
|
||||
'service_kind',
|
||||
'avaliable_monitor',
|
||||
'request_overview',
|
||||
'traffic_overview',
|
||||
'avg_request_per_subscriber_overview',
|
||||
'avg_response_time_overview',
|
||||
'avg_traffic_per_subscriber_overview',
|
||||
'request_total',
|
||||
'traffic_total',
|
||||
'avg_response_time',
|
||||
'avg_request_per_subscriber',
|
||||
'avg_traffic_per_subscriber'
|
||||
],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const serviceOverview = {
|
||||
enableMcp: true,
|
||||
subscriberNum: 12,
|
||||
apiNum: 3,
|
||||
serviceKind: 'ai',
|
||||
avaliableMonitor: false,
|
||||
requestOverview: [
|
||||
{
|
||||
'2xx': 1.0,
|
||||
'4xx': 2.0,
|
||||
'5xx': 3.0,
|
||||
fsdf: 4.0
|
||||
},
|
||||
{
|
||||
'2xx': 2.0,
|
||||
'4xx': 3.0,
|
||||
'5xx': 4.0,
|
||||
fsdf: 5.0
|
||||
},
|
||||
{
|
||||
'2xx': 3.0,
|
||||
'4xx': 4.0,
|
||||
'5xx': 5.0,
|
||||
fsdf: 6.0
|
||||
},
|
||||
{
|
||||
'2xx': 4.0,
|
||||
'4xx': 5.0,
|
||||
'5xx': 6.0,
|
||||
fsdf: 7.0
|
||||
},
|
||||
{
|
||||
'2xx': 5.0,
|
||||
'4xx': 6.0,
|
||||
'5xx': 7.0,
|
||||
fsdf: 8.0
|
||||
},
|
||||
{
|
||||
'2xx': 6.0,
|
||||
'4xx': 7.0,
|
||||
'5xx': 8.0,
|
||||
fsdf: 9.0
|
||||
}
|
||||
],
|
||||
trafficOverview: [
|
||||
{
|
||||
'2xx': 1.0,
|
||||
'4xx': 2.0,
|
||||
'5xx': 3.0
|
||||
},
|
||||
{
|
||||
'2xx': 2.0,
|
||||
'4xx': 3.0,
|
||||
'5xx': 4.0
|
||||
},
|
||||
{
|
||||
'2xx': 3.0,
|
||||
'4xx': 4.0,
|
||||
'5xx': 5.0
|
||||
},
|
||||
{
|
||||
'2xx': 4.0,
|
||||
'4xx': 5.0,
|
||||
'5xx': 6.0
|
||||
},
|
||||
{
|
||||
'2xx': 5.0,
|
||||
'4xx': 6.0,
|
||||
'5xx': 7.0
|
||||
},
|
||||
{
|
||||
'2xx': 6.0,
|
||||
'4xx': 7.0,
|
||||
'5xx': 8.0
|
||||
}
|
||||
],
|
||||
avgRequestPerSubscriberOverview: [1, 2, 3, 4, 5, 6],
|
||||
avgResponseTimeOverview: [11, 231, 343, 1414, 25, 362],
|
||||
avgTrafficPerSubscriberOverview: [4, 5, 1, 11, 4, 9],
|
||||
requestTotal: '12 GB',
|
||||
trafficTotal: '14 GB',
|
||||
avgResponseTime: '1 k',
|
||||
avgRequestPerSubscriber: '2 k',
|
||||
avgTrafficPerSubscriber: '3 k',
|
||||
date: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
}
|
||||
// 存储 REST 服务数据
|
||||
setRestServiceOverview(serviceOverview)
|
||||
// 设置 REST 报表数据
|
||||
setRestChartInfoData(serviceOverview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取排名列表 */
|
||||
const getTopRankingList = () => {
|
||||
fetchData<BasicResponse<{ overview: any }>>('service/monitor/top10', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const serviceOverview = {
|
||||
apis: [
|
||||
{
|
||||
id: '123',
|
||||
name: 'Model 1',
|
||||
request: 100,
|
||||
traffic: 100,
|
||||
token: 100
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
name: 'Model 2',
|
||||
request: 200,
|
||||
traffic: 300,
|
||||
token: 400
|
||||
}
|
||||
],
|
||||
consumers: [
|
||||
{
|
||||
id: '6666',
|
||||
name: 'Customer 1',
|
||||
request: 100,
|
||||
traffic: 100,
|
||||
token: 100
|
||||
}
|
||||
]
|
||||
}
|
||||
// 设置排名表格数据
|
||||
setTopRankingList({
|
||||
'TOP API': serviceOverview.apis,
|
||||
'TOP Consumer': serviceOverview.consumers
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { startTime, endTime } = getTime(defaultTime, [])
|
||||
setTimeRange({
|
||||
start: startTime,
|
||||
end: endTime
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (timeRange) {
|
||||
serviceType === 'aiService' ? getAIServiceOverview() : getRestServiceOverview()
|
||||
getTopRankingList()
|
||||
}
|
||||
}, [timeRange])
|
||||
|
||||
useEffect(() => {
|
||||
if (serviceType === 'aiService') {
|
||||
aiServiceOverview && setAiChartInfoData(aiServiceOverview)
|
||||
} else {
|
||||
restServiceOverview && setRestChartInfoData(restServiceOverview)
|
||||
}
|
||||
}, [state.language])
|
||||
|
||||
return (
|
||||
<Spin
|
||||
className="h-full pb-[20px]"
|
||||
wrapperClassName="h-full min-h-[150px]"
|
||||
indicator={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ transform: 'scale(1.5)' }}>
|
||||
<LoadingOutlined style={{ fontSize: 30 }} spin />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
spinning={dashboardLoading}
|
||||
>
|
||||
<div className="mr-PAGE_INSIDE_X">
|
||||
<Indicator indicatorInfo={indicatorInfo} />
|
||||
<div className="mt-[20px]">
|
||||
<DateSelectFilter selectCallback={selectCallback} defaultTime={defaultTime} />
|
||||
</div>
|
||||
<div className="mt-[20px] flex mb-[10px]">
|
||||
{barChartInfo?.map((item: BarChartInfo, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 cursor-pointer rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{
|
||||
body: 'p-[15px]'
|
||||
}}
|
||||
>
|
||||
<ServiceBarChar key={index} height={400} dataInfo={item} customClassNames="flex-1"></ServiceBarChar>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex mb-[10px]">
|
||||
{perBarChartInfo?.map((item: any, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 cursor-pointer rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{
|
||||
body: 'p-[15px]'
|
||||
}}
|
||||
>
|
||||
{item.type === 'area' ? (
|
||||
<>
|
||||
<ServiceAreaChart
|
||||
key={index}
|
||||
height={250}
|
||||
dataInfo={item}
|
||||
customClassNames="flex-1 relative"
|
||||
></ServiceAreaChart>
|
||||
</>
|
||||
) : (
|
||||
<ServiceBarChar key={index} height={250} dataInfo={item} customClassNames="flex-1"></ServiceBarChar>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<RankingList topRankingList={topRankingList} serviceType={serviceType} />
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceOverview
|
||||
@@ -0,0 +1,58 @@
|
||||
export type BarData = {
|
||||
title: string
|
||||
value: string
|
||||
date: string[]
|
||||
data: any[]
|
||||
}
|
||||
export const setBarChartInfoData = ({ title, value, data, date }: BarData) => {
|
||||
// 首先获取所有的键名(假设所有对象的键名都一样)
|
||||
if (data.length === 0) {
|
||||
return {
|
||||
title,
|
||||
value,
|
||||
date,
|
||||
data: []
|
||||
}
|
||||
}
|
||||
if (typeof data[0] !== 'object') {
|
||||
return {
|
||||
title,
|
||||
value,
|
||||
date,
|
||||
data
|
||||
}
|
||||
}
|
||||
// 从第一个对象中获取所有键名
|
||||
const keys = Object.keys(data[0])
|
||||
// 定义颜色映射
|
||||
const colorMap: Record<string, string> = {
|
||||
'2xx': '#7EC26A',
|
||||
'4xx': '#F2CF59',
|
||||
'5xx': '#F17975',
|
||||
'200': '#7EC26A',
|
||||
'400': '#F2CF59',
|
||||
'500': '#F17975'
|
||||
}
|
||||
|
||||
// 为每个键创建一个数据集
|
||||
const transformedData = keys.map((key, index) => {
|
||||
// 为没有映射颜色的键生成随机颜色
|
||||
const color =
|
||||
colorMap[key] ||
|
||||
`#${Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0')}`
|
||||
|
||||
return {
|
||||
name: key,
|
||||
color: color,
|
||||
value: data.map((item) => item[key])
|
||||
}
|
||||
})
|
||||
return {
|
||||
title,
|
||||
value,
|
||||
date,
|
||||
data: transformedData
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { SystemConfigFieldType } from '../../const/system/type.ts'
|
||||
import { useSystemContext } from '../../contexts/SystemContext.tsx'
|
||||
import ServiceInfoCard from '@common/components/aoplatform/serviceInfoCard.tsx'
|
||||
|
||||
const SystemInsidePage: FC = () => {
|
||||
const { message } = App.useApp()
|
||||
@@ -31,7 +32,7 @@ const SystemInsidePage: FC = () => {
|
||||
fetchData<BasicResponse<{ service: SystemConfigFieldType }>>('service/info', {
|
||||
method: 'GET',
|
||||
eoParams: { team: teamId, service: serviceId }
|
||||
}).then(response => {
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setSystemInfo(data.service)
|
||||
@@ -47,7 +48,7 @@ const SystemInsidePage: FC = () => {
|
||||
fetchData<BasicResponse<{ prefix: string; force: boolean }>>('service/router/define', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId }
|
||||
}).then(response => {
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setApiPrefix(data.prefix)
|
||||
@@ -65,6 +66,7 @@ const SystemInsidePage: FC = () => {
|
||||
'assets',
|
||||
null,
|
||||
[
|
||||
getItem(<Link to="./overview">{$t('总览')}</Link>, 'overview', undefined, undefined, undefined, ''),
|
||||
getItem(
|
||||
<Link to="./route">{$t('API 路由')}</Link>,
|
||||
'route',
|
||||
@@ -146,9 +148,10 @@ const SystemInsidePage: FC = () => {
|
||||
null,
|
||||
[
|
||||
// APP_MODE === 'pro' ? getItem(<Link to="./topology">{$t('调用拓扑图')}</Link>, 'topology',undefined,undefined,undefined,'project.mySystem.topology.view'):null,
|
||||
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting', undefined, undefined, undefined, ''),
|
||||
getItem(
|
||||
<Link to="./setting">{$t('设置')}</Link>,
|
||||
'setting',
|
||||
<Link to="./logs">{$t('日志')}</Link>,
|
||||
'logs',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -166,12 +169,11 @@ const SystemInsidePage: FC = () => {
|
||||
const newMenu = cloneDeep(menu)
|
||||
return newMenu!.filter((m: MenuItemGroupType) => {
|
||||
if (m && m.children && m.children.length > 0) {
|
||||
m.children = m.children.filter(c => {
|
||||
m.children = m.children.filter((c) => {
|
||||
if (!c) return false
|
||||
return (c as MenuItemType & { access: string }).access
|
||||
? checkPermission(
|
||||
(c as MenuItemType & { access: string })
|
||||
.access as keyof (typeof PERMISSION_DEFINITION)[0]
|
||||
(c as MenuItemType & { access: string }).access as keyof (typeof PERMISSION_DEFINITION)[0]
|
||||
)
|
||||
: true
|
||||
})
|
||||
@@ -180,12 +182,8 @@ const SystemInsidePage: FC = () => {
|
||||
})
|
||||
}
|
||||
const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType<MenuItemType>[])
|
||||
const menu =
|
||||
(activeMenu ?? filteredMenu[0]?.children)
|
||||
? filteredMenu[0]?.children?.[0]?.key
|
||||
: filteredMenu[0]?.key
|
||||
if (menu && currentUrl.split('/')[-1] !== menu)
|
||||
navigateTo(`/service/${teamId}/inside/${serviceId}/${menu}`)
|
||||
const menu = (activeMenu ?? filteredMenu[0]?.children) ? filteredMenu[0]?.children?.[0]?.key : filteredMenu[0]?.key
|
||||
if (menu && currentUrl.split('/')[-1] !== menu) navigateTo(`/service/${teamId}/inside/${serviceId}/${menu}`)
|
||||
return filteredMenu || []
|
||||
}, [accessData, accessInit, SYSTEM_PAGE_MENU_ITEMS])
|
||||
|
||||
@@ -208,7 +206,7 @@ const SystemInsidePage: FC = () => {
|
||||
} else if (serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]) {
|
||||
setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1])
|
||||
} else {
|
||||
setActiveMenu('route')
|
||||
setActiveMenu('overview')
|
||||
}
|
||||
}, [currentUrl])
|
||||
|
||||
@@ -233,16 +231,7 @@ const SystemInsidePage: FC = () => {
|
||||
{showMenu ? (
|
||||
<InsidePage
|
||||
pageTitle={systemInfo?.name || '-'}
|
||||
tagList={[
|
||||
...(systemInfo?.enable_mcp ? [{ label: 'MCP', color: '#FFF0C1', className: 'text-[#000]' }] : []),
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
{$t('服务 ID')}:{serviceId || '-'}
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
]}
|
||||
customBanner={<ServiceInfoCard serviceId={serviceId} teamId={teamId} />}
|
||||
backUrl="/service/list"
|
||||
>
|
||||
<div className="flex flex-1 h-full">
|
||||
|
||||
@@ -1,89 +1,578 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import MonitorTotalPage from '@dashboard/component/MonitorTotalPage'
|
||||
import { BasicResponse } from '@common/const/const'
|
||||
import {
|
||||
InvokeData,
|
||||
MessageData,
|
||||
MonitorApiData,
|
||||
MonitorSubscriberData,
|
||||
PieData,
|
||||
SearchBody
|
||||
} from '@dashboard/const/type'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { objectToSearchParameters } from '@common/utils/router'
|
||||
import ScrollableSection from '@common/components/aoplatform/ScrollableSection'
|
||||
import { TimeRange } from '@common/components/aoplatform/TimeRangeSelector'
|
||||
import { useEffect, useState } from 'react'
|
||||
import DateSelectFilter, { TimeOption } from '@core/pages/serviceOverview/filter/DateSelectFilter'
|
||||
import { getTime } from '@dashboard/utils/dashboard'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { Card, Spin } from 'antd'
|
||||
import ServiceBarChar, { BarChartInfo } from '@core/pages/serviceOverview/charts/ServiceBarChar'
|
||||
import ServiceAreaChart from '@core/pages/serviceOverview/charts/ServiceAreaChart'
|
||||
import RankingList from '@core/pages/serviceOverview/rankingList/RankingList'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { setBarChartInfoData } from '@core/pages/serviceOverview/utils'
|
||||
import { App } from 'antd'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
|
||||
export default function DashboardTotal() {
|
||||
/** 获取数据 */
|
||||
const { fetchData } = useFetch()
|
||||
const navigateTo = useNavigate()
|
||||
const fetchPieData: (body: SearchBody) => Promise<BasicResponse<PieData>> = (body: SearchBody) =>
|
||||
fetchData<BasicResponse<PieData>>('monitor/overview/summary', {
|
||||
method: 'POST',
|
||||
eoBody: body,
|
||||
eoTransformKeys: ['request_summary', 'proxy_summary']
|
||||
})
|
||||
|
||||
const fetchInvokeData: (body: SearchBody) => Promise<BasicResponse<InvokeData>> = (body: SearchBody) =>
|
||||
fetchData<BasicResponse<InvokeData>>('monitor/overview/invoke', {
|
||||
method: 'POST',
|
||||
eoBody: body,
|
||||
eoTransformKeys: ['request_total', 'request_rate', 'proxy_total', 'proxy_rate', 'time_interval']
|
||||
})
|
||||
|
||||
const fetchMessageData: (body: SearchBody) => Promise<BasicResponse<MessageData>> = (body: SearchBody) =>
|
||||
fetchData<BasicResponse<MessageData>>('monitor/overview/message', {
|
||||
method: 'POST',
|
||||
eoBody: body,
|
||||
eoTransformKeys: ['time_interval', 'request_message', 'response_message']
|
||||
})
|
||||
|
||||
const fetchTableData: (
|
||||
body: SearchBody,
|
||||
type: 'api' | 'subscribers' | 'providers'
|
||||
) => Promise<BasicResponse<{ top10: MonitorApiData[] | MonitorSubscriberData[] }>> = (
|
||||
body: SearchBody,
|
||||
type: 'api' | 'subscribers' | 'providers'
|
||||
) =>
|
||||
fetchData<BasicResponse<{ api: MonitorApiData[]; subscribers: MonitorSubscriberData }>>('monitor/overview/top10', {
|
||||
method: 'POST',
|
||||
eoBody: { ...body, dataType: type },
|
||||
eoTransformKeys: [
|
||||
'dataType',
|
||||
'request_total',
|
||||
'request_success',
|
||||
'request_rate',
|
||||
'proxy_total',
|
||||
'proxy_success',
|
||||
'proxy_rate',
|
||||
'status_fail',
|
||||
'avg_resp',
|
||||
'max_resp',
|
||||
'min_resp',
|
||||
'avg_traffic',
|
||||
'max_traffic',
|
||||
'min_traffic',
|
||||
'min_traffic',
|
||||
'is_red'
|
||||
]
|
||||
})
|
||||
|
||||
const goToDetail: (body: SearchBody, val: MonitorApiData | MonitorSubscriberData, type: string) => void = (
|
||||
body: SearchBody,
|
||||
val: MonitorApiData | MonitorSubscriberData,
|
||||
type: string
|
||||
) => {
|
||||
// ...跳转到详情页...
|
||||
const { start: startTime, end: endTime, clusters } = body
|
||||
navigateTo(
|
||||
`/analytics/${type}/list?${objectToSearchParameters({ id: val.id, clusters: clusters || undefined, start: startTime?.toString(), end: endTime?.toString(), name: val.name }).toString()}`
|
||||
)
|
||||
/** 默认时间 */
|
||||
const [defaultTime] = useState<TimeOption>('sevenDays')
|
||||
/** 当前选中的时间范围 */
|
||||
const [timeRange, setTimeRange] = useState<TimeRange | undefined>()
|
||||
/** 当前激活的标签 */
|
||||
const [activeTab, setActiveTab] = useState('REST')
|
||||
/** 面板 loading */
|
||||
const [dashboardLoading, setDashboardLoading] = useState(false)
|
||||
/** 总数数据 */
|
||||
const [barChartInfo, setBarChartInfo] = useState<any>()
|
||||
/** 平均值数据 */
|
||||
const [perBarChartInfo, setPerBarChartInfo] = useState<any>()
|
||||
/** 排名表格数据 */
|
||||
const [topRankingList, setTopRankingList] = useState<any>([])
|
||||
/** 弹窗组件 */
|
||||
const { message } = App.useApp()
|
||||
/** 全局状态 */
|
||||
const { state } = useGlobalContext()
|
||||
/** AI 服务数据 */
|
||||
const [aiServiceOverview, setAiServiceOverview] = useState<any>()
|
||||
/** REST 服务数据 */
|
||||
const [restServiceOverview, setRestServiceOverview] = useState<any>()
|
||||
/** 时间选择回调 */
|
||||
const selectCallback = (date: TimeRange) => {
|
||||
setTimeRange(date)
|
||||
}
|
||||
|
||||
/** 获取 AI 服务信息 */
|
||||
const getAIServiceOverview = () => {
|
||||
return fetchData<BasicResponse<{ overview: any }>>('monitor/overview/chart/ai', {
|
||||
method: 'GET',
|
||||
eoParams: { start: timeRange?.start, end: timeRange?.end },
|
||||
eoTransformKeys: [
|
||||
'request_overview',
|
||||
'token_overview',
|
||||
'avg_token_overview',
|
||||
'avg_request_per_subscriber_overview',
|
||||
'avg_token_per_subscriber_overview',
|
||||
'request_total',
|
||||
'token_total',
|
||||
'avg_token',
|
||||
'avg_request_per_subscriber',
|
||||
'avg_token_per_subscriber'
|
||||
],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const serviceOverview = {
|
||||
requestOverview: [
|
||||
{
|
||||
'2xx': 1.0,
|
||||
'4xx': 2.0,
|
||||
'5xx': 3.0,
|
||||
fsdf: 4.0
|
||||
},
|
||||
{
|
||||
'2xx': 2.0,
|
||||
'4xx': 3.0,
|
||||
'5xx': 4.0,
|
||||
fsdf: 5.0
|
||||
},
|
||||
{
|
||||
'2xx': 3.0,
|
||||
'4xx': 4.0,
|
||||
'5xx': 5.0,
|
||||
fsdf: 6.0
|
||||
},
|
||||
{
|
||||
'2xx': 4.0,
|
||||
'4xx': 5.0,
|
||||
'5xx': 6.0,
|
||||
fsdf: 7.0
|
||||
},
|
||||
{
|
||||
'2xx': 5.0,
|
||||
'4xx': 6.0,
|
||||
'5xx': 7.0,
|
||||
fsdf: 8.0
|
||||
},
|
||||
{
|
||||
'2xx': 6.0,
|
||||
'4xx': 7.0,
|
||||
'5xx': 8.0,
|
||||
fsdf: 9.0
|
||||
}
|
||||
],
|
||||
tokenOverview: [
|
||||
{
|
||||
'2xx': 1.0,
|
||||
'4xx': 2.0,
|
||||
'5xx': 3.0
|
||||
},
|
||||
{
|
||||
'2xx': 2.0,
|
||||
'4xx': 3.0,
|
||||
'5xx': 4.0
|
||||
},
|
||||
{
|
||||
'2xx': 3.0,
|
||||
'4xx': 4.0,
|
||||
'5xx': 5.0
|
||||
},
|
||||
{
|
||||
'2xx': 4.0,
|
||||
'4xx': 5.0,
|
||||
'5xx': 6.0
|
||||
},
|
||||
{
|
||||
'2xx': 5.0,
|
||||
'4xx': 6.0,
|
||||
'5xx': 7.0
|
||||
},
|
||||
{
|
||||
'2xx': 6.0,
|
||||
'4xx': 7.0,
|
||||
'5xx': 8.0
|
||||
}
|
||||
],
|
||||
avgTokenOverview: [11, 231, 343, 1414, 25, 362],
|
||||
avgRequestPerSubscriberOverview: [1, 2, 3, 4, 5, 6],
|
||||
avgTokenPerSubscriberOverview: [4, 5, 1, 11, 4, 9],
|
||||
requestTotal: '12 GB',
|
||||
tokenTotal: '14 GB',
|
||||
avgToken: '1 k',
|
||||
avgRequestPerSubscriber: '2 k',
|
||||
avgTokenPerSubscriber: '3 k',
|
||||
date: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
}
|
||||
// 存储 AI 服务数据
|
||||
setAiServiceOverview(serviceOverview)
|
||||
// 设置 AI 报表数据
|
||||
setAiChartInfoData(serviceOverview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取 REST 服务信息 */
|
||||
const getRestServiceOverview = () => {
|
||||
return fetchData<BasicResponse<{ overview: any }>>('monitor/overview/chart/rest', {
|
||||
method: 'GET',
|
||||
eoParams: { start: timeRange?.start, end: timeRange?.end },
|
||||
eoTransformKeys: [
|
||||
'request_overview',
|
||||
'traffic_overview',
|
||||
'avg_request_per_subscriber_overview',
|
||||
'avg_response_time_overview',
|
||||
'avg_traffic_per_subscriber_overview',
|
||||
'request_total',
|
||||
'traffic_total',
|
||||
'avg_response_time',
|
||||
'avg_request_per_subscriber',
|
||||
'avg_traffic_per_subscriber'
|
||||
],
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const serviceOverview = {
|
||||
requestOverview: [
|
||||
{
|
||||
'2xx': 33.0,
|
||||
'4xx': 44.0,
|
||||
'5xx': 5.0,
|
||||
fsdf: 6.0
|
||||
},
|
||||
{
|
||||
'2xx': 123.0,
|
||||
'4xx': 324.0,
|
||||
'5xx': 112.0,
|
||||
fsdf: 44.0
|
||||
},
|
||||
{
|
||||
'2xx': 234.0,
|
||||
'4xx': 436.0,
|
||||
'5xx': 123.0,
|
||||
fsdf: 4.0
|
||||
},
|
||||
{
|
||||
'2xx': 4.0,
|
||||
'4xx': 234.0,
|
||||
'5xx': 1233.0,
|
||||
fsdf: 7.0
|
||||
},
|
||||
{
|
||||
'2xx': 5.0,
|
||||
'4xx': 233.0,
|
||||
'5xx': 7123.0,
|
||||
fsdf: 8.0
|
||||
},
|
||||
{
|
||||
'2xx': 444.0,
|
||||
'4xx': 7.0,
|
||||
'5xx': 8.0,
|
||||
fsdf: 9.0
|
||||
}
|
||||
],
|
||||
trafficOverview: [
|
||||
{
|
||||
'2xx': 1123.0,
|
||||
'4xx': 23.0,
|
||||
'5xx': 3.0
|
||||
},
|
||||
{
|
||||
'2xx': 112.0,
|
||||
'4xx': 233.0,
|
||||
'5xx': 44.0
|
||||
},
|
||||
{
|
||||
'2xx': 3.0,
|
||||
'4xx': 1234.0,
|
||||
'5xx': 445.0
|
||||
},
|
||||
{
|
||||
'2xx': 14.0,
|
||||
'4xx': 2345.0,
|
||||
'5xx': 6.0
|
||||
},
|
||||
{
|
||||
'2xx': 132.0,
|
||||
'4xx': 346.0,
|
||||
'5xx': 37.0
|
||||
},
|
||||
{
|
||||
'2xx': 613.0,
|
||||
'4xx': 47.0,
|
||||
'5xx': 81.0
|
||||
}
|
||||
],
|
||||
avgRequestPerSubscriberOverview: [345, 23, 12, 123, 43, 2],
|
||||
avgResponseTimeOverview: [123, 232, 443, 54, 125, 61],
|
||||
avgTrafficPerSubscriberOverview: [44, 235, 11, 114, 234, 239],
|
||||
requestTotal: '11 GB',
|
||||
trafficTotal: '22 GB',
|
||||
avgResponseTime: '33 k',
|
||||
avgRequestPerSubscriber: '44 k',
|
||||
avgTrafficPerSubscriber: '55 k',
|
||||
date: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
}
|
||||
// 设置 REST 服务数据
|
||||
setRestServiceOverview(serviceOverview)
|
||||
// 存储 REST 报表数据
|
||||
setRestChartInfoData(serviceOverview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 REST 服务数据
|
||||
* */
|
||||
const setRestChartInfoData = (serviceOverview: any) => {
|
||||
// 设置总数数据
|
||||
setBarChartInfo([
|
||||
// 服务请求次数
|
||||
setBarChartInfoData({
|
||||
title: $t('请求数'),
|
||||
data: serviceOverview.requestOverview,
|
||||
value: serviceOverview.requestTotal,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
// 流量消耗总数
|
||||
setBarChartInfoData({
|
||||
title: $t('流量'),
|
||||
data: serviceOverview.trafficOverview,
|
||||
value: serviceOverview.trafficTotal,
|
||||
date: serviceOverview.date
|
||||
})
|
||||
])
|
||||
// 设置平均值数据
|
||||
setPerBarChartInfo([
|
||||
// 各个模型使用量
|
||||
{
|
||||
title: $t('平均响应时间'),
|
||||
data: serviceOverview.avgResponseTimeOverview,
|
||||
value: serviceOverview.avgResponseTime,
|
||||
date: serviceOverview.date,
|
||||
type: 'area'
|
||||
},
|
||||
// 平均请求
|
||||
setBarChartInfoData({
|
||||
title: $t('平均请求数'),
|
||||
data: serviceOverview.avgRequestPerSubscriberOverview,
|
||||
value: serviceOverview.avgRequestPerSubscriber,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
// 平均流量消耗
|
||||
setBarChartInfoData({
|
||||
title: $t('平均流量'),
|
||||
data: serviceOverview.avgTrafficPerSubscriberOverview,
|
||||
value: serviceOverview.avgTrafficPerSubscriber,
|
||||
date: serviceOverview.date
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 AI 服务数据
|
||||
* */
|
||||
const setAiChartInfoData = (serviceOverview: any) => {
|
||||
// 设置总数数据
|
||||
setBarChartInfo([
|
||||
// 服务请求次数
|
||||
setBarChartInfoData({
|
||||
title: $t('请求数'),
|
||||
data: serviceOverview.requestOverview,
|
||||
value: serviceOverview.requestTotal,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
// token 消耗总数
|
||||
setBarChartInfoData({
|
||||
title: $t('Token'),
|
||||
data: serviceOverview.tokenOverview,
|
||||
value: serviceOverview.tokenTotal,
|
||||
date: serviceOverview.date
|
||||
})
|
||||
])
|
||||
// 设置平均值数据
|
||||
setPerBarChartInfo([
|
||||
// 平均 token 消耗
|
||||
{
|
||||
title: $t('平均 Token/s 统计'),
|
||||
data: serviceOverview.avgTokenOverview,
|
||||
value: serviceOverview.avgToken,
|
||||
date: serviceOverview.date,
|
||||
type: 'area'
|
||||
},
|
||||
// 平均请求
|
||||
setBarChartInfoData({
|
||||
title: $t('平均请求数'),
|
||||
data: serviceOverview.avgRequestPerSubscriberOverview,
|
||||
value: serviceOverview.avgRequestPerSubscriber,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
// 平均 token 消耗
|
||||
setBarChartInfoData({
|
||||
title: $t('平均 Token/订阅者统计'),
|
||||
data: serviceOverview.avgTokenPerSubscriberOverview,
|
||||
value: serviceOverview.avgTokenPerSubscriber,
|
||||
date: serviceOverview.date
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
/** 获取排名列表 */
|
||||
const getTopRankingList = () => {
|
||||
return fetchData<BasicResponse<{ apis: any; consumers: any }>>(
|
||||
`monitor/overview/top10/${activeTab === 'AI' ? 'ai' : 'rest'}`,
|
||||
{
|
||||
method: 'GET',
|
||||
eoParams: { start: timeRange?.start, end: timeRange?.end },
|
||||
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}
|
||||
).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const aiServiceOverview = {
|
||||
apis: [
|
||||
{
|
||||
id: '123',
|
||||
name: 'Model 21',
|
||||
request: 100,
|
||||
token: 100
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
name: 'Model 22',
|
||||
request: 200,
|
||||
token: 400
|
||||
},
|
||||
{
|
||||
id: '45611',
|
||||
name: 'Model 3',
|
||||
request: 3200,
|
||||
token: 4400
|
||||
},
|
||||
{
|
||||
id: '4536',
|
||||
name: 'Model 4',
|
||||
request: 1200,
|
||||
token: 4200
|
||||
}
|
||||
],
|
||||
consumers: [
|
||||
{
|
||||
id: '6666',
|
||||
name: 'Customer 1',
|
||||
request: 100,
|
||||
token: 100
|
||||
}
|
||||
]
|
||||
}
|
||||
const restServiceOverview = {
|
||||
apis: [
|
||||
{
|
||||
id: '123',
|
||||
name: 'Model 1',
|
||||
request: 100,
|
||||
traffic: 100
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
name: 'Model 2',
|
||||
request: 200,
|
||||
traffic: 300
|
||||
},
|
||||
{
|
||||
id: '12333',
|
||||
name: 'Model 123',
|
||||
request: 200,
|
||||
traffic: 300
|
||||
}
|
||||
],
|
||||
consumers: [
|
||||
{
|
||||
id: '6666',
|
||||
name: 'Customer 1',
|
||||
request: 100,
|
||||
traffic: 100
|
||||
}
|
||||
]
|
||||
}
|
||||
// 设置排名表格数据
|
||||
setTopRankingList({
|
||||
'TOP API': activeTab === 'AI' ? aiServiceOverview.apis : restServiceOverview.apis,
|
||||
'TOP Consumer': activeTab === 'AI' ? aiServiceOverview.consumers : restServiceOverview.consumers
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { startTime, endTime } = getTime(defaultTime, [])
|
||||
setTimeRange({
|
||||
start: startTime,
|
||||
end: endTime
|
||||
})
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setDashboardLoading(true)
|
||||
try {
|
||||
const requests = []
|
||||
// 根据activeTab添加相应的请求
|
||||
if (activeTab === 'AI') {
|
||||
requests.push(getAIServiceOverview())
|
||||
} else {
|
||||
requests.push(getRestServiceOverview())
|
||||
}
|
||||
|
||||
// 添加排名列表请求
|
||||
requests.push(getTopRankingList())
|
||||
|
||||
// 等待所有请求完成
|
||||
await Promise.all(requests)
|
||||
} catch (error) {
|
||||
console.error('加载数据出错:', error)
|
||||
message.error($t('加载数据失败,请重试'))
|
||||
} finally {
|
||||
// 无论成功失败,最后都设置loading为false
|
||||
setDashboardLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (timeRange) {
|
||||
fetchData()
|
||||
}
|
||||
}, [timeRange, activeTab])
|
||||
useEffect(() => {
|
||||
if (activeTab === 'AI') {
|
||||
aiServiceOverview && setAiChartInfoData(aiServiceOverview)
|
||||
} else {
|
||||
restServiceOverview && setRestChartInfoData(restServiceOverview)
|
||||
}
|
||||
}, [state.language])
|
||||
|
||||
return (
|
||||
<MonitorTotalPage
|
||||
fetchPieData={fetchPieData}
|
||||
fetchInvokeData={fetchInvokeData}
|
||||
fetchMessageData={fetchMessageData}
|
||||
fetchTableData={fetchTableData}
|
||||
goToDetail={goToDetail}
|
||||
/>
|
||||
<div className={`h-full overflow-hidden pb-btnybase flex flex-col bg-[#fff] `}>
|
||||
<ScrollableSection>
|
||||
<div className="flex items-center flex-wrap content-before bg-MAIN_BG pr-PAGE_INSIDE_X ">
|
||||
<div className="pt-btnybase mr-[10px]">{$t('服务')}:</div>
|
||||
<div className="mt-3 tab-nav flex rounded-md overflow-hidden border border-solid border-[#3D46F2] w-[150px] mr-[30px]">
|
||||
<div
|
||||
className={`tab-item text-center px-5 py-1.5 cursor-pointer w-[50%] text-sm transition-colors ${activeTab === 'REST' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
|
||||
onClick={() => setActiveTab('REST')}
|
||||
>
|
||||
REST
|
||||
</div>
|
||||
<div
|
||||
className={`tab-item text-center px-5 py-1.5 cursor-pointer w-[50%] text-sm transition-colors ${activeTab === 'AI' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
|
||||
onClick={() => setActiveTab('AI')}
|
||||
>
|
||||
AI
|
||||
</div>
|
||||
</div>
|
||||
<DateSelectFilter selectCallback={selectCallback} customClassNames="pt-[12px]" defaultTime={defaultTime} />
|
||||
</div>
|
||||
<Spin
|
||||
className="h-full pb-[20px]"
|
||||
wrapperClassName={`flex-1 overflow-auto`}
|
||||
indicator={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ transform: 'scale(1.5)' }}>
|
||||
<LoadingOutlined style={{ fontSize: 30 }} spin />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
spinning={dashboardLoading}
|
||||
>
|
||||
<div className="mt-[20px] flex mb-[10px]">
|
||||
{barChartInfo?.map((item: BarChartInfo, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 cursor-pointer rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{
|
||||
body: 'p-[15px]'
|
||||
}}
|
||||
>
|
||||
<ServiceBarChar key={index} height={400} dataInfo={item} customClassNames="flex-1"></ServiceBarChar>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex mb-[10px]">
|
||||
{perBarChartInfo?.map((item: any, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 cursor-pointer rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{
|
||||
body: 'p-[15px]'
|
||||
}}
|
||||
>
|
||||
{item.type === 'area' ? (
|
||||
<>
|
||||
<ServiceAreaChart
|
||||
key={index}
|
||||
height={250}
|
||||
dataInfo={item}
|
||||
customClassNames="flex-1 relative"
|
||||
></ServiceAreaChart>
|
||||
</>
|
||||
) : (
|
||||
<ServiceBarChar key={index} height={250} dataInfo={item} customClassNames="flex-1"></ServiceBarChar>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<RankingList topRankingList={topRankingList} serviceType={activeTab === 'AI' ? 'aiService' : 'restService'} />
|
||||
</Spin>
|
||||
</ScrollableSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||
import McpToolsContainer from '@core/pages/mcpService/McpToolsContainer.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import TopBreadcrumb from '@common/components/aoplatform/Breadcrumb.tsx'
|
||||
import ServiceInfoCard from '@common/components/aoplatform/serviceInfoCard.tsx'
|
||||
|
||||
type TabItemType = {
|
||||
key: string
|
||||
@@ -32,18 +33,12 @@ const ServiceHubDetail = () => {
|
||||
const { serviceId } = useParams<RouterParams>()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const [serviceBasicInfo, setServiceBasicInfo] = useState<ServiceBasicInfoType>()
|
||||
const [serviceName, setServiceName] = useState<string>()
|
||||
const [serviceDesc, setServiceDesc] = useState<string>()
|
||||
const [serviceDoc, setServiceDoc] = useState<string>()
|
||||
const { fetchData } = useFetch()
|
||||
const applyRef = useRef<ApplyServiceHandle>(null)
|
||||
const { modal, message } = App.useApp()
|
||||
const [mySystemOptionList, setMySystemOptionList] = useState<DefaultOptionType[]>()
|
||||
const [service, setService] = useState<ServiceDetailType>()
|
||||
const [serviceMetrics, setServiceMetrics] = useState<{ title: string; icon: React.ReactNode; value: string }[]>([])
|
||||
const [serviceTags, setServiceTags] = useState<
|
||||
{ color: string; textColor: string; title: string; content: React.ReactNode }[]
|
||||
>([])
|
||||
const [tools, setTools] = useState<Tool[]>([])
|
||||
const [tabItem, setTabItem] = useState<TabItemType[]>([])
|
||||
const [currentTab, setCurrentTab] = useState('')
|
||||
@@ -149,10 +144,7 @@ servers:
|
||||
apiDoc: modifyApiDoc(data.service.apiDoc, data.service.basic?.invokeAddress)
|
||||
})
|
||||
setServiceBasicInfo(data.service.basic)
|
||||
setServiceName(data.service.name)
|
||||
setServiceDesc(data.service.description)
|
||||
setServiceDoc(DOMPurify.sanitize(data.service.document))
|
||||
setServiceMetricsList(data.service.basic)
|
||||
setTabItemList(data.service.basic)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -164,54 +156,6 @@ servers:
|
||||
setCurrentTab(value)
|
||||
}
|
||||
|
||||
const setServiceMetricsList = (serviceBasicInfo: ServiceBasicInfoType) => {
|
||||
// 设置服务指标数据
|
||||
setServiceMetrics([
|
||||
{
|
||||
title: 'API 数量',
|
||||
icon: <ApiOutlined className="mr-[1px] text-[14px] h-[14px] w-[14px]" />,
|
||||
value: serviceBasicInfo.apiNum.toString()
|
||||
},
|
||||
{
|
||||
title: '接入消费者数量',
|
||||
icon: <Icon icon="tabler:api-app" width="14" height="14" />,
|
||||
value: serviceBasicInfo.appNum.toString()
|
||||
},
|
||||
{
|
||||
title: '30天内调用次数',
|
||||
icon: <Icon icon="iconoir:graph-up" width="14" height="14" />,
|
||||
value: formatInvokeCount(serviceBasicInfo.invokeCount ?? 0)
|
||||
}
|
||||
])
|
||||
// 设置服务标签数据
|
||||
const tags = [
|
||||
{
|
||||
color: '#7371fc1b',
|
||||
textColor: 'text-theme',
|
||||
title: serviceBasicInfo?.catalogue?.name || '-',
|
||||
content: serviceBasicInfo?.catalogue?.name || '-'
|
||||
},
|
||||
{
|
||||
color: `#${serviceBasicInfo?.serviceKind === 'ai' ? 'EADEFF' : 'DEFFE7'}`,
|
||||
textColor: 'text-[#000]',
|
||||
title: serviceBasicInfo?.serviceKind || '-',
|
||||
content: SERVICE_KIND_OPTIONS.find((x) => x.value === serviceBasicInfo?.serviceKind)?.label || '-'
|
||||
}
|
||||
]
|
||||
|
||||
// 如果启用了MCP,添加MCP标签
|
||||
if (serviceBasicInfo?.enableMcp) {
|
||||
tags.push({
|
||||
color: '#FFF0C1',
|
||||
textColor: 'text-[#000]',
|
||||
title: 'MCP',
|
||||
content: 'MCP'
|
||||
})
|
||||
}
|
||||
|
||||
setServiceTags(tags)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!serviceId) {
|
||||
console.warn('缺少serviceId')
|
||||
@@ -270,7 +214,7 @@ servers:
|
||||
content: (
|
||||
<ApplyServiceModal
|
||||
ref={applyRef}
|
||||
entity={{ ...serviceBasicInfo!, name: serviceName!, id: serviceId! }}
|
||||
entity={{ ...serviceBasicInfo!, name: service?.name || '', id: serviceId! }}
|
||||
mySystemOptionList={mySystemOptionList!}
|
||||
/>
|
||||
),
|
||||
@@ -293,19 +237,6 @@ servers:
|
||||
const handleToolsChange = (value: Tool[]) => {
|
||||
setTools(value)
|
||||
}
|
||||
// 格式化调用次数,添加K和M单位
|
||||
const formatInvokeCount = (count: number | null | undefined): string => {
|
||||
if (count === null || count === undefined) return '-'
|
||||
if (count >= 1000000) {
|
||||
const value = Math.floor(count / 100000) / 10
|
||||
return `${value}M`
|
||||
}
|
||||
if (count >= 1000) {
|
||||
const value = Math.floor(count / 100) / 10
|
||||
return `${value}K`
|
||||
}
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义一个更新标签项的函数,在serviceBasicInfo或tools变化时调用
|
||||
@@ -431,74 +362,21 @@ servers:
|
||||
<header>
|
||||
<TopBreadcrumb handleBackCallback={() => navigate(`/serviceHub/list`)} />
|
||||
</header>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(35deg, rgb(246, 246, 260) 0%, rgb(255, 255, 255) 40%)'
|
||||
<ServiceInfoCard
|
||||
serviceBasicInfo={{
|
||||
...serviceBasicInfo,
|
||||
serviceName: service?.name || '',
|
||||
serviceDesc: service?.description || ''
|
||||
}}
|
||||
className={`w-full mt-[20px]`}
|
||||
classNames={{
|
||||
body: 'p-[15px] h-[180px]'
|
||||
}}
|
||||
>
|
||||
<div className="service-info">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size={50}
|
||||
className={`rounded-[12px] border-none rounded-[12px] ${serviceBasicInfo?.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`}
|
||||
src={
|
||||
serviceBasicInfo?.logo ? (
|
||||
<img
|
||||
src={serviceBasicInfo?.logo}
|
||||
alt="Logo"
|
||||
style={{ maxWidth: '200px', width: '45px', height: '45px', objectFit: 'unset' }}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
icon={serviceBasicInfo?.logo ? '' : <Icon icon="tabler:api-app" />}
|
||||
>
|
||||
{' '}
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="pl-[20px] w-[calc(100%-50px)] overflow-hidden">
|
||||
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold w-full flex items-center gap-[4px]">
|
||||
{serviceName}
|
||||
</p>
|
||||
<div className="mt-[5px] h-[20px] flex items-center font-normal">
|
||||
{serviceTags.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
color={tag.color}
|
||||
className={`${tag.textColor} font-normal border-0 mr-[12px] max-w-[150px] truncate`}
|
||||
bordered={false}
|
||||
title={tag.title}
|
||||
>
|
||||
{tag.content}
|
||||
</Tag>
|
||||
))}
|
||||
{serviceMetrics.map((item, index) => (
|
||||
<Tooltip key={index} title={$t(item.title)}>
|
||||
<span className="mr-[12px] flex items-center">
|
||||
<span className="h-[14px] mr-[4px] flex items-center">{item.icon}</span>
|
||||
<span className="font-normal text-[14px]">{item.value}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="line-clamp-2 mt-[15px] text-[12px] text-[#666]" title={serviceDesc}>
|
||||
{serviceDesc || $t('暂无服务描述')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute bottom-[15px]">
|
||||
<Button type="primary" onClick={() => openModal('apply')}>
|
||||
{$t('申请')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
customClassName="mt-[20px]"
|
||||
actionSlot={
|
||||
<>
|
||||
<Button type="primary" onClick={() => openModal('apply')}>
|
||||
{$t('申请')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="flex">
|
||||
<Tabs
|
||||
className="p-btnbase pr-0 overflow-hidden [&>.ant-tabs-content-holder]:overflow-auto w-full flex-1 mr-[10px]"
|
||||
|
||||
Reference in New Issue
Block a user