fix: delete files & width of pic in editor & timing bug

This commit is contained in:
maggieyyy
2024-10-18 18:05:47 +08:00
parent 27294a10a7
commit 67deef3c4f
19 changed files with 26 additions and 1493 deletions
@@ -110,7 +110,7 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => {
updateDate: '2024-07-01',
powered:'Powered by https://apipark.com',
mainPage:'/guide/page',
language:'en'
language:'en-US'
});
const [accessData,setAccessData] = useState<Map<string,string[]>>(new Map())
const [pluginAccessDictionary, setPluginAccessDictionary] = useState<{[k:string]:string}>({})
@@ -6,8 +6,12 @@ import crc32 from 'crc/crc32';
// 引入需要实现国际化的简体、繁体、英文三种数据的json文件
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
import jaJP from 'antd/locale/ja_JP';
import zhTW from 'antd/locale/zh_TW';
import localZh_CN from './scan/zh-CN.json'; // 本地翻译中文文件
import localEn_US from './scan/en-US.json'; // 本地翻译英文文件
import localZh_TW from './scan/zh-TW.json'; // 本地翻译英文文件
import localJa_JP from './scan/ja-JP.json'; // 本地翻译英文文件
// import config from '../../../../i18next-scanner.config.js';
const resources = {
@@ -18,6 +22,14 @@ const resources = {
'en-US': {
translation: localEn_US,
...enUS
},
'zh-TW': {
translation: localZh_TW,
...zhTW
},
'ja-JP': {
translation: localJa_JP,
...jaJP
}
};
@@ -1,416 +0,0 @@
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { App, Button, Form, Input, Radio, Row, Select, TreeSelect, Upload } from "antd";
import { Link, useNavigate, useParams } from "react-router-dom";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
import { BasicResponse, DELETE_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const.tsx";
import { useFetch} from "@common/hooks/http.ts";
import { DefaultOptionType } from "antd/es/cascader";
import { EntityItem, MemberItem, SimpleTeamItem } from "@common/const/type.ts";
import { v4 as uuidv4 } from 'uuid'
import { validateUrlSlash } from "@common/utils/validate.ts";
import { normFile } from "@common/utils/uploadPic.ts";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx";
import { SERVICE_VISUALIZATION_OPTIONS } from "@core/const/system/const.tsx";
import { RcFile, UploadChangeParam, UploadFile, UploadProps } from "antd/es/upload/interface";
import { LoadingOutlined } from "@ant-design/icons";
import { getImgBase64 } from "@common/utils/dataTransfer.ts";
import { CategorizesType } from "@market/const/serviceHub/type.ts";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { $t } from "@common/locales/index.ts";
import { AiServiceConfigHandle, AiServiceConfigFieldType } from "@core/const/ai-service/type";
import { useAiServiceContext } from "@core/contexts/AiServiceContext";
type SimpleAiProviderItem = EntityItem & {
configured:boolean
logo:string
}
const AiServiceConfig = forwardRef<AiServiceConfigHandle>((_,ref) => {
const { message,modal } = App.useApp()
const { teamId, serviceId } = useParams<RouterParams>();
const [onEdit, setOnEdit] = useState<boolean>(!!teamId)
const [form] = Form.useForm();
const {fetchData} = useFetch()
const [teamOptionList, setTeamOptionList] = useState<DefaultOptionType[]>()
const [providerOptionList, setProviderOptionList] = useState<DefaultOptionType[]>()
const navigate = useNavigate();
const {setBreadcrumb} = useBreadcrumb()
const { setAiServiceInfo} = useAiServiceContext()
const [showClassify, setShowClassify] = useState<boolean>()
const [imageBase64, setImageBase64] = useState<string | null>(null);
const [tagOptionList, setTagOptionList] = useState<DefaultOptionType[]>([])
const [serviceClassifyOptionList, setServiceClassifyOptionList] = useState<DefaultOptionType[]>()
const [uploadLoading, setUploadLoading] = useState<boolean>(false)
const {checkPermission,accessInit, getGlobalAccessData,state, aiConfigFlushed, setAiConfigFlushed} = useGlobalContext()
useImperativeHandle(ref, () => ({
save:onFinish
}));
const beforeUpload = async (file: RcFile) => {
if (!['image/png', 'image/jpeg', 'image/svg+xml'].includes(file.type)) {
alert($t('只允许上传PNG、JPG或SVG格式的图片'));
return false;
}
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
setImageBase64(e.target?.result as string);
form.setFieldValue('logo', e.target?.result);
};
reader.readAsDataURL(file);
// }
return false;
};
const handleChange: UploadProps['onChange'] = (info: UploadChangeParam<UploadFile>) => {
if (info.file.status === 'uploading') {
setUploadLoading(true);
return;
}
if (info.file.status === 'done') {
getImgBase64(info.file.originFileObj as RcFile, () => {
setUploadLoading(false);
});
}
if (info.fileList.length === 0) {
form.setFieldValue( "logo", null );
}
};
const uploadButton = (
<div>
{uploadLoading ? <LoadingOutlined /> : <Icon icon="ic:baseline-add" width="24" height="24"/>}
</div>
);
const getTagAndServiceClassifyList = ()=>{
setTagOptionList([])
setServiceClassifyOptionList([])
fetchData<BasicResponse<{ catalogues:CategorizesType[],tags:EntityItem[]}>>('catalogues',{method:'GET'}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTagOptionList(data.tags?.map((x:EntityItem)=>{return {
label:x.name, value:x.name
}})||[])
setServiceClassifyOptionList(data.catalogues)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
// 获取表单默认值
const getAiServiceInfo = () => {
fetchData<BasicResponse<{ service: AiServiceConfigFieldType }>>('ai-service/info',{method:'GET',eoParams:{team:teamId, service:serviceId},eoTransformKeys:['team_id','service_type']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTimeout(()=>{
form.setFieldsValue({
...data.service,
team:data.service.team.id,
catalogue:data.service.catalogue?.id,
tags:data.service.tags?.map((x:EntityItem)=>x.name),
provider:data.service.provider.id,
logoFile:[
{
uid: '-1', // 文件唯一标识
name: 'image.png', // 文件名
status: 'done', // 状态有:uploading, done, error, removed
url: data.service?.logo || '', // 图片 Base64 数据
}
]
})
setImageBase64(data.service.logo)
setShowClassify(data.service.serviceType === 'public')
},0)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
};
const onFinish:()=>Promise<boolean|string> = () => {
return form.validateFields().then((value)=>{
return fetchData<BasicResponse<{service:{id:string}}>>(serviceId === undefined? 'team/ai-service':'ai-service/info',{method:serviceId === undefined? 'POST' : 'PUT',eoParams: {...(serviceId === undefined ? {team:value.team} :{service:serviceId,team:teamId})},eoBody:({...value,prefix:value.prefix?.trim()}), eoTransformKeys:['serviceType']},).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
setAiServiceInfo(data.service)
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)
})
})
};
const getProviderOptionList = ()=>{
setProviderOptionList([])
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers',{method:'GET',eoTransformKeys:[]}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setProviderOptionList(data.providers?.filter(x=>x.configured)?.map((x:SimpleAiProviderItem)=>{return {...x,
label: <div className="flex items-center" dangerouslySetInnerHTML={{ __html: x.logo }} />, value:x.id
}}))
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const getTeamOptionList = ()=>{
setTeamOptionList([])
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(!checkPermission('system.workspace.team.view_all') ?'simple/teams/mine' :'simple/teams',{method:'GET',eoTransformKeys:[]}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTeamOptionList(data.teams?.map((x:MemberItem)=>{return {...x,
label:x.name, value:x.id
}}))
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const deleteAiService = ()=>{
fetchData<BasicResponse<null>>('team/ai-service',{method:'DELETE',eoParams:{team:teamId,service:serviceId}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
navigate(`/aiservice/list`)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
useEffect(()=>{
aiConfigFlushed && getProviderOptionList()
},[aiConfigFlushed])
useEffect(() => {
getProviderOptionList()
getTagAndServiceClassifyList()
if(accessInit){
getTeamOptionList()
}else{
getGlobalAccessData()?.then(()=>{
getTeamOptionList()
})
}
if (serviceId !== undefined) {
setOnEdit(true);
getAiServiceInfo();
setBreadcrumb([
{
title: <Link to={`/aiservice/list`}>{$t('服务')}</Link>
},
{
title: $t('设置')
}])
} else {
setOnEdit(false);
form.setFieldValue('id',uuidv4());
form.setFieldValue('team',teamId);
form.setFieldValue('serviceType','inner');
}
return (form.setFieldsValue({}))
}, [serviceId]);
const deleteAiServiceModal = async ()=>{
modal.confirm({
title:$t('删除'),
content:$t(DELETE_TIPS.default),
onOk:()=> {
return deleteAiService()
},
width:600,
okText:$t('确认'),
okButtonProps:{
danger:true
},
cancelText:$t('取消'),
closable:true,
icon:<></>
})
}
const visualizationOptions = useMemo(()=>SERVICE_VISUALIZATION_OPTIONS.map((x)=>({...x, label:$t(x.label)})),[state.language])
return (
<>
<WithPermission access={onEdit ? 'team.service.service.edit' :''}>
<Form
layout='vertical'
labelAlign='left'
scrollToFirstError
form={form}
className="w-full pr-PAGE_INSIDE_X "
name="systemConfig"
onFinish={onFinish}
autoComplete="off"
>
<div>
<Form.Item<AiServiceConfigFieldType>
label={$t("服务名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("服务ID")}
name="id"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("AI 模型供应商")}
name="provider"
rules={[{ required: true }]}
>{
(providerOptionList && providerOptionList.length >0 ) ? <Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={providerOptionList} >
</Select> : <p> AI <a href="/aisetting" target="_blank" onClick={()=>setAiConfigFlushed(false)}></a></p>
}
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("API 调用前缀")}
name="prefix"
extra={$t("选填,作为服务内所有API的前缀,比如host/{service_name}/{api_path},一旦保存无法修改")}
rules={[
{
validator: validateUrlSlash,
}]}
>
<Input prefix={onEdit ? '' : '/'} className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("图标")}
name="logoFile"
extra={$t("仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩")}
valuePropName="fileList" getValueFromEvent={normFile}
>
<Upload
listType="picture"
beforeUpload={beforeUpload}
onChange={handleChange}
showUploadList={false}
maxCount={1}
accept=".png, .jpg, .jpeg, .svg"
>
<div className="h-[68px] w-[68px] border-[1px] border-dashed border-BORDER flex items-center justify-center rounded bg-bar-theme cursor-pointer" style={{ marginTop: 8 }}>
{imageBase64 ? <img src={imageBase64} alt="Logo" style={{ maxWidth: '200px', width:'68px',height:'68px'}} /> : uploadButton}
</div>
</Upload>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("描述")}
name="description"
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("Logo")}
name="logo"
hidden
>
</Form.Item>
{!onEdit && <Form.Item<AiServiceConfigFieldType>
label={$t("所属团队")}
name="team"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)} options={teamOptionList} >
</Select>
</Form.Item>}
<Form.Item<AiServiceConfigFieldType>
label={$t("标签")}
name="tags"
>
<Select
className="w-INPUT_NORMAL"
mode="tags"
placeholder={$t(PLACEHOLDER.select)}
options={tagOptionList}>
</Select>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("服务类型")}
name="serviceType"
rules={[{required: true}]}
>
<Radio.Group className="flex flex-col" options={visualizationOptions} onChange={(e)=>{setShowClassify(e.target.value === 'public')}} />
</Form.Item>
{showClassify &&
<Form.Item<AiServiceConfigFieldType>
label={$t("所属服务分类")}
name="catalogue"
extra={$t("设置服务展示在服务市场中的哪个分类下")}
rules={[{required: true}]}
>
<TreeSelect
className="w-INPUT_NORMAL"
fieldNames={{label:'name',value:'id',children:'children'}}
showSearch
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
placeholder={$t(PLACEHOLDER.select)}
allowClear
treeDefaultExpandAll
treeData={serviceClassifyOptionList}
/>
</Form.Item>
}
{onEdit && <>
<Row className="mb-[10px]"
// wrapperCol={{ offset: 5, span: 19 }}
>
<WithPermission access={onEdit ? 'team.service.service.edit' :''}>
<Button type="primary" htmlType="submit">
{$t('保存')}
</Button>
</WithPermission>
</Row></>}
</div>
{onEdit && <>
<WithPermission access="team.service.service.delete" showDisabled={false}>
<div className="bg-[rgb(255_120_117_/_5%)] rounded-[10px] mt-[50px] p-btnrbase pb-0">
<p className="text-left"><span className="font-bold">{$t('删除服务')}</span>{$t('删除操作不可恢复,请谨慎操作!')}</p>
<div className="text-left">
<WithPermission access="team.service.service.delete">
<Button className="m-auto mt-[16px] mb-[20px]" type="default" danger={true} onClick={deleteAiServiceModal}>{$t('删除服务')}</Button>
</WithPermission>
</div>
</div>
</WithPermission>
</>}
</Form>
</WithPermission>
</>
)
})
export default AiServiceConfig
@@ -80,7 +80,7 @@ const ServiceInsideDocument = ()=>{
], toolbar: 'undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify | codesample |table|' +
'bullist numlist outdent indent | link image | print preview media fullscreen | ' +
'forecolor backcolor emoticons | help',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px; } img { max-width: 100%; }',
setup: setupEditor,
codesample_languages:[
{
@@ -1,159 +0,0 @@
import PageList from "@common/components/aoplatform/PageList.tsx"
import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {useNavigate} from "react-router-dom";
import { App} from "antd";
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 { SimpleTeamItem ,SimpleMemberItem} from "@common/const/type.ts";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx";
import AiServiceConfig from "./AiServiceConfig.tsx";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { $t } from "@common/locales/index.ts";
import { AiServiceTableListItem, AiServiceConfigHandle } from "@core/const/ai-service/type.ts";
import { AI_SERVICE_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx";
const AiServiceList:FC = ()=>{
const navigate = useNavigate();
const [tableSearchWord, setTableSearchWord] = useState<string>('')
const { setBreadcrumb } = useBreadcrumb()
const [teamList, setTeamList] = useState<{ [k: string]: { text: string; }; }>()
const {fetchData} = useFetch()
const [tableListDataSource, setTableListDataSource] = useState<AiServiceTableListItem[]>([]);
const [tableHttpReload, setTableHttpReload] = useState(true);
const { message } = App.useApp()
const pageListRef = useRef<ActionType>(null);
const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({})
const [open, setOpen] = useState(false);
const drawerFormRef = useRef<AiServiceConfigHandle>(null)
const {checkPermission,accessInit, getGlobalAccessData,state} = useGlobalContext()
const getAiServiceList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{
getAiServiceList()
})
return
}
if(!tableHttpReload){
setTableHttpReload(true)
return Promise.resolve({
data: tableListDataSource,
success: true,
});
}
return fetchData<BasicResponse<{services:AiServiceTableListItem[]}>>(!checkPermission('system.workspace.service.view_all') ? 'my_ai_services':'ai-services',{method:'GET',eoParams:{keyword:tableSearchWord},eoTransformKeys:['api_num','can_delete','create_time']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTableListDataSource(data.services)
setTableHttpReload(false)
return {data:data.services, success: true}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
}).catch(() => {
return {data:[], success:false}
})
}
const getTeamsList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{
getTeamsList()
})
return
}
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(!checkPermission('system.workspace.team.view_all') ?'simple/teams/mine' :'simple/teams',{method:'GET',eoTransformKeys:[]}).then(response=>{
const {code,data,msg} = response
setTeamList(data.teams)
if(code === STATUS_CODE.SUCCESS){
const tmpValueEnum:{[k:string]:{text:string}} = {}
data.teams?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
setTeamList(tmpValueEnum)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
})
}
const manualReloadTable = () => {
setTableHttpReload(true); // 表格数据需要从后端接口获取
pageListRef.current?.reload()
};
const getMemberList = async ()=>{
setMemberValueEnum({})
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
if(code === STATUS_CODE.SUCCESS){
const tmpValueEnum:{[k:string]:{text:string}} = {}
data.members?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
setMemberValueEnum(tmpValueEnum)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
useEffect(() => {
getTeamsList();
getMemberList()
setBreadcrumb([
{
title: $t('服务')
}])
}, []);
const onClose = () => {
setOpen(false);
};
const columns = useMemo(()=>{
const res = AI_SERVICE_TABLE_COLUMNS.map(x=>{
if(x.filters &&((x.dataIndex as string[])?.indexOf('master') !== -1 ) ){
x.valueEnum = memberValueEnum
}
if(x.filters &&((x.dataIndex as string[])?.indexOf('team') !== -1 ) ){
x.valueEnum = teamList
}
return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}})
return res
},[memberValueEnum,teamList,state.language]);
return (
<div className="h-full w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B">
{/* <Joyride steps={steps} run={true} /> */}
<PageList
id="global_ai_system"
ref={pageListRef}
columns={[...columns]}
request={()=>getAiServiceList()}
addNewBtnTitle={$t("添加服务")}
addNewBtnWrapperClass={'my-first-step'}
searchPlaceholder={$t("输入名称、ID、所属团队、负责人查找服务")}
onAddNewBtnClick={() => {
setOpen(true)
}}
manualReloadTable={manualReloadTable}
onChange={() => {
setTableHttpReload(false)
}}
onSearchWordChange={(e) => {
setTableSearchWord(e.target.value)
}}
onRowClick={(row:AiServiceTableListItem)=>navigate(`/aiservice/${row.team.id}/inside/${row.id}`)}
/>
<DrawerWithFooter title={$t("添加 AI 服务")} open={open} onClose={onClose} onSubmit={()=>drawerFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})} >
<AiServiceConfig ref={drawerFormRef} />
</DrawerWithFooter>
</div>
)
}
export default AiServiceList
@@ -67,7 +67,7 @@ export default function ApiRequestSetting(){
>
<Form.Item<ApiRequestSettingFieldType>
label={$t("API 调用地址")}
name="name"
name="invokeAddress"
rules={[{ required: true,whitespace:true }]}
extra={$t("API base URL 一般设置为API 网关的外部网络访问地址,或者是API网关绑定的域名。")}
>
@@ -1,277 +0,0 @@
import TreeWithMore from "@common/components/aoplatform/TreeWithMore";
import WithPermission from "@common/components/aoplatform/WithPermission";
import { BasicResponse, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
import { PERMISSION_DEFINITION } from "@common/const/permissions";
import { useFetch } from "@common/hooks/http";
import { checkAccess } from "@common/utils/permission";
import { CategorizesType, ServiceHubCategoryConfigHandle } from "@market/const/serviceHub/type";
import { App, Button, Spin, Tree, TreeDataNode, TreeProps } from "antd";
import { DataNode } from "antd/es/tree";
import { Key, useEffect, useMemo, useRef, useState } from "react";
import { ServiceHubCategoryConfig } from "./ServiceHubCategoryConfig";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext";
import { LoadingOutlined } from "@ant-design/icons";
import { cloneDeep } from "lodash-es";
import { Icon } from "@iconify/react/dist/iconify.js";
import InsidePage from "@common/components/aoplatform/InsidePage";
import { EntityItem } from "@common/const/type";
import { $t } from "@common/locales";
export default function ServiceCategory(){
const [gData, setGData] = useState<CategorizesType[]>([]);
const [cateData, setCateData] = useState<CategorizesType[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const {message,modal} = App.useApp()
const {fetchData} = useFetch()
const addRef = useRef<ServiceHubCategoryConfigHandle>(null)
const addChildRef = useRef<ServiceHubCategoryConfigHandle>(null)
const renameRef = useRef<ServiceHubCategoryConfigHandle>(null)
const {accessData} = useGlobalContext()
const { setBreadcrumb } = useBreadcrumb()
const [loading, setLoading] = useState<boolean>(false)
const onDrop: TreeProps['onDrop'] = (info) => {
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // the drop position relative to the drop node, inside 0, top -1, bottom 1
const loop = (
data: TreeDataNode[],
key: React.Key,
callback: (node: TreeDataNode, i: number, data: TreeDataNode[]) => void,
) => {
for (let i = 0; i < data.length; i++) {
if (data[i].id === key) {
return callback(data[i], i, data);
}
if (data[i].children) {
loop(data[i].children!, key, callback);
}
}
};
const data = cloneDeep(gData);
// Find dragObject
let dragObj: TreeDataNode;
loop(data, dragKey, (item, index, arr) => {
arr.splice(index, 1);
dragObj = item;
});
if (!info.dropToGap) {
// Drop on the content
loop(data, dropKey, (item) => {
item.children = item.children || [];
// where to insert. New item was inserted to the start of the array in this example, but can be anywhere
item.children.unshift(dragObj);
});
} else {
let ar: TreeDataNode[] = [];
let i: number;
loop(data, dropKey, (_item, index, arr) => {
ar = arr;
i = index;
});
if (dropPosition === -1) {
// Drop on the top of the drop node
ar.splice(i!, 0, dragObj!);
} else {
// Drop on the bottom of the drop node
ar.splice(i! + 1, 0, dragObj!);
}
}
setGData(data);
sortCategories(data)
};
const dropdownMenu = (entity:CategorizesType) => [
{
key: 'addChildCate',
label: (
<WithPermission access="system.api_market.service_classification.add"><Button className="border-none p-0 flex items-center bg-transparent " onClick={()=>openModal('addChildCate',entity)}>
{$t('添加子分类')}
</Button></WithPermission>
),
},
{
key: 'renameCate',
label: (
<WithPermission access="system.api_market.service_classification.edit"><Button className=" border-none p-0 flex items-center bg-transparent " onClick={()=>openModal('renameCate',entity)}>
{$t('修改分类名称')}
</Button></WithPermission>
),
},
{
key: 'delete',
label: (
<WithPermission access="system.api_market.service_classification.delete"><Button className=" border-none p-0 flex items-center bg-transparent " onClick={()=>openModal('delete',entity)}>
{$t('删除')}
</Button></WithPermission>
),
},
];
const treeData = useMemo(() => {
setExpandedKeys([])
const loop = (data: CategorizesType[]): DataNode[] =>
data?.map((item) => {
if (item.children) {
setExpandedKeys(prev=>[...prev,item.id])
return {
title: <TreeWithMore
stopClick={false}
dropdownMenu={dropdownMenu(item as CategorizesType)}>{item.name}</TreeWithMore> ,
key: item.id, children: loop(item.children)
};
}
return {
title: <TreeWithMore
stopClick={false}
dropdownMenu={dropdownMenu(item as CategorizesType)}>{item.name}</TreeWithMore>,
key: item.id,
};
});
return loop(gData ?? [])
}, [gData]);
const isActionAllowed = (type:'addCate'|'addChildCate'|'renameCate'|'delete') => {
const actionToPermissionMap = {
'addCate': 'add',
'addChildCate': 'add',
'renameCate': 'edit',
'delete': 'delete'
};
const action = actionToPermissionMap[type];
const permission :keyof typeof PERMISSION_DEFINITION[0]= `system.api_market.service_classification.${action}`;
return !checkAccess(permission, accessData);
};
const openModal = (type:'addCate'|'addChildCate'|'renameCate'|'delete',entity?:CategorizesType)=>{
let title:string = ''
let content:string|React.ReactNode = ''
switch (type){
case 'addCate':
title=$t('添加分类')
content=<ServiceHubCategoryConfig WithPermission={WithPermission} ref={addRef} type={type} />
break;
case 'addChildCate':
title=$t('添加子分类')
content=<ServiceHubCategoryConfig WithPermission={WithPermission} ref={addChildRef} type={type} entity={entity} />
break;
case 'renameCate':
title=$t('重命名分类')
content=<ServiceHubCategoryConfig WithPermission={WithPermission} ref={renameRef} type={type} entity={entity} />
break;
case 'delete':
title=$t('删除')
content=$t(DELETE_TIPS.default)
break;
}
modal.confirm({
title,
content,
onOk:()=>{
switch (type){
case 'addCate':
return addRef.current?.save().then((res)=>{if(res === true) getCategoryList()})
case 'addChildCate':
return addChildRef.current?.save().then((res)=>{if(res === true) getCategoryList()})
case 'renameCate':
return renameRef.current?.save().then((res)=>{if(res === true) getCategoryList()})
case 'delete':
return deleteCate(entity!).then((res)=>{if(res === true) getCategoryList()})
}
},
width:600,
okText:$t('确认'),
okButtonProps:{
disabled : isActionAllowed(type)
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
})
}
const deleteCate = (entity:CategorizesType)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>('catalogue',{method:'DELETE',eoParams:{catalogue:entity.id},}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
})
}
const sortCategories = (newData:CategorizesType[])=>{
setLoading(true)
fetchData<BasicResponse<null>>('catalogue/sort',{method:'PUT',eoBody:newData}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
getCategoryList()
}else{
setGData(cateData)
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).catch(()=>{setGData(cateData)}).finally(()=>{setLoading(false)})
}
const getCategoryList = ()=>{
setLoading(true)
fetchData<BasicResponse<{ catalogues:CategorizesType[],tags:EntityItem[]}>>('catalogues',{method:'GET'}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setGData(data.catalogues)
setCateData(data.catalogues)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>{setLoading(false)})
}
useEffect(()=>{
setBreadcrumb([
{
title: $t('服务分类管理')}])
getCategoryList()
},[])
return (
<InsidePage
pageTitle={$t('服务分类管理')}
description={$t("设置服务可选择的分类,方便团队成员快速找到API。")}
showBorder={false}
contentClassName="pr-PAGE_INSIDE_X"
scrollPage={false}
>
<div className="border border-solid border-BORDER p-[20px] rounded-[10px] ">
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className=''>
<Tree
showIcon
draggable
blockNode
expandedKeys={expandedKeys}
onExpand={(expandedKeys:Key[])=>{setExpandedKeys(expandedKeys as string[])}}
onDrop={onDrop}
treeData={treeData}
/>
<WithPermission access="system.api_market.service_classification.add">
<Button type="link" className="mt-[12px] pl-[0px]" onClick={()=>openModal('addCate')}><Icon icon="ic:baseline-add" width="18" height="18" className='mr-[2px]'/>{$t('添加分类')}</Button>
</WithPermission>
</Spin>
</div>
</InsidePage>
)
}
@@ -1,122 +0,0 @@
import {App, Form, Input} from "antd";
import {forwardRef, useEffect, useImperativeHandle} from "react";
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { ServiceHubCategoryConfigHandle, ServiceHubCategoryConfigFieldType, ServiceHubCategoryConfigProps } from "@market/const/serviceHub/type.ts"
import WithPermission from "@common/components/aoplatform/WithPermission";
import { $t } from "@common/locales";
export const ServiceHubCategoryConfig = forwardRef<ServiceHubCategoryConfigHandle,ServiceHubCategoryConfigProps>((props,ref)=>{
const { message } = App.useApp()
const [form] = Form.useForm();
const {type,entity} = props
const {fetchData} = useFetch()
const save:()=>Promise<boolean | string> = ()=>{
const url:string = 'catalogue'
let method:string
switch (type){
case 'addCate':
case 'addChildCate':
method = 'POST'
break;
case 'renameCate':
method = 'PUT'
break
}
return new Promise((resolve, reject)=>{
if(!url || !method){
reject($t(RESPONSE_TIPS.error))
return
}
form.validateFields().then((value)=>{
fetchData<BasicResponse<null>>(url,{method,eoBody:(value), eoParams:{ ...(type === 'renameCate' ? {catalogue:value.id} :undefined)}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
save
})
)
useEffect(() => {
switch(type){
case 'addCate':
form.setFieldsValue({})
break
case 'addChildCate':
form.setFieldsValue({parent:entity!.id})
break
case 'renameCate':
form.setFieldsValue(entity)
break
}
}, []);
return (
<WithPermission access={type === 'addCate'? 'system.api_market.service_classification.add': 'system.api_market.service_classification.edit'}>
<Form
layout='vertical'
scrollToFirstError
labelAlign='left'
form={form}
className="mx-auto "
name="serviceHubCategoryConfig"
autoComplete="off"
>
{type === 'renameCate' &&
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t("ID")}
name="id"
hidden
rules={[{ required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
}
{(type === 'addCate' || type === 'renameCate') &&
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t("分类名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>}
{type === 'addChildCate' &&<>
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t("父分类 ID")}
name="parent"
hidden
rules={[{ required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t("子分类名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
</>
}
</Form>
</WithPermission>
)
})
@@ -237,7 +237,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_,ref) => {
if(accessInit){
getTeamOptionList()
}else{
getGlobalAccessData()?.then(()=>{
getGlobalAccessData()?.then?.(()=>{
getTeamOptionList()
})
}
@@ -80,7 +80,7 @@ const ServiceInsideDocument = ()=>{
], toolbar: 'undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify | codesample |table|' +
'bullist numlist outdent indent | link image | print preview media fullscreen | ' +
'forecolor backcolor emoticons | help',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
content_style: 'body{ font-family:Helvetica,Arial,sans-serif; font-size:14px } img{ max-width: 100%; }',
setup: setupEditor,
codesample_languages:[
{
@@ -34,7 +34,7 @@ const SystemList:FC = ()=>{
const getSystemList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{
getGlobalAccessData()?.then?.(()=>{
getSystemList()
})
return
@@ -63,7 +63,7 @@ const SystemList:FC = ()=>{
const getTeamsList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{
getGlobalAccessData()?.then?.(()=>{
getTeamsList()
})
return
@@ -1,156 +0,0 @@
import {App, Col, Form, Input, Row, Select} from "antd";
import {forwardRef, useEffect, useImperativeHandle, useRef} from "react";
import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal.tsx";
import styles from "./SystemInsideApi.module.css"
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { HTTP_METHOD, MATCH_CONFIG } from "../../../const/system/const.tsx";
import { SystemInsideApiCreateHandle, SystemInsideApiCreateProps, SystemApiProxyFieldType, SystemInsideApiProxyHandle } from "../../../const/system/type.ts";
import { MatchItem } from "@common/const/type.ts";
import { validateUrlSlash } from "@common/utils/validate.ts";
import { $t } from "@common/locales/index.ts";
import SystemInsideApiProxy from "@core/pages/system/api/SystemInsideApiProxy";
const SystemInsideApiCreate = forwardRef<SystemInsideApiCreateHandle,SystemInsideApiCreateProps>((props, ref) => {
const { message } = App.useApp()
const {type, entity, serviceId,teamId, modalApiPrefix:apiPrefix, modalPrefixForce:prefixForce} = props
const [form] = Form.useForm();
const {fetchData} = useFetch()
const proxyRef = useRef<SystemInsideApiProxyHandle>(null)
const onFinish = ()=>{
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([,formValue])=>{
const body = {...formValue,path:formValue.path.trim(),proxy:{...formValue.proxy,path:formValue.proxy.path ? (formValue.proxy.path.startsWith('/')? formValue.proxy.path: '/'+ formValue.proxy.path) : undefined}}
return fetchData<BasicResponse<{api:SystemApiProxyFieldType}>>('service/api',{method:'POST',eoBody:(body), eoParams: {service:serviceId,team:teamId},eoTransformKeys:['matchType']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch(errInfo=>Promise.reject(errInfo))
})
}
const copy: ()=>Promise<boolean | string> = ()=>{
return new Promise((resolve, reject)=>{
return form.validateFields().then((value)=>{
fetchData<BasicResponse<{api:SystemApiProxyFieldType}>>('service/api/copy',{method:'POST',eoParams:{service:serviceId,team:teamId, api:entity!.id},eoBody:({...value,path:value.path.trim()})}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return resolve(data.api.id)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
copy,
save:onFinish
})
)
useEffect(() => {
if(type === 'copy'){
form.setFieldsValue({
...entity,
name:`${$t('副本')}-${entity!.name}`,
...(prefixForce?
{prefix:apiPrefix,path: entity!.path.substring(apiPrefix?.length|| 0)}:
{}),
proxy:{timeout:10000, retry:0, ...entity?.proxy}
});
}
else{
form.setFieldValue('prefix',apiPrefix)
form.setFieldValue(['proxy','timeout'],10000)
form.setFieldValue(['proxy','retry'],0)
}
return (form.setFieldsValue({}))
}, []);
return (<div className="h-full w-full">
<Form
layout='vertical'
labelAlign='left'
scrollToFirstError
form={form}
className="mx-auto flex flex-col h-full"
name="systemInsideApiCreate"
onFinish={onFinish}
autoComplete="off"
>
<div className="">
<Row className="mb-btnybase" > <Col ><span className="font-bold mr-[13px]">{$t('API 基础信息')}</span></Col></Row>
<Form.Item<SystemApiProxyFieldType>
label={$t("API 名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("描述")}
name="description"
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("请求方式")}
name="method"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} options={HTTP_METHOD.map((method:string)=>{
return { label:method, value:method}
})}>
</Select>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("请求路径")}
name="path"
rules={[{ required: true,whitespace:true },
{
validator: validateUrlSlash,
}]}
className={styles['form-input-group']}
>
<Input prefix={(prefixForce ? `${apiPrefix}/` :"/")} className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("高级匹配")}
name="match"
>
<EditableTableWithModal<MatchItem & {_id:string}>
configFields={MATCH_CONFIG}
/>
</Form.Item>
{/* } */}
{ type !== 'copy' &&<>
<Row className="mb-btnybase mt-[40px]"><Col ><span className="font-bold mr-[13px]">{$t('转发规则设置')} </span></Col></Row>
<Form.Item<SystemApiProxyFieldType>
className="mb-0 bg-transparent border-none p-0"
name="proxy"
>
<SystemInsideApiProxy serviceId={serviceId!} teamId={teamId!} ref={proxyRef} />
</Form.Item>
</>}
</div>
</Form>
</div>
)
})
export default SystemInsideApiCreate
@@ -1,99 +0,0 @@
import {useEffect, useRef, useState} from "react";
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {App, Button, Spin} from "antd";
import ApiBasicInfoDisplay from "@common/components/postcat/api/ApiPreview/components/ApiBasicInfoDisplay";
import ApiPreview from "@common/components/postcat/ApiPreview.tsx";
import ApiMatch from "@common/components/postcat/api/ApiPreview/components/ApiMatch";
import {v4 as uuidv4} from 'uuid'
import ApiProxy from "@common/components/postcat/api/ApiPreview/components/ApiProxy";
import { ProxyHeaderItem, SystemApiDetail, SystemInsideApiDetailProps, SystemInsideApiDocumentHandle } from "../../../const/system/type.ts";
import { MatchItem } from "@common/const/type.ts";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx";
import SystemInsideApiDocument from "./SystemInsideApiDocument.tsx";
import ScrollableSection from "@common/components/aoplatform/ScrollableSection.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { LoadingOutlined } from "@ant-design/icons";
import { $t } from "@common/locales/index.ts";
const SystemInsideApiDetail = (props:SystemInsideApiDetailProps)=>{
const { message } = App.useApp()
const {serviceId, teamId, apiId} = props
const {fetchData} = useFetch()
const [apiDetail, setApiDetail] = useState<SystemApiDetail>()
const [open, setOpen] = useState(false);
const drawerFormRef = useRef<SystemInsideApiDocumentHandle>(null)
const [loading, setLoading] = useState<boolean>(false)
const getApiDetail = ()=>{
setLoading(true)
fetchData<BasicResponse<{api:SystemApiDetail}>>('service/api/detail',{method:'GET',eoParams:{service:serviceId,team:teamId, api:apiId},eoTransformKeys:['create_time','update_time','match_type','upstream_id','opt_type']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
const newApiDetail = {
...data.api,
match:data.api.match?.map((x:MatchItem)=>{x.id = x.id ?? uuidv4();return x}) || [],
...data.api.proxy && {proxy:{...data.api.proxy,
headers:data.api.proxy?.headers?.map((x:ProxyHeaderItem)=>{x.id = x.id?? uuidv4();return x || []
})}
}
}
setApiDetail(newApiDetail)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>{setLoading(false)})
}
const onClose = ()=>{
setOpen(false)
}
useEffect(() => {
getApiDetail()
}, []);
return (
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className="h-full 1" rootClassName="h-full 2" wrapperClassName="h-full 3" >
<div className="pb-btnbase h-full overflow-hidden box-border">
<ScrollableSection>
<div className="content-before pb-[8px] mb-[4px]">
{
apiDetail !== undefined && <>
<div className="flex justify-between">
<ApiBasicInfoDisplay apiName={apiDetail?.name} protocol={apiDetail?.protocol || 'HTTP'} method={apiDetail?.method} uri={apiDetail?.path} />
<WithPermission access="team.service.api.edit"><Button type="primary" onClick={()=>setOpen(true)}>{$t('编辑文档')}</Button></WithPermission>
</div>
<p className="text-[14px] leading-[22px] text-[#999999]">
<span className="mr-[20px]">{$t('创建者')}:{apiDetail?.creator.name || '-'}</span>
<span className="mr-[20px]">{$t('最后编辑人')}:{apiDetail?.updater.name || '-'}</span><span>{$t('更新时间')}:{apiDetail?.updateTime || '-'}</span></p></>
}
</div>
<div className="scroll-area h-[calc(100%-84px)] overflow-auto">
{
apiDetail?.match && apiDetail.match?.length > 0 &&
<ApiMatch title={$t('高级匹配')} rows={apiDetail?.match} />
}
{
apiDetail?.proxy && Object.keys(apiDetail?.proxy).length > 0 &&
<ApiProxy title={$t('转发规则')} proxyInfo={apiDetail?.proxy} />
}
{apiDetail && <ApiPreview entity={{...apiDetail.doc,name:apiDetail.name, method:apiDetail.method,uri:apiDetail.path, protocol:apiDetail.protocol||'HTTP'}} />}
</div>
</ScrollableSection>
<DrawerWithFooter
title={$t("编辑 API")}
open={open}
onClose={onClose}
onSubmit={()=>drawerFormRef.current?.save()?.then((res)=>{res&& getApiDetail();return res})}
showLastStep={true}
>
<SystemInsideApiDocument ref={drawerFormRef} serviceId={serviceId} teamId={teamId} apiId={apiId}/>
</DrawerWithFooter>
</div>
</Spin>)
}
export default SystemInsideApiDetail
@@ -1,251 +0,0 @@
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx"
import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {Link, useParams} from "react-router-dom";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {App, Divider} from "antd";
import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import { SimpleMemberItem} from '@common/const/type.ts'
import {useFetch} from "@common/hooks/http.ts";
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
import SystemInsideApiCreate from "./SystemInsideApiCreate.tsx";
import {useSystemContext} from "../../../contexts/SystemContext.tsx";
import { SYSTEM_API_TABLE_COLUMNS } from "../../../const/system/const.tsx";
import { SystemApiSimpleFieldType, SystemApiTableListItem, SystemInsideApiCreateHandle, SystemInsideApiDocumentHandle } from "../../../const/system/type.ts";
import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { checkAccess } from "@common/utils/permission.ts";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx";
import SystemInsideApiDetail from "./SystemInsideApiDetail.tsx";
import SystemInsideApiDocument from "./SystemInsideApiDocument.tsx";
import { $t } from "@common/locales/index.ts";
const SystemInsideApiList:FC = ()=>{
const [searchWord, setSearchWord] = useState<string>('')
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
// const [confirmLoading, setConfirmLoading] = useState(false);
const [init, setInit] = useState<boolean>(true)
const [tableListDataSource, setTableListDataSource] = useState<SystemApiTableListItem[]>([]);
const [tableHttpReload, setTableHttpReload] = useState(true);
const {fetchData} = useFetch()
const pageListRef = useRef<ActionType>(null);
const copyRef = useRef<SystemInsideApiCreateHandle>(null)
const {apiPrefix, prefixForce} = useSystemContext()
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
const {accessData,state} = useGlobalContext()
const [drawerType,setDrawerType]= useState<'add'|'edit'|'view'|'upstream'|undefined>()
const [open, setOpen] = useState(false);
const drawerEditFormRef = useRef<SystemInsideApiDocumentHandle>(null)
const drawerAddFormRef = useRef<SystemInsideApiCreateHandle>(null)
const {serviceId, teamId} = useParams<RouterParams>()
const [curApi, setCurApi] = useState<SystemApiTableListItem>()
const getApiList = (): Promise<{ data: SystemApiTableListItem[], success: boolean }>=> {
//console.log(sorter, filter)
if(!tableHttpReload){
setTableHttpReload(true)
return Promise.resolve({
data: tableListDataSource,
success: true,
});
}
return fetchData<BasicResponse<{apis:SystemApiTableListItem}>>('service/apis',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','can_delete']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTableListDataSource(data.apis)
setInit((prev)=>prev ? false : prev)
setTableHttpReload(false)
return {data:data.apis, success: true}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
}).catch(() => {
return {data:[], success:false}
})
}
const deleteApi = (entity:SystemApiTableListItem)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>('service/api',{method:'DELETE',eoParams:{service:serviceId,team:teamId, api:entity!.id}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
})
}
const openModal = async (type:'copy' | 'delete',entity:SystemApiTableListItem) =>{
let title:string = ''
let content:string|React.ReactNode = ''
switch (type){
case 'copy':{
title=$t('复制 API')
message.loading($t(RESPONSE_TIPS.loading))
const {code,data,msg} = await fetchData<BasicResponse<{api:SystemApiSimpleFieldType}>>('service/api/detail/simple',{method:'GET',eoParams:{service:serviceId,team:teamId, api:entity!.id}})
message.destroy()
if(code === STATUS_CODE.SUCCESS){
content=<SystemInsideApiCreate ref={copyRef} type={type} entity={{...data.api, path:(data.api.path?.startsWith('/')? data.api.path.substring(1): data.api.path),serviceId:serviceId}} serviceId={serviceId!} teamId={teamId!} modalApiPrefix={apiPrefix} modalPrefixForce={prefixForce}/>
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return
}
break;}
case 'delete':
title=$t('删除')
content=$t(DELETE_TIPS.default)
break;
}
modal.confirm({
title,
content,
onOk:()=> {
switch (type){
case 'copy':
return copyRef.current?.copy().then(()=> {
manualReloadTable()
})
case 'delete':
return deleteApi(entity).then((res)=>{if(res === true) manualReloadTable()})
}
},
width:type==='copy'? 900: 600,
okText:$t('确认'),
okButtonProps:{
disabled : !checkAccess( `team.service.api.${type}`, accessData )
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
})
}
const operation:PageProColumns<SystemApiTableListItem>[] =[
{
title: COLUMNS_TITLE.operate,
key: 'option',
btnNums:4,
fixed:'right',
valueType: 'option',
render: (_: React.ReactNode, entity: SystemApiTableListItem) => [
<TableBtnWithPermission access="team.service.api.view" key="view" btnType="view" onClick={()=>{openDrawer('view',entity)}} btnTitle="详情"/>,
<Divider type="vertical" className="mx-0" key="div1" />,
<TableBtnWithPermission access="team.service.api.copy" key="copy" btnType="copy" onClick={()=>{openModal('copy',entity)}} btnTitle="复制"/>,
<Divider type="vertical" className="mx-0" key="div2"/>,
<TableBtnWithPermission access="team.service.api.edit" key="edit" btnType="edit" onClick={()=>{openDrawer('edit',entity)}} btnTitle="编辑"/>,
entity.canDelete && <Divider type="vertical" className="mx-0" key="div3"/>,
entity.canDelete && <TableBtnWithPermission access="team.service.api.delete" key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle="删除"/>,
],
}
]
const manualReloadTable = () => {
setTableHttpReload(true); // 表格数据需要从后端接口获取
pageListRef.current?.reload()
};
const getMemberList = async ()=>{
setMemberValueEnum([])
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
if(code === STATUS_CODE.SUCCESS){
setMemberValueEnum(data.members)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
const openDrawer = (type:'add'|'edit'|'view',entity?:SystemApiTableListItem)=>{
setCurApi(entity)
setDrawerType(type)
}
useEffect(()=>{drawerType !== undefined ? setOpen(true):setOpen(false)},[drawerType])
useEffect(() => {
setBreadcrumb([
{
title:<Link to={`/service/list`}>{$t('服务')}</Link>
},
{
title:$t('API')
}
])
getMemberList()
manualReloadTable()
}, [serviceId]);
const onClose = () => {
setDrawerType(undefined);
setCurApi(undefined)
};
const columns = useMemo(()=>{
return [...SYSTEM_API_TABLE_COLUMNS].map(x=>{
if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){
const tmpValueEnum:{[k:string]:{text:string}} = {}
memberValueEnum?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
x.valueEnum = tmpValueEnum
}
return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}})
},[memberValueEnum,state.language])
const handlerSubmit:() => Promise<string | boolean>|undefined= ()=>{
switch(drawerType){
case 'add':{
return drawerAddFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})
}
case 'edit':{
return drawerEditFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})
}
default:return undefined
}
}
return (
<>
<PageList
id="global_system_api"
ref={pageListRef}
columns = {[...columns,...operation]}
request={()=>getApiList()}
dataSource={tableListDataSource}
addNewBtnTitle={$t('添加 API')}
searchPlaceholder={$t('输入名称、URL 查找 API')}
onAddNewBtnClick={()=>{openDrawer('add')}}
addNewBtnAccess="team.service.api.add"
tableClickAccess="team.service.api.view"
manualReloadTable={manualReloadTable}
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
onChange={() => {
setTableHttpReload(false)
}}
onRowClick={(row:SystemApiTableListItem)=>openDrawer('view',row)}
tableClass="mr-PAGE_INSIDE_X "
/>
<DrawerWithFooter
title={drawerType === 'add' ? $t("添加 API"):$t("API 详情")}
open={open}
onClose={onClose}
onSubmit={()=>handlerSubmit()}
showOkBtn={drawerType !== 'view'}
>
{drawerType === 'add' && <SystemInsideApiCreate ref={drawerAddFormRef} modalApiPrefix={apiPrefix} serviceId={serviceId!} teamId={teamId!} modalPrefixForce={prefixForce}/>}
{drawerType === 'edit' && <SystemInsideApiDocument ref={drawerEditFormRef} serviceId={serviceId!} teamId={teamId!} apiId={curApi!.id!}/>}
{drawerType === 'view' && <SystemInsideApiDetail serviceId={serviceId!} teamId={teamId!} apiId={curApi!.id!}/>}
</DrawerWithFooter>
</>
)
}
export default SystemInsideApiList
@@ -35,7 +35,7 @@ const TeamList:FC = ()=>{
const getTeamList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{getTeamList()})
getGlobalAccessData()?.then?.(()=>{getTeamList()})
return
}
return fetchData<BasicResponse<{teams:TeamTableListItem}>>(!checkPermission('system.workspace.team.view_all') ? 'teams':'manager/teams',{method:'GET',eoParams:{keyword:searchWord},eoTransformKeys:['create_time','service_num','can_delete']}).then(response=>{
@@ -17,6 +17,7 @@ export type ServiceBasicInfoType = {
logo?:string
invokeAddress:string
approvalType:'auto'|'manual'
serviceType:'ai'|'rest'
}
export type ServiceDetailType = {
@@ -35,7 +35,7 @@ const ServiceHubDetail = ()=>{
const navigate = useNavigate();
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']}).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_type']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setService(data.service)
@@ -113,7 +113,7 @@ const ServiceHubDetail = ()=>{
{
key: 'api-document',
label: $t('API 文档'),
children: <div className="p-btnbase"><ServiceHubApiDocument service={service!} /></div>,
children: <div className={`p-btnbase ${serviceBasicInfo?.serviceType?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}><ServiceHubApiDocument service={service!} /></div>,
icon: <ApiFilled />
}
]
@@ -78,7 +78,7 @@ export const initialServiceHubListState = {
if(!dataSet.selectedTag || dataSet.selectedTag.length === 0) return false
if((!x.tags || !x.tags.length )&& dataSet.selectedTag.indexOf('empty') === -1) return false
if(x.tags && x.tags.length && !x.tags.some(tag => dataSet.selectedTag.includes(tag.id))) return false;
if( dataSet.keyword && !x.name.includes(dataSet.keyword)) return false
if( dataSet.keyword && !x.name.toLocaleLowerCase().includes(dataSet.keyword.toLocaleLowerCase())) return false
return true
})
}
@@ -33,7 +33,7 @@ export default function ServiceHubManagement() {
const getServiceList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{getServiceList()})
getGlobalAccessData()?.then?.(()=>{getServiceList()})
return
}
setServiceLoading(true)
@@ -56,7 +56,7 @@ const getServiceList = ()=>{
const getTeamsList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{getTeamsList()})
getGlobalAccessData()?.then?.(()=>{getTeamsList()})
return
}
setPageLoading(true)