feat: Global/Service Policy Development, Add Service Details Integration Tab

This commit is contained in:
lcx
2024-11-15 10:44:16 +08:00
parent 3e4d2b069a
commit 9d9dbd0048
18 changed files with 2638 additions and 1808 deletions
@@ -1,13 +1,14 @@
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 { 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';
@@ -24,262 +25,263 @@ 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 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'),
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 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'),
getNavItem($t('API 市场'), 'serviceHub', '/serviceHub', <Icon icon="ic:baseline-hub" width="18" height="18" />, undefined, undefined, 'system.api_portal.api_portal.view'),
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,
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>, 'globalPolicy', '/globalPolicy', <Icon icon="uil:comment-shield" 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])
], [state.language, accessInit])
useEffect(() => {
if(currentUrl === '/'){
navigator(mainPage)
}
}, [currentUrl]);
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;
}
// 如果没有 access 和 routes,则保留
return item;
})
.filter(x => x); // 过滤掉处理后为 null 的项
};
// 初始过滤操作
const res = [...TOTAL_MENU_ITEMS]!.filter(x => x).map((x: any) => (x.routes ? { ...x, routes: filterMenu(x.routes) } : x));
// 返回处理后的数据
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]);
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))
}
})
useEffect(() => {
if (currentUrl === '/') {
navigator(mainPage)
}
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))
}, [currentUrl]);
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;
}
// 如果没有 access 和 routes,则保留
return item;
})
}
.filter(x => x); // 过滤掉处理后为 null 的项
};
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 res = [...TOTAL_MENU_ITEMS]!.filter(x => x).map((x: any) => (x.routes ? { ...x, routes: filterMenu(x.routes) } : x));
// 返回处理后的数据
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]);
return(
<div
id="test-pro-layout"
style={{
height: '100vh',
overflow: 'auto',
}}
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))
}
})
}
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))
}
})
}
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]);
return (
<div
id="test-pro-layout"
style={{
height: '100vh',
overflow: 'auto',
}}
>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
return document.getElementById('test-pro-layout') || document.body;
}}
>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
return document.getElementById('test-pro-layout') || document.body;
<ProLayout
prefixCls="apipark-layout"
location={{
pathname,
}}
siderWidth={220}
breakpoint={'lg'}
route={headerMenuData}
token={themeToken}
siderMenuType="group"
menu={{
type: 'group',
collapsedShowGroupTitle: true,
}}
disableMobile={true}
avatarProps={{
src: AvatarPic || userInfo?.avatar,
size: 'small',
title: userInfo?.username || 'unknown',
render: (props, dom) => {
return (
<Dropdown
menu={{
items
}}
>
<ProLayout
prefixCls="apipark-layout"
location={{
pathname,
}}
siderWidth={220}
breakpoint={'lg'}
route={headerMenuData}
token={themeToken}
siderMenuType="group"
menu={{
type: 'group',
collapsedShowGroupTitle: true,
}}
disableMobile={true}
avatarProps={{
src: AvatarPic || userInfo?.avatar,
size: 'small',
title: userInfo?.username||'unknown',
render: (props, dom) => {
return (
<Dropdown
menu={{
items
}}
>
<div className='avatar-dom'>{dom}
</div>
</Dropdown>
);
},
}}
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>
];
}}
headerTitleRender={() => (
<div className="w-[192px] flex items-center">
<img
className="h-[20px] cursor-pointer "
src={Logo}
onClick={()=> navigator(mainPage)}
/>
</div>
)}
logo={Logo}
pageTitleRender={()=>$t('APIPark')}
menuFooterRender={(props) => {
if (props?.collapsed) return undefined;
}}
menuItemRender={(item, dom) => (
<div
onClick={() => {
// 同级目录点击无效
if(item.key && routerKeyMap.get(item.key) && routerKeyMap.get(item.key).length > 0 && routerKeyMap.get(item.key)?.indexOf(pathname.split('/')[1]) !== -1){
return
}
if(item.key === pathname.split('/')[1]){
return
}
if(item.path){
navigator(item.path)
}
setPathname(item.path || '');
}}
>
{dom}
</div>
)}
fixSiderbar={true}
layout='mix'
splitMenus={true}
collapsed={false}
collapsedButtonRender={false}
>
<div className={`w-full h-calc-100vh-minus-navbar pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T ${currentUrl.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden' }`}>
<Outlet />
>
<div className='avatar-dom'>{dom}
</div>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</div>
)
</Dropdown>
);
},
}}
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>
];
}}
headerTitleRender={() => (
<div className="w-[192px] flex items-center">
<img
className="h-[20px] cursor-pointer "
src={Logo}
onClick={() => navigator(mainPage)}
/>
</div>
)}
logo={Logo}
pageTitleRender={() => $t('APIPark')}
menuFooterRender={(props) => {
if (props?.collapsed) return undefined;
}}
menuItemRender={(item, dom) => (
<div
onClick={() => {
// 同级目录点击无效
if (item.key && routerKeyMap.get(item.key) && routerKeyMap.get(item.key).length > 0 && routerKeyMap.get(item.key)?.indexOf(pathname.split('/')[1]) !== -1) {
return
}
if (item.key === pathname.split('/')[1]) {
return
}
if (item.path) {
navigator(item.path)
}
setPathname(item.path || '');
}}
>
{dom}
</div>
)}
fixSiderbar={true}
layout='mix'
splitMenus={true}
collapsed={false}
collapsedButtonRender={false}
>
<div className={`w-full h-calc-100vh-minus-navbar pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T ${currentUrl.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden'}`}>
<Outlet />
</div>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</div>
)
}
export default BasicLayout
@@ -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,10 @@ export const TranslateWord = ()=>{
{$t('地址')}
{$t('新增')}
{$t('申请方消费者')}
{$t('策略名称')}
{$t('优先级')}
{$t('筛选条件')}
{$t('处理数')}
</>
)
}
@@ -18,6 +18,7 @@
"角色": "Kf644225f",
"集成": "K4057391a",
"数据源": "K8fa58214",
"全局策略": "Ke8cbb878",
"证书": "K481e8a05",
"日志": "Kca53edd0",
"资源": "Kb283e720",
@@ -488,6 +489,14 @@
"统计图表": "K1358acf",
"地址(IP:端口)": "K62dabdf6",
"组织(Organization": "K2db12335",
"添加策略": "K34d0d409",
"输入名称、筛选条件查找": "Kbb4298ac",
"策略名称": "K931615d7",
"优先级": "K31faa2a1",
"筛选条件": "Kbdec9fa",
"处理数": "Kbcbb7391",
"数据脱敏": "Kabac9caf",
"支持对系统全局进行统一的策略配置,从而简化管理并确保一致性。全局策略的优先级比服务策略略低。": "Kc975cd5a",
"资源配置": "K8e7a0f80",
"设置角色的权限范围。": "K95c3fd8b",
"系统级别角色": "K138facd3",
@@ -523,6 +532,7 @@
"删除服务": "Kde6bae17",
"删除操作不可恢复,请谨慎操作!": "K885ea699",
"上游": "Kda8d5ea1",
"服务策略": "K52f72551",
"服务提供了高性能 API 网关,并且可以无缝接入多种大型 AI 模型,并将这些 AI 能力打包成 API 进行调用,从而大幅简化了 AI 模型的使用门槛。同时,我们的平台提供了完善的 API 管理功能,支持 API 的创建、监控、访问控制等,保障开发者可以高效、安全地开发和管理 API 服务。": "K12f58863",
"添加服务": "K2d6658ed",
"输入名称、ID、所属团队、负责人查找服务": "K7b8f623f",
@@ -616,6 +626,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",
@@ -679,5 +679,29 @@
"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"
}
@@ -701,5 +701,29 @@
"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": "編集"
}
File diff suppressed because it is too large Load Diff
@@ -701,5 +701,29 @@
"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": "編輯"
}
File diff suppressed because it is too large Load Diff
+13
View File
@@ -1005,6 +1005,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;
@@ -0,0 +1,463 @@
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 { DATA_MASSKING_TABLE_COLUMNS } from './dataMaskingColumn'
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";
const DataMasking = (props: any) => {
const {
// 是否显示发布按钮
publishBtn = false,
// 行操作
rowOperation = []
} = props;
const { checkPermission, getGlobalAccessData, accessInit, state } = useGlobalContext()
/**
* ref
*/
const pageListRef = useRef<ActionType>(null);
/**
*
*/
const [tableHttpReload, setTableHttpReload] = useState(true);
/**
*
*/
const [tableListDataSource, setTableListDataSource] = useState<ServiceHubAppListItem[]>([]);
/**
*
*/
const { fetchData } = useFetch()
/**
*
*/
const [searchWord, setSearchWord] = useState<string>('')
/**
*
*/
const columns = useMemo(() => {
const res = DATA_MASSKING_TABLE_COLUMNS.map(x => {
// 启动列渲染
if (x.dataIndex === 'enabled') {
x.render = (text: any, record: any) => <Switch checked={record.enabled} 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>
}
// 名称筛选,这里是全量返回时候的,分页的话应该要接口返回对应的筛选数据
if (x.dataIndex === 'name') {
const nameList = tableListDataSource.map(item => item.name)
const valueEnum: any = {}
nameList.forEach(item => {
valueEnum[item] = { text: item }
})
x.valueEnum = valueEnum
}
return {
...x,
title: typeof x.title === 'string' ? $t(x.title as string) : x.title
}
})
return res
}, [tableListDataSource, 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') ? [<TableBtnWithPermission access="system.organization.member.edit" key="delete" btnType="delete" onClick={() => { deletePolicy(entity) }} btnTitle="删除" />] : []),
],
}
] : []
/**
*
*/
const manualReloadTable = () => {
setTableHttpReload(true)
pageListRef.current?.reload()
};
/**
*
* @param enabled
* @param entity
*/
const changeOpenApiStatus = (enabled: boolean, entity: any) => {
console.log('更改启动状态', enabled, entity);
manualReloadTable()
// fetchData<BasicResponse<null>>(
// `external-app/${enabled ? 'disable' : 'enable'}`,
// {
// method: 'PUT',
// eoParams: {
// id: 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 getServiceList = () => {
if (!accessInit) {
getGlobalAccessData()?.then?.(() => { getServiceList() })
return Promise.resolve({ data: [], success: false })
}
if (!tableHttpReload) {
setTableHttpReload(true)
return Promise.resolve({
data: tableListDataSource,
success: true,
});
}
return fetchData<BasicResponse<any>>(
!checkPermission('system.workspace.team.view_all') ? 'teams' : 'manager/teams',
{
method: 'GET',
eoParams: { keyword: searchWord },
eoTransformKeys: ['create_time', 'service_num', 'can_delete']
}
).then(response => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const data = [
{
name: 'test',
priority: 1,
status: true,
enabled: true,
condition: 'test',
treatmentNumber: 1,
updater: 'test',
createTime: '2021-10-01'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
},
{
name: 'test2',
priority: 2,
status: false,
enabled: false,
condition: 'test2',
treatmentNumber: 2,
updater: 'test2',
createTime: '2021-10-02'
}
]
// 保存数据
setTableListDataSource(data)
setTableHttpReload(false)
return {
data,
success: true
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return { data: [], success: false }
}
}).catch(() => {
return { data: [], success: false }
})
}
/**
*
* @param type
*/
const addPolicy = () => {
console.log('添加策略');
}
/**
*
*/
const publish = () => {
console.log('发布策略');
}
/**
*
*/
const openEditModal = (entity: any) => {
console.log('编辑', entity);
}
/**
*
* @param entity
*/
const openLogsModal = (entity: any) => {
console.log('日志', entity);
}
/**
*
* @param entity
*/
const deletePolicy = (entity: any) => {
console.log('删除', entity);
manualReloadTable()
}
return (
<>
<PageList
id="data_masking_list"
ref={pageListRef}
columns={[...columns, ...operation]}
request={() => getServiceList()}
addNewBtnTitle={$t("添加策略")}
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}
onChange={() => {
setTableHttpReload(false)
}}
/>
</>
)
}
export default DataMasking;
@@ -0,0 +1,70 @@
import { PageProColumns } from "@common/components/aoplatform/PageList";
import { frontendTimeSorter } from "@common/utils/dataTransfer";
import { $t } from "@common/locales";
export const DATA_MASSKING_TABLE_COLUMNS: PageProColumns<any>[] = [
{
title: ('策略名称'),
dataIndex: 'name',
ellipsis: true,
filters: true,
onFilter: true,
valueType: 'select',
filterSearch: 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: 'status',
filters: true,
onFilter: true,
width: 140,
valueEnum: new Map([
[true, <span className="text-status_success">{$t('已发布')}</span>],
[false, <span className="text-status_fail">{$t('未发布')}</span>]
])
},
{
title: ('启用'),
dataIndex: 'enabled',
filters: true,
onFilter: true,
valueEnum: {
true: { text: <span className="text-status_success">{$t('启用')}</span> },
false: { text: <span className="text-status_fail">{$t('禁用')}</span> }
}
},
{
title: ('筛选条件'),
dataIndex: 'condition',
ellipsis: true
},
{
title: ('处理数'),
dataIndex: 'treatmentNumber',
ellipsis: true
},
{
title: ('更新者'),
dataIndex: 'updater',
width: 140,
ellipsis: true
},
{
title: ('更新时间'),
dataIndex: 'createTime',
width: 182,
ellipsis: true,
sorter: (a, b) => frontendTimeSorter(a, b, 'createTime')
},
];
@@ -0,0 +1,33 @@
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
import { $t } from "@common/locales/index.ts";
import PolicyTabContainer from "./policyTabContainer.tsx";
import DataMasking from "./dataMasking.tsx";
const PartitionInsideGlobalPolicy = () => {
/**
* tab列表
*/
const tabItems = [
{
key: 'dataMasking',
label: $t('数据脱敏'),
children: <div className="pr-[40px] preview-document h-full pb-[40px]"><DataMasking publishBtn rowOperation={['edit', 'logs', 'delete']} /></div>
}
]
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 "@core/pages/policy/dataMasking";
import PolicyTabContainer from "@core/pages/policy/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,
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting',undefined,undefined,undefined,'')],
'group'),
],[state.language])
[
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,16 +1,17 @@
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 { 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 { ApplyServiceHandle, ServiceBasicInfoType, ServiceDetailType } from "../../const/serviceHub/type.ts";
import { EntityItem } from "@common/const/type.ts";
import { ApplyServiceModal } from "./ApplyServiceModal.tsx";
import ServiceHubApiDocument from "./ServiceHubApiDocument.tsx";
import { ApiFilled, ArrowLeftOutlined } from "@ant-design/icons";
import Integrate from "./integrate.tsx";
import { ApiFilled, ArrowLeftOutlined, BgColorsOutlined } from "@ant-design/icons";
import { SimpleSystemItem } from "@core/const/system/type.ts";
import { Icon } from "@iconify/react/dist/iconify.js";
import DOMPurify from 'dompurify';
@@ -18,174 +19,182 @@ 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'] }).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,62 @@
import { ServiceDetailType } from "@market/const/serviceHub/type"
import { Input, Button, Space, message } from 'antd'
import { $t } from "@common/locales"
import { RESPONSE_TIPS } from '@common/const/const'
import { downloadFile } from "@common/utils/download.ts"
const Integrate = ({ service }: { service: ServiceDetailType }) => {
console.log('service', service);
const stepClass = "leading-[20px] truncate font-bold items-center gap-[4px] mt-[15px]";
const url = 'https://www.baidu.com';
/**
* 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 = () => {
console.log('downloadFile');
downloadFile({
body: '',
contentType: 'raw',
filename: 'test_response',
responseType: 'text',
uri: ''
})
}
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>
<Space.Compact className="w-[500px]">
<Input disabled defaultValue={url} />
<Button type="primary" onClick={copyURL}>{$t('复制 URL')}</Button>
</Space.Compact>
<span className="text-[14px] font-bold mx-[30px]">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;