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
commit 40c786656c3217c789939f196ab09d0929f15f52 Author: Marat Gubaidullin <[email protected]> AuthorDate: Fri Apr 19 10:59:35 2024 -0400 Simplified Projet and File creation logic --- karavan-app/pom.xml | 4 - .../camel/karavan/api/ProjectFileResource.java | 17 +- .../apache/camel/karavan/api/ProjectResource.java | 18 +- .../karavan/{shared => model}/Configuration.java | 2 +- .../org/apache/camel/karavan/model/Project.java | 17 +- .../camel/karavan/service/ConfigService.java | 2 +- .../camel/karavan/service/ProjectService.java | 124 +++++----- .../org/apache/camel/karavan/shared/Property.java | 18 -- .../apache/camel/karavan/shared/error/Error.java | 78 ------- .../camel/karavan/shared/error/ErrorResponse.java | 59 ----- .../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 -- .../project/ProjectFileCreateValidator.java | 34 --- .../validation/project/ProjectModifyValidator.java | 40 ---- karavan-app/src/main/webui/package-lock.json | 58 ----- karavan-app/src/main/webui/package.json | 3 - karavan-app/src/main/webui/src/api/KaravanApi.tsx | 46 +++- .../src/main/webui/src/api/ProjectModels.ts | 22 +- .../src/main/webui/src/api/ProjectService.ts | 15 -- .../webui/src/designer/route/DslConnections.tsx | 2 +- .../src/main/webui/src/project/ProjectPanel.tsx | 4 +- .../src/main/webui/src/project/ProjectTitle.tsx | 6 +- .../main/webui/src/project/beans/BeanWizard.tsx | 18 +- .../webui/src/project/files/CreateFileModal.tsx | 258 +++++---------------- .../src/project/files/CreateIntegrationModal.tsx | 176 ++++++++++++++ .../src/main/webui/src/project/files/FilesTab.tsx | 10 +- .../webui/src/project/files/UploadFileModal.tsx | 6 +- .../main/webui/src/projects/CreateProjectModal.tsx | 186 +++++---------- .../main/webui/src/projects/DeleteProjectModal.tsx | 1 - .../main/webui/src/services/CreateServiceModal.tsx | 194 ---------------- .../main/webui/src/services/DeleteServiceModal.tsx | 70 ------ .../src/main/webui/src/services/ServicesPage.tsx | 15 +- karavan-app/src/main/webui/src/util/CodeUtils.ts | 29 ++- karavan-app/src/main/webui/src/util/StringUtils.ts | 10 + karavan-app/src/main/webui/src/util/form-util.css | 9 +- .../src/main/webui/src/util/useFormUtil.tsx | 27 ++- 39 files changed, 550 insertions(+), 1228 deletions(-) diff --git a/karavan-app/pom.xml b/karavan-app/pom.xml index 977e4430..bb52d843 100644 --- a/karavan-app/pom.xml +++ b/karavan-app/pom.xml @@ -128,10 +128,6 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-qute</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-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java index 366c9423..9de9c10b 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java @@ -19,11 +19,11 @@ package org.apache.camel.karavan.api; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.apache.camel.karavan.code.CodeService; import org.apache.camel.karavan.service.KaravanCacheService; import org.apache.camel.karavan.model.Project; import org.apache.camel.karavan.model.ProjectFile; -import org.apache.camel.karavan.validation.project.ProjectFileCreateValidator; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -41,9 +41,6 @@ public class ProjectFileResource { @Inject CodeService codeService; - @Inject - ProjectFileCreateValidator projectFileCreateValidator; - @GET @Produces(MediaType.APPLICATION_JSON) @Path("/{projectId}") @@ -65,11 +62,15 @@ public class ProjectFileResource { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - public ProjectFile create(ProjectFile file) throws Exception { + public Response create(ProjectFile file) throws Exception { file.setLastUpdate(Instant.now().toEpochMilli()); - projectFileCreateValidator.validate(file).failOnError(); - karavanCacheService.saveProjectFile(file); - return file; + boolean projectFileExists = karavanCacheService.getProjectFile(file.getProjectId(), file.getName()) != null; + if (projectFileExists) { + return Response.serverError().entity("File with given name already exists").build(); + } else { + karavanCacheService.saveProjectFile(file); + return Response.ok(file).build(); + } } @PUT diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java index 846a502b..856d4117 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java @@ -81,8 +81,12 @@ public class ProjectResource { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - public Project save(Project project) { - return projectService.save(project); + public Response save(Project project) { + try { + return Response.ok(projectService.save(project)).build(); + } catch (Exception e) { + return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build(); + } } @DELETE @@ -159,7 +163,7 @@ public class ProjectResource { camelStatus.setStatuses(stats); return camelStatus; }).toList(); - if (statuses != null && !statuses.isEmpty()) { + if (!statuses.isEmpty()) { return Response.ok(statuses).build(); } else { return Response.noContent().build(); @@ -170,7 +174,11 @@ public class ProjectResource { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Path("/copy/{sourceProject}") - public Project copy(@PathParam("sourceProject") String sourceProject, Project project) { - return projectService.copy(sourceProject, project); + public Response copy(@PathParam("sourceProject") String sourceProject, Project project) { + try { + return Response.ok(projectService.copy(sourceProject, project)).build(); + } catch (Exception e) { + return Response.serverError().entity(e.getMessage()).build(); + } } } \ No newline at end of file diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/shared/Configuration.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/Configuration.java similarity index 98% rename from karavan-app/src/main/java/org/apache/camel/karavan/shared/Configuration.java rename to karavan-app/src/main/java/org/apache/camel/karavan/model/Configuration.java index c95affec..301a5a19 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/Configuration.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/Configuration.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.karavan.shared; +package org.apache.camel.karavan.model; import java.util.List; diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java index d2d5e38c..8e5222de 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java @@ -17,8 +17,6 @@ package org.apache.camel.karavan.model; -import jakarta.validation.constraints.NotBlank; - import java.io.Serial; import java.io.Serializable; import java.time.Instant; @@ -39,11 +37,8 @@ public class Project implements Serializable { ephemeral, } - @NotBlank String projectId; - @NotBlank String name; - @NotBlank String description; String lastCommit; Long lastCommitTimestamp; @@ -127,4 +122,16 @@ public class Project implements Serializable { this.type = type; } + + @Override + public String toString() { + return "Project{" + + "projectId='" + projectId + '\'' + + ", name='" + name + '\'' + + ", description='" + description + '\'' + + ", lastCommit='" + lastCommit + '\'' + + ", lastCommitTimestamp=" + lastCommitTimestamp + + ", type=" + type + + '}'; + } } diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/ConfigService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/ConfigService.java index fca69c3b..7a200892 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/service/ConfigService.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/ConfigService.java @@ -19,7 +19,7 @@ package org.apache.camel.karavan.service; import io.quarkus.runtime.StartupEvent; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; -import org.apache.camel.karavan.shared.Configuration; +import org.apache.camel.karavan.model.Configuration; import org.eclipse.microprofile.config.inject.ConfigProperty; import java.nio.file.Files; diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java index bc966850..8686b506 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java @@ -29,8 +29,6 @@ import org.apache.camel.karavan.docker.DockerForKaravan; import org.apache.camel.karavan.kubernetes.KubernetesService; import org.apache.camel.karavan.model.*; import org.apache.camel.karavan.shared.Constants; -import org.apache.camel.karavan.shared.Property; -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; @@ -60,10 +58,6 @@ public class ProjectService implements HealthCheck { @ConfigProperty(name = "karavan.environment") String environment; - - @Inject - ProjectModifyValidator projectModifyValidator; - @Inject KaravanCacheService karavanCacheService; @@ -185,59 +179,66 @@ public class ProjectService implements HealthCheck { } } - public Project save(Project project) { - projectModifyValidator.validate(project).failOnError(); - - karavanCacheService.saveProject(project); + public Project save(Project project) throws Exception { + boolean projectIdExists = karavanCacheService.getProject(project.getProjectId()) != null; - ProjectFile appProp = codeService.getApplicationProperties(project); - karavanCacheService.saveProjectFile(appProp); - if (!ConfigService.inKubernetes()) { - ProjectFile projectCompose = codeService.createInitialProjectCompose(project); - karavanCacheService.saveProjectFile(projectCompose); - } else if (kubernetesService.isOpenshift()){ - ProjectFile projectCompose = codeService.createInitialDeployment(project); - karavanCacheService.saveProjectFile(projectCompose); + if (projectIdExists) { + throw new Exception("Project with id " + project.getProjectId() + " already exists"); + } else { + karavanCacheService.saveProject(project); + ProjectFile appProp = codeService.getApplicationProperties(project); + karavanCacheService.saveProjectFile(appProp); + if (!ConfigService.inKubernetes()) { + ProjectFile projectCompose = codeService.createInitialProjectCompose(project); + karavanCacheService.saveProjectFile(projectCompose); + } else if (kubernetesService.isOpenshift()) { + ProjectFile projectCompose = codeService.createInitialDeployment(project); + karavanCacheService.saveProjectFile(projectCompose); + } } - return project; } - public Project copy(String sourceProjectId, Project project) { - projectModifyValidator.validate(project).failOnError(); - - Project sourceProject = karavanCacheService.getProject(sourceProjectId); - - // Save project - karavanCacheService.saveProject(project); - - // Copy files from the source and make necessary modifications - Map<String, ProjectFile> filesMap = karavanCacheService.getProjectFilesMap(sourceProjectId).entrySet().stream() - .filter(e -> !Objects.equals(e.getValue().getName(), PROJECT_COMPOSE_FILENAME) && - !Objects.equals(e.getValue().getName(), PROJECT_DEPLOYMENT_JKUBE_FILENAME) - ) - .collect(Collectors.toMap( - e -> GroupedKey.create(project.getProjectId(), DEFAULT_ENVIRONMENT, e.getValue().getName()), - e -> { - ProjectFile file = e.getValue(); - file.setProjectId(project.getProjectId()); - if (Objects.equals(file.getName(), APPLICATION_PROPERTIES_FILENAME)) { - modifyPropertyFileOnProjectCopy(file, sourceProject, project); - } - return file; - }) - ); - karavanCacheService.saveProjectFiles(filesMap); - - if (!ConfigService.inKubernetes()) { - ProjectFile projectCompose = codeService.createInitialProjectCompose(project); - karavanCacheService.saveProjectFile(projectCompose); - } else if (kubernetesService.isOpenshift()) { - ProjectFile projectCompose = codeService.createInitialDeployment(project); - karavanCacheService.saveProjectFile(projectCompose); - } + public Project copy (String sourceProjectId, Project project) throws Exception { + boolean projectIdExists = karavanCacheService.getProject(project.getProjectId()) != null; - return project; + if (projectIdExists) { + throw new Exception("Project with id " + project.getProjectId() + " already exists"); + } else { + + Project sourceProject = karavanCacheService.getProject(sourceProjectId); + + // Save project + karavanCacheService.saveProject(project); + + // Copy files from the source and make necessary modifications + Map<String, ProjectFile> filesMap = karavanCacheService.getProjectFilesMap(sourceProjectId).entrySet().stream() + .filter(e -> !Objects.equals(e.getValue().getName(), PROJECT_COMPOSE_FILENAME) && + !Objects.equals(e.getValue().getName(), PROJECT_DEPLOYMENT_JKUBE_FILENAME) + ) + .collect(Collectors.toMap( + e -> GroupedKey.create(project.getProjectId(), DEFAULT_ENVIRONMENT, e.getValue().getName()), + e -> { + ProjectFile file = e.getValue(); + file.setProjectId(project.getProjectId()); + if (Objects.equals(file.getName(), APPLICATION_PROPERTIES_FILENAME)) { + modifyPropertyFileOnProjectCopy(file, sourceProject, project); + } + return file; + }) + ); + karavanCacheService.saveProjectFiles(filesMap); + + if (!ConfigService.inKubernetes()) { + ProjectFile projectCompose = codeService.createInitialProjectCompose(project); + karavanCacheService.saveProjectFile(projectCompose); + } else if (kubernetesService.isOpenshift()) { + ProjectFile projectCompose = codeService.createInitialDeployment(project); + karavanCacheService.saveProjectFile(projectCompose); + } + + return project; + } } private void modifyPropertyFileOnProjectCopy(ProjectFile propertyFile, Project sourceProject, Project project) { @@ -447,4 +448,21 @@ public class ProjectService implements HealthCheck { public DockerComposeService getProjectDockerComposeService(String projectId) { return codeService.getDockerComposeService(projectId); } + + public enum Property { + PROJECT_ID("camel.karavan.project-id=%s"), + PROJECT_NAME("camel.karavan.project-name=%s"), + PROJECT_DESCRIPTION("camel.karavan.project-description=%s"), + GAV("camel.jbang.gav=org.camel.karavan.demo:%s:1"); + + private final String keyValueFormatter; + + Property(String keyValueFormatter) { + this.keyValueFormatter = keyValueFormatter; + } + + public String getKeyValueFormatter() { + return keyValueFormatter; + } + } } diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/shared/Property.java b/karavan-app/src/main/java/org/apache/camel/karavan/shared/Property.java deleted file mode 100644 index 5958f1e9..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/Property.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.apache.camel.karavan.shared; - -public enum Property { - PROJECT_ID("camel.karavan.project-id=%s"), - PROJECT_NAME("camel.karavan.project-name=%s"), - PROJECT_DESCRIPTION("camel.karavan.project-description=%s"), - GAV("camel.jbang.gav=org.camel.karavan.demo:%s:1"); - - private final String keyValueFormatter; - - Property(String keyValueFormatter) { - this.keyValueFormatter = keyValueFormatter; - } - - public String getKeyValueFormatter() { - return keyValueFormatter; - } -} diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/Error.java b/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/Error.java deleted file mode 100644 index dee5372e..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/Error.java +++ /dev/null @@ -1,78 +0,0 @@ -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-app/src/main/java/org/apache/camel/karavan/shared/error/ErrorResponse.java b/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/ErrorResponse.java deleted file mode 100644 index 9749bdf8..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/error/ErrorResponse.java +++ /dev/null @@ -1,59 +0,0 @@ -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-app/src/main/java/org/apache/camel/karavan/shared/exception/ValidationException.java b/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ValidationException.java deleted file mode 100644 index c56fb075..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ValidationException.java +++ /dev/null @@ -1,40 +0,0 @@ -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-app/src/main/java/org/apache/camel/karavan/shared/validation/SimpleValidator.java b/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/SimpleValidator.java deleted file mode 100644 index b9d095cd..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/SimpleValidator.java +++ /dev/null @@ -1,33 +0,0 @@ -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-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationError.java b/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationError.java deleted file mode 100644 index 7c9e3b39..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationError.java +++ /dev/null @@ -1,59 +0,0 @@ -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-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationResult.java b/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationResult.java deleted file mode 100644 index e9563634..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/ValidationResult.java +++ /dev/null @@ -1,45 +0,0 @@ -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-app/src/main/java/org/apache/camel/karavan/shared/validation/Validator.java b/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/Validator.java deleted file mode 100644 index c48fbfc5..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/shared/validation/Validator.java +++ /dev/null @@ -1,23 +0,0 @@ -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-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectFileCreateValidator.java b/karavan-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectFileCreateValidator.java deleted file mode 100644 index 0cc12533..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectFileCreateValidator.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.apache.camel.karavan.validation.project; - -import java.util.List; - -import org.apache.camel.karavan.service.KaravanCacheService; -import org.apache.camel.karavan.model.ProjectFile; -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 ProjectFileCreateValidator extends Validator<ProjectFile> { - - private final SimpleValidator simpleValidator; - private final KaravanCacheService karavanCacheService; - - public ProjectFileCreateValidator(SimpleValidator simpleValidator, KaravanCacheService karavanCacheService) { - this.simpleValidator = simpleValidator; - this.karavanCacheService = karavanCacheService; - } - - @Override - protected void validationRules(ProjectFile value, List<ValidationError> errors) { - simpleValidator.validate(value, errors); - - boolean projectFileExists = karavanCacheService.getProjectFile(value.getProjectId(), value.getName()) != null; - - if (projectFileExists) { - errors.add(new ValidationError("name", "File with given name already exists")); - } - } -} diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectModifyValidator.java b/karavan-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectModifyValidator.java deleted file mode 100644 index 166f491e..00000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/validation/project/ProjectModifyValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.apache.camel.karavan.validation.project; - -import java.util.List; - -import org.apache.camel.karavan.service.KaravanCacheService; -import org.apache.camel.karavan.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 KaravanCacheService karavanCacheService; - - public ProjectModifyValidator(SimpleValidator simpleValidator, KaravanCacheService karavanCacheService) { - this.simpleValidator = simpleValidator; - this.karavanCacheService = karavanCacheService; - } - - - @Override - protected void validationRules(Project value, List<ValidationError> errors) { - simpleValidator.validate(value, errors); - - boolean projectIdExists = karavanCacheService.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-app/src/main/webui/package-lock.json b/karavan-app/src/main/webui/package-lock.json index 7b3cfe36..63d4a700 100644 --- a/karavan-app/src/main/webui/package-lock.json +++ b/karavan-app/src/main/webui/package-lock.json @@ -8,8 +8,6 @@ "name": "karavan", "version": "4.5.1", "dependencies": { - "@hookform/error-message": "^2.0.1", - "@hookform/resolvers": "^3.3.4", "@microsoft/fetch-event-source": "^2.0.1", "@monaco-editor/react": "4.6.0", "@patternfly/patternfly": "^5.2.1", @@ -34,7 +32,6 @@ "react-router-dom": "^6.22.3", "rxjs": "7.8.1", "uuid": "9.0.1", - "yup": "^1.4.0", "zustand": "^4.5.2" }, "devDependencies": { @@ -2557,24 +2554,6 @@ "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": "3.3.4", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", - "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==", - "peerDependencies": { - "react-hook-form": "^7.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -17685,11 +17664,6 @@ "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", @@ -20310,11 +20284,6 @@ "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", @@ -20351,11 +20320,6 @@ "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", @@ -22359,28 +22323,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yup": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", - "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", - "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.5.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", diff --git a/karavan-app/src/main/webui/package.json b/karavan-app/src/main/webui/package.json index 4f8d52d4..f8fca861 100644 --- a/karavan-app/src/main/webui/package.json +++ b/karavan-app/src/main/webui/package.json @@ -30,8 +30,6 @@ ] }, "dependencies": { - "@hookform/error-message": "^2.0.1", - "@hookform/resolvers": "^3.3.4", "@microsoft/fetch-event-source": "^2.0.1", "@monaco-editor/react": "4.6.0", "@patternfly/patternfly": "^5.2.1", @@ -56,7 +54,6 @@ "react-router-dom": "^6.22.3", "rxjs": "7.8.1", "uuid": "9.0.1", - "yup": "^1.4.0", "zustand": "^4.5.2" }, "devDependencies": { diff --git a/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-app/src/main/webui/src/api/KaravanApi.tsx index e8bc7f59..39f53f4a 100644 --- a/karavan-app/src/main/webui/src/api/KaravanApi.tsx +++ b/karavan-app/src/main/webui/src/api/KaravanApi.tsx @@ -277,12 +277,34 @@ export class KaravanApi { }); } - static async postProject(project: Project) { - return instance.post('/ui/project', project); + static async postProject(project: Project, after: (result: boolean, res: AxiosResponse<Project> | any) => void) { + try { + instance.post('/ui/project', project) + .then(res => { + if (res.status === 200) { + after(true, res); + } + }).catch(err => { + after(false, err); + }); + } catch (error: any) { + after(false, error); + } } - static async copyProject(sourceProject: string, project: Project) { - return instance.post('/ui/project/copy/' + sourceProject, project); + static copyProject(sourceProject: string, project: Project, after: (result: boolean, res: AxiosResponse<Project> | any) => void) { + try { + instance.post('/ui/project/copy/' + sourceProject, project) + .then(res => { + if (res.status === 200) { + after(true, res); + } + }).catch(err => { + after(false, err); + }); + } catch (error: any) { + after(false, error); + } } static async deleteProject(project: Project, deleteContainers: boolean, after: (res: AxiosResponse<any>) => void) { @@ -326,8 +348,20 @@ export class KaravanApi { }); } - static async saveProjectFile(file: ProjectFile) { - return instance.post('/ui/file', file); + static async saveProjectFile(file: ProjectFile, after: (result: boolean, file: ProjectFile | any) => void) { + console.log(file) + try { + instance.post('/ui/file', file) + .then(res => { + if (res.status === 200) { + after(true, res.data); + } + }).catch(err => { + after(false, err); + }); + } catch (error: any) { + after(false, error); + } } static async putProjectFile(file: ProjectFile, after: (res: AxiosResponse<any>) => void) { diff --git a/karavan-app/src/main/webui/src/api/ProjectModels.ts b/karavan-app/src/main/webui/src/api/ProjectModels.ts index 49beac67..6b701562 100644 --- a/karavan-app/src/main/webui/src/api/ProjectModels.ts +++ b/karavan-app/src/main/webui/src/api/ProjectModels.ts @@ -160,14 +160,22 @@ export const ProjectFileTypes: ProjectFileType[] = [ new ProjectFileType("OTHER", "Other", "*"), ]; - -export function getProjectFileType (file: ProjectFile) { - if (file.name.endsWith(".camel.yaml")) return ProjectFileTypes.filter(p => p.name === "INTEGRATION").map(p => p.title)[0]; - if (file.name.endsWith(".kamelet.yaml")) return ProjectFileTypes.filter(p => p.name === "KAMELET").map(p => p.title)[0]; - if (file.name.endsWith(".json")) return ProjectFileTypes.filter(p => p.name === "JSON").map(p => p.title)[0]; - if (file.name.endsWith(".yaml")) return ProjectFileTypes.filter(p => p.name === "YAML").map(p => p.title)[0]; +function getProjectFileType (file: ProjectFile) { + if (file.name.endsWith(".camel.yaml")) return ProjectFileTypes.filter(p => p.name === "INTEGRATION") + if (file.name.endsWith(".kamelet.yaml")) return ProjectFileTypes.filter(p => p.name === "KAMELET") + if (file.name.endsWith(".json")) return ProjectFileTypes.filter(p => p.name === "JSON") + if (file.name.endsWith(".yaml")) return ProjectFileTypes.filter(p => p.name === "YAML") const extension = file.name.substring(file.name.lastIndexOf('.') + 1); - const types = ProjectFileTypes.filter(p => p.extension === extension); + return ProjectFileTypes.filter(p => p.extension === extension); +} + +export function getProjectFileTypeName (file: ProjectFile) { + const types = getProjectFileType(file); + return types.length >0 ? types.map(p => p.name)[0] : "OTHER"; +} + +export function getProjectFileTypeTitle (file: ProjectFile) { + const types = getProjectFileType(file); return types.length >0 ? types.map(p => p.title)[0] : "Other"; } diff --git a/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-app/src/main/webui/src/api/ProjectService.ts index 2281075b..952b95fd 100644 --- a/karavan-app/src/main/webui/src/api/ProjectService.ts +++ b/karavan-app/src/main/webui/src/api/ProjectService.ts @@ -235,21 +235,6 @@ export class ProjectService { }); } - public static async createProject(project: Project) { - const result = await KaravanApi.postProject(project); - return result.data; - } - - public static async copyProject(sourceProject: string, project: Project) { - const result = await KaravanApi.copyProject(sourceProject, project); - return result.data; - } - - public static async createFile(file: ProjectFile) { - const result = await KaravanApi.saveProjectFile(file); - return result.data; - } - public static async createOpenApiFile(file: ProjectFile, generateRest: boolean, generateRoutes: boolean, integrationName: string) { const result = await KaravanApi.postOpenApi(file, generateRest, generateRoutes, integrationName); return result.data; diff --git a/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx b/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx index 1c0b284a..a711780b 100644 --- a/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx +++ b/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx @@ -47,7 +47,7 @@ export function DslConnections() { setTons(prevState => { const data = new Map<string, string[]>(); TopologyUtils.findTopologyOutgoingNodes(integrations).forEach(t => { - const key = (t.step as any)?.uri + ':' + (t.step as any)?.parameters?.name; + const key = (t.step as any)?.uri + ':' + (t.step as any)?.parameters.name; if (data.has(key)) { const list = data.get(key) || []; list.push(t.routeId); diff --git a/karavan-app/src/main/webui/src/project/ProjectPanel.tsx b/karavan-app/src/main/webui/src/project/ProjectPanel.tsx index c98e9d02..b19297f6 100644 --- a/karavan-app/src/main/webui/src/project/ProjectPanel.tsx +++ b/karavan-app/src/main/webui/src/project/ProjectPanel.tsx @@ -33,10 +33,10 @@ import {ProjectContainerTab} from "./container/ProjectContainerTab"; import {IntegrationFile} from "karavan-core/lib/model/IntegrationDefinition"; import {TopologyTab} from "../topology/TopologyTab"; import {Buffer} from "buffer"; -import {CreateFileModal} from "./files/CreateFileModal"; import {ProjectType} from "../api/ProjectModels"; import {ReadmeTab} from "./readme/ReadmeTab"; import {BeanWizard} from "./beans/BeanWizard"; +import {CreateIntegrationModal} from "./files/CreateIntegrationModal"; export function ProjectPanel() { @@ -89,7 +89,7 @@ export function ProjectPanel() { }} onSetFile={(fileName) => selectFile(fileName)} /> - <CreateFileModal types={['INTEGRATION']} isKameletsProject={false}/> + <CreateIntegrationModal type={'INTEGRATION'} isKameletsProject={false}/> <BeanWizard/> </> ) diff --git a/karavan-app/src/main/webui/src/project/ProjectTitle.tsx b/karavan-app/src/main/webui/src/project/ProjectTitle.tsx index 1c43c281..180fa59a 100644 --- a/karavan-app/src/main/webui/src/project/ProjectTitle.tsx +++ b/karavan-app/src/main/webui/src/project/ProjectTitle.tsx @@ -23,10 +23,10 @@ import { Text, TextContent, Flex, - FlexItem, Button + FlexItem, } from '@patternfly/react-core'; import '../designer/karavan.css'; -import {getProjectFileType} from "../api/ProjectModels"; +import {getProjectFileTypeTitle} from "../api/ProjectModels"; import {useFileStore, useProjectStore} from "../api/ProjectStore"; import TopologyIcon from "@patternfly/react-icons/dist/js/icons/topology-icon"; import FilesIcon from "@patternfly/react-icons/dist/js/icons/folder-open-icon"; @@ -87,7 +87,7 @@ export function ProjectTitle() { <FlexItem> <Flex direction={{default: "row"}}> <FlexItem> - <Badge>{getProjectFileType(file)}</Badge> + <Badge>{getProjectFileTypeTitle(file)}</Badge> </FlexItem> <FlexItem> <TextContent className="description"> diff --git a/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx b/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx index 896dbe12..9c599762 100644 --- a/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx +++ b/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx @@ -33,8 +33,6 @@ import {useFilesStore, useFileStore, useProjectStore, useWizardStore} from "../. import {shallow} from "zustand/shallow"; import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; import {useForm} from "react-hook-form"; -import {yupResolver} from "@hookform/resolvers/yup"; -import * as yup from "yup"; import {ProjectService} from "../../api/ProjectService"; import {EventBus} from "../../designer/utils/EventBus"; import {useResponseErrorHandler} from "../../shared/error/UseResponseErrorHandler"; @@ -49,13 +47,6 @@ const BEAN_TEMPLATE_SUFFIX_FILENAME = "-bean-template.camel.yaml"; export function BeanWizard() { - const formValidationSchema = yup.object().shape({ - filename: yup - .string() - .matches(/^[a-zA-Z0-9_\-.]+$/, 'Incorrect filename') - .required("File name is required"), - }); - const { register, setError, @@ -64,7 +55,6 @@ export function BeanWizard() { reset, setValue } = useForm({ - resolver: yupResolver(formValidationSchema), mode: "onChange", defaultValues: {filename: ''} }); @@ -74,7 +64,7 @@ export function BeanWizard() { ]); const [project] = useProjectStore((s) => [s.project], shallow); - const [operation, setFile, designerTab] = useFileStore((s) => [s.operation, s.setFile, s.designerTab], shallow); + const [setFile, designerTab] = useFileStore((s) => [s.setFile, s.designerTab], shallow); const [files] = useFilesStore((s) => [s.files], shallow); const [showWizard, setShowWizard] = useWizardStore((s) => [s.showWizard, s.setShowWizard], shallow) const [templateFiles, setTemplateFiles] = useState<ProjectFile[]>([]); @@ -116,9 +106,9 @@ export function BeanWizard() { } const fullFileName = filename + CAMEL_YAML_EXT; const file = new ProjectFile(fullFileName, project.projectId, code, Date.now()); - return ProjectService.createFile(file) - .then(() => handleOnFormSubmitSuccess(file)) - .catch((error) => registerResponseErrors(error)); + // return ProjectService.createFile(file) + // .then(() => handleOnFormSubmitSuccess(file)) + // .catch((error) => registerResponseErrors(error)); } } diff --git a/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx b/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx index 61af5453..20d76296 100644 --- a/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx +++ b/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx @@ -15,250 +15,106 @@ * limitations under the License. */ -import React, {useEffect, useState} from 'react'; +import React, {useEffect} from 'react'; import { Button, Modal, - FormGroup, ModalVariant, Form, - ToggleGroupItem, ToggleGroup, TextInput, Alert, Divider, Grid, Text, + Alert, FormAlert, } from '@patternfly/react-core'; import '../../designer/karavan.css'; -import {Integration, KameletTypes, MetadataLabels} from "karavan-core/lib/model/IntegrationDefinition"; -import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml"; import {useFileStore, useProjectStore} from "../../api/ProjectStore"; -import {ProjectFile, ProjectFileTypes} from "../../api/ProjectModels"; -import {CamelUi} from "../../designer/utils/CamelUi"; +import {getProjectFileTypeName, ProjectFile} from "../../api/ProjectModels"; import {ProjectService} from "../../api/ProjectService"; import {shallow} from "zustand/shallow"; -import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; -import {KameletApi} from "karavan-core/lib/api/KameletApi"; -import {TypeaheadSelect, Value} from "../../designer/ui/TypeaheadSelect"; -import * as yup from "yup"; -import {useForm} from "react-hook-form"; -import {yupResolver} from "@hookform/resolvers/yup"; -import {useResponseErrorHandler} from "../../shared/error/UseResponseErrorHandler"; +import {SubmitHandler, useForm} from "react-hook-form"; import {EventBus} from "../../designer/utils/EventBus"; -import {AxiosError} from "axios"; -import {isEmpty} from "../../util/StringUtils"; +import {isValidFileName} from "../../util/StringUtils"; +import {useFormUtil} from "../../util/useFormUtil"; +import {KaravanApi} from "../../api/KaravanApi"; +import {CodeUtils} from "../../util/CodeUtils"; -interface Props { - types: string[], - isKameletsProject: boolean -} - -export function CreateFileModal(props: Props) { - - const formValidationSchema = yup.object().shape({ - name: yup - .string() - .required("File name is required"), - }); - - const defaultFormValues = { - name: "" - }; - - const responseToFormErrorFields = new Map<string, string>([ - ["name", "name"] - ]); +export function CreateFileModal() { + const [project] = useProjectStore((s) => [s.project], shallow); + const [operation, setFile] = useFileStore((s) => [s.operation, s.setFile], shallow); + const [isReset, setReset] = React.useState(false); + const [backendError, setBackendError] = React.useState<string>(); + const formContext = useForm<ProjectFile>({mode: "all"}); + const {getTextField} = useFormUtil(formContext); const { - register, - setError, + formState: {errors}, handleSubmit, - formState: { errors }, reset, - clearErrors - } = useForm({ - resolver: yupResolver(formValidationSchema), - mode: "onChange", - defaultValues: defaultFormValues - }); - - - const [project] = useProjectStore((s) => [s.project], shallow); - const [operation, setFile, designerTab] = useFileStore((s) => [s.operation, s.setFile, s.designerTab], shallow); - const [name, setName] = useState<string>(''); - const [fileType, setFileType] = useState<string>(); - const [kameletType, setKameletType] = useState<KameletTypes>('source'); - const [selectedKamelet, setSelectedKamelet] = useState<string>(); - const [globalErrors, registerResponseErrors, resetGlobalErrors] = useResponseErrorHandler( - responseToFormErrorFields, - setError - ); + trigger + } = formContext; useEffect(() => { - if (props.types.length > 0) { - setFileType(props.types[0]); - } - }, [props]); + reset(new ProjectFile('', project.projectId, '', 0)); + setBackendError(undefined); + setReset(true); + }, [reset, operation]); - function resetForm() { - resetGlobalErrors(); - reset(defaultFormValues); - setName("") - setFileType(props.types.at(0) || 'INTEGRATION'); - } + React.useEffect(() => { + isReset && trigger(); + }, [trigger, isReset]); function closeModal() { setFile("none"); - resetForm(); - } - - function handleFormSubmit() { - const code = getCode(); - const fullFileName = getFullFileName(name, fileType); - const file = new ProjectFile(fullFileName, project.projectId, code, Date.now()); - - return ProjectService.createFile(file) - .then(() => handleOnFormSubmitSuccess(code, file)) - .catch((error) => handleOnFormSubmitFailure(error)); } - function handleOnFormSubmitSuccess (code: string, file: ProjectFile) { - const message = "File successfully created."; - EventBus.sendAlert( "Success", message, "success"); - - ProjectService.refreshProjectData(file.projectId); - - resetForm(); - if (code) { - setFile('select', file, designerTab); - } else { - setFile("none"); - } + const onSubmit: SubmitHandler<ProjectFile> = (data) => { + data.projectId = project.projectId; + data.code = CodeUtils.getCodeForNewFile(data.name, getProjectFileTypeName(data)); + KaravanApi.saveProjectFile(data, (result, file) => { + if (result) { + onSuccess(file); + } else { + setBackendError(file?.response?.data); + } + }) } - function handleOnFormSubmitFailure(error: AxiosError) { - registerResponseErrors(error); + function onSuccess (file: ProjectFile) { + EventBus.sendAlert( "Success", "File successfully created", "success"); + ProjectService.refreshProjectData(project.projectId); + setFile('select', file); } function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void { - if (event.key === 'Enter' && !isEmpty(name)) { - handleFormSubmit(); - event.preventDefault(); - } - } - - function getCode(): string { - if (fileType === 'INTEGRATION') { - return CamelDefinitionYaml.integrationToYaml(Integration.createNew(name, 'plain')); - } else if (fileType === 'KAMELET') { - const kameletName = name + (isKamelet ? '-' + kameletType : ''); - const integration = Integration.createNew(kameletName, 'kamelet'); - const meta: MetadataLabels = new MetadataLabels({"camel.apache.org/kamelet.type": kameletType}); - integration.metadata.labels = meta; - if (selectedKamelet !== undefined && selectedKamelet !== '') { - const kamelet= KameletApi.getKamelets().filter(k => k.metadata.name === selectedKamelet).at(0); - if (kamelet) { - (integration as any).spec = kamelet.spec; - (integration as any).metadata.labels = kamelet.metadata.labels; - (integration as any).metadata.annotations = kamelet.metadata.annotations; - const i = CamelUtil.cloneIntegration(integration); - return CamelDefinitionYaml.integrationToYaml(i); - } - } - return CamelDefinitionYaml.integrationToYaml(integration); - } else { - return ''; + if (event.key === 'Enter') { + handleSubmit(onSubmit)() } } - function fileNameCheck(title: string) { - return title.replace(/[^0-9a-zA-Z.]+/gi, "-").toLowerCase(); - } - - const isKamelet = props.isKameletsProject; - - const listOfValues: Value[] = KameletApi.getKamelets() - .filter(k => k.metadata.labels["camel.apache.org/kamelet.type"] === kameletType) - .map(k => { - const v: Value = {value: k.metadata.name, children: k.spec.definition.title} - return v; - }) - - function getFullFileName(name: string, type?: string) { - let extension = ProjectFileTypes.filter(value => value.name === type)[0]?.extension; - extension = extension === '*' ? '' : '.' + extension; - const filename = (extension !== '.java') - ? fileNameCheck(name) - : CamelUi.javaNameFromTitle(name); - return filename + (isKamelet ? '-' + kameletType : '') + extension; - } - - function update(value: string, type?: string) { - setName(value); - setFileType(type); - } - return ( <Modal - title={"Create " + (isKamelet ? "Kamelet" : "")} + title="Create file" variant={ModalVariant.small} isOpen={["create", "copy"].includes(operation)} onClose={closeModal} onKeyDown={onKeyDown} actions={[ - <Button key="confirm" variant="primary" onClick={handleSubmit(handleFormSubmit)}>Save</Button>, + <Button key="confirm" variant="primary" onClick={handleSubmit(onSubmit)} + isDisabled={Object.getOwnPropertyNames(errors).length > 0} + > + Save + </Button>, <Button key="cancel" variant="secondary" onClick={closeModal}>Cancel</Button> ]} > <Form autoComplete="off" isHorizontal className="create-file-form"> - {!isKamelet && <FormGroup label="Type" fieldId="type" isRequired> - <ToggleGroup aria-label="Type" isCompact> - {ProjectFileTypes.filter(p => props.types.includes(p.name)) - .map(p => { - const title = p.title + ' (' + p.extension + ')'; - return <ToggleGroupItem key={title} text={title} buttonId={p.name} - isSelected={fileType === p.name} - onChange={(_, selected) => { - resetGlobalErrors(); - clearErrors('name'); - update(name, p.name); - }}/> - })} - </ToggleGroup> - </FormGroup>} - {isKamelet && <FormGroup label="Type" fieldId="kameletType" isRequired> - <ToggleGroup aria-label="Kamelet Type"> - {['source', 'action', 'sink'].map((type) => { - const title = CamelUtil.capitalizeName(type); - return <ToggleGroupItem key={type} text={title} buttonId={type} - isSelected={kameletType === type} - onChange={(_, selected) => { - setKameletType(type as KameletTypes); - setSelectedKamelet(undefined) - }}/> - })} - </ToggleGroup> - </FormGroup>} - <FormGroup label="Name" fieldId="name" isRequired> - <TextInput className="text-field" type="text" id="name" - aria-label="name" - value={name} - validated={!!errors.name ? 'error' : 'default'} - {...register('name')} - onChange={(e, value) => { - update(value, fileType); - register('name').onChange(e); - }} - /> - {!!errors.name && <Text style={{ color: 'red', fontStyle: 'italic'}}>{errors?.name?.message}</Text>} - </FormGroup> - {isKamelet && <FormGroup label="Copy from" fieldId="kamelet"> - <TypeaheadSelect listOfValues={listOfValues} onSelect={value => { - setSelectedKamelet(value) - }}/> - </FormGroup>} - <Grid> - {globalErrors && - globalErrors.map((error) => ( - <Alert title={error} key={error} variant="danger"></Alert> - ))} - <Divider role="presentation" /> - </Grid> + {getTextField('name', 'Name', { + regex: v => isValidFileName(v) || 'Not a valid filename', + length: v => v.length > 5 || 'File name should be longer that 5 characters', + name: v => !['templates', 'kamelets', 'karavan'].includes(v) || "'templates', 'kamelets', 'karavan' can't be used as filename", + })} + {backendError && + <FormAlert> + <Alert variant="danger" title={backendError} aria-live="polite" isInline /> + </FormAlert> + } </Form> </Modal> ) diff --git a/karavan-app/src/main/webui/src/project/files/CreateIntegrationModal.tsx b/karavan-app/src/main/webui/src/project/files/CreateIntegrationModal.tsx new file mode 100644 index 00000000..109b81c4 --- /dev/null +++ b/karavan-app/src/main/webui/src/project/files/CreateIntegrationModal.tsx @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, {useEffect, useState} from 'react'; +import { + Button, + Modal, + FormGroup, + ModalVariant, + Form, + ToggleGroupItem, ToggleGroup, Alert, FormAlert, capitalize, +} from '@patternfly/react-core'; +import '../../designer/karavan.css'; +import {KameletTypes} from "karavan-core/lib/model/IntegrationDefinition"; +import {useFileStore, useProjectStore} from "../../api/ProjectStore"; +import {ProjectFile, ProjectFileTypes} from "../../api/ProjectModels"; +import {ProjectService} from "../../api/ProjectService"; +import {shallow} from "zustand/shallow"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; +import {KameletApi} from "karavan-core/lib/api/KameletApi"; +import {TypeaheadSelect, Value} from "../../designer/ui/TypeaheadSelect"; +import {SubmitHandler, useForm} from "react-hook-form"; +import {EventBus} from "../../designer/utils/EventBus"; +import {isValidFileName} from "../../util/StringUtils"; +import {useFormUtil} from "../../util/useFormUtil"; +import {KaravanApi} from "../../api/KaravanApi"; +import {CodeUtils} from "../../util/CodeUtils"; + +interface Props { + type: string, + isKameletsProject: boolean +} + +export function CreateIntegrationModal(props: Props) { + + const [project] = useProjectStore((s) => [s.project], shallow); + const [operation, setFile, designerTab] = useFileStore((s) => [s.operation, s.setFile, s.designerTab], shallow); + const [fileType, setFileType] = useState<string>('INTEGRATION'); + const [kameletType, setKameletType] = useState<KameletTypes>('source'); + const [selectedKamelet, setSelectedKamelet] = useState<string>(); + const [isReset, setReset] = React.useState(false); + const [backendError, setBackendError] = React.useState<string>(); + const formContext = useForm<ProjectFile>({mode: "all"}); + const {getTextFieldSuffix} = useFormUtil(formContext); + const { + formState: {errors}, + handleSubmit, + reset, + trigger + } = formContext; + + useEffect(() => { + reset(new ProjectFile('', project.projectId, '', 0)); + setBackendError(undefined); + setReset(true); + }, [reset, operation]); + + React.useEffect(() => { + isReset && trigger(); + }, [trigger, isReset]); + + function closeModal() { + setFile("none"); + } + + const onSubmit: SubmitHandler<ProjectFile> = (data) => { + data.projectId = project.projectId; + data.name = getFullFileName(data.name, props.type); + data.code = CodeUtils.getCodeForNewFile(data.name, fileType, selectedKamelet); + KaravanApi.saveProjectFile(data, (result, file) => { + if (result) { + onSuccess(file); + } else { + setBackendError(file?.response?.data); + } + }) + } + + function onSuccess (file: ProjectFile) { + EventBus.sendAlert( "Success", "File successfully created", "success"); + ProjectService.refreshProjectData(project.projectId); + if (file.code) { + setFile('select', file, designerTab); + } else { + setFile("none"); + } + } + + function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void { + if (event.key === 'Enter') { + handleSubmit(onSubmit)() + } + } + + const isKamelet = props.isKameletsProject; + + const listOfValues: Value[] = KameletApi.getKamelets() + .filter(k => k.metadata.labels["camel.apache.org/kamelet.type"] === kameletType) + .map(k => { + const v: Value = {value: k.metadata.name, children: k.spec.definition.title} + return v; + }) + + function getFileExtension(type?: string) { + let extension = ProjectFileTypes.filter(value => value.name === type)[0]?.extension; + extension = extension === '*' ? '' : '.' + extension; + return extension; + } + + function getFullFileName(name: string, type?: string) { + return name + (isKamelet ? '-' + kameletType : '') + getFileExtension(type); + } + + return ( + <Modal + title={"Create " + (isKamelet ? "Kamelet" : capitalize(designerTab || ' '))} + variant={ModalVariant.small} + isOpen={["create", "copy"].includes(operation)} + onClose={closeModal} + onKeyDown={onKeyDown} + actions={[ + <Button key="confirm" variant="primary" onClick={handleSubmit(onSubmit)} + isDisabled={Object.getOwnPropertyNames(errors).length > 0} + > + Save + </Button>, + <Button key="cancel" variant="secondary" onClick={closeModal}>Cancel</Button> + ]} + > + <Form autoComplete="off" isHorizontal className="create-file-form"> + {isKamelet && <FormGroup label="Type" fieldId="kameletType" isRequired> + <ToggleGroup aria-label="Kamelet Type"> + {['source', 'action', 'sink'].map((type) => { + const title = CamelUtil.capitalizeName(type); + return <ToggleGroupItem key={type} text={title} buttonId={type} + isSelected={kameletType === type} + onChange={(_, selected) => { + setKameletType(type as KameletTypes); + setSelectedKamelet(undefined) + }}/> + })} + </ToggleGroup> + </FormGroup>} + {getTextFieldSuffix('name', 'Name', getFileExtension(props.type), true, { + regex: v => isValidFileName(v) || 'Only characters, numbers and dashes allowed', + length: v => v.length > 3 || 'File name should be longer that 3 characters', + name: v => !['templates', 'kamelets', 'karavan'].includes(v) || "'templates', 'kamelets', 'karavan' can't be used as project", + })} + {isKamelet && <FormGroup label="Copy from" fieldId="kamelet"> + <TypeaheadSelect listOfValues={listOfValues} onSelect={value => { + setSelectedKamelet(value) + }}/> + </FormGroup>} + {backendError && + <FormAlert> + <Alert variant="danger" title={backendError} aria-live="polite" isInline /> + </FormAlert> + } + </Form> + </Modal> + ) +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/project/files/FilesTab.tsx b/karavan-app/src/main/webui/src/project/files/FilesTab.tsx index b9c181ee..a686bab0 100644 --- a/karavan-app/src/main/webui/src/project/files/FilesTab.tsx +++ b/karavan-app/src/main/webui/src/project/files/FilesTab.tsx @@ -36,7 +36,11 @@ import {Table} from '@patternfly/react-table/deprecated'; import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon"; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import {useFilesStore, useFileStore, useProjectStore} from "../../api/ProjectStore"; -import {getProjectFileType, Project, ProjectFile, ProjectFileTypes} from "../../api/ProjectModels"; +import { + getProjectFileTypeTitle, + ProjectFile, + ProjectFileTypes +} from "../../api/ProjectModels"; import {FileToolbar} from "./FilesToolbar"; import DownloadIcon from "@patternfly/react-icons/dist/esm/icons/download-icon"; import FileSaver from "file-saver"; @@ -107,7 +111,7 @@ export function FilesTab () { </Thead> <Tbody> {files.map(file => { - const type = getProjectFileType(file) + const type = getProjectFileTypeTitle(file) return <Tr key={file.name}> <Td> <Badge>{type}</Badge> @@ -159,7 +163,7 @@ export function FilesTab () { </Table> </div> <UploadFileModal projectId={project.projectId}/> - <CreateFileModal types={types} isKameletsProject={isKameletsProject()}/> + <CreateFileModal/> <DeleteFileModal /> </PageSection> ) diff --git a/karavan-app/src/main/webui/src/project/files/UploadFileModal.tsx b/karavan-app/src/main/webui/src/project/files/UploadFileModal.tsx index 2a69e161..e3c11a5c 100644 --- a/karavan-app/src/main/webui/src/project/files/UploadFileModal.tsx +++ b/karavan-app/src/main/webui/src/project/files/UploadFileModal.tsx @@ -95,9 +95,9 @@ export function UploadFileModal(props: Props) { .then(() => handleOnFormSubmitSuccess()) .catch((error) => handleOnFormSubmitFailure(error)); } else { - return ProjectService.createFile(file) - .then(() => handleOnFormSubmitSuccess()) - .catch((error) => handleOnFormSubmitFailure(error)); + // return ProjectService.createFile(file) + // .then(() => handleOnFormSubmitSuccess()) + // .catch((error) => handleOnFormSubmitFailure(error)); } } diff --git a/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx b/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx index 2e1d670d..be05a643 100644 --- a/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx +++ b/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx @@ -15,117 +15,80 @@ * limitations under the License. */ -import React, {useState} from 'react'; +import React, {useEffect} from 'react'; import { Alert, Button, - Divider, - Form, - FormGroup, - Grid, + Form, FormAlert, Modal, 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 {isEmpty} from "../util/StringUtils"; +import {isValidProjectId} from "../util/StringUtils"; import {EventBus} from "../designer/utils/EventBus"; -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"; +import {SubmitHandler, useForm} from "react-hook-form"; +import {useFormUtil} from "../util/useFormUtil"; +import {KaravanApi} from "../api/KaravanApi"; +import {AxiosResponse} from "axios"; -export function CreateProjectModal () { - - 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"] - ]); +export function CreateProjectModal() { + const {project, operation, setOperation} = useProjectStore(); + const [isReset, setReset] = React.useState(false); + const [backendError, setBackendError] = React.useState<string>(); + const formContext = useForm<Project>({mode: "all"}); + const {getTextField} = useFormUtil(formContext); const { - register, - setError, + formState: {errors}, handleSubmit, - formState: { errors }, - reset - } = useForm({ - resolver: yupResolver(formValidationSchema), - mode: "onChange", - defaultValues: defaultFormValues - }); + reset, + trigger + } = formContext; - const {project, operation, setOperation} = useProjectStore(); - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); - const [projectId, setProjectId] = useState(''); - const [globalErrors, registerResponseErrors, resetGlobalErrors] = useResponseErrorHandler( - responseToFormErrorFields, - setError - ); + useEffect(() => { + reset(new Project()); + setBackendError(undefined); + setReset(true); + }, [reset]); - function resetForm() { - resetGlobalErrors(); - reset(defaultFormValues); - } + React.useEffect(() => { + isReset && trigger(); + }, [trigger, isReset]); function closeModal() { setOperation("none"); - resetForm(); } - 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})) + const onSubmit: SubmitHandler<Project> = (data) => { + if (operation === 'copy') { + KaravanApi.copyProject(data.projectId, project, after) + } else { + KaravanApi.postProject(data, after) + } + } - return action - .then(() => handleOnFormSubmitSuccess()) - .catch((error) => handleOnFormSubmitFailure(error)); + function after (result: boolean, res: AxiosResponse<Project> | any) { + if (result) { + onSuccess(res.projectId); + } else { + setBackendError(res?.response?.data); + } } - function handleOnFormSubmitSuccess () { + function onSuccess (projectId: string) { 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 { - if (event.key === 'Enter' && !isEmpty(name) && !isEmpty(description) && !isEmpty(projectId)) { - handleFormSubmit(); + if (event.key === 'Enter') { + handleSubmit(onSubmit)() } } @@ -137,56 +100,33 @@ export function CreateProjectModal () { onClose={closeModal} onKeyDown={onKeyDown} actions={[ - <Button key="confirm" variant="primary" onClick={handleSubmit(handleFormSubmit)}>Save</Button>, + <Button key="confirm" variant="primary" + onClick={handleSubmit(onSubmit)} + isDisabled={Object.getOwnPropertyNames(errors).length > 0} + > + 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" - value={name} - 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" - value={description} - 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" - value={projectId} - onFocus={e => setProjectId(projectId === '' ? CamelUi.nameFromTitle(name) : projectId)} - validated={!!errors.projectId ? 'error' : 'default'} - {...register('projectId')} - onChange={(e, v) => { - setProjectId(CamelUi.nameFromTitle(v)); - register('projectId').onChange(e); - }} - /> - {!!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> + {getTextField('name', 'Name', { + length: v => v.length > 5 || 'Project name should be longer that 5 characters', + })} + {getTextField('description', 'Description', { + length: v => v.length > 5 || 'Description name should be longer that 5 characters', + })} + {getTextField('projectId', 'ProjectID', { + regex: v => isValidProjectId(v) || 'Only lowercase characters, numbers and dashes allowed', + length: v => v.length > 5 || 'Project ID should be longer that 5 characters', + name: v => !['templates', 'kamelets', 'karavan'].includes(v) || "'templates', 'kamelets', 'karavan' can't be used as project", + })} + {backendError && + <FormAlert> + <Alert variant="danger" title={backendError} aria-live="polite" isInline /> + </FormAlert> + } </Form> </Modal> ) diff --git a/karavan-app/src/main/webui/src/projects/DeleteProjectModal.tsx b/karavan-app/src/main/webui/src/projects/DeleteProjectModal.tsx index ce80c9f2..3e10d7d0 100644 --- a/karavan-app/src/main/webui/src/projects/DeleteProjectModal.tsx +++ b/karavan-app/src/main/webui/src/projects/DeleteProjectModal.tsx @@ -24,7 +24,6 @@ import { import '../designer/karavan.css'; import {useProjectStore} from "../api/ProjectStore"; import {ProjectService} from "../api/ProjectService"; -import ExclamationIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-icon'; export function DeleteProjectModal () { diff --git a/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx b/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx deleted file mode 100644 index 22f7b67a..00000000 --- a/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, {useState} from 'react'; -import { - Alert, - Button, - Divider, - Form, - FormGroup, - Grid, - Modal, - 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 {EventBus} from "../designer/utils/EventBus"; -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 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 [globalErrors, registerResponseErrors, resetGlobalErrors] = useResponseErrorHandler( - responseToFormErrorFields, - setError - ); - - function resetForm() { - resetGlobalErrors(); - reset(defaultFormValues); - } - - function closeModal() { - setOperation("none"); - resetForm(); - } - - 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 { - if (event.key === 'Enter' && name !== undefined && description !== undefined && projectId !== undefined) { - handleFormSubmit(); - } - } - - return ( - <Modal - title={operation !== 'copy' ? "Create new project" : "Copy project from " + project?.projectId} - variant={ModalVariant.small} - isOpen={["create", "copy"].includes(operation)} - onClose={closeModal} - onKeyDown={onKeyDown} - actions={[ - <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" - value={name} - 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" - value={description} - 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" - value={projectId} - onFocus={e => setProjectId(projectId === '' ? CamelUi.nameFromTitle(name) : projectId)} - validated={!!errors.projectId ? 'error' : 'default'} - {...register('projectId')} - onChange={(e, v) => { - setProjectId(CamelUi.nameFromTitle(v)); - register('projectId').onChange(e); - }} - /> - {!!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> - ) -} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/services/DeleteServiceModal.tsx b/karavan-app/src/main/webui/src/services/DeleteServiceModal.tsx deleted file mode 100644 index 458fabfe..00000000 --- a/karavan-app/src/main/webui/src/services/DeleteServiceModal.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { - Button, - Modal, - ModalVariant, -} from '@patternfly/react-core'; -import '../designer/karavan.css'; -import {useProjectStore} from "../api/ProjectStore"; -import {ProjectService} from "../api/ProjectService"; - -export function DeleteServiceModal () { - - const {project, operation} = useProjectStore(); - - function closeModal () { - useProjectStore.setState({operation: "none"}) - } - - function confirmAndCloseModal () { - ProjectService.deleteProject(project); - useProjectStore.setState({operation: "none"}); - } - - const isOpen= operation === "delete"; - return ( - <Modal - title="Confirmation" - variant={ModalVariant.small} - isOpen={isOpen} - onClose={() => closeModal()} - actions={[ - <Button key="confirm" variant="primary" onClick={e => confirmAndCloseModal()}>Delete</Button>, - <Button key="cancel" variant="link" - onClick={e => closeModal()}>Cancel</Button> - ]} - onEscapePress={e => closeModal()}> - <div>{"Are you sure you want to delete the project " + project?.projectId + "?"}</div> - </Modal> - // } - // {(this.state.isProjectDeploymentModalOpen === true) && <Modal - // variant={ModalVariant.small} - // isOpen={this.state.isProjectDeploymentModalOpen} - // onClose={() => this.setState({ isProjectDeploymentModalOpen: false })} - // onEscapePress={e => this.setState({ isProjectDeploymentModalOpen: false })}> - // <div> - // <Alert key={this.state.projectToDelete?.projectId} className="main-alert" variant="warning" - // title={"Deployment is Running!!"} isInline={true} isPlain={true}> - // {"Delete the deployment (" + this.state.projectToDelete?.projectId + ")" + " first."} - // </Alert> - // </div> - // </Modal> - ) -} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/services/ServicesPage.tsx b/karavan-app/src/main/webui/src/services/ServicesPage.tsx index 28f09a92..48a082a5 100644 --- a/karavan-app/src/main/webui/src/services/ServicesPage.tsx +++ b/karavan-app/src/main/webui/src/services/ServicesPage.tsx @@ -44,11 +44,9 @@ import { } from '@patternfly/react-table/deprecated'; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import {ServicesTableRow} from "./ServicesTableRow"; -import {DeleteServiceModal} from "./DeleteServiceModal"; -import {CreateServiceModal} from "./CreateServiceModal"; -import {useProjectStore, useStatusesStore} from "../api/ProjectStore"; +import {useStatusesStore} from "../api/ProjectStore"; import {MainToolbar} from "../designer/MainToolbar"; -import {Project, ProjectType} from "../api/ProjectModels"; +import {ProjectType} from "../api/ProjectModels"; import {KaravanApi} from "../api/KaravanApi"; import {DockerComposeService, DockerCompose, ServicesYaml} from "../api/ServiceModels"; import {shallow} from "zustand/shallow"; @@ -60,7 +58,6 @@ export function ServicesPage () { const [services, setServices] = useState<DockerCompose>(); const [containers] = useStatusesStore((state) => [state.containers, state.setContainers], shallow); - const [operation] = useState<'create' | 'delete' | 'none'>('none'); const [loading] = useState<boolean>(false); useEffect(() => { @@ -87,12 +84,6 @@ export function ServicesPage () { <ToolbarItem> <Button variant="link" icon={<RefreshIcon/>} onClick={e => getServices()}/> </ToolbarItem> - <ToolbarItem> - <Button className="dev-action-button" icon={<PlusIcon/>} - onClick={e => - useProjectStore.setState({operation: "create", project: new Project()})} - >Create</Button> - </ToolbarItem> </ToolbarContent> </Toolbar> } @@ -155,8 +146,6 @@ export function ServicesPage () { <PageSection isFilled className="kamelets-page"> {getServicesTable()} </PageSection> - {["create"].includes(operation) && <CreateServiceModal/>} - {["delete"].includes(operation) && <DeleteServiceModal/>} <ProjectLogPanel/> </PageSection> ) diff --git a/karavan-app/src/main/webui/src/util/CodeUtils.ts b/karavan-app/src/main/webui/src/util/CodeUtils.ts index 69f6fcfa..6df49034 100644 --- a/karavan-app/src/main/webui/src/util/CodeUtils.ts +++ b/karavan-app/src/main/webui/src/util/CodeUtils.ts @@ -1,8 +1,10 @@ import {ProjectFile} from "../api/ProjectModels"; import {RegistryBeanDefinition} from "karavan-core/lib/model/CamelDefinition"; -import {Integration} from "karavan-core/lib/model/IntegrationDefinition"; +import {Integration, KameletTypes, MetadataLabels} from "karavan-core/lib/model/IntegrationDefinition"; import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml"; import {CamelUi} from "../designer/utils/CamelUi"; +import {KameletApi} from "karavan-core/lib/api/KameletApi"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; export class CodeUtils { @@ -43,4 +45,29 @@ export class CodeUtils { const file = files.filter(f => f.name === 'application.properties')?.at(0); return file?.code; } + + static getCodeForNewFile(fileName: string, type: string, copyFromKamelet?: string): string { + if (type === 'INTEGRATION') { + return CamelDefinitionYaml.integrationToYaml(Integration.createNew(fileName, 'plain')); + } else if (type === 'KAMELET') { + const type: string | undefined = fileName.replace('.kamelet.yaml', '').split('-').pop(); + const kameletType: KameletTypes | undefined = (type === "sink" || type === "source" || type === "action") ? type : undefined; + const integration = Integration.createNew(fileName, 'kamelet'); + const meta: MetadataLabels = new MetadataLabels({"camel.apache.org/kamelet.type": kameletType}); + integration.metadata.labels = meta; + if (copyFromKamelet !== undefined && copyFromKamelet !== '') { + const kamelet= KameletApi.getKamelets().filter(k => k.metadata.name === copyFromKamelet).at(0); + if (kamelet) { + (integration as any).spec = kamelet.spec; + (integration as any).metadata.labels = kamelet.metadata.labels; + (integration as any).metadata.annotations = kamelet.metadata.annotations; + const i = CamelUtil.cloneIntegration(integration); + return CamelDefinitionYaml.integrationToYaml(i); + } + } + return CamelDefinitionYaml.integrationToYaml(integration); + } else { + return ''; + } + } } \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/util/StringUtils.ts b/karavan-app/src/main/webui/src/util/StringUtils.ts index 2a149781..0c32809b 100644 --- a/karavan-app/src/main/webui/src/util/StringUtils.ts +++ b/karavan-app/src/main/webui/src/util/StringUtils.ts @@ -1,3 +1,13 @@ export function isEmpty(str: string) { return !str?.trim(); } + +export function isValidFileName(input: string): boolean { + const pattern =/^[a-zA-Z0-9._-]+$/; + return pattern.test(input); +} + +export function isValidProjectId(input: string): boolean { + const pattern = /^[a-z][a-z0-9-]*$/; + return pattern.test(input); +} diff --git a/karavan-app/src/main/webui/src/util/form-util.css b/karavan-app/src/main/webui/src/util/form-util.css index 47c4819f..a5755b2a 100644 --- a/karavan-app/src/main/webui/src/util/form-util.css +++ b/karavan-app/src/main/webui/src/util/form-util.css @@ -9,4 +9,11 @@ .pf-v5-c-modal-box .text-field-with-prefix .pf-v5-c-text-input-group__text-input { padding-left: 0; -} \ No newline at end of file +} + +.pf-v5-c-modal-box .text-field-with-suffix .text-field-suffix { + margin-top: auto; + margin-bottom: auto; + padding-left: 3px; + padding-right: 3px; +} diff --git a/karavan-app/src/main/webui/src/util/useFormUtil.tsx b/karavan-app/src/main/webui/src/util/useFormUtil.tsx index fb7903d8..6e14da3e 100644 --- a/karavan-app/src/main/webui/src/util/useFormUtil.tsx +++ b/karavan-app/src/main/webui/src/util/useFormUtil.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {FieldError, UseFormReturn} from "react-hook-form"; +import {Controller, FieldError, UseFormReturn} from "react-hook-form"; import { Flex, FormGroup, @@ -54,7 +54,6 @@ export function useFormUtil(formContext: UseFormReturn<any>) { <TextInputGroup> <TextInputGroupMain className="text-field-with-prefix" type="text" id={fieldName} value={getValues()[fieldName]} - // validated={!!errors[fieldName] ? 'error' : 'default'} {...register(fieldName, {required: (required ? "Required field" : false), validate: validate})} onChange={(e, v) => { setValue(fieldName, v, {shouldValidate: true}); @@ -68,6 +67,28 @@ export function useFormUtil(formContext: UseFormReturn<any>) { ) } + function getTextFieldSuffix(fieldName: string, label: string, suffix: string, + required: boolean, + validate?: ((value: string, formValues: any) => boolean | string) | Record<string, (value: string, formValues: any) => boolean | string>) { + const {setValue, getValues, register, formState: {errors}} = formContext; + return ( + <FormGroup label={label} fieldId={fieldName} isRequired> + <TextInputGroup className="text-field-with-suffix"> + <TextInputGroupMain type="text" id={fieldName} + value={getValues()[fieldName]} + {...register(fieldName, {required: (required ? "Required field" : false), validate: validate})} + onChange={(e, v) => { + setValue(fieldName, v, {shouldValidate: true}); + }} + > + </TextInputGroupMain> + <Text className='text-field-suffix' component={TextVariants.p}>{suffix}</Text> + </TextInputGroup> + {getHelper((errors as any)[fieldName])} + </FormGroup> + ) + } + function getFormSelect(fieldName: string, label: string, options: [string, string][]) { const {register, watch, setValue, formState: {errors}} = formContext; return ( @@ -125,6 +146,6 @@ export function useFormUtil(formContext: UseFormReturn<any>) { ) } - return {getFormSelect, getTextField, getSwitches, getTextFieldPrefix} + return {getFormSelect, getTextField, getSwitches, getTextFieldPrefix, getTextFieldSuffix} }
