From db88b2cf28ee9fc1a9351f941fbc14dd4d6ab1dd Mon Sep 17 00:00:00 2001 From: kirillius Date: Sun, 1 Feb 2026 00:22:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=BE=D1=80=20property.=20?= =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D0=BB=20=D0=B3=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20spec.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../XCP/ApiGenerator/SpecGenerator.java | 41 +- .../XCP/Commons/GenerateApiSpec.java | 2 +- .../Entities/PropertyDescriptor.java | 3 +- .../kirillius/XCP/Properties/Constraint.java | 3 +- .../XCP/Properties/NumberConstraint.java | 4 + .../XCP/Properties/StringConstraint.java | 9 + .../XCP/Properties/ValueListConstraint.java | 2 + app/src/main/resources/defaultEntities.json | 17 + .../Persistence/RepositoryServiceImpl.java | 3 +- .../ru/kirillius/XCP/web/WebServiceImpl.java | 4 +- web-ui/vue-app/scripts/generate-rpc-client.ts | 9 +- .../vue-app/src/components/PropertyEditor.vue | 369 ++++++++++++++++++ .../vue-app/src/components/PropertyWidget.vue | 307 +++++++++++++++ web-ui/vue-app/src/views/ProfileView.vue | 43 +- 14 files changed, 768 insertions(+), 48 deletions(-) create mode 100644 web-ui/vue-app/src/components/PropertyEditor.vue create mode 100644 web-ui/vue-app/src/components/PropertyWidget.vue diff --git a/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java b/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java index 6b0212b..d196b85 100644 --- a/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java +++ b/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java @@ -118,14 +118,13 @@ public class SpecGenerator { continue; } var classAnnotation = type.getAnnotation(GenerateApiSpec.class); - if (classAnnotation == null || !type.isInterface()) { + if (classAnnotation == null) { continue; } for (var aClass : classAnnotation.directInheritors()) { if (!types.contains(aClass)) { - types.add(aClass); - typesQueue.add(aClass); + typesQueue.add(registerType(aClass)); } } @@ -135,8 +134,8 @@ public class SpecGenerator { var parents = typeDescriptor.putArray("parents"); for (var aClass : type.getInterfaces()) { if (!types.contains(aClass)) { - types.add(aClass); - typesQueue.add(aClass); + + typesQueue.add(registerType(aClass)); } if (!aClass.isAnnotationPresent(GenerateApiSpec.class)) { continue; @@ -145,6 +144,29 @@ public class SpecGenerator { } var fields = typeDescriptor.putArray("fields"); + if (!type.isInterface()) { + for (var field : type.getDeclaredFields()) { + var annotation = field.getAnnotation(GenerateApiSpec.class); + if (annotation == null) { + continue; + } + var returnType = annotation.type(); + if (returnType == void.class) { + returnType = field.getType(); + } + + if (!types.contains(returnType)) { + typesQueue.put(registerType(returnType)); + } + + var fieldDescriptor = fields.addObject(); + fieldDescriptor.put("name", annotation.alias().isEmpty() ? field.getName() : annotation.alias()); + fieldDescriptor.put("type", getTypeName(returnType)); + + } + + } + for (var method : type.getMethods()) { var methodAnnotation = method.getAnnotation(GenerateApiSpec.class); if (methodAnnotation == null) { @@ -157,8 +179,7 @@ public class SpecGenerator { } if (!types.contains(returnType)) { - types.add(returnType); - typesQueue.put(returnType); + typesQueue.put(registerType(returnType)); } var methodName = method.getName(); @@ -195,18 +216,20 @@ public class SpecGenerator { } } - private void registerType(Class type) { + private Class registerType(Class type) { if (type.isArray()) { types.add(type.getComponentType()); + return type.getComponentType(); } else { types.add(type); + return type; } } private String getTypeName(Class type) { if (type == null) return "unknown"; if (type == boolean.class) return "boolean"; - if (type == int.class || type == long.class) return "number"; + if (type == int.class || type == long.class || type == double.class || type == float.class) return "number"; if (type == String.class) return "string"; if (type == void.class || type == Void.class) return "void"; diff --git a/api/src/main/java/ru/kirillius/XCP/Commons/GenerateApiSpec.java b/api/src/main/java/ru/kirillius/XCP/Commons/GenerateApiSpec.java index bd69736..f84a9ef 100644 --- a/api/src/main/java/ru/kirillius/XCP/Commons/GenerateApiSpec.java +++ b/api/src/main/java/ru/kirillius/XCP/Commons/GenerateApiSpec.java @@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) public @interface GenerateApiSpec { String alias() default ""; diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/PropertyDescriptor.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/PropertyDescriptor.java index cdc6637..8943da5 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/PropertyDescriptor.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/PropertyDescriptor.java @@ -2,12 +2,13 @@ package ru.kirillius.XCP.Persistence.Entities; import ru.kirillius.XCP.Commons.GenerateApiSpec; import ru.kirillius.XCP.Persistence.PersistenceEntity; +import ru.kirillius.XCP.Properties.Constraint; import ru.kirillius.XCP.Properties.Constraints; import ru.kirillius.XCP.Properties.PropertyType; @GenerateApiSpec public interface PropertyDescriptor extends PersistenceEntity { - @GenerateApiSpec + @GenerateApiSpec(type = Constraint[].class) Constraints getConstraints(); void setConstraints(Constraints constraints); diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/Constraint.java b/api/src/main/java/ru/kirillius/XCP/Properties/Constraint.java index d6b0ae4..0c8b872 100644 --- a/api/src/main/java/ru/kirillius/XCP/Properties/Constraint.java +++ b/api/src/main/java/ru/kirillius/XCP/Properties/Constraint.java @@ -10,7 +10,8 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec; }) public interface Constraint { @JsonProperty(value = "type") - default String type() { + @GenerateApiSpec + default String getType() { return getClass().getSimpleName(); } } diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/NumberConstraint.java b/api/src/main/java/ru/kirillius/XCP/Properties/NumberConstraint.java index 64849cd..a4ea4bf 100644 --- a/api/src/main/java/ru/kirillius/XCP/Properties/NumberConstraint.java +++ b/api/src/main/java/ru/kirillius/XCP/Properties/NumberConstraint.java @@ -12,11 +12,15 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec; @NoArgsConstructor public final class NumberConstraint implements Constraint { @JsonProperty + @GenerateApiSpec private double min; @JsonProperty + @GenerateApiSpec private double max; @JsonProperty + @GenerateApiSpec private double step; @JsonProperty + @GenerateApiSpec private boolean integer; } diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/StringConstraint.java b/api/src/main/java/ru/kirillius/XCP/Properties/StringConstraint.java index bde17e8..2832414 100644 --- a/api/src/main/java/ru/kirillius/XCP/Properties/StringConstraint.java +++ b/api/src/main/java/ru/kirillius/XCP/Properties/StringConstraint.java @@ -12,5 +12,14 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec; @NoArgsConstructor public final class StringConstraint implements Constraint { @JsonProperty + @GenerateApiSpec private int maxLength; + + @JsonProperty + @GenerateApiSpec + private boolean multiline; + + @JsonProperty + @GenerateApiSpec + private String regexp; } diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/ValueListConstraint.java b/api/src/main/java/ru/kirillius/XCP/Properties/ValueListConstraint.java index 56840fe..bc60958 100644 --- a/api/src/main/java/ru/kirillius/XCP/Properties/ValueListConstraint.java +++ b/api/src/main/java/ru/kirillius/XCP/Properties/ValueListConstraint.java @@ -3,6 +3,7 @@ package ru.kirillius.XCP.Properties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; import ru.kirillius.XCP.Commons.GenerateApiSpec; +import tools.jackson.databind.node.ArrayNode; import java.util.Collection; @@ -14,5 +15,6 @@ import java.util.Collection; @NoArgsConstructor public final class ValueListConstraint implements Constraint { @JsonProperty + @GenerateApiSpec(type = ArrayNode.class) private Collection values; } diff --git a/app/src/main/resources/defaultEntities.json b/app/src/main/resources/defaultEntities.json index 4c62b80..5453131 100644 --- a/app/src/main/resources/defaultEntities.json +++ b/app/src/main/resources/defaultEntities.json @@ -9,5 +9,22 @@ "uuid": "00000000-0000-0000-0000-000000000000", "passwordHash": "$argon2id$v=19$m=65536,t=3,p=1$SBqQtx5adxoG53V0TgqmDw$zIy0Wiq53m9r/SOldtCXWXLWbvZuS0F3HHILxpUsLhQ" } + }, + { + "type": "PropertyDescriptor", + "entity": { + "constraints": [ + { + "type": "StringConstraint", + "maxLength": 15, + "multiline": false, + "regexp": "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" + } + ], + "array": false, + "propertyType": "Text", + "name": "host.ip", + "uuid": "00bd0f8a-e9d8-4789-83e5-d2f2eb94f876" + } } ] \ No newline at end of file diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java index 8abef39..912ed07 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java @@ -63,7 +63,8 @@ public final class RepositoryServiceImpl implements RepositoryService { InputRepositoryImpl.class, OutputRepositoryImpl.class, TagRepositoryImpl.class, - UserRepositoryImpl.class + UserRepositoryImpl.class, + PropertyDescriptorRepositoryImpl.class ); configuration = new Configuration(); configuration.configure(); diff --git a/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java b/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java index 68f8858..ea10d31 100644 --- a/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java +++ b/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java @@ -9,6 +9,7 @@ import ru.kirillius.XCP.Commons.Context; import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcServlet; import ru.kirillius.XCP.RPC.Services.Auth; import ru.kirillius.XCP.RPC.Services.Profile; +import ru.kirillius.XCP.RPC.Services.Properties; import ru.kirillius.XCP.RPC.Services.Users; import ru.kirillius.XCP.Services.ServiceLoadPriority; import ru.kirillius.XCP.Services.WebService; @@ -30,7 +31,8 @@ public class WebServiceImpl implements WebService { jsonRpc.registerRpcService( Users.class, Auth.class, - Profile.class + Profile.class, + Properties.class ); var config = context.getConfig(); server = new Server(new InetSocketAddress(config.getHost(), config.getHttpPort())); diff --git a/web-ui/vue-app/scripts/generate-rpc-client.ts b/web-ui/vue-app/scripts/generate-rpc-client.ts index cff87eb..3c1c3c5 100644 --- a/web-ui/vue-app/scripts/generate-rpc-client.ts +++ b/web-ui/vue-app/scripts/generate-rpc-client.ts @@ -30,6 +30,7 @@ interface ApiModule { interface ApiType { name: string type: 'class' | 'enum' + parents?: string[] fields?: Array<{ name: string type: string @@ -77,8 +78,12 @@ ${values.map((value) => ` ${value} = "${value}"`).join(',\n')} }` } else if (apiType.type === 'class') { const fields = apiType.fields || [] - const fieldDefinitions = fields.map((field) => ` ${field.name}: ${field.type};`).join('\n') - return `export interface ${apiType.name} { + const fieldDefinitions = fields + .map((field) => ` ${field.name}: ${field.type === 'array' ? '[]' : field.type};`) + .join('\n') + const parents = apiType.parents || [] + const extendsClause = parents.length > 0 ? ` extends ${parents.join(', ')}` : '' + return `export interface ${apiType.name}${extendsClause} { ${fieldDefinitions} }` } diff --git a/web-ui/vue-app/src/components/PropertyEditor.vue b/web-ui/vue-app/src/components/PropertyEditor.vue new file mode 100644 index 0000000..6268c1e --- /dev/null +++ b/web-ui/vue-app/src/components/PropertyEditor.vue @@ -0,0 +1,369 @@ + + + + + diff --git a/web-ui/vue-app/src/components/PropertyWidget.vue b/web-ui/vue-app/src/components/PropertyWidget.vue new file mode 100644 index 0000000..03f53f5 --- /dev/null +++ b/web-ui/vue-app/src/components/PropertyWidget.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/web-ui/vue-app/src/views/ProfileView.vue b/web-ui/vue-app/src/views/ProfileView.vue index f316660..37458ff 100644 --- a/web-ui/vue-app/src/views/ProfileView.vue +++ b/web-ui/vue-app/src/views/ProfileView.vue @@ -128,34 +128,10 @@ - + - - - - ID пользователя - {{ currentUser?.id }} - - - - - UUID - {{ currentUser?.uuid }} - - - - - Роль - {{ userRole }} - - + @@ -168,6 +144,7 @@ import { ref, computed, onMounted, watch } from 'vue' import { useAppStore } from '@/stores/app' import { useNotificationStore } from '@/stores/notification' +import PropertyEditor from '@/components/PropertyEditor.vue' const appStore = useAppStore() const { showSuccess, showError } = useNotificationStore() @@ -199,6 +176,9 @@ const profileData = ref({ confirmPassword: '' }) +// Свойства пользователя для редактора +const userValues = ref>({}) + const originalData = ref({ login: '', name: '', @@ -227,7 +207,8 @@ const passwordRules = [ ] const confirmPasswordRules = [ - (v: string) => !profileData.newPassword || v === profileData.newPassword || 'Пароли не совпадают' + (v: string) => + !profileData.value.newPassword || v === profileData.value.newPassword || 'Пароли не совпадают' ] const getUserDisplayName = () => { @@ -239,10 +220,7 @@ const getUserDisplayName = () => { } const startEditing = () => { - originalData.value = { - login: profileData.value.login, - name: profileData.value.name - } + originalData.value = { ...profileData.value } isEditing.value = true } @@ -272,7 +250,7 @@ const saveProfile = async () => { const success = await appStore.api.Profile.save( profileData.value.login, profileData.value.name, - currentUser.value?.values || {}, + userValues.value, password, profileData.value.currentPassword ) @@ -312,6 +290,7 @@ const loadProfileData = () => { confirmPassword: '' } originalData.value = { ...profileData.value } + userValues.value = user.values || {} } }