feat:openapi ui

This commit is contained in:
maggieyyy
2024-08-30 19:22:38 +08:00
parent 100841d6f7
commit 2bdd24180c
37 changed files with 844 additions and 335 deletions
+1
View File
@@ -44,6 +44,7 @@
"react-i18next": "^15.0.1",
"react-joyride": "^2.8.2",
"react-router-dom": "^6.20.0",
"swagger-ui-react": "^5.17.14",
"tailwindcss": "^3.3.5",
"uuid": "^9.0.1",
"vite-tsconfig-paths": "^4.3.2"
@@ -132,7 +132,12 @@ const PUBLIC_ROUTES:RouteConfig[] = [
{
path:'api',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideApiList.tsx')),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideApiDocument.tsx')),
},
{
path:'router',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideRouterList')),
},
{
path:'upstream',
@@ -207,7 +212,7 @@ const PUBLIC_ROUTES:RouteConfig[] = [
}
]
},{
path:'dashboardsetting',
path:'datasourcing',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideDashboardSetting.tsx')),
},
@@ -0,0 +1 @@
<svg width="130" height="80" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="52.348%" y1="74.611%" x2="52.348%" y2="-17.635%" id="a"><stop stop-color="#DEDEDE" stop-opacity="0" offset="0%"/><stop stop-color="#A9A9A9" stop-opacity=".3" offset="100%"/></linearGradient><linearGradient x1="44.79%" y1="100%" x2="44.79%" y2="0%" id="b"><stop stop-color="#FFF" stop-opacity="0" offset="0%"/><stop stop-color="#96A1C5" stop-opacity=".373" offset="100%"/></linearGradient><linearGradient x1="50%" y1="100%" x2="50%" y2="-19.675%" id="c"><stop stop-color="#FFF" stop-opacity="0" offset="0%"/><stop stop-color="#919191" stop-opacity=".15" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="50%" y2="44.95%" id="d"><stop stop-color="#5389F5" offset="0%"/><stop stop-color="#416FDC" offset="100%"/></linearGradient><linearGradient x1="63.345%" y1="100%" x2="63.345%" y2="-5.316%" id="e"><stop stop-color="#DCE9FF" offset="0%"/><stop stop-color="#B6CFFF" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="f"><stop stop-color="#7CA5F7" offset="0%"/><stop stop-color="#C4D6FC" offset="100%"/></linearGradient></defs><g transform="translate(-1.866 .364)" fill="none" fill-rule="evenodd"><path d="M27.94 14.864c1.326-4.192 2.56-6.802 3.7-7.831 3.157-2.848 7.522-1.298 8.45-1.076 3.26.782 2.2-4.364 4.997-5.41 1.864-.697 3.397.155 4.6 2.556C50.752.863 52.375-.163 54.556.02c3.272.277 4.417 11.328 8.913 8.909 4.497-2.42 10.01-2.973 12.365.623.509.778.704-.429 4.166-4.55C83.462.88 86.914-.936 93.996 1.464c3.22 1.09 5.868 4.045 7.947 8.864 0 6.878 5.06 10.95 15.178 12.213 15.179 1.895 3.397 18.214-15.178 22.993-18.576 4.78-61.343 7.36-84.551-4.716C1.92 32.769 5.436 24.117 27.939 14.864z" fill="url(#a)" opacity=".8"/><ellipse fill="url(#b)" cx="66" cy="69.166" rx="27.987" ry="6.478"/><path d="M113.25 77.249c-21.043 5.278-92.87-.759-100.515-3.516-3.721-1.343-7.075-3.868-10.061-7.576a2.822 2.822 0 0 1 2.198-4.593h125.514c2.605 6.938-3.107 12.166-17.136 15.685z" fill="url(#c)" opacity=".675"/><g fill-rule="nonzero"><path d="M43.396 12.098L33.825.906a2.434 2.434 0 0 0-1.837-.86h-20.58c-.706 0-1.377.324-1.837.86L0 12.098v6.144h43.396v-6.144z" fill="url(#d)" transform="translate(44.08 39.707)"/><path d="M40.684 18.468L32.307 8.72a2.136 2.136 0 0 0-1.622-.725H12.711c-.617 0-1.22.256-1.622.725l-8.377 9.748v5.354h37.972v-5.354z" fill="url(#e)" transform="translate(44.08 39.707)"/><path d="M43.396 25.283c0 .853-.384 1.62-.99 2.134l-.123.1a2.758 2.758 0 0 1-1.67.56H2.784c-.342 0-.669-.062-.971-.176l-.15-.06A2.802 2.802 0 0 1 0 25.282V12.165h10.529c1.163 0 2.1.957 2.1 2.118v.015c0 1.162.948 2.099 2.111 2.099h13.916a2.113 2.113 0 0 0 2.111-2.107c0-1.166.938-2.125 2.1-2.125h10.53z" fill="url(#f)" transform="translate(44.08 39.707)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,39 @@
import React from "react"
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
export default function ApiDocument({spec}:{spec?:string|object}) {
class OperationsLayout extends React.Component {
render() {
const {
getComponent
} = this.props
const Operations = getComponent("operations", true)
return (
<div className="swagger-ui">
<Operations />
</div>
)
}
}
// Create the plugin that provides our layout component
const OperationsLayoutPlugin = () => {
return {
components: {
OperationsLayout: OperationsLayout
}
}
}
return(
<SwaggerUI
spec={spec}
supportedSubmitMethods={[]}
customComponents={{Header:()=>null}}
layout="OperationsLayout"
plugins={[OperationsLayoutPlugin ]} />
)
}
@@ -57,9 +57,9 @@ const themeToken = {
getNavItem(<a>{$t('API 市场')}</a>, 'serviceHub','/serviceHub',<Icon icon="ic:baseline-hub" width="18" height="18"/>,undefined,undefined,'system.workspace.api_market.view'),
getNavItem($t('仪表盘'), 'mainPage', APP_MODE === 'pro' ? '/dashboard' : '/dashboard/total',<Icon icon="ic:baseline-bar-chart" width="18" height="18"/>,[
getNavItem(<a >{$t('运行视图')}</a>, 'dashboard',APP_MODE === 'pro' ? '/dashboard' : '/dashboard/total' ,<ProjectFilled />,undefined,undefined,'system.dashboard.dashboard.view'),
getNavItem(<a >{$t('运行视图')}</a>, 'dashboard',APP_MODE === 'pro' ? '/dashboard' : '/dashboard/total' ,<ProjectFilled />,undefined,undefined,'system.dashboard.run_view.view'),
APP_MODE === 'pro' ? getNavItem(<a >{$t('系统拓扑图')}</a>, 'systemrunning','/systemrunning',<ProjectFilled />,undefined,undefined,'system.dashboard.systemrunning.view') : null,
]),
],undefined,'system.dashboard.run_view.view'),
getNavItem($t('系统设置'), 'operationCenter','/member',<Icon icon="ic:baseline-settings" width="18" height="18"/>, [
getNavItem($t('组织'), 'organization','/member',null,[
@@ -72,7 +72,7 @@ const themeToken = {
getNavItem($t('运维与集成'), 'maintenanceCenter','/cluster', null, [
getNavItem(<a>{$t('集群')}</a>, 'cluster','/cluster',<Icon icon="ic:baseline-device-hub" width="18" height="18"/>,undefined,undefined,'system.devops.cluster.view'),
getNavItem(<a>{$t('监控报表')}</a>, 'dashboardsetting','/dashboardsetting',<Icon icon="ic:baseline-monitor-heart" width="18" height="18"/>,undefined,undefined,'system.devops.dashboardsetting.view'),
getNavItem(<a>{$t('数据源')}</a>, 'datasourcing','/datasourcing',<Icon icon="ic:baseline-monitor-heart" width="18" height="18"/>,undefined,undefined,'system.devops.data_source.view'),
getNavItem(<a>{$t('证书')}</a>, 'cert','/cert',<Icon icon="ic:baseline-security" width="18" height="18"/>,undefined,undefined,'system.devops.ssl_certificate.view'),
getNavItem(<a>{$t('日志')}</a>, 'logsettings','/logsettings',<Icon icon="ic:baseline-sticky-note-2" width="18" height="18"/>,undefined,undefined,'system.devops.log_configuration.view'),
APP_MODE === 'pro' ? getNavItem(<a>{$t('资源')}</a>, 'resourcesettings','/resourcesettings',null,undefined,undefined,'system.partition.self.view'):null,
@@ -91,7 +91,7 @@ const EditableTableWithModal = <T extends { _id?: string }>({
title:$t(title),
dataIndex: key as string,
key: key as string,
render: renderText ? (value, record) => $t(renderText(value, record)) : undefined,
render: renderText ? (value, record) => $t(renderText(value, record) || '') : undefined,
ellipsis:true
})),
...(disabled ? []:[{
@@ -2,12 +2,14 @@ import {App, Col, Form, Input, Row, Table, Tooltip} from "antd";
import {forwardRef, useEffect, useImperativeHandle, useMemo} from "react";
import {PublishApprovalInfoType, PublishApprovalModalHandle, PublishApprovalModalProps, PublishVersionTableListItem} from "@common/const/approval/type.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE} from "@common/const/const.tsx";
import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, STATUS_COLOR, VALIDATE_MESSAGE} from "@common/const/const.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from "@core/const/system/const.tsx";
import { $t } from "@common/locales";
import { ApprovalApiColumns, ApprovalStatusColorClass, ApprovalUpstreamColumns, ChangeTypeEnum } from "@common/const/approval/const";
import { ApprovalRouteColumns, ApprovalStatusColorClass, ApprovalUpstreamColumns, ChangeTypeEnum } from "@common/const/approval/const";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { LoadingOutlined } from "@ant-design/icons";
import { SystemInsidePublishOnlineItems } from "@core/pages/system/publish/SystemInsidePublishOnline";
export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle,PublishApprovalModalProps>((props, ref) => {
@@ -107,7 +109,7 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
})),[state.language])
const translatedApiColumns = useMemo(()=>ApprovalApiColumns.map((x)=>({
const translatedRouteColumns = useMemo(()=>ApprovalRouteColumns.map((x)=>({
...x,
...(x.dataIndex === 'change' ? {
render:(_,entity)=>(
@@ -122,6 +124,22 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
title: typeof x.title === 'string' ? $t(x.title) : x.title,
})),[state.language])
const translatedPublishColumns = useMemo(()=>SYSTEM_PUBLISH_ONLINE_COLUMNS.map((x)=>{
if(x.dataIndex === 'status'){
return {...x,title:$t(x.title),
render:(_:unknown,entity:SystemInsidePublishOnlineItems)=>{
switch(entity.status){
case 'done':
return <span className={STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]}>{$t('成功')}</span>
case 'error':
return <Tooltip title={entity.error || $t('上线失败')}><span className={`${STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]} truncate block`}>{$t('失败')} {entity.error}</span></Tooltip>
default:
return <LoadingOutlined className="text-theme" spin />
}
}}
}
}),[state.language])
return (
<>
{!insideSystem && <>
@@ -180,11 +198,11 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('API 列表')}</span></Row>
<Row className="mb-mbase ">
<Table
columns={translatedApiColumns}
columns={translatedRouteColumns}
bordered={true}
rowKey="id"
size="small"
dataSource={data.diffs?.apis || []}
dataSource={data.diffs?.routers || []}
pagination={false}
/></Row>
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('上游列表')}</span></Row>
@@ -217,11 +235,12 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
]);}}/>
</Form.Item>} */}
{['error','done'].indexOf(data.status) !== -1 && data.clusterPublishStatus &&data.clusterPublishStatus.length > 0 && <> <Row className="text-left h-[32px] mb-8px]" span={3}><span>线</span></Row>
{['error','done'].indexOf(data.status) !== -1 && data.clusterPublishStatus &&data.clusterPublishStatus.length > 0 && <>
<Row className="text-left h-[32px] mb-8px]" span={3}><span>{$t('上线情况')}</span></Row>
<Row span={24} className="mb-mbase">
<Table
bordered={true}
columns={[...SYSTEM_PUBLISH_ONLINE_COLUMNS]}
columns={[...translatedPublishColumns]}
size="small"
rowKey="id"
dataSource={data.clusterPublishStatus || []}
@@ -4,46 +4,10 @@ import { $t } from "@common/locales"
export const TranslateWord = ()=>{
return (
<>
{$t('文件日志')}
{$t('HTTP日志')}
{$t('Kafka日志')}
{$t('NSQ日志')}
{$t('Syslog日志')}
{$t('未分配')}
{$t('超级管理员')}
{$t('团队管理员')}
{$t('运维管理员')}
{$t('普通成员')}
{$t('只读成员')}
{$t('服务管理员')}
{$t('服务开发者')}
{$t('应用开发者')}
{$t('应用管理员')}
{$t('驱动名称')}
{$t('请求失败数')}
{$t('转发失败数')}
{$t('作用范围')}
{$t('添加条目')}
{$t('添加地址')}
{$t('文件名称')}
{$t('存放目录')}
{$t('日志分割周期')}
{$t('过期时间')}
{$t('单位:天')}
{$t('输出格式')}
{$t('格式化配置')}
{$t('服务器地址')}
{$t('Access日志')}
{$t('NSQD地址列表')}
{$t('鉴权Secret')}
{$t('网络协议')}
{$t('日志等级')}
{$t('单行')}
{$t('小时')}
{$t('天')}
{$t('未发布')}
{$t('待发布')}
{$t('单位:s,最小值:1')}
{$t('上传文件')}
{$t('替换文件')}
{$t('是否放行')}
{$t('监控')}
</>
)
}
@@ -4,6 +4,7 @@ import { ReactElement, cloneElement, useEffect, useMemo, useState } from "reac
import { useGlobalContext } from "../../contexts/GlobalStateContext";
import { PERMISSION_DEFINITION } from "@common/const/permissions";
import { $t } from "@common/locales";
import { last } from "lodash-es";
type WithPermissionProps = {
access?:string | string[]
@@ -36,7 +37,7 @@ const WithPermission = ({access, tooltip, children,disabled, showDisabled = true
{editAccess && disabled && <Tooltip title={tooltip}>
{ cloneElement(children, {disabled:true})}
</Tooltip>}
{!editAccess && (children?.type?.displayName !== 'Button' && showDisabled ) && <Tooltip title={tooltip ?? $t("暂无操作权限,请联系管理员分配。")}>
{!editAccess && (children?.type?.displayName !== 'Button' && children?.type?.displayName !== 'Upload' && showDisabled ) && <Tooltip title={tooltip ?? $t("暂无操作权限,请联系管理员分配。")}>
{ cloneElement(children, {disabled:true})}
</Tooltip>}
@@ -22,8 +22,10 @@ interface CodeboxProps {
height?: string | null
readOnly?: boolean
apiRef?: RefObject<CodeboxApiRef>
language?: 'html' | 'json' | 'xml' | 'javascript' | 'css' | 'plaintext'
language?: 'html' | 'json' | 'xml' | 'javascript' | 'css' | 'plaintext'|'yaml'
extraContent?:React.ReactNode
sx?:Record<string,unknown>
editorTheme?:'vs' | 'vs-dark' | 'hc-black'
}
export const Codebox = memo((props: CodeboxProps) => {
@@ -37,7 +39,8 @@ export const Codebox = memo((props: CodeboxProps) => {
apiRef,
readOnly = false,
language = 'plaintext',
extraContent
extraContent,
editorTheme = 'vs'
} = props
const [code, setCode] = useState<string>(``)
@@ -153,7 +156,8 @@ export const Codebox = memo((props: CodeboxProps) => {
sx={{
// border: `1px solid ${theme.palette.divider}`,
height: '100%',
width: '100%'
width: '100%',
...props.sx
}}
>
{enableToolbar ? (<>
@@ -189,6 +193,7 @@ export const Codebox = memo((props: CodeboxProps) => {
value={isControlled ? controlledValue : code}
options={{ ...defaultOptions, ...options }}
onChange={handleEditorChange}
theme={editorTheme}
/>
</Box>
)
@@ -212,22 +212,23 @@ export const ApprovalStatusColorClass = {
export const ApprovalApiColumns = [
{
title:('API 名称'),
dataIndex:'name',
ellipsis:true
},
export const ApprovalRouteColumns = [
{
title:('请求方式'),
dataIndex:'method',
ellipsis:true
ellipsis:true,
renderText:(value)=>value.join(',')
},
{
title:('路径'),
dataIndex:'path',
ellipsis:true
},
{
title:('描述'),
dataIndex:'description',
},
{
title:('类型'),
dataIndex:'change',
+1 -1
View File
@@ -26,7 +26,7 @@ export const routerKeyMap = new Map<string, string[]|string>([
['operationCenter',['member','user','role','servicecategories']],
['organization',['member','user','role']],
['serviceHubSetting',['servicecategories']],
['maintenanceCenter',['dashboardsetting','cluster','cert','logsettings','resourcesettings','openapi']
['maintenanceCenter',['datasourcing','cluster','cert','logsettings','resourcesettings','openapi']
]])
@@ -139,9 +139,19 @@ export const PERMISSION_DEFINITION = [
"anyOf": [{ "backend": ["system.api_market.service_classification.manager"] }]
}
},
"system.devops.dashboardsetting.view":{
"system.dashboard.run_view.view":{
"granted": {
"anyOf": [{ "backend": ['system.dashboard.run_view.view'] }]
}
},
"system.devops.data_source.view":{
"grented":{
"anyOf":[{"backend":[]}]
"anyOf":[{"backend":['system.devops.data_source.view']}]
}
},
"system.devops.data_source.edit":{
"grented":{
"anyOf":[{"backend":['system.devops.data_source.manager']}]
}
},
"system.devops.cluster.view": {
@@ -239,34 +249,44 @@ export const PERMISSION_DEFINITION = [
"anyOf": [{ "backend": [] }]
}
},
"team.service.api.view": {
"team.service.api_doc.view": {
"granted": {
"anyOf": [{ "backend": ["team.service.api.view"] }]
"anyOf": [{ "backend": ["team.service.api_doc.view"] }]
}
},
"team.service.api.add": {
"team.service.api_doc.add": {
"granted": {
"anyOf": [{ "backend": ["team.service.api.manager"] }]
"anyOf": [{ "backend": ["team.service.api_doc.manager"] }]
}
},
"team.service.api.edit": {
"team.service.api_doc.edit": {
"granted": {
"anyOf": [{ "backend": ["team.service.api.manager"] }]
"anyOf": [{ "backend": ["team.service.api_doc.manager"] }]
}
},
"team.service.api.copy": {
"team.service.api_doc.import": {
"granted": {
"anyOf": [{ "backend": ["team.service.api.manager"] }]
"anyOf": [{ "backend": ["team.service.api_doc.manager"] }]
}
},
"team.service.api.delete": {
"team.service.router.view": {
"granted": {
"anyOf": [{ "backend": ["team.service.api.manager"] }]
"anyOf": [{ "backend": ["team.service.router.view"] }]
}
},
"team.service.api.import": {
"team.service.router.add": {
"granted": {
"anyOf": [{ "backend": ["team.service.api.manager"] }]
"anyOf": [{ "backend": ["team.service.router.manager"] }]
}
},
"team.service.router.edit": {
"granted": {
"anyOf": [{ "backend": ["team.service.router.manager"] }]
}
},
"team.service.router.delete": {
"granted": {
"anyOf": [{ "backend": ["team.service.router.manager"] }]
}
},
"team.service.upstream.view": {
@@ -109,12 +109,17 @@ team:
cname: API
value: 'api'
children:
- team.service.api.view
- team.service.api.add
- team.service.api.edit
- team.service.api.copy
- team.service.api.delete
- team.service.api.import
- team.service.api_doc.view
- team.service.api_doc.add
- team.service.api_doc.edit
- name: route
cname: route
value: 'route'
children:
- team.service.router.view
- team.service.router.add
- team.service.router.edit
- team.service.router.delete
- name: upstream
cname: 上游
value: 'upstream'
+1 -1
View File
@@ -140,7 +140,7 @@ type EoHeaders = Headers | {[k:string]:string}
export function useFetch(){
function fetchData<T>(url:string, options: EoRequest ) {
// 合并传入的headers与默认headers
const headers = { ...DEFAULT_HEADERS, ...options.headers };
const headers = { ...(options.body ? {}:DEFAULT_HEADERS), ...options.headers };
// 检查是否需要转换键
const shouldTransformKeys = !shouldNotTransform(url) && options?.eoTransformKeys && options?.eoTransformKeys?.length > 0;
@@ -5,7 +5,7 @@
"Kfe93ef35": "Applications",
"Kb58e0c3f": "Services",
"Kc9e489f5": "Team",
"K61c89f5f": "API Marketplace",
"K61c89f5f": "API Portal",
"K16d71239": "Dashboard",
"K714c192d": "Runtime",
"Kd57dfe97": "Topology",
@@ -24,7 +24,7 @@
"K6535ff9c": "Account Settings",
"Kf15499b4": "Log Out",
"Kabbd6e6": "Documentation",
"K1196b104": "APIPark - API Open Platform",
"K1196b104": "APIPark - API Developer Portal",
"K1f42de3": "HTTP Status Codes",
"K4770dff4": "System Status Codes",
"Kf89e58f1": "Description",
@@ -336,15 +336,15 @@
"K3818f03d": "Approval",
"K56b4254f": "Publishing Application",
"Kd518ba3e": "Hello! Welcome to APIPark",
"Ke66e4182": "APIPark allows you to quickly build an API open portal/marketplace within your enterprise, offering extreme forwarding performance, API observability, service governance, multi-tenant management, subscription approval processes, and many other benefits.",
"Ke66e4182": "APIPark allows you to quickly build an API open portal within your enterprise, offering extreme forwarding performance, API observability, service governance, multi-tenant management, subscription approval processes, and many other benefits.",
"Kedd41c18": "If you like our product, please consider giving us a Star or providing feedback.",
"Kef02fd87": "Quick Start",
"K43a3b38d": "We've provided some tasks to help you quickly get acquainted with APIPark.",
"Kc8239422": "Teams include personnel, applications, and services. Data between different teams is isolated, and can be used to manage different departments/project teams/teams within the enterprise.",
"Kd5be0cd7": "Services include a set of APIs and can be published to the API Marketplace for use by other teams.",
"K4ea67613": "Applications serve as identities for applying for services and calling APIs. They can apply for service calls in the API Marketplace, and each application has its own independent API access authentication.",
"Kd5be0cd7": "Services include a set of APIs and can be published to the API Portal for use by other teams.",
"K4ea67613": "Applications serve as identities for applying for services and calling APIs. They can apply for service calls in the API Portal, and each application has its own independent API access authentication.",
"Ka4748416": "Search for Services and APIs",
"K383e17e5": "You can view all public services in the API Marketplace.",
"K383e17e5": "You can view all public services in the API Portal.",
"K8f7808e6": "Subscribe to Services",
"Kb0755523": "To call an API of a particular service, you need to subscribe to the service first and wait for approval from the team providing the service before initiating API requests.",
"Kd28a1aa5": "Review Subscription Applications",
@@ -442,7 +442,7 @@
"K427a5bd5": "Only .png .jpg .jpeg .svg Format Images Are Supported, Files Larger Than 1KB Will Be Compressed",
"K44bc352d": "Logo",
"Kf52a584d": "Service Category",
"K72b21be5": "Set the Category Where the Service Is Displayed in the Service Marketplace",
"K72b21be5": "Set the Category Where the Service Is Displayed in the Service Portal",
"Kde6bae17": "Delete Service",
"K885ea699": "This Action Is Irreversible, Please Proceed with Caution!",
"K617f34f1": "Updated By",
@@ -582,7 +582,7 @@
"K3c7b175f": "Number of Subscribed Services: (0) Approved, (1) Pending",
"K850b4b2d": "Status Code",
"Kbe3e9335": "Exit Test",
"K370a3eb2": "Service Marketplace",
"K370a3eb2": "Service Portal",
"Kf7ec36d": "Service Details",
"K59cdbec3": "Introduction",
"K4aa9ed2c": "Apply",
@@ -641,5 +641,36 @@
"K3509a9f8": "Day",
"Kb3960e83": "Unpublished",
"K8bd1e18": "Pending",
"K225a6c43": "Unit: Seconds, Minimum Value: 1"
"K225a6c43": "Unit: Seconds, Minimum Value: 1",
"Ka450909c": "Organization",
"K62933442": "View System Roles",
"Kd677d04a": "View Team Roles",
"Kd352fa1d": "API Portal",
"K39280ee": "Operations",
"K4bf109e8": "View All Applications",
"Keceae2": "View All Services",
"K7c866f28": "View All Teams",
"Kf9dcef3a": "Routing",
"K6134bbe8": "Add Route",
"Kad6d2797": "Search Routes by Name and URL",
"K28435c5c": "Route Details",
"Kfa088d49": "Configure Cluster and Enable Monitoring",
"K3da3b9a0": "Monitoring function is used to assist in managing cluster information. Please configure the cluster and set up monitoring information before viewing the current cluster monitoring status.",
"Kaddacfb": "Cluster Configuration",
"K4ac33975": "Configure cluster address to ensure monitoring system can correctly identify and connect to the cluster",
"Ke5ed9810": "Configure Cluster Information",
"K1a132228": "Monitoring Settings",
"K6af08c3c": "Configure Monitoring Information",
"K7e52ffa3": "Deployment Status",
"Kad1c674c": "Protocol",
"Kad01bc3e": "Method",
"Kca1dc104": "Open Editor",
"Kba92c499": "Intercept Requests to This Interface",
"Kb7df6ac1": "Block",
"K5c1722fe": "Allow",
"Ke00c858c": "Upload File",
"K6d9dd1f5": "Replace File",
"K71753476": "Allow Access",
"K597435c5": "Runtime",
"Kde9d6e8e": "Once interception is enabled, the gateway will block all traffic to this endpoint, similar to a firewall restricting access to a specific resource."
}
@@ -1,2 +1,5 @@
{
"K71753476": "是否放行",
"K597435c5": "监控",
"Kde9d6e8e": "开启拦截后,网关会拦截所有该路径的请求,相当于防火墙禁用了特定路径的访问。"
}
@@ -17,8 +17,6 @@ export const checkAccess:(access:AccessDataType, accessData:Map<string,string[]>
}
const hasIntersection = (arr1:string[], arr2:string[])=> {
// 当没有对应后端权限字段时,默认有权限
if(arr1.length === 0) return true
const set = new Set(arr1.length > arr2.length ? arr2:arr1)
const arr = arr1.length > arr2.length ? arr1:arr2
for (const item of arr) {
@@ -16,7 +16,6 @@ describe('useCopyToClipboard', () => {
globalThis.navigator = {};
}
// 确保 clipboard 对象存在
console.log(globalThis.navigator, navigator,navigator.clipboard)
if (!navigator.clipboard) {
// @ts-expect-error clipboard object may not exist in some environments
navigator.clipboard = {};
+2 -4
View File
@@ -15,10 +15,8 @@
},
"dependencies": {
"@tinymce/tinymce-react": "^4.3.2",
"tinymce": "^6.8.1",
"fs-extra": "^11.2.0",
"highlight.js": "^11.9.0",
"fs-extra": "^11.2.0"
},
"devDependencies": {
"tinymce": "^6.8.1"
}
}
+13
View File
@@ -208,4 +208,17 @@ a{
overflow: hidden;
border-radius: 10px;
border:1px solid var(--table-border-color) !important;
}
.swagger-ui{
width: 100%;
.model-box-control:focus,.models-control:focus, .opblock-summary-control:focus{
outline:unset !important;
}
.information-container{
.info{
display: none;
}
}
}
@@ -139,7 +139,12 @@ const PUBLIC_ROUTES:RouteConfig[] = [
{
path:'api',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideApiList.tsx')),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideApiDocument.tsx')),
},
{
path:'route',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideRouterList')),
},
{
path:'upstream',
@@ -215,7 +220,7 @@ const PUBLIC_ROUTES:RouteConfig[] = [
]
},
{
path:'dashboardsetting',
path:'datasourcing',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideDashboardSetting.tsx')),
},
@@ -71,6 +71,10 @@ export const SYSTEM_I18NEXT_FOR_ENUM = {
}
export const HTTP_METHOD = ['GET','POST','PUT','DELETE','PATCH','HEAD']
export const API_PROTOCOL = [
{label:'HTTP',value:'http'},
{label:'HTTPS',value:'https'}
]
export const ALGORITHM_ITEM = [
@@ -234,9 +238,6 @@ export const MATCH_CONFIG:ConfigField<MatchItem>[] = [
{
title:('参数位置'),
key: 'position',
component: <Select className="w-INPUT_NORMAL" options={Object.entries(MatchPositionEnum)?.map(([key,value])=>{
return { label:value, value:key}
})}/>,
renderText: (value:keyof typeof MatchPositionEnum) => {
return MatchPositionEnum[value]
},
@@ -251,9 +252,6 @@ export const MATCH_CONFIG:ConfigField<MatchItem>[] = [
}, {
title:('匹配类型'),
key: 'matchType',
component: <Select className="w-INPUT_NORMAL" options={Object.entries(MatchTypeEnum)?.map(([key,value])=>{
return { label:value, value:key}
})}/>,
renderText: (value:keyof typeof MatchTypeEnum) => {
return MatchTypeEnum[value]
},
@@ -272,34 +270,33 @@ export const MATCH_CONFIG:ConfigField<MatchItem>[] = [
export const SYSTEM_API_TABLE_COLUMNS: PageProColumns<SystemApiTableListItem>[] = [
{
title:('名称'),
dataIndex: 'name',
ellipsis:true,
width:160,
fixed:'left',
valueType: 'text',
sorter: (a,b)=> {
return a.name.localeCompare(b.name)
},
title:('URL'),
dataIndex: 'requestPath',
ellipsis:true
},
{
title:('协议/方法'),
title:('协议'),
dataIndex: 'protocols',
ellipsis:true,
renderText:(value)=>value?.join(',')
},
{
title:('方法'),
dataIndex: 'method',
ellipsis:true,
renderText:(value)=>value?.join(',')
},
{
title:'是否放行',
dataIndex:'isDisabled',
ellipsis:true,
filters: true,
onFilter: true,
valueType: 'select',
valueEnum: {
POST: { text: 'POST' },
PUT: { text: 'PUT' },
GET: { text: 'GET' },
DELETE: { text: 'DELETE' },
PATCH: { text: 'PATCH' },
},
valueType: 'select'
},
{
title:('URL'),
dataIndex: 'requestPath',
title:('描述'),
dataIndex: 'description',
ellipsis:true
},
{
@@ -636,16 +633,6 @@ export const SYSTEM_TOPOLOGY_NODE_TYPE_COLOR_MAP = {
ellipsis:{
showTitle:true
},
render:(_:unknown,entity:SystemInsidePublishOnlineItems)=>{
switch(entity.status){
case 'done':
return <span className={STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]}>{('成功')}</span>
case 'error':
return <Tooltip title={entity.error || ('上线失败')}><span className={`${STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]} truncate block`}>{('失败')} {entity.error}</span></Tooltip>
default:
return <LoadingOutlined className="text-theme" spin />
}
}
},
]
+10 -50
View File
@@ -77,49 +77,9 @@ export type SystemMemberTableListItem = {
};
export type SystemApiDetail = {
id:string
name:string
description:string
protocol:Protocol
method:HTTPMethod
path:string
creator:EntityItem
createTime:string
updater:EntityItem
content:string
updateTime:string
match?:MatchItem[]
proxy?:SystemApiProxyType
doc?:{
encoding: string,
tag: string,
requestParams: {
headerParams: HeaderParamsType[],
bodyParams: BodyParamsType[],
queryParams: QueryParamsType[],
restParams: RestParamsType[]
},
resultList: ResultListType[],
responseList: [{
id: number,
responseUuid: string,
apiUuid: string,
oldId: number,
name: string,
httpCode: string,
contentType: ApiBodyType,
isDefault: number,
updateUserId: number,
createUserId: number,
createTime: number,
updateTime: number,
responseParams: {
headerParams: HeaderParamsType[],
bodyParams: BodyParamsType[]
queryParams: QueryParamsType[],
restParams: RestParamsType[]
}
}]
}
updater:string
}
@@ -131,11 +91,11 @@ export type SystemApiProxyType = {
}
export type SystemApiProxyFieldType = {
name: string;
protocols: string[];
id:string;
description?:string;
path:string;
method:string;
method:string[];
match:MatchItem[]
isDisable?: boolean;
service?:string;
@@ -155,16 +115,16 @@ export type SystemApiSimpleFieldType = {
update_time: string
}
export type SystemInsideApiCreateProps = {
type?:'copy'
entity?:SystemApiProxyFieldType &{systemId:string}
export type SystemInsideRouterCreateProps = {
type?:'add'|'edit'|'copy'
entity?:SystemApiTableListItem
modalApiPrefix?:string
modalPrefixForce?:boolean
serviceId:string
teamId:string
}
export type SystemInsideApiCreateHandle = {
export type SystemInsideRouterCreateHandle = {
copy:()=>Promise<boolean|string>;
save:()=>Promise<boolean|string>;
}
@@ -172,14 +132,14 @@ export type SystemInsideApiCreateHandle = {
export type SystemApiTableListItem = {
id:string;
name: string;
method:string;
protocols:string;
requestPath:string;
description:string
creator:EntityItem;
createTime:string;
updater:EntityItem
updateTime:string
canDelete:boolean
};
@@ -38,14 +38,14 @@ const PartitionInsideDashboardSetting:FC = ()=> {
useEffect(() => {
setBreadcrumb([
{title: $t('监控报表')}
{title: $t('数据源')}
])
getDashboardSettingInfo()
}, []);
const setDashboardSettingBtn = ()=>{
return (<>
{showStatus === 'view' && <WithPermission access="" key="changeClusterConfig">
{showStatus === 'view' && <WithPermission access="system.devops.data_source.edit" key="changeClusterConfig">
<Button type="primary" onClick={() => setShowStatus('edit')}>{$t('修改配置')}</Button>
</WithPermission> }</>
)
@@ -54,7 +54,7 @@ const PartitionInsideDashboardSetting:FC = ()=> {
return (
<>
<InsidePage
pageTitle={$t('监控报表')}
pageTitle={$t('数据源')}
description={$t("设置监控报表的数据来源,设置完成之后即可获得详细的API调用统计图表。")}
showBorder={false}
scrollPage={true}
@@ -7,6 +7,7 @@ import { useNavigate, useParams } from "react-router-dom";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
import { ArrowLeftOutlined } from "@ant-design/icons";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
type PermissionItem = {
name:string
@@ -40,6 +41,7 @@ type PermissionInfo = {
const PermissionContent = ({permits,onChange,value=[],id,dependenciesMap}:{permits:PermissionClassify[],dependenciesMap:DependenciesMapType,value:string[],id:string, onChange?: (value:string[]) => void;})=>{
const onSingleCheckboxChange: GetProp<typeof Checkbox, 'onChange'> = (e) => {
if(e.target.checked){
onChange?.(Array.from(new Set([...value, e.target.id, ...(dependenciesMap?.get(e.target.id!)?.dependents || [])] as string[])))
@@ -55,9 +57,9 @@ const PermissionContent = ({permits,onChange,value=[],id,dependenciesMap}:{permi
permits.map((item:PermissionClassify)=>(
<>
<div className="flex flex-col gap-btnbase" key={`group-${item.name}`}>
{item.cname !== '' && <p className="">{item.cname}</p>}
{item.cname !== '' && <p className="">{$t(item.cname)}</p>}
<div className=" pl-[20px]">
{item.children.map(x=><Checkbox id={x.value} key={x.value} checked={value && value.length > 0 && value.indexOf(x.value)>-1} onChange={onSingleCheckboxChange}>{x.cname}</Checkbox>)}
{item.children.map(x=><Checkbox id={x.value} key={x.value} checked={value && value.length > 0 && value.indexOf(x.value)>-1} onChange={onSingleCheckboxChange}>{$t(x.cname)}</Checkbox>)}
</div>
</div>
</>
@@ -69,15 +71,16 @@ const PermissionContent = ({permits,onChange,value=[],id,dependenciesMap}:{permi
const PermissionCollapse:React.FC<PermissionCollapseProps> = (props)=>{
const { id, value = [], onChange,permissionTemplate ,dependenciesMap} = props;
const [openCollapses, setOpenCollapses] = useState<string[]>([])
const {state} = useGlobalContext()
const items = useMemo(()=>{
const generatePermissionItem = (permissionItem:RolePermissionItem[])=> permissionItem.map((item:RolePermissionItem)=>({
key:item.name,
label:item.cname,
label:$t(item.cname),
children:<PermissionContent value={value} permits={item.children} onChange={(e)=>onChange?.(e)} id={id!} dependenciesMap={dependenciesMap!}/>
}))
return permissionTemplate && permissionTemplate.length > 0 ? generatePermissionItem(permissionTemplate) : []
},[permissionTemplate,value])
},[permissionTemplate,value,state.language])
useEffect(()=>{
permissionTemplate && setOpenCollapses(permissionTemplate?.map(x=>x.name))
@@ -100,7 +103,8 @@ const RoleConfig = ()=>{
const [permissionTemplate, setPermissionTemplate] = useState<RolePermissionItem[]>()
const [dependenciesMap, setDependenciesMap] = useState<DependenciesMapType>()
const APP_MODE = import.meta.env.VITE_APP_MODE;
const [permissionInfo, setPermissionInfo] = useState<PermissionInfo>()
const { state } = useGlobalContext()
const generateDependenciesMap = (data:RolePermissionItem[])=>{
const map = new Map<string, {dependents:string[], control:string[]}>()
@@ -161,15 +165,17 @@ const RoleConfig = ()=>{
fetchData<BasicResponse<{role:PermissionInfo}>>(`${roleType}/role`,{method:'GET',eoParams:{role:roleId}}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
form.setFieldsValue({name:data.role.name,permits:data.role.permit})
return Promise.resolve(true)
setPermissionInfo(data.role)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errInfo)=>Promise.reject(errInfo))
}).catch((errInfo)=>console.error(errInfo))
}
useEffect(()=>{
form.setFieldsValue({name:$t(permissionInfo?.name || ''),permits:permissionInfo?.permit})
},[permissionInfo, state.language])
useEffect(() => {
getPermissionTemplate()
form.setFieldsValue({name:'',permits:[]})
@@ -195,7 +201,7 @@ const RoleConfig = ()=>{
return (<div className="h-full flex flex-col overflow-hidden ">
<div className="text-[18px] leading-[25px] pb-[12px]">
<Button className="flex items-center" type="text" onClick={()=>navigateTo(-1)}><ArrowLeftOutlined className="max-h-[14px]" /><span></span></Button>
<Button className="flex items-center" type="text" onClick={()=>navigateTo(-1)}><ArrowLeftOutlined className="max-h-[14px]" /><span>{$t('返回')}</span></Button>
</div>
<WithPermission access={roleId !== undefined ? `system.organization.role.${roleType}.edit`: `system.organization.role.${roleType}.add`}>
<Form
@@ -150,7 +150,7 @@ const RoleList = ()=>{
id="global_role"
ref={pageListRef}
tableClass="role_table "
columns={[...ROLE_TABLE_COLUMNS as PageProColumns<RoleTableListItem, "text">[], ...operation('team')]}
columns={[...columns as PageProColumns<RoleTableListItem, "text">[], ...operation('team')]}
request={()=>getRoleList('team')}
showPagination={false}
addNewBtnTitle={$t("添加角色")}
@@ -188,7 +188,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_,ref) => {
useEffect(() => {
if(accessInit){
getTeamOptionList
getTeamOptionList()
}else{
getGlobalAccessData()?.then(()=>{
getTeamOptionList()
@@ -42,7 +42,7 @@ const SystemInsidePage:FC = ()=> {
const getApiDefine = ()=>{
setApiPrefix('')
setPrefixForce(false)
fetchData<BasicResponse<{ prefix:string, force:boolean }>>('service/api/define',{method:'GET',eoParams:{service:serviceId,team:teamId}}).then(response=>{
fetchData<BasicResponse<{ prefix:string, force:boolean }>>('service/router/define',{method:'GET',eoParams:{service:serviceId,team:teamId}}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setApiPrefix(data.prefix)
@@ -58,7 +58,8 @@ const SystemInsidePage:FC = ()=> {
const SYSTEM_PAGE_MENU_ITEMS = useMemo(()=>[
getItem($t('服务'), 'assets', null,
[
getItem(<Link to="./api">{$t('API')}</Link>, 'api',undefined,undefined,undefined,'team.service.api.view'),
getItem(<Link to="./api">{$t('API')}</Link>, 'api',undefined,undefined,undefined,'team.service.api_doc.view'),
getItem(<Link to="./route">{$t('路由')}</Link>, 'route',undefined,undefined,undefined,'team.service.router.view'),
getItem(<Link to="./upstream">{$t('上游')}</Link>, 'upstream',undefined,undefined,undefined,'team.service.upstream.view'),
getItem(<Link to="./document">{$t('使用说明')}</Link>, 'document',undefined,undefined,undefined,''),
getItem(<Link to="./publish">{$t('发布')}</Link>, 'publish',undefined,undefined,undefined,'team.service.release.view'),
@@ -113,7 +114,7 @@ const SystemInsidePage:FC = ()=> {
}, [currentUrl]);
useEffect(()=>{
if(accessData && accessData.get('team') && accessData.get('team')?.indexOf('team.service.api.view') !== -1){
if(accessData && accessData.get('team') && accessData.get('team')?.indexOf('team.service.router.view') !== -1){
getApiDefine()
}
},[accessData])
@@ -1,63 +1,146 @@
import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react";
import ApiEdit, {ApiEditApi} from "@common/components/postcat/ApiEdit.tsx";
import { Spin, message} from "antd";
import {forwardRef, useEffect, useState} from "react";
import { Button, Empty, Spin, Upload, message} from "antd";
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { SystemApiDetail, SystemInsideApiDocumentHandle, SystemInsideApiDocumentProps } from "../../../const/system/type.ts";
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 { Codebox } from "@common/components/postcat/api/Codebox/index.tsx";
import { useParams } from "react-router-dom";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
const SystemInsideApiDocument = forwardRef<SystemInsideApiDocumentHandle,SystemInsideApiDocumentProps>((props, ref) => {
const {serviceId, teamId, apiId} = props
const {serviceId, teamId} = useParams<RouterParams>()
const {fetchData} = useFetch()
const [apiDetail, setApiDetail] = useState<SystemApiDetail>()
const apiEditRef = useRef<ApiEditApi>(null)
const [loaded,setLoaded] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(false)
useImperativeHandle(ref, ()=>({
save
})
)
const [showEditor, setShowEditor] = useState<boolean>(false)
useEffect(() => {
getApiDetail()
}, []);
const getApiDetail = ()=>{
setLoading(true)
fetchData<BasicResponse<{api:SystemApiDetail}>>('service/api/detail',{method:'GET',eoParams:{service:serviceId,team:teamId, api:apiId},eoTransformKeys:['create_time','update_time','match_type','upstream_id','opt_type']}).then(response=>{
fetchData<BasicResponse<{doc:SystemApiDetail}>>('service/api_doc',{method:'GET',eoParams:{service:serviceId,team:teamId },eoTransformKeys:['update_time']}).then(response=>{
const {code,data,msg} = response
//console.log(data,code, STATUS_CODE.SUCCESS,code === STATUS_CODE.SUCCESS)
if(code === STATUS_CODE.SUCCESS){
setApiDetail(data.api)
setLoaded(true)
setApiDetail(data.doc?.content)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>{setLoading(false)})
}
const save = ()=>{
return apiEditRef.current?.getData()?.then((res)=>{
return fetchData<BasicResponse<{id:string}>>('service/api',{method:'PUT',eoParams:{service:serviceId,team:teamId,api:apiId},eoBody:(res.apiInfo)}).then(response=>{
const UploadBtn = ({type, updated}:{type:'new'|'edit', updated:()=>void})=>{
const {fetchData} = useFetch()
const uploadFile = (file:File)=>{
const body = new FormData()
body.append('doc',file)
fetchData<BasicResponse<{doc:SystemApiDetail}>>('service/api_doc/upload',{method:'POST',body:body,eoParams:{service:serviceId,team:teamId },eoTransformKeys:['update_time']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
updated?.()
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg|| $t(RESPONSE_TIPS.error))
}
}).catch(errInfo => Promise.reject(errInfo))
})
}).finally(()=>{setLoading(false)})
}
return (
<WithPermission access="team.service.api_doc.edit">
<Upload name='file' accept=".json,.yaml" maxCount={1} showUploadList={false} beforeUpload={(file)=>{
uploadFile(file)
return false;
}}>
<Button type="primary">{$t(type === 'new' ? '上传文件' :'替换文件')}</Button>
</Upload>
</WithPermission>
)
}
const ApiEdit = ({spec,updated}:{spec?:string|object,updated:()=>void})=>{
const [code, setCode] = useState<string>('')
const [saveLoading, setSaveLoading] = useState<boolean>(false)
useEffect(()=>{
try{
setCode(typeof spec === 'string' ? spec:JSON.stringify(spec) )
}catch(e){
console.warn('文档解析失败',e)
}
},[spec])
const saveCode = ()=>{
setSaveLoading(true)
fetchData<BasicResponse<{doc:SystemApiDetail}>>('service/api_doc',{method:'PUT',eoBody:{content:code},eoParams:{service:serviceId,team:teamId },eoTransformKeys:['update_time']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
updated?.()
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>{setSaveLoading(false)})
}
return (
<div className="flex flex-col h-full overflow-hidden">
<div className="flex items-center gap-btnbase justify-end mr-PAGE_INSIDE_X pr-btnbase pb-btnbase">
<UploadBtn type="edit" updated={updated}/>
<WithPermission access="team.service.api_doc.edit">
<Button type="primary" loading={saveLoading} onClick={()=>saveCode()}>{$t('保存')}</Button>
</WithPermission>
</div>
<div className="flex-1 flex items-center pr-PAGE_INSIDE_X overflow-hidden gap-btnbase">
<div className="flex-1 h-full">
<Codebox enableToolbar={false} width="100%" height="100%" editorTheme="vs-dark" language='yaml' value={code} onChange={setCode}/>
</div>
<div className="flex-1 h-full overflow-auto" >{
!code ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> : <ApiDocument spec={code}/>}</div>
</div>
</div>)
}
const ApiPreview = ({setShowEditor,spec,updated}:{setShowEditor:(show:boolean)=>void,spec?:string | object, updated:()=>void})=>{
return (<div className="flex flex-col h-full overflow-hidden">
<div className="flex items-center gap-btnbase justify-end pb-btnbase pr-btnbase mr-PAGE_INSIDE_X">
<UploadBtn type="edit" updated={updated}/>
<WithPermission access="team.service.api_doc.edit">
<Button type="primary" onClick={()=>setShowEditor(true)}>{$t('打开编辑器')}</Button>
</WithPermission>
</div>
<div className="flex-1 overflow-auto pr-PAGE_INSIDE_X">
<ApiDocument spec={spec}/>
</div>
</div>
)
}
const updated = ()=>{
getApiDetail(); setShowEditor(false)
}
return (<>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className=' h-full overflow-auto '>
<div className="pb-[20px]">
<ApiEdit apiInfo={apiDetail} editorRef={apiEditRef} loaded={loaded} serviceId={serviceId} teamId={teamId}/>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} wrapperClassName=' h-full overflow-hidden '>
<div className=" h-full">
{!showEditor && apiDetail && <ApiPreview setShowEditor={setShowEditor} spec={apiDetail} updated={updated}/>}
{showEditor && <ApiEdit updated={updated} spec={apiDetail}/>}
{!showEditor && !apiDetail && <Empty image={EmptySVG} >
<div className="flex items-center gap-btnbase justify-center">
<UploadBtn type="new" updated={updated}/>
<WithPermission access="team.service.api_doc.edit">
<Button type="primary" onClick={()=>setShowEditor(true)}>{$t('打开编辑器')}</Button>
</WithPermission>
</div>
</Empty>}
</div>
</Spin>
</>)
@@ -43,7 +43,7 @@ const SystemInsideApiProxy = forwardRef<SystemInsideApiProxyHandle,SystemInsideA
scrollToFirstError
form={form}
className={`mx-auto flex flex-col overflow-hidden h-full ${className}`}
name="systemInsideApiProxy"
name="SystemInsideApiProxy"
onValuesChange={(_,allValues)=>{onChange?.(allValues)}}
autoComplete="off">
@@ -0,0 +1,209 @@
import {App, Col, Form, Input, Row, Select, Spin, Switch} from "antd";
import {forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react";
import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal.tsx";
import styles from "./SystemInsideApi.module.css"
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { API_PROTOCOL, HTTP_METHOD, MATCH_CONFIG, MatchPositionEnum, MatchTypeEnum } from "../../../const/system/const.tsx";
import { SystemInsideRouterCreateHandle, SystemInsideRouterCreateProps, SystemApiProxyFieldType, SystemInsideApiProxyHandle } from "../../../const/system/type.ts";
import { MatchItem } from "@common/const/type.ts";
import { validateUrlSlash } from "@common/utils/validate.ts";
import { $t } from "@common/locales/index.ts";
import SystemInsideApiProxy from "@core/pages/system/api/SystemInsideApiProxy.tsx";
import { LoadingOutlined } from "@ant-design/icons";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle,SystemInsideRouterCreateProps>((props, ref) => {
const { message } = App.useApp()
const {type, entity, serviceId,teamId, modalApiPrefix:apiPrefix, modalPrefixForce:prefixForce} = props
const [form] = Form.useForm();
const {fetchData} = useFetch()
const [loading, setLoading] = useState<boolean>(false)
const proxyRef = useRef<SystemInsideApiProxyHandle>(null)
const { state } = useGlobalContext()
const onFinish = ()=>{
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([,formValue])=>{
const body = {...formValue,path:formValue.path.trim(),proxy:{...formValue.proxy,path:formValue.proxy.path ? (formValue.proxy.path.startsWith('/')? formValue.proxy.path: '/'+ formValue.proxy.path) : undefined}}
return fetchData<BasicResponse<null>>('service/router',{method: type === 'add' ? 'POST' : 'PUT',eoBody:(body), eoParams: {service:serviceId,team:teamId, ...(type === 'edit' ? {router:entity?.id}: {})},eoTransformKeys:['matchType','isDisable']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch(errInfo=>Promise.reject(errInfo))
})
}
const copy: ()=>Promise<boolean | string> = ()=>{
return new Promise((resolve, reject)=>{
return form.validateFields().then((value)=>{
fetchData<BasicResponse<{api:SystemApiProxyFieldType}>>('service/api/copy',{method:'POST',eoParams:{service:serviceId,team:teamId, api:entity!.id},eoBody:({...value,path:value.path.trim()})}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return resolve(data.api.id)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
copy,
save:onFinish
})
)
const getRouterConfig = ()=>{
setLoading(true)
fetchData<BasicResponse<{router:SystemApiProxyFieldType}>>('service/router/detail',{method:'GET',eoParams:{service:serviceId,team:teamId, router:entity!.id}}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
const {isDisable, protocols, path, method, description, match, proxy} = data.router
form.setFieldsValue({isDisable, protocols, path, method, description, match,proxy
})
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> console.error(errorInfo))
.finally(()=>setLoading(false))
}
useEffect(() => {
switch(type){
case 'edit':
getRouterConfig()
break;
case 'add':
form.setFieldValue('prefix',apiPrefix)
form.setFieldValue(['proxy','timeout'],10000)
form.setFieldValue(['proxy','retry'],0)
form.setFieldValue('protocols',['http','https'])
break;
case 'copy':
// form.setFieldsValue({
// ...entity,
// name:`${$t('副本')}-${entity!.name}`,
// ...(prefixForce?
// {prefix:apiPrefix,path: entity!.path.substring(apiPrefix?.length|| 0)}:
// {}),
// proxy:{timeout:10000, retry:0, ...entity?.proxy}
// });
break;
}
return (form.setFieldsValue({}))
}, []);
const translatedMatchConfig = useMemo(()=>{
return MATCH_CONFIG.map((item)=>{
if(item.key === 'position'){
return ({...item,component:<Select className="w-INPUT_NORMAL" options={Object.entries(MatchPositionEnum)?.map(([key,value])=>{
return { label:$t(value), value:key}
})}/>})
}
if(item.key === 'matchType'){
return ({...item, component: <Select className="w-INPUT_NORMAL" options={Object.entries(MatchTypeEnum)?.map(([key,value])=>{
return { label:$t(value), value:key}
})}/>})
}
return {...item}
})
}, [state.language])
return (<div className="h-full w-full">
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className=''>
<Form
layout='vertical'
labelAlign='left'
scrollToFirstError
form={form}
className="mx-auto flex flex-col h-full"
name="SystemInsideRouterCreate"
onFinish={onFinish}
autoComplete="off"
>
<div className="">
<Row className="mb-btnybase" > <Col ><span className="font-bold mr-[13px]">{$t('API 基础信息')}</span></Col></Row>
<Form.Item<SystemApiProxyFieldType>
label={$t("拦截该接口的请求")}
name="isDisable"
extra={$t('开启拦截后,网关会拦截所有该路径的请求,相当于防火墙禁用了特定路径的访问。')}
>
<Switch />
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("请求协议")}
name="protocols"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} mode="multiple" options={API_PROTOCOL}>
</Select>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("请求路径")}
name="path"
rules={[{ required: true,whitespace:true },
{
validator: validateUrlSlash,
}]}
className={styles['form-input-group']}
>
<Input prefix={(prefixForce ? `${apiPrefix}/` :"/")} className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("请求方式")}
name="method"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} mode="multiple" options={HTTP_METHOD.map((method:string)=>{
return { label:method, value:method}
})}>
</Select>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("描述")}
name="description"
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("高级匹配")}
name="match"
>
<EditableTableWithModal<MatchItem & {_id:string}>
configFields={translatedMatchConfig}
/>
</Form.Item>
{/* } */}
{ type !== 'copy' &&<>
<Row className="mb-btnybase mt-[40px]"><Col ><span className="font-bold mr-[13px]">{$t('转发规则设置')} </span></Col></Row>
<Form.Item<SystemApiProxyFieldType>
className="mb-0 bg-transparent border-none p-0"
name="proxy"
>
<SystemInsideApiProxy serviceId={serviceId!} teamId={teamId!} ref={proxyRef} />
</Form.Item>
</>}
</div>
</Form>
</Spin>
</div>
)
})
export default SystemInsideRouterCreate
@@ -0,0 +1,229 @@
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx"
import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {Link, useParams} from "react-router-dom";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {App, Divider} from "antd";
import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import { SimpleMemberItem} from '@common/const/type.ts'
import {useFetch} from "@common/hooks/http.ts";
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
import SystemInsideRouterCreate from "./SystemInsideRouterCreate.tsx";
import {useSystemContext} from "../../../contexts/SystemContext.tsx";
import { SYSTEM_API_TABLE_COLUMNS } from "../../../const/system/const.tsx";
import {SystemApiTableListItem, SystemInsideRouterCreateHandle, SystemInsideApiDocumentHandle } from "../../../const/system/type.ts";
import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { checkAccess } from "@common/utils/permission.ts";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx";
import { $t } from "@common/locales/index.ts";
const SystemInsideRouterList:FC = ()=>{
const [searchWord, setSearchWord] = useState<string>('')
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
const [tableListDataSource, setTableListDataSource] = useState<SystemApiTableListItem[]>([]);
const [tableHttpReload, setTableHttpReload] = useState(true);
const {fetchData} = useFetch()
const pageListRef = useRef<ActionType>(null);
const {apiPrefix, prefixForce} = useSystemContext()
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
const {accessData,state} = useGlobalContext()
const [drawerType,setDrawerType]= useState<'add'|'edit'|'view'|'upstream'|undefined>()
const [open, setOpen] = useState(false);
const drawerAddFormRef = useRef<SystemInsideRouterCreateHandle>(null)
const {serviceId, teamId} = useParams<RouterParams>()
const [curApi, setCurApi] = useState<SystemApiTableListItem>()
const getRoutesList = (): Promise<{ data: SystemApiTableListItem[], success: boolean }>=> {
//console.log(sorter, filter)
if(!tableHttpReload){
setTableHttpReload(true)
return Promise.resolve({
data: tableListDataSource,
success: true,
});
}
return fetchData<BasicResponse<{routers:SystemApiTableListItem}>>('service/routers',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','is_disable']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTableListDataSource(data.routers)
setTableHttpReload(false)
return {data:data.routers, success: true}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
}).catch(() => {
return {data:[], success:false}
})
}
const deleteRoute = (entity:SystemApiTableListItem)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>('service/router',{method:'DELETE',eoParams:{service:serviceId,team:teamId, router:entity!.id}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
})
}
const openModal = async (type: 'delete',entity:SystemApiTableListItem) =>{
let title:string = ''
let content:string|React.ReactNode = ''
switch (type){
case 'delete':
title=$t('删除')
content=$t(DELETE_TIPS.default)
break;
}
modal.confirm({
title,
content,
onOk:()=> {
switch (type){
case 'delete':
return deleteRoute(entity).then((res)=>{if(res === true) manualReloadTable()})
}
},
width:600,
okText:$t('确认'),
okButtonProps:{
disabled : !checkAccess( `team.service.router.${type}`, accessData )
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
})
}
const operation:PageProColumns<SystemApiTableListItem>[] =[
{
title: COLUMNS_TITLE.operate,
key: 'option',
btnNums:2,
fixed:'right',
valueType: 'option',
render: (_: React.ReactNode, entity: SystemApiTableListItem) => [
<TableBtnWithPermission access="team.service.router.edit" key="edit" btnType="edit" onClick={()=>{openDrawer('edit',entity)}} btnTitle="编辑"/>,
<Divider type="vertical" className="mx-0" key="div3"/>,
<TableBtnWithPermission access="team.service.router.delete" key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle="删除"/>,
],
}
]
const manualReloadTable = () => {
setTableHttpReload(true); // 表格数据需要从后端接口获取
pageListRef.current?.reload()
};
const getMemberList = async ()=>{
setMemberValueEnum([])
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
if(code === STATUS_CODE.SUCCESS){
setMemberValueEnum(data.members)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
const openDrawer = (type:'add'|'edit'|'view',entity?:SystemApiTableListItem)=>{
setCurApi(entity)
setDrawerType(type)
}
useEffect(()=>{drawerType !== undefined ? setOpen(true):setOpen(false)},[drawerType])
useEffect(() => {
setBreadcrumb([
{
title:<Link to={`/service/list`}>{$t('服务')}</Link>
},
{
title:$t('路由')
}
])
getMemberList()
manualReloadTable()
}, [serviceId]);
const onClose = () => {
setDrawerType(undefined);
setCurApi(undefined)
};
const columns = useMemo(()=>{
return [...SYSTEM_API_TABLE_COLUMNS].map(x=>{
if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){
const tmpValueEnum:{[k:string]:{text:string}} = {}
memberValueEnum?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
x.valueEnum = tmpValueEnum
}
if(x.filters &&((x.dataIndex as string[])?.indexOf('isDisabled') !== -1) ){
x.valueEnum = {
true:{text:<span className="text-status_fail">{$t('拦截')}</span>},
false:{text:<span className="text-status_success">{$t('放行')}</span>}
}
}
return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}})
},[memberValueEnum,state.language])
const handlerSubmit:() => Promise<string | boolean>|undefined= ()=>{
switch(drawerType){
case 'add':{
return drawerAddFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})
}
case 'edit':{
return drawerAddFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})
}
default:return undefined
}
}
return (
<>
<PageList
id="global_system_api"
ref={pageListRef}
columns = {[...columns,...operation]}
request={()=>getRoutesList()}
dataSource={tableListDataSource}
addNewBtnTitle={$t('添加路由')}
searchPlaceholder={$t('输入名称、URL 查找路由')}
onAddNewBtnClick={()=>{openDrawer('add')}}
addNewBtnAccess="team.service.router.add"
tableClickAccess="team.service.router.view"
manualReloadTable={manualReloadTable}
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
onChange={() => {
setTableHttpReload(false)
}}
onRowClick={(row:SystemApiTableListItem)=>openDrawer('view',row)}
tableClass="mr-PAGE_INSIDE_X "
/>
<DrawerWithFooter
title={drawerType === 'add' ? $t("添加路由"):$t("路由详情")}
open={open}
onClose={onClose}
onSubmit={()=>handlerSubmit()}
showOkBtn={drawerType !== 'view'}
>
<SystemInsideRouterCreate ref={drawerAddFormRef} type={drawerType as 'add'|'edit'|'copy'} entity={drawerType === 'edit' ? curApi : undefined} modalApiPrefix={apiPrefix} serviceId={serviceId!} teamId={teamId!} modalPrefixForce={prefixForce}/>
</DrawerWithFooter>
</>
)
}
export default SystemInsideRouterList
@@ -1,26 +1,27 @@
import { $t } from "@common/locales";
import { Link } from "react-router-dom";
export default function DashboardInstruction({showClusterIns, showMonitorIns}:{showClusterIns:boolean, showMonitorIns:boolean}) {
return (
<div className="h-full w-full overflow-auto">
<div className=" m-auto mt-[10%] flex flex-col items-center p-[20px]">
<p className="text-[20px] font-medium leading-[32px] text-MAIN_TEXT"></p>
<p className="text-[12px] font-normal leading-[20px] text-DESC_TEXT mt-[12px]" ></p>
<p className="text-[20px] font-medium leading-[32px] text-MAIN_TEXT">{$t('集群配置并开启监控')}</p>
<p className="text-[12px] font-normal leading-[20px] text-DESC_TEXT mt-[12px]" >{$t('监控功能用于辅助管理集群内信息,请配置集群、设置监控信息后查看当前集群监控情况;')}</p>
{/* <p className="text-[12px] font-normal leading-[20px] text-DESC_TEXT mt-[8px]">
{/* <a></a> *
</p> */}
<div className="flex mt-[28px] gap-[20px] w-full justify-center items-center">
{showClusterIns && <div className="h-[208px] w-[50%] max-w-[384px] flex flex-col items-center py-[32px] px-[24px] gap-[16px] rounded-DEFAULT bg-MENU_BG mr-[24px]">
<p className="text-[20px] font-medium leading-[32px] text-MAIN_TEXT"></p>
<p className="text-[12px] font-normal leading-[20px] text-DESC_TEXT"></p>
<p><a href="/cluster" target="_blank"></a></p>
{showClusterIns && <div className="h-[208px] w-[50%] max-w-[384px] flex flex-col items-center py-[32px] px-[24px] gap-[16px] rounded-DEFAULT bg-MENU_BG mr-[24px] justify-around">
<p className="text-[20px] font-medium leading-[32px] text-MAIN_TEXT">{$t('集群配置')}</p>
<p className="text-[12px] font-normal leading-[20px] text-DESC_TEXT">{$t('配置集群地址,以确保监控系统能够正确识别和连接到集群')}</p>
<p><a href="/cluster" target="_blank">{$t('配置集群信息')}</a></p>
</div>}
{showMonitorIns &&
<div className="h-[208px] w-[50%] max-w-[384px] flex flex-col items-center py-[32px] px-[24px] gap-[16px] rounded-DEFAULT bg-MENU_BG ">
<p className="text-[20px] font-medium leading-[32px] text-MAIN_TEXT"></p>
<p className="text-[12px] font-normal leading-[20px] text-DESC_TEXT">API调用统计图表</p>
<p><a href="/dashboardsetting" target="_blank"></a></p>
<div className="h-[208px] w-[50%] max-w-[384px] flex flex-col items-center py-[32px] px-[24px] gap-[16px] rounded-DEFAULT bg-MENU_BG justify-around">
<p className="text-[20px] font-medium leading-[32px] text-MAIN_TEXT">{$t('监控设置')}</p>
<p className="text-[12px] font-normal leading-[20px] text-DESC_TEXT">{$t('设置监控报表的数据来源,设置完成之后即可获得详细的API调用统计图表。')}</p>
<p><a href="/datasourcing" target="_blank">{$t('配置监控信息')}</a></p>
</div>
}
@@ -1,6 +1,5 @@
import { DefaultOptionType } from "antd/es/select"
import { ApiDetail } from "@common/const/api-detail"
import { EntityItem } from "@common/const/type"
import { SubscribeEnum, SubscribeFromEnum } from "@core/const/system/const"
import WithPermission from "@common/components/aoplatform/WithPermission"
@@ -22,7 +21,7 @@ export type ServiceDetailType = {
name:string
description:string
basic:ServiceBasicInfoType
apis:ApiDetail[]
apiDoc:string
applied:boolean
}
@@ -1,50 +1,25 @@
import { useParams} from "react-router-dom";
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
import {Anchor, Button, Collapse, Drawer, FloatButton, Input, Space} from "antd";
import { useEffect, useMemo, useState} from "react";
import ApiPreview from "@common/components/postcat/ApiPreview.tsx";
import { Button, Drawer, Empty} from "antd";
import { useEffect, useState} from "react";
import ApiTestGroup from "./ApiTestGroup.tsx";
import {ApiDetail} from "@common/const/api-detail";
import {ServiceDetailType } from "../../const/serviceHub/type.ts";
import ApiMatch from "@common/components/postcat/api/ApiPreview/components/ApiMatch/index.tsx";
import ApiProxy from "@common/components/postcat/api/ApiPreview/components/ApiProxy/index.tsx";
import { $t } from "@common/locales/index.ts";
import ApiDocument from "@common/components/aoplatform/ApiDocument.tsx";
const ServiceHubApiDocument = ({service}:{service:ServiceDetailType})=>{
const {serviceId} = useParams<RouterParams>();
const [apiTestDrawOpen, setApiTestDrawOpen] = useState(false);
const [serviceName, setServiceName] = useState<string>()
const [apiDocs,setApiDocs ] = useState<ApiDetail[]>()
const [selectedTestApi,setSelectedTestApi] = useState<string>()
const [activeKey, setActiveKey] = useState<string[]>([])
const [apiDocument, setApiDocument] = useState<string>()
useEffect(()=>{
if(!service) return
setServiceName(service?.name)
setApiDocs(service?.apis)
setActiveKey(service?.apis.map((x)=>x.id))
setApiDocument(service.apiDoc)
},[service])
const category = useMemo(() => [
{
key: 'apiDocument-list',
href: '#apiDocument-list',
title:$t('API 列表'),
children:apiDocs?.map((x)=>({
key:x.id,
href:`#apiDocument-${x.id}`,
title:x.name
})) || []
},
// {
// key: 'apiDocument-statusCode',
// href: '#apiDocument-statusCode',
// title:$t('状态码',
// },
], [apiDocs]);
const floatButtonStyle = { top:'10px',position:'sticky', width:'180px',height:'200px'}
useEffect(() => {
if(!serviceId){
console.warn('缺少serviceId')
@@ -52,10 +27,6 @@ const ServiceHubApiDocument = ({service}:{service:ServiceDetailType})=>{
}
}, [serviceId]);
const testClick = (id:string)=>{//console.log('test');
setApiTestDrawOpen(true)
setSelectedTestApi(id)}
const onClose = () => {
setApiTestDrawOpen(false);
};
@@ -64,54 +35,9 @@ const ServiceHubApiDocument = ({service}:{service:ServiceDetailType})=>{
return (
<>
<div className="flex flex-col p-btnbase pt-[4px] h-full flex-1 overflow-auto" id='layout-ref'>
<div className='bg-[#fff] rounded p-btnbase pl-0 flex justify-between'>
<div className="w-[calc(100%-220px)]" >
<p className="font-bold text-[20px] leading-[32px] mb-[12px] h-[32px]" id="apiDocument-list">{$t('API 列表')}</p>
<div className="">
{apiDocs?.map((apiDetail)=>(
<div className="mb-btnbase " key={apiDetail.id} id={`apiDocument-${apiDetail.id}`}>
<Collapse key={`apiDocument-${apiDetail.id}`}
expandIcon={({isActive})=>(isActive? <iconpark-icon name="shouqi-2"></iconpark-icon>:<iconpark-icon name="zhankai"></iconpark-icon> )}
items={[{
key: apiDetail.id,
label: <span><span className="text-status_update font-bold mr-[8px]">{apiDetail.method}</span><span>{apiDetail.name}</span></span>,
children:<div className="scroll-area h-[calc(100%-84px)] overflow-auto">
<Space direction="vertical" className="mb-btnybase w-full mt-btnybase">
<Input
readOnly
addonBefore={apiDetail?.method}
value={apiDetail?.path}
/>
</Space>
{
apiDetail?.match && apiDetail.match?.length > 0 &&
<ApiMatch title={$t('高级匹配')} rows={apiDetail?.match} />
}
{
apiDetail?.proxy && Object.keys(apiDetail?.proxy).length > 0 &&
<ApiProxy title={$t('转发规则')} proxyInfo={apiDetail?.proxy} />
}
{apiDetail && <ApiPreview entity={{...apiDetail.doc,name:apiDetail.name, method:apiDetail.method,uri:apiDetail.path, protocol:apiDetail.protocol||'HTTP'}} />}
</div>
}]}
activeKey={activeKey}
onChange={(val)=>{setActiveKey(val as string[])}}
/>
</div>
))}
</div>
</div>
<FloatButton.Group shape="circle" style={floatButtonStyle}>
<Anchor
targetOffset={60}
getContainer = {()=> document.getElementById('layout-ref')!}
items={category}
/>
</FloatButton.Group>
<div className='bg-[#fff] rounded p-btnbase pt-0 pl-0 flex justify-between '>
{apiDocument ? <ApiDocument spec={apiDocument } /> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
}
</div>
</div>
@@ -28,22 +28,22 @@ const ServiceHubDetail = ()=>{
const applyRef = useRef<ApplyServiceHandle>(null)
const { modal,message } = App.useApp()
const [mySystemOptionList, setMySystemOptionList] = useState<DefaultOptionType[]>()
const [applied,setApplied] = useState<boolean>(false)
const [activeKey, setActiveKey] = useState<string[]>([])
// const [applied,setApplied] = useState<boolean>(false)
// const [activeKey, setActiveKey] = useState<string[]>([])
const [service, setService] = useState<ServiceDetailType>()
const navigate = useNavigate();
const getServiceBasicInfo = ()=>{
fetchData<BasicResponse<{service:ServiceDetailType}>>('catalogue/service',{method:'GET',eoParams:{service:serviceId}, eoTransformKeys:['app_num','api_num','update_time']}).then(response=>{
fetchData<BasicResponse<{service:ServiceDetailType}>>('catalogue/service',{method:'GET',eoParams:{service:serviceId}, eoTransformKeys:['app_num','api_num','update_time','api_doc']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setService(data.service)
setServiceBasicInfo(data.service.basic)
setServiceName(data.service.name)
setServiceDesc(data.service.description)
setApplied(data.service.applied)
// setApplied(data.service.applied)
setServiceDoc(DOMPurify.sanitize(data.service.document))
setActiveKey(data.service.apis.map((x)=>x.id))
// setActiveKey(data.service.apis.map((x)=>x.id))
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
@@ -91,7 +91,7 @@ const ServiceHubDetail = ()=>{
content:<ApplyServiceModal ref={applyRef} entity={{...serviceBasicInfo!, name:serviceName!, id:serviceId!}} mySystemOptionList={mySystemOptionList!}/>,
onOk:()=>{
return applyRef.current?.apply().then((res)=>{
if(res === true) setApplied(true)
// if(res === true) setApplied(true)
})
},
okText:$t('确认'),