diff --git a/frontend/package.json b/frontend/package.json index 0f6fe4b5..24db0635 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx index d5bcaa05..5bf074d5 100644 --- a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx +++ b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx @@ -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() const pluginSlotHub = usePluginSlotHub() diff --git a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx index 68b0143d..44f8bdb7 100644 --- a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx +++ b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx @@ -26,6 +26,7 @@ class InsidePageProps { scrollInsidePage?: boolean = false customPadding?: boolean customBtn?: ReactNode + customBanner?: ReactNode } const InsidePage: FC = ({ @@ -46,7 +47,8 @@ const InsidePage: FC = ({ scrollPage = true, scrollInsidePage = false, customPadding = false, - customBtn + customBtn, + customBanner }) => { const navigate = useNavigate() @@ -61,8 +63,13 @@ const InsidePage: FC = ({
- {!pageTitle && !description && !backUrl && !customBtn ? ( + {!pageTitle && !description && !backUrl && !customBtn && !customBanner ? ( <> + ) : customBanner ? ( +
+ {backUrl && goBack()} />} + {customBanner} +
) : (
{backUrl && goBack()} />} diff --git a/frontend/packages/common/src/components/aoplatform/PageList.tsx b/frontend/packages/common/src/components/aoplatform/PageList.tsx index dab31d82..d81ce14e 100644 --- a/frontend/packages/common/src/components/aoplatform/PageList.tsx +++ b/frontend/packages/common/src/components/aoplatform/PageList.tsx @@ -58,7 +58,7 @@ interface PageListProps extends ProTableProps, RefAttributes void, + manualReloadTable?: () => void customEmptyRender?: () => React.ReactNode } @@ -109,6 +109,7 @@ const PageList = >( const [allowTableClick, setAllowTableClick] = useState(false) const { accessData, checkPermission, accessInit, state } = useGlobalContext() const [minTableWidth, setMinTableWidth] = useState(0) + const [enableVirtual, setEnableVirtual] = useState(false) useImperativeHandle(ref, () => actionRef.current!) @@ -301,7 +302,7 @@ const PageList = >( actionRef={actionRef} columns={newColumns} - virtual + virtual={enableVirtual} scroll={noScroll ? undefined : { x: tableWidth, y: tableHeight }} size="middle" rowSelection={rowSelection} @@ -328,6 +329,10 @@ const PageList = >( } : false }} + postData={(data: any) => { + setEnableVirtual(!!data?.length) + return data + }} showSorterTooltip={false} columnsState={{ persistenceType: 'localStorage', persistenceKey: id }} pagination={ diff --git a/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx b/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx index f2724dd8..6df2e13c 100644 --- a/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx +++ b/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx @@ -27,6 +27,7 @@ type TimeRangeSelectorProps = { bindRef?: any hideBtns?: TimeRangeButton[] defaultTimeButton?: TimeRangeButton + customClassNames?: string } const TimeRangeSelector = (props: TimeRangeSelectorProps) => { const { @@ -38,7 +39,8 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => { labelSize = 'default', bindRef, hideBtns = [], - defaultTimeButton = 'hour' + defaultTimeButton = 'hour', + customClassNames = 'pt-btnybase' } = props const [timeButton, setTimeButton] = useState(initialTimeButton || '') const [datePickerValue, setDatePickerValue] = useState(initialDatePickerValue || [null, null]) @@ -110,8 +112,12 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => { return current && current.valueOf() > dayjs().startOf('day').valueOf() } + useEffect(() => { + setTimeButton(initialTimeButton || '') + }, [initialTimeButton]) + return ( -
+
{!hideTitle && } {hideBtns?.length && hideBtns.includes('hour') ? null : ( diff --git a/frontend/packages/common/src/components/aoplatform/UnUsedWordForTranslate.tsx b/frontend/packages/common/src/components/aoplatform/UnUsedWordForTranslate.tsx index 7298d6cb..7d9c228b 100644 --- a/frontend/packages/common/src/components/aoplatform/UnUsedWordForTranslate.tsx +++ b/frontend/packages/common/src/components/aoplatform/UnUsedWordForTranslate.tsx @@ -187,6 +187,9 @@ export const TranslateWord = () => { {$t('调用地址')} {$t('消费者 IP')} {$t('鉴权名称')} + {$t('日志输出')} + {$t('响应时间')} + {$t('时间戳')} ) } diff --git a/frontend/packages/common/src/components/aoplatform/serviceInfoCard.tsx b/frontend/packages/common/src/components/aoplatform/serviceInfoCard.tsx new file mode 100644 index 00000000..d592b8b9 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/serviceInfoCard.tsx @@ -0,0 +1,293 @@ +import { Avatar, Button, Card, Tag, Tooltip, App } from 'antd' +import { Icon } from '@iconify/react/dist/iconify.js' +import { $t } from '@common/locales/index.ts' +import { ApiOutlined } from '@ant-design/icons' +import { useEffect, useState } from 'react' +import { SERVICE_KIND_OPTIONS } from '@core/const/system/const' +import { IconButton } from '@common/components/postcat/api/IconButton' +import useCopyToClipboard from '@common/hooks/copy' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' + +export type ServiceBasicInfoType = { + id?: string + logo?: string + name: string + description: string + appNum: number + apiNum: number + serviceName: string + serviceDesc: string + invokeCount: number + catalogue: { + name: string + } + serviceKind: string + service_kind: string + enableMcp: boolean + enable_mcp: boolean + isReleased?: boolean +} + +type ServiceInfoCardProps = { + actionSlot?: React.ReactNode + customClassName?: string + serviceId?: string + serviceBasicInfo?: ServiceBasicInfoType + teamId?: string +} +const ServiceInfoCard = ({ + actionSlot, + customClassName, + serviceId, + serviceBasicInfo, + teamId +}: ServiceInfoCardProps) => { + /** 服务指标 */ + const [serviceMetrics, setServiceMetrics] = useState<{ title: string; icon: React.ReactNode; value: string }[]>([]) + /** 服务标签 */ + const [serviceTags, setServiceTags] = useState< + { color: string; textColor: string; title: string; content: React.ReactNode }[] + >([]) + /** 剪切板 */ + const { copyToClipboard } = useCopyToClipboard() + /** 弹窗组件 */ + const { message } = App.useApp() + /** 获取服务信息 */ + const { fetchData } = useFetch() + /** 服务信息 */ + const [serviceOverview, setServiceOverview] = useState() + + /** + * 复制 + * @param value + * @returns + */ + const handleCopy = async (value: string): Promise => { + if (value) { + copyToClipboard(value) + message.success($t(RESPONSE_TIPS.copySuccess)) + } + } + + /** 获取服务信息 */ + const getServiceOverview = () => { + fetchData>('service/overview/basic', { + method: 'GET', + eoParams: { service: serviceId, team: teamId }, + eoTransformKeys: [ + '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: , + value: serviceOverview.apiNum?.toString() || '0' + }, + { + title: '接入消费者数量', + icon: , + value: serviceOverview.appNum?.toString() || '0' + }, + { + title: '30天内调用次数', + icon: , + value: formatInvokeCount(serviceOverview.invokeCount ?? 0) + } + ]) + const serviceKind = serviceOverview?.serviceKind || serviceOverview?.service_kind + // 设置服务标签数据 + const tags = [ + { + color: '#7371fc1b', + textColor: 'text-theme', + title: serviceOverview?.catalogue?.name || '-', + content: serviceOverview?.catalogue?.name || '-' + }, + { + color: `#${serviceKind === 'ai' ? 'EADEFF' : 'DEFFE7'}`, + textColor: 'text-[#000]', + title: serviceKind || '-', + content: SERVICE_KIND_OPTIONS.find((x) => x.value === serviceKind)?.label || '-' + } + ] + + // 如果启用了MCP,添加MCP标签 + if (serviceOverview?.enableMcp) { + tags.push({ + color: '#FFF0C1', + textColor: 'text-[#000]', + title: 'MCP', + content: 'MCP' + }) + } + + setServiceTags(tags) + } + useEffect(() => { + if (!serviceId && serviceBasicInfo) { + setServiceMetricsList(serviceBasicInfo) + setServiceOverview(serviceBasicInfo) + return + } + getServiceOverview() + }, [serviceId, serviceBasicInfo]) + return ( + <> + + {serviceOverview && ( + <> +
+
+
+ + ) : undefined + } + icon={serviceOverview.logo ? '' : } + > + {' '} + +
+
+

+ {serviceOverview.serviceName} +

+
+ {serviceTags.map((tag, index) => ( + + {tag.content} + + ))} + {serviceMetrics.map((item, index) => ( + + + {item.icon} + {item.value} + + + ))} +
+
+ {serviceOverview.id && ( + <> +
+ + {$t('服务 ID')}:{serviceOverview.id || '-'} + handleCopy(serviceOverview.id || '')} + sx={{ + position: 'absolute', + top: '0px', + right: '5px', + color: '#999', + transition: 'none', + '&.MuiButtonBase-root:hover': { + background: 'transparent', + color: '#3D46F2', + transition: 'none' + } + }} + > + + + + +
+ + )} +
+ + {serviceOverview.serviceDesc || $t('暂无服务描述')} + +
+ + )} +
{actionSlot}
+
+ + ) +} + +export default ServiceInfoCard diff --git a/frontend/packages/common/src/const/charts/apipark-chart-palette.js b/frontend/packages/common/src/const/charts/apipark-chart-palette.js new file mode 100644 index 00000000..8a01a55b --- /dev/null +++ b/frontend/packages/common/src/const/charts/apipark-chart-palette.js @@ -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" + } + } + } + }); +})); diff --git a/frontend/packages/common/src/const/charts/apipark-chart-palette.json b/frontend/packages/common/src/const/charts/apipark-chart-palette.json new file mode 100644 index 00000000..d3afdf16 --- /dev/null +++ b/frontend/packages/common/src/const/charts/apipark-chart-palette.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/frontend/packages/common/src/const/charts/initChartTheme.ts b/frontend/packages/common/src/const/charts/initChartTheme.ts new file mode 100644 index 00000000..1a26f2f9 --- /dev/null +++ b/frontend/packages/common/src/const/charts/initChartTheme.ts @@ -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' diff --git a/frontend/packages/common/src/const/charts/theme.ts b/frontend/packages/common/src/const/charts/theme.ts new file mode 100644 index 00000000..e340b378 --- /dev/null +++ b/frontend/packages/common/src/const/charts/theme.ts @@ -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] diff --git a/frontend/packages/common/src/contexts/BreadcrumbContext.tsx b/frontend/packages/common/src/contexts/BreadcrumbContext.tsx index 2075b855..e88c2eb6 100644 --- a/frontend/packages/common/src/contexts/BreadcrumbContext.tsx +++ b/frontend/packages/common/src/contexts/BreadcrumbContext.tsx @@ -22,8 +22,10 @@ export const BreadcrumbProvider = ({ children }: unknown) => { { - newItems.slice(0, newItems.length - 1).forEach((item) => { - item.title = {item.title} + newItems.forEach((item) => { + item.title = ( + {item.title} + ) }) setBreadcrumb(newItems) }, diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index 16246a22..ced39d41 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -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', diff --git a/frontend/packages/common/src/hooks/pluginLoader.ts b/frontend/packages/common/src/hooks/pluginLoader.ts index f9ae66b1..ccdbc4c2 100644 --- a/frontend/packages/common/src/hooks/pluginLoader.ts +++ b/frontend/packages/common/src/hooks/pluginLoader.ts @@ -112,10 +112,10 @@ const mockData = { }, { driver: 'apipark.builtIn.component', - name: 'serviceHub', + name: 'portal', router: [ { - path: 'serviceHub', + path: 'portal', type: 'normal' } ] diff --git a/frontend/packages/common/src/locales/keyHashMap.json b/frontend/packages/common/src/locales/keyHashMap.json index dbdecb5f..f4d84dd4 100644 --- a/frontend/packages/common/src/locales/keyHashMap.json +++ b/frontend/packages/common/src/locales/keyHashMap.json @@ -54,6 +54,10 @@ "上游列表": "K54e44357", "备注": "Kb8e8e6f5", "上线情况": "K7e52ffa3", + "服务 ID": "K1e84ad04", + "服务尚未发布": "Ke1e649cb", + "跳转至详情页": "K2e683a7d", + "暂无服务描述": "Ka4b45550", "申请原因": "K1ab0ae5b", "审核意见": "K53c00c3c", "暂无(0)权限,请联系管理员分配。": "Kfd50704d", @@ -114,6 +118,7 @@ "无需审核:允许任何消费者调用该服务": "K1fc2cc28", "人工审核:仅允许通过人工审核的消费者调用该服务": "K8dabb98e", "开启:AI Agent 等产品能够通过 MCP 方式调用服务": "Ke959f135", + "总览": "Kaf9e8011", "永久": "Kbfe02d7f", "否": "K1e9c479e", "是": "Kaddfcb6b", @@ -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", diff --git a/frontend/packages/common/src/locales/scan/en-US.json b/frontend/packages/common/src/locales/scan/en-US.json index 53f4af02..8487ce4a 100644 --- a/frontend/packages/common/src/locales/scan/en-US.json +++ b/frontend/packages/common/src/locales/scan/en-US.json @@ -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" } diff --git a/frontend/packages/common/src/locales/scan/ja-JP.json b/frontend/packages/common/src/locales/scan/ja-JP.json index 73d1938d..974ddd23 100644 --- a/frontend/packages/common/src/locales/scan/ja-JP.json +++ b/frontend/packages/common/src/locales/scan/ja-JP.json @@ -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": "消費者あたりの平均トークン消費量" } diff --git a/frontend/packages/common/src/locales/scan/newJson/zh-CN.json b/frontend/packages/common/src/locales/scan/newJson/zh-CN.json index c5c43eda..9e26dfee 100644 --- a/frontend/packages/common/src/locales/scan/newJson/zh-CN.json +++ b/frontend/packages/common/src/locales/scan/newJson/zh-CN.json @@ -1,69 +1 @@ -{ - "K630c9e6d": "APIPark", - "Ka3e9f580": "发布名称", - "Kb2480682": "策略列表", - "K76036e25": "HTTP 请求头", - "K44607e3f": "全等匹配", - "Kc287500a": "前缀匹配", - "Kfc0b1147": "后缀匹配", - "Ka4a92043": "子串匹配", - "K30b2e44f": "非等匹配", - "Kb1587991": "空值匹配", - "K1e97dbd8": "存在匹配", - "Kc8ee3e62": "不存在匹配", - "K87c5a801": "区分大小写的正则匹配", - "K95f062f1": "不区分大小写的正则匹配", - "Kfbd230a5": "任意匹配", - "Kd85208a3": "驳回", - "Kad6aa439": "已订阅", - "K9a68443b": "取消申请", - "Kaeba0229": "透传客户端请求 Host", - "K6d7e2fd0": "使用上游服务 Host", - "K31332633": "重写 Host", - "K2c2bc64f": "动态服务发现", - "K78b1ca25": "地址", - "K1644b775": "新增", - "Kec91f0db": "申请方消费者", - "K118d8d74": "数据格式", - "Kfe7c7d2d": "关键字", - "K2f57a694": "正则表达式", - "K8953e0a6": "手机号", - "K6f86a038": "身份证号", - "K7954e7c8": "银行卡号", - "K320fdb17": "金额", - "K7867acda": "日期", - "K7d327ae8": "局部显示", - "Kfbf38e3c": "局部遮蔽", - "Kd8c1fbb0": "截取", - "K89829921": "替换", - "K480a7165": "乱序", - "Kea0d69df": "随机字符串", - "Ke7c84d1d": "自定义字符串", - "K49731763": "请输入IP地址或CIDR范围,每条以换行分割", - "K3a34d49b": "待更新", - "Kd2850420": "待删除", - "K83237c89": "输入的IP或CIDR不符合格式", - "K5ae2c87a": "请正确输入路径,如/usr/*或*/usr/*", - "K67f4e9bb": "与外部平台集成时,获取 API 市场中文档信息的域名", - "Kc82b8374": "编辑策略", - "K4b34a5e5": "策略类型", - "K57f0fee8": "匹配条件", - "K10650c58": "数据脱敏规则", - "K1b34a9ab": "配置脱敏规则", - "K26d22405": "匹配值", - "K1546e1fe": "脱敏类型", - "K9b9b0629": "起始位置", - "K52c84fe1": "长度", - "Kde84409c": "替换类型", - "K338653b4": "替换值", - "Kbaeed3b7": "JSON Path", - "K4cd91d61": "脱敏规则", - "K8dcad979": "自定义字符串; 值:", - "K82e3f7b7": "起始位置:(0)位;长度:(1)位", - "K49dfc123": "已选择(0)项(1)数据", - "K8457ea34": "所有(0)", - "K7ca9a795": "属性名称", - "Kc4391744": "属性值", - "K678e13fc": "配置(0)", - "Kf5fd27ed": "输入名称查找用户" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/frontend/packages/common/src/locales/scan/zh-CN.json b/frontend/packages/common/src/locales/scan/zh-CN.json index 789ab22b..a8502918 100644 --- a/frontend/packages/common/src/locales/scan/zh-CN.json +++ b/frontend/packages/common/src/locales/scan/zh-CN.json @@ -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 消耗" } diff --git a/frontend/packages/common/src/locales/scan/zh-TW.json b/frontend/packages/common/src/locales/scan/zh-TW.json index becfa911..59c1f7ba 100644 --- a/frontend/packages/common/src/locales/scan/zh-TW.json +++ b/frontend/packages/common/src/locales/scan/zh-TW.json @@ -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 消耗" } diff --git a/frontend/packages/core/src/App.tsx b/frontend/packages/core/src/App.tsx index cb98a151..fc4d9cb1 100644 --- a/frontend/packages/core/src/App.tsx +++ b/frontend/packages/core/src/App.tsx @@ -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() diff --git a/frontend/packages/core/src/const/ai-service/const.tsx b/frontend/packages/core/src/const/ai-service/const.tsx index a058f636..fad67f2b 100644 --- a/frontend/packages/core/src/const/ai-service/const.tsx +++ b/frontend/packages/core/src/const/ai-service/const.tsx @@ -6,17 +6,18 @@ import { AiServiceRouterTableListItem, VariableItems } from './type' import { PageProColumns } from '@common/components/aoplatform/PageList' export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + width: 200, + ellipsis: true + }, { title: 'URL', dataIndex: 'requestPath', ellipsis: true, width: 200 }, - { - title: '名称', - dataIndex: 'name', - ellipsis: true - }, { title: '模型', dataIndex: ['model', 'name'], diff --git a/frontend/packages/core/src/const/ai-service/type.ts b/frontend/packages/core/src/const/ai-service/type.ts index 82c3825c..401e47a7 100644 --- a/frontend/packages/core/src/const/ai-service/type.ts +++ b/frontend/packages/core/src/const/ai-service/type.ts @@ -15,7 +15,7 @@ export type AiServiceConfigFieldType = { logoFile?:UploadFile; tags?:Array; description?: string; - team?:string; + team?:EntityItem; master?:string; serviceType?:'public'|'inner'; catalogue?:string | string[]; diff --git a/frontend/packages/core/src/const/const.tsx b/frontend/packages/core/src/const/const.tsx index f763da82..2a7fc46d 100644 --- a/frontend/packages/core/src/const/const.tsx +++ b/frontend/packages/core/src/const/const.tsx @@ -96,6 +96,20 @@ export const routerMap: Map = new Map([ key: 'restServiceInside', lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsidePage.tsx')), children: [ + { + path: 'overview', + key: 'restServiceInsideOverview', + lazy: lazy( + () => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceOverview/RestServiceContainer') + ) + }, + { + path: 'logs', + key: 'restServiceInsideLogs', + lazy: lazy( + () => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceLogs/RestServiceLogsContainer') + ) + }, { path: 'api', key: 'restServiceInsideApi', @@ -268,6 +282,20 @@ export const routerMap: Map = new Map([ () => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsidePage.tsx') ), children: [ + { + path: 'overview', + key: 'aiServiceInsideOverview', + lazy: lazy( + () => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceOverview/AiServiceContainer') + ) + }, + { + path: 'logs', + key: 'aiServiceInsideLogs', + lazy: lazy( + () => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceLogs/AiServiceLogsContainer') + ) + }, { path: 'api', key: 'aiServiceInsideApi', @@ -507,7 +535,7 @@ export const routerMap: Map = new Map([ ], [ - 'serviceHub', + 'portal', { type: 'module', component: , @@ -674,12 +702,12 @@ export const routerMap: Map = 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: , children: [ { diff --git a/frontend/packages/core/src/const/partitions/types.ts b/frontend/packages/core/src/const/partitions/types.ts index 99d9d507..ec998209 100644 --- a/frontend/packages/core/src/const/partitions/types.ts +++ b/frontend/packages/core/src/const/partitions/types.ts @@ -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[] = [ diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index c356ee6a..23bad822 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -13,6 +13,7 @@ import { } from './type' import { PageProColumns } from '@common/components/aoplatform/PageList' +import { LogItem } from '@core/pages/serviceLogs/ServiceLogs' export enum SubscribeEnum { Rejected = 0, @@ -241,6 +242,12 @@ export const MATCH_CONFIG: ConfigField[] = [ ] export const SYSTEM_API_TABLE_COLUMNS: PageProColumns[] = [ + { + 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[] = [ + { + title: '名称', + dataIndex: 'name', + ellipsis: true + }, + { + title: '请求总数', + dataIndex: 'request', + ellipsis: true + }, + { + title: 'Token', + dataIndex: 'token', + ellipsis: true + } +] + +/** REST 服务排行 */ +export const REST_SERVICE_TOP_RANKING_LIST: PageProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + ellipsis: true + }, + { + title: '请求总数', + dataIndex: 'request', + ellipsis: true + }, + { + title: '流量', + dataIndex: 'traffic', + ellipsis: true + } +] + +/** REST 服务日志 */ +export const REST_SERVICE_LOG_LIST: PageProColumns[] = [ + { + title: '时间戳', + dataIndex: 'logTime', + 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[] = [ + { + 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 + } +] diff --git a/frontend/packages/core/src/const/system/type.ts b/frontend/packages/core/src/const/system/type.ts index 9d4a82fe..0f1d70a2 100644 --- a/frontend/packages/core/src/const/system/type.ts +++ b/frontend/packages/core/src/const/system/type.ts @@ -97,6 +97,7 @@ export type SystemApiProxyType = { export type SystemApiProxyFieldType = { protocols: string[]; id:string; + name:string description?:string; disable:boolean; path:string; diff --git a/frontend/packages/core/src/index.css b/frontend/packages/core/src/index.css index 9809791a..8a5679e3 100644 --- a/frontend/packages/core/src/index.css +++ b/frontend/packages/core/src/index.css @@ -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; diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsideDocument.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsideDocument.tsx index e9059968..b39f14ee 100644 --- a/frontend/packages/core/src/pages/aiService/AiServiceInsideDocument.tsx +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsideDocument.tsx @@ -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() @@ -19,8 +18,6 @@ const ServiceInsideDocument = () => { const [doc, setDoc] = useState() const { fetchData } = useFetch() const { serviceId, teamId } = useParams() - 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() }, []) diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx index f42c6b47..80daf817 100644 --- a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx @@ -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() const navigateTo = useNavigate() const [showMenu, setShowMenu] = useState(false) + const { setBreadcrumb } = useBreadcrumb() const getAiServiceInfo = () => { fetchData>('service/info', { @@ -67,6 +69,7 @@ const AiServiceInsidePage: FC = () => { 'assets', null, [ + getItem({$t('总览')}, 'overview', undefined, undefined, undefined, ''), getItem( {$t('API 路由')}, 'route', @@ -149,7 +152,8 @@ const AiServiceInsidePage: FC = () => { 'project.myAiService.topology.view' ) : null, - getItem({$t('设置')}, 'setting', undefined, undefined, undefined, '') + getItem({$t('设置')}, 'setting', undefined, undefined, undefined, ''), + getItem({$t('日志')}, 'logs', undefined, undefined, undefined, '') ], 'group' ) @@ -202,7 +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 ? ( - {$t('服务 ID')}:{serviceId || '-'} - - ) - } - ]} backUrl="/service/list" + customBanner={} >
{ - const { setBreadcrumb } = useBreadcrumb() const { modal,message } = App.useApp() const {fetchData} = useFetch() const {serviceId, teamId} = useParams() @@ -26,7 +24,6 @@ const AiServiceInsideSubscriber:FC = ()=>{ const pageListRef = useRef(null); const [memberValueEnum, setMemberValueEnum] = useState([]) const {accessData,state} = useGlobalContext() - const navigator = useNavigate() const getAiServiceSubscriber = ()=>{ return fetchData>('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]); diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiDocument.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiDocument.tsx index 44a419b8..909696e2 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiDocument.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiDocument.tsx @@ -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(() => { const { serviceId, teamId } = useParams() const { fetchData } = useFetch() const [apiDetail, setApiDetail] = useState() const [loading, setLoading] = useState(false) - const { setBreadcrumb } = useBreadcrumb() - const navigator = useNavigate() useEffect(() => { - setBreadcrumb([ - { - title: $t('服务'), - onClick: () => navigator('/service/list') - }, - { - title: $t('API 文档') - } - ]) getApiDetail() }, []) diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx index fa0ef96f..32bc732f 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx @@ -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]) diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx index 55d2e3b0..7fe058df 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx @@ -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('') - const { setBreadcrumb } = useBreadcrumb() const { modal, message } = App.useApp() const [tableListDataSource, setTableListDataSource] = useState([]) 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) => { diff --git a/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApprovalList.tsx b/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApprovalList.tsx index 6621ffec..13cc843a 100644 --- a/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApprovalList.tsx +++ b/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApprovalList.tsx @@ -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(); const [init, setInit] = useState(true) @@ -40,7 +38,6 @@ const AiServiceInsideApprovalList:FC = ()=>{ const [approvalBtnLoading,setApprovalBtnLoading] = useState(false) const [memberValueEnum, setMemberValueEnum] = useState([]) 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]); diff --git a/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublish.tsx b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublish.tsx index b80b82b1..a492b3e4 100644 --- a/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublish.tsx +++ b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublish.tsx @@ -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 ( <> diff --git a/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishList.tsx b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishList.tsx index c3ec8573..09a17009 100644 --- a/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishList.tsx +++ b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishList.tsx @@ -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(null); const [tableHttpReload, setTableHttpReload] = useState(true); @@ -45,7 +43,6 @@ const AiServiceInsidePublicList:FC = ()=>{ const [drawerData, setDrawerData] = useState({} as PublishTableListItem) const [drawerOkTitle, setDrawerOkTitle] = useState('确认') const [isOkToPublish, setIsOkToPublish] = useState(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]); diff --git a/frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx b/frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx index 29338962..5a586ede 100644 --- a/frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx +++ b/frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx @@ -39,6 +39,7 @@ const ApiKeyContent: React.FC = 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) diff --git a/frontend/packages/core/src/pages/logsettings/LogSettings.tsx b/frontend/packages/core/src/pages/logsettings/LogSettings.tsx index 124ee8d3..21488dad 100644 --- a/frontend/packages/core/src/pages/logsettings/LogSettings.tsx +++ b/frontend/packages/core/src/pages/logsettings/LogSettings.tsx @@ -68,7 +68,7 @@ const LogSettings = () => { <>
diff --git a/frontend/packages/core/src/pages/partitions/DataLogSettingEdit.tsx b/frontend/packages/core/src/pages/partitions/DataLogSettingEdit.tsx index 7dafa743..189c6993 100644 --- a/frontend/packages/core/src/pages/partitions/DataLogSettingEdit.tsx +++ b/frontend/packages/core/src/pages/partitions/DataLogSettingEdit.tsx @@ -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>('log/loki', { method: 'POST', body: JSON.stringify(formData), eoParams: {} }).then(response => { + fetchData>('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 ( <>
- +
{ autoComplete="off" > - label={$t("请求前缀")} - name="url" + label={$t('数据源类型')} + name="driver" rules={[{ required: true }]} > + + + label={$t('请求前缀')} name="url" rules={[{ required: true }]}> - - label={$t("HTTP 头部")} - name="headers" - > + label={$t('HTTP 头部')} name="headers"> configFields={PARTITION_DATA_LOG_CONFIG_TABLE_COLUMNS} />
- + @@ -84,7 +111,7 @@ const DataLogSettingEdit = (props: DashboardSettingEditProps) => {
- ); + ) } -export default DataLogSettingEdit; \ No newline at end of file +export default DataLogSettingEdit diff --git a/frontend/packages/core/src/pages/partitions/PartitionInsideDashboardSetting.tsx b/frontend/packages/core/src/pages/partitions/PartitionInsideDashboardSetting.tsx index 6e243094..b732c6ab 100644 --- a/frontend/packages/core/src/pages/partitions/PartitionInsideDashboardSetting.tsx +++ b/frontend/packages/core/src/pages/partitions/PartitionInsideDashboardSetting.tsx @@ -161,7 +161,7 @@ const PartitionInsideDashboardSetting: FC = () => { className="overflow-hidden mt-[30px] w-full max-h-full flex flex-col justify-between" title={
- {$t('数据日志')} + {$t('请求日志')} {!dataLogLoading && !dataLogData && {$t('未配置')}}
} @@ -220,6 +220,11 @@ export function DataLogConfigPreview(x: PartitionDataLogConfigFieldType) { return (
+ + {$t('数据源')}: + {/* 先写死,或许会有选择列表,但现在可以不用 */} + Loki + {$t('请求前缀')}: {x?.url} diff --git a/frontend/packages/core/src/pages/policy/ServicePolicyLayout.tsx b/frontend/packages/core/src/pages/policy/ServicePolicyLayout.tsx index 3ac38da1..ed15ddb1 100644 --- a/frontend/packages/core/src/pages/policy/ServicePolicyLayout.tsx +++ b/frontend/packages/core/src/pages/policy/ServicePolicyLayout.tsx @@ -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]) diff --git a/frontend/packages/core/src/pages/policy/dataMasking/DataMaskingConfig.tsx b/frontend/packages/core/src/pages/policy/dataMasking/DataMaskingConfig.tsx index ec4aa016..efc778d3 100644 --- a/frontend/packages/core/src/pages/policy/dataMasking/DataMaskingConfig.tsx +++ b/frontend/packages/core/src/pages/policy/dataMasking/DataMaskingConfig.tsx @@ -80,19 +80,6 @@ const DataMaskingConfig = forwardRef((_,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(); diff --git a/frontend/packages/core/src/pages/serviceLogs/AiServiceLogsContainer.tsx b/frontend/packages/core/src/pages/serviceLogs/AiServiceLogsContainer.tsx new file mode 100644 index 00000000..ecd24a38 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/AiServiceLogsContainer.tsx @@ -0,0 +1,6 @@ +import ServiceLogs from "./ServiceLogs" +const AiServiceLogsContainer = () => { + return +} + +export default AiServiceLogsContainer \ No newline at end of file diff --git a/frontend/packages/core/src/pages/serviceLogs/ApiNetWorkDataPreview.tsx b/frontend/packages/core/src/pages/serviceLogs/ApiNetWorkDataPreview.tsx new file mode 100644 index 00000000..53d447da --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/ApiNetWorkDataPreview.tsx @@ -0,0 +1,89 @@ +import { IconButton } from '@common/components/postcat/api/IconButton' +import useCopyToClipboard from '@common/hooks/copy' +import { RESPONSE_TIPS } from '@common/const/const' +import { $t } from '@common/locales/index.ts' +import { App } from 'antd' +import ReactJson from 'react-json-view' + +const ApiNetWorkDataPreview = ({ configContent = {} }: { configContent?: { [key: string]: string | undefined } }) => { + /** 复制组件 */ + const { copyToClipboard } = useCopyToClipboard() + /** 弹窗组件 */ + const { message } = App.useApp() + /** + * 复制 + * @param value + * @returns + */ + const handleCopy = async (value: string): Promise => { + if (value) { + copyToClipboard(value) + message.success($t(RESPONSE_TIPS.copySuccess)) + } + } + /** + * 判断字符串是否是有效的JSON对象字符串 + */ + const isJsonString = (str: string): boolean => { + try { + const parsed = JSON.parse(str) + return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) + } catch (e) { + return false + } + } + + return ( + <> + {Object.keys(configContent).filter((item) => !!configContent[item]).map((item) => { + return ( +
+
{item}
+
+ {!configContent[item] ? ( +

+              ) : isJsonString(configContent[item] || '') ? (
+                // 如果是有效的JSON对象字符串,使用ReactJson渲染
+                
+              ) : (
+                // 如果是普通字符串,直接用pre渲染
+                
{configContent[item]}
+ )} + handleCopy(configContent[item] || '')} + sx={{ + position: 'absolute', + top: '5px', + right: '5px', + color: '#999', + transition: 'none', + '&.MuiButtonBase-root:hover': { + background: 'transparent', + color: '#3D46F2', + transition: 'none' + } + }} + > +
+
+ ) + })} + + ) +} +export default ApiNetWorkDataPreview diff --git a/frontend/packages/core/src/pages/serviceLogs/LogDetail.tsx b/frontend/packages/core/src/pages/serviceLogs/LogDetail.tsx new file mode 100644 index 00000000..5b4bb9f4 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/LogDetail.tsx @@ -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() + /** 全局状态 */ + 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 ? {status} : status + } + + /** + * 获取标签页内容 + */ + const tabItems = useMemo( + () => [ + { + key: 'request', + label: 'Request', + children: + }, + { + key: 'response', + label: 'Response', + children: + } + ], + [state.language, requestInfoData, responseInfoData] + ) + + /** + * 设置 AI 描述文案 + */ + const getAIServiceDescriptionItemsList = ({ + time, + api, + consumer, + status, + model, + ip + }: { + time: string + api: string + consumer: string + status: string + model: string + ip: string + }) => { + setDescriptionItems([ + { + key: 'time', + label: $t('时间戳'), + children: time + }, + { + key: 'api', + label: $t('API / Tools'), + children: api + }, + { + key: 'consumer', + label: $t('消费者'), + children: consumer + }, + { + key: 'httpStatus', + label: $t('HTTP 状态'), + children: renderStatusWithColor(status) + }, + { + key: 'model', + label: $t('模型'), + children: model + }, + { + key: 'ip', + label: $t('IP'), + children: ip + } + ]) + } + + /** + * 设置 REST 描述文案 + */ + const getRestServiceDescriptionItemsList = ({ + time, + api, + consumer, + isSystemConsumer, + status, + ip + }: { + time: string + api: string + consumer: string + isSystemConsumer?: boolean + status: string + ip: string + }) => { + setDescriptionItems([ + { + key: 'time', + label: $t('时间戳'), + children: time + }, + { + key: 'api', + label: $t('API / Tools'), + children: api + }, + { + key: 'consumer', + label: $t('消费者'), + children: ( + <> + {consumer} + {isSystemConsumer && ( + + System-level API Key + + + + + + + )} + + ) + }, + { + key: 'httpStatus', + label: $t('HTTP 状态'), + children: renderStatusWithColor(status) + }, + { + key: 'ip', + label: $t('IP'), + children: ip + } + ]) + } + + /** + * 获取 AI 服务日志详情 + */ + const getAIServiceLogDetail = () => { + fetchData>('service/log/ai', { + method: 'GET', + eoParams: { log: selectedRow?.id, service: serviceId, team: teamId }, + eoTransformKeys: ['is_system_consumer', 'log_time'] + }).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>('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 ( + +
+ +
+
+ } + spinning={dashboardLoading} + > + +
+ +
+ + ) +} + +export default LogDetail diff --git a/frontend/packages/core/src/pages/serviceLogs/RestServiceLogsContainer.tsx b/frontend/packages/core/src/pages/serviceLogs/RestServiceLogsContainer.tsx new file mode 100644 index 00000000..db2ac201 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/RestServiceLogsContainer.tsx @@ -0,0 +1,7 @@ +import ServiceLogs from "./ServiceLogs" + +const RestServiceLogsContainer = () => { + return +} + +export default RestServiceLogsContainer \ No newline at end of file diff --git a/frontend/packages/core/src/pages/serviceLogs/ServiceLogs.tsx b/frontend/packages/core/src/pages/serviceLogs/ServiceLogs.tsx new file mode 100644 index 00000000..71f13d8f --- /dev/null +++ b/frontend/packages/core/src/pages/serviceLogs/ServiceLogs.tsx @@ -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() + /** 默认时间 */ + const [defaultTime] = useState('day') + /** 全局状态 */ + const { state } = useGlobalContext() + /** + * 请求数据 + */ + const { fetchData } = useFetch() + // 打开侧边弹窗 + const [drawerOpen, setDrawerOpen] = useState(false) + /** 选中的行 */ + const [selectedRow, setSelectedRow] = useState() + /** + * 列表ref + */ + const pageListRef = useRef(null) + /** 列 */ + const columns = useMemo(() => { + return [...(serviceType === 'aiService' ? AI_SERVICE_LOG_LIST : REST_SERVICE_LOG_LIST)].map((x) => { + if (x.dataIndex === 'status') { + x.render = (text: any, record: any) => ( + <> +
{renderStatusWithColor(record.status)}
+ + ) + } + return { + ...x, + title: typeof x.title === 'string' ? $t(x.title as string) : x.title + } + }) + }, [state.language]) + + /** + * 根据状态码返回对应颜色的文本 + * @param status 状态 + * @returns + */ + const renderStatusWithColor = (status: string | number) => { + // 获取状态码首位数字 + const firstDigit = status.toString().charAt(0) + let color = '' + switch (firstDigit) { + case '2': + color = HttpStatusColor.SUCCESS + break + case '4': + color = HttpStatusColor.CLIENT_ERROR + break + case '5': + color = HttpStatusColor.SERVER_ERROR + break + default: + break + } + return color ? {status} : status + } + + /** + * 获取 AI 列表数据 + * @param dataType + * @returns + */ + const getAiServiceLogList = ( + params: ParamsType & { + pageSize?: number | undefined + current?: number | undefined + keyword?: string | undefined + } + ) => { + return fetchData>(`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>(`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 ( + +
+ +
+
+ } + spinning={dashboardLoading} + > +
+ +
+ (serviceType === 'aiService' ? getAiServiceLogList(params) : getRestServiceLogList(params))} + onRowClick={(row: LogItem) => handleRowClick(row)} + /> +
+ setDrawerOpen(false)} + open={drawerOpen} + > + + +
+ + ) +} + +export default ServiceLogs diff --git a/frontend/packages/core/src/pages/serviceOverview/AiServiceContainer.tsx b/frontend/packages/core/src/pages/serviceOverview/AiServiceContainer.tsx new file mode 100644 index 00000000..501fb783 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/AiServiceContainer.tsx @@ -0,0 +1,7 @@ +import ServiceOverview from "./serviceOverview" + +const AiServiceContainer = () => { + return +} + +export default AiServiceContainer \ No newline at end of file diff --git a/frontend/packages/core/src/pages/serviceOverview/RestServiceContainer.tsx b/frontend/packages/core/src/pages/serviceOverview/RestServiceContainer.tsx new file mode 100644 index 00000000..2f527421 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/RestServiceContainer.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react' +import ServiceOverview from './serviceOverview' + +const RestServiceContainer: FC = () => { + return ( + <> + + + ) +} + +export default RestServiceContainer diff --git a/frontend/packages/core/src/pages/serviceOverview/charts/ServiceAreaChart.tsx b/frontend/packages/core/src/pages/serviceOverview/charts/ServiceAreaChart.tsx new file mode 100644 index 00000000..015d2970 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/charts/ServiceAreaChart.tsx @@ -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(null) + const [option, setOption] = useState({}) + 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 = `
` + const marker = `` + tooltipContent += `
+
${marker}
${param.name}
${param.value}
+
` + tooltipContent += '
' + 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 ( +
+
+
+
+
+ +
+ {dataInfo?.max} +
+ +
+ {dataInfo?.min} +
+
+
+
+ +
+
+ ) +} + +export default ServiceAreaChart diff --git a/frontend/packages/core/src/pages/serviceOverview/charts/ServiceBarChar.tsx b/frontend/packages/core/src/pages/serviceOverview/charts/ServiceBarChar.tsx new file mode 100644 index 00000000..1267f2bd --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/charts/ServiceBarChar.tsx @@ -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(null) + const [option, setOption] = useState({}) + // 使用从主题配置中导入的默认颜色,而不是硬编码的颜色值 + 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 = `
+
${isNumberArray ? '' : params.name}
` + 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 = `` + tooltipContent += `
+
${marker} ${name}
${value}
+
` + }) + + tooltipContent += '
' + 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 = { + 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 ( +
+ { + hideIndicatorValue && ( +
+
+
+
+ +
+ {dataInfo?.max} +
+ +
+ {dataInfo?.min} +
+
+
+ ) + } +
+ +
+
+ ) +} + +export default ServiceBarChar diff --git a/frontend/packages/core/src/pages/serviceOverview/filter/DateSelectFilter.tsx b/frontend/packages/core/src/pages/serviceOverview/filter/DateSelectFilter.tsx new file mode 100644 index 00000000..2a59b9a2 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/filter/DateSelectFilter.tsx @@ -0,0 +1,37 @@ +import TimeRangeSelector, { RangeValue, TimeRange } from '@common/components/aoplatform/TimeRangeSelector' +import { useState } from 'react' + +export type TimeOption = '' | 'hour' | 'day' | 'threeDays' | 'sevenDays' +const DateSelectFilter = ({ + selectCallback, + defaultTime, + customClassNames +}: { + selectCallback: (timeRange: TimeRange) => void + defaultTime: TimeOption + customClassNames?: string +}) => { + /** 默认时间 */ + const [timeButton, setTimeButton] = useState(defaultTime || 'hour') + /** 日期选择 */ + const [datePickerValue, setDatePickerValue] = useState() + /** 时间范围变化 */ + const handleTimeRangeChange = (timeRange: TimeRange) => { + selectCallback(timeRange) + } + + return ( +
+ +
+ ) +} + +export default DateSelectFilter diff --git a/frontend/packages/core/src/pages/serviceOverview/indicator/Indicator.tsx b/frontend/packages/core/src/pages/serviceOverview/indicator/Indicator.tsx new file mode 100644 index 00000000..c4c48a6c --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/indicator/Indicator.tsx @@ -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([]) + /** 路由跳转 */ + 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: ( + <> + + + ) + } + ]) + } + + useEffect(() => { + if (!indicatorInfo) return + setIndicatorList() + }, [indicatorInfo]) + + return ( +
+ {indicatorList.map((item, index) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'py-[20px] px-[18px]' + }} + onClick={() => { + if (item.link) { + navigateTo(item.link) + } + }} + > +
+ {item.title} + {item.link && } +
+
+ {item.content} +
+
+ ))} +
+ ) +} + +export default Indicator diff --git a/frontend/packages/core/src/pages/serviceOverview/rankingList/RankingList.tsx b/frontend/packages/core/src/pages/serviceOverview/rankingList/RankingList.tsx new file mode 100644 index 00000000..d5cf296f --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/rankingList/RankingList.tsx @@ -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 ( +
+ {Object.keys(topRankingList)?.map((item: any, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'p-[15px]' + }} + > +
+ {item === 'TOP API' ? $t('API 使用排名') : $t('消费者使用排名')} +
+ getTableData(item)} + showPagination={false} + tableClass="ranking-list" + ref={ref => { + if (ref) tableRefs.current[item] = ref; + }} + /> +
+ ))} +
+ ) +} + +export default RankingList diff --git a/frontend/packages/core/src/pages/serviceOverview/serviceOverview.tsx b/frontend/packages/core/src/pages/serviceOverview/serviceOverview.tsx new file mode 100644 index 00000000..502e39cc --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/serviceOverview.tsx @@ -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('day') + /** 当前选中的时间范围 */ + const [timeRange, setTimeRange] = useState() + /** 总数数据 */ + const [barChartInfo, setBarChartInfo] = useState() + /** 平均值数据 */ + const [perBarChartInfo, setPerBarChartInfo] = useState() + /** 指标数据 */ + const [indicatorInfo, setIndicatorInfo] = useState([]) + /** 排名表格数据 */ + const [topRankingList, setTopRankingList] = useState([]) + /** 获取服务信息 */ + const { fetchData } = useFetch() + /** 弹窗组件 */ + const { message } = App.useApp() + /** 全局状态 */ + const { state } = useGlobalContext() + /** AI 服务数据 */ + const [aiServiceOverview, setAiServiceOverview] = useState() + /** REST 服务数据 */ + const [restServiceOverview, setRestServiceOverview] = useState() + /** 时间选择回调 */ + const selectCallback = (date: TimeRange) => { + setTimeRange(date) + } + + /** 获取 AI 服务信息 */ + const getAIServiceOverview = () => { + fetchData>('service/overview/monitor/ai', { + method: 'GET', + eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end }, + eoTransformKeys: [ + 'enable_mcp', + 'subscriber_num', + 'api_num', + 'service_kind', + 'avaliable_monitor', + 'request_overview', + 'token_overview', + 'avg_token_overview', + 'avg_request_per_subscriber_overview', + 'avg_token_per_subscriber_overview', + 'request_total', + 'token_total', + 'avg_token', + '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>('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>('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 ( + +
+ +
+
+ } + spinning={dashboardLoading} + > +
+ +
+ +
+
+ {barChartInfo?.map((item: BarChartInfo, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'py-[15px] px-[0px]' + }} + > + + + ))} +
+
+ {perBarChartInfo?.map((item: any, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'py-[15px] px-[0px]' + }} + > + {item.type === 'area' ? ( + <> + + + ) : ( + + )} + + ))} +
+ +
+ + ) +} + +export default ServiceOverview diff --git a/frontend/packages/core/src/pages/serviceOverview/utils.ts b/frontend/packages/core/src/pages/serviceOverview/utils.ts new file mode 100644 index 00000000..ad68a882 --- /dev/null +++ b/frontend/packages/core/src/pages/serviceOverview/utils.ts @@ -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 = { + '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 + } +} diff --git a/frontend/packages/core/src/pages/system/SystemConfig.tsx b/frontend/packages/core/src/pages/system/SystemConfig.tsx index 14426200..d12671d2 100644 --- a/frontend/packages/core/src/pages/system/SystemConfig.tsx +++ b/frontend/packages/core/src/pages/system/SystemConfig.tsx @@ -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((_, ref) => { const { fetchData } = useFetch() const [teamOptionList, setTeamOptionList] = useState() const navigate = useNavigate() - const { setBreadcrumb } = useBreadcrumb() const { setSystemInfo } = useSystemContext() const [showClassify, setShowClassify] = useState(true) const [showAI, setShowAI] = useState(false) @@ -355,15 +353,6 @@ const SystemConfig = forwardRef((_, ref) => { if (serviceId !== undefined) { setOnEdit(true) getSystemInfo() - setBreadcrumb([ - { - title: $t('服务'), - onClick: () => navigate('/service/list') - }, - { - title: $t('设置') - } - ]) } else { getProviderOptionList() setOnEdit(false) diff --git a/frontend/packages/core/src/pages/system/SystemInsideDocument.tsx b/frontend/packages/core/src/pages/system/SystemInsideDocument.tsx index 719bd653..cfa226f1 100644 --- a/frontend/packages/core/src/pages/system/SystemInsideDocument.tsx +++ b/frontend/packages/core/src/pages/system/SystemInsideDocument.tsx @@ -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() @@ -19,8 +18,6 @@ const ServiceInsideDocument = () => { const [doc, setDoc] = useState() const { fetchData } = useFetch() const { serviceId, teamId } = useParams() - 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() }, []) diff --git a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx index f7d53bca..840ca14e 100644 --- a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx +++ b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx @@ -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() const navigateTo = useNavigate() const [showMenu, setShowMenu] = useState(false) + const { setBreadcrumb } = useBreadcrumb() const getSystemInfo = () => { fetchData>('service/info', { method: 'GET', eoParams: { team: teamId, service: serviceId } - }).then(response => { + }).then((response) => { const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) { setSystemInfo(data.service) @@ -47,7 +50,7 @@ const SystemInsidePage: FC = () => { fetchData>('service/router/define', { method: 'GET', eoParams: { service: serviceId, team: teamId } - }).then(response => { + }).then((response) => { const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) { setApiPrefix(data.prefix) @@ -65,6 +68,7 @@ const SystemInsidePage: FC = () => { 'assets', null, [ + getItem({$t('总览')}, 'overview', undefined, undefined, undefined, ''), getItem( {$t('API 路由')}, 'route', @@ -146,9 +150,10 @@ const SystemInsidePage: FC = () => { null, [ // APP_MODE === 'pro' ? getItem({$t('调用拓扑图')}, 'topology',undefined,undefined,undefined,'project.mySystem.topology.view'):null, + getItem({$t('设置')}, 'setting', undefined, undefined, undefined, ''), getItem( - {$t('设置')}, - 'setting', + {$t('日志')}, + 'logs', undefined, undefined, undefined, @@ -166,12 +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[]) - 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 ? ( - {$t('服务 ID')}:{serviceId || '-'} - - ) - } - ]} + customBanner={} backUrl="/service/list" >
diff --git a/frontend/packages/core/src/pages/system/SystemInsideSubscriber.tsx b/frontend/packages/core/src/pages/system/SystemInsideSubscriber.tsx index 586e21f8..10aed7a0 100644 --- a/frontend/packages/core/src/pages/system/SystemInsideSubscriber.tsx +++ b/frontend/packages/core/src/pages/system/SystemInsideSubscriber.tsx @@ -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() @@ -39,7 +37,6 @@ const SystemInsideSubscriber: FC = () => { const pageListRef = useRef(null) const [memberValueEnum, setMemberValueEnum] = useState([]) const { accessData, state } = useGlobalContext() - const navigator = useNavigate() const getSystemSubscriber = () => { return fetchData>( 'service/subscribers', @@ -162,15 +159,6 @@ const SystemInsideSubscriber: FC = () => { ] useEffect(() => { - setBreadcrumb([ - { - title: $t('服务'), - onClick: () => navigator('/service/list') - }, - { - title: $t('订阅方管理') - } - ]) getMemberList() manualReloadTable() }, [serviceId]) diff --git a/frontend/packages/core/src/pages/system/SystemTopology.tsx b/frontend/packages/core/src/pages/system/SystemTopology.tsx index 8bfd47c9..d5cb7601 100644 --- a/frontend/packages/core/src/pages/system/SystemTopology.tsx +++ b/frontend/packages/core/src/pages/system/SystemTopology.tsx @@ -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(null) const { fetchData } = useFetch() const { systemInfo } = useSystemContext() - const { setBreadcrumb } = useBreadcrumb() const [zoomNum, setZoomNum] = useState(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(() => { diff --git a/frontend/packages/core/src/pages/system/api/SystemInsideApiDocument.tsx b/frontend/packages/core/src/pages/system/api/SystemInsideApiDocument.tsx index 77f82539..814a496e 100644 --- a/frontend/packages/core/src/pages/system/api/SystemInsideApiDocument.tsx +++ b/frontend/packages/core/src/pages/system/api/SystemInsideApiDocument.tsx @@ -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() const [loading, setLoading] = useState(false) const [showEditor, setShowEditor] = useState(false) - const { setBreadcrumb } = useBreadcrumb() - const navigator = useNavigate() useEffect(() => { - setBreadcrumb([ - { - title: $t('服务'), - onClick: () => navigator('/service/list') - }, - { - title: $t('API 文档') - } - ]) getApiDetail() }, []) diff --git a/frontend/packages/core/src/pages/system/api/SystemInsideRouterCreate.tsx b/frontend/packages/core/src/pages/system/api/SystemInsideRouterCreate.tsx index 5ce1ecb4..16cf380c 100644 --- a/frontend/packages/core/src/pages/system/api/SystemInsideRouterCreate.tsx +++ b/frontend/packages/core/src/pages/system/api/SystemInsideRouterCreate.tsx @@ -26,7 +26,6 @@ import { SystemInsideRouterCreateHandle, SystemInsideRouterCreateProps } from '../../../const/system/type.ts' -import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx' const SystemInsideRouterCreate = forwardRef( (props, ref) => { @@ -39,14 +38,12 @@ const SystemInsideRouterCreate = forwardRef { 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 { 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 { - 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 + + className="flex-1" + label={$t('路由名称')} + name="name" + rules={[{ required: true, whitespace: true }]} + > + + label={$t('请求协议')} name="protocols" rules={[{ required: true }]}> - 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={} @@ -249,17 +263,6 @@ export default function MonitorApiPage(props: MonitorApiPageProps) { - diff --git a/frontend/packages/dashboard/src/component/MonitorAppPage.tsx b/frontend/packages/dashboard/src/component/MonitorAppPage.tsx index 55667089..7df1c67e 100644 --- a/frontend/packages/dashboard/src/component/MonitorAppPage.tsx +++ b/frontend/packages/dashboard/src/component/MonitorAppPage.tsx @@ -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 (
{$t('重置')} - diff --git a/frontend/packages/dashboard/src/component/MonitorDetailPage.tsx b/frontend/packages/dashboard/src/component/MonitorDetailPage.tsx index 4b0e2c73..55756ca8 100644 --- a/frontend/packages/dashboard/src/component/MonitorDetailPage.tsx +++ b/frontend/packages/dashboard/src/component/MonitorDetailPage.tsx @@ -78,7 +78,17 @@ export default function MonitorDetailPage(props: MonitorDetailPageProps) { const monitorTableRef = useRef(null) const [modalTitle, setModalTitle] = useState($t('调用趋势')) const [queryBtnLoading, setQueryBtnLoading] = useState(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 (
{$t('重置')} -
diff --git a/frontend/packages/dashboard/src/component/MonitorSubPage.tsx b/frontend/packages/dashboard/src/component/MonitorSubPage.tsx index c400b52d..82a7f137 100644 --- a/frontend/packages/dashboard/src/component/MonitorSubPage.tsx +++ b/frontend/packages/dashboard/src/component/MonitorSubPage.tsx @@ -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 (
{$t('重置')} - diff --git a/frontend/packages/dashboard/src/pages/Dashboard.tsx b/frontend/packages/dashboard/src/pages/Dashboard.tsx index 8eb762d7..68e01e5b 100644 --- a/frontend/packages/dashboard/src/pages/Dashboard.tsx +++ b/frontend/packages/dashboard/src/pages/Dashboard.tsx @@ -16,7 +16,7 @@ export default function Dashboard() { const { message } = App.useApp() const [clusters, setClusters] = useState>([]) const [enabledClusters, setEnabledClusters] = useState>([]) - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState(true) const getClusters = () => { setLoading(true) fetchData }>>( @@ -50,11 +50,7 @@ export default function Dashboard() { return ( <> - } - spinning={loading} - > + } spinning={loading}> {!loading && ( <> {enabledClusters.length > 0 ? ( diff --git a/frontend/packages/dashboard/src/pages/DashboardList.tsx b/frontend/packages/dashboard/src/pages/DashboardList.tsx index 573e0170..0fa96714 100644 --- a/frontend/packages/dashboard/src/pages/DashboardList.tsx +++ b/frontend/packages/dashboard/src/pages/DashboardList.tsx @@ -10,8 +10,8 @@ export default function DashboardList() { return ( <> {dashboardType === 'api' && } - {dashboardType === 'subscriber' && } - {dashboardType === 'provider' && } + {dashboardType === 'service' && } + {dashboardType === 'consumer' && } ) } diff --git a/frontend/packages/dashboard/src/pages/DashboardTotal.tsx b/frontend/packages/dashboard/src/pages/DashboardTotal.tsx index 7c322233..f91f46bb 100644 --- a/frontend/packages/dashboard/src/pages/DashboardTotal.tsx +++ b/frontend/packages/dashboard/src/pages/DashboardTotal.tsx @@ -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> = (body: SearchBody) => - fetchData>('monitor/overview/summary', { - method: 'POST', - eoBody: body, - eoTransformKeys: ['request_summary', 'proxy_summary'] - }) - - const fetchInvokeData: (body: SearchBody) => Promise> = (body: SearchBody) => - fetchData>('monitor/overview/invoke', { - method: 'POST', - eoBody: body, - eoTransformKeys: ['request_total', 'request_rate', 'proxy_total', 'proxy_rate', 'time_interval'] - }) + /** 默认时间 */ + const [defaultTime] = useState('day') + /** 当前选中的时间范围 */ + const [timeRange, setTimeRange] = useState() + /** 当前激活的标签 */ + const [activeTab, setActiveTab] = useState('REST') + /** 面板 loading */ + const [dashboardLoading, setDashboardLoading] = useState(false) + /** 总数数据 */ + const [barChartInfo, setBarChartInfo] = useState() + /** 平均值数据 */ + const [perBarChartInfo, setPerBarChartInfo] = useState() + /** 排名表格数据 */ + const [topRankingList, setTopRankingList] = useState([]) + /** 弹窗组件 */ + const { message } = App.useApp() + /** 全局状态 */ + const { state } = useGlobalContext() + /** AI 服务数据 */ + const [aiServiceOverview, setAiServiceOverview] = useState() + /** REST 服务数据 */ + const [restServiceOverview, setRestServiceOverview] = useState() + /** 时间选择回调 */ + const selectCallback = (date: TimeRange) => { + setTimeRange(date) + } - const fetchMessageData: (body: SearchBody) => Promise> = (body: SearchBody) => - fetchData>('monitor/overview/message', { - method: 'POST', - eoBody: body, - eoTransformKeys: ['time_interval', 'request_message', 'response_message'] + /** 获取 AI 服务信息 */ + const getAIServiceOverview = () => { + return fetchData>('monitor/overview/chart/ai', { + method: 'GET', + eoParams: { start: timeRange?.start, end: timeRange?.end }, + eoTransformKeys: [ + 'request_overview', + 'token_overview', + 'avg_token_overview', + 'avg_request_per_subscriber_overview', + 'avg_token_per_subscriber_overview', + 'request_total', + 'token_total', + 'avg_token', + '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)) + } }) + } - const fetchTableData: ( - body: SearchBody, - type: 'api' | 'subscribers' | 'providers' - ) => Promise> = ( - body: SearchBody, - type: 'api' | 'subscribers' | 'providers' - ) => - fetchData>('monitor/overview/top10', { - method: 'POST', - eoBody: { ...body, dataType: type }, + /** 获取 REST 服务信息 */ + const getRestServiceOverview = () => { + return fetchData>('monitor/overview/chart/rest', { + method: 'GET', + eoParams: { start: timeRange?.start, end: timeRange?.end }, eoTransformKeys: [ - 'dataType', + 'request_overview', + 'traffic_overview', + 'avg_request_per_subscriber_overview', + 'avg_response_time_overview', + 'avg_traffic_per_subscriber_overview', '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' + '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)) + } }) + } - 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()}` - ) + /** + * 设置 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>( + `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 ( - +
+ +
+
{$t('服务')}:
+
+
setActiveTab('REST')} + > + REST +
+
setActiveTab('AI')} + > + AI +
+
+ +
+ +
+ +
+
+ } + spinning={dashboardLoading} + > +
+
+ {barChartInfo?.map((item: BarChartInfo, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'py-[15px] px-[0px]' + }} + > + + + ))} +
+
+ {perBarChartInfo?.map((item: any, index: number) => ( + 0 ? 'ml-[10px]' : ''}`} + classNames={{ + body: 'py-[15px] px-[0px]' + }} + > + {item.type === 'area' ? ( + <> + + + ) : ( + + )} + + ))} +
+ +
+
+ +
) } diff --git a/frontend/packages/dashboard/src/utils/dashboard.ts b/frontend/packages/dashboard/src/utils/dashboard.ts index d81a0f60..58b20cb6 100644 --- a/frontend/packages/dashboard/src/utils/dashboard.ts +++ b/frontend/packages/dashboard/src/utils/dashboard.ts @@ -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 类似,但对小于1000的数字也进行保留1位小数的格式化 + * - 小于1000:保留1位小数 + * - 千级别(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' // 转换为拉字节 + } +} diff --git a/frontend/packages/market/src/components/aoplatform/RenderRoutes.tsx b/frontend/packages/market/src/components/aoplatform/RenderRoutes.tsx index 001117b8..1ecfe444 100644 --- a/frontend/packages/market/src/components/aoplatform/RenderRoutes.tsx +++ b/frontend/packages/market/src/components/aoplatform/RenderRoutes.tsx @@ -53,7 +53,7 @@ const PUBLIC_ROUTES: RouteConfig[] = [ key: uuidv4(), children: [ { - path: 'serviceHub', + path: 'portal', component: , key: uuidv4(), children: [ @@ -151,7 +151,7 @@ const PUBLIC_ROUTES: RouteConfig[] = [ }, { path: '*', - component: , + component: , key: uuidv4() } ] diff --git a/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx b/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx index 82e41dd5..466c05a4 100644 --- a/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx +++ b/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx @@ -20,6 +20,7 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' import McpToolsContainer from '@core/pages/mcpService/McpToolsContainer.tsx' import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx' import TopBreadcrumb from '@common/components/aoplatform/Breadcrumb.tsx' +import ServiceInfoCard from '@common/components/aoplatform/serviceInfoCard.tsx' type TabItemType = { key: string @@ -32,18 +33,12 @@ const ServiceHubDetail = () => { const { serviceId } = useParams() const { setBreadcrumb } = useBreadcrumb() const [serviceBasicInfo, setServiceBasicInfo] = useState() - const [serviceName, setServiceName] = useState() - const [serviceDesc, setServiceDesc] = useState() const [serviceDoc, setServiceDoc] = useState() const { fetchData } = useFetch() const applyRef = useRef(null) const { modal, message } = App.useApp() const [mySystemOptionList, setMySystemOptionList] = useState() const [service, setService] = useState() - const [serviceMetrics, setServiceMetrics] = useState<{ title: string; icon: React.ReactNode; value: string }[]>([]) - const [serviceTags, setServiceTags] = useState< - { color: string; textColor: string; title: string; content: React.ReactNode }[] - >([]) const [tools, setTools] = useState([]) const [tabItem, setTabItem] = useState([]) const [currentTab, setCurrentTab] = useState('') @@ -149,10 +144,7 @@ servers: apiDoc: modifyApiDoc(data.service.apiDoc, data.service.basic?.invokeAddress) }) setServiceBasicInfo(data.service.basic) - setServiceName(data.service.name) - setServiceDesc(data.service.description) setServiceDoc(DOMPurify.sanitize(data.service.document)) - setServiceMetricsList(data.service.basic) setTabItemList(data.service.basic) } else { message.error(msg || $t(RESPONSE_TIPS.error)) @@ -164,54 +156,6 @@ servers: setCurrentTab(value) } - const setServiceMetricsList = (serviceBasicInfo: ServiceBasicInfoType) => { - // 设置服务指标数据 - setServiceMetrics([ - { - title: 'API 数量', - icon: , - value: serviceBasicInfo.apiNum.toString() - }, - { - title: '接入消费者数量', - icon: , - value: serviceBasicInfo.appNum.toString() - }, - { - title: '30天内调用次数', - icon: , - value: formatInvokeCount(serviceBasicInfo.invokeCount ?? 0) - } - ]) - // 设置服务标签数据 - const tags = [ - { - color: '#7371fc1b', - textColor: 'text-theme', - title: serviceBasicInfo?.catalogue?.name || '-', - content: serviceBasicInfo?.catalogue?.name || '-' - }, - { - color: `#${serviceBasicInfo?.serviceKind === 'ai' ? 'EADEFF' : 'DEFFE7'}`, - textColor: 'text-[#000]', - title: serviceBasicInfo?.serviceKind || '-', - content: SERVICE_KIND_OPTIONS.find((x) => x.value === serviceBasicInfo?.serviceKind)?.label || '-' - } - ] - - // 如果启用了MCP,添加MCP标签 - if (serviceBasicInfo?.enableMcp) { - tags.push({ - color: '#FFF0C1', - textColor: 'text-[#000]', - title: 'MCP', - content: 'MCP' - }) - } - - setServiceTags(tags) - } - useEffect(() => { if (!serviceId) { console.warn('缺少serviceId') @@ -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: ( ), @@ -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 (
- navigate(`/serviceHub/list`)} /> + navigate(`/portal/list`)} />
- -
-
-
- - ) : undefined - } - icon={serviceBasicInfo?.logo ? '' : } - > - {' '} - -
-
-

- {serviceName} -

-
- {serviceTags.map((tag, index) => ( - - {tag.content} - - ))} - {serviceMetrics.map((item, index) => ( - - - {item.icon} - {item.value} - - - ))} -
-
-
- - {serviceDesc || $t('暂无服务描述')} - -
-
- -
-
+ customClassName="mt-[20px]" + actionSlot={ + <> + + + } + />
window.open(`/serviceHub/detail/${item.service.id}`, '_blank')} + onClick={() => window.open(`/portal/detail/${item.service.id}`, '_blank')} > {$t('API 文档')}