增加formily json schema生成

This commit is contained in:
dengqichen 2025-01-15 16:40:15 +08:00
parent 72178f3393
commit 5cd9f4c4d3
7 changed files with 322 additions and 151 deletions

View File

@ -1,141 +1,114 @@
package com.qqchen.deploy.backend.deploy.dto.variables.build;
import com.qqchen.deploy.backend.workflow.annotation.CodeEditorConfig;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import com.qqchen.deploy.backend.workflow.annotation.SchemaPropertyDataSource;
import com.qqchen.deploy.backend.workflow.annotation.SchemaPropertyDataSourceParam;
import com.qqchen.deploy.backend.framework.annotation.formily.*;
import lombok.Data;
/**
* Jenkins构建变量
*/
@Data
@FormilyForm(name = "Jenkins构建配置")
public class JenkinsBaseBuildVariables {
@SchemaProperty(
@FormilyField(
title = "绑定三方Jenkins系统",
description = "请选择三方Jenkins系统",
required = true,
dataSource = @SchemaPropertyDataSource(
type = "api",
url = "/api/v1/external-system/list?type=JENKINS",
type = "string",
component = "Select",
props = @FormilyComponentProps(
api = "/api/v1/external-system/list?type=JENKINS",
labelField = "name",
valueField = "id",
labelField = "name"
showSearch = true,
allowClear = true,
placeholder = "请选择Jenkins系统"
),
order = 2
validators = {
@FormilyValidator(
required = true,
message = "请选择Jenkins系统"
)
}
)
private String externalSystemId;
@SchemaProperty(
@FormilyField(
title = "绑定Jenkins视图",
description = "Jenkins视图",
required = true,
dataSource = @SchemaPropertyDataSource(
type = "api",
url = "/api/v1/jenkins-view/list",
valueField = "id",
type = "string",
component = "Select",
props = @FormilyComponentProps(
labelField = "viewName",
dependsOn = {"externalSystemId"},
params = {
@SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${externalSystemId}")
}
valueField = "id",
showSearch = true,
allowClear = true,
placeholder = "请选择Jenkins视图"
),
order = 3
validators = {
@FormilyValidator(
required = true,
message = "请选择Jenkins视图"
)
},
reactions = {
@FormilyReaction(
dependencies = {"externalSystemId"},
state = "dataSource",
value = "{{$fetch('/api/v1/jenkins-view/list?externalSystemId=' + $deps.externalSystemId).then(data => data.data)}}"
)
}
)
private String viewId;
@SchemaProperty(
@FormilyField(
title = "绑定Jenkins任务",
description = "Jenkins任务",
required = true,
dataSource = @SchemaPropertyDataSource(
type = "api",
url = "/api/v1/jenkins-job/list",
valueField = "id",
type = "string",
component = "Select",
props = @FormilyComponentProps(
labelField = "jobName",
dependsOn = {"externalSystemId", "viewId"},
params = {
@SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${externalSystemId}"),
@SchemaPropertyDataSourceParam(name = "viewId", value = "${viewId}")
}
valueField = "id",
showSearch = true,
allowClear = true,
placeholder = "请选择Jenkins任务"
),
order = 4
validators = {
@FormilyValidator(
required = true,
message = "请选择Jenkins任务"
)
},
reactions = {
@FormilyReaction(
dependencies = {"externalSystemId", "viewId"},
state = "dataSource",
value = "{{$fetch('/api/v1/jenkins-job/list?externalSystemId=' + $deps.externalSystemId + '&viewId=' + $deps.viewId).then(data => data.data)}}"
)
}
)
private String jobId;
@SchemaProperty(
@FormilyField(
title = "Pipeline script",
description = "流水线脚本",
required = true,
format = "monaco-editor", // 使用 Monaco Editor
defaultValue = "#!/bin/bash\n\necho \"Hello World\"",
codeEditor = @CodeEditorConfig(
language = "groovy",
theme = "vs-dark",
minimap = false,
lineNumbers = true,
wordWrap = true,
fontSize = 14,
tabSize = 2,
autoComplete = true,
folding = true
type = "string",
component = "MonacoEditor",
props = @FormilyComponentProps(
editor = @FormilyEditorProps(
language = "groovy",
theme = "vs-dark",
minimap = false,
lineNumbers = true,
wordWrap = true,
fontSize = 14,
tabSize = 4,
automaticLayout = true,
folding = true,
placeholder = "请输入Pipeline脚本"
)
),
order = 5
validators = {
@FormilyValidator(
required = true,
message = "请输入Pipeline脚本"
)
}
)
private String script;
//
// @SchemaProperty(
// title = "绑定三方Git系统",
// description = "请选择三方Git系统",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/external-system/list?type=GIT",
// valueField = "id",
// labelField = "name"
// ),
// order = 6
// )
// private String gitExternalSystemId;
//
// @SchemaProperty(
// title = "绑定Git项目",
// description = "绑定Git项目",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/repository-project/list",
// valueField = "repoProjectId",
// labelField = "name",
// dependsOn = {"gitExternalSystemId"},
// params = {
// @SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${gitExternalSystemId}")
// }
// ),
// order = 7
// )
// private String repoProjectId;
//
// @SchemaProperty(
// title = "绑定代码分支",
// description = "请选择代码分支",
// required = true,
// dataSource = @SchemaPropertyDataSource(
// type = "api",
// url = "/api/v1/repository-branch/list",
// valueField = "name",
// labelField = "name",
// dependsOn = {"gitExternalSystemId", "repoProjectId"},
// params = {
// @SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${gitExternalSystemId}"),
// @SchemaPropertyDataSourceParam(name = "repoProjectId", value = "${repoProjectId}")
// }
// ),
// order = 8
// )
// private String branch;
}

View File

@ -1,5 +1,6 @@
package com.qqchen.deploy.backend.deploy.dto.variables.build;
import com.qqchen.deploy.backend.framework.annotation.formily.FormilyForm;
import lombok.Data;
import lombok.EqualsAndHashCode;

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.deploy.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.deploy.converter.ApplicationConverter;
import com.qqchen.deploy.backend.deploy.converter.DeployLogConverter;
@ -20,6 +21,7 @@ import com.qqchen.deploy.backend.deploy.repository.IDeployLogRepository;
import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository;
import com.qqchen.deploy.backend.deploy.service.IDeployAppConfigService;
import com.qqchen.deploy.backend.deploy.service.IDeployLogService;
import com.qqchen.deploy.backend.framework.annotation.formily.FormilySchemaFactory;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.converter.WorkflowDefinitionConverter;
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
@ -40,7 +42,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.qqchen.deploy.backend.workflow.util.GenerateSchemaUtils.generateSchema;
import static java.util.stream.Collectors.toList;
/**
@ -129,7 +130,9 @@ public class DeployAppConfigServiceImpl extends BaseServiceImpl<DeployAppConfig,
definedDTO.setName(buildType.getName());
definedDTO.setBuildType(buildType);
definedDTO.setLanguageType(languages[i]);
definedDTO.setBuildVariablesSchema(generateSchema(buildVariablesClasses[i]));
// 使用新的FormilySchemaFactory生成Schema
JsonNode schema = FormilySchemaFactory.generateSchema(buildVariablesClasses[i]);
definedDTO.setBuildVariablesSchema(schema);
result.add(definedDTO);
}
}
@ -171,7 +174,8 @@ public class DeployAppConfigServiceImpl extends BaseServiceImpl<DeployAppConfig,
@Override
public DeployAppConfigDTO create(DeployAppConfigDTO dto) {
DeployAppConfig entity = converter.toEntity(dto);
entity.setFormVariablesSchema(generateSchema(dto.getBuildType().getFormVariablesSchema()));
// 使用新的FormilySchemaFactory生成Schema
entity.setFormVariablesSchema(FormilySchemaFactory.generateSchema(dto.getBuildType().getFormVariablesSchema()));
this.repository.save(entity);
return converter.toDto(entity);
}

View File

@ -3,8 +3,9 @@ package com.qqchen.deploy.backend.framework.annotation.formily;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Target({})
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FormilyComponentProps {
String api() default ""; // 数据源API
@ -16,4 +17,7 @@ public @interface FormilyComponentProps {
String mode() default ""; // 模式(single/multiple等)
String[] enum_() default {}; // 枚举值
String[] enumNames() default {}; // 枚举显示值
// 编辑器属性
FormilyEditorProps editor() default @FormilyEditorProps; // 编辑器配置
}

View File

@ -0,0 +1,60 @@
package com.qqchen.deploy.backend.framework.annotation.formily;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FormilyEditorProps {
/**
* 编辑器语言
*/
String language() default "plaintext";
/**
* 编辑器主题
*/
String theme() default "vs";
/**
* 是否启用小地图
*/
boolean minimap() default true;
/**
* 是否显示行号
*/
boolean lineNumbers() default true;
/**
* 是否自动换行
*/
boolean wordWrap() default false;
/**
* 字体大小
*/
int fontSize() default 14;
/**
* Tab大小
*/
int tabSize() default 4;
/**
* 是否自动布局
*/
boolean automaticLayout() default true;
/**
* 是否启用代码折叠
*/
boolean folding() default true;
/**
* 占位符文本
*/
String placeholder() default "";
}

View File

@ -6,7 +6,9 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FormilySchemaFactory {
private static final ObjectMapper objectMapper = new ObjectMapper();
@ -17,11 +19,23 @@ public class FormilySchemaFactory {
* @return Formily JSON Schema
*/
public static JsonNode generateSchema(Class<?> clazz) {
FormilyForm formDef = clazz.getAnnotation(FormilyForm.class);
if (formDef == null) {
throw new IllegalArgumentException("Class must be annotated with @FormilyForm");
if (clazz == null) {
throw new IllegalArgumentException("Class cannot be null");
}
// 获取所有字段包括父类的字段
List<Field> allFields = getAllFields(clazz);
if (allFields.isEmpty()) {
throw new IllegalArgumentException("No fields found in class: " + clazz.getName());
}
// 获取最近的带有FormilyForm注解的类可能是父类
Class<?> formClass = findNearestFormClass(clazz);
if (formClass == null) {
throw new IllegalArgumentException("No @FormilyForm annotation found in class hierarchy: " + clazz.getName());
}
FormilyForm formDef = formClass.getAnnotation(FormilyForm.class);
ObjectNode schema = objectMapper.createObjectNode();
schema.put("type", "object");
schema.put("name", formDef.name());
@ -30,17 +44,58 @@ public class FormilySchemaFactory {
}
ObjectNode properties = schema.putObject("properties");
boolean hasFields = false;
for (Field field : clazz.getDeclaredFields()) {
for (Field field : allFields) {
FormilyField fieldDef = field.getAnnotation(FormilyField.class);
if (fieldDef != null) {
properties.set(field.getName(), generateFieldSchema(fieldDef));
hasFields = true;
}
}
if (!hasFields) {
throw new IllegalArgumentException("No @FormilyField annotations found in class hierarchy: " + clazz.getName());
}
return schema;
}
/**
* 获取类的所有字段包括父类的字段
* @param clazz 要获取字段的类
* @return 所有字段的列表
*/
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
fields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
currentClass = currentClass.getSuperclass();
}
return fields;
}
/**
* 查找最近的带有FormilyForm注解的类
* @param clazz 要查找的类
* @return 带有FormilyForm注解的最近的类如果没有找到则返回null
*/
private static Class<?> findNearestFormClass(Class<?> clazz) {
Class<?> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
if (currentClass.isAnnotationPresent(FormilyForm.class)) {
return currentClass;
}
currentClass = currentClass.getSuperclass();
}
return null;
}
private static JsonNode generateFieldSchema(FormilyField field) {
ObjectNode fieldSchema = objectMapper.createObjectNode();
@ -56,7 +111,7 @@ public class FormilySchemaFactory {
fieldSchema.put("x-component", field.component());
// 组件属性配置
ObjectNode componentProps = generateComponentProps(field.props());
ObjectNode componentProps = generateComponentProps(field.props(), field.component());
if (componentProps.size() > 0) {
fieldSchema.set("x-component-props", componentProps);
}
@ -91,37 +146,64 @@ public class FormilySchemaFactory {
return fieldSchema;
}
private static ObjectNode generateComponentProps(FormilyComponentProps props) {
private static ObjectNode generateComponentProps(FormilyComponentProps props, String componentType) {
ObjectNode node = objectMapper.createObjectNode();
if (!props.api().isEmpty()) {
node.put("api", props.api());
// Select组件属性
if ("Select".equals(componentType)) {
if (!props.api().isEmpty()) {
node.put("api", props.api());
}
if (!props.labelField().equals("label")) {
node.put("labelField", props.labelField());
}
if (!props.valueField().equals("value")) {
node.put("valueField", props.valueField());
}
if (props.allowClear()) {
node.put("allowClear", true);
}
if (props.showSearch()) {
node.put("showSearch", true);
}
if (!props.placeholder().isEmpty()) {
node.put("placeholder", props.placeholder());
}
if (!props.mode().isEmpty()) {
node.put("mode", props.mode());
}
if (props.enum_().length > 0) {
ArrayNode enumValues = node.putArray("enum");
Arrays.stream(props.enum_()).forEach(enumValues::add);
}
if (props.enumNames().length > 0) {
ArrayNode enumNames = node.putArray("enumNames");
Arrays.stream(props.enumNames()).forEach(enumNames::add);
}
}
if (!props.labelField().equals("label")) {
node.put("labelField", props.labelField());
}
if (!props.valueField().equals("value")) {
node.put("valueField", props.valueField());
}
if (props.allowClear()) {
node.put("allowClear", true);
}
if (props.showSearch()) {
node.put("showSearch", true);
}
if (!props.placeholder().isEmpty()) {
node.put("placeholder", props.placeholder());
}
if (!props.mode().isEmpty()) {
node.put("mode", props.mode());
}
if (props.enum_().length > 0) {
ArrayNode enumValues = node.putArray("enum");
Arrays.stream(props.enum_()).forEach(enumValues::add);
}
if (props.enumNames().length > 0) {
ArrayNode enumNames = node.putArray("enumNames");
Arrays.stream(props.enumNames()).forEach(enumNames::add);
// MonacoEditor组件属性
if ("MonacoEditor".equals(componentType)) {
FormilyEditorProps editorProps = props.editor();
if (editorProps != null) {
if (!editorProps.placeholder().isEmpty()) {
node.put("placeholder", editorProps.placeholder());
}
ObjectNode options = node.putObject("options");
options.put("language", editorProps.language());
options.put("theme", editorProps.theme());
ObjectNode minimap = options.putObject("minimap");
minimap.put("enabled", editorProps.minimap());
options.put("lineNumbers", editorProps.lineNumbers() ? "on" : "off");
options.put("wordWrap", editorProps.wordWrap() ? "on" : "off");
options.put("fontSize", editorProps.fontSize());
options.put("tabSize", editorProps.tabSize());
options.put("automaticLayout", editorProps.automaticLayout());
options.put("folding", editorProps.folding());
}
}
return node;

View File

@ -42,7 +42,7 @@ public class FormilySchemaFactoryTest {
JsonNode fulfillB = reactionB.get("fulfill");
JsonNode stateB = fulfillB.get("state");
assertEquals("{{$fetch('/api/options/B?a=' + $deps.a).then(data => data.data)}}",
assertEquals("{{$fetch('/api/v1/jenkins-view/list?externalSystemId=' + $deps.a).then(data => data.data)}}",
stateB.get("dataSource").asText());
// 验证字段C的联动
@ -57,9 +57,29 @@ public class FormilySchemaFactoryTest {
JsonNode fulfillC = reactionC.get("fulfill");
JsonNode stateC = fulfillC.get("state");
assertEquals("{{$fetch('/api/options/C?a=' + $deps.a + '&b=' + $deps.b).then(data => data.data)}}",
assertEquals("{{$fetch('/api/v1/jenkins-job/list?externalSystemId=' + $deps.a + '&viewId=' + $deps.b).then(data => data.data)}}",
stateC.get("dataSource").asText());
// 验证编辑器字段
JsonNode fieldScript = schema.get("properties").get("script");
assertEquals("string", fieldScript.get("type").asText());
assertEquals("Pipeline script", fieldScript.get("title").asText());
assertEquals("MonacoEditor", fieldScript.get("x-component").asText());
// 验证编辑器属性
JsonNode scriptProps = fieldScript.get("x-component-props");
JsonNode options = scriptProps.get("options");
assertEquals("groovy", options.get("language").asText());
assertEquals("vs-dark", options.get("theme").asText());
assertFalse(options.get("minimap").get("enabled").asBoolean());
assertEquals("on", options.get("lineNumbers").asText());
assertEquals("on", options.get("wordWrap").asText());
assertEquals(14, options.get("fontSize").asInt());
assertEquals(4, options.get("tabSize").asInt());
assertTrue(options.get("automaticLayout").asBoolean());
assertTrue(options.get("folding").asBoolean());
assertEquals("请输入Pipeline脚本", scriptProps.get("placeholder").asText());
// 打印生成的Schema以便查看
System.out.println(schema.toPrettyString());
}
@ -89,7 +109,7 @@ class TestForm {
title = "选择B",
component = "Select",
props = @FormilyComponentProps(
labelField = "name",
labelField = "viewName",
valueField = "id"
),
validators = {
@ -102,7 +122,7 @@ class TestForm {
@FormilyReaction(
dependencies = {"a"},
state = "dataSource",
value = "{{$fetch('/api/options/B?a=' + $deps.a).then(data => data.data)}}"
value = "{{$fetch('/api/v1/jenkins-view/list?externalSystemId=' + $deps.a).then(data => data.data)}}"
)
}
)
@ -112,7 +132,7 @@ class TestForm {
title = "选择C",
component = "Select",
props = @FormilyComponentProps(
labelField = "name",
labelField = "jobName",
valueField = "id"
),
validators = {
@ -125,9 +145,36 @@ class TestForm {
@FormilyReaction(
dependencies = {"a", "b"},
state = "dataSource",
value = "{{$fetch('/api/options/C?a=' + $deps.a + '&b=' + $deps.b).then(data => data.data)}}"
value = "{{$fetch('/api/v1/jenkins-job/list?externalSystemId=' + $deps.a + '&viewId=' + $deps.b).then(data => data.data)}}"
)
}
)
private String c;
@FormilyField(
title = "Pipeline script",
type = "string",
component = "MonacoEditor",
props = @FormilyComponentProps(
editor = @FormilyEditorProps(
language = "groovy",
theme = "vs-dark",
minimap = false,
lineNumbers = true,
wordWrap = true,
fontSize = 14,
tabSize = 4,
automaticLayout = true,
folding = true,
placeholder = "请输入Pipeline脚本"
)
),
validators = {
@FormilyValidator(
required = true,
message = "请输入Pipeline脚本"
)
}
)
private String script;
}