This is an automated email from the ASF dual-hosted git repository.
marat pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
The following commit(s) were added to refs/heads/main by this push:
new 37a0d298 Introduce validation framework (#1041)
37a0d298 is described below
commit 37a0d298105ef07ff4372c7a446644264067e14b
Author: Mario Volf <[email protected]>
AuthorDate: Wed Jan 3 18:11:18 2024 +0100
Introduce validation framework (#1041)
* mvolf - web - Implement copy project functionality
* mvolf - web - Implement copy project functionality
* #973 - Copy project
* #973 - Copy project
* #973 - Don't copy project if existing project-id is used.
* #973 - Fixed code formatting
* #973 - Handle duplicate project id on project create and copy. Some
additional small modifications and fixes.
* Introduced validation framework and used it for create and copy project
actions
---------
Co-authored-by: mvolf <[email protected]>
---
karavan-space/package-lock.json | 2 +-
karavan-web/karavan-app/pom.xml | 4 +
.../apache/camel/karavan/api/ProjectResource.java | 27 +---
.../camel/karavan/infinispan/model/Project.java | 5 +
.../camel/karavan/service/ProjectService.java | 25 ++-
.../apache/camel/karavan/shared/error/Error.java | 78 ++++++++++
.../camel/karavan/shared/error/ErrorResponse.java | 59 ++++++++
.../exception/ExceptionToResponseMapper.java | 66 ++++++++
.../shared/exception/ProjectExistsException.java | 7 -
.../shared/exception/ValidationException.java | 40 +++++
.../karavan/shared/validation/SimpleValidator.java | 33 ++++
.../karavan/shared/validation/ValidationError.java | 59 ++++++++
.../shared/validation/ValidationResult.java | 45 ++++++
.../camel/karavan/shared/validation/Validator.java | 23 +++
.../validation/project/ProjectModifyValidator.java | 40 +++++
.../karavan-app/src/main/webui/package-lock.json | 75 +++++++++
.../karavan-app/src/main/webui/package.json | 6 +-
.../src/main/webui/src/api/KaravanApi.tsx | 35 +----
.../src/main/webui/src/api/ProjectService.ts | 6 +-
.../main/webui/src/projects/CreateProjectModal.tsx | 164 ++++++++++++++------
.../main/webui/src/services/CreateServiceModal.tsx | 167 +++++++++++++++------
.../src/main/webui/src/shared/error/Error.ts | 11 ++
.../main/webui/src/shared/error/ErrorResponse.ts | 15 ++
.../webui/src/shared/error/ProjectExistsError.ts | 6 -
.../src/shared/error/UseResponseErrorHandler.ts | 43 ++++++
25 files changed, 863 insertions(+), 178 deletions(-)
diff --git a/karavan-space/package-lock.json b/karavan-space/package-lock.json
index 902d4ddc..71287714 100644
--- a/karavan-space/package-lock.json
+++ b/karavan-space/package-lock.json
@@ -45,7 +45,7 @@
}
},
"../karavan-core": {
- "version": "4.1.1",
+ "version": "4.3.0",
"license": "Apache-2.0",
"dependencies": {
"@types/js-yaml": "^4.0.7",
diff --git a/karavan-web/karavan-app/pom.xml b/karavan-web/karavan-app/pom.xml
index 59c20d87..4481d289 100644
--- a/karavan-web/karavan-app/pom.xml
+++ b/karavan-web/karavan-app/pom.xml
@@ -146,6 +146,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
</dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-hibernate-validator</artifactId>
+ </dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
index 29541520..d9d95f66 100644
---
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
@@ -30,7 +30,6 @@ import org.apache.camel.karavan.infinispan.model.Project;
import org.apache.camel.karavan.kubernetes.KubernetesService;
import org.apache.camel.karavan.service.ConfigService;
import org.apache.camel.karavan.service.ProjectService;
-import org.apache.camel.karavan.shared.exception.ProjectExistsException;
import org.jboss.logging.Logger;
import java.net.URLDecoder;
@@ -82,17 +81,8 @@ public class ProjectResource {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
- public Response save(Project project) throws Exception {
- try {
- Project createdProject = projectService.save(project);
- return Response.ok().entity(createdProject).build();
- } catch (ProjectExistsException exception) {
- LOGGER.error(exception.getMessage());
- return
Response.status(Response.Status.CONFLICT).entity(exception.getMessage()).build();
- } catch (Exception exception) {
- LOGGER.error(exception.getMessage());
- return
Response.serverError().entity(exception.getMessage()).build();
- }
+ public Project save(Project project) {
+ return projectService.save(project);
}
@DELETE
@@ -180,16 +170,7 @@ public class ProjectResource {
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("/copy/{sourceProject}")
- public Response copy(@PathParam("sourceProject") String sourceProject,
Project project) throws Exception {
- try {
- Project copiedProject = projectService.copy(sourceProject,
project);
- return Response.ok().entity(copiedProject).build();
- } catch (ProjectExistsException exception) {
- LOGGER.error(exception.getMessage());
- return
Response.status(Response.Status.CONFLICT).entity(exception.getMessage()).build();
- } catch (Exception exception) {
- LOGGER.error(exception.getMessage());
- return
Response.serverError().entity(exception.getMessage()).build();
- }
+ public Project copy(@PathParam("sourceProject") String sourceProject,
Project project) {
+ return projectService.copy(sourceProject, project);
}
}
\ No newline at end of file
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/Project.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/Project.java
index 6c4beac4..87311fac 100644
---
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/Project.java
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/Project.java
@@ -23,6 +23,8 @@ import org.infinispan.protostream.annotations.ProtoField;
import java.time.Instant;
+import jakarta.validation.constraints.NotBlank;
+
public class Project {
public static final String CACHE = "projects";
@@ -36,10 +38,13 @@ public class Project {
}
@ProtoField(number = 1)
+ @NotBlank
String projectId;
@ProtoField(number = 2)
+ @NotBlank
String name;
@ProtoField(number = 3)
+ @NotBlank
String description;
@ProtoField(number = 4)
String lastCommit;
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
index 75d0f004..64522eb6 100644
---
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
@@ -36,7 +36,7 @@ import org.apache.camel.karavan.kubernetes.KubernetesService;
import org.apache.camel.karavan.registry.RegistryService;
import org.apache.camel.karavan.shared.Constants;
import org.apache.camel.karavan.shared.Property;
-import org.apache.camel.karavan.shared.exception.ProjectExistsException;
+import org.apache.camel.karavan.validation.project.ProjectModifyValidator;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -62,6 +62,10 @@ public class ProjectService implements HealthCheck {
@ConfigProperty(name = "karavan.environment")
String environment;
+
+ @Inject
+ ProjectModifyValidator projectModifyValidator;
+
@Inject
InfinispanService infinispanService;
@@ -167,11 +171,8 @@ public class ProjectService implements HealthCheck {
}
}
- public Project save(Project project) throws Exception {
- boolean projectExists =
infinispanService.getProject(project.getProjectId()) != null;
- if(projectExists) {
- throw new ProjectExistsException("Project with project id [" +
project.getProjectId() + "] already exists");
- }
+ public Project save(Project project) {
+ projectModifyValidator.validate(project).failOnError();
infinispanService.saveProject(project);
@@ -188,11 +189,9 @@ public class ProjectService implements HealthCheck {
return project;
}
- public Project copy(String sourceProjectId, Project project) throws
Exception {
- boolean projectExists =
infinispanService.getProject(project.getProjectId()) != null;
- if(projectExists) {
- throw new ProjectExistsException("Project with project id [" +
project.getProjectId() + "] already exists");
- }
+ public Project copy(String sourceProjectId, Project project) {
+ projectModifyValidator.validate(project).failOnError();
+
Project sourceProject = infinispanService.getProject(sourceProjectId);
// Save project
@@ -208,7 +207,7 @@ public class ProjectService implements HealthCheck {
e -> {
ProjectFile file = e.getValue();
file.setProjectId(project.getProjectId());
- if(Objects.equals(file.getName(),
APPLICATION_PROPERTIES_FILENAME)) {
+ if (Objects.equals(file.getName(),
APPLICATION_PROPERTIES_FILENAME)) {
modifyPropertyFileOnProjectCopy(file,
sourceProject, project);
}
return file;
@@ -219,7 +218,7 @@ public class ProjectService implements HealthCheck {
if (!ConfigService.inKubernetes()) {
ProjectFile projectCompose =
codeService.createInitialProjectCompose(project);
infinispanService.saveProjectFile(projectCompose);
- } else if (kubernetesService.isOpenshift()){
+ } else if (kubernetesService.isOpenshift()) {
ProjectFile projectCompose =
codeService.createInitialDeployment(project);
infinispanService.saveProjectFile(projectCompose);
}
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/Error.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/Error.java
new file mode 100644
index 00000000..dee5372e
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/Error.java
@@ -0,0 +1,78 @@
+package org.apache.camel.karavan.shared.error;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class Error {
+ private String field;
+ private String message;
+ private String code;
+
+ public Error(String message) {
+ this.message = message;
+ }
+
+ public Error(String field, String message) {
+ this.field = field;
+ this.message = message;
+ }
+
+ public Error(String field, String message, String code) {
+ this.field = field;
+ this.message = message;
+ this.code = code;
+ }
+
+ public String getField() {
+ return field;
+ }
+
+ public void setField(String field) {
+ this.field = field;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Error error = (Error) o;
+ return Objects.equals(field, error.field) && Objects.equals(message,
error.message)
+ && Objects.equals(code, error.code);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(field, message, code);
+ }
+
+ @Override
+ public String toString() {
+ return "Error{" +
+ "field='" + field + '\'' +
+ ", message='" + message + '\'' +
+ ", code='" + code + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/ErrorResponse.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/ErrorResponse.java
new file mode 100644
index 00000000..9749bdf8
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/ErrorResponse.java
@@ -0,0 +1,59 @@
+package org.apache.camel.karavan.shared.error;
+
+import java.util.Collection;
+import java.util.Date;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ErrorResponse {
+ private final Date timestamp;
+ private final int status;
+ private final String error;
+ private final String message;
+ private Collection<Error> errors;
+
+ public ErrorResponse(
+ int httpStatus,
+ String reasonPhrase,
+ String message
+ ) {
+ this.timestamp = new Date();
+ this.status = httpStatus;
+ this.error = reasonPhrase;
+ this.message = message;
+ }
+
+ public ErrorResponse(
+ int httpStatus,
+ String reasonPhrase,
+ String message,
+ Collection<Error> errors
+ ) {
+ this.timestamp = new Date();
+ this.status = httpStatus;
+ this.error = reasonPhrase;
+ this.message = message;
+ this.errors = errors;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public Collection<Error> getErrors() {
+ return errors;
+ }
+}
\ No newline at end of file
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ExceptionToResponseMapper.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ExceptionToResponseMapper.java
new file mode 100644
index 00000000..b26b49fc
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ExceptionToResponseMapper.java
@@ -0,0 +1,66 @@
+package org.apache.camel.karavan.shared.exception;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.camel.karavan.shared.error.Error;
+import org.apache.camel.karavan.shared.error.ErrorResponse;
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.reactive.RestResponse;
+import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
+
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+public class ExceptionToResponseMapper {
+ private static final Logger LOGGER =
Logger.getLogger(ExceptionToResponseMapper.class.getName());
+
+ @ServerExceptionMapper
+ public RestResponse<Object> validationException(ValidationException
exception) {
+ List<Error> errors = (exception).getErrors()
+ .stream()
+ .map(fieldError -> new Error(fieldError.getField(),
fieldError.getMessage()))
+ .toList();
+
+ return logAndBuildResponse(
+ exception,
+ Response.Status.BAD_REQUEST.getStatusCode(),
+ Response.Status.BAD_REQUEST.getReasonPhrase(),
+ errors
+ );
+ }
+
+ private RestResponse<Object> logAndBuildResponse(
+ Throwable exception,
+ int status,
+ String reasonPhrase,
+ Collection<Error> errors
+ ) {
+ LOGGER.error("Error occurred", exception);
+
+ String cause = (exception.getCause() != null) ?
exception.getCause().getMessage() : null;
+ String message = (cause != null) ? exception.getMessage() + ", caused
by: " + cause : exception.getMessage();
+
+ if (message == null) {
+ message = exception.getClass().toString();
+ }
+
+ // Hide errors array if there are no errors and leave just error
message
+ if (errors != null && errors.isEmpty()) {
+ errors = null;
+ }
+
+ ErrorResponse responseBody = new ErrorResponse(
+ status,
+ reasonPhrase,
+ message,
+ errors
+ );
+
+ return RestResponse.ResponseBuilder
+ .create(status)
+ .entity(responseBody)
+ .type(MediaType.APPLICATION_JSON)
+ .build();
+ }
+}
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ProjectExistsException.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ProjectExistsException.java
deleted file mode 100644
index 478028bb..00000000
---
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ProjectExistsException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.apache.camel.karavan.shared.exception;
-
-public class ProjectExistsException extends RuntimeException {
- public ProjectExistsException(String message) {
- super(message);
- }
-}
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ValidationException.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ValidationException.java
new file mode 100644
index 00000000..c56fb075
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ValidationException.java
@@ -0,0 +1,40 @@
+package org.apache.camel.karavan.shared.exception;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.camel.karavan.shared.validation.ValidationError;
+
+public class ValidationException extends RuntimeException {
+ private final transient Collection<ValidationError> errors;
+
+ public ValidationException(String message, Collection<ValidationError>
errors) {
+ super(message);
+ this.errors = errors;
+ }
+
+ public ValidationException(String message, ValidationError error) {
+ super(message);
+ this.errors = List.of(error);
+ }
+
+ public ValidationException(String message) {
+ super(message);
+ this.errors = List.of();
+ }
+
+ public ValidationError getFirstErrorOrNull() {
+ return errors.stream().findFirst().orElse(null);
+ }
+
+ public Collection<ValidationError> getErrors() {
+ return errors;
+ }
+
+ @Override
+ public String toString() {
+ return "ValidationException{" +
+ "errors=" + errors +
+ '}';
+ }
+}
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/SimpleValidator.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/SimpleValidator.java
new file mode 100644
index 00000000..b9d095cd
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/SimpleValidator.java
@@ -0,0 +1,33 @@
+package org.apache.camel.karavan.shared.validation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validator;
+
+@ApplicationScoped
+public class SimpleValidator {
+ private final Validator validator;
+
+ public SimpleValidator(Validator validator) {
+ this.validator = validator;
+ }
+
+ public <T> void validate(T object, List<ValidationError> errors) {
+ Set<ConstraintViolation<T>> violations = validator.validate(object);
+
+ violations
+ .forEach(violation -> errors.add(new
ValidationError(violation.getPropertyPath().toString(),
violation.getMessage())));
+ }
+
+ public <T> ValidationResult validate(T object) {
+ List<ValidationError> errors = new ArrayList<>();
+
+ validate(object, errors);
+
+ return new ValidationResult(errors);
+ }
+}
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationError.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationError.java
new file mode 100644
index 00000000..7c9e3b39
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationError.java
@@ -0,0 +1,59 @@
+package org.apache.camel.karavan.shared.validation;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+public class ValidationError implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1905122041950251207L;
+
+ private String field;
+ private String message;
+
+ public ValidationError(String field, String message) {
+ this.field = field;
+ this.message = message;
+ }
+
+ public String getField() {
+ return field;
+ }
+
+ public void setField(String field) {
+ this.field = field;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ValidationError that = (ValidationError) o;
+ return Objects.equals(field, that.field) && Objects.equals(message,
that.message);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(field, message);
+ }
+
+ @Override
+ public String toString() {
+ return "ValidationError{" +
+ "field='" + field + '\'' +
+ ", message='" + message + '\'' +
+ '}';
+ }
+}
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationResult.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationResult.java
new file mode 100644
index 00000000..e9563634
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationResult.java
@@ -0,0 +1,45 @@
+package org.apache.camel.karavan.shared.validation;
+
+import java.util.List;
+
+import org.apache.camel.karavan.shared.exception.ValidationException;
+
+public class ValidationResult {
+ private List<ValidationError> errors;
+
+ ValidationResult(List<ValidationError> errors) {
+ this.errors = errors;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private List<ValidationError> errors;
+
+ Builder() {}
+
+ public Builder errors(List<ValidationError> errors) {
+ this.errors = errors;
+ return this;
+ }
+
+ public ValidationResult build() {
+ return new ValidationResult(errors);
+ }
+
+ @Override
+ public String toString() {
+ return "Builder{" +
+ "errors=" + errors +
+ '}';
+ }
+ }
+
+ public void failOnError() {
+ if (!errors.isEmpty()) {
+ throw new ValidationException("Object failed validation", errors);
+ }
+ }
+}
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/Validator.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/Validator.java
new file mode 100644
index 00000000..c48fbfc5
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/Validator.java
@@ -0,0 +1,23 @@
+package org.apache.camel.karavan.shared.validation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class Validator<T> {
+ public ValidationResult validate(T object) {
+ List<ValidationError> errors = new ArrayList<>();
+
+ validationRules(object, errors);
+
+ return ValidationResult.builder()
+ .errors(errors)
+ .build();
+ }
+
+ protected abstract void validationRules(T object, List<ValidationError>
errors);
+
+ public static class ValidationErrors {
+ private ValidationErrors() {
+ }
+ }
+}
diff --git
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectModifyValidator.java
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectModifyValidator.java
new file mode 100644
index 00000000..3ba08dac
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectModifyValidator.java
@@ -0,0 +1,40 @@
+package org.apache.camel.karavan.validation.project;
+
+import java.util.List;
+
+import org.apache.camel.karavan.infinispan.InfinispanService;
+import org.apache.camel.karavan.infinispan.model.Project;
+import org.apache.camel.karavan.shared.validation.SimpleValidator;
+import org.apache.camel.karavan.shared.validation.ValidationError;
+import org.apache.camel.karavan.shared.validation.Validator;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class ProjectModifyValidator extends Validator<Project> {
+ private static final List<String> FORBIDDEN_PROJECT_ID_VALUES =
List.of("templates", "kamelets");
+
+ private final SimpleValidator simpleValidator;
+ private final InfinispanService infinispanService;
+
+ public ProjectModifyValidator(SimpleValidator simpleValidator,
InfinispanService infinispanService) {
+ this.simpleValidator = simpleValidator;
+ this.infinispanService = infinispanService;
+ }
+
+
+ @Override
+ protected void validationRules(Project value, List<ValidationError>
errors) {
+ simpleValidator.validate(value, errors);
+
+ boolean projectIdExists =
infinispanService.getProject(value.getProjectId()) != null;
+
+ if(projectIdExists) {
+ errors.add(new ValidationError("projectId", "Project ID already
exists"));
+ }
+
+ if(FORBIDDEN_PROJECT_ID_VALUES.contains(value.getProjectId())) {
+ errors.add(new ValidationError("projectId", "'templates' or
'kamelets' can't be used as project ID"));
+ }
+ }
+}
diff --git a/karavan-web/karavan-app/src/main/webui/package-lock.json
b/karavan-web/karavan-app/src/main/webui/package-lock.json
index 67918528..67f8f5ab 100644
--- a/karavan-web/karavan-app/src/main/webui/package-lock.json
+++ b/karavan-web/karavan-app/src/main/webui/package-lock.json
@@ -8,6 +8,8 @@
"name": "karavan",
"version": "4.3.1",
"dependencies": {
+ "@hookform/error-message": "^2.0.1",
+ "@hookform/resolvers": "^2.9.10",
"@microsoft/fetch-event-source": "^2.0.1",
"@monaco-editor/react": "4.6.0",
"@patternfly/patternfly": "^5.1.0",
@@ -29,9 +31,11 @@
"keycloak-js": "23.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "^7.49.1",
"react-router-dom": "^6.15.0",
"rxjs": "7.8.1",
"uuid": "9.0.1",
+ "yup": "^1.3.2",
"zustand": "^4.4.3"
},
"devDependencies": {
@@ -2530,6 +2534,24 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@hookform/error-message": {
+ "version": "2.0.1",
+ "resolved":
"https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz",
+ "integrity":
"sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0",
+ "react-hook-form": "^7.0.0"
+ }
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "2.9.11",
+ "resolved":
"https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.11.tgz",
+ "integrity":
"sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
"resolved":
"https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
@@ -17725,6 +17747,11 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/property-expr": {
+ "version": "2.0.6",
+ "resolved":
"https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
+ "integrity":
"sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
+ },
"node_modules/property-information": {
"version": "6.4.0",
"resolved":
"https://registry.npmjs.org/property-information/-/property-information-6.4.0.tgz",
@@ -18079,6 +18106,22 @@
"resolved":
"https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity":
"sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
+ "node_modules/react-hook-form": {
+ "version": "7.49.2",
+ "resolved":
"https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz",
+ "integrity":
"sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==",
+ "engines": {
+ "node": ">=18",
+ "pnpm": "8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -20442,6 +20485,11 @@
"integrity":
"sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
+ "node_modules/tiny-case": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
+ "integrity":
"sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -20478,6 +20526,11 @@
"node": ">=0.6"
}
},
+ "node_modules/toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity":
"sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
+ },
"node_modules/tough-cookie": {
"version": "4.1.3",
"resolved":
"https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
@@ -22571,6 +22624,28 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/yup": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-1.3.3.tgz",
+ "integrity":
"sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw==",
+ "dependencies": {
+ "property-expr": "^2.0.5",
+ "tiny-case": "^1.0.3",
+ "toposort": "^2.0.2",
+ "type-fest": "^2.19.0"
+ }
+ },
+ "node_modules/yup/node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved":
"https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity":
"sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/zustand": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz",
diff --git a/karavan-web/karavan-app/src/main/webui/package.json
b/karavan-web/karavan-app/src/main/webui/package.json
index 92130f83..3b8aa31d 100644
--- a/karavan-web/karavan-app/src/main/webui/package.json
+++ b/karavan-web/karavan-app/src/main/webui/package.json
@@ -40,6 +40,8 @@
"@types/js-yaml": "4.0.7",
"@types/node": "18.16.3",
"@types/uuid": "9.0.1",
+ "@hookform/error-message": "^2.0.1",
+ "@hookform/resolvers": "^2.9.10",
"@uiw/react-markdown-preview": "^5.0.3",
"axios": "1.6.2",
"buffer": "6.0.3",
@@ -50,10 +52,12 @@
"keycloak-js": "23.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "^7.49.1",
"react-router-dom": "^6.15.0",
"rxjs": "7.8.1",
"uuid": "9.0.1",
- "zustand": "^4.4.3"
+ "zustand": "^4.4.3",
+ "yup": "^1.3.2"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
index e5d54d05..95ad0698 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
@@ -28,7 +28,6 @@ import {Buffer} from 'buffer';
import {SsoApi} from "./SsoApi";
import {EventStreamContentType, fetchEventSource} from
"@microsoft/fetch-event-source";
import {ProjectEventBus} from "./ProjectEventBus";
-import {ProjectExistsError} from "../shared/error/ProjectExistsError";
axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.common['Content-Type'] = 'application/json';
@@ -220,38 +219,12 @@ export class KaravanApi {
});
}
- static async postProject(project: Project): Promise<[Error | null, Project
| null]> {
- return instance.post('/api/project', project)
- .then(res => {
- if(res.status === 200 || res.status === 201) {
- return [null, res.data as Project] as [null, Project]
- } else {
- return [Error("Error while creating project"), null] as
[Error, null]
- }
- }).catch(err => {
- if(err.response?.status === 409) {
- return [new ProjectExistsError("Project with id " +
project.projectId + " already exists."), null] as [Error, null]
- } else {
- return [err as Error, null] as [Error, null];
- }
- });
+ static async postProject(project: Project) {
+ return instance.post('/api/project', project);
}
- static async copyProject(sourceProject: string, project: Project):
Promise<[Error | null, Project | null]> {
- return instance.post('/api/project/copy/' + sourceProject, project)
- .then(res => {
- if(res.status === 200 || res.status === 201) {
- return [null, res.data as Project] as [null, Project]
- } else {
- return [Error("Error while copying project"), null] as
[Error, null]
- }
- }).catch(err => {
- if(err.response?.status === 409) {
- return [new ProjectExistsError("Project with id " +
project.projectId + " already exists."), null] as [Error, null]
- } else {
- return [err as Error, null] as [Error, null];
- }
- });
+ static async copyProject(sourceProject: string, project: Project) {
+ return instance.post('/api/project/copy/' + sourceProject, project);
}
static async deleteProject(project: Project, deleteContainers: boolean,
after: (res: AxiosResponse<any>) => void) {
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
index 6f567937..4cefba6a 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
@@ -237,11 +237,13 @@ export class ProjectService {
}
public static async createProject(project: Project) {
- return KaravanApi.postProject(project);
+ const result = await KaravanApi.postProject(project);
+ return result.data;
}
public static async copyProject(sourceProject: string, project: Project) {
- return KaravanApi.copyProject(sourceProject, project);
+ const result = await KaravanApi.copyProject(sourceProject, project);
+ return result.data;
}
public static createFile(file: ProjectFile) {
diff --git
a/karavan-web/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx
b/karavan-web/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx
index b92a2218..2e1d670d 100644
--- a/karavan-web/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx
@@ -17,58 +17,110 @@
import React, {useState} from 'react';
import {
- Button, Form, FormGroup, FormHelperText, HelperText, HelperTextItem,
+ Alert,
+ Button,
+ Divider,
+ Form,
+ FormGroup,
+ Grid,
Modal,
- ModalVariant, TextInput, Text
+ ModalVariant,
+ Text,
+ TextInput
} from '@patternfly/react-core';
import '../designer/karavan.css';
import {useProjectStore} from "../api/ProjectStore";
import {ProjectService} from "../api/ProjectService";
import {Project} from "../api/ProjectModels";
import {CamelUi} from "../designer/utils/CamelUi";
-import {shallow} from "zustand/shallow";
import {isEmpty} from "../util/StringUtils";
import {EventBus} from "../designer/utils/EventBus";
-import {ProjectExistsError} from "../shared/error/ProjectExistsError";
+import {useResponseErrorHandler} from
"../shared/error/UseResponseErrorHandler";
+import {useForm} from "react-hook-form";
+import * as yup from 'yup';
+import {yupResolver} from '@hookform/resolvers/yup';
+import {AxiosError} from "axios";
export function CreateProjectModal () {
- const [operation, project, setOperation] = useProjectStore((s) =>
[s.operation, s.project, s.setOperation], shallow)
+ const formValidationSchema = yup.object().shape({
+ name: yup
+ .string()
+ .required("Project name is required"),
+ description: yup
+ .string()
+ .required("Project description is required"),
+ projectId: yup
+ .string()
+ .required("Project ID is required")
+ .notOneOf(['templates', 'kamelets'], "'templates' or 'kamelets'
can't be used as project ID")
+ });
+
+ const defaultFormValues = {
+ name: "",
+ description: "",
+ projectId: ""
+ };
+
+ const responseToFormErrorFields = new Map<string, string>([
+ ["projectId", "projectId"],
+ ["name", "name"],
+ ["description", "description"]
+ ]);
+
+ const {
+ register,
+ setError,
+ handleSubmit,
+ formState: { errors },
+ reset
+ } = useForm({
+ resolver: yupResolver(formValidationSchema),
+ mode: "onChange",
+ defaultValues: defaultFormValues
+ });
+
+ const {project, operation, setOperation} = useProjectStore();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [projectId, setProjectId] = useState('');
- const [isValidationError, setIsValidationError] = useState(false);
+ const [globalErrors, registerResponseErrors, resetGlobalErrors] =
useResponseErrorHandler(
+ responseToFormErrorFields,
+ setError
+ );
- function cleanValues() {
- setName("");
- setDescription("");
- setProjectId("");
+ function resetForm() {
+ resetGlobalErrors();
+ reset(defaultFormValues);
}
function closeModal() {
- setOperation('none');
- cleanValues();
+ setOperation("none");
+ resetForm();
}
- async function handleFormSubmit() {
- setIsValidationError(false);
- const [ err, createdProject ] = operation !== 'copy' ?
- await ProjectService.createProject(new Project({name: name,
description: description, projectId: projectId})) :
- await ProjectService.copyProject(project?.projectId, new
Project({name: name, description: description, projectId: projectId}));
-
- if (createdProject !== null) {
- EventBus.sendAlert( 'Success', 'Project created', 'success');
- ProjectService.refreshProjectData(project.projectId);
- ProjectService.refreshProjects();
- setOperation('none');
- cleanValues();
- } else if (err !== null && err instanceof ProjectExistsError) {
- setIsValidationError(true);
- } else {
- operation !== 'copy' ?
- EventBus.sendAlert( 'Warning', 'Error when creating project:'
+ err?.message, 'warning') :
- EventBus.sendAlert( 'Warning', 'Error when copying project:' +
err?.message, 'warning');
- }
+ function handleFormSubmit() {
+ const action = operation !== "copy" ?
+ ProjectService.createProject(new Project({name: name, description:
description, projectId: projectId})) :
+ ProjectService.copyProject(project?.projectId, new Project({name:
name, description: description, projectId: projectId}))
+
+ return action
+ .then(() => handleOnFormSubmitSuccess())
+ .catch((error) => handleOnFormSubmitFailure(error));
+ }
+
+ function handleOnFormSubmitSuccess () {
+ const message = operation !== "copy" ? "Project successfully created."
: "Project successfully copied.";
+
+ EventBus.sendAlert( "Success", message, "success");
+ ProjectService.refreshProjectData(projectId);
+ ProjectService.refreshProjects();
+ setOperation("none");
+ resetForm();
+ }
+
+ function handleOnFormSubmitFailure(error: AxiosError) {
+ registerResponseErrors(error);
}
function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
@@ -77,7 +129,6 @@ export function CreateProjectModal () {
}
}
- const isReady = projectId && name && description && !['templates',
'kamelets'].includes(projectId);
return (
<Modal
title={operation !== 'copy' ? "Create new project" : "Copy project
from " + project?.projectId}
@@ -86,37 +137,56 @@ export function CreateProjectModal () {
onClose={closeModal}
onKeyDown={onKeyDown}
actions={[
- <Button key="confirm" variant="primary" isDisabled={!isReady}
- onClick={handleFormSubmit}>Save</Button>,
+ <Button key="confirm" variant="primary"
onClick={handleSubmit(handleFormSubmit)}>Save</Button>,
<Button key="cancel" variant="secondary"
onClick={closeModal}>Cancel</Button>
]}
className="new-project"
>
<Form isHorizontal={true} autoComplete="off">
<FormGroup label="Name" fieldId="name" isRequired>
- <TextInput className="text-field" type="text" id="name"
name="name"
+ <TextInput className="text-field" type="text" id="name"
value={name}
- onChange={(_, e) => setName(e)}/>
+ validated={!!errors.name ? 'error' : 'default'}
+ {...register('name')}
+ onChange={(e, v) => {
+ setName(v);
+ register('name').onChange(e);
+ }}
+ />
+ {!!errors.name && <Text style={{ color: 'red', fontStyle:
'italic'}}>{errors?.name?.message}</Text>}
</FormGroup>
<FormGroup label="Description" fieldId="description"
isRequired>
- <TextInput className="text-field" type="text"
id="description" name="description"
+ <TextInput className="text-field" type="text"
id="description"
value={description}
- onChange={(_, e) => setDescription(e)}/>
+ validated={!!errors.description ? 'error' :
'default'}
+ {...register('description')}
+ onChange={(e, v) => {
+ setDescription(v);
+ register('description').onChange(e);
+ }}
+ />
+ {!!errors.description && <Text style={{ color: 'red',
fontStyle: 'italic'}}>{errors?.description?.message}</Text>}
</FormGroup>
<FormGroup label="Project ID" fieldId="projectId" isRequired>
- <TextInput className="text-field" type="text"
id="projectId" name="projectId"
+ <TextInput className="text-field" type="text"
id="projectId"
value={projectId}
onFocus={e => setProjectId(projectId === '' ?
CamelUi.nameFromTitle(name) : projectId)}
- onChange={(_, e) =>
setProjectId(CamelUi.nameFromTitle(e))}
- validated={isValidationError ? 'error' :
'default'}
+ validated={!!errors.projectId ? 'error' :
'default'}
+ {...register('projectId')}
+ onChange={(e, v) => {
+ setProjectId(CamelUi.nameFromTitle(v));
+ register('projectId').onChange(e);
+ }}
/>
- {isValidationError && <Text style={{ color: 'red',
fontStyle: 'italic'}}>Project ID must be unique</Text>}
- <FormHelperText>
- <HelperText>
- <HelperTextItem>Unique project
name</HelperTextItem>
- </HelperText>
- </FormHelperText>
+ {!!errors.projectId && <Text style={{ color: 'red',
fontStyle: 'italic'}}>{errors?.projectId?.message}</Text>}
</FormGroup>
+ <Grid>
+ {globalErrors &&
+ globalErrors.map((error) => (
+ <Alert title={error} key={error}
variant="danger"></Alert>
+ ))}
+ <Divider role="presentation" />
+ </Grid>
</Form>
</Modal>
)
diff --git
a/karavan-web/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx
b/karavan-web/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx
index 38977138..22f7b67a 100644
--- a/karavan-web/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx
@@ -17,56 +17,111 @@
import React, {useState} from 'react';
import {
- Button, Form, FormGroup, FormHelperText, HelperText, HelperTextItem,
+ Alert,
+ Button,
+ Divider,
+ Form,
+ FormGroup,
+ Grid,
Modal,
- ModalVariant, TextInput, Text
+ ModalVariant,
+ Text,
+ TextInput
} from '@patternfly/react-core';
import '../designer/karavan.css';
-import {useAppConfigStore, useProjectStore} from "../api/ProjectStore";
+import {useProjectStore} from "../api/ProjectStore";
import {ProjectService} from "../api/ProjectService";
import {Project} from "../api/ProjectModels";
import {CamelUi} from "../designer/utils/CamelUi";
import {EventBus} from "../designer/utils/EventBus";
-import {ProjectExistsError} from "../shared/error/ProjectExistsError";
+import {useResponseErrorHandler} from
"../shared/error/UseResponseErrorHandler";
+import {useForm} from "react-hook-form";
+import * as yup from 'yup';
+import {yupResolver} from '@hookform/resolvers/yup';
+import {AxiosError} from "axios";
export function CreateServiceModal () {
- const {project, operation} = useProjectStore();
+ const formValidationSchema = yup.object().shape({
+ name: yup
+ .string()
+ .required("Project name is required"),
+ description: yup
+ .string()
+ .required("Project description is required"),
+ projectId: yup
+ .string()
+ .required("Project ID is required")
+ .notOneOf(['templates', 'kamelets'], "'templates' or 'kamelets'
can't be used as project ID")
+ });
+
+ const defaultFormValues = {
+ name: "",
+ description: "",
+ projectId: ""
+ };
+
+ const responseToFormErrorFields = new Map<string, string>([
+ ["projectId", "projectId"],
+ ["name", "name"],
+ ["description", "description"]
+ ]);
+
+ const {
+ register,
+ setError,
+ handleSubmit,
+ formState: { errors },
+ reset
+ } = useForm({
+ resolver: yupResolver(formValidationSchema),
+ mode: "onChange",
+ defaultValues: defaultFormValues
+ });
+
+ const {project, operation, setOperation} = useProjectStore();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [runtime, setRuntime] = useState('');
const [projectId, setProjectId] = useState('');
- const {config} = useAppConfigStore();
- const [isValidationError, setIsValidationError] = useState(false);
-
- function cleanValues() {
- setName("");
- setDescription("");
- setRuntime("");
- setProjectId("");
+ const [globalErrors, registerResponseErrors, resetGlobalErrors] =
useResponseErrorHandler(
+ responseToFormErrorFields,
+ setError
+ );
+
+ function resetForm() {
+ resetGlobalErrors();
+ reset(defaultFormValues);
}
- function closeModal () {
- useProjectStore.setState({operation: "none"});
- cleanValues();
+ function closeModal() {
+ setOperation("none");
+ resetForm();
}
- async function handleFormSubmit () {
- setIsValidationError(false);
- const [ err, createdProject ] = await ProjectService.createProject(new
Project({name: name, description: description, projectId: projectId}));
-
- if (createdProject !== null) {
- EventBus.sendAlert( 'Success', 'Project created', 'success');
- ProjectService.refreshProjectData(project.projectId);
- ProjectService.refreshProjects();
- useProjectStore.setState({operation: "none"});
- cleanValues();
- } else if (err !== null && err instanceof ProjectExistsError) {
- setIsValidationError(true);
- } else {
- EventBus.sendAlert( 'Warning', 'Error when creating project:' +
err?.message, 'warning');
- }
+ function handleFormSubmit() {
+ const action = operation !== "copy" ?
+ ProjectService.createProject(new Project({name: name, description:
description, projectId: projectId})) :
+ ProjectService.copyProject(project?.projectId, new Project({name:
name, description: description, projectId: projectId}))
+
+ return action
+ .then(() => handleOnFormSubmitSuccess())
+ .catch((error) => handleOnFormSubmitFailure(error));
+ }
+
+ function handleOnFormSubmitSuccess () {
+ const message = operation !== "copy" ? "Project successfully created."
: "Project successfully copied.";
+
+ EventBus.sendAlert( "Success", message, "success");
+ ProjectService.refreshProjectData(projectId);
+ ProjectService.refreshProjects();
+ setOperation("none");
+ resetForm();
+ }
+
+ function handleOnFormSubmitFailure(error: AxiosError) {
+ registerResponseErrors(error);
}
function onKeyDown (event: React.KeyboardEvent<HTMLDivElement>): void {
@@ -75,7 +130,6 @@ export function CreateServiceModal () {
}
}
- const isReady = projectId && name && description && !['templates',
'kamelets'].includes(projectId);
return (
<Modal
title={operation !== 'copy' ? "Create new project" : "Copy project
from " + project?.projectId}
@@ -84,37 +138,56 @@ export function CreateServiceModal () {
onClose={closeModal}
onKeyDown={onKeyDown}
actions={[
- <Button key="confirm" variant="primary" isDisabled={!isReady}
- onClick={handleFormSubmit}>Save</Button>,
+ <Button key="confirm" variant="primary"
onClick={handleSubmit(handleFormSubmit)}>Save</Button>,
<Button key="cancel" variant="secondary"
onClick={closeModal}>Cancel</Button>
]}
className="new-project"
>
<Form isHorizontal={true} autoComplete="off">
<FormGroup label="Name" fieldId="name" isRequired>
- <TextInput className="text-field" type="text" id="name"
name="name"
+ <TextInput className="text-field" type="text" id="name"
value={name}
- onChange={(_, e) => setName(e)}/>
+ validated={!!errors.name ? 'error' : 'default'}
+ {...register('name')}
+ onChange={(e, v) => {
+ setName(v);
+ register('name').onChange(e);
+ }}
+ />
+ {!!errors.name && <Text style={{ color: 'red', fontStyle:
'italic'}}>{errors?.name?.message}</Text>}
</FormGroup>
<FormGroup label="Description" fieldId="description"
isRequired>
- <TextInput className="text-field" type="text"
id="description" name="description"
+ <TextInput className="text-field" type="text"
id="description"
value={description}
- onChange={(_, e) => setDescription(e)}/>
+ validated={!!errors.description ? 'error' :
'default'}
+ {...register('description')}
+ onChange={(e, v) => {
+ setDescription(v);
+ register('description').onChange(e);
+ }}
+ />
+ {!!errors.description && <Text style={{ color: 'red',
fontStyle: 'italic'}}>{errors?.description?.message}</Text>}
</FormGroup>
<FormGroup label="Project ID" fieldId="projectId" isRequired>
- <TextInput className="text-field" type="text"
id="projectId" name="projectId"
+ <TextInput className="text-field" type="text"
id="projectId"
value={projectId}
onFocus={e => setProjectId(projectId === '' ?
CamelUi.nameFromTitle(name) : projectId)}
- onChange={(_, e) =>
setProjectId(CamelUi.nameFromTitle(e))}
- validated={isValidationError ? 'error' :
'default'}
+ validated={!!errors.projectId ? 'error' :
'default'}
+ {...register('projectId')}
+ onChange={(e, v) => {
+ setProjectId(CamelUi.nameFromTitle(v));
+ register('projectId').onChange(e);
+ }}
/>
- {isValidationError && <Text style={{ color: 'red',
fontStyle: 'italic'}}>Project ID must be unique</Text>}
- <FormHelperText>
- <HelperText>
- <HelperTextItem>Unique service
name</HelperTextItem>
- </HelperText>
- </FormHelperText>
+ {!!errors.projectId && <Text style={{ color: 'red',
fontStyle: 'italic'}}>{errors?.projectId?.message}</Text>}
</FormGroup>
+ <Grid>
+ {globalErrors &&
+ globalErrors.map((error) => (
+ <Alert title={error} key={error}
variant="danger"></Alert>
+ ))}
+ <Divider role="presentation" />
+ </Grid>
</Form>
</Modal>
)
diff --git a/karavan-web/karavan-app/src/main/webui/src/shared/error/Error.ts
b/karavan-web/karavan-app/src/main/webui/src/shared/error/Error.ts
new file mode 100644
index 00000000..b0e0dac3
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/shared/error/Error.ts
@@ -0,0 +1,11 @@
+export class Error {
+ field: string = '';
+ message: string = '';
+ code: string = '';
+
+ constructor(field: string, message: string, code: string) {
+ this.field = field;
+ this.message = message;
+ this.code = code;
+ }
+}
\ No newline at end of file
diff --git
a/karavan-web/karavan-app/src/main/webui/src/shared/error/ErrorResponse.ts
b/karavan-web/karavan-app/src/main/webui/src/shared/error/ErrorResponse.ts
new file mode 100644
index 00000000..70728e36
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/shared/error/ErrorResponse.ts
@@ -0,0 +1,15 @@
+import {Error} from "./Error";
+
+export class ErrorResponse {
+ status: number = 0;
+ error: string = '';
+ message: string = '';
+ errors: Array<Error> = [];
+
+ constructor(status: number, error: string, message: string, errors:
Array<Error>) {
+ this.status = status;
+ this.error = error;
+ this.message = message;
+ this.errors = errors;
+ }
+}
\ No newline at end of file
diff --git
a/karavan-web/karavan-app/src/main/webui/src/shared/error/ProjectExistsError.ts
b/karavan-web/karavan-app/src/main/webui/src/shared/error/ProjectExistsError.ts
deleted file mode 100644
index 6adee70b..00000000
---
a/karavan-web/karavan-app/src/main/webui/src/shared/error/ProjectExistsError.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export class ProjectExistsError extends Error {
- constructor(message: string) {
- super(message);
- this.name = 'ProjectExistsError';
- }
-}
\ No newline at end of file
diff --git
a/karavan-web/karavan-app/src/main/webui/src/shared/error/UseResponseErrorHandler.ts
b/karavan-web/karavan-app/src/main/webui/src/shared/error/UseResponseErrorHandler.ts
new file mode 100644
index 00000000..7e8c557a
--- /dev/null
+++
b/karavan-web/karavan-app/src/main/webui/src/shared/error/UseResponseErrorHandler.ts
@@ -0,0 +1,43 @@
+import {useState} from 'react';
+import {ErrorResponse} from "./ErrorResponse";
+import {Error} from "./Error";
+import {AxiosError} from "axios";
+
+export function useResponseErrorHandler(errorResponseToFormFields: Map<string,
string>, setError: any) {
+ const [globalErrors, setGlobalErrors] = useState<string[]>([]);
+
+ function registerResponseErrors(axiosError: AxiosError) {
+ const errorResponse: ErrorResponse = axiosError.response?.data as
ErrorResponse;
+ // Register field errors if field errors were returned
+ if (errorResponse.errors) {
+ // Reset global error
+ setGlobalErrors([]);
+
+ // Register all field errors
+ errorResponse.errors?.forEach((error: Error) => {
+ // If field name was found in mapping table, register it to
the form error
+ if (errorResponseToFormFields.get(error.field)) {
+ setError(errorResponseToFormFields.get(error.field), {
+ type: 'custom',
+ message: error.message
+ });
+ }
+ // If field name was not found in mapping table, register it
to the global error
+ else {
+ // Make copy as state shouldn't be mutated
+ setGlobalErrors(prevGlobalErrors => [...prevGlobalErrors,
error.message]);
+ }
+ });
+ }
+ // Register global error if no field errors were returned
+ else {
+ setGlobalErrors([errorResponse.message]);
+ }
+ }
+
+ function resetGlobalErrors() {
+ setGlobalErrors([]);
+ }
+
+ return [globalErrors, registerResponseErrors, resetGlobalErrors] as const;
+}