This commit is contained in:
dengqichen 2025-01-20 13:33:18 +08:00
parent 6235f0e09b
commit 3b9a65bcc8
3 changed files with 556 additions and 403 deletions

View File

@ -0,0 +1,420 @@
import React, { useState, useEffect } from 'react';
import {Modal, Button, message} from 'antd';
import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons';
import type {DeploymentConfig} from '../types';
import './styles.less';
import {FormButtonGroup, FormItem, Select, Submit, FormGrid, Input, ArrayTable} from '@formily/antd-v5'
import {createForm, Field, FieldDataSource, onFormInit} from '@formily/core'
import {createSchemaField, FormProvider, ISchema} from '@formily/react'
import {action} from '@formily/reactive'
import request from '@/utils/request';
import Editor from '@/components/Editor';
// 定义 ScriptEditor 组件
const ScriptEditor: React.FC<any> = (props) => {
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isFullscreen) {
setIsFullscreen(false);
}
};
document.addEventListener('keydown', handleEscKey);
return () => {
document.removeEventListener('keydown', handleEscKey);
};
}, [isFullscreen]);
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen);
};
const editorStyle = isFullscreen ? {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1000,
backgroundColor: '#1e1e1e'
} : {};
return (
<div style={{ position: 'relative', height: isFullscreen ? '100vh' : '240px', ...editorStyle }}>
<Editor {...props} height={isFullscreen ? '100vh' : '240px'} />
<div style={{
position: 'absolute',
top: '10px',
right: '10px',
zIndex: 1001,
display: 'flex',
gap: '8px'
}}>
<Button
type="text"
icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onClick={toggleFullscreen}
style={{ color: '#fff' }}
>
{isFullscreen ? '退出全屏' : '全屏'}
</Button>
</div>
</div>
);
};
interface DeploymentConfigModalProps {
open: boolean;
onCancel: () => void;
onSuccess: () => void;
initialValues?: DeploymentConfig;
envId: number;
}
// 定义字段映射接口
interface FieldMapping {
label?: string;
value?: string;
}
const DeploymentConfigModal: React.FC<DeploymentConfigModalProps> = ({
open,
onCancel,
onSuccess,
initialValues,
envId,
}) => {
// 通用的异步数据源加载方法
const useAsyncDataSource = (url: string | null, mapping: FieldMapping = {}) => (field: Field) => {
if (!url) {
field.dataSource = [];
return;
}
const { label = 'name', value = 'id' } = mapping;
field.loading = true;
request.get(url)
.then(action.bound?.((response) => {
field.dataSource = response.map((item: any) => ({
label: item[label],
value: item[value]
}));
field.loading = false;
}))
.catch(action.bound?.((error) => {
console.error(`Failed to load data from ${url}:`, error);
field.dataSource = [];
field.loading = false;
}));
};
const SchemaField = createSchemaField({
components: {
Select,
FormItem,
FormGrid,
Input,
ArrayTable,
ScriptEditor
},
scope: {
useAsyncDataSource
}
})
// 创建表单实例
const form = createForm()
const schema: ISchema = {
type: 'object',
properties: {
jenkinsConfig: {
type: 'void',
'x-component': 'FormGrid',
'x-component-props': {
maxColumns: 3,
minColumns: 3,
columnGap: 24
},
properties: {
externalSystemId: {
type: 'string',
title: 'Jenkins系统',
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left'
},
'x-component': 'Select',
'x-component-props': {
style: {
width: '100%'
},
placeholder: '请选择三方系统',
allowClear: true
},
'x-reactions': ["{{useAsyncDataSource('/api/v1/external-system/list?type=JENKINS', { label: 'name' })}}"],
},
viewId: {
type: 'string',
title: 'Jenkins视图',
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left'
},
'x-component': 'Select',
'x-component-props': {
style: {
width: '100%'
},
placeholder: '请选择Jenkins视图',
disabled: '{{!$form.values.externalSystemId}}',
allowClear: true
},
'x-reactions': {
dependencies: ['externalSystemId'],
fulfill: {
state: {
value: undefined
},
run: '{{useAsyncDataSource($deps[0] ? `/api/v1/jenkins-view/list?externalSystemId=${$deps[0]}` : null, { label: "viewName" })($self)}}'
}
}
},
jobId: {
type: 'string',
title: 'Jenkins作业',
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left'
},
'x-component': 'Select',
'x-component-props': {
style: {
width: '100%'
},
placeholder: '请选择Jenkins作业',
disabled: '{{!$form.values.viewId}}',
allowClear: true
},
'x-reactions': {
dependencies: ['externalSystemId', 'viewId'],
fulfill: {
state: {
value: undefined
},
run: '{{useAsyncDataSource(($deps[0] && $deps[1]) ? `/api/v1/jenkins-job/list?externalSystemId=${$deps[0]}&viewId=${$deps[1]}` : null, { label: "jobName" })($self)}}'
}
}
},
}
},
envConfig: {
type: 'void',
'x-component': 'FormGrid',
'x-component-props': {
maxColumns: 1,
minColumns: 1
},
properties: {
envs: {
type: 'array',
title: '环境变量',
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left'
},
'x-component': 'ArrayTable',
'x-component-props': {
pagination: { pageSize: 10 },
scroll: { x: '100%' },
style: {
minHeight: '160px',
maxHeight: '240px',
overflow: 'auto'
}
},
items: {
type: 'object',
properties: {
key: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { title: '键', width: '40%' },
properties: {
key: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': [
{
required: true,
message: '请输入环境变量名'
},
{
pattern: '^[a-zA-Z][a-zA-Z0-9_]*$',
message: '只能包含字母、数字和下划线,且必须以字母开头'
}
],
'x-component-props': {
placeholder: '请输入环境变量名',
allowClear: true
}
}
}
},
value: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { title: '值', width: '40%' },
properties: {
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': {
required: true,
message: '请输入环境变量值'
},
'x-component-props': {
placeholder: '请输入环境变量值',
allowClear: true
}
}
}
},
column3: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: '操作',
dataIndex: 'operations',
width: '20%',
},
properties: {
remove: {
type: 'void',
'x-component': 'ArrayTable.Remove'
},
moveUp: {
type: 'void',
'x-component': 'ArrayTable.MoveUp'
},
moveDown: {
type: 'void',
'x-component': 'ArrayTable.MoveDown'
}
}
}
}
},
properties: {
add: {
type: 'void',
title: '添加环境变量',
'x-component': 'ArrayTable.Addition'
}
}
},
script: {
type: 'string',
title: '脚本内容',
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left',
style: {
marginBottom: '16px'
}
},
'x-component': 'ScriptEditor',
'x-component-props': {
language: 'shell',
theme: 'vs-dark',
options: {
minimap: {
enabled: true,
scale: 2,
showSlider: "mouseover",
renderCharacters: false
},
scrollBeyondLastLine: false,
fontSize: 14,
lineNumbers: 'on',
automaticLayout: true,
tabSize: 2
}
}
}
}
}
}
};
const handleSubmit = async () => {
try {
const values = await form.submit()
console.log('表单提交的值:', values)
console.log('Schema:', schema)
console.log('FORMILY', JSON.stringify(schema, null, 2))
onSuccess?.()
form.reset()
} catch (e) {
console.error('表单提交出错:', e)
message.error('提交失败,请检查表单数据是否正确')
}
}
return (
<Modal
title="部署配置"
open={open}
onCancel={onCancel}
width={800}
centered
footer={[
<Button key="cancel" onClick={onCancel}>
</Button>,
<Button key="submit" type="primary" onClick={handleSubmit}>
</Button>
]}
bodyStyle={{
padding: '24px',
maxHeight: '80vh',
overflow: 'auto'
}}
>
<FormProvider form={form}>
<SchemaField schema={schema}/>
</FormProvider>
</Modal>
);
};
export default DeploymentConfigModal;

View File

@ -1,419 +1,147 @@
import React, { useState, useEffect } from 'react'; import React, {useEffect, useState} from 'react';
import {Modal, Button, message} from 'antd'; import {
import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons'; Dialog,
import type {DeploymentConfig} from '../types'; DialogContent,
import './styles.less'; DialogHeader,
import {FormButtonGroup, FormItem, Select, Submit, FormGrid, Input, ArrayTable} from '@formily/antd-v5' DialogTitle,
import {createForm, Field, FieldDataSource, onFormInit} from '@formily/core' DialogFooter,
import {createSchemaField, FormProvider, ISchema} from '@formily/react' } from "@/components/ui/dialog";
import {action} from '@formily/reactive' import {Button} from "@/components/ui/button";
import request from '@/utils/request'; import {
import Editor from '@/components/Editor'; Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {useForm} from "react-hook-form";
import {getApplicationList} from '../../../Application/List/service';
import {getExternalSystemList} from '../service';
import type {ExternalSystem} from '@/pages/Deploy/External/types';
// 定义 ScriptEditor 组件 interface Application {
const ScriptEditor: React.FC<any> = (props) => { id: number;
const [isFullscreen, setIsFullscreen] = useState(false); appName: string;
}
useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isFullscreen) {
setIsFullscreen(false);
}
};
document.addEventListener('keydown', handleEscKey);
return () => {
document.removeEventListener('keydown', handleEscKey);
};
}, [isFullscreen]);
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen);
};
const editorStyle = isFullscreen ? {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1000,
backgroundColor: '#1e1e1e'
} : {};
return (
<div style={{ position: 'relative', height: isFullscreen ? '100vh' : '240px', ...editorStyle }}>
<Editor {...props} height={isFullscreen ? '100vh' : '240px'} />
<div style={{
position: 'absolute',
top: '10px',
right: '10px',
zIndex: 1001,
display: 'flex',
gap: '8px'
}}>
<Button
type="text"
icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onClick={toggleFullscreen}
style={{ color: '#fff' }}
>
{isFullscreen ? '退出全屏' : '全屏'}
</Button>
</div>
</div>
);
};
interface DeploymentConfigModalProps { interface DeploymentConfigModalProps {
open: boolean; open: boolean;
onCancel: () => void; onCancel: () => void;
onSuccess: () => void; onSuccess: () => void;
initialValues?: DeploymentConfig;
envId: number;
} }
// 定义字段映射接口 const DeploymentConfigModal: React.FC<DeploymentConfigModalProps> = ({open, onCancel, onSuccess}) => {
interface FieldMapping { const [applications, setApplications] = useState<Application[]>([]);
label?: string; const [externalSystems, setExternalSystems] = useState<ExternalSystem[]>([]);
value?: string; const form = useForm({
} defaultValues: {
applicationId: undefined,
const DeploymentConfigModal: React.FC<DeploymentConfigModalProps> = ({ externalSystemId: undefined,
open,
onCancel,
onSuccess,
initialValues,
envId,
}) => {
// 通用的异步数据源加载方法
const useAsyncDataSource = (url: string | null, mapping: FieldMapping = {}) => (field: Field) => {
if (!url) {
field.dataSource = [];
return;
} }
});
const { label = 'name', value = 'id' } = mapping; useEffect(() => {
field.loading = true; if (open) {
request.get(url) // 获取应用列表
.then(action.bound?.((response) => { getApplicationList().then(data => {
field.dataSource = response.map((item: any) => ({ setApplications(data);
label: item[label], });
value: item[value] // 获取外部系统列表
})); getExternalSystemList('JENKINS').then(data => {
field.loading = false; setExternalSystems(data);
})) });
.catch(action.bound?.((error) => {
console.error(`Failed to load data from ${url}:`, error);
field.dataSource = [];
field.loading = false;
}));
};
const SchemaField = createSchemaField({
components: {
Select,
FormItem,
FormGrid,
Input,
ArrayTable,
ScriptEditor
},
scope: {
useAsyncDataSource
} }
}) }, [open]);
// 创建表单实例 const handleSubmit = form.handleSubmit((values) => {
const form = createForm() console.log('表单提交的值:', values);
onSuccess?.();
const schema: ISchema = { });
type: 'object',
properties: {
jenkinsConfig: {
type: 'void',
'x-component': 'FormGrid',
'x-component-props': {
maxColumns: 3,
minColumns: 3,
columnGap: 24
},
properties: {
externalSystemId: {
type: 'string',
title: 'Jenkins系统',
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left'
},
'x-component': 'Select',
'x-component-props': {
style: {
width: '100%'
},
placeholder: '请选择三方系统',
allowClear: true
},
'x-reactions': ["{{useAsyncDataSource('/api/v1/external-system/list?type=JENKINS', { label: 'name' })}}"],
},
viewId: {
type: 'string',
title: 'Jenkins视图',
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left'
},
'x-component': 'Select',
'x-component-props': {
style: {
width: '100%'
},
placeholder: '请选择Jenkins视图',
disabled: '{{!$form.values.externalSystemId}}',
allowClear: true
},
'x-reactions': {
dependencies: ['externalSystemId'],
fulfill: {
state: {
value: undefined
},
run: '{{useAsyncDataSource($deps[0] ? `/api/v1/jenkins-view/list?externalSystemId=${$deps[0]}` : null, { label: "viewName" })($self)}}'
}
}
},
jobId: {
type: 'string',
title: 'Jenkins作业',
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left'
},
'x-component': 'Select',
'x-component-props': {
style: {
width: '100%'
},
placeholder: '请选择Jenkins作业',
disabled: '{{!$form.values.viewId}}',
allowClear: true
},
'x-reactions': {
dependencies: ['externalSystemId', 'viewId'],
fulfill: {
state: {
value: undefined
},
run: '{{useAsyncDataSource(($deps[0] && $deps[1]) ? `/api/v1/jenkins-job/list?externalSystemId=${$deps[0]}&viewId=${$deps[1]}` : null, { label: "jobName" })($self)}}'
}
}
},
}
},
envConfig: {
type: 'void',
'x-component': 'FormGrid',
'x-component-props': {
maxColumns: 1,
minColumns: 1
},
properties: {
envs: {
type: 'array',
title: '环境变量',
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left'
},
'x-component': 'ArrayTable',
'x-component-props': {
pagination: { pageSize: 10 },
scroll: { x: '100%' },
style: {
minHeight: '160px',
maxHeight: '240px',
overflow: 'auto'
}
},
items: {
type: 'object',
properties: {
key: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { title: '键', width: '40%' },
properties: {
key: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': [
{
required: true,
message: '请输入环境变量名'
},
{
pattern: '^[a-zA-Z][a-zA-Z0-9_]*$',
message: '只能包含字母、数字和下划线,且必须以字母开头'
}
],
'x-component-props': {
placeholder: '请输入环境变量名',
allowClear: true
}
}
}
},
value: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { title: '值', width: '40%' },
properties: {
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': {
required: true,
message: '请输入环境变量值'
},
'x-component-props': {
placeholder: '请输入环境变量值',
allowClear: true
}
}
}
},
column3: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: '操作',
dataIndex: 'operations',
width: '20%',
},
properties: {
remove: {
type: 'void',
'x-component': 'ArrayTable.Remove'
},
moveUp: {
type: 'void',
'x-component': 'ArrayTable.MoveUp'
},
moveDown: {
type: 'void',
'x-component': 'ArrayTable.MoveDown'
}
}
}
}
},
properties: {
add: {
type: 'void',
title: '添加环境变量',
'x-component': 'ArrayTable.Addition'
}
}
},
script: {
type: 'string',
title: '脚本内容',
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 24,
wrapperCol: 24,
layout: 'vertical',
colon: false,
labelAlign: 'left',
style: {
marginBottom: '16px'
}
},
'x-component': 'ScriptEditor',
'x-component-props': {
language: 'shell',
theme: 'vs-dark',
options: {
minimap: {
enabled: true,
scale: 2,
showSlider: "mouseover",
renderCharacters: false
},
scrollBeyondLastLine: false,
fontSize: 14,
lineNumbers: 'on',
automaticLayout: true,
tabSize: 2
}
}
}
}
}
}
};
const handleSubmit = async () => {
try {
const values = await form.submit()
console.log('表单提交的值:', values)
console.log('Schema:', schema)
console.log('FORMILY', JSON.stringify(schema, null, 2))
onSuccess?.()
form.reset()
} catch (e) {
console.error('表单提交出错:', e)
message.error('提交失败,请检查表单数据是否正确')
}
}
return ( return (
<Modal <Dialog open={open} onOpenChange={(open) => !open && onCancel()}>
title="部署配置" <DialogContent className="sm:max-w-[600px]">
open={open} <DialogHeader>
onCancel={onCancel} <DialogTitle></DialogTitle>
width={800} </DialogHeader>
centered <Form {...form}>
footer={[ <form onSubmit={handleSubmit} className="space-y-4">
<Button key="cancel" onClick={onCancel}> <div className="grid grid-cols-2 gap-4">
<FormField
</Button>, control={form.control}
<Button key="submit" type="primary" onClick={handleSubmit}> name="applicationId"
render={({field}) => (
</Button> <FormItem>
]} <FormLabel></FormLabel>
bodyStyle={{ <Select
padding: '24px', onValueChange={(value) => field.onChange(Number(value))}
maxHeight: '80vh', value={field.value?.toString()}
overflow: 'auto' >
}} <FormControl>
> <SelectTrigger>
<FormProvider form={form}> <SelectValue placeholder="请选择应用" />
<SchemaField schema={schema}/> </SelectTrigger>
</FormProvider> </FormControl>
</Modal> <SelectContent>
{applications.map((app) => (
<SelectItem key={app.id} value={String(app.id)}>
{app.appName}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="externalSystemId"
render={({field}) => (
<FormItem>
<FormLabel></FormLabel>
<Select
onValueChange={(value) => field.onChange(Number(value))}
value={field.value?.toString()}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="请选择三方系统" />
</SelectTrigger>
</FormControl>
<SelectContent>
{externalSystems.map((system) => (
<SelectItem key={system.id} value={String(system.id)}>
{system.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</form>
</Form>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
</Button>
<Button onClick={form.handleSubmit(handleSubmit)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
); );
}; };

View File

@ -1,6 +1,7 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type {DeploymentConfig, CreateDeploymentConfigRequest, UpdateDeploymentConfigRequest, DeploymentConfigQueryParams, DeployConfigTemplate} from './types'; import type {DeploymentConfig, CreateDeploymentConfigRequest, UpdateDeploymentConfigRequest, DeploymentConfigQueryParams, DeployConfigTemplate} from './types';
import type {Page} from '@/types/base'; import type {Page} from '@/types/base';
import type {ExternalSystem} from '@/pages/Deploy/External/types';
const BASE_URL = '/api/v1/deploy-app-config'; const BASE_URL = '/api/v1/deploy-app-config';
@ -31,3 +32,7 @@ export const getDeploymentConfigsByEnv = (environmentId: number) =>
// 获取部署配置模板列表 // 获取部署配置模板列表
export const getDeployConfigTemplates = () => export const getDeployConfigTemplates = () =>
request.get<DeployConfigTemplate[]>(`${BASE_URL}/defined`); request.get<DeployConfigTemplate[]>(`${BASE_URL}/defined`);
// 获取外部系统列表
export const getExternalSystemList = (type: string) =>
request.get<ExternalSystem[]>('/api/v1/external-system/list', {params: {type}});