diff --git a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx index 68b0143d..44f8bdb7 100644 --- a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx +++ b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx @@ -26,6 +26,7 @@ class InsidePageProps { scrollInsidePage?: boolean = false customPadding?: boolean customBtn?: ReactNode + customBanner?: ReactNode } const InsidePage: FC = ({ @@ -46,7 +47,8 @@ const InsidePage: FC = ({ scrollPage = true, scrollInsidePage = false, customPadding = false, - customBtn + customBtn, + customBanner }) => { const navigate = useNavigate() @@ -61,8 +63,13 @@ const InsidePage: FC = ({
- {!pageTitle && !description && !backUrl && !customBtn ? ( + {!pageTitle && !description && !backUrl && !customBtn && !customBanner ? ( <> + ) : customBanner ? ( +
+ {backUrl && goBack()} />} + {customBanner} +
) : (
{backUrl && goBack()} />} diff --git a/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx b/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx index f2724dd8..56dbba6e 100644 --- a/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx +++ b/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx @@ -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(initialDatePickerValue || [null, null]) @@ -111,7 +113,7 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => { } return ( -
+
{!hideTitle && } {hideBtns?.length && hideBtns.includes('hour') ? null : ( diff --git a/frontend/packages/common/src/components/aoplatform/serviceInfoCard.tsx b/frontend/packages/common/src/components/aoplatform/serviceInfoCard.tsx new file mode 100644 index 00000000..926969f7 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/serviceInfoCard.tsx @@ -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() + + /** + * 复制 + * @param value + * @returns + */ + const handleCopy = async (value: string): Promise => { + if (value) { + copyToClipboard(value) + message.success($t(RESPONSE_TIPS.copySuccess)) + } + } + + /** 获取服务信息 */ + const getServiceOverview = () => { + fetchData>('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: , + value: serviceOverview.apiNum?.toString() || '0' + }, + { + title: '接入消费者数量', + icon: , + value: serviceOverview.appNum?.toString() || '0' + }, + { + title: '30天内调用次数', + icon: , + 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 ( + <> + + {serviceOverview && ( + <> +
+
+
+ + ) : undefined + } + icon={serviceOverview.logo ? '' : } + > + {' '} + +
+
+

+ {serviceOverview.serviceName} +

+
+ {serviceTags.map((tag, index) => ( + + {tag.content} + + ))} + {serviceMetrics.map((item, index) => ( + + + {item.icon} + {item.value} + + + ))} +
+
+ {serviceOverview.id && ( + <> +
+ + {$t('服务 ID')}:{serviceOverview.id || '-'} + handleCopy(serviceOverview.id || '')} + sx={{ + position: 'absolute', + top: '0px', + right: '5px', + color: '#999', + transition: 'none', + '&.MuiButtonBase-root:hover': { + background: 'transparent', + color: '#3D46F2', + transition: 'none' + } + }} + > + + + + +
+ + )} +
+ + {serviceOverview.serviceDesc || $t('暂无服务描述')} + +
+ + )} +
{actionSlot}
+
+ + ) +} + +export default ServiceInfoCard diff --git a/frontend/packages/common/src/locales/keyHashMap.json b/frontend/packages/common/src/locales/keyHashMap.json index dbdecb5f..1d283fed 100644 --- a/frontend/packages/common/src/locales/keyHashMap.json +++ b/frontend/packages/common/src/locales/keyHashMap.json @@ -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", diff --git a/frontend/packages/common/src/locales/scan/en-US.json b/frontend/packages/common/src/locales/scan/en-US.json index 53f4af02..936e9eb5 100644 --- a/frontend/packages/common/src/locales/scan/en-US.json +++ b/frontend/packages/common/src/locales/scan/en-US.json @@ -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" } diff --git a/frontend/packages/common/src/locales/scan/ja-JP.json b/frontend/packages/common/src/locales/scan/ja-JP.json index 73d1938d..dff7d694 100644 --- a/frontend/packages/common/src/locales/scan/ja-JP.json +++ b/frontend/packages/common/src/locales/scan/ja-JP.json @@ -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": "平均トークン/加入者 統計" } diff --git a/frontend/packages/common/src/locales/scan/newJson/zh-CN.json b/frontend/packages/common/src/locales/scan/newJson/zh-CN.json index c5c43eda..9e26dfee 100644 --- a/frontend/packages/common/src/locales/scan/newJson/zh-CN.json +++ b/frontend/packages/common/src/locales/scan/newJson/zh-CN.json @@ -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": "输入名称查找用户" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/frontend/packages/common/src/locales/scan/zh-CN.json b/frontend/packages/common/src/locales/scan/zh-CN.json index 789ab22b..99343db3 100644 --- a/frontend/packages/common/src/locales/scan/zh-CN.json +++ b/frontend/packages/common/src/locales/scan/zh-CN.json @@ -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/订阅者统计" } diff --git a/frontend/packages/common/src/locales/scan/zh-TW.json b/frontend/packages/common/src/locales/scan/zh-TW.json index becfa911..9afebe7d 100644 --- a/frontend/packages/common/src/locales/scan/zh-TW.json +++ b/frontend/packages/common/src/locales/scan/zh-TW.json @@ -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 統計" } diff --git a/frontend/packages/core/src/const/ai-service/type.ts b/frontend/packages/core/src/const/ai-service/type.ts index 82c3825c..401e47a7 100644 --- a/frontend/packages/core/src/const/ai-service/type.ts +++ b/frontend/packages/core/src/const/ai-service/type.ts @@ -15,7 +15,7 @@ export type AiServiceConfigFieldType = { logoFile?:UploadFile; tags?:Array; description?: string; - team?:string; + team?:EntityItem; master?:string; serviceType?:'public'|'inner'; catalogue?:string | string[]; diff --git a/frontend/packages/core/src/const/const.tsx b/frontend/packages/core/src/const/const.tsx index f763da82..f4cf1da3 100644 --- a/frontend/packages/core/src/const/const.tsx +++ b/frontend/packages/core/src/const/const.tsx @@ -96,6 +96,20 @@ export const routerMap: Map = 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 = 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', diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index c356ee6a..214a35b6 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -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[] = [ + { + 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[] = [ + { + title: '名称', + dataIndex: 'name', + ellipsis: true + }, + { + title: '请求总数', + dataIndex: 'request', + ellipsis: true + }, + { + title: '流量', + dataIndex: 'traffic', + ellipsis: true + } +] + +/** REST 服务日志 */ +export const REST_SERVICE_LOG_LIST: PageProColumns[] = [ + { + 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[] = [ + { + 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 + } +] diff --git a/frontend/packages/core/src/index.css b/frontend/packages/core/src/index.css index 9809791a..a18fb3bb 100644 --- a/frontend/packages/core/src/index.css +++ b/frontend/packages/core/src/index.css @@ -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; } diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx index f42c6b47..0dc81acd 100644 --- a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx @@ -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({$t('总览')}, 'overview', undefined, undefined, undefined, ''), getItem( {$t('API 路由')}, 'route', @@ -149,7 +150,8 @@ const AiServiceInsidePage: FC = () => { 'project.myAiService.topology.view' ) : null, - getItem({$t('设置')}, 'setting', undefined, undefined, undefined, '') + getItem({$t('设置')}, 'setting', undefined, undefined, undefined, ''), + getItem({$t('日志')}, '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 ? ( - {$t('服务 ID')}:{serviceId || '-'} - - ) - } - ]} backUrl="/service/list" + customBanner={} >
{ + return +} + +export default AiServiceLogsContainer \ No newline at end of file diff --git a/frontend/packages/core/src/pages/serviceLogs/ApiNetWorkDataPreview.tsx b/frontend/packages/core/src/pages/serviceLogs/ApiNetWorkDataPreview.tsx new file mode 100644 index 00000000..ce758d38 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/ApiNetWorkDataPreview.tsx @@ -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 => { + 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 ( +
+
{item}
+
+ {!configContent[item] ? ( +

+              ) : isJsonString(configContent[item] || '') ? (
+                // 如果是有效的JSON对象字符串,使用ReactJson渲染
+                
+              ) : (
+                // 如果是普通字符串,直接用pre渲染
+                
{configContent[item]}
+ )} + handleCopy(configContent[item] || '')} + sx={{ + position: 'absolute', + top: '5px', + right: '5px', + color: '#999', + transition: 'none', + '&.MuiButtonBase-root:hover': { + background: 'transparent', + color: '#3D46F2', + transition: 'none' + } + }} + > +
+
+ ) + })} + + ) +} +export default ApiNetWorkDataPreview diff --git a/frontend/packages/core/src/pages/serviceLogs/LogDetail.tsx b/frontend/packages/core/src/pages/serviceLogs/LogDetail.tsx new file mode 100644 index 00000000..5a607f12 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/LogDetail.tsx @@ -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() + /** 全局状态 */ + 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 ? {status} : status + } + + /** + * 获取标签页内容 + */ + const tabItems = useMemo( + () => [ + { + key: 'request', + label: 'Request', + children: + }, + { + key: 'response', + label: 'Response', + children: + } + ], + [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: ( + <> + {consumer} + {isSystemConsumer && ( + + System-level API Key + + + + + + + )} + + ) + }, + { + key: 'httpStatus', + label: $t('HTTP 状态'), + children: renderStatusWithColor(status) + }, + { + key: 'ip', + label: $t('IP'), + children: ip + } + ]) + } + + /** + * 获取 AI 服务日志详情 + */ + const getAIServiceLogDetail = () => { + fetchData>('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>('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 ( + +
+ +
+
+ } + spinning={dashboardLoading} + > + +
+ +
+ + ) +} + +export default LogDetail diff --git a/frontend/packages/core/src/pages/serviceLogs/RestServiceLogsContainer.tsx b/frontend/packages/core/src/pages/serviceLogs/RestServiceLogsContainer.tsx new file mode 100644 index 00000000..db2ac201 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/RestServiceLogsContainer.tsx @@ -0,0 +1,7 @@ +import ServiceLogs from "./ServiceLogs" + +const RestServiceLogsContainer = () => { + return +} + +export default RestServiceLogsContainer \ No newline at end of file diff --git a/frontend/packages/core/src/pages/serviceLogs/ServiceLogs.tsx b/frontend/packages/core/src/pages/serviceLogs/ServiceLogs.tsx new file mode 100644 index 00000000..0fc440ed --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/ServiceLogs.tsx @@ -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() + /** 默认时间 */ + const [defaultTime] = useState('sevenDays') + /** 全局状态 */ + const { state } = useGlobalContext() + /** + * 请求数据 + */ + const { fetchData } = useFetch() + // 打开侧边弹窗 + const [drawerOpen, setDrawerOpen] = useState(false) + /** 选中的行 */ + const [selectedRow, setSelectedRow] = useState() + /** + * 列表ref + */ + const pageListRef = useRef(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) => ( + <> +
+ {renderStatusWithColor(record.status)} +
+ + ) + } + 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 ? {status} : status + } + + /** + * 获取 AI 列表数据 + * @param dataType + * @returns + */ + const getAiServiceLogList = () => { + return fetchData>(`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>(`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 ( + +
+ +
+
+ } + spinning={dashboardLoading} + > +
+ +
+ (serviceType === 'aiService' ? getAiServiceLogList() : getRestServiceLogList())} + onRowClick={(row: LogItem) => handleRowClick(row)} + /> +
+ setDrawerOpen(false)} + open={drawerOpen} + > + + +
+ + ) +} + +export default ServiceLogs diff --git a/frontend/packages/core/src/pages/serviceOverview/AiServiceContainer.tsx b/frontend/packages/core/src/pages/serviceOverview/AiServiceContainer.tsx new file mode 100644 index 00000000..501fb783 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/AiServiceContainer.tsx @@ -0,0 +1,7 @@ +import ServiceOverview from "./serviceOverview" + +const AiServiceContainer = () => { + return +} + +export default AiServiceContainer \ No newline at end of file diff --git a/frontend/packages/core/src/pages/serviceOverview/RestServiceContainer.tsx b/frontend/packages/core/src/pages/serviceOverview/RestServiceContainer.tsx new file mode 100644 index 00000000..2f527421 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/RestServiceContainer.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react' +import ServiceOverview from './serviceOverview' + +const RestServiceContainer: FC = () => { + return ( + <> + + + ) +} + +export default RestServiceContainer diff --git a/frontend/packages/core/src/pages/serviceOverview/charts/ServiceAreaChart.tsx b/frontend/packages/core/src/pages/serviceOverview/charts/ServiceAreaChart.tsx new file mode 100644 index 00000000..200c985d --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/charts/ServiceAreaChart.tsx @@ -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(null) + const [option, setOption] = useState({}) + 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 ( +
+
+
{$t(dataInfo?.title || '')}
+
+ {dataInfo?.value} +
+
+ + 381 T/s +
+
+ + 381 T/s +
+
+
+
+ +
+ ) +} + +export default ServiceAreaChart diff --git a/frontend/packages/core/src/pages/serviceOverview/charts/ServiceBarChar.tsx b/frontend/packages/core/src/pages/serviceOverview/charts/ServiceBarChar.tsx new file mode 100644 index 00000000..7108aea2 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/charts/ServiceBarChar.tsx @@ -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(null) + const [option, setOption] = useState({}) + 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 = `
+
${isNumberArray ? '' : params.name}
` + 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 = `` + tooltipContent += `
+ ${marker} ${name} ${value} +
` + }) + + tooltipContent += '
' + 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 ( +
+ +
+ ) +} + +export default ServiceBarChar diff --git a/frontend/packages/core/src/pages/serviceOverview/filter/DateSelectFilter.tsx b/frontend/packages/core/src/pages/serviceOverview/filter/DateSelectFilter.tsx new file mode 100644 index 00000000..2a59b9a2 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/filter/DateSelectFilter.tsx @@ -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(defaultTime || 'hour') + /** 日期选择 */ + const [datePickerValue, setDatePickerValue] = useState() + /** 时间范围变化 */ + const handleTimeRangeChange = (timeRange: TimeRange) => { + selectCallback(timeRange) + } + + return ( +
+ +
+ ) +} + +export default DateSelectFilter diff --git a/frontend/packages/core/src/pages/serviceOverview/indicator/Indicator.tsx b/frontend/packages/core/src/pages/serviceOverview/indicator/Indicator.tsx new file mode 100644 index 00000000..836c7b2b --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/indicator/Indicator.tsx @@ -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([]) + /** 路由跳转 */ + 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 */} + + + ) + } + ]) + } + + useEffect(() => { + if (!indicatorInfo) return + setIndicatorList() + }, [indicatorInfo]) + + return ( +
+ {indicatorList.map((item, index) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'p-[15px]' + }} + onClick={() => { + window.open(item.link) + }} + > +
+ {item.title} + {item.link && } +
+
{item.content}
+
+ ))} +
+ ) +} + +export default Indicator diff --git a/frontend/packages/core/src/pages/serviceOverview/rankingList/RankingList.tsx b/frontend/packages/core/src/pages/serviceOverview/rankingList/RankingList.tsx new file mode 100644 index 00000000..97717a2c --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/rankingList/RankingList.tsx @@ -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 ( +
+ {Object.keys(topRankingList)?.map((item: any, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'p-[15px] pb-[0px]' + }} + > +
+ {item === 'TOP API' ? $t('API 使用排名') : $t('消费者使用排名')} +
+ getTableData(item)} + showPagination={false} + tableClass="ranking-list" + ref={ref => { + if (ref) tableRefs.current[item] = ref; + }} + /> +
+ ))} +
+ ) +} + +export default RankingList diff --git a/frontend/packages/core/src/pages/serviceOverview/serviceOverview.tsx b/frontend/packages/core/src/pages/serviceOverview/serviceOverview.tsx new file mode 100644 index 00000000..8ba0a917 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/serviceOverview.tsx @@ -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('sevenDays') + /** 当前选中的时间范围 */ + const [timeRange, setTimeRange] = useState() + /** 总数数据 */ + const [barChartInfo, setBarChartInfo] = useState() + /** 平均值数据 */ + const [perBarChartInfo, setPerBarChartInfo] = useState() + /** 指标数据 */ + const [indicatorInfo, setIndicatorInfo] = useState([]) + /** 排名表格数据 */ + const [topRankingList, setTopRankingList] = useState([]) + /** 获取服务信息 */ + const { fetchData } = useFetch() + /** 弹窗组件 */ + const { message } = App.useApp() + /** 全局状态 */ + const { state } = useGlobalContext() + /** AI 服务数据 */ + const [aiServiceOverview, setAiServiceOverview] = useState() + /** REST 服务数据 */ + const [restServiceOverview, setRestServiceOverview] = useState() + /** 时间选择回调 */ + const selectCallback = (date: TimeRange) => { + setTimeRange(date) + } + + /** 获取 AI 服务信息 */ + const getAIServiceOverview = () => { + fetchData>('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>('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>('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 ( + +
+ +
+
+ } + spinning={dashboardLoading} + > +
+ +
+ +
+
+ {barChartInfo?.map((item: BarChartInfo, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'p-[15px]' + }} + > + + + ))} +
+
+ {perBarChartInfo?.map((item: any, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'p-[15px]' + }} + > + {item.type === 'area' ? ( + <> + + + ) : ( + + )} + + ))} +
+ +
+ + ) +} + +export default ServiceOverview diff --git a/frontend/packages/core/src/pages/serviceOverview/utils.ts b/frontend/packages/core/src/pages/serviceOverview/utils.ts new file mode 100644 index 00000000..3581b38b --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/utils.ts @@ -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 = { + '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 + } +} diff --git a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx index f7d53bca..041819b9 100644 --- a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx +++ b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx @@ -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>('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>('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({$t('总览')}, 'overview', undefined, undefined, undefined, ''), getItem( {$t('API 路由')}, 'route', @@ -146,9 +148,10 @@ const SystemInsidePage: FC = () => { null, [ // APP_MODE === 'pro' ? getItem({$t('调用拓扑图')}, 'topology',undefined,undefined,undefined,'project.mySystem.topology.view'):null, + getItem({$t('设置')}, 'setting', undefined, undefined, undefined, ''), getItem( - {$t('设置')}, - 'setting', + {$t('日志')}, + '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[]) - 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 ? ( - {$t('服务 ID')}:{serviceId || '-'} - - ) - } - ]} + customBanner={} backUrl="/service/list" >
diff --git a/frontend/packages/dashboard/src/pages/DashboardTotal.tsx b/frontend/packages/dashboard/src/pages/DashboardTotal.tsx index 7c322233..2ee65308 100644 --- a/frontend/packages/dashboard/src/pages/DashboardTotal.tsx +++ b/frontend/packages/dashboard/src/pages/DashboardTotal.tsx @@ -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> = (body: SearchBody) => - fetchData>('monitor/overview/summary', { - method: 'POST', - eoBody: body, - eoTransformKeys: ['request_summary', 'proxy_summary'] - }) - - const fetchInvokeData: (body: SearchBody) => Promise> = (body: SearchBody) => - fetchData>('monitor/overview/invoke', { - method: 'POST', - eoBody: body, - eoTransformKeys: ['request_total', 'request_rate', 'proxy_total', 'proxy_rate', 'time_interval'] - }) - - const fetchMessageData: (body: SearchBody) => Promise> = (body: SearchBody) => - fetchData>('monitor/overview/message', { - method: 'POST', - eoBody: body, - eoTransformKeys: ['time_interval', 'request_message', 'response_message'] - }) - - const fetchTableData: ( - body: SearchBody, - type: 'api' | 'subscribers' | 'providers' - ) => Promise> = ( - body: SearchBody, - type: 'api' | 'subscribers' | 'providers' - ) => - fetchData>('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('sevenDays') + /** 当前选中的时间范围 */ + const [timeRange, setTimeRange] = useState() + /** 当前激活的标签 */ + const [activeTab, setActiveTab] = useState('REST') + /** 面板 loading */ + const [dashboardLoading, setDashboardLoading] = useState(false) + /** 总数数据 */ + const [barChartInfo, setBarChartInfo] = useState() + /** 平均值数据 */ + const [perBarChartInfo, setPerBarChartInfo] = useState() + /** 排名表格数据 */ + const [topRankingList, setTopRankingList] = useState([]) + /** 弹窗组件 */ + const { message } = App.useApp() + /** 全局状态 */ + const { state } = useGlobalContext() + /** AI 服务数据 */ + const [aiServiceOverview, setAiServiceOverview] = useState() + /** REST 服务数据 */ + const [restServiceOverview, setRestServiceOverview] = useState() + /** 时间选择回调 */ + const selectCallback = (date: TimeRange) => { + setTimeRange(date) } + /** 获取 AI 服务信息 */ + const getAIServiceOverview = () => { + return fetchData>('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>('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>( + `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 ( - +
+ +
+
{$t('服务')}:
+
+
setActiveTab('REST')} + > + REST +
+
setActiveTab('AI')} + > + AI +
+
+ +
+ +
+ +
+
+ } + spinning={dashboardLoading} + > +
+ {barChartInfo?.map((item: BarChartInfo, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'p-[15px]' + }} + > + + + ))} +
+
+ {perBarChartInfo?.map((item: any, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'p-[15px]' + }} + > + {item.type === 'area' ? ( + <> + + + ) : ( + + )} + + ))} +
+ + + +
) } diff --git a/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx b/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx index 82e41dd5..8248dcaf 100644 --- a/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx +++ b/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx @@ -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() const { setBreadcrumb } = useBreadcrumb() const [serviceBasicInfo, setServiceBasicInfo] = useState() - const [serviceName, setServiceName] = useState() - const [serviceDesc, setServiceDesc] = useState() const [serviceDoc, setServiceDoc] = useState() const { fetchData } = useFetch() const applyRef = useRef(null) const { modal, message } = App.useApp() const [mySystemOptionList, setMySystemOptionList] = useState() const [service, setService] = useState() - 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([]) const [tabItem, setTabItem] = useState([]) 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: , - value: serviceBasicInfo.apiNum.toString() - }, - { - title: '接入消费者数量', - icon: , - value: serviceBasicInfo.appNum.toString() - }, - { - title: '30天内调用次数', - icon: , - 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: ( ), @@ -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:
navigate(`/serviceHub/list`)} />
- -
-
-
- - ) : undefined - } - icon={serviceBasicInfo?.logo ? '' : } - > - {' '} - -
-
-

- {serviceName} -

-
- {serviceTags.map((tag, index) => ( - - {tag.content} - - ))} - {serviceMetrics.map((item, index) => ( - - - {item.icon} - {item.value} - - - ))} -
-
-
- - {serviceDesc || $t('暂无服务描述')} - -
-
- -
-
+ customClassName="mt-[20px]" + actionSlot={ + <> + + + } + />