增加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; package com.qqchen.deploy.backend.deploy.dto.variables.build;
import com.qqchen.deploy.backend.workflow.annotation.CodeEditorConfig; import com.qqchen.deploy.backend.framework.annotation.formily.*;
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 lombok.Data; import lombok.Data;
/** /**
* Jenkins构建变量 * Jenkins构建变量
*/ */
@Data @Data
@FormilyForm(name = "Jenkins构建配置")
public class JenkinsBaseBuildVariables { public class JenkinsBaseBuildVariables {
@SchemaProperty( @FormilyField(
title = "绑定三方Jenkins系统", title = "绑定三方Jenkins系统",
description = "请选择三方Jenkins系统", type = "string",
required = true, component = "Select",
dataSource = @SchemaPropertyDataSource( props = @FormilyComponentProps(
type = "api", api = "/api/v1/external-system/list?type=JENKINS",
url = "/api/v1/external-system/list?type=JENKINS", labelField = "name",
valueField = "id", valueField = "id",
labelField = "name" showSearch = true,
allowClear = true,
placeholder = "请选择Jenkins系统"
), ),
order = 2 validators = {
@FormilyValidator(
required = true,
message = "请选择Jenkins系统"
)
}
) )
private String externalSystemId; private String externalSystemId;
@FormilyField(
@SchemaProperty(
title = "绑定Jenkins视图", title = "绑定Jenkins视图",
description = "Jenkins视图", type = "string",
required = true, component = "Select",
dataSource = @SchemaPropertyDataSource( props = @FormilyComponentProps(
type = "api",
url = "/api/v1/jenkins-view/list",
valueField = "id",
labelField = "viewName", labelField = "viewName",
dependsOn = {"externalSystemId"}, valueField = "id",
params = { showSearch = true,
@SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${externalSystemId}") 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; private String viewId;
@FormilyField(
@SchemaProperty(
title = "绑定Jenkins任务", title = "绑定Jenkins任务",
description = "Jenkins任务", type = "string",
required = true, component = "Select",
dataSource = @SchemaPropertyDataSource( props = @FormilyComponentProps(
type = "api",
url = "/api/v1/jenkins-job/list",
valueField = "id",
labelField = "jobName", labelField = "jobName",
dependsOn = {"externalSystemId", "viewId"}, valueField = "id",
params = { showSearch = true,
@SchemaPropertyDataSourceParam(name = "externalSystemId", value = "${externalSystemId}"), allowClear = true,
@SchemaPropertyDataSourceParam(name = "viewId", value = "${viewId}") 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; private String jobId;
@FormilyField(
@SchemaProperty(
title = "Pipeline script", title = "Pipeline script",
description = "流水线脚本", type = "string",
required = true, component = "MonacoEditor",
format = "monaco-editor", // 使用 Monaco Editor props = @FormilyComponentProps(
defaultValue = "#!/bin/bash\n\necho \"Hello World\"", editor = @FormilyEditorProps(
codeEditor = @CodeEditorConfig( language = "groovy",
language = "groovy", theme = "vs-dark",
theme = "vs-dark", minimap = false,
minimap = false, lineNumbers = true,
lineNumbers = true, wordWrap = true,
wordWrap = true, fontSize = 14,
fontSize = 14, tabSize = 4,
tabSize = 2, automaticLayout = true,
autoComplete = true, folding = true,
folding = true placeholder = "请输入Pipeline脚本"
)
), ),
order = 5 validators = {
@FormilyValidator(
required = true,
message = "请输入Pipeline脚本"
)
}
) )
private String script; 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; package com.qqchen.deploy.backend.deploy.dto.variables.build;
import com.qqchen.deploy.backend.framework.annotation.formily.FormilyForm;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.deploy.service.impl; package com.qqchen.deploy.backend.deploy.service.impl;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.deploy.converter.ApplicationConverter; import com.qqchen.deploy.backend.deploy.converter.ApplicationConverter;
import com.qqchen.deploy.backend.deploy.converter.DeployLogConverter; 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.repository.IEnvironmentRepository;
import com.qqchen.deploy.backend.deploy.service.IDeployAppConfigService; import com.qqchen.deploy.backend.deploy.service.IDeployAppConfigService;
import com.qqchen.deploy.backend.deploy.service.IDeployLogService; 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.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.converter.WorkflowDefinitionConverter; import com.qqchen.deploy.backend.workflow.converter.WorkflowDefinitionConverter;
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
@ -40,7 +42,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import static com.qqchen.deploy.backend.workflow.util.GenerateSchemaUtils.generateSchema;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
/** /**
@ -129,7 +130,9 @@ public class DeployAppConfigServiceImpl extends BaseServiceImpl<DeployAppConfig,
definedDTO.setName(buildType.getName()); definedDTO.setName(buildType.getName());
definedDTO.setBuildType(buildType); definedDTO.setBuildType(buildType);
definedDTO.setLanguageType(languages[i]); definedDTO.setLanguageType(languages[i]);
definedDTO.setBuildVariablesSchema(generateSchema(buildVariablesClasses[i])); // 使用新的FormilySchemaFactory生成Schema
JsonNode schema = FormilySchemaFactory.generateSchema(buildVariablesClasses[i]);
definedDTO.setBuildVariablesSchema(schema);
result.add(definedDTO); result.add(definedDTO);
} }
} }
@ -171,7 +174,8 @@ public class DeployAppConfigServiceImpl extends BaseServiceImpl<DeployAppConfig,
@Override @Override
public DeployAppConfigDTO create(DeployAppConfigDTO dto) { public DeployAppConfigDTO create(DeployAppConfigDTO dto) {
DeployAppConfig entity = converter.toEntity(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); this.repository.save(entity);
return converter.toDto(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.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Target({}) @Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface FormilyComponentProps { public @interface FormilyComponentProps {
String api() default ""; // 数据源API String api() default ""; // 数据源API
@ -16,4 +17,7 @@ public @interface FormilyComponentProps {
String mode() default ""; // 模式(single/multiple等) String mode() default ""; // 模式(single/multiple等)
String[] enum_() default {}; // 枚举值 String[] enum_() default {}; // 枚举值
String[] enumNames() 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 com.fasterxml.jackson.databind.node.ObjectNode;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
public class FormilySchemaFactory { public class FormilySchemaFactory {
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
@ -17,11 +19,23 @@ public class FormilySchemaFactory {
* @return Formily JSON Schema * @return Formily JSON Schema
*/ */
public static JsonNode generateSchema(Class<?> clazz) { public static JsonNode generateSchema(Class<?> clazz) {
FormilyForm formDef = clazz.getAnnotation(FormilyForm.class); if (clazz == null) {
if (formDef == null) { throw new IllegalArgumentException("Class cannot be null");
throw new IllegalArgumentException("Class must be annotated with @FormilyForm");
} }
// 获取所有字段包括父类的字段
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(); ObjectNode schema = objectMapper.createObjectNode();
schema.put("type", "object"); schema.put("type", "object");
schema.put("name", formDef.name()); schema.put("name", formDef.name());
@ -30,17 +44,58 @@ public class FormilySchemaFactory {
} }
ObjectNode properties = schema.putObject("properties"); ObjectNode properties = schema.putObject("properties");
boolean hasFields = false;
for (Field field : clazz.getDeclaredFields()) { for (Field field : allFields) {
FormilyField fieldDef = field.getAnnotation(FormilyField.class); FormilyField fieldDef = field.getAnnotation(FormilyField.class);
if (fieldDef != null) { if (fieldDef != null) {
properties.set(field.getName(), generateFieldSchema(fieldDef)); properties.set(field.getName(), generateFieldSchema(fieldDef));
hasFields = true;
} }
} }
if (!hasFields) {
throw new IllegalArgumentException("No @FormilyField annotations found in class hierarchy: " + clazz.getName());
}
return schema; 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) { private static JsonNode generateFieldSchema(FormilyField field) {
ObjectNode fieldSchema = objectMapper.createObjectNode(); ObjectNode fieldSchema = objectMapper.createObjectNode();
@ -56,7 +111,7 @@ public class FormilySchemaFactory {
fieldSchema.put("x-component", field.component()); fieldSchema.put("x-component", field.component());
// 组件属性配置 // 组件属性配置
ObjectNode componentProps = generateComponentProps(field.props()); ObjectNode componentProps = generateComponentProps(field.props(), field.component());
if (componentProps.size() > 0) { if (componentProps.size() > 0) {
fieldSchema.set("x-component-props", componentProps); fieldSchema.set("x-component-props", componentProps);
} }
@ -91,37 +146,64 @@ public class FormilySchemaFactory {
return fieldSchema; return fieldSchema;
} }
private static ObjectNode generateComponentProps(FormilyComponentProps props) { private static ObjectNode generateComponentProps(FormilyComponentProps props, String componentType) {
ObjectNode node = objectMapper.createObjectNode(); ObjectNode node = objectMapper.createObjectNode();
if (!props.api().isEmpty()) { // Select组件属性
node.put("api", props.api()); 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()); // MonacoEditor组件属性
} if ("MonacoEditor".equals(componentType)) {
if (!props.valueField().equals("value")) { FormilyEditorProps editorProps = props.editor();
node.put("valueField", props.valueField()); if (editorProps != null) {
} if (!editorProps.placeholder().isEmpty()) {
if (props.allowClear()) { node.put("placeholder", editorProps.placeholder());
node.put("allowClear", true); }
}
if (props.showSearch()) { ObjectNode options = node.putObject("options");
node.put("showSearch", true); options.put("language", editorProps.language());
} options.put("theme", editorProps.theme());
if (!props.placeholder().isEmpty()) {
node.put("placeholder", props.placeholder()); ObjectNode minimap = options.putObject("minimap");
} minimap.put("enabled", editorProps.minimap());
if (!props.mode().isEmpty()) {
node.put("mode", props.mode()); options.put("lineNumbers", editorProps.lineNumbers() ? "on" : "off");
} options.put("wordWrap", editorProps.wordWrap() ? "on" : "off");
if (props.enum_().length > 0) { options.put("fontSize", editorProps.fontSize());
ArrayNode enumValues = node.putArray("enum"); options.put("tabSize", editorProps.tabSize());
Arrays.stream(props.enum_()).forEach(enumValues::add); options.put("automaticLayout", editorProps.automaticLayout());
} options.put("folding", editorProps.folding());
if (props.enumNames().length > 0) { }
ArrayNode enumNames = node.putArray("enumNames");
Arrays.stream(props.enumNames()).forEach(enumNames::add);
} }
return node; return node;

View File

@ -42,7 +42,7 @@ public class FormilySchemaFactoryTest {
JsonNode fulfillB = reactionB.get("fulfill"); JsonNode fulfillB = reactionB.get("fulfill");
JsonNode stateB = fulfillB.get("state"); 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()); stateB.get("dataSource").asText());
// 验证字段C的联动 // 验证字段C的联动
@ -57,9 +57,29 @@ public class FormilySchemaFactoryTest {
JsonNode fulfillC = reactionC.get("fulfill"); JsonNode fulfillC = reactionC.get("fulfill");
JsonNode stateC = fulfillC.get("state"); 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()); 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以便查看 // 打印生成的Schema以便查看
System.out.println(schema.toPrettyString()); System.out.println(schema.toPrettyString());
} }
@ -89,7 +109,7 @@ class TestForm {
title = "选择B", title = "选择B",
component = "Select", component = "Select",
props = @FormilyComponentProps( props = @FormilyComponentProps(
labelField = "name", labelField = "viewName",
valueField = "id" valueField = "id"
), ),
validators = { validators = {
@ -102,7 +122,7 @@ class TestForm {
@FormilyReaction( @FormilyReaction(
dependencies = {"a"}, dependencies = {"a"},
state = "dataSource", 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", title = "选择C",
component = "Select", component = "Select",
props = @FormilyComponentProps( props = @FormilyComponentProps(
labelField = "name", labelField = "jobName",
valueField = "id" valueField = "id"
), ),
validators = { validators = {
@ -125,9 +145,36 @@ class TestForm {
@FormilyReaction( @FormilyReaction(
dependencies = {"a", "b"}, dependencies = {"a", "b"},
state = "dataSource", 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; 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;
} }