feat: Complete static pages for Phase 1 of V1.3

This commit is contained in:
杨梦洁
2024-11-21 16:08:08 +08:00
parent 0b7f0405d5
commit dce9a7addb
47 changed files with 2035 additions and 609 deletions
@@ -75,21 +75,20 @@ const themeToken = {
if (filteredRoutes.length === 0) {
return false
}
return { ...item, routes: filteredRoutes };
return { ...item,routes: filteredRoutes,name:$t(item.name) };
}
// 处理没有 routes 的菜单项
if (item.access) {
return (item.access === 'all' || hasAccess(item.access)) ? item : null;
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 = [...(menuItems || [])]!.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,menuItems]);
@@ -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>
</>)
})
@@ -163,6 +163,22 @@ export const TranslateWord = ()=>{
{$t('优先级')}
{$t('筛选条件')}
{$t('处理数')}
{$t('数据格式')}
{$t('关键字')}
{$t('正则表达式')}
{$t('手机号')}
{$t('身份证号')}
{$t('银行卡号')}
{$t('金额')}
{$t('日期')}
{$t('局部显示')}
{$t('局部遮蔽')}
{$t('截取')}
{$t('替换')}
{$t('乱序')}
{$t('随机字符串')}
{$t('自定义字符串')}
{$t('请输入IP地址或CIDR范围,每条以换行分割')}
</>
)
}
+33 -8
View File
@@ -1,10 +1,5 @@
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 { TenantManagementProvider } from '@market/contexts/TenantManagementContext'
import { lazy } from 'react'
import { Outlet, Navigate } from 'react-router-dom'
import { $t } from '@common/locales'
import { StrategyStatusColorClass, StrategyStatusEnum } from './policy/consts'
export type BasicResponse<T> = {
code:number
@@ -24,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']],
@@ -52,6 +47,7 @@ export const routerKeyMap = new Map<string, string[]|string>([
startWithAlphabet:('英文数字下划线任意一种,首字母必须为英文'),
specialStartWithAlphabet:('支持字母开头、英文数字中横线下划线组合'),
onlyAlphabet:('字符非法,仅支持英文'),
ipAndCidr:'请输入IP地址或CIDR范围,每条以换行分割'
}
export const FORM_ERROR_TIPS = {
@@ -81,3 +77,32 @@ export const routerKeyMap = new Map<string, string[]|string>([
{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
}
+27 -1
View File
@@ -3,6 +3,7 @@ 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
@@ -141,6 +142,7 @@ export type RouterParams = {
roleType:string
roleId:string
routeId:string
policyId:string
}
@@ -185,4 +187,28 @@ export type RouterMapConfig = {
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>>
}
@@ -91,14 +91,14 @@ export type GlobalAction =
},
{
"name": "仪表盘",
"key": "mainPage",
"key": "analytics",
"path": "/analytics",
"icon": "ic:baseline-bar-chart",
"children": [
{
"name": "运行视图",
"key": "analytics",
"path": "/analytics/total",
"path": "/analytics",
"icon": "ic:baseline-bar-chart",
"access": "system.analysis.run_view.view"
}
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
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';
@@ -21,12 +21,9 @@ const languageMap: Record<string, Locale> = {
const LocaleContext = createContext<{
locale: Locale;
setLocale: (locale: string) => void;
}>({
locale: zhCN,
setLocale: () => {},
});
}|undefined>(undefined);
export const LocaleProvider: React.FC = ({ children }) => {
export const LocaleProvider: FC<{children:ReactNode}> = ({ children }) => {
const [locale, setLocaleState] = useState<Locale>(zhCN);
const setLocale = (language: string) => {
@@ -41,4 +38,10 @@ export const LocaleProvider: React.FC = ({ children }) => {
);
};
export const useLocaleContext = () => useContext(LocaleContext);
export const useLocaleContext = () => {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('useLocaleContext must be used within a LocaleContext');
}
return context;
};
@@ -1,32 +1,8 @@
{
"工作空间": "Kc0e5ef9f",
"首页": "K4de11e23",
"服务": "Kb58e0c3f",
"消费者": "K7acfcfad",
"团队": "Kc9e489f5",
"API 市场": "K61c89f5f",
"仪表盘": "K16d71239",
"运行视图": "K714c192d",
"系统拓扑图": "Kd57dfe97",
"系统设置": "K3fe97dcc",
"系统": "Kecbb0e45",
"常规": "Ka358e23d",
"API 网关": "K449058e9",
"AI 模型": "K99935e6f",
"用户": "K1deaa2dd",
"账号": "K80a560a1",
"角色": "Kf644225f",
"集成": "K4057391a",
"数据源": "K8fa58214",
"全局策略": "Ke8cbb878",
"证书": "K481e8a05",
"日志": "Kca53edd0",
"资源": "Kb283e720",
"Open API": "K631d646f",
"账号设置": "K6535ff9c",
"退出登录": "Kf15499b4",
"文档": "Kabbd6e6",
"APIPark - 企业API数据开放平台": "K1196b104",
"APIPark": "K630c9e6d",
"HTTP 状态码": "K1f42de3",
"系统状态码": "K4770dff4",
"描述": "Kf89e58f1",
@@ -44,6 +20,8 @@
"ID": "K11d3633a",
"名称": "Kbff43de3",
"Driver": "K16ca79ef",
"资源": "Kb283e720",
"日志": "Kca53edd0",
"已发布": "K7a369eef",
"下线": "Kcfa1a4d2",
"上线": "K771dc3b7",
@@ -51,6 +29,8 @@
"删除": "Kecbd7449",
"确认": "K1cbe2507",
"搜索(0)名称": "K48325b6",
"发布名称": "Ka3e9f580",
"策略列表": "Kb2480682",
"上下文": "Kc6340091",
"查询内容": "K74ecb1fa",
"会话历史": "K79f2e2f9",
@@ -235,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",
@@ -322,11 +322,14 @@
"至": "K7e1ab4b0",
"详情": "Kf1b166e7",
"暂不支持带有双斜杠//的url": "K28555332",
"输入的IP或CIDR不符合格式": "K83237c89",
"请正确输入路径,如/usr/*或*/usr/*": "K5ae2c87a",
"必填项": "K71661ee8",
"不是有效邮箱地址": "Kcbee3f8",
"最近一次更新者": "K617f34f1",
"最近一次更新时间": "K6ebca204",
"保存": "Kabfe9512",
"服务": "Kb58e0c3f",
"API 路由": "K51d1eb5d",
"API 文档": "Ka2b6d281",
"使用说明": "Kdefa9caa",
@@ -402,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",
@@ -414,6 +419,7 @@
"如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。": "Kf2410413",
"审核订阅申请": "K6c2e44b8",
"提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。": "Kaa717866",
"集成": "K4057391a",
"APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。": "K3453272",
"Hello!欢迎使用 APIPark": "Kd518ba3e",
"你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。": "K7e04ea16",
@@ -425,6 +431,7 @@
"了解更多功能": "K48f7e21f",
"隐藏该教程": "K698296e2",
"请输入账号": "Kf076f63c",
"账号": "K80a560a1",
"请输入密码": "K25c895d5",
"密码": "K551b0348",
"登录": "Kd2c1a316",
@@ -467,6 +474,7 @@
"密钥": "K8ef69ee2",
"上传密钥": "Kba3507d6",
"密钥文件的后缀名一般为 .key 的文件内容": "K93ac0f23",
"证书": "K481e8a05",
"上传证书": "K7cdd1331",
"证书文件的后缀名一般为 .crt 或 .pem 的文件内容": "K6d91905d",
"添加证书": "Kd0f6ded7",
@@ -485,19 +493,39 @@
"同步地址": "K2a49373f",
"集群地址": "K5878440c",
"下一步": "K5e9022f8",
"数据源": "K8fa58214",
"设置监控报表的数据来源,设置完成之后即可获得详细的API调用统计图表。": "Kdbafd6f9",
"统计图表": "K1358acf",
"地址(IP:端口)": "K62dabdf6",
"组织(Organization": "K2db12335",
"添加策略": "K34d0d409",
"输入名称、筛选条件查找": "Kbb4298ac",
"策略名称": "K931615d7",
"优先级": "K31faa2a1",
"筛选条件": "Kbdec9fa",
"处理数": "Kbcbb7391",
"编辑策略": "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",
@@ -578,6 +606,7 @@
"退出全屏": "Kaf70c3b",
"(0)调用详情": "Kd22841a4",
"消费者调用统计": "K61cca533",
"消费者": "K7acfcfad",
"请选择消费者": "Kdfff59d4",
"调用趋势": "K8c7f2d2e",
"(0)-(1)调用趋势": "K657c3452",
@@ -610,6 +639,7 @@
"暂无调用量统计数据": "Kcd125e4d",
"暂无报文量统计数据": "Kaa114e8b",
"报文量统计": "K3ad84406",
"运行视图": "K714c192d",
"集群配置并开启监控": "Kfa088d49",
"监控功能用于辅助管理集群内信息,请配置集群、设置监控信息后查看当前集群监控情况;": "K3da3b9a0",
"集群配置": "Kaddacfb",
@@ -699,6 +729,7 @@
"配置 Open Api": "K7829bb78",
"Open Api": "Kcdf76005",
"调用服务": "Ke2601944",
"系统拓扑图": "Kd57dfe97",
"放大": "K8504bca8",
"缩小": "K693c1b41"
}
@@ -703,5 +703,46 @@
"K31faa2a1": "Priority",
"Kbdec9fa": "Filter Criteria",
"Kbcbb7391": "Processed Count",
"Kad207008": "Edit"
"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)"
}
@@ -725,5 +725,46 @@
"K31faa2a1": "優先度",
"Kbdec9fa": "フィルタ条件",
"Kbcbb7391": "処理数",
"Kad207008": "編集"
"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。",
@@ -725,5 +725,46 @@
"K31faa2a1": "優先級",
"Kbdec9fa": "篩選條件",
"Kbcbb7391": "處理數",
"Kad207008": "編輯"
"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)"
}
@@ -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;
}
}
+43 -4
View File
@@ -172,7 +172,7 @@ import { Outlet, Navigate } from 'react-router-dom';
{
path:'servicepolicy',
key: 'servicePolicy',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/servicePolicy')),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/ServicePolicy')),
children:[
]
@@ -266,7 +266,7 @@ import { Outlet, Navigate } from 'react-router-dom';
{
path:'servicepolicy',
key: 'servicePolicy',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/servicePolicy')),
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/ServicePolicy')),
children:[
]
@@ -413,6 +413,23 @@ import { Outlet, Navigate } from 'react-router-dom';
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',
@@ -449,6 +466,28 @@ import { Outlet, Navigate } from 'react-router-dom';
key:'changePsw'
}]}],
['globalPolicy', { type: 'module',
lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/policy/globalPolicy.tsx')),
key:'globalPolicy'}]
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
},
+6 -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;
+3
View File
@@ -3,6 +3,7 @@ import {StrictMode} from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { LocaleProvider } from '@common/contexts/LocaleContext.tsx';
async function initializeApp() {
try {
// 初始化行为
@@ -11,7 +12,9 @@ async function initializeApp() {
// 异步操作完成后,渲染React消费者
ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<LocaleProvider>
<App />
</LocaleProvider>
</StrictMode>,
);
} catch (error) {
@@ -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>)
}
@@ -1,463 +0,0 @@
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,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;
@@ -2,16 +2,14 @@
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,
filters: true,
onFilter: true,
valueType: 'select',
filterSearch: true,
width: 160
},
{
@@ -25,46 +23,47 @@ export const DATA_MASSKING_TABLE_COLUMNS: PageProColumns<any>[] = [
},
{
title: ('发布状态'),
dataIndex: 'status',
dataIndex: 'publishStatus',
filters: true,
onFilter: true,
onFilter: false ,
width: 140,
valueEnum: new Map([
[true, <span className="text-status_success">{$t('已发布')}</span>],
[false, <span className="text-status_fail">{$t('未发布')}</span>]
])
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: 'enabled',
dataIndex: 'isStop',
filters: true,
onFilter: true,
onFilter: false ,
valueEnum: {
true: { text: <span className="text-status_success">{$t('启用')}</span> },
false: { text: <span className="text-status_fail">{$t('禁用')}</span> }
false: { text: <span className="text-status_success">{$t('启用')}</span> },
true: { text: <span className="text-status_fail">{$t('禁用')}</span> }
}
},
{
title: ('筛选条件'),
dataIndex: 'condition',
dataIndex: 'filters',
ellipsis: true
},
{
title: ('处理数'),
dataIndex: 'treatmentNumber',
dataIndex: 'processedTotal',
ellipsis: true
},
{
title: ('更新者'),
dataIndex: 'updater',
dataIndex: 'operator',
width: 140,
ellipsis: true
},
{
title: ('更新时间'),
dataIndex: 'createTime',
dataIndex: 'updateTime',
width: 182,
ellipsis: true,
sorter: (a, b) => frontendTimeSorter(a, b, 'createTime')
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;
@@ -1,20 +1,23 @@
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";
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 = [
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 (
<>
@@ -1,6 +1,6 @@
import { $t } from "@common/locales/index.ts";
import DataMasking from "@core/pages/policy/dataMasking";
import PolicyTabContainer from "@core/pages/policy/policyTabContainer";
import DataMasking from "./dataMasking/DataMasking";
import PolicyTabContainer from "./PolicyTabContainer";
const servicePolicy = () => {
/**
@@ -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";
@@ -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,18 +1,16 @@
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 { 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 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';
import { $t } from "@common/locales/index.ts";
@@ -54,7 +52,7 @@ const ServiceHubDetail = () => {
}
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 => {
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) })
@@ -1,15 +1,21 @@
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"
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 }) => {
console.log('service', service);
const stepClass = "leading-[20px] truncate font-bold items-center gap-[4px] mt-[15px]";
const url = 'https://www.baidu.com';
const [url, setUrl] = useState('');
const { serviceId} = useParams<RouterParams>()
const {fetchData} = useFetch()
useEffect(()=>{
setUrl(`${service?.basic?.sitePrefix || window.location?.origin}/${serviceId}/swagger` )
},[service])
/**
* Agent
*/
@@ -29,13 +35,15 @@ const Integrate = ({ service }: { service: ServiceDetailType }) => {
*
*/
const onDownload = () => {
console.log('downloadFile');
downloadFile({
body: '',
contentType: 'raw',
filename: 'test_response',
responseType: 'text',
uri: ''
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 (
@@ -46,12 +54,13 @@ const Integrate = ({ service }: { service: ServiceDetailType }) => {
<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} />
<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 mx-[30px]">OR</span> <Button onClick={onDownload}>{$t('下载 Json 文件')}</Button>
<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>