feature/1.8-Improve system observability

This commit is contained in:
ningyv
2025-04-25 18:42:59 +08:00
parent 2dc16f4bb8
commit 65ad7657ef
31 changed files with 3161 additions and 342 deletions
@@ -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
}
]
+16
View File
@@ -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]"