Merge pull request #299 from APIParkLab/feature/1.8-cx

Feature/1.8 cx
This commit is contained in:
ningyv
2025-05-06 18:52:12 +08:00
committed by GitHub
83 changed files with 4532 additions and 776 deletions
+2 -1
View File
@@ -50,7 +50,8 @@
"vite-tsconfig-paths": "^4.3.2",
"react-json-view": "^1.21.3",
"zod": "^3.23.8",
"@modelcontextprotocol/sdk": "^1.9.0"
"@modelcontextprotocol/sdk": "^1.9.0",
"echarts-for-react": "^3.0.2"
},
"devDependencies": {
"@ant-design/cssinjs": "^1.18.2",
@@ -36,7 +36,7 @@ function BasicLayout({ project = 'core' }: { project: string }) {
const { state, accessData, checkPermission, accessInit, dispatch, resetAccess, getGlobalAccessData, menuList } =
useGlobalContext()
const [pathname, setPathname] = useState(currentUrl)
const mainPage = project === 'core' ? '/service/list' : '/serviceHub/list'
const mainPage = project === 'core' ? '/service/list' : '/portal/list'
const [menuItems, setMenuItems] = useState<MenuProps['items']>()
const pluginSlotHub = usePluginSlotHub()
@@ -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()} />}
@@ -58,7 +58,7 @@ interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<Acti
delayLoading?: boolean
noScroll?: boolean
/* 前端分页的表格,需要传入该字段以支持后端搜索 */
manualReloadTable?: () => void,
manualReloadTable?: () => void
customEmptyRender?: () => React.ReactNode
}
@@ -109,6 +109,7 @@ const PageList = <T extends Record<string, unknown>>(
const [allowTableClick, setAllowTableClick] = useState<boolean>(false)
const { accessData, checkPermission, accessInit, state } = useGlobalContext()
const [minTableWidth, setMinTableWidth] = useState<number>(0)
const [enableVirtual, setEnableVirtual] = useState(false)
useImperativeHandle(ref, () => actionRef.current!)
@@ -301,7 +302,7 @@ const PageList = <T extends Record<string, unknown>>(
<ProTable<T>
actionRef={actionRef}
columns={newColumns}
virtual
virtual={enableVirtual}
scroll={noScroll ? undefined : { x: tableWidth, y: tableHeight }}
size="middle"
rowSelection={rowSelection}
@@ -328,6 +329,10 @@ const PageList = <T extends Record<string, unknown>>(
}
: false
}}
postData={(data: any) => {
setEnableVirtual(!!data?.length)
return data
}}
showSorterTooltip={false}
columnsState={{ persistenceType: 'localStorage', persistenceKey: id }}
pagination={
@@ -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])
@@ -110,8 +112,12 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
return current && current.valueOf() > dayjs().startOf('day').valueOf()
}
useEffect(() => {
setTimeButton(initialTimeButton || '')
}, [initialTimeButton])
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 : (
@@ -187,6 +187,9 @@ export const TranslateWord = () => {
{$t('调用地址')}
{$t('消费者 IP')}
{$t('鉴权名称')}
{$t('日志输出')}
{$t('响应时间')}
{$t('时间戳')}
</>
)
}
@@ -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: [
'api_num',
'enable_mcp',
'service_kind',
'subscriber_num',
'invoke_num',
'avaliable_monitor',
'is_released'
]
}).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(`/portal/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
@@ -0,0 +1,431 @@
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
// CommonJS
factory(exports, require('echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
}(this, function (exports, echarts) {
var log = function (msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
echarts.registerTheme('apipark chart palette', {
"color": [
"#4429e6",
"#fd6280",
"#28dbe2",
"#ffc404",
"#b92325",
"#1b9f17",
"#fe8705",
"#97b552",
"#95706d",
"#dc69aa",
"#07a2a4",
"#9a7fd1",
"#588dd5",
"#f5994e",
"#333333"
],
"backgroundColor": "rgba(0,0,0,0)",
"textStyle": {},
"title": {
"textStyle": {
"color": "#333333"
},
"subtextStyle": {
"color": "#999999"
}
},
"line": {
"itemStyle": {
"borderWidth": "2"
},
"lineStyle": {
"width": "2"
},
"symbolSize": "5",
"symbol": "circle",
"smooth": true
},
"radar": {
"itemStyle": {
"borderWidth": "2"
},
"lineStyle": {
"width": "2"
},
"symbolSize": "5",
"symbol": "circle",
"smooth": true
},
"bar": {
"itemStyle": {
"barBorderWidth": "2",
"barBorderColor": "rgba(255,255,255,0.3)"
}
},
"pie": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"scatter": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"boxplot": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"parallel": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"sankey": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"funnel": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"gauge": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"candlestick": {
"itemStyle": {
"color": "#d87a80",
"color0": "#2ec7c9",
"borderColor": "#d87a80",
"borderColor0": "#2ec7c9",
"borderWidth": 1
}
},
"graph": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
},
"lineStyle": {
"width": 1,
"color": "#aaaaaa"
},
"symbolSize": "5",
"symbol": "circle",
"smooth": true,
"color": [
"#4429e6",
"#fd6280",
"#28dbe2",
"#ffc404",
"#b92325",
"#1b9f17",
"#fe8705",
"#97b552",
"#95706d",
"#dc69aa",
"#07a2a4",
"#9a7fd1",
"#588dd5",
"#f5994e",
"#333333"
],
"label": {
"color": "#fefefe"
}
},
"map": {
"itemStyle": {
"areaColor": "#dddddd",
"borderColor": "#eeeeee",
"borderWidth": 0.5
},
"label": {
"color": "#d87a80"
},
"emphasis": {
"itemStyle": {
"areaColor": "rgba(254,153,78,1)",
"borderColor": "#444",
"borderWidth": 1
},
"label": {
"color": "rgb(100,0,0)"
}
}
},
"geo": {
"itemStyle": {
"areaColor": "#dddddd",
"borderColor": "#eeeeee",
"borderWidth": 0.5
},
"label": {
"color": "#d87a80"
},
"emphasis": {
"itemStyle": {
"areaColor": "rgba(254,153,78,1)",
"borderColor": "#444",
"borderWidth": 1
},
"label": {
"color": "rgb(100,0,0)"
}
}
},
"categoryAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisLabel": {
"show": true,
"color": "#333333"
},
"splitLine": {
"show": false,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.3)",
"rgba(200,200,200,0.3)"
]
}
}
},
"valueAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisLabel": {
"show": true,
"color": "#333"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": true,
"areaStyle": {
"color": [
"#ffffff",
"rgba(0,0,0,0.02)"
]
}
}
},
"logAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisLabel": {
"show": true,
"color": "#333"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": true,
"areaStyle": {
"color": [
"#ffffff",
"rgba(0,0,0,0.02)"
]
}
}
},
"timeAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisLabel": {
"show": true,
"color": "#333"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.3)",
"rgba(200,200,200,0.3)"
]
}
}
},
"toolbox": {
"iconStyle": {
"borderColor": "#000000"
},
"emphasis": {
"iconStyle": {
"borderColor": "#000000"
}
}
},
"legend": {
"textStyle": {
"color": "#333333"
}
},
"tooltip": {
"axisPointer": {
"lineStyle": {
"color": "rgba(0,0,0,0.3)",
"width": "1"
},
"crossStyle": {
"color": "rgba(0,0,0,0.3)",
"width": "1"
}
}
},
"timeline": {
"lineStyle": {
"color": "#008acd",
"width": 1
},
"itemStyle": {
"color": "#008acd",
"borderWidth": 1
},
"controlStyle": {
"color": "#008acd",
"borderColor": "#008acd",
"borderWidth": 0.5
},
"checkpointStyle": {
"color": "#2ec7c9",
"borderColor": "#2ec7c9"
},
"label": {
"color": "#008acd"
},
"emphasis": {
"itemStyle": {
"color": "#a9334c"
},
"controlStyle": {
"color": "#008acd",
"borderColor": "#008acd",
"borderWidth": 0.5
},
"label": {
"color": "#008acd"
}
}
},
"visualMap": {
"color": [
"#ffffff",
"#4429e6"
]
},
"dataZoom": {
"backgroundColor": "rgba(47,69,84,0)",
"dataBackgroundColor": "#efefff",
"fillerColor": "rgba(182,162,222,0.2)",
"handleColor": "#008acd",
"handleSize": "100%",
"textStyle": {
"color": "#333333"
}
},
"markPoint": {
"label": {
"color": "#fefefe"
},
"emphasis": {
"label": {
"color": "#fefefe"
}
}
}
});
}));
@@ -0,0 +1,409 @@
{
"color": [
"#4429e6",
"#fd6280",
"#28dbe2",
"#ffc404",
"#b92325",
"#1b9f17",
"#fe8705",
"#97b552",
"#95706d",
"#dc69aa",
"#07a2a4",
"#9a7fd1",
"#588dd5",
"#f5994e",
"#333333"
],
"backgroundColor": "rgba(0,0,0,0)",
"textStyle": {},
"title": {
"textStyle": {
"color": "#333333"
},
"subtextStyle": {
"color": "#999999"
}
},
"line": {
"itemStyle": {
"borderWidth": "2"
},
"lineStyle": {
"width": "2"
},
"symbolSize": "5",
"symbol": "circle",
"smooth": true
},
"radar": {
"itemStyle": {
"borderWidth": "2"
},
"lineStyle": {
"width": "2"
},
"symbolSize": "5",
"symbol": "circle",
"smooth": true
},
"bar": {
"itemStyle": {
"barBorderWidth": "2",
"barBorderColor": "rgba(255,255,255,0.3)"
}
},
"pie": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"scatter": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"boxplot": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"parallel": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"sankey": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"funnel": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"gauge": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
}
},
"candlestick": {
"itemStyle": {
"color": "#d87a80",
"color0": "#2ec7c9",
"borderColor": "#d87a80",
"borderColor0": "#2ec7c9",
"borderWidth": 1
}
},
"graph": {
"itemStyle": {
"borderWidth": "2",
"borderColor": "rgba(255,255,255,0.3)"
},
"lineStyle": {
"width": 1,
"color": "#aaaaaa"
},
"symbolSize": "5",
"symbol": "circle",
"smooth": true,
"color": [
"#4429e6",
"#fd6280",
"#28dbe2",
"#ffc404",
"#b92325",
"#1b9f17",
"#fe8705",
"#97b552",
"#95706d",
"#dc69aa",
"#07a2a4",
"#9a7fd1",
"#588dd5",
"#f5994e",
"#333333"
],
"label": {
"color": "#fefefe"
}
},
"map": {
"itemStyle": {
"areaColor": "#dddddd",
"borderColor": "#eeeeee",
"borderWidth": 0.5
},
"label": {
"color": "#d87a80"
},
"emphasis": {
"itemStyle": {
"areaColor": "rgba(254,153,78,1)",
"borderColor": "#444",
"borderWidth": 1
},
"label": {
"color": "rgb(100,0,0)"
}
}
},
"geo": {
"itemStyle": {
"areaColor": "#dddddd",
"borderColor": "#eeeeee",
"borderWidth": 0.5
},
"label": {
"color": "#d87a80"
},
"emphasis": {
"itemStyle": {
"areaColor": "rgba(254,153,78,1)",
"borderColor": "#444",
"borderWidth": 1
},
"label": {
"color": "rgb(100,0,0)"
}
}
},
"categoryAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisLabel": {
"show": true,
"color": "#333333"
},
"splitLine": {
"show": false,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.3)",
"rgba(200,200,200,0.3)"
]
}
}
},
"valueAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisLabel": {
"show": true,
"color": "#333"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": true,
"areaStyle": {
"color": [
"#ffffff",
"rgba(0,0,0,0.02)"
]
}
}
},
"logAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisLabel": {
"show": true,
"color": "#333"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": true,
"areaStyle": {
"color": [
"#ffffff",
"rgba(0,0,0,0.02)"
]
}
}
},
"timeAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "rgba(0,0,0,0.1)"
}
},
"axisLabel": {
"show": true,
"color": "#333"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.3)",
"rgba(200,200,200,0.3)"
]
}
}
},
"toolbox": {
"iconStyle": {
"borderColor": "#000000"
},
"emphasis": {
"iconStyle": {
"borderColor": "#000000"
}
}
},
"legend": {
"textStyle": {
"color": "#333333"
}
},
"tooltip": {
"axisPointer": {
"lineStyle": {
"color": "rgba(0,0,0,0.3)",
"width": "1"
},
"crossStyle": {
"color": "rgba(0,0,0,0.3)",
"width": "1"
}
}
},
"timeline": {
"lineStyle": {
"color": "#008acd",
"width": 1
},
"itemStyle": {
"color": "#008acd",
"borderWidth": 1
},
"controlStyle": {
"color": "#008acd",
"borderColor": "#008acd",
"borderWidth": 0.5
},
"checkpointStyle": {
"color": "#2ec7c9",
"borderColor": "#2ec7c9"
},
"label": {
"color": "#008acd"
},
"emphasis": {
"itemStyle": {
"color": "#a9334c"
},
"controlStyle": {
"color": "#008acd",
"borderColor": "#008acd",
"borderWidth": 0.5
},
"label": {
"color": "#008acd"
}
}
},
"visualMap": {
"color": [
"#ffffff",
"#4429e6"
]
},
"dataZoom": {
"backgroundColor": "rgba(47,69,84,0)",
"dataBackgroundColor": "#efefff",
"fillerColor": "rgba(182,162,222,0.2)",
"handleColor": "#008acd",
"handleSize": "100%",
"textStyle": {
"color": "#333333"
}
},
"markPoint": {
"label": {
"color": "#fefefe"
},
"emphasis": {
"label": {
"color": "#fefefe"
}
}
}
}
@@ -0,0 +1,12 @@
// 导入echarts核心模块
import * as echarts from 'echarts/core'
// 导入主题JSON
import themeJson from './apipark-chart-palette.json'
// 全局注册主题
export function registerApiparkTheme() {
echarts.registerTheme('apipark', themeJson)
}
// 导出主题名称,方便组件使用
export const THEME_NAME = 'apipark'
@@ -0,0 +1,11 @@
// 导入主题配置
import themeJson from './apipark-chart-palette.json'
// 导出主题配置
export const apiparkTheme = themeJson
// 导出颜色列表,方便单独使用
export const chartColors = themeJson.color
// 导出默认颜色
export const defaultColor = chartColors[0]
@@ -22,8 +22,10 @@ export const BreadcrumbProvider = ({ children }: unknown) => {
<BreadcrumbContext.Provider
value={{
setBreadcrumb: (newItems) => {
newItems.slice(0, newItems.length - 1).forEach((item) => {
item.title = <span className="cursor-pointer hover:text-theme">{item.title}</span>
newItems.forEach((item) => {
item.title = (
<span className={`${item.onClick ? 'cursor-pointer hover:text-theme' : ''}`}>{item.title}</span>
)
})
setBreadcrumb(newItems)
},
@@ -87,8 +87,8 @@ const mockData = [
},
{
name: 'API 市场',
key: 'serviceHub',
path: '/serviceHub',
key: 'portal',
path: '/portal',
icon: 'ic:baseline-hub',
access: 'system.api_portal.api_portal.view'
},
@@ -107,15 +107,15 @@ const mockData = [
},
{
name: '服务',
key: 'analyticsSubscriber',
path: '/analytics/subscriber/list',
key: 'analyticsService',
path: '/analytics/service/list',
icon: 'ic:baseline-blinds-closed',
access: 'system.analysis.run_view.view'
},
{
name: '消费者',
key: 'analyticsProvider',
path: '/analytics/provider/list',
key: 'analyticsConsumer',
path: '/analytics/consumer/list',
icon: 'ic:baseline-apps',
access: 'system.analysis.run_view.view'
},
@@ -253,7 +253,7 @@ const mockData = [
access: 'system.settings.ssl_certificate.view'
},
{
name: '日志',
name: '日志输出',
key: 'logsettings',
path: '/logsettings',
icon: 'ic:baseline-sticky-note-2',
@@ -112,10 +112,10 @@ const mockData = {
},
{
driver: 'apipark.builtIn.component',
name: 'serviceHub',
name: 'portal',
router: [
{
path: 'serviceHub',
path: 'portal',
type: 'normal'
}
]
@@ -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",
@@ -241,6 +246,9 @@
"调用地址": "K2f5fdf5e",
"消费者 IP": "K1bc5e0a3",
"鉴权名称": "K6f39ea21",
"日志输出": "K3c722abd",
"响应时间": "K1be06929",
"时间戳": "K5e51f5d",
"暂无操作权限,请联系管理员分配。": "K23fda291",
"微信小程序": "K4618cb0a",
"获取文件,需填路径": "Ka854f511",
@@ -345,11 +353,10 @@
"重置": "K50d471b2",
"查询": "Kee8ae330",
"请输入 APIURL 搜索": "Kf8187c33",
"服务": "Kb58e0c3f",
"说明文档": "K6cd677b",
"最近一次更新者": "K617f34f1",
"最近一次更新时间": "K6ebca204",
"保存": "Kabfe9512",
"服务": "Kb58e0c3f",
"API 路由": "K51d1eb5d",
"API 文档": "Ka2b6d281",
"使用说明": "Kdefa9caa",
@@ -361,14 +368,10 @@
"管理": "K5974bf24",
"调用拓扑图": "K3fa5c4c3",
"设置": "Kb5c7b82d",
"服务 ID": "K1e84ad04",
"新增订阅方": "K39ab0358",
"手动添加": "K18307d56",
"订阅申请": "K705fe9f5",
"订阅方": "K3a67ea90",
"API": "K3ba29a85",
"编辑 API": "Ke93388fd",
"添加 API": "K84aabfd4",
"AI 路由设置": "Kefa2a4cf",
"路由名称": "K66060758",
"请求路径": "K5582ac8",
@@ -379,7 +382,6 @@
"拦截接口": "Kee4139c2",
"开启拦截后,网关会拦截所有该路径的请求。": "K3e38ea",
"模型配置": "K8a35059b",
"路由": "Kf9dcef3a",
"添加路由": "K6134bbe8",
"输入 URL 查找路由": "Kf85b83a0",
"线上模型": "K84b2cf2d",
@@ -554,11 +556,15 @@
"访客模式": "K192b3e38",
"您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。": "K91aa4801",
"Version (0)-(1)": "K480045ce",
"日志置": "Kadee8e49",
"日志输出设置": "K74a5fbc0",
"提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。": "K2724314b",
"日志配置": "Kadee8e49",
"MCP 配置": "K6e9c928f",
"Open API 文档": "Kb6d0eb39",
"AI 代理集成": "Ke6908f16",
"请先订阅该服务": "K71ed51fa",
"申请": "K4aa9ed2c",
"选择 API Key": "K1bec8cbe",
"新增 API Key": "Kb0e0aeda",
"API 密钥可用于调用系统级 Open API 和 MCP。": "K9d81999c",
"MCP 服务": "Kf106bc62",
@@ -619,7 +625,7 @@
"数据源": "K8fa58214",
"设置监控报表的数据来源,设置完成之后即可获得详细的API调用统计图表。": "Kdbafd6f9",
"统计图表": "K1358acf",
"数据日志": "K17dc3a62",
"请求日志": "Kc8bf447",
"地址(IP:端口)": "K62dabdf6",
"组织(Organization": "K2db12335",
"添加策略": "K34d0d409",
@@ -627,8 +633,6 @@
"处理日志": "Ke429194e",
"脱敏前": "K8c34c02f",
"脱敏后": "K8e3d388d",
"编辑服务策略": "Kf06f6737",
"添加服务策略": "K205971e1",
"编辑策略": "Kc82b8374",
"策略类型": "K4b34a5e5",
"匹配条件": "K57f0fee8",
@@ -660,6 +664,28 @@
"系统级别角色": "K138facd3",
"添加角色": "K6eac768d",
"团队级别角色": "Kb9c2cf02",
"API / Tools": "K9d526cac",
"消费者": "K7acfcfad",
"HTTP 状态": "Kc68ba0f4",
"IP": "Kb09b747",
"通过系统级别的 API Key 来调用": "K2eacb44f",
"日志详情": "K764bca7c",
"暂无数据": "Kf8525cf2",
"输入 Token": "K33bc1ad1",
"输出 Token": "Ke00ff18b",
"订阅数量": "Ke04bc00d",
"已开启": "K1b97ae0a",
"开启 MCP": "K19ec733b",
"API 使用排名": "Kbee2340",
"消费者使用排名": "Kf6af1f40",
"请求次数": "K9d3f2d9d",
"网络流量": "Ke2241377",
"平均响应时间": "K7c8d5c23",
"平均每消费者的请求次数": "K6c267c7b",
"平均每消费者的网络流量": "K133d4291",
"Token 消耗": "K37c5f1d0",
"平均 Token 消耗": "K10a8bee3",
"平均每消费者的 Token 消耗": "Kb98264d4",
"单位:ms,最小值:1": "K2a16c93b",
"API 路由设置": "Ka945cfb1",
"API 基础信息": "K2e050340",
@@ -742,7 +768,6 @@
"退出全屏": "Kaf70c3b",
"(0)调用详情": "Kd22841a4",
"消费者调用统计": "K61cca533",
"消费者": "K7acfcfad",
"请选择消费者": "Kdfff59d4",
"调用趋势": "K8c7f2d2e",
"(0)-(1)调用趋势": "K657c3452",
@@ -783,14 +808,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 +874,7 @@
"版本": "K81634069",
"更新时间": "Keefda53d",
"介绍": "K59cdbec3",
"暂无服务描述": "Ka4b45550",
"申请": "K4aa9ed2c",
"API": "K3ba29a85",
"无标签": "K96a2f1c8",
"分类": "Kb32f0afe",
"服务市场": "K370a3eb2",
@@ -4,7 +4,7 @@
"Kb58e0c3f": "Service",
"Kc9e489f5": "Team",
"K61c89f5f": "API Portal",
"K16d71239": "Analysis",
"K16d71239": "Analytics",
"K714c192d": "Call Statistics",
"Kd57dfe97": "Topology",
"K3fe97dcc": "System Settings",
@@ -186,7 +186,7 @@
"K617f34f1": "Updated By",
"K6ebca204": "Update Time",
"Kabfe9512": "Save",
"K51d1eb5d": "API",
"K51d1eb5d": "API Routes",
"Ka2b6d281": "API Docs",
"Kdefa9caa": "Usage Instructions",
"K36856e71": "Publish",
@@ -210,7 +210,7 @@
"K469e475a": "Max Retry Times",
"K8a35059b": "Model Settings",
"Kf9dcef3a": "API",
"K6134bbe8": "Add API",
"K6134bbe8": "Add API Route",
"Kf85b83a0": "Enter URL to Search",
"Kcf9f90b8": "Model Provider",
"Kfede1c7c": "Model",
@@ -927,5 +927,50 @@
"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",
"K1639a17a": "API Routes Docs",
"K33bc1ad1": "Input Token",
"Ke00ff18b": "Output Token",
"K81140e5b": "Total Token",
"K3c722abd": "Log Output",
"K74a5fbc0": "Log Output Settings",
"Kc8bf447": "Request Log",
"Kf8525cf2": "No Data",
"K1be06929": "Response Time",
"K5e51f5d": "Timestamp",
"K9d3f2d9d": "Requests",
"Ke2241377": "Traffic",
"K6c267c7b": "Avg Requests per Subscriber",
"K133d4291": "Avg Traffic per Subscriber",
"K37c5f1d0": "Token",
"Kb98264d4": "Avg Token per Subscriber"
}
@@ -189,7 +189,7 @@
"K617f34f1": "更新者",
"K6ebca204": "更新日時",
"Kabfe9512": "保存",
"K51d1eb5d": "API",
"K51d1eb5d": "APIルート",
"Ka2b6d281": "API ドキュメント",
"Kdefa9caa": "説明ドキュメント",
"K36856e71": "公開",
@@ -213,7 +213,7 @@
"K469e475a": "リトライ回数",
"K8a35059b": "モデル設定",
"Kf9dcef3a": "API",
"K6134bbe8": "API を追加",
"K6134bbe8": "APIルートを追加する",
"Kf85b83a0": "URL を入力して検索",
"Kcf9f90b8": "モデルプロバイダー",
"Kfede1c7c": "モデル",
@@ -949,5 +949,50 @@
"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": "平均トークン/加入者 統計",
"K1639a17a": "APIルートのドキュメント",
"K33bc1ad1": "入力トークン",
"Ke00ff18b": "出力トークン",
"K81140e5b": "合計トークン",
"K3c722abd": "ログ出力",
"K74a5fbc0": "ログ出力設定",
"Kc8bf447": "リクエストログ",
"Kf8525cf2": "データがありません",
"K1be06929": "応答時間",
"K5e51f5d": "タイムスタンプ",
"K9d3f2d9d": "リクエスト数",
"Ke2241377": "ネットワークトラフィック",
"K6c267c7b": "消費者あたりの平均リクエスト数",
"K133d4291": "消費者あたりの平均ネットワークトラフィック",
"K37c5f1d0": "トークン消費量",
"Kb98264d4": "消費者あたりの平均トークン消費量"
}
@@ -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": "输入名称查找用户"
}
{}
@@ -189,7 +189,7 @@
"K617f34f1": "更新者",
"K6ebca204": "更新时间",
"Kabfe9512": "保存",
"K51d1eb5d": "API",
"K51d1eb5d": "API 路由",
"Ka2b6d281": "API 文档",
"Kdefa9caa": "说明文档",
"K36856e71": "发布",
@@ -213,7 +213,7 @@
"K469e475a": "最大重试次数",
"K8a35059b": "模型设置",
"Kf9dcef3a": "API",
"K6134bbe8": "添加 API",
"K6134bbe8": "添加 API 路由",
"Kf85b83a0": "输入 URL 查找",
"Kcf9f90b8": "模型供应商",
"Kfede1c7c": "模型",
@@ -880,5 +880,48 @@
"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/订阅者统计",
"K1639a17a": "API 路由文档",
"K33bc1ad1": "输入 Token",
"Ke00ff18b": "输出 Token",
"K81140e5b": "总 Token",
"K3c722abd": "日志输出",
"K74a5fbc0": "日志输出设置",
"Kc8bf447": "请求日志",
"Kf8525cf2": "暂无数据",
"K1be06929": "响应时间",
"K5e51f5d": "时间",
"K9d3f2d9d": "请求次数",
"Ke2241377": "网络流量",
"K6c267c7b": "平均每消费者的请求次数",
"K133d4291": "平均每消费者的网络流量",
"K37c5f1d0": "Token 消耗",
"Kb98264d4": "平均每消费者的 Token 消耗"
}
@@ -189,7 +189,7 @@
"K617f34f1": "更新者",
"K6ebca204": "更新時間",
"Kabfe9512": "保存",
"K51d1eb5d": "API",
"K51d1eb5d": "API 路由",
"Ka2b6d281": "API 文檔",
"Kdefa9caa": "說明文檔",
"K36856e71": "發布",
@@ -213,7 +213,7 @@
"K469e475a": "最大重試次數",
"K8a35059b": "模型設置",
"Kf9dcef3a": "API",
"K6134bbe8": "添加 API",
"K6134bbe8": "添加 API 路由",
"Kf85b83a0": "輸入 URL 查找",
"Kcf9f90b8": "模型供應商",
"Kfede1c7c": "模型",
@@ -949,5 +949,50 @@
"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 統計",
"K1639a17a": "API 路由文件",
"K33bc1ad1": "輸入 Token",
"Ke00ff18b": "輸出 Token",
"K81140e5b": "總計 Token",
"K3c722abd": "日誌輸出",
"K74a5fbc0": "日誌輸出設定",
"Kc8bf447": "請求日誌",
"Kf8525cf2": "暫無資料",
"K1be06929": "回應時間",
"K5e51f5d": "時間",
"K9d3f2d9d": "請求次數",
"Ke2241377": "網路流量",
"K6c267c7b": "平均每位使用者的請求次數",
"K133d4291": "平均每位使用者的網路流量",
"K37c5f1d0": "Token 消耗",
"Kb98264d4": "平均每位使用者的 Token 消耗"
}
+4
View File
@@ -6,6 +6,7 @@ import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'
import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext'
import useInitializeMonaco from '@common/hooks/useInitializeMonaco'
import { $t } from '@common/locales'
import { registerApiparkTheme } from '@common/const/charts/initChartTheme'
import RenderRoutes from '@core/components/aoplatform/RenderRoutes'
import { App as AppAntd, ConfigProvider } from 'antd'
import { useMemo } from 'react'
@@ -130,6 +131,9 @@ const antdComponentThemeToken = {
}
}
// 注册 ECharts 主题
registerApiparkTheme()
function App() {
const { locale } = useLocaleContext()
useInitializeMonaco()
@@ -6,17 +6,18 @@ import { AiServiceRouterTableListItem, VariableItems } from './type'
import { PageProColumns } from '@common/components/aoplatform/PageList'
export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns<AiServiceRouterTableListItem>[] = [
{
title: '名称',
dataIndex: 'name',
width: 200,
ellipsis: true
},
{
title: 'URL',
dataIndex: 'requestPath',
ellipsis: true,
width: 200
},
{
title: '名称',
dataIndex: 'name',
ellipsis: true
},
{
title: '模型',
dataIndex: ['model', 'name'],
@@ -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[];
+31 -3
View File
@@ -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',
@@ -507,7 +535,7 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
],
[
'serviceHub',
'portal',
{
type: 'module',
component: <Outlet />,
@@ -674,12 +702,12 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
children: [
{
path: 'total',
key: 'analytics2',
key: 'analyticsTotal',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardTotal.tsx'))
},
{
path: ':dashboardType',
key: 'analytics3',
key: 'analyticsOther',
component: <Outlet />,
children: [
{
@@ -123,6 +123,7 @@ export type PartitionDataLogHeaderListFieldType = {
export type PartitionDataLogConfigFieldType = {
headers: PartitionDataLogHeaderListFieldType[]
url: string
driver?: string
}
export const PARTITION_DATA_LOG_CONFIG_TABLE_COLUMNS: PageProColumns<PartitionDataLogConfigFieldType & { _id: string }>[] = [
@@ -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,
@@ -241,6 +242,12 @@ export const MATCH_CONFIG: ConfigField<MatchItem>[] = [
]
export const SYSTEM_API_TABLE_COLUMNS: PageProColumns<SystemApiTableListItem>[] = [
{
title: '名称',
dataIndex: 'name',
width: 200,
ellipsis: true
},
{
title: 'URL',
dataIndex: 'requestPath',
@@ -500,3 +507,133 @@ 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',
copyable: false,
width: 180,
ellipsis: true
},
{
title: 'API / Tools',
dataIndex: ['api', 'name'],
ellipsis: true
},
{
title: '消费者',
dataIndex: ['consumer', 'name'],
ellipsis: true
},
{
title: 'HTTP 状态',
dataIndex: 'status',
ellipsis: true
},
{
title: 'IP',
dataIndex: 'ip',
copyable: true,
width: 140,
ellipsis: true
},
{
title: '响应时间',
dataIndex: 'responseTime',
width: 130,
ellipsis: true
},
{
title: '流量',
dataIndex: 'traffic',
ellipsis: true
}
]
/** AI 服务日志 */
export const AI_SERVICE_LOG_LIST: PageProColumns<LogItem>[] = [
{
title: '时间戳',
dataIndex: 'logTime',
copyable: false,
width: 200,
ellipsis: true
},
{
title: 'API / Tools',
dataIndex: ['api', 'name'],
ellipsis: true
},
{
title: '消费者',
dataIndex: ['consumer', 'name'],
ellipsis: true
},
{
title: 'HTTP 状态',
dataIndex: 'status',
ellipsis: true
},
{
title: '模型',
dataIndex: 'model',
ellipsis: true
},
{
title: 'IP',
dataIndex: 'ip',
copyable: true,
width: 140,
ellipsis: true
},
{
title: 'Token/s',
dataIndex: 'tokenPerSecond',
ellipsis: true
},
{
title: 'Token',
dataIndex: 'token',
ellipsis: true
}
]
@@ -97,6 +97,7 @@ export type SystemApiProxyType = {
export type SystemApiProxyFieldType = {
protocols: string[];
id:string;
name:string
description?:string;
disable:boolean;
path:string;
+23
View File
@@ -1156,9 +1156,32 @@ 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-container .ant-table-thead th{
background-color: #fff !important;
padding: 10px 10px !important;
}
.ranking-list .ant-table-container .ant-table-thead th::before{
display: none;
}
.ant-alert-info{
background: #1784FC1A !important;
}
.service-log-tab .ant-tabs .ant-tabs-nav .ant-tabs-tab{
padding-left: 0px;
padding-right: 0px;
}
.service-log-tab .ant-tabs .ant-tabs-tab+.ant-tabs-tab {
margin-left: 15px;
}
.monaco-editor .find-widget .monaco-inputbox.synthetic-focus{
outline-color: var(--primary-color) !important;
@@ -8,9 +8,8 @@ import { App, Button } from 'antd'
import { EntityItem } from '@common/const/type.ts'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
import { useNavigate, useParams } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { $t } from '@common/locales'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
const ServiceInsideDocument = () => {
const { message } = App.useApp()
const [updater, setUpdater] = useState<string>()
@@ -19,8 +18,6 @@ const ServiceInsideDocument = () => {
const [doc, setDoc] = useState<string>()
const { fetchData } = useFetch()
const { serviceId, teamId } = useParams<RouterParams>()
const { setBreadcrumb } = useBreadcrumb()
const navigator = useNavigate()
const save = () => {
fetchData<
@@ -80,15 +77,6 @@ const ServiceInsideDocument = () => {
}
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('使用说明')
}
])
getServiceDoc()
}, [])
@@ -9,11 +9,12 @@ 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'
import { useAiServiceContext } from '../../contexts/AiServiceContext.tsx'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
const APP_MODE = import.meta.env.VITE_APP_MODE
const AiServiceInsidePage: FC = () => {
@@ -27,6 +28,7 @@ const AiServiceInsidePage: FC = () => {
const [activeMenu, setActiveMenu] = useState<string>()
const navigateTo = useNavigate()
const [showMenu, setShowMenu] = useState<boolean>(false)
const { setBreadcrumb } = useBreadcrumb()
const getAiServiceInfo = () => {
fetchData<BasicResponse<{ service: AiServiceConfigFieldType }>>('service/info', {
@@ -67,6 +69,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 +152,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 +206,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])
@@ -213,10 +217,19 @@ const AiServiceInsidePage: FC = () => {
}, [accessData])
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigateTo('/service/list')
},
{
title: aiServiceInfo?.name || ''
}
])
if (activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]) {
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${activeMenu}`)
}
}, [activeMenu])
}, [activeMenu, state.language, aiServiceInfo])
useEffect(() => {
serviceId && getAiServiceInfo()
@@ -231,17 +244,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
@@ -1,8 +1,7 @@
import {ActionType} from "@ant-design/pro-components";
import {FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react";
import {Link, useNavigate, useParams} from "react-router-dom";
import {useParams} from "react-router-dom";
import {App, Form,TreeSelect} from "antd";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE} from "@common/const/const.tsx";
@@ -18,7 +17,6 @@ import { checkAccess } from "@common/utils/permission.ts";
import { $t } from "@common/locales/index.ts";
const AiServiceInsideSubscriber:FC = ()=>{
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
const {fetchData} = useFetch()
const {serviceId, teamId} = useParams<RouterParams>()
@@ -26,7 +24,6 @@ const AiServiceInsideSubscriber:FC = ()=>{
const pageListRef = useRef<ActionType>(null);
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
const {accessData,state} = useGlobalContext()
const navigator = useNavigate()
const getAiServiceSubscriber = ()=>{
return fetchData<BasicResponse<{subscribers:AiServiceSubscriberTableListItem[]}>>('service/subscribers',{method:'GET',eoParams:{service:serviceId,team:teamId},eoTransformKeys:['apply_time']}).then(response=>{
const {code,data,msg} = response
@@ -120,15 +117,6 @@ const AiServiceInsideSubscriber:FC = ()=>{
]
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title:$t('订阅方管理')
}
])
getMemberList()
manualReloadTable()
}, [serviceId]);
@@ -6,33 +6,21 @@ import { LoadingOutlined } from '@ant-design/icons'
import EmptySVG from '@common/assets/empty.svg'
import { $t } from '@common/locales/index.ts'
import ApiDocument from '@common/components/aoplatform/ApiDocument.tsx'
import { useNavigate, useParams } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
import {
AiServiceInsideApiDocumentHandle,
AiServiceInsideApiDocumentProps,
AiServiceApiDetail
} from '@core/const/ai-service/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
const AiServiceInsideApiDocument = forwardRef<AiServiceInsideApiDocumentHandle, AiServiceInsideApiDocumentProps>(() => {
const { serviceId, teamId } = useParams<RouterParams>()
const { fetchData } = useFetch()
const [apiDetail, setApiDetail] = useState<AiServiceApiDetail>()
const [loading, setLoading] = useState<boolean>(false)
const { setBreadcrumb } = useBreadcrumb()
const navigator = useNavigate()
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('API 文档')
}
])
getApiDetail()
}, [])
@@ -291,19 +291,6 @@ const AiServiceInsideRouterCreate = () => {
}
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title:$t('API'),
onClick: () => navigator(backUrl)
},
{
title: routeId ? $t('编辑 API') : $t('添加 API')
}
])
!routeId && aiServiceInfo?.provider && getDefaultModelConfig()
}, [aiServiceInfo])
@@ -3,7 +3,6 @@ import PageList, { PageProColumns } from '@common/components/aoplatform/PageList
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { SimpleMemberItem } from '@common/const/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales/index.ts'
@@ -17,7 +16,6 @@ import { Link, useNavigate, useParams } from 'react-router-dom'
const AiServiceInsideRouterList: FC = () => {
const [searchWord, setSearchWord] = useState<string>('')
const { setBreadcrumb } = useBreadcrumb()
const { modal, message } = App.useApp()
const [tableListDataSource, setTableListDataSource] = useState<AiServiceRouterTableListItem[]>([])
const [tableHttpReload, setTableHttpReload] = useState(true)
@@ -162,17 +160,6 @@ const AiServiceInsideRouterList: FC = () => {
getMemberList()
manualReloadTable()
}, [serviceId])
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('路由')
}
])
}, [state.language])
const columns = useMemo(() => {
return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map((x) => {
@@ -3,7 +3,6 @@ import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {Link, useLocation, useNavigate, useParams} from "react-router-dom";
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {App, Button} from "antd";
import {
SUBSCRIBE_APPROVAL_INNER_DONE_TABLE_COLUMN,
@@ -26,7 +25,6 @@ import { SubscribeApprovalInfoType } from "@common/const/approval/type.tsx";
import { $t } from "@common/locales";
const AiServiceInsideApprovalList:FC = ()=>{
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
const {serviceId, teamId} = useParams<RouterParams>();
const [init, setInit] = useState<boolean>(true)
@@ -40,7 +38,6 @@ const AiServiceInsideApprovalList:FC = ()=>{
const [approvalBtnLoading,setApprovalBtnLoading] = useState<boolean>(false)
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
const {accessData,state} = useGlobalContext()
const navigator = useNavigate()
const openModal = async (type:'approval'|'view',entity:SubscribeApprovalTableListItem)=>{
message.loading($t(RESPONSE_TIPS.loading))
@@ -142,15 +139,6 @@ const AiServiceInsideApprovalList:FC = ()=>{
}, [query]);
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title:$t('订阅审核')
}
])
getMemberList()
manualReloadTable()
}, [serviceId]);
@@ -2,13 +2,11 @@
import { Tabs } from "antd"
import { useState, useEffect, FC, useMemo } from "react"
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"
import { SYSTEM_PUBLISH_TAB_ITEMS } from "../../../const/system/const"
import { $t } from "@common/locales"
import { useGlobalContext } from "@common/contexts/GlobalStateContext"
const AiServiceInsidePublic:FC = ()=>{
const { setBreadcrumb } = useBreadcrumb()
const query =new URLSearchParams(useLocation().search)
const location = useLocation()
const currentUrl = location.pathname
@@ -25,18 +23,6 @@ const AiServiceInsidePublic:FC = ()=>{
setPageStatus(Number(query.get('status') ||0) as 0|1)
}, [currentUrl]);
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigateTo('/service/list')
},
{
title:$t('发布')
}
])
}, []);
const tabItems = useMemo(()=>SYSTEM_PUBLISH_TAB_ITEMS?.map((x)=>({...x, label:$t(x.label as string) })),[state.language])
return (
<>
@@ -9,7 +9,6 @@ import { PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN, PUBLISH_APPROVAL_VERSION_IN
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
import { SimpleMemberItem } from "@common/const/type.ts";
import { MemberTableListItem } from "../../../const/member/type";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext";
import { useFetch } from "@common/hooks/http";
import WithPermission from "@common/components/aoplatform/WithPermission";
import { AiServicePublishReleaseItem } from "../../../const/system/type";
@@ -23,7 +22,6 @@ import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter
import { $t } from "@common/locales";
const AiServiceInsidePublicList:FC = ()=>{
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
const pageListRef = useRef<ActionType>(null);
const [tableHttpReload, setTableHttpReload] = useState(true);
@@ -45,7 +43,6 @@ const AiServiceInsidePublicList:FC = ()=>{
const [drawerData, setDrawerData] = useState<PublishTableListItem|PublishVersionTableListItem >({} as PublishTableListItem)
const [drawerOkTitle, setDrawerOkTitle] = useState<string>('确认')
const [isOkToPublish, setIsOkToPublish] = useState<boolean>(false)
const navigator = useNavigate()
const getAiServicePublishList = (params?: ParamsType & {
pageSize?: number | undefined;
current?: number | undefined;
@@ -351,15 +348,6 @@ const AiServiceInsidePublicList:FC = ()=>{
]
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('发布')
}
])
getMemberList()
manualReloadTable()
}, [serviceId]);
@@ -39,6 +39,7 @@ const ApiKeyContent: React.FC<ApiKeyContentProps> = forwardRef(({ provider, enti
const handleOk = async () => {
try {
// 表单校验
const values = await form.validateFields()
const { expire_time, ...restValues } = values
const expireTime = neverExpire ? 0 : Math.trunc(expire_time.valueOf() / 1000)
@@ -68,7 +68,7 @@ const LogSettings = () => {
<>
<Skeleton className="m-btnbase w-calc-100vw-minus-padding-r" active loading={loading}>
<InsidePage
pageTitle={$t('日志置')}
pageTitle={$t('日志输出设置')}
description={'APIPark ' + $t('提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。')}
>
<div className="flex h-full">
@@ -1,11 +1,15 @@
import EditableTable from "@common/components/aoplatform/EditableTable"
import WithPermission from "@common/components/aoplatform/WithPermission"
import { BasicResponse, PLACEHOLDER, STATUS_CODE } from "@common/const/const"
import { useFetch } from "@common/hooks/http"
import { $t } from "@common/locales"
import { PARTITION_DATA_LOG_CONFIG_TABLE_COLUMNS, PartitionDataLogConfigFieldType, PartitionDataLogHeaderListFieldType } from "@core/const/partitions/types"
import { Button, Form, Input, message } from "antd"
import { useEffect } from "react"
import EditableTable from '@common/components/aoplatform/EditableTable'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { BasicResponse, PLACEHOLDER, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import {
PARTITION_DATA_LOG_CONFIG_TABLE_COLUMNS,
PartitionDataLogConfigFieldType,
PartitionDataLogHeaderListFieldType
} from '@core/const/partitions/types'
import { Button, Form, Input, message, Select } from 'antd'
import { useEffect } from 'react'
export type DashboardPageShowStatus = 'view' | 'edit'
export type DashboardSettingEditProps = {
@@ -15,7 +19,7 @@ export type DashboardSettingEditProps = {
}
const DataLogSettingEdit = (props: DashboardSettingEditProps) => {
const { changeStatus, refreshData, data } = props
const [form] = Form.useForm();
const [form] = Form.useForm()
const { fetchData } = useFetch()
const onFinish = () => {
@@ -23,10 +27,16 @@ const DataLogSettingEdit = (props: DashboardSettingEditProps) => {
const formData = {
config: {
url: value.url,
headers: value.headers.filter((item: PartitionDataLogHeaderListFieldType) => item.key).map((item: PartitionDataLogHeaderListFieldType) => ({key:item.key, value:item.value || ''}))
headers: value.headers
.filter((item: PartitionDataLogHeaderListFieldType) => item.key)
.map((item: PartitionDataLogHeaderListFieldType) => ({ key: item.key, value: item.value || '' }))
}
}
fetchData<BasicResponse<{ info: PartitionDataLogConfigFieldType }>>('log/loki', { method: 'POST', body: JSON.stringify(formData), eoParams: {} }).then(response => {
fetchData<BasicResponse<{ info: PartitionDataLogConfigFieldType }>>('log/loki', {
method: 'POST',
body: JSON.stringify(formData),
eoParams: {}
}).then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t('操作成功,即将刷新页面'))
@@ -38,15 +48,26 @@ const DataLogSettingEdit = (props: DashboardSettingEditProps) => {
})
}
useEffect(() => { form.setFieldsValue(data) }, [data])
useEffect(() => {
form.setFieldsValue({
...data,
headers: data?.headers?.length ? data.headers : [
{
key: '',
value: ''
}
],
driver: 'loki'
})
}, [data])
useEffect(() => {
return (form.setFieldsValue({}))
}, []);
return form.setFieldsValue({})
}, [])
return (
<>
<div className="overflow-auto h-full">
<WithPermission access={''} >
<WithPermission access={''}>
<Form
form={form}
className="mx-auto flex flex-col justify-between h-full"
@@ -55,23 +76,29 @@ const DataLogSettingEdit = (props: DashboardSettingEditProps) => {
autoComplete="off"
>
<Form.Item<PartitionDataLogConfigFieldType>
label={$t("请求前缀")}
name="url"
label={$t('数据源类型')}
name="driver"
rules={[{ required: true }]}
>
<Select
showSearch
optionFilterProp="label"
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
options={[{ label: 'Loki', value: 'loki' }]}
></Select>
</Form.Item>
<Form.Item<PartitionDataLogConfigFieldType> label={$t('请求前缀')} name="url" rules={[{ required: true }]}>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item<PartitionDataLogConfigFieldType>
label={$t("HTTP 头部")}
name="headers"
>
<Form.Item<PartitionDataLogConfigFieldType> label={$t('HTTP 头部')} name="headers">
<EditableTable<PartitionDataLogConfigFieldType & { _id: string }>
configFields={PARTITION_DATA_LOG_CONFIG_TABLE_COLUMNS}
/>
</Form.Item>
<div className="flex gap-btnbase">
<WithPermission access='system.devops.data_source.edit'>
<WithPermission access="system.devops.data_source.edit">
<Button type="primary" htmlType="submit">
{$t('保存')}
</Button>
@@ -84,7 +111,7 @@ const DataLogSettingEdit = (props: DashboardSettingEditProps) => {
</WithPermission>
</div>
</>
);
)
}
export default DataLogSettingEdit;
export default DataLogSettingEdit
@@ -161,7 +161,7 @@ const PartitionInsideDashboardSetting: FC = () => {
className="overflow-hidden mt-[30px] w-full max-h-full flex flex-col justify-between"
title={
<div>
<span className="text-MAIN_TEXT my-btnybase mr-btnbase"> {$t('数据日志')}</span>
<span className="text-MAIN_TEXT my-btnybase mr-btnbase"> {$t('请求日志')}</span>
{!dataLogLoading && !dataLogData && <Tag color="#f50">{$t('未配置')}</Tag>}
</div>
}
@@ -220,6 +220,11 @@ export function DataLogConfigPreview(x: PartitionDataLogConfigFieldType) {
return (
<div className="flex flex-col gap-[4px] ">
<Row className="">
<Col className="font-bold text-right pr-[4px]">{$t('数据源')}</Col>
{/* 先写死,或许会有选择列表,但现在可以不用 */}
<Col>Loki</Col>
</Row>
<Row className="">
<Col className="font-bold text-right pr-[4px]">{$t('请求前缀')}</Col>
<Col>{x?.url}</Col>
@@ -1,25 +1,13 @@
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
import { useEffect } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import { $t } from '@common/locales'
export default function ServicePolicyLayout() {
const location = useLocation()
const pathName = location.pathname
const navigator = useNavigate()
const { setBreadcrumb } = useBreadcrumb()
useEffect(() => {
const tmpPath = pathName.split('/')
if (tmpPath[tmpPath.length - 1] === 'servicepolicy') {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('服务策略')
}
])
navigator('datamasking/list')
}
}, [pathName])
@@ -80,19 +80,6 @@ const DataMaskingConfig = forwardRef<DataMaskingConfigHandle>((_,ref) => {
};
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title:$t('服务策略'),
onClick: () => navigator(serviceId ? `/service/${teamId}/aiInside/${serviceId}/servicepolicy` : '')
},
{
title: policyId !== undefined ? $t('编辑服务策略') : $t('添加服务策略')
}
])
if (policyId !== undefined) {
setOnEdit(true);
getPolicyInfo();
@@ -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).filter((item) => !!configContent[item]).map((item) => {
return (
<div className="overflow-auto mb-[15px]">
<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,362 @@
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 = String(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']
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const result = data.log
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
})
setResponseInfoData({
Header: result.response.header,
Body: result.response.body
})
} 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']
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const result = data.log
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,
Body: result.request.body
})
setResponseInfoData({
Header: result.response.header,
Body: result.response.body
})
} 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] service-log-tab">
<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,263 @@
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, ParamsType } 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>('day')
/** 全局状态 */
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 = (
params: ParamsType & {
pageSize?: number | undefined
current?: number | undefined
keyword?: string | undefined
}
) => {
return fetchData<BasicResponse<{ log: LogItem[] }>>(`service/logs/ai`, {
method: 'GET',
eoParams: {
service: serviceId,
team: teamId,
start: timeRange?.start,
end: timeRange?.end,
page: params?.current,
page_size:params?.pageSize
},
eoTransformKeys: ['log_time', 'response_time', 'token_per_second']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 保存数据
return {
data: data.logs,
total: data.total,
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 = (
params: ParamsType & {
pageSize?: number | undefined
current?: number | undefined
keyword?: string | undefined
}
) => {
console.log('params===', params)
return fetchData<BasicResponse<{ log: LogItem[] }>>(`service/logs/rest`, {
method: 'GET',
eoParams: {
service: serviceId,
team: teamId,
start: timeRange?.start,
end: timeRange?.end,
page: params?.current,
page_size:params?.pageSize
},
eoTransformKeys: ['log_time', 'response_time', 'token_per_second']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 保存数据
return {
data: data.logs,
total: data.total,
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] overflow-hidden"
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="h-full mr-PAGE_INSIDE_X">
<DateSelectFilter selectCallback={selectCallback} customClassNames={'pt-[0px]'} defaultTime={defaultTime} />
<div style={{ height: 'calc(100% - -3px)' }} className="mt-[20px] overflow-hidden">
<PageList
ref={pageListRef}
id={`${serviceType}_logs`}
columns={[...columns]}
request={async (
params: ParamsType & {
pageSize?: number | undefined
current?: number | undefined
keyword?: string | undefined
}
) => (serviceType === 'aiService' ? getAiServiceLogList(params) : getRestServiceLogList(params))}
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,278 @@
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[]
max: string
min: string
originValue?: number
showXAxis?: boolean
}
type ServiceAreaCharProps = {
customClassNames?: string
dataInfo?: AreaChartInfo
height?: number
showAvgLine?: boolean
customMarkLineValue?: number
}
const ServiceAreaChart = ({ customClassNames, dataInfo, height, showAvgLine, customMarkLineValue }: ServiceAreaCharProps) => {
const chartRef = useRef<ECharts>(null)
const [option, setOption] = useState<EChartsOption | undefined>({})
const [hasData, setHasData] = useState(true)
const setChartOption = (dataInfo: AreaChartInfo) => {
const dataExists = dataInfo.data && dataInfo.data.length > 0
// 更新hasData状态
setHasData(dataExists)
const option = {
tooltip: dataExists ? {
trigger: 'axis',
formatter: function (value: any) {
// 如果是数组,取第一个参数的name
const param = Array.isArray(value) ? value[0] : value
let tooltipContent = `<div style="min-width:140px;padding:8px;">`
const marker = `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${param.color};"></span>`
tooltipContent += `<div style="margin-top: 8px; display: flex; justify-content: space-between; align-items: center;">
<div style="margin-right: 4px;">${marker}</div>${param.name} <div style="font-weight:bold; margin-left: 20px;">${param.value}</div>
</div>`
tooltipContent += '</div>'
return tooltipContent
}
} : {
show: false // 没有数据时不显示tooltip
},
title: [
{
text: '{titleStyle|' + $t(dataInfo.title) + '}\n\n{valueStyle|' + dataInfo.value + '}',
left: '2%',
top: '0',
textStyle: {
rich: {
titleStyle: {
fontSize: 14,
color: '#999999',
fontWeight: 'normal',
lineHeight: 20
},
valueStyle: {
fontSize: 32,
color: '#101010',
fontWeight: 500,
lineHeight: 40
}
}
}
}
],
toolbox: {
show: false
},
grid: {
left: '3%',
right: '3%',
bottom: '0%',
top: '110px',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dataInfo.date,
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#ccc'
}
},
show: false
},
yAxis: {
type: 'value',
boundaryGap: [0, '5%'],
show: dataExists, // 没有数据时不显示Y轴
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: false
}
},
// 添加数据缩放组件,实现鼠标放大缩小,后续可能需要
// dataZoom: [
// {
// type: 'inside', // 内置的数据区域缩放组件(使用鼠标滚轮缩放)
// xAxisIndex: 0, // 设置缩放作用在第一个x轴
// filterMode: 'filter',
// start: 0,
// end: 100
// },
// {
// type: 'slider', // 滑动条型数据区域缩放组件
// xAxisIndex: 0,
// filterMode: 'filter',
// height: 20,
// bottom: 0,
// start: 0,
// end: 100,
// handleIcon:
// 'path://M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
// handleSize: '80%',
// handleStyle: {
// color: '#fff',
// shadowBlur: 3,
// shadowColor: 'rgba(0, 0, 0, 0.6)',
// shadowOffsetX: 2,
// shadowOffsetY: 2
// },
// show: false // 默认隐藏底部的滑动条,可以改为 true 显示
// }
// ],
// 添加空状态提示
silent: !dataExists,
graphic: !dataExists
? [
{
type: 'text',
left: 'center',
top: 'middle',
style: {
text: $t('暂无数据'),
fontSize: 14,
fill: '#999'
}
}
]
: [],
series: [
{
name: dataInfo.title,
type: 'line',
symbol: 'none',
sampling: 'lttb',
itemStyle: {
color: 'rgb(255, 70, 131)'
},
markLine: showAvgLine ? {
silent: false,
symbol: 'none',
lineStyle: {
width: 1,
type: 'dashed'
},
label: {
show: false,
position: 'insideEndTop',
formatter: '{c}',
color: '#000',
fontSize: 10,
backgroundColor: 'transparent',
padding: [10, 4],
borderRadius: 2,
distance: -5
},
emphasis: {
lineStyle: {
width: 1 // 保持线条宽度不变,禁用默认的悬停加粗
},
label: {
show: false // 悬停时不显示标签
}
},
data: dataInfo?.originValue !== undefined ?
[{ yAxis: dataInfo?.originValue, name: '自定义值' }] :
[{ type: 'average', name: 'Avg' }]
} : undefined,
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
}
]
}
const echartsInstance = chartRef.current?.getEchartsInstance()
if (echartsInstance) {
echartsInstance.setOption(option)
}
}
// 使用深度监听来确保图表数据更新
useEffect(() => {
if (!dataInfo) return
// 直接获取 ECharts 实例并设置选项
const echartsInstance = chartRef.current?.getEchartsInstance()
if (echartsInstance) {
// 清除已有的图表
echartsInstance.clear()
// 重新设置选项
setChartOption(dataInfo)
}
}, [dataInfo, JSON.stringify(dataInfo)])
// 添加窗口大小变化监听,实现自适应
useEffect(() => {
// 定义resize处理函数
const handleResize = () => {
const echartsInstance = chartRef.current?.getEchartsInstance()
if (echartsInstance) {
echartsInstance.resize()
}
}
// 添加监听
window.addEventListener('resize', handleResize)
// 组件卸载时移除监听
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return (
<div className={`w-full ${customClassNames}`}>
<div className="absolute top-[10px] left-[10px] w-full">
<div className="relative top-[5px]">
<div className="absolute top-[23px] right-[5%] grid grid-cols-[auto_auto] justify-items-end">
<div className="flex justify-center items-center">
<span className="text-[#FE564D] text-[9px]"></span>
</div>
<span className="ml-1 text-right">{dataInfo?.max}</span>
<div className="flex justify-center items-center">
<span className="text-[#27B148] text-[9px]"></span>
</div>
<span className="ml-1 text-right">{dataInfo?.min}</span>
</div>
</div>
</div>
<div style={!hasData ? { cursor: 'default', pointerEvents: 'none' } : {}}>
<ECharts ref={chartRef} option={option} theme="apipark" style={{ height: height || 400 }} opts={{ renderer: 'svg' }} />
</div>
</div>
)
}
export default ServiceAreaChart
@@ -0,0 +1,395 @@
import ECharts, { EChartsOption } from 'echarts-for-react'
import { useEffect, useRef, useState } from 'react'
import { $t } from '@common/locales/index.ts'
import { chartColors, defaultColor } from '@common/const/charts/theme'
export type BarChartInfo = {
title: string
value: string
date: string[]
data: {
name: string
color: string
value: number[]
}[]
showXAxis?: boolean
inputTokenTotal?: string
outputTokenTotal?: string
request2xxTotal?: string
request4xxTotal?: string
request5xxTotal?: string
traffic2xxTotal?: string
traffic4xxTotal?: string
traffic5xxTotal?: string
max?: string | number
min?: string | number
}
type ServiceBarCharProps = {
customClassNames?: string
dataInfo?: BarChartInfo
height?: number
showAvgLine?: boolean
showLegendIndicator?: boolean
hideIndicatorValue?: boolean
}
const ServiceBarChar = ({
customClassNames,
dataInfo,
height,
showAvgLine,
showLegendIndicator,
hideIndicatorValue
}: ServiceBarCharProps) => {
const chartRef = useRef<ECharts>(null)
const [option, setOption] = useState<EChartsOption | undefined>({})
// 使用从主题配置中导入的默认颜色,而不是硬编码的颜色值
const [detaultColor] = useState(defaultColor)
const [hasData, setHasData] = useState(true)
const tokenMap = {
inputToken: $t('输入 Token'),
outputToken: $t('输出 Token')
}
const setChartOption = (dataInfo: BarChartInfo) => {
const isNumberArray = typeof dataInfo.data[0] !== 'object'
const legendData = isNumberArray ? [dataInfo.title] : dataInfo.data.map((item) => item.name)
const dataExists = dataInfo.data && dataInfo.data.length > 0
// 更新hasData状态
setHasData(dataExists)
const tooltipFormatter = (params: { name: string; color: string; seriesIndex?: number }) => {
let tooltipContent = `<div style="min-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 ? item.color : index < chartColors.length ? chartColors[index] : detaultColor
const name = tokenMap[item.name as keyof typeof tokenMap] || 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; display: flex; justify-content: space-between; align-items: center;">
<div>${marker} ${name}</div> <div style="font-weight:bold; margin-left: 20px;">${value}</div>
</div>`
})
tooltipContent += '</div>'
return tooltipContent
}
const option: EChartsOption = {
title: [
{
text:
'{titleStyle|' +
$t(dataInfo.title) +
`}${hideIndicatorValue ? '' : '\n\n{valueStyle|' + dataInfo.value + '}'}`,
left: '2%',
top: '0',
textStyle: {
rich: {
titleStyle: {
fontSize: 14,
color: '#999999',
fontWeight: 'normal',
lineHeight: 20
},
valueStyle: {
fontSize: 32,
color: '#101010',
fontWeight: 500,
lineHeight: 40
}
}
}
}
],
grid: {
left: '3%',
right: '3%',
bottom: '0%',
top: '110px',
containLabel: true
},
tooltip: dataExists
? {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params: any) {
// 如果是数组,取第一个参数的name
const param = Array.isArray(params) ? params[0] : params
return tooltipFormatter(param)
}
}
: {
show: false // 没有数据时不显示tooltip
},
legend: {
show: !isNumberArray,
data: legendData,
right: '10px',
top: hideIndicatorValue ? '10px' : '60px',
itemWidth: 10,
itemHeight: 10,
textStyle: {
color: '#333'
},
icon: 'rect',
formatter: function (name: string): string {
// 这里可以映射或自定义图例文本
const customNames: Record<string, string> = {
inputToken: `${$t('输入 Token')} ${showLegendIndicator ? `(${dataInfo.inputTokenTotal})` : ''}`,
outputToken: `${$t('输出 Token')} ${showLegendIndicator ? `(${dataInfo.outputTokenTotal})` : ''}`,
'2xx': `${'2xx'} ${showLegendIndicator ? `(${dataInfo.request2xxTotal || dataInfo.traffic2xxTotal})` : ''}`,
'4xx': `${'4xx'} ${showLegendIndicator ? `(${dataInfo.request4xxTotal || dataInfo.traffic4xxTotal})` : ''}`,
'5xx': `${'5xx'} ${showLegendIndicator ? `(${dataInfo.request5xxTotal || dataInfo.traffic5xxTotal})` : ''}`
}
return customNames[name] || name
}
},
xAxis: {
type: 'category',
data: dataInfo.date,
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#ccc'
}
},
show: false
},
yAxis: {
type: 'value',
name: '',
min: 0,
...(showAvgLine ? {} : { minInterval: 1 }),
show: dataExists, // 没有数据时不显示Y轴
splitLine: {
show: dataExists, // 没有数据时不显示网格线
lineStyle: {
type: 'dashed',
color: '#eee'
}
},
axisLabel: {
formatter: '{value}'
}
},
// 添加数据缩放组件,实现鼠标放大缩小,后续可能需要
// dataZoom: [
// {
// type: 'inside', // 内置的数据区域缩放组件(使用鼠标滚轮缩放)
// xAxisIndex: 0, // 设置缩放作用在第一个x轴
// filterMode: 'filter',
// start: 0,
// end: 100
// },
// {
// type: 'slider', // 滑动条型数据区域缩放组件
// xAxisIndex: 0,
// filterMode: 'filter',
// height: 20,
// bottom: 0,
// start: 0,
// end: 100,
// handleIcon: 'path://M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
// handleSize: '80%',
// handleStyle: {
// color: '#fff',
// shadowBlur: 3,
// shadowColor: 'rgba(0, 0, 0, 0.6)',
// shadowOffsetX: 2,
// shadowOffsetY: 2
// },
// show: false // 默认隐藏底部的滑动条,可以改为 true 显示
// }
// ],
// 添加空状态提示
silent: !dataExists,
graphic: !dataExists
? [
{
type: 'text',
left: 'center',
top: 'middle',
style: {
text: $t('暂无数据'),
fontSize: 14,
fill: '#999'
}
}
]
: [],
series: isNumberArray
? [
{
name: dataInfo.title,
type: 'bar',
stack: '总量',
emphasis: {
focus: 'series'
},
itemStyle: {
color: detaultColor
},
markLine: showAvgLine
? {
silent: false,
symbol: 'none',
lineStyle: {
width: 1,
type: 'dashed'
},
label: {
show: false,
position: 'insideEndTop',
formatter: '{c}',
color: '#000',
fontSize: 10,
backgroundColor: 'transparent',
padding: [10, 4],
borderRadius: 2,
distance: -5
},
emphasis: {
lineStyle: {
width: 1 // 保持线条宽度不变,禁用默认的悬停加粗
},
label: {
show: false // 悬停时不显示标签
}
},
data: [{ type: 'average', name: 'Avg' }]
}
: undefined,
data: dataInfo.data
}
]
: dataInfo.data.map((item, index) => ({
name: item.name,
type: 'bar',
stack: '总量',
markLine: showAvgLine
? {
silent: false,
symbol: 'none',
lineStyle: {
width: 1,
type: 'dashed'
},
label: {
show: false,
position: 'insideEndTop',
formatter: '{c}',
color: '#000',
fontSize: 10,
backgroundColor: 'transparent',
padding: [10, 4],
borderRadius: 2,
distance: -5
},
emphasis: {
lineStyle: {
width: 1 // 保持线条宽度不变,禁用默认的悬停加粗
},
label: {
show: false // 悬停时不显示标签
}
},
data: [{ type: 'average', name: 'Avg' }]
}
: undefined,
emphasis: {
focus: 'series'
},
itemStyle: {
// 使用主题中的颜色列表,如果索引超出范围则使用项目自带的颜色
color: item.color ? item.color : index < chartColors.length ? chartColors[index] : detaultColor
},
data: item.value
}))
}
const echartsInstance = chartRef.current?.getEchartsInstance()
if (echartsInstance) {
echartsInstance.setOption(option)
}
}
// 使用深度监听来确保图表数据更新
useEffect(() => {
if (!dataInfo) return
// 直接获取 ECharts 实例并设置选项
const echartsInstance = chartRef.current?.getEchartsInstance()
if (echartsInstance) {
// 清除已有的图表
echartsInstance.clear()
// 重新设置选项
setChartOption(dataInfo)
}
}, [dataInfo, JSON.stringify(dataInfo)])
// 添加窗口大小变化监听,实现自适应
useEffect(() => {
// 定义resize处理函数
const handleResize = () => {
const echartsInstance = chartRef.current?.getEchartsInstance()
if (echartsInstance) {
echartsInstance.resize()
}
}
// 添加监听
window.addEventListener('resize', handleResize)
// 组件卸载时移除监听
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return (
<div className={`w-full ${customClassNames}`}>
{
hideIndicatorValue && (
<div className="absolute top-[26px] left-[10px] w-full">
<div className="relative top-[5px]">
<div className="absolute top-[23px] right-[5%] grid grid-cols-[auto_auto] justify-items-end">
<div className="flex justify-center items-center">
<span className="text-[#FE564D] text-[9px]"></span>
</div>
<span className="ml-1 text-right">{dataInfo?.max}</span>
<div className="flex justify-center items-center">
<span className="text-[#27B148] text-[9px]"></span>
</div>
<span className="ml-1 text-right">{dataInfo?.min}</span>
</div>
</div>
</div>
)
}
<div style={!hasData ? { cursor: 'default', pointerEvents: 'none' } : {}}>
<ECharts
ref={chartRef}
option={option}
style={{ height: height || 400 }}
opts={{ renderer: 'svg' }}
theme="apipark" // 这里应用主题名称,需要先在应用入口注册
/>
</div>
</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,88 @@
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 = () => {
const side = indicatorInfo?.serviceKind === 'ai' ? 'aiInside' : 'inside'
setIndicator([
{
title: indicatorInfo?.enableMcp ? 'APIs / Tools' : 'APIs',
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/route`,
content: indicatorInfo?.apiNum ?? 0
},
{
title: $t('订阅数量'),
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/subscriber`,
content: indicatorInfo?.subscriberNum ?? 0
},
{
title: 'MCP',
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/setting`,
content: (
<>
<Button
color={indicatorInfo?.enableMcp ? 'green' : 'primary'}
className="w-full rounded-[10px]"
variant="outlined"
onClick={(e) => {
e.stopPropagation()
navigateTo(`/service/${indicatorInfo?.teamId}/${side}/${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 rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{
body: 'py-[20px] px-[18px]'
}}
onClick={() => {
if (item.link) {
navigateTo(item.link)
}
}}
>
<div className="text-[14px] text-[#999999] mb-[10px]" style={{ fontFamily: 'Microsoft YaHei' }}>
{item.title}
{item.link && <Icon icon="uiw:right" width="16" height="16" className="absolute top-[14px] right-[14px]" />}
</div>
<div className={`${index < 2 ? 'text-[32px] font-medium text-[#101010]' : 'block mt-[30px]'}`} style={{ fontFamily: 'Microsoft YaHei' }}>
{item.content}
</div>
</Card>
))}
</div>
)
}
export default Indicator
@@ -0,0 +1,92 @@
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 min-w-[430px] h-fit rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{
body: 'p-[15px]'
}}
>
<div className="mb-[10px]">
<span className="text-[14px] text-[#999999]" style={{ fontFamily: 'Microsoft YaHei' }}>{item === 'TOP API' ? $t('API 使用排名') : $t('消费者使用排名')}</span>
</div>
<PageList
id={item}
columns={[...columns]}
minVirtualHeight={430}
noScroll
request={() => getTableData(item)}
showPagination={false}
tableClass="ranking-list"
ref={ref => {
if (ref) tableRefs.current[item] = ref;
}}
/>
</Card>
))}
</div>
)
}
export default RankingList
@@ -0,0 +1,428 @@
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 { abbreviateFloat, formatBytes, formatDuration, formatNumberWithUnit, 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>('day')
/** 当前选中的时间范围 */
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',
'max_token',
'min_token',
'avg_request_per_subscriber',
'avg_token_per_subscriber',
'input_token',
'output_token',
'total_token',
'request_2xx_total',
'request_4xx_total',
'request_5xx_total',
'input_token_total',
'output_token_total',
'max_token_per_subscriber',
'min_token_per_subscriber',
'max_request_per_subscriber',
'min_request_per_subscriber'
]
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 存储 AI 服务数据
setAiServiceOverview(data.overview)
// 设置 AI 报表数据
setAiChartInfoData(data.overview)
} 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,
serviceKind: serviceOverview.serviceKind,
serviceId: serviceId
})
// 设置总数数据
setBarChartInfo([
// 服务请求次数
{
...setBarChartInfoData({
title: $t('请求次数'),
data: serviceOverview.requestOverview,
value: formatNumberWithUnit(serviceOverview.requestTotal),
date: serviceOverview.date
}),
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
},
// 流量消耗总数
{
...setBarChartInfoData({
title: $t('网络流量'),
data: serviceOverview.trafficOverview,
value: formatBytes(serviceOverview.trafficTotal),
date: serviceOverview.date
}),
traffic2xxTotal: formatBytes(serviceOverview.traffic2xxTotal),
traffic4xxTotal: formatBytes(serviceOverview.traffic4xxTotal),
traffic5xxTotal: formatBytes(serviceOverview.traffic5xxTotal)
}
])
// 设置平均值数据
setPerBarChartInfo([
// 各个模型使用量
{
title: $t('平均响应时间'),
data: serviceOverview.avgResponseTimeOverview,
value: formatDuration(serviceOverview.avgResponseTime),
originValue: serviceOverview.avgResponseTime,
date: serviceOverview.date,
max: formatDuration(serviceOverview.maxResponseTime),
min: formatDuration(serviceOverview.minResponseTime),
type: 'area',
showXAxis: false
},
// 平均请求
{
...setBarChartInfoData({
title: $t('平均每消费者的请求次数'),
data: serviceOverview.avgRequestPerSubscriberOverview,
date: serviceOverview.date,
showXAxis: false
}),
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
},
// 平均流量消耗
{
...setBarChartInfoData({
title: $t('平均每消费者的网络流量'),
data: serviceOverview.avgTrafficPerSubscriberOverview,
date: serviceOverview.date,
showXAxis: false
}),
max: formatBytes(serviceOverview.maxTrafficPerSubscriber),
min: formatBytes(serviceOverview.minTrafficPerSubscriber)
}
])
}
/**
* AI
* */
const setAiChartInfoData = (serviceOverview: any) => {
// 设置指标数据
setIndicatorInfo({
apiNum: serviceOverview.apiNum,
subscriberNum: serviceOverview.subscriberNum,
teamId: teamId,
enableMcp: serviceOverview.enableMcp,
serviceKind: serviceOverview.serviceKind,
serviceId: serviceId
})
// 设置总数数据
setBarChartInfo([
// 服务请求次数
{
...setBarChartInfoData({
title: $t('请求次数'),
data: serviceOverview.requestOverview,
value: formatNumberWithUnit(serviceOverview.requestTotal),
date: serviceOverview.date
}),
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
},
// token 消耗总数
{
...setBarChartInfoData({
title: $t('Token 消耗'),
data: serviceOverview.tokenOverview.map((item: { inputToken: number; outputToken: number }) => ({
inputToken: item.inputToken,
outputToken: item.outputToken
})),
value: formatNumberWithUnit(serviceOverview.tokenTotal),
date: serviceOverview.date
}),
inputTokenTotal: formatNumberWithUnit(serviceOverview.inputTokenTotal),
outputTokenTotal: formatNumberWithUnit(serviceOverview.outputTokenTotal)
}
])
// 设置平均值数据
setPerBarChartInfo([
// 平均 token 消耗
{
title: $t('平均 Token 消耗'),
data: serviceOverview.avgTokenOverview,
value: formatNumberWithUnit(serviceOverview.avgToken) + ' Token/s',
originValue: serviceOverview.avgToken,
date: serviceOverview.date,
min: formatNumberWithUnit(serviceOverview.minToken) + ' Token/s',
max: formatNumberWithUnit(serviceOverview.maxToken) + ' Token/s',
type: 'area'
},
{
// 平均请求
...setBarChartInfoData({
title: $t('平均每消费者的请求次数'),
data: serviceOverview.avgRequestPerSubscriberOverview,
date: serviceOverview.date
}),
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
},
// 评价 token 消耗
{
...setBarChartInfoData({
title: $t('平均每消费者的 Token 消耗'),
data: serviceOverview.avgTokenPerSubscriberOverview.map(
(item: { inputToken: number; outputToken: number }) => ({
inputToken: item.inputToken,
outputToken: item.outputToken
})
),
date: serviceOverview.date
}),
max: abbreviateFloat(serviceOverview.maxTokenPerSubscriber),
min: abbreviateFloat(serviceOverview.minTokenPerSubscriber)
}
])
}
/** 获取 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',
'max_response_time',
'min_response_time',
'avg_response_time',
'avg_request_per_subscriber',
'avg_traffic_per_subscriber',
'request_2xx_total',
'request_4xx_total',
'request_5xx_total',
'traffic_2xx_total',
'traffic_4xx_total',
'traffic_5xx_total',
'max_request_per_subscriber',
'min_request_per_subscriber',
'max_traffic_per_subscriber',
'min_traffic_per_subscriber'
]
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 存储 REST 服务数据
setRestServiceOverview(data.overview)
// 设置 REST 报表数据
setRestChartInfoData(data.overview)
} 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 }
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 设置排名表格数据
setTopRankingList({
'TOP API': data.apis,
'TOP Consumer': data.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-[30px]">
<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 min-w-[430px] rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{
body: 'py-[15px] px-[0px]'
}}
>
<ServiceBarChar
showLegendIndicator={true}
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 rounded-[10px] min-w-[284px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{
body: 'py-[15px] px-[0px]'
}}
>
{item.type === 'area' ? (
<>
<ServiceAreaChart
key={index}
height={270}
dataInfo={item}
showAvgLine={true}
customClassNames="flex-1 relative"
></ServiceAreaChart>
</>
) : (
<ServiceBarChar
key={index}
height={270}
dataInfo={item}
hideIndicatorValue={true}
customClassNames="flex-1"
></ServiceBarChar>
)}
</Card>
))}
</div>
<RankingList topRankingList={topRankingList} serviceType={serviceType} />
</div>
</Spin>
)
}
export default ServiceOverview
@@ -0,0 +1,55 @@
export type BarData = {
title: string
value?: string
date: string[]
data: any[]
showXAxis?: boolean
}
export const setBarChartInfoData = ({ title, value, data, date, showXAxis }: BarData) => {
// 首先获取所有的键名(假设所有对象的键名都一样)
if (data.length === 0) {
return {
title,
value,
date,
data: [],
showXAxis: !!showXAxis
}
}
if (typeof data[0] !== 'object') {
return {
title,
value,
date,
data,
showXAxis: !!showXAxis
}
}
// 从第一个对象中获取所有键名
const keys = Object.keys(data[0])
// 定义颜色映射
const colorMap: Record<string, string> = {
'2xx': '#3ba272',
'4xx': '#ffc404',
'5xx': '#b92325'
}
// 为每个键创建一个数据集
const transformedData = keys.map((key) => {
// 为没有映射颜色的键生成随机颜色
const color = colorMap[key]
return {
name: key,
color: color,
value: data.map((item) => item[key])
}
})
return {
title,
value,
date,
data: transformedData,
showXAxis: !!showXAxis
}
}
@@ -2,7 +2,6 @@ import { LoadingOutlined } from '@ant-design/icons'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { BasicResponse, DELETE_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { EntityItem, MemberItem, SimpleTeamItem } from '@common/const/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales/index.ts'
@@ -49,7 +48,6 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
const { fetchData } = useFetch()
const [teamOptionList, setTeamOptionList] = useState<DefaultOptionType[]>()
const navigate = useNavigate()
const { setBreadcrumb } = useBreadcrumb()
const { setSystemInfo } = useSystemContext()
const [showClassify, setShowClassify] = useState<boolean>(true)
const [showAI, setShowAI] = useState<boolean>(false)
@@ -355,15 +353,6 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
if (serviceId !== undefined) {
setOnEdit(true)
getSystemInfo()
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigate('/service/list')
},
{
title: $t('设置')
}
])
} else {
getProviderOptionList()
setOnEdit(false)
@@ -1,7 +1,6 @@
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { EntityItem } from '@common/const/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales'
import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
@@ -10,7 +9,7 @@ import { App, Button } from 'antd'
import hljs from 'highlight.js'
import 'highlight.js/styles/default.css'
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useParams } from 'react-router-dom'
const ServiceInsideDocument = () => {
const { message } = App.useApp()
const [updater, setUpdater] = useState<string>()
@@ -19,8 +18,6 @@ const ServiceInsideDocument = () => {
const [doc, setDoc] = useState<string>()
const { fetchData } = useFetch()
const { serviceId, teamId } = useParams<RouterParams>()
const { setBreadcrumb } = useBreadcrumb()
const navigator = useNavigate()
const save = () => {
fetchData<
BasicResponse<{
@@ -88,15 +85,6 @@ const ServiceInsideDocument = () => {
}
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('使用说明')
}
])
getServiceDoc()
}, [])
@@ -14,6 +14,8 @@ 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'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
const SystemInsidePage: FC = () => {
const { message } = App.useApp()
@@ -26,12 +28,13 @@ const SystemInsidePage: FC = () => {
const [activeMenu, setActiveMenu] = useState<string>()
const navigateTo = useNavigate()
const [showMenu, setShowMenu] = useState<boolean>(false)
const { setBreadcrumb } = useBreadcrumb()
const getSystemInfo = () => {
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 +50,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 +68,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 +150,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 +171,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 +184,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 +208,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])
@@ -219,10 +219,19 @@ const SystemInsidePage: FC = () => {
}, [accessData])
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigateTo('/service/list')
},
{
title: systemInfo?.name || ''
}
])
if (activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]) {
navigateTo(`/service/${teamId}/inside/${serviceId}/${activeMenu}`)
}
}, [activeMenu])
}, [activeMenu, systemInfo, state.language])
useEffect(() => {
serviceId && getSystemInfo()
@@ -233,16 +242,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">
@@ -11,7 +11,6 @@ import {
STATUS_CODE
} from '@common/const/const.tsx'
import { SimpleMemberItem } from '@common/const/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales/index.ts'
@@ -20,7 +19,7 @@ import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
import { App, Form, TreeSelect } from 'antd'
import { DefaultOptionType } from 'antd/es/cascader'
import { FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { SYSTEM_SUBSCRIBER_TABLE_COLUMNS } from '../../const/system/const.tsx'
import {
SimpleSystemItem,
@@ -31,7 +30,6 @@ import {
} from '../../const/system/type.ts'
const SystemInsideSubscriber: FC = () => {
const { setBreadcrumb } = useBreadcrumb()
const { modal, message } = App.useApp()
const { fetchData } = useFetch()
const { serviceId, teamId } = useParams<RouterParams>()
@@ -39,7 +37,6 @@ const SystemInsideSubscriber: FC = () => {
const pageListRef = useRef<ActionType>(null)
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
const { accessData, state } = useGlobalContext()
const navigator = useNavigate()
const getSystemSubscriber = () => {
return fetchData<BasicResponse<{ subscribers: SystemSubscriberTableListItem[] }>>(
'service/subscribers',
@@ -162,15 +159,6 @@ const SystemInsideSubscriber: FC = () => {
]
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('订阅方管理')
}
])
getMemberList()
manualReloadTable()
}, [serviceId])
@@ -2,7 +2,6 @@ import { ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons'
import G6, { EdgeConfig, Graph, NodeConfig } from '@antv/g6'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { EntityItem } from '@common/const/type'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { getNodeSpacing } from '@common/utils/systemRunning'
@@ -10,7 +9,7 @@ import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
import { App, Button } from 'antd'
import { debounce } from 'lodash-es'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { RELATIVE_PICTURE_NODE_FONTSIZE } from '../../const/system-running/const'
import { GraphData } from '../../const/system-running/type'
import { SYSTEM_TOPOLOGY_NODE_TYPE_COLOR_MAP } from '../../const/system/const'
@@ -26,9 +25,7 @@ export default function SystemTopology() {
const [graph, setGraph] = useState<Graph | null>(null)
const { fetchData } = useFetch()
const { systemInfo } = useSystemContext()
const { setBreadcrumb } = useBreadcrumb()
const [zoomNum, setZoomNum] = useState<number>(1)
const navigate = useNavigate()
const getNodeData = () => {
@@ -105,15 +102,6 @@ export default function SystemTopology() {
useEffect(() => {
getNodeData()
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigate('/service/list')
},
{
title: $t('调用拓扑图')
}
])
}, [serviceId])
useEffect(() => {
@@ -9,13 +9,12 @@ import { $t } from '@common/locales/index.ts'
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
import { Button, Empty, Spin, Upload, message } from 'antd'
import { forwardRef, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import {
SystemApiDetail,
SystemInsideApiDocumentHandle,
SystemInsideApiDocumentProps
} from '../../../const/system/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
const SystemInsideApiDocument = forwardRef<
SystemInsideApiDocumentHandle,
@@ -26,18 +25,7 @@ const SystemInsideApiDocument = forwardRef<
const [apiDetail, setApiDetail] = useState<SystemApiDetail>()
const [loading, setLoading] = useState<boolean>(false)
const [showEditor, setShowEditor] = useState<boolean>(false)
const { setBreadcrumb } = useBreadcrumb()
const navigator = useNavigate()
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('API 文档')
}
])
getApiDetail()
}, [])
@@ -26,7 +26,6 @@ import {
SystemInsideRouterCreateHandle,
SystemInsideRouterCreateProps
} from '../../../const/system/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, SystemInsideRouterCreateProps>(
(props, ref) => {
@@ -39,14 +38,12 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
const { state } = useGlobalContext()
const { apiPrefix, prefixForce } = useSystemContext()
const navigator = useNavigate()
const { setBreadcrumb } = useBreadcrumb()
const onFinish = () => {
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([, formValue]) => {
const body = {
...formValue,
path: `${prefixForce ? apiPrefix + '/' : ''}${formValue.path.trim()}${formValue.pathMatch === 'prefix' ? '/*' : ''}`,
path: `${prefixForce ? apiPrefix + (!formValue.path?.trim() ? '': '/') : ''}${(formValue.path?.trim() || '')}${formValue.pathMatch === 'prefix' ? '/*' : ''}`,
proxy: {
...formValue.proxy,
path: formValue.proxy.path
@@ -118,7 +115,7 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const { disable, protocols, path, methods, description, match, proxy } = data.router
const { disable, protocols, path, name, methods, description, match, proxy } = data.router
let newPath = path
let pathMatch = 'full'
if (prefixForce && path?.startsWith(apiPrefix + '/')) {
@@ -131,6 +128,7 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
form.setFieldsValue({
disable,
protocols,
name,
path: newPath,
pathMatch,
methods,
@@ -147,19 +145,6 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
}
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title:$t('API'),
onClick: () => navigator(`/service/${teamId}/inside/${serviceId}/route`)
},
{
title: routeId ? $t('编辑 API') : $t('添加 API')
}
])
if (routeId) {
getRouterConfig()
} else {
@@ -253,6 +238,14 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
>
<Switch />
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
className="flex-1"
label={$t('路由名称')}
name="name"
rules={[{ required: true, whitespace: true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item<SystemApiProxyFieldType> label={$t('请求协议')} name="protocols" rules={[{ required: true }]}>
<Select
@@ -3,7 +3,6 @@ import PageList, { PageProColumns } from '@common/components/aoplatform/PageList
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { SimpleMemberItem } from '@common/const/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales/index.ts'
@@ -11,13 +10,12 @@ import { checkAccess } from '@common/utils/permission.ts'
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
import { App, Divider, Typography } from 'antd'
import { FC, useEffect, useMemo, useRef, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { useNavigate, useParams } from 'react-router-dom'
import { SYSTEM_API_TABLE_COLUMNS } from '../../../const/system/const.tsx'
import { SystemApiTableListItem } from '../../../const/system/type.ts'
const SystemInsideRouterList: FC = () => {
const [searchWord, setSearchWord] = useState<string>('')
const { setBreadcrumb } = useBreadcrumb()
const { modal, message } = App.useApp()
const [tableListDataSource, setTableListDataSource] = useState<SystemApiTableListItem[]>([])
const [tableHttpReload, setTableHttpReload] = useState(true)
@@ -162,17 +160,6 @@ const SystemInsideRouterList: FC = () => {
getMemberList()
manualReloadTable()
}, [serviceId])
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title: $t('路由')
}
])
}, [state.language])
const columns = useMemo(() => {
return [...SYSTEM_API_TABLE_COLUMNS].map((x) => {
@@ -1,9 +1,8 @@
import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {Link, useLocation, useNavigate, useParams} from "react-router-dom";
import { useLocation, useParams} from "react-router-dom";
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {App, Button} from "antd";
import {
SUBSCRIBE_APPROVAL_INNER_DONE_TABLE_COLUMN,
@@ -26,7 +25,6 @@ import { SubscribeApprovalInfoType } from "@common/const/approval/type.tsx";
import { $t } from "@common/locales";
const SystemInsideApprovalList:FC = ()=>{
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
const {serviceId, teamId} = useParams<RouterParams>();
const [init, setInit] = useState<boolean>(true)
@@ -40,7 +38,6 @@ const SystemInsideApprovalList:FC = ()=>{
const [approvalBtnLoading,setApprovalBtnLoading] = useState<boolean>(false)
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
const {accessData,state} = useGlobalContext()
const navigator = useNavigate()
const openModal = async (type:'approval'|'view',entity:SubscribeApprovalTableListItem)=>{
message.loading($t(RESPONSE_TIPS.loading))
@@ -142,15 +139,6 @@ const SystemInsideApprovalList:FC = ()=>{
}, [query]);
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigator('/service/list')
},
{
title:$t('订阅审核')
}
])
getMemberList()
manualReloadTable()
}, [serviceId]);
@@ -1,14 +1,12 @@
import { Tabs } from "antd"
import { useState, useEffect, FC, useMemo } from "react"
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"
import { Outlet, useLocation, useNavigate } from "react-router-dom"
import { SYSTEM_PUBLISH_TAB_ITEMS } from "../../../const/system/const"
import { $t } from "@common/locales"
import { useGlobalContext } from "@common/contexts/GlobalStateContext"
const SystemInsidePublic:FC = ()=>{
const { setBreadcrumb } = useBreadcrumb()
const query =new URLSearchParams(useLocation().search)
const location = useLocation()
const currentUrl = location.pathname
@@ -25,18 +23,6 @@ const SystemInsidePublic:FC = ()=>{
setPageStatus(Number(query.get('status') ||0) as 0|1)
}, [currentUrl]);
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigateTo('/service/list')
},
{
title:$t('发布')
}
])
}, []);
const tabItems = useMemo(()=>SYSTEM_PUBLISH_TAB_ITEMS?.map((x)=>({...x, label:$t(x.label as string) })),[state.language])
return (
<>
@@ -1,14 +1,13 @@
import { ActionType, ParamsType } from "@ant-design/pro-components";
import { App, Button, Divider } from "antd";
import { useState, useRef, useEffect, useMemo, FC } from "react";
import { useParams, Link, useLocation, useNavigate } from "react-router-dom";
import { useParams, useLocation } from "react-router-dom";
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList";
import { PublishApprovalModalContent } from "@common/components/aoplatform/PublishApprovalModalContent";
import { PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN, PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN, PublishApplyStatusEnum, PublishStatusEnum, PublishTableStatusColorClass } from "@common/const/approval/const";
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
import { RouterParams, SimpleMemberItem } from "@common/const/type.ts";
import { MemberTableListItem } from "../../../const/member/type";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext";
import { useFetch } from "@common/hooks/http";
import WithPermission from "@common/components/aoplatform/WithPermission";
import { SystemPublishReleaseItem } from "../../../const/system/type";
@@ -22,7 +21,6 @@ import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter
import { $t } from "@common/locales";
const SystemInsidePublicList:FC = ()=>{
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
const pageListRef = useRef<ActionType>(null);
const [tableHttpReload, setTableHttpReload] = useState(true);
@@ -44,7 +42,6 @@ const SystemInsidePublicList:FC = ()=>{
const [drawerData, setDrawerData] = useState<PublishTableListItem|PublishVersionTableListItem >({} as PublishTableListItem)
const [drawerOkTitle, setDrawerOkTitle] = useState<string>('确认')
const [isOkToPublish, setIsOkToPublish] = useState<boolean>(false)
const navigateTo = useNavigate()
const getSystemPublishList = (params?: ParamsType & {
pageSize?: number | undefined;
current?: number | undefined;
@@ -350,15 +347,6 @@ const SystemInsidePublicList:FC = ()=>{
]
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigateTo('/service/list')
},
{
title:$t('发布')
}
])
getMemberList()
manualReloadTable()
}, [serviceId]);
@@ -8,11 +8,10 @@ import EditableTable from "@common/components/aoplatform/EditableTable.tsx";
import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { UPSTREAM_TYPE_OPTIONS, SYSTEM_UPSTREAM_GLOBAL_CONFIG_TABLE_COLUMNS, schemeOptions, UPSTREAM_BALANCE_OPTIONS, UPSTREAM_PASS_HOST_OPTIONS, PROXY_HEADER_CONFIG, UPSTREAM_PROXY_HEADER_TYPE_OPTIONS } from "../../../const/system/const.tsx";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useParams } from "react-router-dom";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE } from "@common/const/const.tsx";
import { useFetch } from "@common/hooks/http.ts";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx";
import { $t } from "@common/locales/index.ts";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
@@ -33,10 +32,8 @@ const SystemInsideUpstreamContent= forwardRef<SystemInsideUpstreamContentHandle>
const {fetchData} = useFetch()
const [, forceUpdate] = useState<unknown>(null);
const [formShowHost, setFormShowHost] = useState<boolean>(false);
const { setBreadcrumb } = useBreadcrumb()
const [form] = Form.useForm();
const {state} = useGlobalContext()
const navigate = useNavigate()
useImperativeHandle(ref, () => ({
save:()=>formRef.current?.save()
@@ -109,16 +106,7 @@ const globalConfigNodesRule: FormItemProps['rules'] = [
];
useEffect(() => {
setBreadcrumb([
{
title: $t('服务'),
onClick: () => navigate('/service/list')
},
{
title: $t('上游')
}])
getUpstreamInfo();
getUpstreamInfo();
}, [serviceId]);
const typeOptions = useMemo(()=>UPSTREAM_TYPE_OPTIONS.map(x=>({...x, label:$t(x.label)})),[state.language])
@@ -9,7 +9,6 @@ import { useFetch } from '@common/hooks/http.ts'
import { DefaultOptionType } from 'antd/es/cascader'
import { TeamConfigFieldType } from '../../const/team/type.ts'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useTeamContext } from '../../contexts/TeamContext.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { $t } from '@common/locales/index.ts'
@@ -32,7 +31,6 @@ const TeamConfig = forwardRef<TeamConfigHandle, TeamConfigProps>((props, ref) =>
const currentUrl = location.pathname
const { fetchData } = useFetch()
const [managerOption, setManagerOption] = useState<DefaultOptionType[]>([])
const { setBreadcrumb } = useBreadcrumb()
const { setTeamInfo } = useTeamContext()
const { checkPermission, accessInit, state } = useGlobalContext()
const pageType = useMemo(() => {
@@ -133,15 +131,6 @@ const TeamConfig = forwardRef<TeamConfigHandle, TeamConfigProps>((props, ref) =>
.catch((errorInfo) => reject(errorInfo))
})
}
useEffect(() => {
setBreadcrumb([
{
title: $t('团队'),
onClick: () => navigateTo('/team/list')
},
{ title: $t('设置') }
])
}, [state.language])
useEffect(() => {
getManagerList()
@@ -1,8 +1,7 @@
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx'
import { ActionType } from '@ant-design/pro-components'
import { FC, useEffect, useMemo, useRef, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useParams } from 'react-router-dom'
import { App, Button, Modal, Select } from 'antd'
import { BasicResponse, COLUMNS_TITLE, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { useFetch } from '@common/hooks/http.ts'
@@ -59,7 +58,6 @@ export const addMemberToDepartment = (
const TeamInsideMember: FC = () => {
const [searchWord, setSearchWord] = useState<string>('')
const { setBreadcrumb } = useBreadcrumb()
const { modal, message } = App.useApp()
const { fetchData } = useFetch()
const { teamId } = useParams<RouterParams>()
@@ -73,7 +71,6 @@ const TeamInsideMember: FC = () => {
const [addMemberBtnDisabled, setAddMemberBtnDisabled] = useState<boolean>(true)
const [allMemberSelectedDepartIds, setAllMemberSelectedDepartIds] = useState<string[]>([])
const [roleList, setRoleList] = useState<EntityItem[]>([])
const navigator = useNavigate()
const operation: PageProColumns<TeamMemberTableListItem>[] = [
{
@@ -355,13 +352,6 @@ const TeamInsideMember: FC = () => {
}, [teamId])
useEffect(() => {
setBreadcrumb([
{
title: $t('团队'),
onClick: () => navigator('/team/list')
},
{ title: $t('成员') }
])
getRoleList()
}, [state.language])
@@ -16,6 +16,7 @@ import { PERMISSION_DEFINITION } from "@common/const/permissions.ts";
import { TeamConfigType } from "@core/const/team/type.ts";
import { $t } from "@common/locales/index.ts";
import { getItem } from "@common/utils/navigation.tsx";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx";
const TeamInsidePage:FC = ()=> {
const { message } = App.useApp()
@@ -26,6 +27,7 @@ const TeamInsidePage:FC = ()=> {
const {getTeamAccessData,cleanTeamAccessData,accessData,checkPermission,teamDataFlushed,accessInit,state} = useGlobalContext()
const navigateTo = useNavigate()
const [activeMenu, setActiveMenu] = useState<string>()
const { setBreadcrumb } = useBreadcrumb()
const onMenuClick: MenuProps['onClick'] = ({key}) => {
setActiveMenu(key)
@@ -88,6 +90,16 @@ const TeamInsidePage:FC = ()=> {
}
},[activeMenu])
useEffect(()=>{
setBreadcrumb([
{
title: $t('团队'),
onClick: () => navigateTo('/team/list')
},
{ title: teamInfo?.name || '-' }
])
},[state.language, teamInfo])
useEffect(()=>{
getTeamInfo()
teamId && getTeamAccessData(teamId)
@@ -62,6 +62,17 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
getProjectList()
}, [])
/**
*
*/
let resetTimeRange = () => {}
/**
*
* @param instance
*/
const bindRef = (instance: any) => {
resetTimeRange = instance.reset
}
const getApiList = (projectIds?: string[]) => {
return fetchData<{ apis: EntityItem[] }>('simple/service/apis', {
method: 'POST',
@@ -146,6 +157,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
}
const clearSearch = () => {
resetTimeRange()
setTimeButton('hour')
setDatePickerValue(null)
setQueryData(undefined)
@@ -186,11 +198,17 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
setDrawerOpen(true)
}
useEffect(() => {
setQueryBtnLoading(true)
getApiTableList()
}, [queryData])
return (
<div className="overflow-hidden h-full pr-PAGE_INSIDE_X">
<ScrollableSection>
<div className="pl-btnbase pr-btnrbase pb-btnbase content-before">
<TimeRangeSelector
bindRef={bindRef}
labelSize="small"
initialTimeButton={timeButton}
onTimeButtonChange={setTimeButton}
@@ -235,12 +253,8 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
<div className="w-[346px] inline-block">
{/* <SearchInputGroup eoSingle={false} eoInputVal={queryData.path} eoClick={() => setQueryData({ ...queryData, path: '' })} /> */}
<Input
value={queryData?.path}
onChange={(e) =>
debounce((e) => {
setQueryData((prevData) => ({ ...(prevData || {}), path: e.target.value }))
}, 100)(e)
}
value={queryData?.path || ''}
onChange={(e) => setQueryData((prevData) => ({ ...(prevData || {}), path: e.target.value }))}
allowClear
placeholder={$t('请输入请求路径进行搜索')}
prefix={<SearchOutlined className="cursor-pointer" />}
@@ -249,17 +263,6 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
<Button className="ml-btnybase" onClick={clearSearch}>
{$t('重置')}
</Button>
<Button
type="primary"
loading={queryBtnLoading}
className="ml-btnybase"
onClick={() => {
setQueryBtnLoading(true)
getApiTableList()
}}
>
{$t('查询')}
</Button>
<Button className="ml-btnybase" loading={exportLoading} onClick={exportData}>
{$t('导出')}
</Button>
@@ -58,7 +58,17 @@ export default function MonitorAppPage(props: MonitorAppPageProps) {
getMonitorData()
getAppList()
}, [])
/**
*
*/
let resetTimeRange = () => {}
/**
*
* @param instance
*/
const bindRef = (instance: any) => {
resetTimeRange = instance.reset
}
const getMonitorData = () => {
let query = queryData
if (!queryData || queryData.start === undefined) {
@@ -86,6 +96,7 @@ export default function MonitorAppPage(props: MonitorAppPageProps) {
}
const clearSearch = () => {
resetTimeRange()
setTimeButton('hour')
setDatePickerValue(null)
setQueryData({ type: 'subscriber' })
@@ -163,10 +174,16 @@ export default function MonitorAppPage(props: MonitorAppPageProps) {
setDrawerOpen(true)
}
useEffect(() => {
setQueryBtnLoading(true)
getAppTableList()
}, [queryData])
return (
<div className="h-full overflow-hidden pr-PAGE_INSIDE_X">
<div className="pl-btnbase pr-btnrbase pb-btnybase">
<TimeRangeSelector
bindRef={bindRef}
initialTimeButton={timeButton}
onTimeButtonChange={setTimeButton}
initialDatePickerValue={datePickerValue}
@@ -194,17 +211,6 @@ export default function MonitorAppPage(props: MonitorAppPageProps) {
<Button className="ml-btnybase" onClick={clearSearch}>
{$t('重置')}
</Button>
<Button
type="primary"
loading={queryBtnLoading}
className="ml-btnybase"
onClick={() => {
setQueryBtnLoading(true)
getAppTableList()
}}
>
{$t('查询')}
</Button>
<Button className="ml-btnybase" loading={exportLoading} onClick={exportData}>
{$t('导出')}
</Button>
@@ -78,7 +78,17 @@ export default function MonitorDetailPage(props: MonitorDetailPageProps) {
const monitorTableRef = useRef<MonitorTableHandler>(null)
const [modalTitle, setModalTitle] = useState<string>($t('调用趋势'))
const [queryBtnLoading, setQueryBtnLoading] = useState<boolean>(false)
/**
*
*/
let resetTimeRange = () => {}
/**
*
* @param instance
*/
const bindRef = (instance: any) => {
resetTimeRange = instance.reset
}
useEffect(() => {
// 初始化数据
getMonitorData()
@@ -144,6 +154,7 @@ export default function MonitorDetailPage(props: MonitorDetailPageProps) {
}
const clearSearch = () => {
resetTimeRange()
setTimeButton('hour')
setDatePickerValue(null)
setQueryData(null)
@@ -178,11 +189,17 @@ export default function MonitorDetailPage(props: MonitorDetailPageProps) {
setQueryData((pre) => ({ ...pre, ...timeRange }) as SearchBody)
}
useEffect(() => {
setQueryBtnLoading(true)
getMonitorData()
}, [queryData])
return (
<div className="pb-[20px] h-full box-border flex flex-col">
<div className="pl-btnbase pr-btnrbase pb-btnybase sticky top-[0] bg-[#fff] z-[10] shadow-SCROLL_TOP ">
<div className="flex flex-nowrap items-center mr-btnybase">
<TimeRangeSelector
bindRef={bindRef}
initialTimeButton={timeButton}
initialDatePickerValue={datePickerValue}
onTimeButtonChange={setTimeButton}
@@ -192,16 +209,6 @@ export default function MonitorDetailPage(props: MonitorDetailPageProps) {
<Button className="ml-btnybase mt-btnybase" onClick={clearSearch}>
{$t('重置')}
</Button>
<Button
className="ant-btn-primary ml-btnybase mt-btnybase"
loading={queryBtnLoading}
onClick={() => {
setQueryBtnLoading(true)
getMonitorData()
}}
>
{$t('查询')}
</Button>
</div>
</div>
<div className={`flex overflow-y-hidden flex-col flex-1`}>
@@ -63,7 +63,17 @@ export default function MonitorSubPage(props: MonitorSubPageProps) {
getMonitorData()
getProjectList()
}, [])
/**
*
*/
let resetTimeRange = () => {}
/**
*
* @param instance
*/
const bindRef = (instance: any) => {
resetTimeRange = instance.reset
}
const getMonitorData = () => {
let query = queryData
if (!queryData || queryData.start === undefined) {
@@ -91,6 +101,7 @@ export default function MonitorSubPage(props: MonitorSubPageProps) {
}
const clearSearch = () => {
resetTimeRange()
setTimeButton('hour')
setDatePickerValue(null)
setQueryData({ type: 'provider' })
@@ -168,10 +179,16 @@ export default function MonitorSubPage(props: MonitorSubPageProps) {
setDrawerOpen(true)
}
useEffect(() => {
setQueryBtnLoading(true)
getAppTableList()
}, [queryData])
return (
<div className="overflow-hidden h-full pr-PAGE_INSIDE_X">
<div className="pl-btnbase pr-btnrbase pb-btnybase">
<TimeRangeSelector
bindRef={bindRef}
initialTimeButton={timeButton}
onTimeButtonChange={setTimeButton}
initialDatePickerValue={datePickerValue}
@@ -198,17 +215,6 @@ export default function MonitorSubPage(props: MonitorSubPageProps) {
<Button className="ml-btnybase" onClick={clearSearch}>
{$t('重置')}
</Button>
<Button
type="primary"
loading={queryBtnLoading}
className="ml-btnybase"
onClick={() => {
setQueryBtnLoading(true)
getAppTableList()
}}
>
{$t('查询')}
</Button>
<Button className="ml-btnybase" loading={exportLoading} onClick={exportData}>
{$t('导出')}
</Button>
@@ -16,7 +16,7 @@ export default function Dashboard() {
const { message } = App.useApp()
const [clusters, setClusters] = useState<Array<{ id: string; name: string; enable: boolean }>>([])
const [enabledClusters, setEnabledClusters] = useState<Array<EntityItem>>([])
const [loading, setLoading] = useState(false)
const [loading, setLoading] = useState(true)
const getClusters = () => {
setLoading(true)
fetchData<BasicResponse<{ clusters: Array<{ id: string; name: string; enable: boolean }> }>>(
@@ -50,11 +50,7 @@ export default function Dashboard() {
return (
<>
<Spin
wrapperClassName="h-full w-full pb-PAGE_INSIDE_B "
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
spinning={loading}
>
<Spin wrapperClassName="h-full w-full pb-PAGE_INSIDE_B " indicator={<></>} spinning={loading}>
{!loading && (
<>
{enabledClusters.length > 0 ? (
@@ -10,8 +10,8 @@ export default function DashboardList() {
return (
<>
{dashboardType === 'api' && <DashboardApiList />}
{dashboardType === 'subscriber' && <DashboardProjectList />}
{dashboardType === 'provider' && <DashboardApplicationList />}
{dashboardType === 'service' && <DashboardProjectList />}
{dashboardType === 'consumer' && <DashboardApplicationList />}
</>
)
}
@@ -1,89 +1,436 @@
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 { abbreviateFloat, formatBytes, formatDuration, formatNumberWithUnit, 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>('day')
/** 当前选中的时间范围 */
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',
'min_token',
'max_token',
'avg_request_per_subscriber',
'avg_token_per_subscriber',
'input_token',
'output_token',
'total_token',
'request_2xx_total',
'request_4xx_total',
'request_5xx_total',
'input_token_total',
'output_token_total',
'max_request_per_subscriber',
'min_request_per_subscriber',
'max_token_per_subscriber',
'min_token_per_subscriber'
]
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 存储 AI 服务数据
setAiServiceOverview(data?.overview)
// 设置 AI 报表数据
setAiChartInfoData(data?.overview)
} 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',
'max_response_time',
'min_response_time',
'avg_request_per_subscriber',
'avg_traffic_per_subscriber',
'request_2xx_total',
'request_4xx_total',
'request_5xx_total',
'traffic_2xx_total',
'traffic_4xx_total',
'traffic_5xx_total',
'max_request_per_subscriber',
'min_request_per_subscriber',
'max_traffic_per_subscriber',
'min_traffic_per_subscriber'
]
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 存储 REST 服务数据
setRestServiceOverview(data?.overview)
// 设置 REST 报表数据
setRestChartInfoData(data?.overview)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
/**
* REST
* */
const setRestChartInfoData = (serviceOverview: any) => {
// 设置总数数据
setBarChartInfo([
// 服务请求次数
{
...setBarChartInfoData({
title: $t('请求次数'),
data: serviceOverview.requestOverview,
value: formatNumberWithUnit(serviceOverview.requestTotal),
date: serviceOverview.date
}),
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
},
// 流量消耗总数
{
...setBarChartInfoData({
title: $t('网络流量'),
data: serviceOverview.trafficOverview,
value: formatBytes(serviceOverview.trafficTotal),
date: serviceOverview.date
}),
traffic2xxTotal: formatBytes(serviceOverview.traffic2xxTotal),
traffic4xxTotal: formatBytes(serviceOverview.traffic4xxTotal),
traffic5xxTotal: formatBytes(serviceOverview.traffic5xxTotal)
}
])
// 设置平均值数据
setPerBarChartInfo([
// 各个模型使用量
{
title: $t('平均响应时间'),
data: serviceOverview.avgResponseTimeOverview,
value: formatDuration(serviceOverview.avgResponseTime),
originValue: serviceOverview.avgResponseTime,
date: serviceOverview.date,
min: formatDuration(serviceOverview.minResponseTime),
max: formatDuration(serviceOverview.maxResponseTime),
type: 'area'
},
// 平均请求
{
...setBarChartInfoData({
title: $t('平均每消费者的请求次数'),
data: serviceOverview.avgRequestPerSubscriberOverview,
date: serviceOverview.date
}),
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
},
// 平均流量消耗
{
...setBarChartInfoData({
title: $t('平均每消费者的网络流量'),
data: serviceOverview.avgTrafficPerSubscriberOverview,
date: serviceOverview.date
}),
max: formatBytes(serviceOverview.maxTrafficPerSubscriber),
min: formatBytes(serviceOverview.minTrafficPerSubscriber)
}
])
}
/**
* AI
* */
const setAiChartInfoData = (serviceOverview: any) => {
// 设置总数数据
setBarChartInfo([
// 服务请求次数
{
...setBarChartInfoData({
title: $t('请求次数'),
data: serviceOverview.requestOverview,
value: formatNumberWithUnit(serviceOverview.requestTotal),
date: serviceOverview.date
}),
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
},
// token 消耗总数
{
...setBarChartInfoData({
title: $t('Token 消耗'),
data: serviceOverview.tokenOverview.map((item: { inputToken: number; outputToken: number }) => ({
inputToken: item.inputToken,
outputToken: item.outputToken
})),
value: formatNumberWithUnit(serviceOverview.tokenTotal),
date: serviceOverview.date
}),
inputTokenTotal: formatNumberWithUnit(serviceOverview.inputTokenTotal),
outputTokenTotal: formatNumberWithUnit(serviceOverview.outputTokenTotal)
}
])
// 设置平均值数据
setPerBarChartInfo([
// 平均 token 消耗
{
title: $t('平均 Token 消耗'),
data: serviceOverview.avgTokenOverview,
value: formatNumberWithUnit(serviceOverview.avgToken) + ' Token/s',
originValue: serviceOverview.avgToken,
date: serviceOverview.date,
min: formatNumberWithUnit(serviceOverview.minToken) + ' Token/s',
max: formatNumberWithUnit(serviceOverview.maxToken) + ' Token/s',
type: 'area'
},
// 平均请求
{
...setBarChartInfoData({
title: $t('平均每消费者的请求次数'),
data: serviceOverview.avgRequestPerSubscriberOverview,
date: serviceOverview.date
}),
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
},
// 平均 token 消耗
{
...setBarChartInfoData({
title: $t('平均每消费者的 Token 消耗'),
data: serviceOverview.avgTokenPerSubscriberOverview.map(
(item: { inputToken: number; outputToken: number }) => ({
inputToken: item.inputToken,
outputToken: item.outputToken
})
),
date: serviceOverview.date
}),
max: abbreviateFloat(serviceOverview.maxTokenPerSubscriber),
min: abbreviateFloat(serviceOverview.minTokenPerSubscriber)
}
])
}
/** 获取排名列表 */
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 }
}
).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 设置排名表格数据
setTopRankingList({
'TOP API': data.apis,
'TOP Consumer': data.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="mr-[40px]">
<div className="mt-[20px] flex mb-[10px]">
{barChartInfo?.map((item: BarChartInfo, index: number) => (
<Card
key={index}
className={`flex-1 min-w-[430px] rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{
body: 'py-[15px] px-[0px]'
}}
>
<ServiceBarChar
showLegendIndicator={true}
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 min-w-[284px] rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{
body: 'py-[15px] px-[0px]'
}}
>
{item.type === 'area' ? (
<>
<ServiceAreaChart
key={index}
height={270}
showAvgLine={true}
dataInfo={item}
customClassNames="flex-1 relative"
></ServiceAreaChart>
</>
) : (
<ServiceBarChar
key={index}
height={270}
hideIndicatorValue={true}
dataInfo={item}
customClassNames="flex-1"
></ServiceBarChar>
)}
</Card>
))}
</div>
<RankingList
topRankingList={topRankingList}
serviceType={activeTab === 'AI' ? 'aiService' : 'restService'}
/>
</div>
</Spin>
</ScrollableSection>
</div>
)
}
@@ -89,3 +89,114 @@ export function yUnitFormatter(value: number): string {
}
return res
}
/**
*
*
* - 1000
* - (K): 1,000 - 999,999
* - (M): 1,000,000 - 999,999,999
* - 亿(B): 1,000,000,000 - 999,999,999,999
* - 亿(T): 1,000,000,000,000
* @param count
* @returns
*/
export function formatNumberWithUnit(count: number) {
if (count < 1000) {
return count.toString() // 小于1000直接返回
} else if (count < 1_000_000) {
return (count / 1000).toFixed(1) + 'K' // 千级别,如1.5K
} else if (count < 1_000_000_000) {
return (count / 1_000_000).toFixed(1) + 'M' // 百万级别,如2.3M
} else if (count < 1_000_000_000_000) {
return (count / 1_000_000_000).toFixed(1) + 'B' // 十亿级别,如4.5B
} else {
return (count / 1_000_000_000_000).toFixed(1) + 'T' // 万亿级别,如7.8T
}
}
/**
*
* formatNumberWithUnit 10001
* - 10001
* - (K): 1,000 - 999,999
* - (M): 1,000,000 - 999,999,999
* - 亿(B): 1,000,000,000 - 999,999,999,999
* - 亿(T): 1,000,000,000,000
* @param count
* @returns
*/
export function abbreviateFloat(count: number) {
if (count < 1000) {
return count.toFixed(1) // 小于1000的数字保留1位小数,如5.0
} else if (count < 1_000_000) {
return (count / 1000).toFixed(1) + 'K' // 千级别,如1.5K
} else if (count < 1_000_000_000) {
return (count / 1_000_000).toFixed(1) + 'M' // 百万级别,如2.3M
} else if (count < 1_000_000_000_000) {
return (count / 1_000_000_000).toFixed(1) + 'B' // 十亿级别,如4.5B
} else {
return (count / 1_000_000_000_000).toFixed(1) + 'T' // 万亿级别,如7.8T
}
}
/**
*
*
* - (ms): < 1000
* - (s): 1,000 - 999,999
* - (min): 1,000,000 - 999,999,999
* - (hour): 1,000,000,000 - 999,999,999,999
* - (day): 1,000,000,000,000
* @param durationNano
* @returns
*/
export function formatDuration(durationNano: number) {
if (durationNano < 1000) {
return `${durationNano}ms` // 小于1000,返回毫秒
}
if (durationNano < 1_000_000) {
return (durationNano / 1000).toFixed(1) + 's' // 转换为秒
}
if (durationNano < 1_000_000_000) {
return (durationNano / 1_000_000).toFixed(1) + 'min' // 转换为分钟
}
if (durationNano < 1_000_000_000_000) {
return (durationNano / 1_000_000_000).toFixed(1) + 'hour' // 转换为小时
}
return (durationNano / 1_000_000_000_000).toFixed(1) + 'day' // 转换为天
}
/**
*
*
* - (B): < 1000
* - (KB): 1,000 - 999,999
* - (MB): 1,000,000 - 999,999,999
* - (GB): 1,000,000,000 - 999,999,999,999
* - (TB): 1,000,000,000,000 - 999,999,999,999,999
* - (PB): 1,000,000,000,000,000
* @param bytes
* @returns
*/
export function formatBytes(bytes: number) {
const KB = 1000 // 千字节
const MB = KB * 1000 // 兆字节
const GB = MB * 1000 // 吉字节
const TB = GB * 1000 // 太字节
const PB = TB * 1000 // 拉字节
if (bytes < KB) {
return `${bytes}B` // 直接显示字节
} else if (bytes < MB) {
return (bytes / KB).toFixed(1) + 'KB' // 转换为千字节
} else if (bytes < GB) {
return (bytes / MB).toFixed(1) + 'MB' // 转换为兆字节
} else if (bytes < TB) {
return (bytes / GB).toFixed(1) + 'GB' // 转换为吉字节
} else if (bytes < PB) {
return (bytes / TB).toFixed(1) + 'TB' // 转换为太字节
} else {
return (bytes / PB).toFixed(1) + 'PB' // 转换为拉字节
}
}
@@ -53,7 +53,7 @@ const PUBLIC_ROUTES: RouteConfig[] = [
key: uuidv4(),
children: [
{
path: 'serviceHub',
path: 'portal',
component: <Outlet />,
key: uuidv4(),
children: [
@@ -151,7 +151,7 @@ const PUBLIC_ROUTES: RouteConfig[] = [
},
{
path: '*',
component: <Navigate to={'/serviceHub/list'} replace />,
component: <Navigate to={'/portal/list'} replace />,
key: uuidv4()
}
]
@@ -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')
@@ -227,11 +171,12 @@ servers:
setBreadcrumb([
{
title: $t('API 门户'),
onClick: () => navigate(`/serviceHub/list`)
onClick: () => navigate(`/portal/list`)
},
{ title: service?.name || '-' },
{ title: $t('服务详情') }
])
}, [state.language])
}, [state.language, service])
const getMySelectList = () => {
setMySystemOptionList([])
@@ -270,7 +215,7 @@ servers:
content: (
<ApplyServiceModal
ref={applyRef}
entity={{ ...serviceBasicInfo!, name: serviceName!, id: serviceId! }}
entity={{ ...serviceBasicInfo!, name: service?.name || '', id: serviceId! }}
mySystemOptionList={mySystemOptionList!}
/>
),
@@ -293,19 +238,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变化时调用
@@ -429,76 +361,23 @@ servers:
return (
<div className="pr-[40px]">
<header>
<TopBreadcrumb handleBackCallback={() => navigate(`/serviceHub/list`)} />
<TopBreadcrumb handleBackCallback={() => navigate(`/portal/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]"
@@ -254,7 +254,7 @@ export default function ManagementInsideService() {
<Button
type="text"
className="bg-[#7371fc20] hover:bg-[#7371fc19] text-theme"
onClick={() => window.open(`/serviceHub/detail/${item.service.id}`, '_blank')}
onClick={() => window.open(`/portal/detail/${item.service.id}`, '_blank')}
>
{$t('API 文档')}
</Button>