Merge branch 'feature/v1.3/mj' into 'main'

Feature/v1.3/mj

See merge request apipark/APIPark!82
This commit is contained in:
杨梦洁
2024-11-21 16:11:42 +08:00
102 changed files with 5284 additions and 2927 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"cSpell.words": [
"Antd",
"apinto",
"Apipark",
"logsettings",
"resourcesettings"
]
}
-17
View File
@@ -1,17 +0,0 @@
# 部署
## 代码同步
packages目录下,部分子项目为企业版独有,不要同步到开源版:
packages/businessEntry, packages/openApi, packages/systemRunning, README.pro.md
## 安装依赖
建议使用pnpm
`npm install -g pnpm`
使用pnpm安装依赖
`pnpm install`
## 编译
### 开源版本
`pnpm run build`
### 企业版本
`pnpm run build:pro`
-2
View File
@@ -9,11 +9,9 @@
"scripts": {
"test": "jest",
"build": "set NODE_OPTIONS=--max-old-space-size=4096 && lerna run build --scope=core --stream --verbose ",
"build:pro": "set NODE_OPTIONS=--max-old-space-size=4096 && lerna run build --scope=business-entry --stream --verbose ",
"serve": "lerna run preview --parallel",
"serve:remotes": "lerna run serve --scope=remote --parallel",
"dev": "lerna run dev --scope=core --stream",
"dev:pro": "lerna run dev --scope=business-entry --stream",
"stop": "kill-port --port 5000",
"scan": "i18next-scanner --config i18next-scanner.config.js"
},
-5
View File
@@ -1,5 +0,0 @@
// .env.pro
VITE_APP_MODE=pro
VITE_APP_TITLE=My Production App
VITE_API_BASE_URL=https://api.production.example.com
@@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs','public','code-snippet','ace-editor'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
@@ -1,25 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
public/tinymce
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-11
View File
@@ -1,11 +0,0 @@
# `businessEntry`
> TODO: description
## Usage
```
const businessEntry = require('businessEntry');
// TODO: DEMONSTRATE API
```
@@ -1,7 +0,0 @@
'use strict';
const businessEntry = require('..');
const assert = require('assert').strict;
assert.strictEqual(businessEntry(), 'Hello from businessEntry');
console.info('businessEntry tests passed');
@@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/frontend/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>2APIPark</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="/frontend/iconpark_eolink.js"></script>
<script src="/frontend/iconpark_apinto.js"></script>
</body>
</html>
@@ -1,17 +0,0 @@
{
"name": "business-entry",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": " vite --port 5000 --strictPort",
"build": "vite build ",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview --port 5000 --strictPort",
"serve": "vite preview --port 5000 --strictPort"
},
"dependencies": {
},
"devDependencies": {
}
}
@@ -1,9 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-158
View File
@@ -1,158 +0,0 @@
import '@core/App.css'
import { ConfigProvider } from 'antd';
import RenderRoutes from '@businessEntry/components/aoplatform/RenderRoutes';
import {BreadcrumbProvider} from "@common/contexts/BreadcrumbContext.tsx";
import { StyleProvider } from '@ant-design/cssinjs';
import zhCN from 'antd/locale/zh_CN';
import useInitializeMonaco from "@common/hooks/useInitializeMonaco";
import ThemeSwitcher from '@common/components/aoplatform/ThemeSwitcher'
const antdComponentThemeToken = {
token: {
// Seed Token,影响范围大
colorPrimary: '#3D46F2',
colorLink:'#3D46F2',
colorBorder:'#ededed',
colorText:'#333',
borderRadius: 4,
// 派生变量,影响范围小
colorBgContainer: '#fff',
colorPrimaryBg:'#EBEEF2',
colorTextQuaternary:'#BBB',
colorTextTertiary:'#999'
},
components:{
// 派生变量,影响范围小
Input:{
activeShadow:'none'
},
Select:{
activeShadow:'none'
},
Checkbox:{
activeShadow:'none'
},
Cascader:{
activeShadow:'none',
optionSelectedBg:'#EBEEF2',
optionHoverBg:'#EBEEF2'
},
Layout: {
bodyBg: '#17163E',
headerBg: 'transparent',
headerColor: '#333',
headerPadding: '10 20px',
lightSiderBg: 'transparent',
siderBg: 'transparent',
},
Breadcrumb:{
itemColor:'#666',
linkColor:'#666',
lastItemColor:'#333',
},
Table:{
headerBorderRadius:0,
headerSplitColor:'#ededed',
borderColor:'#ededed',
cellPaddingBlockMD:'10px',
cellPaddingInlineMD:'12px',
cellPaddingBlockSM:'8px',
cellPaddingInlineSM:'12px',
headerFilterHoverBg:'#EBEEF2',
headerSortActiveBg:'#F7F8FA',
headerSortHoverBg:'#F7F8FA',
fixedHeaderSortActiveBg:'#F7F8FA',
headerBg:'#F7F8FA',
rowHoverBg:'#EBEEF2'
},
Segmented:{
itemColor:'#333',
itemSelectedColor:'#333',
trackBg:'#f7f8fa',
trackPadding:0,
// itemHoverColor:'#EBEEF2',
itemActiveBg:'#EBEEF2',
itemHoverBg:'#EBEEF2',
itemSelectedBg:'#EBEEF2',
},
Tree:{
// titleHeight:30,
// fontSize:12,
directoryNodeSelectedBg:'#EBEEF2',
directoryNodeSelectedColor:'#333',
nodeSelectedBg:'#EBEEF2',
nodeHoverBg:'#EBEEF2'
},
Collapse:{
headerBg:'#f7f8fa',
headerPadding:"12px",
contentPadding:"0 10px 12px 10px"
},
Button:{
// paddingInline:8,
dangerShadow:'none',
defaultShadow:'none',
primaryShadow:'none'
},
Tabs:{
cardBg:'#EBEEF2',
cardHeight:42,
horizontalItemGutter:8,
horizontalItemPaddingSM:'12px 8px 8px 8px',
horizontalItemPadding:'12px 8px 8px 8px',
},
Menu:{
// itemBg:'#F7F8FA',
// subMenuItemBg:'#F7F8FA',
// itemMarginBlock:0,
// activeBarBorderWidth:0,
// itemSelectedColor:'#333',
// itemSelectedBg:'#EBEEF2',
// itemHoverBg:'#EBEEF2'
// itemHeight:'72px',
// darkItemBg:'transparent',
// itemBg:'transparent',
// itemSelectedBg:'transparent',
// darkItemSelectedBg:'transparent',
// subMenuItemBg:'transparent',
// itemActiveBg:'transparent',
// darkSubMenuItemBg:'transparent',
// activeBarHeight:'2px',
// activeBarBorderWidth:2
},
List:{
itemPadding:'8px 0'
},
Form:{
itemMarginBottom:10,
},
Alert:{
defaultPadding:'12px 16px'
},
Tag:{
defaultBg:"#f7f8fa"
},
}
}
function App() {
useInitializeMonaco()
return (
<StyleProvider hashPriority={"high"}>
<ConfigProvider
locale={zhCN}
wave={{disabled:true}}
theme={antdComponentThemeToken}>
<ThemeSwitcher />
<BreadcrumbProvider>
<RenderRoutes />
</BreadcrumbProvider>
</ConfigProvider>
</StyleProvider>
);
}
export default App
@@ -1,484 +0,0 @@
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import Login from "@core/pages/Login.tsx"
import BasicLayout from '@common/components/aoplatform/BasicLayout';
import {createElement, ReactElement,ReactNode,Suspense} from 'react';
import { v4 as uuidv4 } from 'uuid'
import {App, Skeleton} from "antd";
import ApprovalPage from "@core/pages/approval/ApprovalPage.tsx";
import {SystemProvider} from "@core/contexts/SystemContext.tsx";
import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx";
import {FC,lazy} from 'react';
import { TeamProvider } from '@core/contexts/TeamContext.tsx';
import SystemOutlet from '@core/pages/system/SystemOutlet.tsx';
import { DashboardProvider } from '@core/contexts/DashboardContext.tsx';
import { TenantManagementProvider } from '@market/contexts/TenantManagementContext.tsx';
type RouteConfig = {
path:string
component?:ReactElement
children?:(RouteConfig|false)[]
key:string
provider?:FC<{ children: ReactNode; }>
lazy?:unknown
}
const APP_MODE = import.meta.env.VITE_APP_MODE;
export type RouterParams = {
teamId:string
apiId:string
serviceId:string
clusterId:string;
memberGroupId:string
userGroupId:string
pluginName:string
moduleId:string
accessType:'project'|'team'|'service'
categoryId:string
tagId:string
dashboardType:string
dashboardDetailId:string
topologyId:string
appId:string
roleType:string
roleId:string
}
const PUBLIC_ROUTES:RouteConfig[] = [
{
path:'/',
component:<Login/>,
key: uuidv4(),
},
{
path:'/login',
component:<Login/>,
key: uuidv4()
},
{
path:'/',
component:<ProtectedRoute/>,
key: uuidv4(),
children:[
{
path:'approval/*',
component:<ApprovalPage />,
key:uuidv4()
},
{
path:'team',
component:<Outlet/>,
key: uuidv4(),
provider: TeamProvider,
children:[
{
path:'',
key: uuidv4(),
component: <Navigate to="list" />
},
{
path:'list',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamList.tsx'))
},
{
path:'inside/:teamId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsidePage.tsx')),
key: uuidv4(),
children:[
{
path:'member',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsideMember.tsx')),
},
{
path:'setting',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamConfig.tsx')),
},
]
}
]
},
{
path:'service',
component:<SystemOutlet />,
key: uuidv4(),
provider: SystemProvider,
children:[
{
path:'',
key:uuidv4(),
component:<Navigate to="list" />
},
{
path:'list',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')),
},
{
path:'list/:teamId',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')),
},
{
path:':teamId',
component:<Outlet/>,
key: uuidv4(),
children:[
{
path:'inside/:serviceId',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsidePage.tsx')),
children:[
{
path:'api',
key: uuidv4(),
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',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/upstream/SystemInsideUpstreamContent.tsx')),
},
{
path:'document',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideDocument.tsx')),
},
{
path:'subscriber',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideSubscriber.tsx')),
children:[
]
},
{
path:'approval',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApproval.tsx')),
children:[
{
path:'',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')),
},
{
path:'*',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')),
}
]
},
{
path:'topology',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemTopology.tsx')),
key: uuidv4(),
children:[
]
},
{
path:'publish',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublish.tsx')),
children:[
{
path:'',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')),
},
{
path:'*',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')),
}
]
},
{
path:'setting',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemConfig.tsx')),
children:[
]
},
]
}
]
}
]
},{
path:'datasourcing',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideDashboardSetting.tsx')),
},
{
path:'cluster',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCluster.tsx')),
},
{
path:'cert',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCert.tsx')),
},
{
path:'serviceHub',
component:<Outlet />,
key:uuidv4(),
children:[
{
path:'',
key: uuidv4(),
component: <Navigate to="list" />
},
{
path:'list',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubList.tsx')),
},
{
path:'detail/:serviceId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubDetail.tsx')),
}]
},
{
path:'commonsetting',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/common/CommonPage.tsx')),
key:uuidv4(),
},
{
path:'consumer',
component:<Outlet />,
provider:TenantManagementProvider,
key:uuidv4(),
children:[
{
path:'',
key:uuidv4(),
component:<Navigate to="list" />
},
{
path:':teamId/inside/:appId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsidePage.tsx')),
children:[
{
path:'service',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideService.tsx')),
},
{
path:'authorization',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideAuth.tsx')),
},
{
path:'setting',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementAppSetting.tsx')),
},
]
},
{
path:'list',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')),
},
{
path:'list/:teamId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')),
},
]
},
{
path:'member',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberPage.tsx')),
children:[
{
path:'',
key:uuidv4(),
component:<Navigate to="list" />
},
{
path:'list',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')),
},
{
path:'list/:memberGroupId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')),
}
]
},
{
path:'role',
key:uuidv4(),
component:<Outlet></Outlet>,
children:[
{
path: '',
key: uuidv4(),
component: <Navigate to="list" />
},
{
path:'list',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleList.tsx')),
},
{
path:':roleType/config',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')),
},
{
path:':roleType/config/:roleId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')),
}
]
},
APP_MODE === 'pro' &&{
path:'openapi',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@openApi/pages/OpenApiList.tsx')),
key:uuidv4(),
},
{
path:'assets',
component:<p></p>,
key:uuidv4()
},
APP_MODE === 'pro' &&{
path:'analytics',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/Dashboard.tsx')),
key:uuidv4(),
children:[
{
path:':dashboardType',
component:<Outlet/>,
key:uuidv4(),
provider:DashboardProvider,
children:[
{
path:'list',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardList.tsx')),
key:uuidv4()
},
{
path:'detail/:dashboardDetailId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardDetail.tsx')),
key:uuidv4()
},
]
},
]
},
{
path:'systemrunning',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@systemRunning/pages/SystemRunning.tsx')),
key:uuidv4()
},
{
path:'template/:moduleId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '../../../../common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
key:uuidv4()
},
{
path:'logsettings/*',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/logsettings/LogSettings.tsx')),
key: uuidv4(),
children:[{
path:'template/:moduleId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
key:uuidv4()
}]
},
APP_MODE ==='pro' && {
path:'resourcesettings/*',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/resourcesettings/ResourceSettings.tsx')),
key: uuidv4(),
children:[{
path:'template/:moduleId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
key:uuidv4()
}]
},
{
path:'userProfile/*',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/userProfile/UserProfile.tsx')),
key:uuidv4(),
children:[{
path:'changepsw',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/userProfile/ChangePsw.tsx')),
key:uuidv4()
}]
}
]
},
]
const RenderRoutes = ()=> {
return (
<App className="h-full" message={{ maxCount: 1 }}>
<Router>
<Routes>
{generateRoutes(PUBLIC_ROUTES)}
</Routes>
</Router>
</App>
)
}
const generateRoutes = (routerConfig: RouteConfig[]) => {
return routerConfig?.map((route: RouteConfig) => {
let routeElement;
if (route.lazy) {
const LazyComponent = route.lazy as React.ExoticComponent<unknown>;
routeElement = (
<Suspense fallback={ <div className=''><Skeleton className='m-btnbase w-calc-100vw-minus-padding-r' active /></div>}>
{route.provider ? (
createElement(route.provider, {}, <LazyComponent />)
) : (
<LazyComponent />
)}
</Suspense>
);
} else {
routeElement = route.provider ? (
createElement(route.provider, {}, route.component)
) : (
route.component
);
}
return (
<Route
key={route.key}
path={route.path}
element={routeElement}
>
{route.children && generateRoutes(route.children as RouteConfig[])}
</Route>
);
}
)
}
// 保护的路由组件
function ProtectedRoute() {
const {state} = useGlobalContext()
return state.isAuthenticated? <BasicLayout project="core" /> : <Navigate to="/login" />;
}
export default RenderRoutes
@@ -1,27 +0,0 @@
import {StrictMode} from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import '@core/index.css'
import {GlobalProvider} from "@common/contexts/GlobalStateContext.tsx";
async function initializeApp() {
try {
// 初始化行为
// await fetchInitialConfig(); // 示例:获取初始配置
// 异步操作完成后,渲染React应用
ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<GlobalProvider>
<App />
</GlobalProvider>
</StrictMode>,
);
} catch (error) {
console.error('Initialization failed:', error);
// 处理初始化失败的情况,比如渲染一个错误界面
}
}
// 执行初始化
initializeApp();
-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
@@ -1,17 +0,0 @@
// start-vite.js// start-vite.js
import { exec } from 'child_process';
const viteProcess = exec('pnpm run build');
viteProcess.stdout.on('data', (data) => {
console.log(data.toString());
});
viteProcess.stderr.on('data', (data) => {
console.error(data.toString());
});
viteProcess.on('close', (code) => {
console.log(`Vite process exited with code ${code}`);
});
@@ -1,33 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@core/*": ["../core/src/*"],
"@common/*": ["../common/src/*"],
"@market/*": ["../market/src/*"],
"@dashboard/*": ["../dashboard/src/*"],
"@openApi/*": ["../openApi/src/*"],
"@systemRunning/*": ["../systemRunning/src/*"],
"@businessEntry/*": ["./src/*"],
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/Navigation.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../common/src/components/aoplatform/ScrollableSection.tsx", "../common/src/utils/postcat.tsx", "../common/src/utils/curl.ts", "../common/src/components/aoplatform/ResetPsw.tsx", "../common/src/components/aoplatform/SubscribeApprovalModalContent.tsx", "src/components/aoplatform/RenderRoutes.tsx", "../common/src/components/aoplatform/PublishApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePage.tsx", "../common/src/const/type.ts", "../common/src/components/aoplatform/intelligent-plugin", "../common/src/const/domain"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
@@ -1,80 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
export default defineConfig({
cacheDir: './node_modules/.vite',
build:{
outDir:'../../dist',
sourcemap: false,
chunkSizeWarningLimit: 50000,
cacheDir: './node_modules/.vite',
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
// 针对 pnpm 和 Monorepo 特殊处理
if (id.includes('.pnpm')) {
const segments = id.split(path.sep);
const packageName = segments[segments.indexOf('.pnpm') + 1].split('@')[0];
return packageName;
}
}
},
},
css: {
postcss: {
plugins: [
tailwindcss(path.resolve(__dirname, '../common/tailwind.config.js')),
autoprefixer
],
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
},
},
modules:{
localsConvention:"camelCase",
generateScopedName:"[local]_[hash:base64:2]"
}
},
plugins: [react(),
dynamicImportVars({
include:["src"],
exclude:[],
warnOnError:false
}),
],
resolve: {
alias: [
{ find: /^~/, replacement: '' },
{ find: '@common', replacement: path.resolve(__dirname, '../common/src') },
{ find: '@market', replacement: path.resolve(__dirname, '../market/src') },
{ find: '@core', replacement: path.resolve(__dirname, '../core/src') },
{ find: '@dashboard', replacement: path.resolve(__dirname, '../dashboard/src') },
{ find: '@openApi', replacement: path.resolve(__dirname, '../openApi/src') },
{ find: '@systemRunning', replacement: path.resolve(__dirname, '../systemRunning/src') },
{ find: '@businessEntry', replacement: path.resolve(__dirname, './src') },
]
},
server: {
proxy: {
'/api/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true,
},
'/api2/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true,
}
}
},
logLevel:'info'
})
File diff suppressed because one or more lines are too long
@@ -1,180 +1,162 @@
import {
MenuProps,
App,
Button,
ConfigProvider,
Dropdown} from 'antd';
import { Outlet, useLocation, useNavigate} from "react-router-dom";
import {
MenuProps,
App,
Button,
ConfigProvider,
Dropdown
} from 'antd';
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import Logo from '@common/assets/layout-logo.png';
import AvatarPic from '@common/assets/default-avatar.png'
import { useEffect, useMemo, useState} from "react";
import { useCallback, useEffect, useMemo, useState} from "react";
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx';
import { PERMISSION_DEFINITION } from '@common/const/permissions.ts';
import { BasicResponse, RESPONSE_TIPS, routerKeyMap, STATUS_CODE } from '@common/const/const.tsx';
import { UserInfoType } from '@common/const/type.ts';
import { useFetch } from '@common/hooks/http.ts';
import { ProjectFilled } from '@ant-design/icons';
import { getNavItem } from '@common/utils/navigation';
import { getNavItem, transformMenuData } from '@common/utils/navigation';
import { Icon } from '@iconify/react';
import { $t } from '@common/locales';
import { ProConfigProvider, ProLayout } from '@ant-design/pro-components';
import LanguageSetting from './LanguageSetting';
import { usePluginSlotHub } from '@common/contexts/PluginSlotHubContext';
const APP_MODE = import.meta.env.VITE_APP_MODE;
export type MenuItem = Required<MenuProps>['items'][number];
const themeToken = {
bgLayout:'#17163E;',
header: {
heightLayoutHeader:72
},
pageContainer:{
paddingBlockPageContainerContent:0,
paddingInlinePageContainerContent:0,
}
bgLayout: '#17163E;',
header: {
heightLayoutHeader: 72
},
pageContainer: {
paddingBlockPageContainerContent: 0,
paddingInlinePageContainerContent: 0,
}
}
function BasicLayout({project = 'core'}:{project:string}){
const navigator = useNavigate()
const location = useLocation()
const currentUrl = location.pathname
const { state,accessData,checkPermission,accessInit,dispatch,resetAccess,getGlobalAccessData} = useGlobalContext()
const [pathname, setPathname] = useState(currentUrl); const mainPage = project === 'core' ?'/service/list':'/serviceHub/list'
const navigator = useNavigate()
const location = useLocation()
const currentUrl = location.pathname
const { state,accessData,checkPermission,accessInit,dispatch,resetAccess,getGlobalAccessData, menuList} = useGlobalContext()
const [pathname, setPathname] = useState(currentUrl);
const mainPage = project === 'core' ?'/service/list':'/serviceHub/list'
const [menuItems, setMenuItems] = useState<MenuProps['items']>();
const pluginSlotHub = usePluginSlotHub()
const TOTAL_MENU_ITEMS:MenuProps['items'] = useMemo(() => [
getNavItem($t('工作空间'), 'workspace','/guide/page',<Icon icon="ic:baseline-space-dashboard" width="18" height="18"/>, [
getNavItem(<a>{$t('首页')}</a>, 'guide','/guide/page',<Icon icon="ic:baseline-home" width="18" height="18"/>,undefined,undefined,'all'),
getNavItem(<a>{$t('服务')}</a>, 'service','/service',<Icon icon="ic:baseline-blinds-closed" width="18" height="18"/>,undefined,undefined,'all'),
getNavItem(<a>{$t('消费者')}</a>, 'consumer','/consumer',<Icon icon="ic:baseline-apps" width="18" height="18"/>,undefined,undefined,'all'),
getNavItem(<a>{$t('团队')}</a>, 'team','/team',<Icon icon="ic:baseline-people-alt" width="18" height="18"/>,undefined,undefined,'all'),
]),
getNavItem($t('API 市场'), 'serviceHub','/serviceHub',<Icon icon="ic:baseline-hub" width="18" height="18"/>,undefined,undefined,'system.api_portal.api_portal.view'),
useEffect(()=>{
const newMenu = transformMenuData(menuList)
setMenuItems(newMenu);
},[menuList, state.language,accessInit])
getNavItem($t('仪表盘'), 'mainPage', APP_MODE === 'pro' ? '/analytics' : '/analytics/total',<Icon icon="ic:baseline-bar-chart" width="18" height="18"/>,[
getNavItem(<a >{$t('运行视图')}</a>, 'analytics',APP_MODE === 'pro' ? '/analytics' : '/analytics/total' ,<ProjectFilled />,undefined,undefined,'system.analysis.run_view.view'),
APP_MODE === 'pro' ? getNavItem(<a >{$t('系统拓扑图')}</a>, 'systemrunning','/systemrunning',<ProjectFilled />,undefined,undefined,'system.dashboard.systemrunning.view') : null,
],undefined,'system.analysis.run_view.view'),
getNavItem($t('系统设置'), 'operationCenter','/commonsetting',<Icon icon="ic:baseline-settings" width="18" height="18"/>, [
getNavItem($t('系统'), 'serviceHubSetting','/commonsetting',null,[
getNavItem(<a>{$t('常规')}</a>, 'commonsetting','/commonsetting',<Icon icon="ic:baseline-hub" width="18" height="18"/>,undefined,undefined,'system.api_market.service_classification.view'),
getNavItem(<a>{$t('API 网关')}</a>, 'cluster','/cluster',<Icon icon="ic:baseline-device-hub" width="18" height="18"/>,undefined,undefined,'system.settings.api_gateway.view'),
getNavItem(<a>{$t('AI 模型')}</a>, 'aisetting','/aisetting',<Icon icon="hugeicons:ai-network" width="18" height="18"/>,undefined,undefined,'system.settings.api_gateway.view'),
],undefined,'system.api_market.service_classification.view'),
getNavItem($t('用户'), 'organization','/member',null,[
getNavItem(<a>{$t('账号')}</a>, 'member','/member',<Icon icon="ic:baseline-people-alt" width="18" height="18"/>,undefined,undefined,'system.settings.account.view'),
getNavItem(<a>{$t('角色')}</a>, 'role','/role',<Icon icon="ic:baseline-verified-user" width="18" height="18"/>,undefined,undefined,'system.organization.role.view'),
],undefined,''),
getNavItem($t('集成'), 'maintenanceCenter','/datasourcing', null, [
getNavItem(<a>{$t('数据源')}</a>, 'datasourcing','/datasourcing',<Icon icon="ic:baseline-monitor-heart" width="18" height="18"/>,undefined,undefined,'system.settings.data_source.view'),
getNavItem(<a>{$t('证书')}</a>, 'cert','/cert',<Icon icon="ic:baseline-security" width="18" height="18"/>,undefined,undefined,'system.settings.ssl_certificate.view'),
getNavItem(<a>{$t('日志')}</a>, 'logsettings','/logsettings',<Icon icon="ic:baseline-sticky-note-2" width="18" height="18"/>,undefined,undefined,'system.settings.log_configuration.view'),
APP_MODE === 'pro' ? getNavItem(<a>{$t('资源')}</a>, 'resourcesettings','/resourcesettings',null,undefined,undefined,'system.partition.self.view'):null,
APP_MODE === 'pro' ? getNavItem(<a>{$t('Open API')}</a>, 'openapi','/openapi',null,undefined,undefined,'system.openapi.self.view'):null,
]),
]),
],[state.language,accessInit])
useEffect(() => {
if (currentUrl === '/') {
navigator(mainPage)
}
}, [currentUrl]);
useEffect(() => {
if(currentUrl === '/'){
navigator(mainPage)
}
}, [currentUrl]);
const headerMenuData = useMemo(() => {
// 判断权限
const hasAccess = (access: unknown) => checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]);
const headerMenuData = useMemo(() => {
// 判断权限
const hasAccess = (access: unknown) => checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]);
// 过滤菜单项
const filterMenu = (menu: Array<{ [k: string]: unknown }>) => {
return [...menu]
.filter(x => x) // 过滤掉空数据
.map((item: any) => {
if (item.routes && item.routes.length > 0) {
// 递归处理子菜单
const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes);
if(filteredRoutes.length === 0){
return false
}
return {...item, routes: filteredRoutes};
}
// 处理没有 routes 的菜单项
if (item.access) {
return (item.access === 'all' || hasAccess(item.access)) ? item : null;
}
// 过滤菜单项
const filterMenu = (menu: Array<{ [k: string]: unknown }>) => {
return [...menu]
.filter(x => x) // 过滤掉空数据
.map((item: any) => {
if (item.routes && item.routes.length > 0) {
// 递归处理子菜单
const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes);
if (filteredRoutes.length === 0) {
return false
}
return { ...item,routes: filteredRoutes,name:$t(item.name) };
}
// 处理没有 routes 的菜单项
if (item.access) {
return (item.access === 'all' || hasAccess(item.access)) ? {...item,name:$t(item.name)} : null;
}
// 如果没有 access 和 routes,则保留
return item;
return {...item,name:$t(item.name) };
})
.filter(x => x); // 过滤掉处理后为 null 的项
};
// 初始过滤操作
const res = [...TOTAL_MENU_ITEMS]!.filter(x => x).map((x: any) => (x.routes ? { ...x, routes: filterMenu(x.routes) } : x));
const res = [...(menuItems || [])]!.filter(x => x).map((x: any) => (x.routes ? { ...x,name:$t(x.name), routes: filterMenu(x.routes) } : {...x,name:$t(x.name)}));
// 返回处理后的数据
return { path: '/', routes: res.map(x=> ({...x, routes: x.routes?.filter(x=> (x.access || x.routes?.length > 0))})).filter(x=> (x.access || x.routes?.length > 0)) };
}, [accessData, state.language]);
}, [accessData, state.language,menuItems]);
const { message } = App.useApp()
const [userInfo,setUserInfo] = useState<UserInfoType>()
const {fetchData} = useFetch()
const navigate = useNavigate();
const getUserInfo = ()=>{
fetchData<BasicResponse<{profile:UserInfoType}>>('account/profile',{method:'GET'})
.then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setUserInfo(data.profile)
dispatch({type:'UPDATE_USERDATA',userData:data.profile})
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const getUserInfo = () => {
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' })
.then(response => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setUserInfo(data.profile)
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
useEffect(() => {
getUserInfo()
getGlobalAccessData()
}, []);
const logOut = ()=>{
fetchData<BasicResponse<null>>('account/logout',{method:'GET'}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
dispatch({type:'LOGOUT'})
resetAccess()
// message.success(msg || $t(RESPONSE_TIPS.logoutSuccess))
navigate('/login')
}else{
message.error(msg ||$t(RESPONSE_TIPS.error))
}
})
}
useEffect(() => {
getUserInfo()
getGlobalAccessData()
}, []);
const items: MenuProps['items'] = useMemo(() => [
userInfo?.type !== 'guest' && {
key: '2',
label: (
<Button key="changePsw" type="text" className="flex items-center p-0 bg-transparent border-none " onClick={()=>navigator('/userProfile/changepsw')}>
{$t('账号设置')}
</Button>)
},
{
key: '3',
label: (
<Button key="logout" type="text" className="flex items-center p-0 bg-transparent border-none " onClick={logOut}>
{$t('退出登录')}
</Button>)
},
].filter(Boolean), [userInfo]);
const logOut = () => {
fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then(response => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'LOGOUT' })
resetAccess()
// message.success(msg || $t(RESPONSE_TIPS.logoutSuccess))
navigate('/login')
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const items: MenuProps['items'] = useMemo(() => [
userInfo?.type !== 'guest' && {
key: '2',
label: (
<Button key="changePsw" type="text" className="flex items-center p-0 bg-transparent border-none " onClick={() => navigator('/userProfile/changepsw')}>
{$t('账号设置')}
</Button>)
},
{
key: '3',
label: (
<Button key="logout" type="text" className="flex items-center p-0 bg-transparent border-none " onClick={logOut}>
{$t('退出登录')}
</Button>)
},
].filter(Boolean), [userInfo]);
const actionRender =useMemo( ()=>{
return [
<LanguageSetting />,
<Button className=" text-[#ffffffb3] hover:text-[#fff] border-none" type="default" ghost onClick={()=>{window.open('https://docs.apipark.com','_blank')}}>
<span className='flex items-center gap-[8px]'> <Icon icon="ic:baseline-help" width="14" height="14"/>{$t('文档')}</span>
</Button> ,
...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[] )||[] )
]
},[pluginSlotHub.getSlot('basicLayoutAfterBtns') ])
return(
@@ -226,12 +208,7 @@ const themeToken = {
actionsRender={(props) => {
if (props.isMobile) return [];
if (typeof window === 'undefined') return [];
return [
<LanguageSetting />,
<Button className=" text-[#ffffffb3] hover:text-[#fff] border-none" type="default" ghost onClick={()=>{window.open('https://docs.apipark.com','_blank')}}>
<span className='flex items-center gap-[8px]'> <Icon icon="ic:baseline-help" width="14" height="14"/>{$t('文档')}</span>
</Button>
];
return actionRender;
}}
headerTitleRender={() => (
<div className="w-[192px] flex items-center">
@@ -0,0 +1,22 @@
import React, { useEffect, useState } from 'react';
import { Result, Skeleton } from 'antd';
const NotFound: React.FC = () => {
const [showPage, setShowPage] = useState<boolean>(false)
useEffect(()=>{
setTimeout(()=>setShowPage(true), 1000)
},[])
return (
<div className={`h-full w-full flex flex-1 align-middle ${showPage ? 'items-center' : ''}`}>
{ showPage ? <Result
className='w-full'
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
/> : <Skeleton active /> }
</div>
)}
export default NotFound;
@@ -0,0 +1,96 @@
import {App, Form, Input, Row, Table} from "antd";
import {forwardRef, useEffect, useImperativeHandle, useMemo} from "react";
import {useFetch} from "@common/hooks/http.ts";
import {BasicResponse, PLACEHOLDER, PolicyPublishColumns, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { PolicyPublishModalHandle, PolicyPublishModalProps } from "@common/const/type";
export const PolicyPublishModalContent = forwardRef<PolicyPublishModalHandle,PolicyPublishModalProps>((props, ref) => {
const { message } = App.useApp()
const { data} = props
const [form] = Form.useForm();
const {fetchData} = useFetch()
const {state} = useGlobalContext()
const publish:()=>Promise<boolean | string | Record<string, unknown>> = ()=>{
return new Promise((resolve, reject)=>{
form.validateFields().then((value)=>{
const body = {...value, source:data.source}
fetchData<BasicResponse<null>>('strategy/global/data-masking/publish',{method: 'POST',eoBody:body,eoTransformKeys:['versionName']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(response)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
publish,
})
)
useEffect(()=>{
form.setFieldsValue(data)
},[data])
const translatedPolicyColumns = useMemo(()=>PolicyPublishColumns.map((x)=>({
...x,
title: typeof x.title === 'string' ? $t(x.title) : x.title,
})),[state.language])
console.log(translatedPolicyColumns,data.strategies)
return (
<>
<WithPermission access=""><Form
className=" mx-auto"
form={form}
labelAlign='left'
layout='vertical'
scrollToFirstError
name="publishApprovalModalContent"
// labelCol={{span: 3}}
// wrapperCol={{span: 21}}
autoComplete="off"
>
<Form.Item
label={$t("发布名称")}
name='versionName'
rules={[{required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item
label={$t("描述")}
name="desc"
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('策略列表')}</span></Row>
<Row className="mb-mbase ">
<Table
columns={translatedPolicyColumns}
bordered={true}
rowKey="name"
size="small"
dataSource={data.strategies || []}
pagination={false}
/>
{!data?.isPublish&& data?.unpublishMsg&& <p className="text-status_fail mt-[4px]">{data.unpublishMsg}</p>}
</Row>
</Form>
</WithPermission>
</>)
})
@@ -8,64 +8,65 @@ import { Icon } from "@iconify/react/dist/iconify.js"
import { $t } from "@common/locales"
type TableBtnWithPermissionProps = {
btnTitle:string
access?:keyof typeof PERMISSION_DEFINITION[0],
tooltip?:string,
disabled?:boolean,
navigateTo?:string,
onClick?:(args?:unknown)=>void
className?:string
btnType:string
btnTitle: string
access?: keyof typeof PERMISSION_DEFINITION[0],
tooltip?: string,
disabled?: boolean,
navigateTo?: string,
onClick?: (args?: unknown) => void
className?: string
btnType: string
}
const TableIconName={
'add':'ic:baseline-add',
'edit':'ic:baseline-edit',
'delete':'ic:baseline-delete',
'remove':'ic:baseline-minus',
'copy':'ic:baseline-file-copy',
'view':'ic:baseline-remove-red-eye',
'publish':'ic:baseline-publish',
'approval':'ic:baseline-approval',
'stop':'ic:baseline-stop-circle',
'online':'ic:baseline-check-circle',
'cancel':'ic:baseline-cancel-schedule-send',
'refresh':'ic:baseline-refresh'
const TableIconName = {
'add': 'ic:baseline-add',
'edit': 'ic:baseline-edit',
'delete': 'ic:baseline-delete',
'remove': 'ic:baseline-minus',
'copy': 'ic:baseline-file-copy',
'view': 'ic:baseline-remove-red-eye',
'publish': 'ic:baseline-publish',
'approval': 'ic:baseline-approval',
'stop': 'ic:baseline-stop-circle',
'online': 'ic:baseline-check-circle',
'cancel': 'ic:baseline-cancel-schedule-send',
'refresh': 'ic:baseline-refresh',
'logs': 'hugeicons:google-doc'
}
// 表格操作栏按钮,受权限控制
const TableBtnWithPermission = ({btnTitle, access, tooltip, disabled, navigateTo, onClick,className,btnType}:TableBtnWithPermissionProps) => {
const [btnAccess, setBtnAccess] = useState<boolean>(false)
const {accessData,checkPermission,accessInit} = useGlobalContext()
const navigate = useNavigate()
const lastAccess = useMemo(()=>{
if(!accessInit) return false
if(!access) return true
return checkPermission(access)
},[access, accessData,checkPermission,accessInit])
const TableBtnWithPermission = ({ btnTitle, access, tooltip, disabled, navigateTo, onClick, className, btnType }: TableBtnWithPermissionProps) => {
useEffect(()=>{
access ? setBtnAccess(lastAccess) : setBtnAccess(true)
},[access, lastAccess])
const [btnAccess, setBtnAccess] = useState<boolean>(false)
const { accessData, checkPermission, accessInit } = useGlobalContext()
const navigate = useNavigate()
const lastAccess = useMemo(() => {
if (!accessInit) return false
if (!access) return true
return checkPermission(access)
}, [access, accessData, checkPermission, accessInit])
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
navigateTo ? navigate(navigateTo) : onClick?.()
}, [navigateTo, navigate, onClick])
return (<>{
!btnAccess || (disabled&&tooltip) ?
<Tooltip placement="top" title={tooltip ?? $t('暂无(0)权限,请联系管理员分配。',[$t(btnTitle).toLowerCase()])}>
<Button type="text" disabled={true} className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className}`} key={btnType} icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18"/>} >{}</Button>
</Tooltip>
:
<Tooltip placement="top" title={$t(btnTitle)}>
<Button type="text" disabled={disabled} className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className} `} key={btnType} icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18"/>} onClick={handleClick}>{}</Button>
</Tooltip>
useEffect(() => {
access ? setBtnAccess(lastAccess) : setBtnAccess(true)
}, [access, lastAccess])
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
navigateTo ? navigate(navigateTo) : onClick?.()
}, [navigateTo, navigate, onClick])
return (<>{
!btnAccess || (disabled && tooltip) ?
<Tooltip placement="top" title={tooltip ?? $t('暂无(0)权限,请联系管理员分配。', [$t(btnTitle).toLowerCase()])}>
<Button type="text" disabled={true} className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className}`} key={btnType} icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />} >{ }</Button>
</Tooltip>
:
<Tooltip placement="top" title={$t(btnTitle)}>
<Button type="text" disabled={disabled} className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className} `} key={btnType} icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />} onClick={handleClick}>{ }</Button>
</Tooltip>
}</>
);
}
}</>
);
}
export default TableBtnWithPermission
@@ -159,7 +159,26 @@ export const TranslateWord = ()=>{
{$t('地址')}
{$t('新增')}
{$t('申请方消费者')}
{$t('策略名称')}
{$t('优先级')}
{$t('筛选条件')}
{$t('处理数')}
{$t('数据格式')}
{$t('关键字')}
{$t('正则表达式')}
{$t('手机号')}
{$t('身份证号')}
{$t('银行卡号')}
{$t('金额')}
{$t('日期')}
{$t('局部显示')}
{$t('局部遮蔽')}
{$t('截取')}
{$t('替换')}
{$t('乱序')}
{$t('随机字符串')}
{$t('自定义字符串')}
{$t('请输入IP地址或CIDR范围,每条以换行分割')}
</>
)
}
@@ -0,0 +1,82 @@
import { set } from 'lodash-es';
import { ExoticComponent, JSXElementConstructor, ReactElement, useEffect, useState } from 'react';
import { useBlocker, useLocation, useNavigate } from 'react-router-dom';
import { JSX } from 'react/jsx-runtime';
const withRouteGuard = (WrappedComponent: ExoticComponent<any> | JSXElementConstructor<any>, {
canActivate,
canLoad ,
canDeactivate,
deactivated,
pathPrefix
}: { pathPrefix?:string, canActivate?: () => Promise<boolean>; canLoad?: () => Promise<boolean>; canDeactivate?: () => Promise<boolean>; deactivated?: () => Promise<void>; } = {}) => {
return function RouteGuard(props: JSX.IntrinsicAttributes) {
const [isActivated, setIsActivated] = useState<boolean>(false);
const location = useLocation();
useEffect(()=>{
console.log('路由守卫')
},[])
// check canActivate
const startLifecycle = async ()=>{
if(canActivate){
const activateRes = await canActivate();
setIsActivated(activateRes);
}else{
setIsActivated(true);
}
}
// check canDeactivate
const handleBeforeUnload =async (event: { preventDefault: () => void; returnValue: string; }) => {
const deactivateRes = canDeactivate? await canDeactivate():true;
if (!deactivateRes) {
event.preventDefault();
event.returnValue = '';
}
};
// 激活组件时的检查
useEffect(() => {
startLifecycle();
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
deactivated?.();
};
}, []);
const blocker = useBlocker((tx) => {
const currentPath = location.pathname;
const targetPath = tx.nextLocation.pathname;
if (pathPrefix && currentPath.startsWith(pathPrefix) && !targetPath.startsWith(pathPrefix) && canDeactivate) {
canDeactivate().then((res) => {
if(res){
return false;
}else{
return true;
}
})
} else {
return false
}
});
const checkCanLoad = async()=>{
const loadRes = await canLoad!();
!loadRes && setIsActivated(false);
}
useEffect(() => {
if (isActivated && canLoad) {
checkCanLoad()
}
}, [isActivated]);
return isActivated ? <WrappedComponent {...props}/> : null;
};
}
export default withRouteGuard;
@@ -58,7 +58,7 @@ import {
UPDATE_DATASETS_EVENT_EMITTER,
UPDATE_HISTORY_EVENT_EMITTER,
} from './constants'
import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
import { useEventEmitterContextContext } from '@common/contexts/EventEmitterContext'
export type PromptEditorProps = {
instanceId?: string
@@ -30,7 +30,7 @@ import { $splitNodeContainingQuery } from '../../utils'
import { useOptions } from './hooks'
import type { PickerBlockMenuOption } from './menu'
// import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
import { useEventEmitterContextContext } from '@common/contexts/EventEmitterContext'
type ComponentPickerProps = {
triggerString: string
@@ -14,7 +14,7 @@ import { DELETE_CONTEXT_BLOCK_COMMAND } from './index'
// PortalToFollowElemContent,
// PortalToFollowElemTrigger,
// } from '@/app/components/base/portal-to-follow-elem'
import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
import { useEventEmitterContextContext } from '@common/contexts/EventEmitterContext'
import { $t } from '@common/locales'
type ContextBlockComponentProps = {
@@ -14,7 +14,7 @@ import { DELETE_HISTORY_BLOCK_COMMAND } from './index'
// PortalToFollowElemContent,
// PortalToFollowElemTrigger,
// } from '@/app/components/base/portal-to-follow-elem'
import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
import { useEventEmitterContextContext } from '@common/contexts/EventEmitterContext'
import { $t } from '@common/locales'
type HistoryBlockComponentProps = {
@@ -3,7 +3,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { textToEditorState } from '../utils'
import { CustomTextNode } from './custom-text/node'
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
import { useEventEmitterContextContext } from '@common/contexts/EventEmitterContext'
export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY'
+35 -3
View File
@@ -1,3 +1,5 @@
import { $t } from '@common/locales'
import { StrategyStatusColorClass, StrategyStatusEnum } from './policy/consts'
export type BasicResponse<T> = {
code:number
@@ -5,7 +7,6 @@ export type BasicResponse<T> = {
msg:string
}
export const STATUS_CODE = {
SUCCESS:0,
UNANTHORIZED:401,
@@ -18,7 +19,7 @@ export const STATUS_COLOR = {
}
// avoid changing route within ths same category
// TODO should be generated dynamically
export const routerKeyMap = new Map<string, string[]|string>([
['workspace',['consumer','service','team','guide']],
['my',['consumer','service','team']],
@@ -29,6 +30,7 @@ export const routerKeyMap = new Map<string, string[]|string>([
['maintenanceCenter',['aisetting','datasourcing','cluster','cert','logsettings','resourcesettings','openapi']
]])
export const COLUMNS_TITLE = {
operate : ''
@@ -45,6 +47,7 @@ export const routerKeyMap = new Map<string, string[]|string>([
startWithAlphabet:('英文数字下划线任意一种,首字母必须为英文'),
specialStartWithAlphabet:('支持字母开头、英文数字中横线下划线组合'),
onlyAlphabet:('字符非法,仅支持英文'),
ipAndCidr:'请输入IP地址或CIDR范围,每条以换行分割'
}
export const FORM_ERROR_TIPS = {
@@ -73,4 +76,33 @@ export const routerKeyMap = new Map<string, string[]|string>([
export const DATA_SHOW_TYPE_OPTIONS = [
{label:'列表', value:'list'},
{label:'块', value:'block'},
]
]
export const PolicyPublishColumns = [
{
title: ('策略名称'),
dataIndex: 'name',
ellipsis: true,
width: 160
},
{
title: ('优先级'),
dataIndex: 'priority',
width: 140,
ellipsis: true
},
{
title: ('状态'),
dataIndex: 'status',
width: 140,
render:(text:string)=> <span className={StrategyStatusColorClass[text as keyof typeof StrategyStatusColorClass]}>{$t(StrategyStatusEnum[text as keyof typeof StrategyStatusEnum])}</span>,
},
{
title: ('更新时间'),
dataIndex: 'optTime',
width: 182,
ellipsis: true,
},
]
@@ -0,0 +1,57 @@
export const MatchRules = [
{ value: 'inner', label: '数据格式' },
{ value: 'keyword', label: '关键字' },
{ value: 'regex', label: '正则表达式' },
{ value: 'json_path', label: 'JSON Path' }
];
export const DataFormatOptions = [
{ label: '姓名', value: 'name' },
{ label: '手机号', value: 'phone' },
{ label: '身份证号', value: 'id-card' },
{ label: '银行卡号', value: 'bank-card' },
{ label: '日期', value: 'date' },
{ label: '金额', value: 'amount' }
];
export const DataMaskBaseOptionOptions = [
{ value: 'partial-display', label: '局部显示' },
{ value: 'partial-masking', label: '局部遮蔽' },
{ value: 'truncation', label: '截取' },
{ value: 'replacement', label: '替换' },
];
export const DataMaskOrderOptions = [
...DataMaskBaseOptionOptions,
{ label: '乱序', value: 'shuffling' }
]
export const DataMaskReplaceStrOptions = [
{ value: 'random', label: '随机字符串' },
{ value: 'custom', label: '自定义字符串' }
];
export const PolicyOptions = [
{label:'数据脱敏',value:'data-masking'},
]
export const StrategyStatusEnum = {
'update':'待更新',
'online':'已发布',
'offline':'未发布',
"delete":'待删除',
}
export const StrategyStatusColorClass = {
"online":'text-status_success',
"update":'text-status_pending',
"offline":'text-status_fail',
"delete":'text-status_offline',
}
@@ -0,0 +1,123 @@
import { DefaultOptionType } from "antd/es/select";
import { StrategyStatusEnum } from "./consts";
export type DataMaskRuleTableProps = {
disabled?: boolean;
value?: MaskRuleData[];
onChange?: (value: MaskRuleData[]) => void;
}
export type MaskRuleData = {
match: {
type: string;
value: string;
};
mask: {
type: string;
begin?: number;
length?: number;
replace?: {
type: string;
value: string;
};
};
eoKey?: string;
}
export type DataMaskRuleFormProps = {
editData?: MaskRuleData;
ruleList: MaskRuleData[];
onSave: (ruleList: MaskRuleData[]) => void;
onClose: () => void;
modalVisible:boolean
}
export type DataMaskingConfigHandle = {
save: (values: any) => void
}
export type PolicyMatchType = {name:string, values:string[], label?:string, title?:string, type?:string}
export type DataMaskingRulesType = {}
export type DataMaskingConfigFieldType = {
id:string
name:string
priority:number
description:string
filters:PolicyMatchType[]
config:{
rules:DataMaskingRulesType
}
}
export type DataMaskStrategyItem = {
id:string
name:string
priority:number
isStop:boolean
isDeleted:boolean
publishStatus:keyof typeof StrategyStatusEnum
filters:string
conf:string
operator:string
updateTime:string
}
export type FilterFormField= {
name: string;
value:string[] |string;
label:string
title:string
}
export type FilterOptionType = {
name:string
pattern:string
title:string
type:'remote'|'pattern'|'static'
options:string[]
}
export type FilterTableProps = {
disabled?: boolean;
drawerTitle?: string;
value?:FilterFormField[];
onChange?:(val:FilterFormField[])=>void
}
export type FilterFormType = {
name:string
value:unknown
}
export type FilterFormProps = {
filterForm: FilterFormType;
filterOptions:DefaultOptionType[];
selectedOptionNameSet: Set<string>;
disabled: boolean;
onFilterFormChange: (form: FilterFormType) => void;
setFormCanSubmit:(canSubmit:boolean)=>void
}
export type FilterFormHandle = {
clear:()=>void
save:()=>Promise<FilterFormField>
}
export type FilterFormItemProps = {
value?: string[];
onChange?: (value: string[]) => void;
disabled:boolean
option:unknown
onShowValueChange?:(value:string)=>void
}
export type RemoteTitleType = {
title:string
field:string
}
+112 -1
View File
@@ -1,5 +1,9 @@
import { FC, ReactElement, ReactNode } from "react"
import { PERMISSION_DEFINITION } from "./permissions"
import { MatchPositionEnum, MatchTypeEnum } from "@core/const/system/const"
import usePluginLoader from "@common/hooks/pluginLoader"
import { useGlobalContext } from "@common/contexts/GlobalStateContext"
import { StrategyStatusEnum } from "./policy/consts"
export type UserInfoType = {
username: string
@@ -8,7 +12,6 @@ export type UserInfoType = {
phone: string
avatar: string
type:string
}
export type UserProfileProps = {
@@ -100,4 +103,112 @@ export type SimpleMemberItem = {
email:string
department:string
avatar:string
}
export type RouteConfig = {
path:string
pathPrefix?:string
component?:ReactElement
children?:(RouteConfig|false)[]
key:string
provider?:FC<{ children: ReactNode; }>
lazy?:unknown
data?:Record<string, string>
lifecycle?:{
canActivate?:()=>Promise<boolean>
canLoad?:()=>Promise<boolean>
canDeactivate?:()=>Promise<boolean>
deactivated?:()=>Promise<boolean>
}
}
export type RouterParams = {
teamId:string
apiId:string
serviceId:string
clusterId:string;
memberGroupId:string
userGroupId:string
pluginName:string
moduleId:string
accessType:'project'|'team'|'service'
categoryId:string
tagId:string
dashboardType:string
dashboardDetailId:string
topologyId:string
appId:string
roleType:string
roleId:string
routeId:string
policyId:string
}
export type PluginRouterConfig = {
name:string
path:string;
type:string;
expose?:string
}
export type CoreObj = {
routerConfig: RouteConfig[];
setExecuteList: (param:unknown[])=>void;
pluginLoader: {
loadModule: (path: string, name: string, expose: string, pluginPath: string) => Promise<any>;
};
pluginProvider: ReturnType<typeof useGlobalContext>
// pluginLifecycleGuard: PluginLifecycleGuard;
builtInPluginLoader: (name: string) => any;
}
export type PluginConfigType = {
name: string;
router: Array<PluginRouterConfig>;
path?: string;
driver:string
}
export type ApiparkPluginDriverType = {
[key:string]:{[key:string]:(coreObj?:CoreObj, pluginConfig?:PluginConfigType)=>(CoreObj|undefined)}
}
export type RouterMapConfig = {
type: 'component' | 'module',
component: ReactElement,
provider?: FC,
lazy?: FC
key?: string
children?: RouteConfig[]
data?:Record<string, string>
pathMatch?:string
}
export type PolichPublishItemType = {
name:string
priority:number
status:keyof typeof StrategyStatusEnum
optTime:string
}
// 发布详情(版本)
export type PolicyPublishInfoType = {
source:string
strategies:Array<PolichPublishItemType>
isPublish:boolean
versionName:string
unpublishMsg:string
};
export type PolicyPublishModalProps = {
data:PolicyPublishInfoType
}
export type PolicyPublishModalHandle = {
publish:()=>Promise<boolean|string|Record<string, unknown>>
}
@@ -1,10 +1,17 @@
import {createContext, Dispatch, FC, ReactNode, useContext, useReducer, useState} from "react";
import {createContext, Dispatch, FC, ReactNode, useContext, useEffect, useReducer, useState} from "react";
import { useFetch } from "@common/hooks/http";
import { App } from "antd";
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
import { checkAccess } from "@common/utils/permission";
import { PERMISSION_DEFINITION } from "@common/const/permissions";
import { $t } from "@common/locales";
import { MenuItem } from "@common/utils/navigation";
import { ErrorBoundary } from "@ant-design/pro-components";
import NotFound from "@common/components/aoplatform/NotFound";
import { RouteConfig } from "@common/const/type";
import { ProtectedRoute } from "@core/components/aoplatform/RenderRoutes";
import Login from "@core/pages/Login";
import { useLocaleContext } from "./LocaleContext";
interface GlobalState {
isAuthenticated: boolean;
@@ -31,6 +38,168 @@ export type GlobalAction =
| { type: 'UPDATE_LANGUAGE'; language: string }
const mockData = [
{
"name": "工作空间",
"key": "workspace",
"path": "/guide/page",
"icon": "ic:baseline-space-dashboard",
"children": [
{
"name": "首页",
"key": "guide",
"path": "/guide/page",
"icon": "ic:baseline-home",
"access": "all"
},
{
"name": "服务",
"key": "service",
"path": "/service",
"icon": "ic:baseline-blinds-closed",
"access": "all"
},
{
"name": "消费者",
"key": "consumer",
"path": "/consumer",
"icon": "ic:baseline-apps",
"access": "all"
},
{
"name": "团队",
"key": "team",
"path": "/team",
"icon": "ic:baseline-people-alt",
"access": "all"
},
// {
// "name": "路由组件",
// "key": "router",
// "path": "/router1",
// "icon": "ic:baseline-people-alt",
// "access": "all"
// }
]
},
{
"name": "API 市场",
"key": "serviceHub",
"path": "/serviceHub",
"icon": "ic:baseline-hub",
"access": "system.api_portal.api_portal.view"
},
{
"name": "仪表盘",
"key": "analytics",
"path": "/analytics",
"icon": "ic:baseline-bar-chart",
"children": [
{
"name": "运行视图",
"key": "analytics",
"path": "/analytics",
"icon": "ic:baseline-bar-chart",
"access": "system.analysis.run_view.view"
}
],
"access": "system.analysis.run_view.view"
},
{
"name": "系统设置",
"key": "operationCenter",
"path": "/commonsetting",
"icon": "ic:baseline-settings",
"children": [
{
"name": "系统",
"key": "serviceHubSetting",
"path": "/commonsetting",
"children": [
{
"name": "常规",
"key": "commonsetting",
"path": "/commonsetting",
"icon": "ic:baseline-hub",
"access": "system.api_market.service_classification.view"
},
{
"name": "API 网关",
"key": "cluster",
"path": "/cluster",
"icon": "ic:baseline-device-hub",
"access": "system.settings.api_gateway.view"
},
{
"name": "AI 模型",
"key": "aisetting",
"path": "/aisetting",
"icon": "hugeicons:ai-network",
"access": "system.settings.ai_provider.view"
}
],
},
{
"name": "用户",
"key": "organization",
"path": "/member",
"children": [
{
"name": "账号",
"key": "member",
"path": "/member",
"icon": "ic:baseline-people-alt",
"access": "system.settings.account.view"
},
{
"name": "角色",
"key": "role",
"path": "/role",
"icon": "ic:baseline-verified-user",
"access": "system.organization.role.view"
}
]
},
{
"name": "集成",
"key": "maintenanceCenter",
"path": "/datasourcing",
"children": [
{
"name": "数据源",
"key": "datasourcing",
"path": "/datasourcing",
"icon": "ic:baseline-monitor-heart",
"access": "system.settings.data_source.view"
},
{
"name": "全局策略",
"key": "globalpolicy",
"path": "/globalpolicy",
"icon": "uil:comment-shield",
"access": "system.settings.data_source.view"
},
{
"name": "证书",
"key": "cert",
"path": "/cert",
"icon": "ic:baseline-security",
"access": "system.settings.ssl_certificate.view"
},
{
"name": "日志",
"key": "logsettings",
"path": "/logsettings",
"icon": "ic:baseline-sticky-note-2",
"access": "system.settings.log_configuration.view"
},
]
}
]
}
]
/*
存储用户登录、信息、权限等数据
*/
@@ -39,9 +208,11 @@ export const GlobalContext = createContext<{
dispatch: Dispatch<GlobalAction>;
accessData:Map<string,string[]>;
pluginAccessDictionary:{[k:string]:string};
menuList:MenuItem[];
getGlobalAccessData:()=>Promise<{ access:string[]}>;
getTeamAccessData:(teamId:string)=>void;
getPluginAccessDictionary:(pluginData:{[k:string]:string})=>void
getMenuList:()=>void
resetAccess:()=>void
cleanTeamAccessData:()=>void
checkPermission:(access:keyof typeof PERMISSION_DEFINITION[0] | Array<keyof typeof PERMISSION_DEFINITION[0]>)=>boolean
@@ -49,6 +220,11 @@ export const GlobalContext = createContext<{
accessInit:boolean
aiConfigFlushed:boolean
setAiConfigFlushed:(flush:boolean)=>void
routeConfig: RouteConfig[];
setRouterConfig: (isRoot: boolean, config: RouteConfig) => void;
addRouteConfig: (parentRoute: RouteConfig, config: RouteConfig) => void;
fetchData: ReturnType<typeof useFetch>['fetchData'];
$t: typeof $t;
} | undefined>(undefined);
const globalReducer = (state: GlobalState, action: GlobalAction): GlobalState => {
@@ -99,10 +275,18 @@ const globalReducer = (state: GlobalState, action: GlobalAction): GlobalState =>
}
};
export const DefaultRouteConfig = [
{ path: '/', pathMatch: 'full', component: <Login /> ,key:'root',},
{ path: '/login', component: <Login /> ,key:'login'},
{ path: '/', pathMatch:'prefix',component:<ProtectedRoute /> ,key:'basciLayout',children:[
{ path: '*', component: <ErrorBoundary><NotFound/></ErrorBoundary>, key: 'errorBoundary' }
]}
]
// Create a context provider component
export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => {
const {fetchData} = useFetch()
const { message } = App.useApp()
const { setLocale } = useLocaleContext();
const [state, dispatch] = useReducer(globalReducer, {
isAuthenticated: true, //mock用
userData: null,
@@ -118,6 +302,43 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => {
const [accessInit, setAccessInit] = useState<boolean>(false)
const [aiConfigFlushed, setAiConfigFlushed] = useState<boolean>(false)
let getGlobalAccessPromise: Promise<BasicResponse<{ access:string[] }>> | null = null
const [menuList, setMenuList] = useState<MenuItem[]>(mockData);
const [routeConfig, setRouteConfigState] = useState<RouteConfig[]>(DefaultRouteConfig)
useEffect(() => {
setLocale(state.language);
}, [state.language, setLocale]);
const { fetchData } = useFetch();
const setRouterConfig = (isRoot: boolean, config: RouteConfig) => {
setRouteConfigState(prevConfig => {
if (isRoot) {
return [config,...prevConfig];
} else {
const rootRoute = prevConfig.find(route => route.path === '/' && route?.pathMatch === 'prefix') ;
if (rootRoute ) {
rootRoute.children = rootRoute.children ? [config, ...rootRoute.children] : [config];
}
return [...prevConfig];
}
});
};
const addRouteConfig = (parentRoute: RouteConfig, config: RouteConfig) => {
const addConfigToParent = (routes: RouteConfig[]): RouteConfig[] => {
return routes.map(route => {
if (route.key === parentRoute.key) {
route.children = route.children ? [...route.children, config] : [config];
} else if (route.children) {
route.children = addConfigToParent(route.children);
}
return route;
});
};
setRouteConfigState(prevConfig => addConfigToParent(prevConfig));
};
const getGlobalAccessData = ()=>{
if(getGlobalAccessPromise){
@@ -151,6 +372,18 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => {
})
}
const getMenuList = ()=>{
// TODO 等待对接后端接口
// fetchData<BasicResponse<{ access:string[]}>>('profile/permission/team',{method:'GET',eoParams:{team:teamId}},).then(response=>{
// const {code,data,msg} = response
// if(code === STATUS_CODE.SUCCESS){
// setMenuList(data.menus)
// }else{
// message.error(msg || $t(RESPONSE_TIPS.error))
// }
// })
}
const cleanTeamAccessData = ()=>{
setTeamDataFlushed(false)
setAccessData(prevData => prevData.set('team',[]))
@@ -176,15 +409,26 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => {
return revs
}
return (
<GlobalContext.Provider value={
{ state, dispatch,accessData,pluginAccessDictionary,
{ state, dispatch,
accessData,
pluginAccessDictionary,
getGlobalAccessData,
getPluginAccessDictionary,
getTeamAccessData,teamDataFlushed,
getTeamAccessData,teamDataFlushed,getMenuList,menuList,
cleanTeamAccessData,
resetAccess ,checkPermission,accessInit,
aiConfigFlushed, setAiConfigFlushed}}>
resetAccess ,checkPermission,
accessInit,
aiConfigFlushed,
setAiConfigFlushed,
routeConfig,
setRouterConfig,
addRouteConfig,
fetchData,
$t:$t,}}>
{children}
</GlobalContext.Provider>
);
@@ -0,0 +1,47 @@
import React, { createContext, useContext, useState, useEffect, useMemo, FC, ReactNode } from 'react';
import { ConfigProviderProps } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
import zhTW from 'antd/locale/zh_TW';
import jaJP from 'antd/locale/ja_JP';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/zh-tw';
import 'dayjs/locale/ja';
type Locale = ConfigProviderProps['locale'];
const languageMap: Record<string, Locale> = {
'zh-CN': zhCN,
'en-US': enUS,
'zh-TW': zhTW,
'ja-JP': jaJP,
};
const LocaleContext = createContext<{
locale: Locale;
setLocale: (locale: string) => void;
}|undefined>(undefined);
export const LocaleProvider: FC<{children:ReactNode}> = ({ children }) => {
const [locale, setLocaleState] = useState<Locale>(zhCN);
const setLocale = (language: string) => {
dayjs.locale(language);
setLocaleState(languageMap[language]);
};
return (
<LocaleContext.Provider value={{ locale, setLocale }}>
{children}
</LocaleContext.Provider>
);
};
export const useLocaleContext = () => {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('useLocaleContext must be used within a LocaleContext');
}
return context;
};
@@ -0,0 +1,76 @@
import { createContext, FC, ReactNode, useContext, useState } from "react";
class EventEmitter {
// 用来存放注册的事件与回调
_events:any
constructor () {
this._events = {}
}
on (eventName:string, callback:Function) {
// 由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
const callbacks = this._events[eventName] || []
callbacks.push(callback)
this._events[eventName] = callbacks
}
// 此处需要处理,emit时需要按顺序执行监听的函数,每个函数都会返回是否中止的参数,如果中止则不执行后续的函数
// emit传入eventName 和 event, 返回 event
emit (eventName:string, event:any) {
return new Promise((resolve) => {
const callbacks = this._events[eventName] || []
for (const cb of callbacks) {
const cbRes = cb(event.data)
if (cbRes.continue === false) {
resolve(cbRes)
break
} else {
event = cbRes
}
}
resolve(event.data)
})
}
// 取消订阅
off (eventName:string, callback:Function) {
const callbacks = this._events[eventName] || []
const newCallbacks = callbacks.filter((fn:any) => fn !== callback && fn.initialCallback !== callback /* 用于once的取消订阅 */)
this._events[eventName] = newCallbacks
}
// 单次订阅,后台插件可以自行决定取消对事件的订阅
once (eventName:string, callback:Function) {
// 由于需要在回调函数执行后,取消订阅当前事件,所以需要对传入的回调函数做一层包装,然后绑定包装后的函数
const one = (...args:any) => {
callback(...args)
this.off(eventName, one)
}
// 由于:我们订阅事件的时候,修改了原回调函数的引用,所以,用户触发 off 的时候不能找到对应的回调函数
// 所以,我们需要在当前函数与用户传入的回调函数做一个绑定,我们通过自定义属性来实现
one.initialCallback = callback
this.on(eventName, one)
}
}
export const PluginEventHubContext = createContext<EventEmitter | undefined>(undefined);
export const PluginEventHubProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [pluginEventHub] = useState<EventEmitter>(new EventEmitter());
return (
<PluginEventHubContext.Provider value={pluginEventHub}>
{children}
</PluginEventHubContext.Provider>
);
};
export const usePluginEventHub = () => {
const context = useContext(PluginEventHubContext);
if (!context) {
throw new Error('usePluginEventHub must be used within a PluginEventHubProvider');
}
return context;
};
@@ -0,0 +1,36 @@
import { createContext, FC, ReactNode, useContext, useState } from "react";
export const PluginSlotHubContext = createContext<{
addSlot: (name: string, content: unknown) => void;
addSlotArr: (name: string, content: unknown[]) => void;
removeSlot: (name: string) => void;
getSlot: (name: string) => unknown;
} | undefined>(undefined);
export const PluginSlotHubProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [pluginSlotHub] = useState<Map<string, unknown>>(new Map());
const pluginSlotHubService = {
addSlot: (name: string, content: any) => {
pluginSlotHub.set(name, pluginSlotHub.get(name) ? [...(pluginSlotHub.get(name) as Array<unknown>), content] : [content] ); },
addSlotArr: (name: string, content: any[]) => { pluginSlotHub.get(name) ? pluginSlotHub.set(name, (pluginSlotHub.get(name) as Array<unknown>).push(content)) : pluginSlotHub.set(name, content); },
removeSlot: (name: string) => { pluginSlotHub.delete(name); },
getSlot: (name: string) => {
return pluginSlotHub.get(name) ; }
};
return (
<PluginSlotHubContext.Provider value={pluginSlotHubService}>
{children}
</PluginSlotHubContext.Provider>
);
};
export const usePluginSlotHub = () => {
const context = useContext(PluginSlotHubContext);
if (!context) {
throw new Error('usePluginSlotHub must be used within a PluginSlotHubProvider');
}
return context;
};
+10 -3
View File
@@ -1,4 +1,6 @@
import { STATUS_CODE } from "@common/const/const";
import { BasicResponse, STATUS_CODE } from "@common/const/const";
import { usePluginEventHub } from "@common/contexts/PluginEventHubContext";
import EventEmitter from "events";
const urlWhiteList = [/api.example.com\/users/, /api.example2.com\/products/]; // 正则白名单
@@ -137,7 +139,10 @@ type EoRequest = RequestInit & {eoParams?:{[k:string]:unknown},eoTransformKeys?:
type EoHeaders = Headers | {[k:string]:string}
export function useFetch(){
export function useFetch() {
// plugin cannot use usePluginEventHub directly, so we need to pass it as a parameter
const pluginEventHub = usePluginEventHub()
function fetchData<T>(url:string, options: EoRequest ) {
// 合并传入的headers与默认headers
const headers = { ...(options.body ? {}:DEFAULT_HEADERS), ...options.headers };
@@ -184,9 +189,11 @@ export function useFetch(){
// 如果响应体为JSON且指定了转换键,则转换响应数据
if ( isJsonHttp(response.headers)) {
const data = await response.json();
return shouldTransformKeys ? keysToCamel(data,options.eoTransformKeys as string[]) as T:data
const newData = await pluginEventHub.emit('httpResponse', {data,continue:true}) as Response;
return shouldTransformKeys ? keysToCamel(newData,options.eoTransformKeys as string[]) as T:data
}
return response;
})
.catch(error => {
@@ -0,0 +1,449 @@
import { useEffect, useState } from "react";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { DEFAULT_LOCAL_PLUGIN_PATH, generateRemoteModuleTemplate, loadRemoteModule, validateExportLifecycle } from "@common/utils/plugin.tsx";
import { useFetch } from "@common/hooks/http";
import { PluginConfigType, RouteConfig } from "@common/const/type.ts";
import { ApiparkPluginDriverType ,RouterMapConfig} from "@common/const/type";
import { usePluginEventHub } from "@common/contexts/PluginEventHubContext";
import { usePluginSlotHub } from "@common/contexts/PluginSlotHubContext";
import { App } from "antd";
const mockData = {
buildAt:'2024-09-13T03:51:25Z',
build_user:'gitlab-runner',
git_commint:'6438d5aaff146dc560ed0d8563788e64a49640a5',
goversion:'go version go1.21.4 linux/amd64',
guide:true,
plugins:[
{
driver:'apipark.builtIn.component',
name:'guide',
router:[
{
path:'guide/*',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'team',
router:[
{
path:'team',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'service',
router:[
{
path:'service',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'datasourcing',
router:[
{
path:'datasourcing',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'cluster',
router:[
{
path:'cluster',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'aisetting',
router:[
{
path:'aisetting',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'cert',
router:[
{
path:'cert',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'serviceHub',
router:[
{
path:'serviceHub',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'commonsetting',
router:[
{
path:'commonsetting',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'consumer',
router:[
{
path:'consumer',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'member',
router:[
{
path:'member',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'role',
router:[
{
path:'role',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'analytics',
router:[
{
path:'analytics',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'template',
router:[
{
path:'template/:moduleId',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'logsettings',
router:[
{
path:'logsettings/*',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'resourcesettings',
router:[
{
path:'resourcesettings/*',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'userProfile',
router:[
{
path:'userProfile',
type:'normal'
}
]
},
{
driver:'apipark.builtIn.component',
name:'globalPolicy',
router:[
{
path:'globalPolicy',
type:'normal'
}
]
},
// {
// "driver": "apipark.remote.normal",
// "name": "remote",
// "router": [
// {
// "path": "remote",
// "type": "normal"
// }
// ]
// },
// {
// "driver": "apipark.local.preload",
// "name": "auth",
// "router": [
// {
// "expose": "AppModule",
// "path": "auth",
// "type": "root"
// },
// {
// "expose": "AuthInfoModule",
// "path": "auth-info",
// "type": "normal"
// }
// ]
// },
// {
// "driver": "apipark.builtIn.component",
// "name": "email",
// "router": [
// {
// "path": "system/email",
// "type": "normal"
// }
// ]
// },
// {
// "driver": "apipark.builtIn.module",
// "name": "open-api",
// "router": [
// {
// "path": "system/ext-app",
// "type": "normal"
// }
// ]
// },
// {
// "driver": "apipark.local.preload",
// "name": "remote",
// "router": [
// {
// "expose": "App",
// "path": "router1/*",
// "type": "normal"
// }
// ]
// },
// {
// "driver": "apipark.remote.normal",
// "name": "apispace",
// "router": [
// {
// "path": "remote/apispace",
// "type": "normal"
// }
// ]
// }
],
powered:'Powered by https://eolink.com',
product:'apipark',
version:'6438d5aa'
}
export type ExecutePluginType = PluginConfigType & {
expose:string,
bootstrap:string
}
const usePluginLoader = (apipark:ApiparkPluginDriverType,routerMap:Map<string, RouterMapConfig>) => {
const [modules, setModules] = useState(new Map());
const [executeList, setExecuteList] = useState<ExecutePluginType[]>([]);
const [baseHref, setBaseHref] = useState('');
const [pendingTasks, setPendingTasks] = useState(0);
const {fetchData} = useFetch();
const pluginProvider = useGlobalContext();
const pluginEventHub = usePluginEventHub();
const pluginSlotHub = usePluginSlotHub();
const { getMenuList,dispatch} = pluginProvider
const { modal,message } = App.useApp()
const [startLoadExecutePlugin, setStartLoadExecutePlugin] = useState<boolean>(false)
const messageService = message;
const modalService = modal;
let startInstallPlugin = false
useEffect(()=>{
if (startLoadExecutePlugin && pendingTasks === 0 && executeList.length > 0) {
loadExecutedPlugin();
}
},[pendingTasks, executeList])
const getModule = (routerPrefix:string, specific = false) => {
if (routerPrefix.startsWith('/')) {
routerPrefix = routerPrefix.substring(1);
}
if (specific) {
return modules.get(routerPrefix);
}
let matchedModule = null;
let matchedLength = 0;
modules.forEach((value, key) => {
if (routerPrefix.startsWith(key) && key.length > matchedLength) {
matchedModule = value;
matchedLength = key.length;
}
});
return matchedModule;
};
const loadModule = async (routerPrefix: string, pluginName: any, exposedModule: string , pluginPath: any) => {
if (!modules.get(routerPrefix)) {
try {
const loadedModule = await loadRemoteModule(generateRemoteModuleTemplate(pluginName, exposedModule, pluginPath));
const Module = loadedModule.default ?? loadedModule
let ModuleBootstrap;
try {
ModuleBootstrap = await loadRemoteModule(generateRemoteModuleTemplate(pluginName, 'Bootstrap', pluginPath));
} catch (error) {
console.warn('Bootstrap module not found:', error);
}
setModules(prevModules => new Map(prevModules).set(routerPrefix,Module[exposedModule] ));
if (!validateExportLifecycle(Module)) {
console.error('需要导出插件生命周期函数');
return;
}
await Module.bootstrap?.({
pluginProvider,
pluginEventHub,
pluginSlotHub
});
return Module;
} catch (error) {
console.error('导入插件失败:', error);
}
}
return getModule(routerPrefix, true);
};
const loadExecutedPlugin = async () => {
setStartLoadExecutePlugin(true)
for (const plugin of executeList) {
try {
const Module = await loadRemoteModule(generateRemoteModuleTemplate(plugin.name, plugin?.expose || 'Bootstrap', plugin.path || `${DEFAULT_LOCAL_PLUGIN_PATH}${plugin.name}/apipark.js`));
const bootstrap = Module.bootstrap;
if (!bootstrap) {
console.warn('立即执行插件未导出Bootstrap模块或bootstrap函数');
} else {
await bootstrap({
pluginEventHub,
pluginSlotHub,
pluginProvider,
platformProvider:null,
messageService,
modalService,
});
}
} catch (error) {
console.error('执行插件失败:', error);
}
}
};
const loadPlugins = () => {
return new Promise((resolve) => {
if(startInstallPlugin) {
return resolve(true)
}
startInstallPlugin = true
installPlugin().then(async (res)=>{
// reset route after loading executed plugins
await loadExecutedPlugin();
return resolve(res)
})
});
};
const installPlugin = () => {
return new Promise((resolve, reject) => {
// fetchData('system/plugins',{method:'GET'}).then((resp) => {
// if (resp.code === 0){
const resp = {data:mockData}
dispatch({type:'UPDATE_VERSION',version:resp.data.version})
dispatch({type:'UPDATE_DATE',updateDate:resp.data.buildAt})
dispatch({type:'UPDATE_POWER',powered:resp.data.powered})
const driverMethod = { apipark: apipark };
const pluginConfigList = resp.data.plugins;
const pluginLoader = { loadModule };
const pluginLifecycleGuard ={};
const builtInPluginLoader = loadBuiltInModule;
pluginSlotHub.addSlot('renewMenu', () => {
getMenuList()
});
for (const plugin of pluginConfigList) {
try {
const driverName = plugin.driver;
if (!driverName) {
console.error('no driver name');
continue;
}
const driver = driverName.split('.').reduce((driverMethod: { [x: string]: any; }, driverName: string | number) => driverMethod[driverName], driverMethod);
if(driverName.split('.')[2] === 'preload'){
setPendingTasks(prev => prev + 1);
}
;(driver as Function )?.({ setExecuteList:(callback:ExecutePluginType[]) => {
setExecuteList(callback);
setPendingTasks(prev => prev - 1);
}, pluginLoader, pluginProvider, pluginLifecycleGuard, builtInPluginLoader }, plugin);
} catch (err) {
console.warn('安装插件出错:', err);
}
}
resolve(true);
// } else {
// messageService.error(resp.msg || '获取插件配置列表失败,请重试!');
// reject(new Error(resp.msg || '获取插件配置列表失败'));
// }
// });
}
);
};
const loadBuiltInModule = (pluginName: any) => {
try {
const { module } = routerMap.get(pluginName)!;
return module;
} catch (err) {
console.warn(`安装内置插件[${pluginName}]出错:`, err);
}
};
return {
loadPlugins,
loadModule,
loadExecutedPlugin,
setBaseHref,
getModule
};
};
export default usePluginLoader;
@@ -1,31 +1,8 @@
{
"工作空间": "Kc0e5ef9f",
"首页": "K4de11e23",
"服务": "Kb58e0c3f",
"消费者": "K7acfcfad",
"团队": "Kc9e489f5",
"API 市场": "K61c89f5f",
"仪表盘": "K16d71239",
"运行视图": "K714c192d",
"系统拓扑图": "Kd57dfe97",
"系统设置": "K3fe97dcc",
"系统": "Kecbb0e45",
"常规": "Ka358e23d",
"API 网关": "K449058e9",
"AI 模型": "K99935e6f",
"用户": "K1deaa2dd",
"账号": "K80a560a1",
"角色": "Kf644225f",
"集成": "K4057391a",
"数据源": "K8fa58214",
"证书": "K481e8a05",
"日志": "Kca53edd0",
"资源": "Kb283e720",
"Open API": "K631d646f",
"账号设置": "K6535ff9c",
"退出登录": "Kf15499b4",
"文档": "Kabbd6e6",
"APIPark - 企业API数据开放平台": "K1196b104",
"APIPark": "K630c9e6d",
"HTTP 状态码": "K1f42de3",
"系统状态码": "K4770dff4",
"描述": "Kf89e58f1",
@@ -43,6 +20,8 @@
"ID": "K11d3633a",
"名称": "Kbff43de3",
"Driver": "K16ca79ef",
"资源": "Kb283e720",
"日志": "Kca53edd0",
"已发布": "K7a369eef",
"下线": "Kcfa1a4d2",
"上线": "K771dc3b7",
@@ -50,6 +29,8 @@
"删除": "Kecbd7449",
"确认": "K1cbe2507",
"搜索(0)名称": "K48325b6",
"发布名称": "Ka3e9f580",
"策略列表": "Kb2480682",
"上下文": "Kc6340091",
"查询内容": "K74ecb1fa",
"会话历史": "K79f2e2f9",
@@ -234,6 +215,26 @@
"地址": "K78b1ca25",
"新增": "K1644b775",
"申请方消费者": "Kec91f0db",
"策略名称": "K931615d7",
"优先级": "K31faa2a1",
"筛选条件": "Kbdec9fa",
"处理数": "Kbcbb7391",
"数据格式": "K118d8d74",
"关键字": "Kfe7c7d2d",
"正则表达式": "K2f57a694",
"手机号": "K8953e0a6",
"身份证号": "K6f86a038",
"银行卡号": "K7954e7c8",
"金额": "K320fdb17",
"日期": "K7867acda",
"局部显示": "K7d327ae8",
"局部遮蔽": "Kfbf38e3c",
"截取": "Kd8c1fbb0",
"替换": "K89829921",
"乱序": "K480a7165",
"随机字符串": "Kea0d69df",
"自定义字符串": "Ke7c84d1d",
"请输入IP地址或CIDR范围,每条以换行分割": "K49731763",
"暂无操作权限,请联系管理员分配。": "K23fda291",
"微信小程序": "K4618cb0a",
"获取文件,需填路径": "Ka854f511",
@@ -321,11 +322,14 @@
"至": "K7e1ab4b0",
"详情": "Kf1b166e7",
"暂不支持带有双斜杠//的url": "K28555332",
"输入的IP或CIDR不符合格式": "K83237c89",
"请正确输入路径,如/usr/*或*/usr/*": "K5ae2c87a",
"必填项": "K71661ee8",
"不是有效邮箱地址": "Kcbee3f8",
"最近一次更新者": "K617f34f1",
"最近一次更新时间": "K6ebca204",
"保存": "Kabfe9512",
"服务": "Kb58e0c3f",
"API 路由": "K51d1eb5d",
"API 文档": "Ka2b6d281",
"使用说明": "Kdefa9caa",
@@ -401,11 +405,13 @@
"现在你可以通过 Token 来调用这些 API。": "Kd6d7ca1f",
"快速接入 REST API": "K86cf95f",
"创建 REST 服务和 API": "K7a3a8417",
"仪表盘": "K16d71239",
"统计 API 调用情况": "K4a84214e",
"仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。": "K297d8563",
"核心功能": "K2cdbb773",
"账号与角色": "K3378c50d",
"邀请你的团队成员加入 APIPark,共同管理和调用 API。": "Kda5bb930",
"团队": "Kc9e489f5",
"团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。": "Keee27105",
"服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。": "Kd5be0cd7",
"权限管理": "K62e89ee7",
@@ -413,6 +419,7 @@
"如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。": "Kf2410413",
"审核订阅申请": "K6c2e44b8",
"提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。": "Kaa717866",
"集成": "K4057391a",
"APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。": "K3453272",
"Hello!欢迎使用 APIPark": "Kd518ba3e",
"你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。": "K7e04ea16",
@@ -424,6 +431,7 @@
"了解更多功能": "K48f7e21f",
"隐藏该教程": "K698296e2",
"请输入账号": "Kf076f63c",
"账号": "K80a560a1",
"请输入密码": "K25c895d5",
"密码": "K551b0348",
"登录": "Kd2c1a316",
@@ -466,6 +474,7 @@
"密钥": "K8ef69ee2",
"上传密钥": "Kba3507d6",
"密钥文件的后缀名一般为 .key 的文件内容": "K93ac0f23",
"证书": "K481e8a05",
"上传证书": "K7cdd1331",
"证书文件的后缀名一般为 .crt 或 .pem 的文件内容": "K6d91905d",
"添加证书": "Kd0f6ded7",
@@ -484,11 +493,39 @@
"同步地址": "K2a49373f",
"集群地址": "K5878440c",
"下一步": "K5e9022f8",
"数据源": "K8fa58214",
"设置监控报表的数据来源,设置完成之后即可获得详细的API调用统计图表。": "Kdbafd6f9",
"统计图表": "K1358acf",
"地址(IP:端口)": "K62dabdf6",
"组织(Organization": "K2db12335",
"添加策略": "K34d0d409",
"输入名称、筛选条件查找": "Kbb4298ac",
"编辑策略": "Kc82b8374",
"策略类型": "K4b34a5e5",
"匹配条件": "K57f0fee8",
"数据脱敏规则": "K10650c58",
"配置脱敏规则": "K1b34a9ab",
"匹配值": "K26d22405",
"脱敏类型": "K1546e1fe",
"起始位置": "K9b9b0629",
"长度": "K52c84fe1",
"替换类型": "Kde84409c",
"替换值": "K338653b4",
"JSON Path": "Kbaeed3b7",
"脱敏规则": "K4cd91d61",
"自定义字符串; 值:": "K8dcad979",
"起始位置:(0)位;长度:(1)位": "K82e3f7b7",
"编辑": "Kad207008",
"已选择(0)项(1)数据": "K49dfc123",
"所有(0)": "K8457ea34",
"属性名称": "K7ca9a795",
"属性值": "Kc4391744",
"配置(0)": "K678e13fc",
"数据脱敏": "Kabac9caf",
"全局策略": "Ke8cbb878",
"支持对系统全局进行统一的策略配置,从而简化管理并确保一致性。全局策略的优先级比服务策略略低。": "Kc975cd5a",
"资源配置": "K8e7a0f80",
"角色": "Kf644225f",
"设置角色的权限范围。": "K95c3fd8b",
"系统级别角色": "K138facd3",
"添加角色": "K6eac768d",
@@ -523,6 +560,7 @@
"删除服务": "Kde6bae17",
"删除操作不可恢复,请谨慎操作!": "K885ea699",
"上游": "Kda8d5ea1",
"服务策略": "K52f72551",
"服务提供了高性能 API 网关,并且可以无缝接入多种大型 AI 模型,并将这些 AI 能力打包成 API 进行调用,从而大幅简化了 AI 模型的使用门槛。同时,我们的平台提供了完善的 API 管理功能,支持 API 的创建、监控、访问控制等,保障开发者可以高效、安全地开发和管理 API 服务。": "K12f58863",
"添加服务": "K2d6658ed",
"输入名称、ID、所属团队、负责人查找服务": "K7b8f623f",
@@ -568,6 +606,7 @@
"退出全屏": "Kaf70c3b",
"(0)调用详情": "Kd22841a4",
"消费者调用统计": "K61cca533",
"消费者": "K7acfcfad",
"请选择消费者": "Kdfff59d4",
"调用趋势": "K8c7f2d2e",
"(0)-(1)调用趋势": "K657c3452",
@@ -600,6 +639,7 @@
"暂无调用量统计数据": "Kcd125e4d",
"暂无报文量统计数据": "Kaa114e8b",
"报文量统计": "K3ad84406",
"运行视图": "K714c192d",
"集群配置并开启监控": "Kfa088d49",
"监控功能用于辅助管理集群内信息,请配置集群、设置监控信息后查看当前集群监控情况;": "K3da3b9a0",
"集群配置": "Kaddacfb",
@@ -616,6 +656,19 @@
"暂无API数据": "K6b75bdbc",
"搜索或选择消费者": "Kb684c806",
"申请理由": "K4b15d6f5",
"支持把当前服务对接主流的 AI Agent平台,实现在 Agent 平台上快速、安全和合规地使用企业开放的 API 能力。": "K2ec0fa56",
"可按以下步骤进行对接:": "K35f23b64",
"步骤一:Agent 平台上创建自定义插件": "Kf5cd608b",
"不同 Agent 平台的操作细节可查看": "K4c81c7b6",
"《 Agent 对接手册》": "K275f7ffa",
"步骤二:导入 API 文档数据": "K49b81d06",
"可通过以下 URL 或 下载 Json 文件,导入 API 文档数据到 Agent 平台中。": "K4a3b62be",
"复制 URL": "K42697e11",
"下载 Json 文件": "K27a809c5",
"步骤三:配置 API 密钥": "K1e61fdee",
"在": "K55912595",
"菜单中,选择已通过本 API 服务申请的消费者,": "K33b1bc3",
"把 \"访问权限\" 菜单下的密钥填入到 Agent 平台对应的插件密钥配置中。": "K62adc41e",
"消费者管理": "Ke0fbd1c8",
"鉴权类型": "Kb71b5a13",
"Iss": "K4d1465ee",
@@ -676,6 +729,7 @@
"配置 Open Api": "K7829bb78",
"Open Api": "Kcdf76005",
"调用服务": "Ke2601944",
"系统拓扑图": "Kd57dfe97",
"放大": "K8504bca8",
"缩小": "K693c1b41"
}
@@ -679,5 +679,70 @@
"Kc8ee3e62": "Non-Existence",
"K1e97dbd8": "Existence",
"Kec91f0db": "Applicant Consumer",
"Kf5fd27ed": "Enter Name to Search User"
"Kf5fd27ed": "Enter Name to Search User",
"K2ec0fa56": "Support integration with mainstream AI Agent platforms to enable the use of enterprise APIs in a fast, secure, and compliant manner on the Agent platform.",
"K35f23b64": "Follow these steps for integration:",
"Kf5cd608b": "Step 1: Create a custom plugin on the Agent platform",
"K4c81c7b6": "For details on the operations of different Agent platforms, refer to",
"K275f7ffa": "the 'Agent Integration Manual'",
"K49b81d06": "Step 2: Import API documentation data",
"K4a3b62be": "You can import API documentation data to the Agent platform via the following URL or by downloading the JSON file.",
"K42697e11": "Copy URL",
"K27a809c5": "Download JSON file",
"K1e61fdee": "Step 3: Configure API keys",
"K55912595": "In the",
"K33b1bc3": "menu, select the consumer that has applied for the API service,",
"K62adc41e": "and fill in the key from the 'Access Permission' menu into the corresponding plugin key configuration on the Agent platform.",
"Ke8cbb878": "Global Policy",
"K34d0d409": "Add Policy",
"Kbb4298ac": "Enter name, filter criteria to search",
"Kabac9caf": "Data Masking",
"Kc975cd5a": "Supports unified policy configuration for the system globally, simplifying management and ensuring consistency. The priority of global policies is slightly lower than that of service policies.",
"K52f72551": "Service Policy",
"K931615d7": "Policy Name",
"K31faa2a1": "Priority",
"Kbdec9fa": "Filter Criteria",
"Kbcbb7391": "Processed Count",
"Kad207008": "Edit",
"K630c9e6d": "APIPark",
"Ka3e9f580": "Release Name",
"Kb2480682": "Policy List",
"K118d8d74": "Data Format",
"Kfe7c7d2d": "Keyword",
"K2f57a694": "Regular Expression",
"K8953e0a6": "Mobile Number",
"K6f86a038": "ID Card Number",
"K7954e7c8": "Bank Card Number",
"K320fdb17": "Amount",
"K7867acda": "Date",
"K7d327ae8": "Partial Display",
"Kfbf38e3c": "Partial Masking",
"Kd8c1fbb0": "Truncate",
"K89829921": "Replace",
"K480a7165": "Shuffle",
"Kea0d69df": "Random String",
"Ke7c84d1d": "Custom String",
"K49731763": "Please enter IP address or CIDR range, each separated by a newline.",
"K83237c89": "The entered IP or CIDR does not meet the format.",
"K5ae2c87a": "Please enter the correct path, such as /usr/* or */usr/*.",
"Kc82b8374": "Edit Policy",
"K4b34a5e5": "Policy Type",
"K57f0fee8": "Match Conditions",
"K10650c58": "Data Masking Rules",
"K1b34a9ab": "Configure Masking Rules",
"K26d22405": "Match Value",
"K1546e1fe": "Masking Type",
"K9b9b0629": "Starting Position",
"K52c84fe1": "Length",
"Kde84409c": "Replace Type",
"K338653b4": "Replace Value",
"Kbaeed3b7": "JSON Path",
"K4cd91d61": "Masking Rules",
"K8dcad979": "Custom String; Value:",
"K82e3f7b7": "Starting Position: (0); Length: (1)",
"K49dfc123": "Selected (0) items (1) data",
"K8457ea34": "All (0)",
"K7ca9a795": "Attribute Name",
"Kc4391744": "Attribute Value",
"K678e13fc": "Configure (0)"
}
@@ -701,5 +701,70 @@
"Kc8ee3e62": "存在しない",
"K1e97dbd8": "存在する",
"Kec91f0db": "申請側コンシューマー",
"Kf5fd27ed": "名前を入力してユーザーを検索"
"Kf5fd27ed": "名前を入力してユーザーを検索",
"K2ec0fa56": "主要なAIエージェントプラットフォームと連携し、エージェントプラットフォーム上で企業のAPIを迅速、安全、かつコンプライアンスに準拠して使用できるようサポートします。",
"K35f23b64": "以下の手順で統合を行います:",
"Kf5cd608b": "ステップ1:エージェントプラットフォームでカスタムプラグインを作成",
"K4c81c7b6": "異なるエージェントプラットフォームの操作詳細については、",
"K275f7ffa": "「エージェント統合マニュアル」を参照してください。",
"K49b81d06": "ステップ2:APIドキュメントデータのインポート",
"K4a3b62be": "以下のURLを使用するか、JSONファイルをダウンロードして、APIドキュメントデータをエージェントプラットフォームにインポートできます。",
"K42697e11": "URLをコピー",
"K27a809c5": "JSONファイルをダウンロード",
"K1e61fdee": "ステップ3APIキーの設定",
"K55912595": "「",
"K33b1bc3": "メニュー」から、APIサービスを申し込んだ消費者を選択し、",
"K62adc41e": "「アクセス権限」メニューのキーをエージェントプラットフォームのプラグインキー設定に入力します。",
"Ke8cbb878": "グローバルポリシー",
"K34d0d409": "ポリシーを追加",
"Kbb4298ac": "名前、フィルタ条件を入力して検索",
"Kabac9caf": "データマスキング",
"Kc975cd5a": "システム全体で統一されたポリシー設定をサポートし、管理の簡素化と一貫性の確保を実現します。グローバルポリシーの優先度はサービスポリシーより少し低いです。",
"K52f72551": "サービスポリシー",
"K931615d7": "ポリシー名",
"K31faa2a1": "優先度",
"Kbdec9fa": "フィルタ条件",
"Kbcbb7391": "処理数",
"Kad207008": "編集",
"K630c9e6d": "APIPark",
"Ka3e9f580": "リリース名",
"Kb2480682": "ポリシーリスト",
"K118d8d74": "データフォーマット",
"Kfe7c7d2d": "キーワード",
"K2f57a694": "正規表現",
"K8953e0a6": "携帯番号",
"K6f86a038": "IDカード番号",
"K7954e7c8": "銀行カード番号",
"K320fdb17": "金額",
"K7867acda": "日付",
"K7d327ae8": "部分表示",
"Kfbf38e3c": "部分マスキング",
"Kd8c1fbb0": "切り取り",
"K89829921": "置換",
"K480a7165": "シャッフル",
"Kea0d69df": "ランダム文字列",
"Ke7c84d1d": "カスタム文字列",
"K49731763": "IPアドレスまたはCIDR範囲を入力してください。各行で改行してください。",
"K83237c89": "入力されたIPまたはCIDRがフォーマットに一致しません。",
"K5ae2c87a": "正しいパスを入力してください。例: /usr/* または */usr/*。",
"Kc82b8374": "ポリシーを編集",
"K4b34a5e5": "ポリシータイプ",
"K57f0fee8": "一致条件",
"K10650c58": "データマスキングルール",
"K1b34a9ab": "マスキングルールを設定",
"K26d22405": "一致値",
"K1546e1fe": "マスキングタイプ",
"K9b9b0629": "開始位置",
"K52c84fe1": "長さ",
"Kde84409c": "置換タイプ",
"K338653b4": "置換値",
"Kbaeed3b7": "JSON パス",
"K4cd91d61": "マスキングルール",
"K8dcad979": "カスタム文字列; 値:",
"K82e3f7b7": "開始位置: (0); 長さ: (1)",
"K49dfc123": "選択された (0) アイテム (1) データ",
"K8457ea34": "すべて (0)",
"K7ca9a795": "属性名",
"Kc4391744": "属性値",
"K678e13fc": "設定 (0)"
}
@@ -1,3 +1 @@
{
"Kf5fd27ed": "输入名称查找用户"
}
{}
@@ -1,3 +1 @@
{
"Kf5fd27ed": "输入名称查找用户"
}
{}
@@ -1,4 +1,7 @@
{
"K630c9e6d": "APIPark",
"Ka3e9f580": "发布名称",
"Kb2480682": "策略列表",
"K76036e25": "HTTP 请求头",
"K44607e3f": "全等匹配",
"Kc287500a": "前缀匹配",
@@ -21,7 +24,43 @@
"K78b1ca25": "地址",
"K1644b775": "新增",
"Kec91f0db": "申请方消费者",
"Kf5fd27ed": "输入名称查找用户",
"Kc3b7bfa8": "暂无消费者描述",
"K3a6f905d": "输入名称、ID 查找消费者"
"K118d8d74": "数据格式",
"Kfe7c7d2d": "关键字",
"K2f57a694": "正则表达式",
"K8953e0a6": "手机号",
"K6f86a038": "身份证号",
"K7954e7c8": "银行卡号",
"K320fdb17": "金额",
"K7867acda": "日期",
"K7d327ae8": "局部显示",
"Kfbf38e3c": "局部遮蔽",
"Kd8c1fbb0": "截取",
"K89829921": "替换",
"K480a7165": "乱序",
"Kea0d69df": "随机字符串",
"Ke7c84d1d": "自定义字符串",
"K49731763": "请输入IP地址或CIDR范围,每条以换行分割",
"K83237c89": "输入的IP或CIDR不符合格式",
"K5ae2c87a": "请正确输入路径,如/usr/*或*/usr/*",
"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": "输入名称查找用户"
}
@@ -1,3 +1 @@
{
"Kf5fd27ed": "输入名称查找用户"
}
{}
@@ -1,4 +1,15 @@
{
"Kc0e5ef9f": "Workspace",
"K4de11e23": "Home",
"K61c89f5f": "API Portal",
"K3fe97dcc": "System Settings",
"Kecbb0e45": "System",
"Ka358e23d": "General",
"K449058e9": "API Gateway",
"K99935e6f": "AI Model",
"K1deaa2dd": "User",
"K631d646f": "Open API",
"K1196b104": "APIPark",
"Kb9052305": "Search Username, Email",
"K40a89bd8": "Enter Name, ID to Search Member"
}
@@ -1,5 +1,16 @@
{
"Kc0e5ef9f": "Workspace",
"K4de11e23": "Home",
"Kfe93ef35": "Application",
"K61c89f5f": "API Portal",
"K3fe97dcc": "設定",
"Kecbb0e45": "システム",
"Ka358e23d": "一般",
"K449058e9": "API ゲートウェイ",
"K99935e6f": "AI モデル",
"K1deaa2dd": "ユーザー",
"K631d646f": "Open API",
"K1196b104": "APIPark",
"Kffd7e274": "無審査:すべてのアプリケーションがこのサービスにサブスクライブできます",
"K8a8b13e4": "手動審査:承認されたアプリケーションのみがこのサービスにサブスクライブできます",
"K9bdd8403": "API を安全に呼び出すためには、アプリケーションとトークンを作成する必要があります。",
@@ -1,5 +1,16 @@
{
"Kc0e5ef9f": "工作空间",
"K4de11e23": "首页",
"Kfe93ef35": "消费者",
"K61c89f5f": "API 门户",
"K3fe97dcc": "系统设置",
"Kecbb0e45": "系统",
"Ka358e23d": "常规",
"K449058e9": "API 网关",
"K99935e6f": "AI 模型",
"K1deaa2dd": "用户",
"K631d646f": "Open API",
"K1196b104": "APIPark",
"Kffd7e274": "无审核:允许所有消费者订阅该服务",
"K8a8b13e4": "人工审核:仅允许审核通过的消费者订阅该服务",
"K9bdd8403": "为了安全地调用 API,你需要创建一个消费者以及Token。",
@@ -1,5 +1,16 @@
{
"Kc0e5ef9f": "工作區",
"K4de11e23": "主頁",
"Kfe93ef35": "應用程式",
"K61c89f5f": "API 門戶",
"K3fe97dcc": "系統設置",
"Kecbb0e45": "系統",
"Ka358e23d": "常規",
"K449058e9": "API 網關",
"K99935e6f": "AI 模型",
"K1deaa2dd": "用戶",
"K631d646f": "Open API",
"K1196b104": "APIPark",
"Kffd7e274": "無審核:允許所有應用程式訂閱該服務",
"K8a8b13e4": "人工審核:僅允許審核通過的應用程式訂閱該服務",
"K9bdd8403": "為了安全地調用 API,你需要創建一個應用以及Token。",
File diff suppressed because it is too large Load Diff
@@ -701,5 +701,70 @@
"Kc8ee3e62": "不存在匹配",
"K1e97dbd8": "存在匹配",
"Kec91f0db": "申請方消費者",
"Kf5fd27ed": "輸入名稱查找使用者"
"Kf5fd27ed": "輸入名稱查找使用者",
"K2ec0fa56": "支援將當前服務對接主流的 AI Agent 平台,實現在 Agent 平台上快速、安全和合規地使用企業開放的 API 能力。",
"K35f23b64": "可按以下步驟進行對接:",
"Kf5cd608b": "步驟一:Agent 平台上創建自定義插件",
"K4c81c7b6": "不同 Agent 平台的操作細節可查看",
"K275f7ffa": "《 Agent 對接手冊》",
"K49b81d06": "步驟二:導入 API 文檔數據",
"K4a3b62be": "可通過以下 URL 或下載 json 文件,導入 API 文檔數據到 Agent 平台中。",
"K42697e11": "複製 URL",
"K27a809c5": "下載 Json 文件",
"K1e61fdee": "步驟三:配置 API 密鑰",
"K55912595": "在",
"K33b1bc3": "菜單中,選擇已通過本 API 服務申請的消費者,",
"K62adc41e": "把「訪問權限」菜單下的密鑰填入到 Agent 平台對應的插件密鑰配置中。",
"Ke8cbb878": "全域策略",
"K34d0d409": "新增策略",
"Kbb4298ac": "輸入名稱、篩選條件查找",
"Kabac9caf": "資料脫敏",
"Kc975cd5a": "支援對系統全域進行統一的策略配置,從而簡化管理並確保一致性。全域策略的優先級比服務策略略低。",
"K52f72551": "服務策略",
"K931615d7": "策略名稱",
"K31faa2a1": "優先級",
"Kbdec9fa": "篩選條件",
"Kbcbb7391": "處理數",
"Kad207008": "編輯",
"K630c9e6d": "APIPark",
"Ka3e9f580": "發布名稱",
"Kb2480682": "策略列表",
"K118d8d74": "數據格式",
"Kfe7c7d2d": "關鍵字",
"K2f57a694": "正則表達式",
"K8953e0a6": "手機號碼",
"K6f86a038": "身份證號碼",
"K7954e7c8": "銀行卡號碼",
"K320fdb17": "金額",
"K7867acda": "日期",
"K7d327ae8": "局部顯示",
"Kfbf38e3c": "局部遮蔽",
"Kd8c1fbb0": "截取",
"K89829921": "替換",
"K480a7165": "亂序",
"Kea0d69df": "隨機字串",
"Ke7c84d1d": "自訂字串",
"K49731763": "請輸入IP地址或CIDR範圍,每行以換行符分隔",
"K83237c89": "輸入的IP或CIDR格式不正確",
"K5ae2c87a": "請正確輸入路徑,例如/usr/*或*/usr/*",
"Kc82b8374": "編輯策略",
"K4b34a5e5": "策略類型",
"K57f0fee8": "匹配條件",
"K10650c58": "數據脫敏規則",
"K1b34a9ab": "配置脫敏規則",
"K26d22405": "匹配值",
"K1546e1fe": "脫敏類型",
"K9b9b0629": "起始位置",
"K52c84fe1": "長度",
"Kde84409c": "替換類型",
"K338653b4": "替換值",
"Kbaeed3b7": "JSON路徑",
"K4cd91d61": "脫敏規則",
"K8dcad979": "自訂字串; 值:",
"K82e3f7b7": "起始位置:(0)位;長度:(1)位",
"K49dfc123": "已選擇(0)項(1)數據",
"K8457ea34": "所有(0)",
"K7ca9a795": "屬性名稱",
"Kc4391744": "屬性值",
"K678e13fc": "配置(0)"
}
@@ -1,25 +1,34 @@
import { Icon } from "@iconify/react/dist/iconify.js";
import { MenuProps } from "antd";
export type MenuItem = Required<MenuProps>['items'][number];
export function getNavItem(
label: React.ReactNode,
key: React.Key,
path:string,
icon?: React.ReactNode,
children?: MenuItem[],
type?: 'group',
access?:string[] | string
): MenuItem {
export function getNavItem({
label,
key,
path,
icon,
children,
type,
access
}: {
label: React.ReactNode;
key: React.Key;
path?: string;
icon?: React.ReactNode;
children?: MenuItem[];
type?: 'group';
access?: string[] | string;
}): MenuItem {
return {
key,
icon :icon ,
path,
icon,
routes:children,
name:label,
type,
access
access,
path
} as MenuItem;
}
@@ -53,4 +62,19 @@ export function getItem(
label,
access
}
}
}
export function transformMenuData(data: any[]): MenuItem[] {
return data.map(item => {
const { name, key, path, icon, children, access } = item;
return getNavItem({
label:name,
key,
path,
icon: icon ? <Icon icon={icon} width="18" height="18" /> : undefined,
children:children ? transformMenuData(children) : undefined,
access}
);
});
}
@@ -0,0 +1,154 @@
import { CoreObj, PluginConfigType, PluginRouterConfig, RouteConfig, RouterMapConfig } from '@common/const/type';
import { isFunction } from 'lodash-es'
// @ts-expect-error module cannot find
import { __federation_method_setRemote,__federation_method_getRemote,__federation_method_unwrapDefault } from 'virtual:__federation__';
import { ApiparkPluginDriverType } from '@common/const/type';
import React from 'react';
interface RemoteModuleConfig{
type:string
remoteEntry:string
exposedModule:string
remoteName:string
}
export async function loadRemoteModule(remoteModuleConfig:RemoteModuleConfig){
__federation_method_setRemote(remoteModuleConfig.remoteName, {
url: () => Promise.resolve(remoteModuleConfig.remoteEntry),
format: 'esm',
from: 'vite',
});
return await __federation_method_getRemote(
remoteModuleConfig.remoteName,
`./${remoteModuleConfig.exposedModule}`
)
}
export function generateRemoteModuleTemplate(
pluginName: string,
exposedModule: string,
pluginPath: string
):RemoteModuleConfig {
return {
type: 'module',
remoteEntry: pluginPath,
exposedModule,
remoteName: pluginName
}
}
/** check the lifecycle method of plugin */
export function validateExportLifecycle(exports: unknown) {
const { bootstrap, mount, unmount } = exports ?? {}
return isFunction(bootstrap) && isFunction(mount) && isFunction(unmount)
}
export const DEFAULT_LOCAL_PLUGIN_PATH = '/plugin-frontend/'
export const ApiparkPluginDriver = (routerMap: Map<string, RouterMapConfig>): ApiparkPluginDriverType => { return {
builtIn: {
component: (coreObj?:CoreObj, pluginConfig?:PluginConfigType) => {
if(!coreObj || !pluginConfig) return coreObj
for (const pluginRouter of pluginConfig.router) {
routerMap.get(pluginConfig.name) && coreObj.pluginProvider.setRouterConfig(pluginRouter.type === 'root' , {
...routerMap.get(pluginConfig.name)!,
key:pluginConfig.name,
path:pluginRouter.path})
}
return coreObj
}
},
remote: {
normal: (coreObj?:CoreObj, pluginConfig?:PluginConfigType) => {
if(!coreObj || !pluginConfig) return coreObj
const routerToChanged:RouteConfig[] = coreObj.routerConfig.find((router: RouteConfig) => router.path === '/' && router?.pathMatch !== 'full')!.children as RouteConfig[]
const remoteRouter:RouteConfig[] = routerToChanged.find((item:RouteConfig) => item?.data?.['type'] === 'remotePlugin') as RouteConfig[]
if (!remoteRouter) {
routerMap.get('remote') && coreObj.pluginProvider.setRouterConfig(false,{
...routerMap.get('remote')!, key:'remote', path:'remote',type:'remotePlugin',children:[{
path:':moduleName',
component: routerMap.get('remote')!.component}
]
})
}
return coreObj
}
},
intelligent: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
normal: (coreObj?:CoreObj, pluginConfig?:PluginConfigType) => {
if(!coreObj || !pluginConfig) return coreObj
if(['logsettings','resourcesettings'].indexOf(pluginConfig.name) !== -1){
const routerToChanged:RouteConfig[] = coreObj.routerConfig.find((router: RouteConfig) => router.path === '/' && router?.pathMatch !== 'full')!.children as RouteConfig[]
const remoteRouter:RouteConfig[] = routerToChanged.find((item:RouteConfig) => item?.data?.['key'] === pluginConfig.name) as RouteConfig[]
if(!remoteRouter){
routerMap.get(pluginConfig.name) && routerToChanged.unshift({...routerMap.get(pluginConfig.name)!, key:pluginConfig.name, path:pluginConfig.path})
}
return
}
const remoteRouter = coreObj.routerConfig.find((item:RouteConfig) => item?.data?.['type'] === 'intelligentPlugin')
if (!remoteRouter) {
// coreObj.pluginProvider.setRouterConfig(false, {
// path: 'template',
// loadChildren: coreObj.builtInPluginLoader('intelligent'),
// data: {
// type: 'intelligentPlugin'
// }
// }, coreObj.routerConfig)
}
return coreObj
}
},
local: {
router: (coreObj?:CoreObj, pluginConfig?:PluginConfigType) => {
if(!coreObj || !pluginConfig) return coreObj
for (const pluginRouter of pluginConfig.router) {
if (pluginRouter.type === 'sub') {
continue
}
updateRouterConfigWithPlugin(coreObj, pluginRouter, pluginConfig)
}
return coreObj
},
preload: (coreObj?:CoreObj, pluginConfig?:PluginConfigType) => {
if(!coreObj || !pluginConfig) return coreObj
coreObj.setExecuteList(prev=>[...prev,{ ...pluginConfig, expose: 'Bootstrap', bootstrap: 'BootstrapModule.bootstrap' }])
for (const pluginRouter of pluginConfig.router) {
updateRouterConfigWithPlugin(coreObj, pluginRouter, pluginConfig)
}
return coreObj
}
// extender: (coreObj?:CoreObj, pluginConfig?:PluginConfigType) => {}
}
}
}
async function updateRouterConfigWithPlugin (coreObj: CoreObj, pluginRouter: PluginRouterConfig, pluginConfig: PluginConfigType) {
if (!pluginRouter.expose) {
throw new Error('pluginRouter.expose is required')
} else {
for (const pluginRouter of pluginConfig.router) {
const loadedModule = await coreObj.pluginLoader.loadModule(
pluginRouter.path,
pluginConfig.name,
pluginRouter.expose!,
pluginConfig.path || `${DEFAULT_LOCAL_PLUGIN_PATH}${pluginConfig.name}/apipark.js`
)
const loadedModulePage = loadedModule[pluginRouter.expose!]
const LazyComponent = React.lazy(() => Promise.resolve({ default: loadedModulePage?.default || loadedModulePage }));
const newRouter: RouteConfig = {
path: pluginRouter.path,
key: pluginConfig.name,
lazy: () => Promise.resolve({ default: (props: any) => <LazyComponent {...props} /> }),
pathPrefix: pluginRouter.path.endsWith('/*') ? pluginRouter.path.slice(0, -2) : pluginRouter.path,
lifecycle:{
canActivate: loadedModule?.beforeMount,
canLoad: loadedModule?.mount,
canDeactivate: loadedModule?.beforeUnmount,
deactivated: loadedModule?.unmount
}
};
coreObj.pluginProvider.setRouterConfig(pluginRouter.type === 'root' , newRouter)
}
}
}
@@ -4,5 +4,38 @@ export const validateUrlSlash = (_, value) => {
if (value && value.includes('//')) {
return Promise.reject(new Error($t('暂不支持带有双斜杠//的url')));
}
return Promise.resolve();
};
export const validateIPorCIDR = (rule, value) => {
if (!value) {
return Promise.resolve();
}
const lines = value.split('\n');
const ipCidrRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
for (const line of lines) {
if (line && !ipCidrRegex.test(line.trim())) {
return Promise.reject($t('输入的IP或CIDR不符合格式'));
}
}
return Promise.resolve();
};
export const validateApiPath = (rule, value) => {
if (!value) {
return Promise.resolve();
}
const invalidCharsRegex = /[^a-zA-Z0-9\-\/\*]/;
const validPathRegex = /^(\/?\*?\/?[a-zA-Z0-9\-\/\*]*)$/;
if (value && (invalidCharsRegex.test(value.trim()) || !validPathRegex.test(value.trim()))) {
return Promise.reject($t('请正确输入路径,如/usr/*或*/usr/*'));
}
return Promise.resolve();
};
+7
View File
@@ -299,4 +299,11 @@ a{
a[disabled]:hover {
color: #BBB;
cursor: not-allowed;
}
.ant-input-group-addon{
height:32px !important;
.ant-btn.ant-btn-default{
height:32px !important;
}
}
+28 -40
View File
@@ -1,30 +1,16 @@
import './App.css'
import { ConfigProvider, ConfigProviderProps, Radio, RadioChangeEvent } from 'antd';
import { ConfigProvider, App as AppAntd } from 'antd';
import RenderRoutes from '@core/components/aoplatform/RenderRoutes';
import {BreadcrumbProvider} from "@common/contexts/BreadcrumbContext.tsx";
import { StyleProvider } from '@ant-design/cssinjs';
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
import zhTW from 'antd/locale/zh_TW';
import jaJP from 'antd/locale/ja_JP';
import useInitializeMonaco from "@common/hooks/useInitializeMonaco";
import { useEffect, useMemo, useState } from 'react';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/zh-tw';
import 'dayjs/locale/ja';
import dayjs from 'dayjs';
import { useGlobalContext } from '@common/contexts/GlobalStateContext';
import { useMemo } from 'react';
import { GlobalProvider } from '@common/contexts/GlobalStateContext';
import { $t } from '@common/locales';
type Locale = ConfigProviderProps['locale'];
const languageMap: Record<string, Locale> = {
'zh-CN':zhCN,
'en-US':enUS,
'zh-TW':zhTW,
'ja-JP':jaJP
}
import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext';
import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext';
import { useLocaleContext } from '@common/contexts/LocaleContext';
import { StyleProvider } from '@ant-design/cssinjs';
const antdComponentThemeToken = {
@@ -150,33 +136,35 @@ const antdComponentThemeToken = {
function App() {
const [locale, setLocal] = useState<Locale>();
const { locale } = useLocaleContext();
useInitializeMonaco()
const { state} = useGlobalContext()
useEffect(() => {
dayjs.locale(state.language);
setLocal(languageMap[state.language]);
},[state.language])
const validateMessages = useMemo(()=>({
required: $t('必填项'),
email:$t('不是有效邮箱地址')}
),[state.language])
),[locale])
return (
<StyleProvider hashPriority={"high"}>
<ConfigProvider
locale={locale}
wave={{disabled:true}}
theme={antdComponentThemeToken}
form={{validateMessages }}>
<BreadcrumbProvider>
<RenderRoutes />
</BreadcrumbProvider>
</ConfigProvider>
</StyleProvider>
<StyleProvider hashPriority={"high"}>
<ConfigProvider
locale={locale}
wave={{disabled:true}}
theme={antdComponentThemeToken}
form={{validateMessages }}>
<AppAntd className="h-full" message={{ maxCount: 1 }}>
<PluginEventHubProvider>
<PluginSlotHubProvider>
<GlobalProvider>
<BreadcrumbProvider>
<RenderRoutes />
</BreadcrumbProvider>
</GlobalProvider>
</PluginSlotHubProvider>
</PluginEventHubProvider>
</AppAntd>
</ConfigProvider>
</StyleProvider>
);
}
@@ -1,572 +1,92 @@
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import Login from "@core/pages/Login.tsx"
import { Navigate, RouterProvider, createBrowserRouter, RouteObject } from 'react-router-dom';
import BasicLayout from '@common/components/aoplatform/BasicLayout';
import {createElement, ReactElement,ReactNode,Suspense} from 'react';
import { v4 as uuidv4 } from 'uuid'
import {App, Skeleton} from "antd";
import {createElement,Suspense, useEffect, useState} from 'react';
import {Skeleton, Spin} from "antd";
import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx";
import {FC,lazy} from 'react';
import { TeamProvider } from '@core/contexts/TeamContext.tsx';
import { TenantManagementProvider } from '@market/contexts/TenantManagementContext.tsx';
import Guide from '@core/pages/guide/Guide';
import { AiServiceProvider } from '@core/contexts/AiServiceContext';
import AiServiceOutlet from '@core/pages/aiService/AiServiceOutlet';
import SystemOutlet from '@core/pages/system/SystemOutlet';
import { SystemProvider } from '@core/contexts/SystemContext';
type RouteConfig = {
path:string
component?:ReactElement
children?:(RouteConfig|false)[]
key:string
provider?:FC<{ children: ReactNode; }>
lazy?:unknown
}
const APP_MODE = import.meta.env.VITE_APP_MODE;
export type RouterParams = {
teamId:string
apiId:string
serviceId:string
clusterId:string;
memberGroupId:string
userGroupId:string
pluginName:string
moduleId:string
accessType:'project'|'team'|'service'
categoryId:string
tagId:string
dashboardType:string
dashboardDetailId:string
topologyId:string
appId:string
roleType:string
roleId:string
routeId:string
}
const PUBLIC_ROUTES:RouteConfig[] = [
{
path:'/',
component:<Login/>,
key: uuidv4(),
},
{
path:'/login',
component:<Login/>,
key: uuidv4()
},
{
path:'/',
component:<ProtectedRoute/>,
key: uuidv4(),
children:[
// {
// path:'approval/*',
// component:<ApprovalPage />,
// key:uuidv4()
// },
{
path:'guide/*',
component:<Guide />,
key:uuidv4()
},
{
path:'team',
component:<Outlet/>,
key: uuidv4(),
provider: TeamProvider,
children:[
{
path:'',
key: uuidv4(),
component: <Navigate to="list" />
},
{
path:'list',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamList.tsx'))
},
{
path:'inside/:teamId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsidePage.tsx')),
key: uuidv4(),
children:[
{
path:'member',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsideMember.tsx')),
},
{
path:'setting',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamConfig.tsx')),
},
]
}
]
},
{
path:'service',
key: uuidv4(),
component:<SystemOutlet />,
provider: SystemProvider,
children:[
{
path:'',
key:uuidv4(),
component:<Navigate to="list" />
},
{
path:'list',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')),
},
{
path:'list/:teamId',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')),
},
{
path:':teamId',
component:<Outlet/>,
key: uuidv4(),
children:[
{
path:'inside/:serviceId',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsidePage.tsx')),
children:[
{
path:'api',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideApiDocument.tsx')),
},
{
path:'route/create',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideRouterCreate')),
},
{
path:'route/:routeId',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideRouterCreate')),
},
{
path:'route',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideRouterList')),
},
{
path:'upstream',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/upstream/SystemInsideUpstreamContent.tsx')),
},
{
path:'document',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideDocument.tsx')),
},
{
path:'subscriber',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideSubscriber.tsx')),
children:[
]
},
{
path:'approval',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApproval.tsx')),
children:[
{
path:'',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')),
},
{
path:'*',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')),
}
]
},
{
path:'topology',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemTopology.tsx')),
key: uuidv4(),
children:[
]
},
{
path:'publish',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublish.tsx')),
children:[
{
path:'',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')),
},
{
path:'*',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')),
}
]
},
{
path:'setting',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemConfig.tsx')),
children:[
]
},
]
},
{
path:'aiInside/:serviceId',
component:<AiServiceOutlet />,
provider: AiServiceProvider,
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsidePage.tsx')),
children:[
{
path:'api',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideApiDocument')),
},
{
path:'route/create',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate')),
},
{
path:'route/:routeId',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate')),
},
{
path:'route',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterList')),
},
{
path:'document',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsideDocument.tsx')),
},
{
path:'subscriber',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsideSubscriber.tsx')),
children:[
]
},
{
path:'approval',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApproval')),
children:[
{
path:'',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApprovalList')),
},
{
path:'*',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApprovalList')),
}
]
},
{
path:'publish',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublish')),
children:[
{
path:'',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublishList')),
},
{
path:'*',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublishList')),
}
]
},
{
path:'setting',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemConfig.tsx')),
children:[
]
},
]
}
]
}
]
},
{
path:'datasourcing',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideDashboardSetting.tsx')),
},
{
path:'cluster',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCluster.tsx')),
},
{
path:'aisetting',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiSetting/AiSettingList.tsx')),
},
{
path:'cert',
key: uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCert.tsx')),
},
{
path:'serviceHub',
component:<Outlet />,
key:uuidv4(),
children:[
{
path:'',
key: uuidv4(),
component: <Navigate to="list" />
},
{
path:'list',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubList.tsx')),
},
{
path:'detail/:serviceId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubDetail.tsx')),
}]
},
{
path:'commonsetting',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/common/CommonPage.tsx')),
key:uuidv4(),
},
{
path:'consumer',
component:<Outlet />,
provider:TenantManagementProvider,
key:uuidv4(),
children:[
{
path:'',
key:uuidv4(),
component:<Navigate to="list" />
},
{
path:':teamId/inside/:appId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsidePage.tsx')),
children:[
{
path:'service',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideService.tsx')),
},
{
path:'authorization',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideAuth.tsx')),
},
{
path:'setting',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementAppSetting.tsx')),
},
]
},
{
path:'list',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')),
},
{
path:'list/:teamId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')),
},
]
},
{
path:'member',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberPage.tsx')),
children:[
{
path:'',
key:uuidv4(),
component:<Navigate to="list" />
},
{
path:'list',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')),
},
{
path:'list/:memberGroupId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')),
}
]
},
{
path:'role',
key:uuidv4(),
component:<Outlet />,
children:[
{
path: '',
key: uuidv4(),
component: <Navigate to="list" />
},
{
path:'list',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleList.tsx')),
},{
path:':roleType/config/:roleId',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')),
},{
path:':roleType/config',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')),
}
]
},
{
path:'assets',
component:<p></p>,
key:uuidv4()
},
{
path:'analytics',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/Dashboard.tsx')),
key:uuidv4(),
children:[
{
path:'total',
key:uuidv4(),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardTotal.tsx')),
},
]
},
{
path:'template/:moduleId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
key:uuidv4()
},
{
path:'logsettings/*',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/logsettings/LogSettings.tsx')),
key: uuidv4(),
children:[{
path:'template/:moduleId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
key:uuidv4()
}]
},
APP_MODE ==='pro' && {
path:'resourcesettings/*',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/resourcesettings/ResourceSettings.tsx')),
key: uuidv4(),
children:[{
path:'template/:moduleId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
key:uuidv4()
}]
},
{
path:'userProfile/*',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/userProfile/UserProfile.tsx')),
key:uuidv4(),
children:[{
path:'changepsw',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/userProfile/ChangePsw.tsx')),
key:uuidv4()
}]
}
]
},
]
import usePluginLoader from '@common/hooks/pluginLoader.ts';
import { RouteConfig } from '@common/const/type.ts';
import { ApiparkPluginDriver } from '@common/utils/plugin.tsx';
import { routerMap } from '@core/const/const';
import withRouteGuard from "@common/components/aoplatform/WithRouteGuard.tsx";
import ErrorBoundary from "@common/components/aoplatform/ErrorBoundary";
import React from 'react';
import { LoadingOutlined } from '@ant-design/icons';
const RenderRoutes = ()=> {
const { loadPlugins,loadExecutedPlugin } = usePluginLoader(ApiparkPluginDriver(routerMap), routerMap)
const { routeConfig } = useGlobalContext();
const [router, setRouter] = useState<unknown>(null);
useEffect(()=>{
loadPlugins().then(()=>{
loadExecutedPlugin()
})
},[])
useEffect(() => {
if (routeConfig && routeConfig.length > 0) {
const routerInstance = createBrowserRouter(generateRoutes(routeConfig));
setRouter(routerInstance);
}
}, [routeConfig]);
if (!router) {
return <Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={true} className='w-full h-full flex items-center justify-center'></Spin>;
}
return (
<App className="h-full" message={{ maxCount: 1 }}>
<Router>
<Routes>
{generateRoutes(PUBLIC_ROUTES)}
</Routes>
</Router>
</App>
)
<RouterProvider router={router} />
);
}
const generateRoutes = (routerConfig: RouteConfig[]) => {
const generateRoutes = (routerConfig: RouteConfig[]):RouteObject[] => {
return routerConfig?.map((route: RouteConfig) => {
let routeElement;
if (route.lazy) {
const LazyComponent = route.lazy as React.ExoticComponent<unknown>;
let LazyComponent;
if (typeof route.lazy === 'function') {
const result = route.lazy();
if (result instanceof Promise) {
LazyComponent = React.lazy(() => result.then(module => ({ default: module.default || module })));
} else {
LazyComponent = result;
}
} else {
LazyComponent = route.lazy;
}
const GuardedComponent = route.lifecycle ? withRouteGuard(LazyComponent, {pathPrefix:`/${route.pathPrefix ?? route.path}`, ...route.lifecycle}): LazyComponent
routeElement = (
<Suspense fallback={ <div className=''><Skeleton className='m-btnbase w-calc-100vw-minus-padding-r' active /></div>}>
{route.provider ? (
createElement(route.provider, {}, <LazyComponent />)
createElement(route.provider, {}, <GuardedComponent />)
) : (
<LazyComponent />
<GuardedComponent />
)}
</Suspense>
);
} else {
routeElement = route.provider ? (
createElement(route.provider, {}, route.component)
createElement(route.provider, {}, route.component)
) : (
route.component
);
}
return (
<Route
key={route.key}
path={route.path}
element={routeElement}
>
{route.children && generateRoutes(route.children as RouteConfig[])}
</Route>
{
path: route.path,
element: <ErrorBoundary>{routeElement}</ErrorBoundary> ,
children: route.children ? generateRoutes(route.children as RouteConfig[]) : undefined,}
);
}
)
}
// 保护的路由组件
function ProtectedRoute() {
export function ProtectedRoute() {
const {state} = useGlobalContext()
return state.isAuthenticated? <BasicLayout project="core" /> : <Navigate to="/login" />;
}
export default RenderRoutes
export default RenderRoutes
+493
View File
@@ -0,0 +1,493 @@
import { RouterMapConfig } from '@common/const/type';
import { ProtectedRoute } from '@core/components/aoplatform/RenderRoutes';
import { AiServiceProvider } from '@core/contexts/AiServiceContext';
import { SystemProvider } from '@core/contexts/SystemContext';
import { TeamProvider } from '@core/contexts/TeamContext';
import AiServiceOutlet from '@core/pages/aiService/AiServiceOutlet';
import Guide from '@core/pages/guide/Guide';
import Login from '@core/pages/Login';
import SystemOutlet from '@core/pages/system/SystemOutlet';
import { TenantManagementProvider } from '@market/contexts/TenantManagementContext';
import { lazy } from 'react';
import { Outlet, Navigate } from 'react-router-dom';
// 内置插件与对应组件/模块
export const routerMap:Map<string, RouterMapConfig> = new Map([
['basicLayout', { type: 'component', component: <ProtectedRoute />}],
['navHidden', { type: 'component', component: <ProtectedRoute /> }],
['login', { type: 'component', component: <Login /> }],
['guide',{
type:'component',
component:<Guide />
}],
['team', {type: 'module',
component:<Outlet/>,
key: 'team',
provider: TeamProvider,
children:[
{
path:'',
key: 'teamList',
component: <Navigate to="list" />
},
{
path:'list',
key: 'teamList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamList.tsx'))
},
{
path:'inside/:teamId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsidePage.tsx')),
key: 'teamInside',
children:[
{
path:'member',
key: 'teamMember',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsideMember.tsx')),
},
{
path:'setting',
key: 'teamSetting',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamConfig.tsx')),
},
]
}
]
}],
['service', {
type: 'module',
path:'service',
component:<SystemOutlet />,
key: 'service',
provider: SystemProvider,
children:[
{
path:'',
key:'serviceList',
component:<Navigate to="list" />
},
{
path:'list',
key: 'serviceList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')),
},
{
path:'list/:teamId',
key: 'serviceList3',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')),
},
{
path:':teamId',
component:<Outlet/>,
key: 'serviceInside',
children:[
{
path:'inside/:serviceId',
key: 'restServiceInside',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsidePage.tsx')),
children:[
{
path:'api',
key: 'restServiceInsideApi',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideApiDocument.tsx')),
},
{
path:'route/create',
key: 'restServiceInsideRouteCreate',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideRouterCreate')),
},
{
path:'route/:routeId',
key: 'restServiceInsideRouteEdit',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideRouterCreate')),
},
{
path:'route',
key: 'restServiceInsideRoute',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideRouterList')),
},
{
path:'upstream',
key: 'restServiceInsideUpstream',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/upstream/SystemInsideUpstreamContent.tsx')),
},
{
path:'document',
key: 'restServiceInsideDocument',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideDocument.tsx')),
},
{
path:'subscriber',
key: 'restServiceInsideSubscriber',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideSubscriber.tsx')),
children:[
]
},
{
path:'approval',
key: 'restServiceInsideApproval',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApproval.tsx')),
children:[
{
path:'',
key: 'restServiceInsideApprovalList',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')),
},
{
path:'*',
key: 'restServiceInsideApprovalList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')),
}
]
},
{
path:'publish',
key: 'systemPublish',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublish.tsx')),
children:[
{
path:'',
key: 'systemPublishList',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')),
},
{
path:'*',
key: 'systemPublishList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')),
}
]
},
{
path:'setting',
key: 'systemConfig',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemConfig.tsx')),
children:[
]
},
{
path:'servicepolicy',
key: 'servicePolicy',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/ServicePolicy')),
children:[
]
},
]
},
{
path:'aiInside/:serviceId',
component:<AiServiceOutlet />,
provider: AiServiceProvider,
key: 'aiServiceInside',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsidePage.tsx')),
children:[
{
path:'api',
key: 'aiServiceInsideApi',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideApiDocument')),
},
{
path:'route/create',
key: 'aiServiceInsideRouteCreate',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate')),
},
{
path:'route/:routeId',
key: 'aiServiceInsideRouteEdit',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate')),
},
{
path:'route',
key: 'aiServiceInsideRouteList',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterList')),
},
{
path:'document',
key: 'aiServiceInsideDocument',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsideDocument.tsx')),
},
{
path:'subscriber',
key: 'aiServiceInsideSubscriber',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsideSubscriber.tsx')),
children:[
]
},
{
path:'approval',
key: 'aiServiceInsideApproval',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApproval')),
children:[
{
path:'',
key: 'aiServiceInsideApprovalList',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApprovalList')),
},
{
path:'*',
key: 'aiServiceInsideApprovalList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApprovalList')),
}
]
},
{
path:'publish',
key: 'aiServiceInsidePublish',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublish')),
children:[
{
path:'',
key: 'aiServiceInsidePublishList',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublishList')),
},
{
path:'*',
key: 'aiServiceInsidePublishList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublishList')),
}
]
},
{
path:'setting',
key: 'aiServiceInsideSetting',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemConfig.tsx')),
children:[
]
},
{
path:'servicepolicy',
key: 'servicePolicy',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/ServicePolicy')),
children:[
]
},
]
}
]
}
]
}],
['datasourcing', { type: 'component',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideDashboardSetting.tsx'))
}],
['cluster', { type: 'component',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCluster.tsx')),
}],
['aisetting', { type: 'component',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiSetting/AiSettingList.tsx')),
}],
['cert', { type: 'component',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCert.tsx')),
}],
['serviceHub', {
type: 'module',
component:<Outlet />,
key:'serviceHub',
children:[
{
path:'',
key: 'serviceHubList',
component: <Navigate to="list" />
},
{
path:'list',
key:'serviceHubList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubList.tsx')),
},
{
path:'detail/:serviceId',
key:'serviceHubDetail',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubDetail.tsx')),
}]
}],
['commonsetting', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/common/CommonPage.tsx')),
}],
['consumer', { type: 'module',
component:<Outlet />,
provider:TenantManagementProvider,
key:'consumer',
children:[
{
path:'',
key:'consumerList',
component:<Navigate to="list" />
},
{
path:':teamId/inside/:appId',
key:'consumerInside',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsidePage.tsx')),
children:[
{
path:'service',
key:'consumerInsideService',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideService.tsx')),
},
{
path:'authorization',
key:'consumerInsideAuthorization',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideAuth.tsx')),
},
{
path:'setting',
key:'consumerSetting',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementAppSetting.tsx')),
},
]
},
{
path:'list',
key:'serviceHubManagementList',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')),
},
{
path:'list/:teamId',
key:'serviceHubManagementList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')),
},
]}],
['member', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberPage.tsx')),
children:[
{
path:'',
key:'memberList',
component:<Navigate to="list" />
},
{
path:'list',
key:'memberList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')),
},
{
path:'list/:memberGroupId',
key:'memberList3',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')),
}
],
}],
['role', { type: 'module',
component:<Outlet></Outlet>,
children:[
{
path: '',
key: 'roleList',
component: <Navigate to="list" />
},
{
path:'list',
key:'roleList2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleList.tsx')),
},
{
path:':roleType/config/:roleId',
key:'roleConfig',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')),
},
{
path:':roleType/config',
key:'roleConfig2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')),
}
]
}],
['analytics', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/Dashboard.tsx')),
key:'analytics',
children:[
{
path:'total',
key:'analytics2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardTotal.tsx')),
},
{
path:':dashboardType',
key:'analytics3',
component:<Outlet />,
children:[
{
path:'list',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardList.tsx')),
key:'analyticsList'
},
{
path:'detail/:dashboardDetailId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardDetail.tsx')),
key:'analyticsDetail'
},
]
}
]
}],
['template', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
}],
['logsettings', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/logsettings/LogSettings.tsx')),
key:'logsettings',
children:[
{
path:'template/:moduleId',
key:'logSettings2',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
},
]
}],
['resourcesettings', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/resourcesettings/ResourceSettings.tsx')),
key:'resourcesettings',
children:[
{
path:'template/:moduleId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')),
key:'resourceSettings2'
},
]
}],
['userProfile', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/userProfile/UserProfile.tsx')),
key:'userProfile',
children:[{
path:'changepsw',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/userProfile/ChangePsw.tsx')),
key:'changePsw'
}]}],
['globalPolicy', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/GlobalPolicyLayout')),
key:'globalPolicy',
children:[{
path:'datamasking',
component:<Outlet />,
key:'dataMasking',
children:[
{
path:'list',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/GlobalPolicy')),
key:'dataMaskingList'
},
{
path:'create',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/dataMasking/DataMaskingConfig')),
key:'dataMaskingAdd'
},
{
path:':policyId',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/dataMasking/DataMaskingConfig')),
key:'dataMaskingAdd'
}]
}]
}],
])
@@ -3,9 +3,10 @@ import { Input, TabsProps } from "antd";
import { MatchItem } from "@common/const/type";
import { ConfigField } from "@common/components/aoplatform/EditableTableWithModal";
import { frontendTimeSorter } from "@common/utils/dataTransfer";
import { COLUMNS_TITLE } from "@common/const/const";
import { COLUMNS_TITLE, PLACEHOLDER } from "@common/const/const";
import { PageProColumns } from "@common/components/aoplatform/PageList";
import { $t } from "@common/locales";
export enum SubscribeEnum{
Rejected = 0,
@@ -222,7 +223,7 @@ export const MATCH_CONFIG:ConfigField<MatchItem>[] = [
}, {
title:('参数名'),
key: 'key',
component: <Input className="w-INPUT_NORMAL" />,
component: <Input className="w-INPUT_NORMAL"/>,
renderText: (value: unknown) => value,
required: true
}, {
@@ -236,7 +237,7 @@ export const MATCH_CONFIG:ConfigField<MatchItem>[] = [
title:('参数值'),
key: 'pattern',
unRender:(formValue)=>{return formValue?.matchType === 'NULL' || formValue?.matchType==='EXIST' || formValue?.matchType === 'UNEXIST'},
component: <Input className="w-INPUT_NORMAL"/>,
component: <Input className="w-INPUT_NORMAL" />,
renderText: (value: string) => {
return value
},
@@ -1,23 +0,0 @@
import {FC, createContext, useContext, useState, ReactNode } from 'react';
import { PartitionConfigFieldType } from '../const/partitions/types.ts';
interface PartitionContextProps {
partitionInfo:PartitionConfigFieldType|undefined
setPartitionInfo:React.Dispatch<React.SetStateAction<PartitionConfigFieldType|undefined>>;
}
const PartitionContext = createContext<PartitionContextProps | undefined>(undefined);
export const usePartitionContext = () => {
const context = useContext(PartitionContext);
if (!context) {
throw new Error('useArray must be used within a ArrayProvider');
}
return context;
};
export const PartitionProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [partitionInfo, setPartitionInfo] = useState<PartitionConfigFieldType>()
return <PartitionContext.Provider value={{ partitionInfo, setPartitionInfo }}>{children}</PartitionContext.Provider>;
};
@@ -1,23 +0,0 @@
import { createContext, useContext, useState, ReactNode, FC } from 'react';
import { MyServiceFieldType } from '../const/system/type.ts';
interface SystemMyServiceContextProps {
serviceInfo:MyServiceFieldType|undefined
setServiceInfo:React.Dispatch<React.SetStateAction<MyServiceFieldType|undefined>>;
}
const SystemMyServiceContext = createContext<SystemMyServiceContextProps | undefined>(undefined);
export const useSystemMyServiceContext = () => {
const context = useContext(SystemMyServiceContext);
if (!context) {
throw new Error('useArray must be used within a ArrayProvider');
}
return context;
};
export const SystemMyServiceProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [serviceInfo, setServiceInfo] = useState<MyServiceFieldType>()
return <SystemMyServiceContext.Provider value={{ serviceInfo, setServiceInfo }}>{children}</SystemMyServiceContext.Provider>;
};
+19 -1
View File
@@ -321,6 +321,11 @@ p{
}
}
.ant-table-body {
scrollbar-width: auto;
scrollbar-color: auto;
}
::-webkit-scrollbar {
width: 10px;
@@ -664,7 +669,7 @@ p{
.ant-pro-table-list-toolbar-setting-items{
position:absolute;
top:18px;
right:16px;
right:22px;
z-index:9;
.ant-pro-table-list-toolbar-setting-item{
font-size:14px;
@@ -1005,6 +1010,19 @@ p{
}
}
.global-policy-tabs {
.ant-tabs-nav {
&::before {
border-bottom: none;
}
}
.ant-tabs-content {
height: 100%;
.ant-tabs-tabpane {
height: 100%;
}
}
}
.ant-tooltip{
max-width: 280px !important;
+3 -4
View File
@@ -3,8 +3,7 @@ import {StrictMode} from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import {GlobalProvider} from "@common/contexts/GlobalStateContext.tsx";
import { LocaleProvider } from '@common/contexts/LocaleContext.tsx';
async function initializeApp() {
try {
// 初始化行为
@@ -13,9 +12,9 @@ async function initializeApp() {
// 异步操作完成后,渲染React消费者
ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<GlobalProvider>
<LocaleProvider>
<App />
</GlobalProvider>
</LocaleProvider>
</StrictMode>,
);
} catch (error) {
+1 -1
View File
@@ -206,7 +206,7 @@ const Login:FC = ()=> {
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
<p className="leading-[28px]">
{$t('Version (0)-(1)',[state.version,state.updateDate])}, {$t(state.powered)}
{$t('Version (0)-(1)',[state?.version,state?.updateDate])}, {$t(state?.powered || '-')}
</p>
<LanguageSetting mode="light"/>
</section>
@@ -42,7 +42,6 @@ const AiServiceInsidePage:FC = ()=> {
const getApiDefine = ()=>{
console.log('@@@@@@@')
setApiPrefix('')
setPrefixForce(false)
fetchData<BasicResponse<{ prefix:string, force:boolean }>>('service/router/define',{method:'GET',eoParams:{service:serviceId,team:teamId}}).then(response=>{
@@ -0,0 +1,256 @@
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
import { Form, Input, Select, Checkbox, Table, Spin, TableColumnsType, message } from 'antd';
import { useFetch } from '@common/hooks/http';
import { LoadingOutlined } from '@ant-design/icons';
import { validateApiPath, validateIPorCIDR } from '@common/utils/validate';
import { $t } from '@common/locales';
import { FilterFormItemProps, RemoteTitleType, FilterFormHandle, FilterFormProps } from '@common/const/policy/type';
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const';
const RemoteFormItem: React.FC<FilterFormItemProps> = (props) =>{
const {value, onChange, disabled,option, onShowValueChange} = props
const [remoteList, setRemoteList] = useState<unknown[]>([])
const [remoteTableColumns, setRemoteTableColumns] = useState<TableColumnsType>([])
const [loading, setLoading] = useState<boolean>(false)
const [rowKey, setRowKey] = useState<string>('')
const title = useMemo(()=>option?.title,[option])
const [remoteCounts, setRemoteCounts] = useState<number>(0)
const [originRemoteList, setOriginRemoteList] = useState<unknown[]>([])
const {fetchData} = useFetch()
const getRemoteDetail = (searchWord?:string)=>{
setLoading(true)
fetchData<BasicResponse<{
key:string,
list:Record<string,unknown>[],
target:string,
titles:Array<RemoteTitleType>,
total:number
value:string
}>>(`strategy/filter-REMOTE/${option?.name}`,{method:'GET', eoParams:{keyword:searchWord}}).then(response=>{
const {code,data, msg} = response
if(code === STATUS_CODE.SUCCESS){
setRemoteList(data[data.target as string] as unknown[])
setRowKey(data.key as string)
setRemoteTableColumns(data.titles.map((x:RemoteTitleType)=>({
title: x.title,dataIndex:x.field,key:x.field,ellipsis:true
})))
setRemoteCounts(data.total)
if(!searchWord){
setOriginRemoteList(data[data.target as string])
if(value?.length === 1 && value[0] === 'ALL'){
const totalDataArr = data[data.target as string]?.map((x:Record<string,unknown>)=>x[data.key as string])
onChange?.(totalDataArr)
onShowValueChange?.(totalDataArr.join(','))
}
}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>{
setLoading(false)
})
}
useEffect(()=>{getRemoteDetail()},[option])
return (
<div className="w-full transfer-section rounded-DEFAULT" style={{ border: '1px solid var(--border-color)', borderBottom: 'none' }}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin/>} spinning={loading}>
<p className="flex items-center mt-[12px] text-[16px] font-bold px-btnbase">
{$t('已选择(0)项(1)数据', [value?.length || 0, title])}
</p>
<div className="flex items-center justify-between py-btnybase px-btnbase">
<div></div>
<Input.Search
className="w-[224px] h-[32px]"
placeholder={$t(PLACEHOLDER.input)}
onSearch={(value)=>getRemoteDetail(value)}
disabled={disabled}
/>
</div>
<Table
columns={remoteTableColumns}
dataSource={remoteList}
pagination={false}
scroll={{ y: 316 }}
rowClassName={() => (disabled ? '' : 'clickable-row')}
rowKey={rowKey}
size='small'
rowSelection={{
type: 'checkbox',
onChange: (selectedRowKeys: React.Key[]) => {
onChange?.(selectedRowKeys as string[]);
onShowValueChange?.(selectedRowKeys.length === remoteCounts? $t('所有(0)',[title]) : originRemoteList.filter(x=>selectedRowKeys?.indexOf(x[option.target]))?.map(x=>x.title).join(' , '))
},
selectedRowKeys: value,
// getCheckboxProps: (record: unknown) => ({
// disabled: record.name === 'Disabled User', // Column configuration not to be checked
// name: record.name,
// })
}}
onRow={(record)=>({
onClick:()=>{
if(value === undefined){
onChange?.([record[rowKey]])
onShowValueChange?.(record.title)
}else if(value?.indexOf(record[rowKey])!== -1){
const newSelectedKeys = value?.filter(x=>x!==record[rowKey])
onChange?.(newSelectedKeys!)
onShowValueChange?.(newSelectedKeys.length === remoteCounts? $t('所有(0)',[option?.title]) : originRemoteList.filter(x=>newSelectedKeys.indexOf(x[rowKey]) !== -1)?.map(x=>x.title)?.join(' , '))
}else{
const newSelectedKeys = [...value,record[rowKey]]
onChange?.(newSelectedKeys)
onShowValueChange?.(newSelectedKeys.length === remoteCounts? $t('所有(0)',[option?.title]) : originRemoteList.filter(x=>newSelectedKeys.indexOf(x[rowKey]) !== -1)?.map(x=>x.title)?.join(' , '))
}
}
})}
/>
</Spin>
</div>)
}
const StaticFormItem: React.FC<FilterFormItemProps> = (props) => {
const {value, onChange, disabled,option,onShowValueChange} = props
const showAll = useMemo(()=>option.options.indexOf('ALL') !== -1,[option])
const allChecked = useMemo(()=>value?.filter(x=>x!== 'ALL').length === option.options.filter(x=>x!== 'ALL').length,[value,option])
useEffect(()=>{
if(value?.length === 1 && value[0] === 'ALL'){
onChange?.(option.options.filter(x=>x!== 'ALL'))
onShowValueChange?.($t('所有(0)',[option?.title]))
}
},[])
return (
<div className="w-auto">
{showAll && (
<Checkbox
className='mr-[8px]'
checked={allChecked}
onChange={(e) =>{
onChange?.(e.target.checked ? option.options : [])
onShowValueChange?.(e.target.checked ? $t('所有(0)',[option?.title]) : '-')
}}
disabled={disabled}
indeterminate={!allChecked && value?.length > 0}
>
ALL
</Checkbox>
)}
<Checkbox.Group
value={value}
options={option?.options.filter(x=>x!== 'ALL')}
onChange={(checkedValues) => {
onChange?.(checkedValues)
onShowValueChange?.(checkedValues.join(','))
}}
disabled={disabled}
/>
</div>)
}
const FilterForm = forwardRef<FilterFormHandle,FilterFormProps>(({
filterForm,
filterOptions,
selectedOptionNameSet,
disabled,
setFormCanSubmit},ref)=> {
const [form] = Form.useForm();
const [filterType, setFilterType] = useState<'remote'|'static'|'pattern'>();
const [curOption, setCurOption] = useState<unknown>()
const [label,setLabel] = useState<string>('')
useImperativeHandle(ref, ()=>({
clear:()=>{
form.resetFields()
},
save:()=>form.validateFields().then((res)=>{
const selectedOption = filterOptions.filter(x=>x.name === res.name)[0]
return Promise.resolve({
...res,
label:filterType === 'pattern' ? res.value : label,
title:selectedOption.label
})
}).catch((errorInfo)=>Promise.reject(errorInfo))
})
)
const handleValuesChange = (changedValues: any, allValues: any) => {
if(!allValues){
setFormCanSubmit(false)
return
}
if(allValues.value instanceof Array){
setFormCanSubmit(allValues.value.length > 0)
return
}
setFormCanSubmit(true)
};
const handleTypeChange = (value:string)=>{
form.setFieldValue('value',filterForm?.name === value ? filterForm.value : undefined)
const selectedOption = filterOptions?.filter(item=>item.name === value)[0]
setFilterType(selectedOption?.type)
setCurOption(selectedOption)
setFormCanSubmit(filterForm?.name === value )
}
const handleIPChange = (e) => {
const inputValue = e.target.value;
const formattedValue = inputValue.replace(/,/g, '\n');
form.setFieldsValue({ value: formattedValue });
};
useEffect(()=>{
if(filterForm?.name){
form.setFieldsValue(filterForm)
const selectedOption = filterOptions.filter(x=>x.name === filterForm?.name)[0]
setFilterType(selectedOption?.type )
setCurOption(selectedOption)
}else{
const firstOption = filterOptions.filter(x=>!selectedOptionNameSet.has(x.name))[0]
form.setFieldValue('name',firstOption?.name)
setFilterType(firstOption?.type)
setCurOption(firstOption)
}
},[filterForm])
const filterOptionsList = useMemo(() => {
return filterOptions.filter(x=>{
return !!(filterForm?.name && x.name === filterForm?.name )|| !selectedOptionNameSet.has(x.name)}).map((item) => ({label:item.title, value:item.name}));
}, [filterOptions,filterForm,selectedOptionNameSet]);
return (
<Form form={form} layout="vertical" onValuesChange={handleValuesChange}>
<Form.Item name="name" label={$t('属性名称')} rules={[{ required: true }]}>
<Select disabled={disabled} onChange={handleTypeChange} options={filterOptionsList} />
</Form.Item>
<Form.Item name="value" label={$t('属性值')} rules={
(filterType === 'pattern' ? ( [{validator:form.getFieldValue('name') === 'ip' ? validateIPorCIDR : validateApiPath}]):[])
}>
{filterType === 'remote' && <RemoteFormItem option={curOption} disabled={disabled} onShowValueChange={setLabel}/>}
{filterType === 'pattern' && form.getFieldValue('name') !== 'ip' && (
<Input
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
/>
)}
{filterType === 'pattern' && form.getFieldValue('name') === 'ip' && (
<Input.TextArea
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.ipAndCidr)}
onChange={handleIPChange}
/>
)}
{filterType === 'static' && <StaticFormItem option={curOption} disabled={disabled} onShowValueChange={setLabel}/>}
</Form.Item>
</Form>
);
})
export default FilterForm;
@@ -0,0 +1,144 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Button, Table, Modal, App, Divider } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { $t } from '@common/locales';
import { useFetch } from '@common/hooks/http';
import { useGlobalContext } from '@common/contexts/GlobalStateContext';
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission';
import { FilterFormField, FilterTableProps, FilterOptionType, FilterFormHandle } from '@common/const/policy/type.ts';
import FilterForm from './FilterForm';
import { BasicResponse, COLUMNS_TITLE, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const';
const FilterTable: React.FC<FilterTableProps> = ({
disabled = false,
drawerTitle = '筛选条件',
value,
onChange
}) => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [filterForm, setFilterForm] = useState<FilterFormField>();
const [filterOptions, setFilterOptions] = useState<FilterOptionType[]>([]);
const {message} = App.useApp()
const {state} = useGlobalContext()
const {fetchData} = useFetch()
const formRef = useRef<FilterFormHandle>(null);
const [formCanSubmit,setFormCanSubmit] = useState(false)
const [selectedOptionNameSet, setSelectedOptionNameSet] = useState<Set<string>>(new Set());
const openDrawer = (type: string, data?: FilterFormField) => {
switch (type) {
case 'addFilter':
setFilterForm(undefined)
break;
case 'editFilter':
console.log(data)
setFilterForm(data)
}
setIsModalVisible(true);
};
const closeDrawer = () => {
setIsModalVisible(false);
cleanFilterForm();
};
const cleanFilterForm = () => {
setFilterForm(undefined);
};
const handleSaveFilter = () => {
const formPromise = formRef.current?.save();
formPromise?.then?.((res)=>{
const newFilterForm = {
name:res.name,
value:res.value instanceof Array ? res.value : [res.value],
label:res.label,
title:res.title
}
onChange?.([newFilterForm, ...(value?.filter(x=>!filterForm?.name|| x.name!==filterForm.name) || [])])
setSelectedOptionNameSet(prev=>{filterForm?.name &&prev.delete(filterForm?.name); prev.add(res.name); return prev})
closeDrawer()
})
};
const handleDeleteFilter = (item: FilterFormField) => {
setSelectedOptionNameSet(prev=>{prev.delete(item?.name); return prev})
const newFilterShowList = value.filter((filter) => filter !== item);
onChange?.(newFilterShowList);
};
const getFilterOptions = ()=>{
fetchData<BasicResponse<{options:FilterOptionType[]}>>('strategy/filter-options',{method:'GET'}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setFilterOptions(data.options)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> console.error(errorInfo))
}
const columns: ColumnsType<FilterFormField> =useMemo(()=>[
{
title: $t('属性名称'),
dataIndex: 'name',
key: 'name',
},
{
title: $t('属性值'),
dataIndex: 'label',
key: 'label',
},
{
title: COLUMNS_TITLE.operate,
key: 'action',
width:100,
render: (_, record) => (
<div className='flex items-center gap-[8px]'>
<TableBtnWithPermission key="edit" btnType="edit" onClick={()=>{openDrawer('editFilter', record)}} btnTitle={$t("编辑")}/>
<Divider type="vertical" className="mx-0" key="div2"/>
<TableBtnWithPermission key="delete" btnType="delete" onClick={()=>{handleDeleteFilter(record)}} btnTitle={$t("删除")}/></div>)
}
],[state.language])
useEffect(()=>{
getFilterOptions()
},[state.language])
return (
<div>
{
!disabled &&<Button onClick={() => openDrawer('addFilter')}>
{$t('添加配置')}
</Button>
}
{value && value.length >0 && <Table
className={`mt-btnybase border-solid border-[1px] border-BORDER border-b-0 rounded ${disabled ? '' : 'mt-btnbase'}`}
pagination={false}
size='small'
columns={columns} dataSource={value} rowKey='id' /> }
<div role="alert" className="ant-form-item-explain-error">
</div>
<Modal
title={filterForm?.name ? $t('编辑(0)',[$t(drawerTitle)]) :$t('配置(0)',[$t(drawerTitle)])}
visible={isModalVisible}
onCancel={closeDrawer}
width={900}
okButtonProps={{ disabled:!formCanSubmit }}
onOk={()=>handleSaveFilter()}
destroyOnClose={true}
>
<FilterForm
ref={formRef}
filterForm={filterForm}
filterOptions={filterOptions}
selectedOptionNameSet={selectedOptionNameSet}
disabled={disabled}
setFormCanSubmit={setFormCanSubmit}
/>
</Modal>
</div>
);
};
export default FilterTable;
@@ -0,0 +1,14 @@
import { useEffect } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
export default function GlobalPolicyLayout(){
const location = useLocation()
const pathName = location.pathname
const navigator = useNavigate()
useEffect(()=>{
if(pathName === '/globalpolicy'){
navigator('/globalpolicy/datamasking/list')
}
},[pathName])
return (<Outlet></Outlet>)
}
@@ -0,0 +1,331 @@
import { ActionType } from "@ant-design/pro-components";
import { useMemo, useRef, useState } from "react";
import { Button, message, Switch } from 'antd'
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const.tsx";
import { useFetch } from "@common/hooks/http";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission";
import { DATA_MASSKING_TABLE_COLUMNS } from "./DataMaskingColumn";
import { useNavigate, useParams } from "react-router-dom";
import { PolicyPublishInfoType, PolicyPublishModalHandle, RouterParams } from "@common/const/type";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter";
import { DataMaskStrategyItem } from "@common/const/policy/type";
import {PolicyPublishModalContent} from '@common/components/aoplatform/PolicyPublishModalContent'
const DataMasking = (props: any) => {
const {
// 是否显示发布按钮
publishBtn = false,
// 行操作
rowOperation = []
} = props;
const { serviceId, teamId } = useParams<RouterParams>()
const { state } = useGlobalContext()
const navigator = useNavigate()
const [drawerVisible, setDrawerVisible] = useState<boolean>(false)
const [drawerData, setDrawerData] = useState<PolicyPublishInfoType >()
const [isOkToPublish, setIsOkToPublish] = useState<boolean>(false)
const drawerRef = useRef<PolicyPublishModalHandle>(null)
/**
* ref
*/
const pageListRef = useRef<ActionType>(null);
/**
*
*/
const { fetchData } = useFetch()
/**
*
*/
const [searchWord, setSearchWord] = useState<string>('')
/**
*
*/
const columns = useMemo(() => {
const res = DATA_MASSKING_TABLE_COLUMNS.map(x => {
// 启动列渲染
if (x.dataIndex === 'isStop') {
x.render = (text: any, record: any) => <Switch checked={!record.isStop} onChange={(e) => { changeOpenApiStatus(e, record) }} />
}
// 处理数列渲染
if (x.dataIndex === 'treatmentNumber') {
x.render = (text: any, record: any) => <span className="w-full block cursor-pointer [&>.ant-typography]:text-theme" onClick={(e) => { openLogsModal(record) }} >{ text }</span>
}
return {
...x,
title: typeof x.title === 'string' ? $t(x.title as string) : x.title
}
})
return res
}, [ state.language])
/**
*
*/
const operation: PageProColumns<any>[] = rowOperation.length ? [
{
title: '',
key: 'option',
btnNums: rowOperation.length,
fixed: 'right',
valueType: 'option',
render: (_: React.ReactNode, entity: any) => [
...(rowOperation.length && rowOperation.find((item: string) => item === 'edit') ? [<TableBtnWithPermission access="system.organization.member.edit" key="edit" btnType="edit" onClick={() => { openEditModal(entity) }} btnTitle="编辑" />] : []),
// ...(rowOperation.length && rowOperation.find((item: string) => item === 'logs') ? [<TableBtnWithPermission access="system.organization.member.edit" key="logs" btnType="logs" onClick={() => { openLogsModal(entity) }} btnTitle="详情" />] : []),
...(rowOperation.length && rowOperation.find((item: string) => item === 'delete') ? [
entity.isDeleted ? <TableBtnWithPermission access="system.organization.member.edit" key="refresh" btnType="refresh" onClick={() => { restorePolicy(entity) }} btnTitle="恢复" /> :
<TableBtnWithPermission access="system.organization.member.edit" key="delete" btnType="delete" onClick={() => { deletePolicy(entity) }} btnTitle="删除" />
] : []),
],
}
] : []
/**
*
*/
const manualReloadTable = () => {
pageListRef.current?.reload()
};
/**
*
* @param enabled
* @param entity
*/
const changeOpenApiStatus = (enabled: boolean, entity: any) => {
fetchData<BasicResponse<null>>(
`strategy/${serviceId === undefined? 'global':'service'}/data-masking/${enabled ? 'disable' : 'enable'}`,
{
method: 'PUT',
eoParams: {
service:serviceId,
team:teamId,
strategy: entity.id
}
}
).then(response => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
manualReloadTable()
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
/**
*
* @param dataType
* @returns
*/
const getPolicyList = (params: DataMaskStrategyItem & {
pageSize: number;
current: number;
},
sort:Record<string, string>,
filter:Record<string, string>) => {
let filters
if(filter){
filters = []
if(filter.isStop){
if(filter.isStop.indexOf('true')!== -1){
filters.push('enable')
}
if(filter.isStop.indexOf('false')!== -1){
filters.push('disable')
}
if(filter.publishStatus?.length > 0){
filters = [...filters, ...filter.publishStatus]
}
}
}
return fetchData<BasicResponse<{list:DataMaskStrategyItem[], total:number}>>(
`strategy/${serviceId === undefined? 'global':'service'}/data-masking/list`,
{
method: 'GET',
eoParams: {
order:Object.keys(sort)?.[0],
sort:Object.keys(sort)?.length > 0 ? Object.values(sort)?.[0] === 'descend' ? 'desc' : 'asc' : undefined,
filters:JSON.stringify(filters),
keyword: searchWord,
service:serviceId,
team:teamId,},
eoTransformKeys: ['is_stop', 'is_deleted', 'update_time','publish_status','processed_total']
}
).then(response => {
const { code,data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
// 保存数据
return {
data:data.strategies,
total:data.total,
success: true
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return { data: [], success: false }
}
}).catch(() => {
return { data: [], success: false }
})
}
/**
*
* @param type
*/
const addPolicy = () => {
navigator('/globalpolicy/datamasking/create')
}
/**
*
*/
const publish = async () => {
message.loading($t(RESPONSE_TIPS.loading));
const { code, data, msg } = await fetchData<BasicResponse<PolicyPublishInfoType>>(
'strategy/global/data-masking/to-publishs',
{ method: 'GET',eoTransformKeys:['opt_time','is_publish','version_name','unpublish_msg'] }
);
message.destroy();
if (code === STATUS_CODE.SUCCESS) {
setDrawerVisible(true)
setDrawerData(data)
setIsOkToPublish(data.isPublish??true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error));
return
}
}
/**
*
*/
const openEditModal = (entity: any) => {
navigator(`/globalpolicy/datamasking/${entity.id}`)
}
/**
*
* @param entity
*/
const openLogsModal = (entity: any) => {
console.log('日志', entity);
}
/**
*
* @param entity
*/
const deletePolicy = (entity: DataMaskStrategyItem) => {
fetchData<BasicResponse<null>>(
`strategy/${serviceId === undefined? 'global':'service'}/data-masking`,
{
method: 'DELETE',
eoParams: {
service:serviceId,
team:teamId,
strategy:entity.id},
}
).then(response => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
manualReloadTable()
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
/**
*
* @param entity
*/
const restorePolicy = (entity: any) => {
fetchData<BasicResponse<null>>(
`strategy/${serviceId === undefined? 'global':'service'}/data-masking/restore`,
{
method: 'PATCH',
eoParams: {
service:serviceId,
team:teamId,
strategy:entity.id},
}
).then(response => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
manualReloadTable()
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const onSubmit = () => {
return drawerRef.current?.publish()?.then((res) => {
manualReloadTable();
return res;
});
}
return (
<>
<PageList<DataMaskStrategyItem>
id="data_masking_list"
ref={pageListRef}
columns={[...columns, ...operation]}
request={async (params: DataMaskStrategyItem & {
pageSize: number;
current: number;
},
sort:Record<string, string>,
filter:Record<string, string>) => getPolicyList(params,sort, filter)}
addNewBtnTitle={$t("添加策略")}
addNewBtnAccess="system.organization.member.edit"
onAddNewBtnClick={() => { addPolicy() }}
searchPlaceholder={$t("输入名称、筛选条件查找")}
afterNewBtn={
publishBtn && [<WithPermission key="removeFromDepPermission" access="system.organization.member.edit">
<Button className="mr-btnbase" key="removeFromDep" onClick={() => publish()}>{$t('发布')}</Button>
</WithPermission>]
}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
}}
manualReloadTable={manualReloadTable}
/>
<DrawerWithFooter
destroyOnClose={true}
title={$t('申请发布')}
width={'60%'}
onClose={()=>{setDrawerVisible(false)}}
okBtnTitle={$t('发布')}
open={drawerVisible}
submitDisabled={!isOkToPublish}
submitAccess={`team.service.release.add`}
onSubmit={onSubmit}
>
<PolicyPublishModalContent
ref={drawerRef}
data={drawerData! }
/>
</DrawerWithFooter>
</>
)
}
export default DataMasking;
@@ -0,0 +1,69 @@
import { PageProColumns } from "@common/components/aoplatform/PageList";
import { frontendTimeSorter } from "@common/utils/dataTransfer";
import { $t } from "@common/locales";
import { StrategyStatusEnum, StrategyStatusColorClass } from "@common/const/policy/consts";
export const DATA_MASSKING_TABLE_COLUMNS: PageProColumns<any>[] = [
{
title: ('策略名称'),
dataIndex: 'name',
ellipsis: true,
width: 160
},
{
title: ('优先级'),
dataIndex: 'priority',
width: 140,
ellipsis: true,
sorter: (a: any, b: any) => {
return (a.priority as number) - (b.priority as number)
}
},
{
title: ('发布状态'),
dataIndex: 'publishStatus',
filters: true,
onFilter: false ,
width: 140,
valueEnum: new Map(
Object.keys(StrategyStatusEnum).map(key=>
[key,
<span className={StrategyStatusColorClass[key as keyof typeof StrategyStatusColorClass]}>{$t(StrategyStatusEnum[key as keyof typeof StrategyStatusEnum])}</span>
]))
},
{
title: ('启用'),
dataIndex: 'isStop',
filters: true,
onFilter: false ,
valueEnum: {
false: { text: <span className="text-status_success">{$t('启用')}</span> },
true: { text: <span className="text-status_fail">{$t('禁用')}</span> }
}
},
{
title: ('筛选条件'),
dataIndex: 'filters',
ellipsis: true
},
{
title: ('处理数'),
dataIndex: 'processedTotal',
ellipsis: true
},
{
title: ('更新者'),
dataIndex: 'operator',
width: 140,
ellipsis: true
},
{
title: ('更新时间'),
dataIndex: 'updateTime',
width: 182,
ellipsis: true,
sorter: (a, b) => frontendTimeSorter(a, b, 'updateTime')
},
];
@@ -0,0 +1,171 @@
import { LoadingOutlined } from "@ant-design/icons"
import InsidePage from "@common/components/aoplatform/InsidePage"
import WithPermission from "@common/components/aoplatform/WithPermission"
import { BasicResponse, STATUS_CODE, RESPONSE_TIPS, PLACEHOLDER } from "@common/const/const"
import { RouterParams } from "@common/const/type"
import { useGlobalContext } from "@common/contexts/GlobalStateContext"
import { useFetch } from "@common/hooks/http"
import { $t } from "@common/locales"
import { App, Button, Form, Input, InputNumber, Row, Select, Spin } from "antd"
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"
import { useParams, useNavigate } from "react-router-dom"
import DataMaskRuleTable from "./DataMaskingRuleTable"
import FilterTable from "../FilterTable"
import { DataMaskingConfigHandle ,DataMaskingConfigFieldType} from "@common/const/policy/type"
import {PolicyOptions} from '@common/const/policy/consts'
const DataMaskingConfig = forwardRef<DataMaskingConfigHandle>((_,ref) => {
const { message,modal } = App.useApp()
const { teamId, serviceId, policyId } = useParams<RouterParams>();
const [onEdit, setOnEdit] = useState<boolean>(!!teamId)
const [form] = Form.useForm();
const {fetchData} = useFetch()
const { state } = useGlobalContext()
const [ loading, setLoading ] = useState<boolean>(false)
const navigator = useNavigate()
useImperativeHandle(ref, () => ({
save:onFinish
}));
// 获取表单默认值
const getPolicyInfo = () => {
setLoading(true)
fetchData<BasicResponse<{ strategy: DataMaskingConfigFieldType }>>( `strategy/${serviceId === undefined? 'global':'service'}/data-masking`,{method:'GET',eoParams:{team:teamId, service:serviceId, strategy:policyId}}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTimeout(()=>{
form.setFieldsValue({
...data.strategy,
type:'data-masking'
})
},0)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>setLoading(false))
};
const onFinish:()=>Promise<boolean|string> = () => {
return form.validateFields().then((value)=>{
return fetchData<BasicResponse<{service:{id:string}}>>(
`strategy/${serviceId === undefined? 'global':'service'}/data-masking`,
{
method:policyId === undefined? 'POST' : 'PUT',
eoParams: {service:serviceId,team:teamId, policyId:policyId},
eoBody:({...value})
}).then(response=>{
const {code,data,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((errorInfo)=>{
return Promise.reject(errorInfo)
})
})
};
useEffect(() => {
if (policyId !== undefined) {
setOnEdit(true);
getPolicyInfo();
} else {
setOnEdit(false);
form.setFieldValue('type','data-masking')
}
return (form.setFieldsValue({}))
}, [policyId]);
const policyOptions = useMemo(()=>PolicyOptions.map((x)=>({...x, label:$t(x.label)})),[state.language])
return (
<InsidePage pageTitle={ $t('编辑策略')|| '-'}
showBorder={false}
scrollPage={false}
className="overflow-y-auto"
>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} wrapperClassName=' pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X'>
<WithPermission access={onEdit ? ['team.service.service.edit'] :''}>
<Form
layout='vertical'
labelAlign='left'
scrollToFirstError
form={form}
className="w-full "
name="systemConfig"
onFinish={onFinish}
autoComplete="off"
>
<div>
<Form.Item<DataMaskingConfigFieldType>
label={$t("策略名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<DataMaskingConfigFieldType>
label={$t("策略类型")}
name="type"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={policyOptions} >
</Select>
</Form.Item>
<Form.Item<DataMaskingConfigFieldType>
label={$t("优先级")}
name={'priority'}
rules={[{required: true}]}
>
<InputNumber className="w-INPUT_NORMAL" min={1} placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item<DataMaskingConfigFieldType>
label={$t("描述")}
name="description"
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<DataMaskingConfigFieldType>
label={$t("匹配条件")}
name="filters"
>
<FilterTable />
</Form.Item>
<Form.Item<DataMaskingConfigFieldType>
label={$t("数据脱敏规则")}
name="rules"
rules={[{required: true}]}
>
<DataMaskRuleTable />
</Form.Item>
<Row className="mb-[10px]">
<WithPermission access={onEdit ? ['team.service.service.edit'] :''}>
<Button type="primary" htmlType="submit">
{$t('保存')}
</Button>
</WithPermission>
<Button className="ml-btnrbase" type="default" onClick={() => navigator('/globalpolicy/datamasking/list')}>
{$t('取消')}
</Button>
</Row>
</div>
</Form>
</WithPermission>
</Spin>
</InsidePage>
)
})
export default DataMaskingConfig
@@ -0,0 +1,158 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Form, Input, Select, Modal } from 'antd';
import { useGlobalContext } from '@common/contexts/GlobalStateContext';
import { $t } from '@common/locales';
import { PLACEHOLDER } from '@common/const/const';
import { v4 as uuidv4 } from 'uuid';
import { DataMaskRuleFormProps } from '@common/const/policy/type';
import { MatchRules, DataFormatOptions, DataMaskReplaceStrOptions, DataMaskBaseOptionOptions, DataMaskOrderOptions } from '@common/const/policy/consts';
const DataMaskRuleForm: React.FC<DataMaskRuleFormProps> = ({ editData, ruleList, onSave, onClose,modalVisible }) => {
const [form] = Form.useForm();
const [matchType, setMatchType] = useState<string>('');
const [matchValue, setMatchValue] = useState<string>('');
const [maskType, setMaskType] = useState<string>('');
const [replaceType, setReplaceType] = useState<string>('');
const {state} = useGlobalContext()
useEffect(() => {
if (editData) {
form.setFieldsValue(editData);
}
}, [editData, form]);
const handleSave = () => {
form.validateFields().then((values) => {
const submitData = prepareSubmitData(values);
const newRuleList =ruleList ? [...ruleList] : [];
if (editData) {
const index = newRuleList.findIndex((rule) => rule.eoKey === editData.eoKey);
if (index !== -1) {
newRuleList.splice(index, 1);
}
}
newRuleList.unshift({ ...submitData, eoKey: editData?.eoKey || uuidv4() });
onSave?.(newRuleList);
onClose?.();
clearData()
});
};
const clearData = ()=>{
form.resetFields()
setMatchType('');
setMaskType('');
setMatchValue('');
setReplaceType('');
}
useEffect(() => {
if (editData) {
form.setFieldsValue(editData);
editData?.match?.type && setMatchType(editData.match.type);
editData?.mask?.type && setMaskType(editData.mask.type);
editData?.match?.value && setMatchValue(editData.match.value);
editData?.mask?.replace?.type && setReplaceType(editData.mask.replace.type);
}
}, [editData, form]);
const handleMatchTypeChange = (value: string) => {
setMatchType(value);
form.resetFields(['match.value','mask.begin', 'mask.length', 'mask.replace.type', 'mask.replace.value']);
};
const handleMatchValueChange = (value: string) => {
setMatchValue(value);
form.resetFields(['mask.begin', 'mask.length', 'mask.replace.type', 'mask.replace.value']);
};
const handleMaskTypeChange = (value: string) => {
setMaskType(value);
form.resetFields(['mask.begin', 'mask.length', 'mask.replace.type', 'mask.replace.value']);
};
const handleReplaceTypeChange = (value: string) => {
setReplaceType(value);
form.resetFields(['mask.replace.value']);
};
const prepareSubmitData = (formData: any) => {
const submitData: any = {
match: {
type: formData.match.type,
value: formData.match.value
},
mask: {
type: formData.mask.type
}
};
switch (formData.mask.type) {
case 'replacement':
submitData.mask = {
...submitData.mask,
replace: formData.mask.replace
};
break;
case 'shuffling':
break;
default:
submitData.mask.begin = formData.mask.begin;
submitData.mask.length = formData.mask.length;
break;
}
return submitData;
};
const matchRuleOptions = useMemo(()=>MatchRules.map(rule => ({ label: $t(rule.label), value: rule.value })),[state.language])
const dataFormatOptions = useMemo(()=>DataFormatOptions.map(rule => ({ label: $t(rule.label), value: rule.value })),[state.language])
const dataMaskBaseOptions = useMemo(()=>DataMaskBaseOptionOptions.map(rule => ({ label: $t(rule.label), value: rule.value })),[state.language])
const dataMaskOrderOptions = useMemo(()=>DataMaskOrderOptions.map(rule => ({ label: $t(rule.label), value: rule.value })),[state.language])
const dataMaskReplaceStrOptions = useMemo(()=>DataMaskReplaceStrOptions.map(rule => ({ label: $t(rule.label), value: rule.value })),[state.language])
return (
<Modal open={modalVisible} onCancel={onClose} onOk={handleSave} title={$t("配置脱敏规则")}>
<Form form={form} layout="vertical" className="p-4">
<Form.Item name={['match', 'type']} label={$t("匹配类型")} rules={[{ required: true }]}>
<Select placeholder={$t(PLACEHOLDER.select)} onChange={handleMatchTypeChange} options={matchRuleOptions}/>
</Form.Item>
{ matchType && <Form.Item name={['match', 'value']} label={$t("匹配值")} rules={[{ required: true }]}>{
matchType === 'inner' ?
<Select placeholder={$t(PLACEHOLDER.select)} onChange={handleMatchValueChange} options={dataFormatOptions}/>
:<Input placeholder={$t(PLACEHOLDER.input)} />}
</Form.Item>
}
<Form.Item name={['mask', 'type']} label={$t("脱敏类型")} rules={[{ required: true }]}>
<Select placeholder={$t(PLACEHOLDER.select)} onChange={handleMaskTypeChange} options={ matchType && ['name', 'phone', 'id-card', 'bank-card'].indexOf(matchValue) !== -1 ? dataMaskOrderOptions:dataMaskBaseOptions} />
</Form.Item>
{['partial-display', 'partial-masking', 'truncation'].includes(maskType) && (
<>
<Form.Item name={['mask', 'begin']} label={$t("起始位置")} rules={[{ required: true }]}>
<Input type="number" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item name={['mask', 'length']} label={$t("长度")} rules={[{ required: true }]}>
<Input type="number" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
</>
)}
{maskType === 'replacement' && (
<>
<Form.Item name={['mask', 'replace', 'type']} label={$t("替换类型")} rules={[{ required: true }]}>
<Select placeholder={$t(PLACEHOLDER.select)} onChange={handleReplaceTypeChange} options={dataMaskReplaceStrOptions}/>
</Form.Item>
{replaceType === 'custom' && (
<Form.Item name={['mask', 'replace', 'value']} label={$t("替换值")} rules={[{ required: true }]}>
<Input placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
)}
</>
)}
</Form>
</Modal>
);
};
export default DataMaskRuleForm;
@@ -0,0 +1,152 @@
import { useMemo, useState } from 'react';
import { Button, Table, Tooltip } from 'antd';
import DataMaskRuleForm from './DataMaskingRuleForm';
import { $t } from '@common/locales';
import {DataMaskRuleTableProps, MaskRuleData} from "@common/const/policy/type";
import { useGlobalContext } from '@common/contexts/GlobalStateContext';
import { COLUMNS_TITLE } from '@common/const/const';
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission';
const DataMaskRuleTable: React.FC<DataMaskRuleTableProps> = ({
disabled = false,
value,
onChange
}) => {
const [editData, setEditData] = useState<MaskRuleData | undefined>(undefined);
const [isModalVisible, setIsModalVisible] = useState(false);
const {state} = useGlobalContext()
const openDrawer = (type:'add'|'edit',data?: MaskRuleData) => {
setEditData(data);
setIsModalVisible(true);
};
const closeDrawer = () => {
setIsModalVisible(false);
setEditData(undefined);
};
const handleSave = (newRuleList: MaskRuleData[]) => {
onChange?.(newRuleList);
};
const columns = useMemo(()=> [
{
title: $t('匹配类型'),
dataIndex: ['match', 'type'],
key: 'matchType',
render: (text: string) => {
switch (text) {
case 'inner':
return $t('数据格式');
case 'keyword':
return $t('关键字');
case 'regex':
return $t('正则表达式');
case 'json_path':
return $t('JSON Path');
default:
return text;
}
},
},
{
title: $t('匹配值'),
dataIndex: ['match', 'value'],
key: 'matchValue',
render: (text: string) => {
switch (text) {
case 'name':
return $t('姓名');
case 'phone':
return $t('手机号');
case 'id-card':
return $t('身份证号');
case 'bank-card':
return $t('银行卡号');
case 'date':
return $t('日期');
case 'amount':
return $t('金额');
default:
return text;
}
},
},
{
title: $t('脱敏类型'),
dataIndex: ['mask', 'type'],
key: 'maskType',
render: (text: string) => {
switch (text) {
case 'partial-display':
return $t('局部显示');
case 'partial-masking':
return $t('局部遮蔽');
case 'truncation':
return $t('截取');
case 'replacement':
return $t('替换');
case 'shuffling':
return $t('乱序');
default:
return text;
}
},
},
{
title: $t('脱敏规则'),
dataIndex: 'mask',
key: 'maskRule',
render: (mask: any) => {
switch (mask.type) {
case 'replacement':
return (
<Tooltip title={`${$t('类型')}${mask.replace.type === 'random' ? $t('随机字符串') : $t('自定义字符串; 值:')}${mask.replace.value}`}>
{$t('类型')}{mask.replace.type === 'random' ? $t('随机字符串') : $t('自定义字符串; 值:')}{mask.replace.value}
</Tooltip>
);
case 'shuffling':
return '-';
default:
return (
<Tooltip title={$t('起始位置:(0)位;长度:(1)位',[mask.begin,mask.length])}>
{$t('起始位置:(0)位;长度:(1)位',[mask.begin,mask.length])}
</Tooltip>
);
}
},
},
{
title: COLUMNS_TITLE.operate,
key: 'action',
render: (_: any, record: MaskRuleData) => (
<TableBtnWithPermission key="edit" btnType="edit" onClick={()=>{openDrawer('edit', record)}} btnTitle={$t("编辑")}/>
),
},
],[state.language])
return (
<div>
{
!disabled &&<Button onClick={() => openDrawer('add')}>
{$t('添加配置')}
</Button>
}
{value && value.length >0 && <Table
className={disabled ? '' : 'mt-btnbase'}
size='small'
pagination={false}
columns={columns} dataSource={value} rowKey="eoKey" /> }
<DataMaskRuleForm
editData={editData}
ruleList={value}
onSave={handleSave}
onClose={closeDrawer}
modalVisible = {isModalVisible}
/>
</div>
);
};
export default DataMaskRuleTable;
@@ -0,0 +1,36 @@
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
import { $t } from "@common/locales/index.ts";
import PolicyTabContainer from "./PolicyTabContainer.tsx";
import DataMasking from "./dataMasking/DataMasking.tsx";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { useMemo } from "react";
const PartitionInsideGlobalPolicy = () => {
const {state} = useGlobalContext()
/**
* tab列表
*/
const tabItems =useMemo(()=> [
{
key: 'dataMasking',
label: $t('数据脱敏'),
children: <div className="pr-[40px] preview-document h-full pb-[40px]"><DataMasking publishBtn rowOperation={['edit', 'logs', 'delete']} /></div>
}
],[state.language])
return (
<>
<InsidePage
pageTitle={$t('全局策略')}
description={$t("支持对系统全局进行统一的策略配置,从而简化管理并确保一致性。全局策略的优先级比服务策略略低。")}
showBorder={false}
scrollPage={false}
>
<PolicyTabContainer tabs={tabItems} />
</InsidePage>
</>
)
}
export default PartitionInsideGlobalPolicy
@@ -0,0 +1,19 @@
import { Tabs } from "antd";
const PolicyTabContainer = (props: any) => {
/**
* tab
*/
const { tabs } = props;
return (
<>
<Tabs
className="overflow-hidden h-full [&>.ant-tabs-content-holder]:overflow-auto global-policy-tabs"
items={tabs}
/>
</>
)
}
export default PolicyTabContainer;
@@ -0,0 +1,23 @@
import { $t } from "@common/locales/index.ts";
import DataMasking from "./dataMasking/DataMasking";
import PolicyTabContainer from "./PolicyTabContainer";
const servicePolicy = () => {
/**
* tab列表
*/
const tabItems = [
{
key: 'dataMasking',
label: $t('数据脱敏'),
children: <div className="pr-[40px] h-full preview-document mb-PAGE_INSIDE_B"><DataMasking rowOperation={['edit', 'logs', 'delete']} /></div>
}
]
return (
<>
<PolicyTabContainer tabs={tabItems} />
</>
)
}
export default servicePolicy;
@@ -1,11 +1,11 @@
import {FC, useEffect, useMemo, useState} from "react";
import {Link, Outlet, useLocation, useNavigate, useParams} from "react-router-dom";
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
import {App, Menu, MenuProps} from "antd";
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { useSystemContext} from "../../contexts/SystemContext.tsx";
import { FC, useEffect, useMemo, useState } from "react";
import { Link, Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
import { App, Menu, MenuProps } from "antd";
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const.tsx";
import { useFetch } from "@common/hooks/http.ts";
import { useSystemContext } from "../../contexts/SystemContext.tsx";
import { SystemConfigFieldType } from "../../const/system/type.ts";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { PERMISSION_DEFINITION } from "@common/const/permissions.ts";
@@ -17,144 +17,147 @@ import { $t } from "@common/locales/index.ts";
import { getItem } from "@common/utils/navigation.tsx";
const APP_MODE = import.meta.env.VITE_APP_MODE;
const SystemInsidePage:FC = ()=> {
const { message } = App.useApp()
const { teamId,serviceId,apiId,routeId} = useParams<RouterParams>();
const location = useLocation()
const currentUrl = location.pathname
const {fetchData} = useFetch()
const { setPrefixForce,setApiPrefix ,systemInfo,setSystemInfo} = useSystemContext()
const { accessData,checkPermission,accessInit,state} = useGlobalContext()
const [activeMenu, setActiveMenu] = useState<string>()
const navigateTo = useNavigate()
const [showMenu, setShowMenu] = useState<boolean>(false)
const SystemInsidePage: FC = () => {
const { message } = App.useApp()
const { teamId, serviceId, apiId, routeId } = useParams<RouterParams>();
const location = useLocation()
const currentUrl = location.pathname
const { fetchData } = useFetch()
const { setPrefixForce, setApiPrefix, systemInfo, setSystemInfo } = useSystemContext()
const { accessData, checkPermission, accessInit, state } = useGlobalContext()
const [activeMenu, setActiveMenu] = useState<string>()
const navigateTo = useNavigate()
const [showMenu, setShowMenu] = useState<boolean>(false)
const getSystemInfo = ()=>{
fetchData<BasicResponse<{ service:SystemConfigFieldType }>>('service/info',{method:'GET',eoParams:{team:teamId, service:serviceId}}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setSystemInfo(data.service)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const getSystemInfo = () => {
fetchData<BasicResponse<{ service: SystemConfigFieldType }>>('service/info', { method: 'GET', eoParams: { team: teamId, service: serviceId } }).then(response => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setSystemInfo(data.service)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const getApiDefine = ()=>{
setApiPrefix('')
setPrefixForce(false)
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)
setPrefixForce(data.force)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const getApiDefine = () => {
setApiPrefix('')
setPrefixForce(false)
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)
setPrefixForce(data.force)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const SYSTEM_PAGE_MENU_ITEMS = useMemo(()=>[
const SYSTEM_PAGE_MENU_ITEMS = useMemo(() => [
getItem($t('服务'), 'assets', null,
[
getItem(<Link to="./route">{$t('API 路由')}</Link>, 'route',undefined,undefined,undefined,'team.service.router.view'),
getItem(<Link to="./api">{$t('API 文档')}</Link>, 'api',undefined,undefined,undefined,'team.service.api_doc.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,'team.service.service_intro.view'),
getItem(<Link to="./publish">{$t('发布')}</Link>, 'publish',undefined,undefined,undefined,'team.service.release.view'),
],
'group'),
[
getItem(<Link to="./route">{$t('API 路由')}</Link>, 'route', undefined, undefined, undefined, 'team.service.router.view'),
getItem(<Link to="./api">{$t('API 文档')}</Link>, 'api', undefined, undefined, undefined, 'team.service.api_doc.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, 'team.service.service_intro.view'),
getItem(<Link to="./servicePolicy">{$t('服务策略')}</Link>, 'servicePolicy', undefined, undefined, undefined, 'team.service.service_intro.view'),
getItem(<Link to="./publish">{$t('发布')}</Link>, 'publish', undefined, undefined, undefined, 'team.service.release.view'),
],
'group'),
getItem($t('订阅管理'), 'provideSer', null,
[
getItem(<Link to="./approval">{$t('订阅审核')}</Link>, 'approval',undefined,undefined,undefined,'team.service.subscription.view'),
getItem(<Link to="./subscriber">{$t('订阅方管理')}</Link>, 'subscriber',undefined,undefined,undefined,'team.service.subscription.view'),
],
'group'),
[
getItem(<Link to="./approval">{$t('订阅审核')}</Link>, 'approval', undefined, undefined, undefined, 'team.service.subscription.view'),
getItem(<Link to="./subscriber">{$t('订阅方管理')}</Link>, 'subscriber', undefined, undefined, undefined, 'team.service.subscription.view'),
],
'group'),
getItem($t('管理'), 'mng', null,
[
APP_MODE === 'pro' ? getItem(<Link to="./topology">{$t('调用拓扑图')}</Link>, 'topology',undefined,undefined,undefined,'project.mySystem.topology.view'):null,
// APP_MODE === 'pro' ? getItem(<Link to="./topology">{$t('调用拓扑图')}</Link>, 'topology',undefined,undefined,undefined,'project.mySystem.topology.view'):null,
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting',undefined,undefined,undefined,'')],
'group'),
],[state.language])
const menuData = useMemo(()=>{
const filterMenu = (menu:MenuItemGroupType<MenuItemType>[])=>{
const newMenu = cloneDeep(menu)
return newMenu!.filter((m:MenuItemGroupType )=>{
if(m&&m.children && m.children.length > 0){
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]):
true))})
}
return m.children && m.children.length > 0
const menuData = useMemo(() => {
const filterMenu = (menu: MenuItemGroupType<MenuItemType>[]) => {
const newMenu = cloneDeep(menu)
return newMenu!.filter((m: MenuItemGroupType) => {
if (m && m.children && m.children.length > 0) {
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]) :
true))
})
}
const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType<MenuItemType>[])
const menu = activeMenu ?? filteredMenu[0]?.children ? filteredMenu[0]?.children?.[0]?.key : filteredMenu[0]?.key
if(menu && currentUrl.split('/')[-1] !== menu) navigateTo(`/service/${teamId}/inside/${serviceId}/${menu}`)
return filteredMenu || []
},[accessData,accessInit, SYSTEM_PAGE_MENU_ITEMS])
const onMenuClick: MenuProps['onClick'] = ({key}) => {
setActiveMenu(key)
};
useEffect(() => {
setShowMenu(!routeId && !currentUrl.includes('route/create'))
if(apiId !== undefined){
setActiveMenu('api')
}else if(serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]){
setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1])
}else{
setActiveMenu('route')
}
}, [currentUrl]);
return m.children && m.children.length > 0
})
}
const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType<MenuItemType>[])
const menu = activeMenu ?? filteredMenu[0]?.children ? filteredMenu[0]?.children?.[0]?.key : filteredMenu[0]?.key
if (menu && currentUrl.split('/')[-1] !== menu) navigateTo(`/service/${teamId}/inside/${serviceId}/${menu}`)
return filteredMenu || []
}, [accessData, accessInit, SYSTEM_PAGE_MENU_ITEMS])
useEffect(()=>{
if(accessData && checkPermission('team.service.router.view')){
getApiDefine()
}
},[accessData])
const onMenuClick: MenuProps['onClick'] = ({ key }) => {
setActiveMenu(key)
};
useEffect(()=>{
if( activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]){
navigateTo(`/service/${teamId}/inside/${serviceId}/${activeMenu}`)
}
},[activeMenu])
useEffect(() => {
setShowMenu(!routeId && !currentUrl.includes('route/create'))
if (apiId !== undefined) {
setActiveMenu('api')
} else if (serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]) {
setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1])
} else {
setActiveMenu('route')
}
}, [currentUrl]);
useEffect(() => {
serviceId && getSystemInfo()
}, [serviceId]);
useEffect(() => {
if (accessData && checkPermission('team.service.router.view')) {
getApiDefine()
}
}, [accessData])
return (
<>{showMenu ?
<InsidePage pageTitle={systemInfo?.name || '-'}
tagList={[{label:
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>{$t('服务 ID')}{serviceId || '-'}</Paragraph>
}]}
backUrl="/service/list">
<div className="flex flex-1 h-full">
<Menu
onClick={onMenuClick}
className="h-full overflow-y-auto"
style={{ width: 220 }}
selectedKeys={[activeMenu!]}
mode="inline"
items={menuData as unknown as ItemType<MenuItemType>[] }
/>
<div className={` ${['setting', 'upstream'].indexOf(activeMenu!) !== -1 ? '' :''} w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B ` }>
<Outlet/>
</div>
</div>
</InsidePage>: <Outlet/> }
useEffect(() => {
if (activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]) {
navigateTo(`/service/${teamId}/inside/${serviceId}/${activeMenu}`)
}
}, [activeMenu])
</>
)
useEffect(() => {
serviceId && getSystemInfo()
}, [serviceId]);
return (
<>{showMenu ?
<InsidePage pageTitle={systemInfo?.name || '-'}
tagList={[{
label:
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>{$t('服务 ID')}{serviceId || '-'}</Paragraph>
}]}
backUrl="/service/list">
<div className="flex flex-1 h-full">
<Menu
onClick={onMenuClick}
className="h-full overflow-y-auto"
style={{ width: 220 }}
selectedKeys={[activeMenu!]}
mode="inline"
items={menuData as unknown as ItemType<MenuItemType>[]}
/>
<div className={` ${['setting', 'upstream'].indexOf(activeMenu!) !== -1 ? '' : ''} w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B `}>
<Outlet />
</div>
</div>
</InsidePage> : <Outlet />}
</>
)
}
export default SystemInsidePage
@@ -1,18 +1,16 @@
import {App, Button, Col, Form, Input, Row, Select, Space, 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_PATH_MATCH_RULES, 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 { MatchItem, RouterParams } 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";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
import { useNavigate, useParams } from "react-router-dom";
import { useSystemContext } from "@core/contexts/SystemContext.tsx";
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
@@ -236,8 +234,6 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle,Syste
configFields={translatedMatchConfig}
/>
</Form.Item>
{/* } */}
<Row className="mb-btnybase mt-[40px]"><Col ><span className="font-bold mr-[13px]">{$t('转发规则设置')} </span></Col></Row>
<Form.Item<SystemApiProxyFieldType>
@@ -4,10 +4,9 @@ import { useState, useRef, useEffect, useMemo, FC } from "react";
import { useParams, Link, useLocation } from "react-router-dom";
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList";
import { PublishApprovalModalContent } from "@common/components/aoplatform/PublishApprovalModalContent";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes";
import { PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN, PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN, PublishApplyStatusEnum, PublishStatusEnum, PublishTableStatusColorClass } from "@common/const/approval/const";
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
import { SimpleMemberItem } from "@common/const/type.ts";
import { RouterParams, SimpleMemberItem } from "@common/const/type.ts";
import { MemberTableListItem } from "../../../const/member/type";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext";
import { useFetch } from "@common/hooks/http";
@@ -79,7 +79,7 @@ const SystemInsideUpstreamContent= forwardRef<SystemInsideUpstreamContentHandle>
if(code === STATUS_CODE.SUCCESS){
setTimeout(()=>{
form.setFieldsValue({...DEFAULT_FORM_VALUE,...data.upstream})
setFormShowHost(data.upstream.passHost === 'rewrite')
setFormShowHost(data.upstream?.passHost === 'rewrite')
},0)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
@@ -31,7 +31,6 @@ const TeamList:FC = ()=>{
const [curTeam, setCurTeam] = useState<TeamConfigFieldType>({} as TeamConfigFieldType)
const [modalVisible, setModalVisible] = useState<boolean>(false)
const [modalType, setModalType] = useState<'add'|'edit'>('add')
const getTeamList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then?.(()=>{getTeamList()})
+13 -1
View File
@@ -5,6 +5,7 @@ import path from 'path'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import federation from "@originjs/vite-plugin-federation";
export default defineConfig({
cacheDir: './node_modules/.vite',
@@ -18,7 +19,7 @@ export default defineConfig({
chunkFileNames: 'assets/eo-[name]-[hash].js',
},
},
},
},
css: {
postcss: {
plugins: [
@@ -42,6 +43,17 @@ export default defineConfig({
exclude:[],
warnOnError:false
}),
federation({
name:"container",
remotes:{
remoteApp: 'http://localhost:5001/assets/remoteEntry.js' // 远程项目的URL
},
shared:[
"react",
"react-dom",
]
})
],
resolve: {
alias: [
@@ -16,8 +16,6 @@ const TableType = {
provider :SERVICE_TABLE_GLOBAL_COLUMNS_CONFIG,
subscribers :APPLICATION_TABLE_GLOBAL_COLUMNS_CONFIG
}
const APP_MODE = import.meta.env.VITE_APP_MODE;
type MonitorTableProps<T> = {
type:'api'|'subscribers'|'provider'
@@ -77,13 +75,12 @@ const MonitorTable = forwardRef<MonitorTableHandler, MonitorTableProps<unknown>>
{
title: COLUMNS_TITLE.operate,
key: 'option',
btnNums:2,
btnNums:1,
fixed:'right',
hideInSetting:true,
valueType: 'option',
render: (_: React.ReactNode, entity: unknown) => [
// <TableBtnWithPermission access="system.dashboard.self.view" key="view" onClick={()=>onRowClick(entity)} btnTitle="查看"/>,
APP_MODE === 'pro' ? <TableBtnWithPermission access="" key="view" btnType="view" onClick={()=>onRowClick(entity)} btnTitle="查看"/> : null
<TableBtnWithPermission access="" key="view" btnType="view" onClick={()=>onRowClick(entity)} btnTitle="查看"/>
],
}
]
@@ -1,13 +1,11 @@
import { App, Select, Button, Tabs, TabsProps, Empty, Drawer, Spin } from "antd";
import { App, Button, Tabs, TabsProps, Empty, Drawer, Spin } from "antd";
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { useState, useEffect, useRef, useReducer } from "react";
import { useParams } from "react-router-dom";
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
import { SummaryPieData, SearchBody, PieData, MonitorApiData, MonitorSubscriberData, InvokeData, MessageData } from "@dashboard/const/type";
import { getTime, getTimeUnit, changeNumberUnit } from "../utils/dashboard";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes";
import ScrollableSection from "@common/components/aoplatform/ScrollableSection";
import { RangeValue, TimeRange } from "@common/components/aoplatform/TimeRangeSelector";
import TimeRangeSelector from "@common/components/aoplatform/TimeRangeSelector";
@@ -19,7 +17,6 @@ import DashboardDetail from "@dashboard/pages/DashboardDetail";
import { $t } from "@common/locales";
dayjs.extend(customParseFormat);
const APP_MODE = import.meta.env.VITE_APP_MODE;
export type MonitorTotalPageProps = {
fetchPieData:(body:SearchBody)=>Promise<BasicResponse<PieData>>
@@ -209,17 +206,17 @@ const MonitorTotalPage = (props:MonitorTotalPageProps) => {
{
label:$t('API 请求量 Top10'),
key:'api',
children:<MonitorTable className="py-[10px]" ref={monitorApiTableRef} type='api' id="dashboard_top10_api" onRowClick={(record)=>{APP_MODE !== 'pro' ? null : getDetailData(record as MonitorApiData,'api')}} request={()=>getTablesData(queryData||{},'api')}/>
children:<MonitorTable className="py-[10px]" ref={monitorApiTableRef} type='api' id="dashboard_top10_api" onRowClick={(record)=>{ getDetailData(record as MonitorApiData,'api')}} request={()=>getTablesData(queryData||{},'api')}/>
},
{
label:$t('消费者调用量 Top10'),
key:'subscribers',
children:<MonitorTable className="py-[10px]" ref={monitorSubTableRef} type='subscribers' id="dashboard_top10_subscriber" onRowClick={(record)=>{APP_MODE !== 'pro' ? null : getDetailData(record as MonitorSubscriberData,'subscriber')}} request={()=>getTablesData(queryData||{},'subscriber')} />
children:<MonitorTable className="py-[10px]" ref={monitorSubTableRef} type='subscribers' id="dashboard_top10_subscriber" onRowClick={(record)=>{getDetailData(record as MonitorSubscriberData,'subscriber')}} request={()=>getTablesData(queryData||{},'subscriber')} />
},
{
label:$t('服务被调用量 Top10'),
key:'providers',
children:<MonitorTable className="py-[10px]" ref={monitorSubTableRef} type='provider' id="dashboard_top10_provider" onRowClick={(record)=>{APP_MODE !== 'pro' ? null : getDetailData(record as MonitorSubscriberData,'provider')}} request={()=>getTablesData(queryData||{},'provider')} />
children:<MonitorTable className="py-[10px]" ref={monitorSubTableRef} type='provider' id="dashboard_top10_provider" onRowClick={(record)=>{getDetailData(record as MonitorSubscriberData,'provider')}} request={()=>getTablesData(queryData||{},'provider')} />
}
]
@@ -2,11 +2,10 @@
import { Tabs, TabsProps } from "antd";
import DashboardTotal from "./DashboardTotal";
import { Outlet, useNavigate, useParams } from "react-router-dom";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes";
import { useEffect, useState } from "react";
import { $t } from "@common/locales";
import { RouterParams } from "@common/const/type";
const APP_MODE = import.meta.env.VITE_APP_MODE;
export default function DashboardTabPage(){
const { dashboardType} = useParams<RouterParams>()
@@ -41,10 +40,10 @@ export default function DashboardTabPage(){
]
return (<>
{APP_MODE === 'pro' ? <Tabs activeKey={activeKey} onChange={(val)=>{
<Tabs activeKey={activeKey} onChange={(val)=>{
setActiveKey(val);
navigateTo(`/analytics/${val === 'total' ? val :`${val}/list`}`)
}}
items={monitorTabItems} className="h-auto mt-[6px]" size="small" tabBarStyle={{paddingLeft:'10px',marginTop:'0px',marginBottom:'0px'}} />
: <Outlet />} </>)
items={monitorTabItems} className="h-full overflow-hidden mt-[6px] [&>.ant-tabs-content-holder]:overflow-auto" size="small" tabBarStyle={{paddingLeft:'10px',marginTop:'0px',marginBottom:'0px'}} />
</>)
}
@@ -18,6 +18,7 @@ export type ServiceBasicInfoType = {
invokeAddress:string
approvalType:'auto'|'manual'
serviceKind:'ai'|'rest'
sitePrefix?:string
}
export type ServiceDetailType = {
+1
View File
@@ -335,6 +335,7 @@ p{
background-color:transparent
}
/* .hidden-switcher .ant-tree-switcher {
@apply hidden;
} */
@@ -1,191 +1,198 @@
import {Link, useNavigate, useParams} from "react-router-dom";
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
import { App, Avatar, Button, Descriptions, Divider, Tabs} from "antd";
import { useEffect, useMemo, useRef, useState} from "react";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {DefaultOptionType} from "antd/es/cascader";
import { Link, useNavigate, useParams } from "react-router-dom";
import { App, Avatar, Button, Descriptions, Divider, Tabs } from "antd";
import { useEffect, useRef, useState } from "react";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx";
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const.tsx";
import { useFetch } from "@common/hooks/http.ts";
import { DefaultOptionType } from "antd/es/cascader";
import { ApplyServiceHandle, ServiceBasicInfoType, ServiceDetailType } from "../../const/serviceHub/type.ts";
import { EntityItem } from "@common/const/type.ts";
import { EntityItem, RouterParams } from "@common/const/type.ts";
import { ApplyServiceModal } from "./ApplyServiceModal.tsx";
import ServiceHubApiDocument from "./ServiceHubApiDocument.tsx";
import { ApiFilled, ArrowLeftOutlined } from "@ant-design/icons";
import { SimpleSystemItem } from "@core/const/system/type.ts";
import Integrate from "./integrate.tsx";
import { ApiFilled, ArrowLeftOutlined, BgColorsOutlined } from "@ant-design/icons";
import { Icon } from "@iconify/react/dist/iconify.js";
import DOMPurify from 'dompurify';
import { $t } from "@common/locales/index.ts";
import { approvalTypeTranslate } from "@market/const/serviceHub/const.tsx";
const ServiceHubDetail = ()=>{
const {serviceId} = useParams<RouterParams>();
const {setBreadcrumb} = useBreadcrumb()
const [serviceBasicInfo, setServiceBasicInfo] = useState<ServiceBasicInfoType>()
const [serviceName, setServiceName] = useState<string>()
const [serviceDesc, setServiceDesc] = useState<string>()
const [serviceDoc, setServiceDoc] = useState<string>()
const {fetchData} = useFetch()
const applyRef = useRef<ApplyServiceHandle>(null)
const { modal,message } = App.useApp()
const [mySystemOptionList, setMySystemOptionList] = useState<DefaultOptionType[]>()
// const [applied,setApplied] = useState<boolean>(false)
// const [activeKey, setActiveKey] = useState<string[]>([])
const [service, setService] = useState<ServiceDetailType>()
const navigate = useNavigate();
const ServiceHubDetail = () => {
const { serviceId } = useParams<RouterParams>();
const { setBreadcrumb } = useBreadcrumb()
const [serviceBasicInfo, setServiceBasicInfo] = useState<ServiceBasicInfoType>()
const [serviceName, setServiceName] = useState<string>()
const [serviceDesc, setServiceDesc] = useState<string>()
const [serviceDoc, setServiceDoc] = useState<string>()
const { fetchData } = useFetch()
const applyRef = useRef<ApplyServiceHandle>(null)
const { modal, message } = App.useApp()
const [mySystemOptionList, setMySystemOptionList] = useState<DefaultOptionType[]>()
// const [applied,setApplied] = useState<boolean>(false)
// const [activeKey, setActiveKey] = useState<string[]>([])
const [service, setService] = useState<ServiceDetailType>()
const navigate = useNavigate();
const modifyApiDoc = (apiDoc:string, apiPrefix:string)=>{
if(!apiDoc) return ''
if(!apiPrefix) return apiDoc
try{
const openApiSpec = JSON.parse(apiDoc);
// 遍历并修改 paths,给每个路径添加前缀
const modifiedPaths:Record<string,unknown> = {};
for (const [path, pathItem] of Object.entries(openApiSpec.paths)) {
modifiedPaths[apiPrefix + path] = pathItem;
}
openApiSpec.paths = modifiedPaths;
return JSON.stringify(openApiSpec);
}catch(err){
console.warn('拼接api前缀失败',err)
}
return apiDoc
const modifyApiDoc = (apiDoc: string, apiPrefix: string) => {
if (!apiDoc) return ''
if (!apiPrefix) return apiDoc
try {
const openApiSpec = JSON.parse(apiDoc);
// 遍历并修改 paths,给每个路径添加前缀
const modifiedPaths: Record<string, unknown> = {};
for (const [path, pathItem] of Object.entries(openApiSpec.paths)) {
modifiedPaths[apiPrefix + path] = pathItem;
}
openApiSpec.paths = modifiedPaths;
return JSON.stringify(openApiSpec);
} catch (err) {
console.warn('拼接api前缀失败', err)
}
return apiDoc
}
const getServiceBasicInfo = ()=>{
fetchData<BasicResponse<{service:ServiceDetailType}>>('catalogue/service',{method:'GET',eoParams:{service:serviceId}, eoTransformKeys:['app_num','api_num','update_time','api_doc','invoke_address','approval_type','service_kind']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setService({...data.service,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))
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
const getServiceBasicInfo = () => {
fetchData<BasicResponse<{ service: ServiceDetailType }>>('catalogue/service', { method: 'GET', eoParams: { service: serviceId }, eoTransformKeys: ['app_num', 'api_num', 'update_time', 'api_doc', 'invoke_address', 'approval_type', 'service_kind','site_prefix'] }).then(response => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setService({ ...data.service, 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))
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
useEffect(() => {
if (!serviceId) {
console.warn('缺少serviceId')
return
}
serviceId && getServiceBasicInfo()
}, [serviceId]);
useEffect(() => {
if(!serviceId){
console.warn('缺少serviceId')
return
}
serviceId && getServiceBasicInfo()
}, [serviceId]);
useEffect(() => {
getMySelectList()
setBreadcrumb(
[
{title:<Link to={`/serviceHub/list`}>{$t('服务市场')}</Link>},
{title:$t('服务详情')}
]
)
}, []);
const getMySelectList = ()=>{
setMySystemOptionList([])
fetchData<BasicResponse<{ app: EntityItem[] }>>('apps/can_subscribe',{method:'GET'}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setMySystemOptionList(data.app?.map((x:EntityItem)=>{return {
label:x.name, value:x.id
}}))
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const openModal = (type:'apply')=>{
modal.confirm({
title:$t('申请服务'),
content:<ApplyServiceModal ref={applyRef} entity={{...serviceBasicInfo!, name:serviceName!, id:serviceId!}} mySystemOptionList={mySystemOptionList!}/>,
onOk:()=>{
return applyRef.current?.apply().then((res)=>{
// if(res === true) setApplied(true)
})
},
okText:$t('确认'),
cancelText:$t('取消'),
closable:true,
icon:<></>,
width:600
})
}
const items = [
{
key: 'introduction',
label: $t('介绍'),
children: <><div className="p-btnbase preview-document mb-PAGE_INSIDE_B" dangerouslySetInnerHTML={{__html: serviceDoc || ''}}></div></>,
icon: <Icon icon="ic:baseline-space-dashboard" width="14" height="14"/>,
},
{
key: 'api-document',
label: $t('API 文档'),
children: <div className={`p-btnbase ${serviceBasicInfo?.serviceKind?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}><ServiceHubApiDocument service={service!} /></div>,
icon: <ApiFilled />
}
]
return (
<section className=" grid grid-cols-5 h-full mr-PAGE_INSIDE_X">
<section className="col-span-4 border-0 border-r-[1px] border-solid border-BORDER flex flex-col overflow-hidden">
<section className="flex flex-col gap-btnbase p-btnbase ">
<div className="text-[18px] leading-[25px] pb-[12px]">
<Button type="text" onClick={()=>navigate(`/serviceHub/list`)}><ArrowLeftOutlined className="max-h-[14px]" />{$t('返回')}</Button>
</div>
<div className="flex">
{/* <Avatar shape="square" size={50} className=" bg-[linear-gradient(135deg,white,#f0f0f0)] text-[#333] rounded-[12px]" > {service?.name?.substring(0,1)}</Avatar> */}
<Avatar shape="square" size={50}
className={ `rounded-[12px] border-none rounded-[12px] ${ serviceBasicInfo?.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`}
src={ serviceBasicInfo?.logo ? <img src={serviceBasicInfo?.logo} alt="Logo" style={{ maxWidth: '200px', width:'45px',height:'45px',objectFit:'unset'}}
/> : undefined}
icon={serviceBasicInfo?.logo ? '' :<iconpark-icon name="auto-generate-api"></iconpark-icon>}> </Avatar>
<div className="pl-[20px] w-[calc(100%-50px)]">
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold flex items-center gap-[4px]">{serviceName}
</p>
<div className="mt-[10px] flex flex-col gap-btnrbase font-normal">
<p>{serviceDesc || '-'}</p>
<p className="flex items-center gap-[4px]"><Icon icon="ic:baseline-link" width="18" height="18" /><span className="font-bold">{$t('Base URL')}</span>: {serviceBasicInfo?.invokeAddress || '-'}</p>
<div>
<Button type="primary" onClick={()=>openModal('apply')}>{$t('申请')}</Button>
</div>
</div>
</div>
</div>
</section>
<Tabs
className="p-btnbase pr-0 overflow-hidden [&>.ant-tabs-content-holder]:overflow-auto"
items={items}
/>
</section>
<section className="col-span-1 p-btnbase px-btnrbase">
<Descriptions title={$t("服务信息")} column={1} size={'small'}>
<Descriptions.Item label={$t("接入消费者")}>{serviceBasicInfo?.appNum ?? '-'}</Descriptions.Item>
<Descriptions.Item label={$t("供应方")}>{serviceBasicInfo?.team?.name || '-'}</Descriptions.Item>
<Descriptions.Item label={$t("审核")}>{serviceBasicInfo?.approvalType ? $t((approvalTypeTranslate[serviceBasicInfo?.approvalType] || '-' )): '-'}</Descriptions.Item>
<Descriptions.Item label={$t("分类")}>{serviceBasicInfo?.catalogue?.name || '-'}</Descriptions.Item>
<Descriptions.Item label={$t("标签")}>{serviceBasicInfo?.tags?.map(x=>x.name)?.join(',') || '-'}</Descriptions.Item>
</Descriptions>
<Divider />
<Descriptions column={1} >
<Descriptions.Item label={$t("版本")}>{ serviceBasicInfo?.version || '-'}</Descriptions.Item>
<Descriptions.Item label={$t("更新时间")}><span className="truncate" title={serviceBasicInfo?.updateTime}>{serviceBasicInfo?.updateTime || '-'}</span></Descriptions.Item>
</Descriptions>
</section>
</section>
useEffect(() => {
getMySelectList()
setBreadcrumb(
[
{ title: <Link to={`/serviceHub/list`}>{$t('服务市场')}</Link> },
{ title: $t('服务详情') }
]
)
}, []);
const getMySelectList = () => {
setMySystemOptionList([])
fetchData<BasicResponse<{ app: EntityItem[] }>>('apps/can_subscribe', { method: 'GET' }).then(response => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setMySystemOptionList(data.app?.map((x: EntityItem) => {
return {
label: x.name, value: x.id
}
}))
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const openModal = (type: 'apply') => {
modal.confirm({
title: $t('申请服务'),
content: <ApplyServiceModal ref={applyRef} entity={{ ...serviceBasicInfo!, name: serviceName!, id: serviceId! }} mySystemOptionList={mySystemOptionList!} />,
onOk: () => {
return applyRef.current?.apply().then((res) => {
// if(res === true) setApplied(true)
})
},
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>,
width: 600
})
}
const items = [
{
key: 'introduction',
label: $t('介绍'),
children: <><div className="p-btnbase preview-document mb-PAGE_INSIDE_B" dangerouslySetInnerHTML={{ __html: serviceDoc || '' }}></div></>,
icon: <Icon icon="ic:baseline-space-dashboard" width="14" height="14" />,
},
{
key: 'api-document',
label: $t('API 文档'),
children: <div className={`p-btnbase ${serviceBasicInfo?.serviceKind?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}><ServiceHubApiDocument service={service!} /></div>,
icon: <ApiFilled />
},
{
key: 'api-integrate',
label: $t('集成'),
children: <div className={`p-btnbase ${serviceBasicInfo?.serviceKind?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}><Integrate service={service!} /></div>,
icon: <BgColorsOutlined />
}
]
return (
<section className=" grid grid-cols-5 h-full mr-PAGE_INSIDE_X">
<section className="col-span-4 border-0 border-r-[1px] border-solid border-BORDER flex flex-col overflow-hidden">
<section className="flex flex-col gap-btnbase p-btnbase ">
<div className="text-[18px] leading-[25px] pb-[12px]">
<Button type="text" onClick={() => navigate(`/serviceHub/list`)}><ArrowLeftOutlined className="max-h-[14px]" />{$t('返回')}</Button>
</div>
<div className="flex">
{/* <Avatar shape="square" size={50} className=" bg-[linear-gradient(135deg,white,#f0f0f0)] text-[#333] rounded-[12px]" > {service?.name?.substring(0,1)}</Avatar> */}
<Avatar shape="square" size={50}
className={`rounded-[12px] border-none rounded-[12px] ${serviceBasicInfo?.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`}
src={serviceBasicInfo?.logo ? <img src={serviceBasicInfo?.logo} alt="Logo" style={{ maxWidth: '200px', width: '45px', height: '45px', objectFit: 'unset' }}
/> : undefined}
icon={serviceBasicInfo?.logo ? '' : <iconpark-icon name="auto-generate-api"></iconpark-icon>}> </Avatar>
<div className="pl-[20px] w-[calc(100%-50px)]">
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold flex items-center gap-[4px]">{serviceName}
</p>
<div className="mt-[10px] flex flex-col gap-btnrbase font-normal">
<p>{serviceDesc || '-'}</p>
<p className="flex items-center gap-[4px]"><Icon icon="ic:baseline-link" width="18" height="18" /><span className="font-bold">{$t('Base URL')}</span>: {serviceBasicInfo?.invokeAddress || '-'}</p>
<div>
<Button type="primary" onClick={() => openModal('apply')}>{$t('申请')}</Button>
</div>
</div>
</div>
</div>
</section>
<Tabs
className="p-btnbase pr-0 overflow-hidden [&>.ant-tabs-content-holder]:overflow-auto"
items={items}
/>
</section>
<section className="col-span-1 p-btnbase px-btnrbase">
<Descriptions title={$t("服务信息")} column={1} size={'small'}>
<Descriptions.Item label={$t("接入消费者")}>{serviceBasicInfo?.appNum ?? '-'}</Descriptions.Item>
<Descriptions.Item label={$t("供应方")}>{serviceBasicInfo?.team?.name || '-'}</Descriptions.Item>
<Descriptions.Item label={$t("审核")}>{serviceBasicInfo?.approvalType ? $t((approvalTypeTranslate[serviceBasicInfo?.approvalType] || '-')) : '-'}</Descriptions.Item>
<Descriptions.Item label={$t("分类")}>{serviceBasicInfo?.catalogue?.name || '-'}</Descriptions.Item>
<Descriptions.Item label={$t("标签")}>{serviceBasicInfo?.tags?.map(x => x.name)?.join(',') || '-'}</Descriptions.Item>
</Descriptions>
<Divider />
<Descriptions column={1} >
<Descriptions.Item label={$t("版本")}>{serviceBasicInfo?.version || '-'}</Descriptions.Item>
<Descriptions.Item label={$t("更新时间")}><span className="truncate" title={serviceBasicInfo?.updateTime}>{serviceBasicInfo?.updateTime || '-'}</span></Descriptions.Item>
</Descriptions>
</section>
</section>
)
}
export default ServiceHubDetail
@@ -0,0 +1,71 @@
import { ServiceDetailType } from "@market/const/serviceHub/type"
import { Input, Button, Space, message } from 'antd'
import { $t } from "@common/locales"
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useParams } from "react-router-dom"
import { useState, useEffect } from "react"
import { RouterParams } from "@common/const/type"
import { useFetch } from "@common/hooks/http"
const Integrate = ({ service }: { service: ServiceDetailType }) => {
const stepClass = "leading-[20px] truncate font-bold items-center gap-[4px] mt-[15px]";
const [url, setUrl] = useState('');
const { serviceId} = useParams<RouterParams>()
const {fetchData} = useFetch()
useEffect(()=>{
setUrl(`${service?.basic?.sitePrefix || window.location?.origin}/${serviceId}/swagger` )
},[service])
/**
* Agent
*/
const agentAddress = '/cluster';
/**
*
*/
const consumerAddress = '/consumer/list';
/**
*
*/
const copyURL = async (): Promise<void> => {
await navigator.clipboard.writeText(url)
message.success($t(RESPONSE_TIPS.copySuccess))
}
/**
*
*/
const onDownload = () => {
fetchData<BasicResponse<null>>(`export/openapi/${serviceId}`, { method: 'GET' ,headers:{
'Content-Type': 'application/octet-stream'
}}).then(response => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
return (
<div>
<div>{$t('支持把当前服务对接主流的 AI Agent平台,实现在 Agent 平台上快速、安全和合规地使用企业开放的 API 能力。')}</div>
<div className='my-[10px]'>{$t('可按以下步骤进行对接:')}</div>
<p className={stepClass}>{$t('步骤一:Agent 平台上创建自定义插件')}</p>
<div className="my-[10px]">{$t('不同 Agent 平台的操作细节可查看')} <a href={agentAddress} target="_blank">{$t('《 Agent 对接手册》')}</a></div>
<p className={stepClass}>{$t('步骤二:导入 API 文档数据')}</p>
<div className='my-[10px]'>{$t('可通过以下 URL 或 下载 Json 文件,导入 API 文档数据到 Agent 平台中。')}</div>
<div className="flex w-full items-center gap-[30px]">
<Space.Compact className=" flex-1 ">
<Input disabled value={url} />
<Button type="primary" onClick={copyURL}>{$t('复制 URL')}</Button>
</Space.Compact>
<span className="text-[14px] font-bold">OR</span>
<Button onClick={onDownload}>{$t('下载 Json 文件')}</Button>
</div>
<p className={stepClass}>{$t('步骤三:配置 API 密钥')}</p>
<div className='my-[10px]'>{$t('在')}<a href={consumerAddress} target="_blank"> {$t('消费者')} </a>{$t('菜单中,选择已通过本 API 服务申请的消费者,')}</div>
<div className='my-[10px]'>{$t('把 "访问权限" 菜单下的密钥填入到 Agent 平台对应的插件密钥配置中。')}</div>
</div>
);
}
export default Integrate;
@@ -78,7 +78,6 @@ export const ManagementAuthorityConfig = forwardRef<ManagementAuthorityConfigHan
useEffect(() => {
//console.log(data)
if(type === 'edit' && data){
form.setFieldsValue({...data,expireTime:data.expireTime === 0 ? '' : dayjs(data.expireTime * 1000)})
forceUpdate({})
@@ -110,7 +110,7 @@ export default function ServiceHubManagement() {
}))
);
if (!teamId && data.teams?.[0]?.id) {
navigateTo(data.teams[0].id);
navigateTo(`/consumer/list/${data.teams[0].id}`);
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error));

Some files were not shown because too many files have changed in this diff Show More