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 f0a01a48 Fixes for 4.7 f0a01a48 is described below commit f0a01a48feb27a12a8c03e9cf6ea083839703f82 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Mon Jul 15 11:04:31 2024 -0400 Fixes for 4.7 --- .../org/apache/camel/karavan/KaravanCache.java | 4 +- .../org/apache/camel/karavan/KaravanConstants.java | 8 ++ .../org/apache/camel/karavan/KaravanEvents.java | 8 +- .../apache/camel/karavan/KaravanStartupLoader.java | 4 +- .../camel/karavan/api/ConfigurationResource.java | 2 +- .../camel/karavan/api/ContainerResource.java | 5 +- .../apache/camel/karavan/api/DevModeResource.java | 6 +- .../apache/camel/karavan/api/ImagesResource.java | 35 +++++-- .../karavan/docker/DockerComposeConverter.java | 2 +- .../camel/karavan/docker/DockerEventHandler.java | 7 ++ .../apache/camel/karavan/docker/DockerService.java | 27 +++++- .../camel/karavan/listener/CommitListener.java | 10 +- .../camel/karavan/listener/ConfigListener.java | 6 +- .../{ConfigListener.java => DockerListener.java} | 43 ++++----- .../karavan/listener/NotificationListener.java | 16 +++- .../apache/camel/karavan/model/Configuration.java | 14 ++- .../apache/camel/karavan/model/ContainerImage.java | 68 ++++++++++++++ .../apache/camel/karavan/service/CodeService.java | 31 +++++- .../camel/karavan/service/ConfigService.java | 30 +++--- .../apache/camel/karavan/service/GitService.java | 2 - .../camel/karavan/service/ProjectService.java | 30 ++---- karavan-app/src/main/webui/src/api/KaravanApi.tsx | 35 +++++-- .../src/main/webui/src/api/NotificationService.ts | 3 + .../src/main/webui/src/api/ProjectModels.ts | 8 ++ .../src/main/webui/src/api/ProjectService.ts | 4 +- karavan-app/src/main/webui/src/api/ProjectStore.ts | 8 +- .../src/main/webui/src/log/ProjectLogPanel.tsx | 19 ++-- karavan-app/src/main/webui/src/main/Main.tsx | 13 +-- .../src/main/webui/src/project/DevModeToolbar.tsx | 1 - .../main/webui/src/project/beans/BeanWizard.tsx | 2 +- .../main/webui/src/project/builder/ImagesPanel.tsx | 104 +++++++++++++++------ .../src/project/container/ProjectContainerTab.tsx | 47 +++++----- .../src/main/webui/src/project/files/FilesTab.tsx | 20 ++-- .../main/webui/src/projects/CreateProjectModal.tsx | 7 +- .../main/webui/src/projects/DeleteProjectModal.tsx | 3 +- karavan-app/src/main/webui/src/util/StringUtils.ts | 4 + 36 files changed, 431 insertions(+), 205 deletions(-) diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanCache.java b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanCache.java index 6f05f72d..b1d29405 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanCache.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanCache.java @@ -97,8 +97,8 @@ public class KaravanCache { } public Map<String, ProjectFile> getProjectFilesMap(String projectId) { - return files.entrySet().stream().filter(es -> !Objects.isNull(es.getValue()) && Objects.equals(es.getValue().getProjectId(), projectId)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return getCopyProjectFiles().stream().filter(pf -> !Objects.isNull(pf) && Objects.equals(pf.getProjectId(), projectId)) + .collect(Collectors.toMap(ProjectFile::getName, ProjectFile::copy)); } public ProjectFile getProjectFile(String projectId, String filename) { diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanConstants.java b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanConstants.java index 63c285df..68e6f031 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanConstants.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanConstants.java @@ -41,6 +41,14 @@ public class KaravanConstants { public static final String LABEL_KUBERNETES_RUNTIME = "app.kubernetes.io/runtime"; public static final String ANNOTATION_COMMIT = "jkube.eclipse.org/git-commit"; + public static final String PROPERTY_NAME_PROJECT_ID = "camel.karavan.projectId"; + public static final String PROPERTY_NAME_PROJECT_NAME = "camel.karavan.projectName"; + public static final String PROPERTY_NAME_GAV = "camel.jbang.gav"; + + public static final String PROPERTY_FORMATTER_PROJECT_ID = PROPERTY_NAME_PROJECT_ID + "=%s"; + public static final String PROPERTY_FORMATTER_PROJECT_NAME = PROPERTY_NAME_PROJECT_NAME + "=%s"; + public static final String PROPERTY_FORMATTER_GAV = PROPERTY_NAME_GAV + "=org.camel.karavan.demo:%s:1"; + public enum CamelRuntime { CAMEL_MAIN("camel-main"), QUARKUS("quarkus"), diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanEvents.java b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanEvents.java index 58e9e366..b4f806bd 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanEvents.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanEvents.java @@ -19,17 +19,19 @@ package org.apache.camel.karavan; public class KaravanEvents { public static final String CMD_PUSH_PROJECT = "CMD_PUSH_PROJECT"; - public static final String PROJECTS_STARTED = "PROJECTS_STARTED"; + public static final String NOTIFICATION_PROJECTS_STARTED = "NOTIFICATION_PROJECTS_STARTED"; public static final String COMMIT_HAPPENED = "COMMIT_HAPPENED"; + public static final String NOTIFICATION_IMAGES_LOADED = "NOTIFICATION_IMAGES_LOADED"; public static final String CMD_SHARE_CONFIGURATION = "CMD_SHARE_CONFIGURATION"; - public static final String SHARE_HAPPENED = "SHARE_HAPPENED"; + public static final String NOTIFICATION_CONFIG_SHARED = "NOTIFICATION_CONFIG_SHARED"; - public static final String ERROR_HAPPENED = "ERROR_HAPPENED"; + public static final String NOTIFICATION_ERROR = "NOTIFICATION_ERROR"; public static final String CMD_COLLECT_CAMEL_STATUS = "CMD_COLLECT_CAMEL_STATUS"; public static final String CMD_COLLECT_CONTAINER_STATISTIC = "CMD_COLLECT_CONTAINER_STATISTIC"; public static final String CMD_CLEAN_STATUSES = "CMD_CLEAN_STATUSES"; + public static final String CMD_PULL_IMAGES = "CMD_PULL_IMAGES"; public static final String CMD_RELOAD_PROJECT_CODE = "CMD_RELOAD_PROJECT_CODE"; public static final String CMD_DELETE_CONTAINER = "CMD_DELETE_CONTAINER"; diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanStartupLoader.java b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanStartupLoader.java index b23f8f07..f6df27ea 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanStartupLoader.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanStartupLoader.java @@ -43,7 +43,7 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import static org.apache.camel.karavan.KaravanConstants.DEV_ENVIRONMENT; -import static org.apache.camel.karavan.KaravanEvents.PROJECTS_STARTED; +import static org.apache.camel.karavan.KaravanEvents.NOTIFICATION_PROJECTS_STARTED; @Default @Readiness @@ -91,7 +91,7 @@ public class KaravanStartupLoader implements HealthCheck { } else { LOGGER.info("Projects loading..."); tryStart(); - eventBus.publish(PROJECTS_STARTED, null); + eventBus.publish(NOTIFICATION_PROJECTS_STARTED, null); LOGGER.info("Projects loaded"); } } diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java index 35780c1d..220fd9b3 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java @@ -44,7 +44,7 @@ public class ConfigurationResource { @GET @Produces(MediaType.APPLICATION_JSON) public Response getConfiguration() throws Exception { - return Response.ok(configService.getConfiguration()).build(); + return Response.ok(configService.getConfiguration(null)).build(); } @GET diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/ContainerResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/ContainerResource.java index a5e82000..d20c0e6e 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/ContainerResource.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/ContainerResource.java @@ -104,7 +104,10 @@ public class ContainerResource { } return Response.ok().build(); } catch (Exception e) { - return Response.serverError().entity(e.getMessage()).build(); + var error = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); + var result = "Error while executing command " + command + " on " + projectId + ": "+ error; + LOGGER.error(result); + return Response.serverError().entity(result).build(); } } diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java index 69eb7bfe..4655b211 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java @@ -28,7 +28,6 @@ import org.apache.camel.karavan.service.ProjectService; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; -import static org.apache.camel.karavan.KaravanConstants.DEV_ENVIRONMENT; import static org.apache.camel.karavan.KaravanEvents.CMD_DELETE_CONTAINER; import static org.apache.camel.karavan.KaravanEvents.CMD_RELOAD_PROJECT_CODE; @@ -37,6 +36,9 @@ public class DevModeResource { private static final Logger LOGGER = Logger.getLogger(DevModeResource.class.getName()); + @ConfigProperty(name = "karavan.environment") + String environment; + @Inject KaravanCache karavanCache; @@ -92,7 +94,7 @@ public class DevModeResource { @Produces(MediaType.APPLICATION_JSON) @Path("/container/{projectId}") public Response getPodStatus(@PathParam("projectId") String projectId) throws RuntimeException { - PodContainerStatus cs = karavanCache.getDevModePodContainerStatus(projectId, DEV_ENVIRONMENT); + PodContainerStatus cs = karavanCache.getDevModePodContainerStatus(projectId, environment); if (cs != null) { return Response.ok(cs).build(); } else { diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java index d9a680a3..fdcfb7da 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java @@ -17,11 +17,13 @@ package org.apache.camel.karavan.api; import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.core.eventbus.EventBus; 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.docker.DockerService; +import org.apache.camel.karavan.model.ContainerImage; import org.apache.camel.karavan.model.RegistryConfig; import org.apache.camel.karavan.service.ConfigService; import org.apache.camel.karavan.service.ProjectService; @@ -29,8 +31,11 @@ import org.apache.camel.karavan.service.RegistryService; import org.jose4j.base64url.Base64; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import static org.apache.camel.karavan.KaravanEvents.CMD_PULL_IMAGES; + @Path("/ui/image") public class ImagesResource { @@ -43,25 +48,30 @@ public class ImagesResource { @Inject ProjectService projectService; + @Inject + EventBus eventBus; + @GET @Produces(MediaType.APPLICATION_JSON) - @Path("/{projectId}") - public List<String> getImagesForProject(@PathParam("projectId") String projectId) { + @Path("/project/{projectId}") + public List<ContainerImage> getImagesForProject(@PathParam("projectId") String projectId) { if (ConfigService.inKubernetes()) { return List.of(); } else { RegistryConfig registryConfig = registryService.getRegistryConfig(); String pattern = registryConfig.getGroup() + "/" + projectId; return dockerService.getImages() - .stream().filter(s -> s.contains(pattern)).sorted(Comparator.reverseOrder()).toList(); + .stream().filter(s -> s.getTag().contains(pattern)) + .sorted(Comparator.comparing(ContainerImage::getCreated).reversed().thenComparing(ContainerImage::getTag)) + .toList(); } } @POST + @Path("/project/{projectId}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @Path("/{projectId}") - public Response build(JsonObject data, @PathParam("projectId") String projectId) throws Exception { + public Response setProjectImage(JsonObject data, @PathParam("projectId") String projectId) throws Exception { try { projectService.setProjectImage(projectId, data); return Response.ok().entity(data.getString("imageName")).build(); @@ -72,7 +82,7 @@ public class ImagesResource { @DELETE @Produces(MediaType.APPLICATION_JSON) - @Path("/{imageName}") + @Path("/project/{imageName}") public Response deleteImage(@PathParam("imageName") String imageName) { imageName= new String(Base64.decode(imageName)); if (ConfigService.inKubernetes()) { @@ -82,4 +92,17 @@ public class ImagesResource { return Response.ok().build(); } } + + @POST + @Path("/pull/") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public Response share(HashMap<String, String> params) { + try { + eventBus.publish(CMD_PULL_IMAGES, JsonObject.mapFrom(params)); + return Response.ok().build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); + } + } } \ No newline at end of file diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerComposeConverter.java b/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerComposeConverter.java index b82114db..27323646 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerComposeConverter.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerComposeConverter.java @@ -45,7 +45,7 @@ public class DockerComposeConverter { DockerComposeService service = convertToDockerComposeService(name, serviceJson); composeServices.put(name, service); }); - json.put("configuration", composeServices); + json.put("services", composeServices); return json.mapTo(DockerCompose.class); } diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerEventHandler.java b/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerEventHandler.java index 7c38bde5..cdda60df 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerEventHandler.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerEventHandler.java @@ -21,6 +21,8 @@ import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.Event; import com.github.dockerjava.api.model.EventType; +import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.core.eventbus.EventBus; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.apache.camel.karavan.model.PodContainerStatus; @@ -32,6 +34,7 @@ import java.io.IOException; import java.util.Objects; import static org.apache.camel.karavan.KaravanConstants.*; +import static org.apache.camel.karavan.KaravanEvents.CMD_PULL_IMAGES; @ApplicationScoped public class DockerEventHandler implements ResultCallback<Event> { @@ -49,6 +52,9 @@ public class DockerEventHandler implements ResultCallback<Event> { LOGGER.info("DockerEventListener started"); } + @Inject + EventBus eventBus; + @Override public void onNext(Event event) { try { @@ -74,6 +80,7 @@ public class DockerEventHandler implements ResultCallback<Event> { private void syncImage(String projectId, String tag) throws InterruptedException { String image = registryService.getRegistryWithGroupForSync() + "/" + projectId + ":" + tag; + eventBus.publish(CMD_PULL_IMAGES, JsonObject.of("projectId", projectId)); dockerService.pullImage(image, true); } diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java b/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java index ded4efe5..43921da5 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java @@ -19,6 +19,8 @@ package org.apache.camel.karavan.docker; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.ListImagesCmd; +import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.model.*; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientConfig; @@ -32,6 +34,7 @@ import io.vertx.core.buffer.Buffer; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; +import org.apache.camel.karavan.model.ContainerImage; import org.apache.camel.karavan.model.DockerComposeService; import org.apache.camel.karavan.model.PodContainerStatus; import org.apache.camel.karavan.service.CodeService; @@ -302,13 +305,24 @@ public class DockerService { .flatMap(Collection::stream) .toList(); - if (pullAlways || !images.stream().anyMatch(i -> tags.contains(image))) { + if (pullAlways || images.stream().noneMatch(i -> tags.contains(image))) { var callback = new DockerPullCallback(LOGGER::info); getDockerClient().pullImageCmd(image).exec(callback); callback.awaitCompletion(); } } + public void pullImagesForProject(String projectId) throws InterruptedException { + if (!Objects.equals(registry, "registry:5000") && username.isPresent() && password.isPresent()) { + var repository = registry + "/" + group + "/" + projectId; + try (PullImageCmd cmd = getDockerClient().pullImageCmd(repository)) { + var callback = new DockerPullCallback(LOGGER::info); + cmd.exec(callback); + callback.awaitCompletion(); + } + } + } + private DockerClientConfig getDockerClientConfig() { LOGGER.info("Docker Client Configuring...."); LOGGER.info("Docker Client Registry " + registry); @@ -350,10 +364,13 @@ public class DockerService { .max().orElse(port); } - public List<String> getImages() { - return getDockerClient().listImagesCmd().withShowAll(true).exec().stream() - .filter(image -> image != null && image.getRepoTags() != null && image.getRepoTags().length > 0) - .map(image -> image.getRepoTags()[0]).toList(); + public List<ContainerImage> getImages() { + try (ListImagesCmd cmd = getDockerClient().listImagesCmd().withShowAll(true)) { + return cmd.exec().stream() + .filter(image -> image != null && image.getRepoTags() != null && image.getRepoTags().length > 0) + .map(image -> new ContainerImage(image.getId(), image.getRepoTags()[0], image.getCreated(), image.getSize())) + .toList(); + } } public void deleteImage(String imageName) { diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/listener/CommitListener.java b/karavan-app/src/main/java/org/apache/camel/karavan/listener/CommitListener.java index a0700899..7a689bf1 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/listener/CommitListener.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/listener/CommitListener.java @@ -43,10 +43,10 @@ public class CommitListener { @ConsumeEvent(value = CMD_PUSH_PROJECT, blocking = true, ordered = true) public void onCommitAndPush(JsonObject event) throws Exception { LOGGER.info("Commit event: " + event.encodePrettily()); - String projectId = event.getString("projectId"); - String message = event.getString("message"); - String userId = event.getString("userId"); - String eventId = event.getString("eventId"); + String projectId = event.getString("projectId"); + String message = event.getString("message"); + String userId = event.getString("userId"); + String eventId = event.getString("eventId"); try { Project p = projectService.commitAndPushProject(projectId, message); if (userId != null) { @@ -56,7 +56,7 @@ public class CommitListener { var error = e.getCause() != null ? e.getCause() : e; LOGGER.error("Failed to commit event", error); if (userId != null) { - eventBus.publish(ERROR_HAPPENED, JsonObject.of( + eventBus.publish(NOTIFICATION_ERROR, JsonObject.of( "userId", userId, "eventId", eventId, "className", Project.class.getSimpleName(), diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/listener/ConfigListener.java b/karavan-app/src/main/java/org/apache/camel/karavan/listener/ConfigListener.java index 703b530b..e6efc3f7 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/listener/ConfigListener.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/listener/ConfigListener.java @@ -39,7 +39,7 @@ public class ConfigListener { @Inject EventBus eventBus; - @ConsumeEvent(value = PROJECTS_STARTED, blocking = true) + @ConsumeEvent(value = NOTIFICATION_PROJECTS_STARTED, blocking = true) public void shareOnStartup(String data) throws Exception { configService.shareOnStartup(); } @@ -51,12 +51,12 @@ public class ConfigListener { LOGGER.info("Config share event: for " + (filename != null ? filename : "all")); try { configService.share(filename); - eventBus.publish(SHARE_HAPPENED, JsonObject.of("userId", userId, "className", "filename", "filename", filename)); + eventBus.publish(NOTIFICATION_CONFIG_SHARED, JsonObject.of("userId", userId, "className", "filename", "filename", filename)); } catch (Exception e) { var error = e.getCause() != null ? e.getCause() : e; LOGGER.error("Failed to share configuration", error); if (userId != null) { - eventBus.publish(ERROR_HAPPENED, JsonObject.of( + eventBus.publish(NOTIFICATION_ERROR, JsonObject.of( "userId", userId, "className", filename, "error", "Failed to share configuration: " + e.getMessage()) diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/listener/ConfigListener.java b/karavan-app/src/main/java/org/apache/camel/karavan/listener/DockerListener.java similarity index 51% copy from karavan-app/src/main/java/org/apache/camel/karavan/listener/ConfigListener.java copy to karavan-app/src/main/java/org/apache/camel/karavan/listener/DockerListener.java index 703b530b..aa340368 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/listener/ConfigListener.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/listener/DockerListener.java @@ -14,54 +14,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.camel.karavan.listener; import io.quarkus.vertx.ConsumeEvent; import io.vertx.core.json.JsonObject; import io.vertx.mutiny.core.eventbus.EventBus; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Default; import jakarta.inject.Inject; -import org.apache.camel.karavan.service.ConfigService; +import org.apache.camel.karavan.docker.DockerService; import org.jboss.logging.Logger; import static org.apache.camel.karavan.KaravanEvents.*; -@Default @ApplicationScoped -public class ConfigListener { +public class DockerListener { - private static final Logger LOGGER = Logger.getLogger(ConfigListener.class.getName()); + private static final Logger LOGGER = Logger.getLogger(DockerListener.class.getName()); @Inject - ConfigService configService; + DockerService dockerService; @Inject EventBus eventBus; - @ConsumeEvent(value = PROJECTS_STARTED, blocking = true) - public void shareOnStartup(String data) throws Exception { - configService.shareOnStartup(); - } - - @ConsumeEvent(value = CMD_SHARE_CONFIGURATION, blocking = true, ordered = true) - public void shareConfig(JsonObject event) throws Exception { - String filename = event.getString("filename"); + @ConsumeEvent(value = CMD_PULL_IMAGES, blocking = true) + void loadImagesForProject(JsonObject event) { + LOGGER.info("Pull image event: " + event.encodePrettily()); + String projectId = event.getString("projectId"); String userId = event.getString("userId"); - LOGGER.info("Config share event: for " + (filename != null ? filename : "all")); try { - configService.share(filename); - eventBus.publish(SHARE_HAPPENED, JsonObject.of("userId", userId, "className", "filename", "filename", filename)); + dockerService.pullImagesForProject(projectId); + eventBus.publish(NOTIFICATION_IMAGES_LOADED, event); } catch (Exception e) { - var error = e.getCause() != null ? e.getCause() : e; - LOGGER.error("Failed to share configuration", error); - if (userId != null) { - eventBus.publish(ERROR_HAPPENED, JsonObject.of( - "userId", userId, - "className", filename, - "error", "Failed to share configuration: " + e.getMessage()) - ); - } + var error = "Failed to load images " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()); + LOGGER.error(error); + eventBus.publish(NOTIFICATION_ERROR, JsonObject.of("userId", userId, "className", "image", "error", error) + ); } } -} +} \ No newline at end of file diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/listener/NotificationListener.java b/karavan-app/src/main/java/org/apache/camel/karavan/listener/NotificationListener.java index a0160d83..0ea7ffe8 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/listener/NotificationListener.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/listener/NotificationListener.java @@ -39,16 +39,16 @@ public class NotificationListener { public static final String EVENT_ERROR = "error"; public static final String EVENT_COMMIT = "commit"; public static final String EVENT_CONFIG_SHARED = "configShared"; + public static final String EVENT_IMAGES_LOADED = "imagesLoaded"; @Inject EventBus eventBus; - @ConsumeEvent(value = ERROR_HAPPENED, blocking = true, ordered = true) + @ConsumeEvent(value = NOTIFICATION_ERROR, blocking = true, ordered = true) public void onErrorHappened(JsonObject event) throws Exception { String eventId = event.getString("eventId"); String userId = event.getString("userId"); String className = event.getString("className"); - String error = event.getString("error"); if (userId != null) { send(userId, eventId, EVENT_ERROR, className, event); } else { @@ -56,7 +56,7 @@ public class NotificationListener { } } - @ConsumeEvent(value = SHARE_HAPPENED, blocking = true, ordered = true) + @ConsumeEvent(value = NOTIFICATION_CONFIG_SHARED, blocking = true, ordered = true) public void onShareHappened(JsonObject event) throws Exception { String userId = event.getString("userId"); String className = event.getString("className"); @@ -67,6 +67,16 @@ public class NotificationListener { } } + @ConsumeEvent(value = NOTIFICATION_IMAGES_LOADED, blocking = true, ordered = true) + public void onImageLoaded(JsonObject event) throws Exception { + String userId = event.getString("userId"); + if (userId != null) { + send(userId, null, EVENT_IMAGES_LOADED, "image", event); + } else { + sendSystem(null, EVENT_IMAGES_LOADED, "image", event); + } + } + @ConsumeEvent(value = COMMIT_HAPPENED, blocking = true, ordered = true) public void onCommitHappened(JsonObject event) throws Exception { JsonObject pj = event.getJsonObject("project"); diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/Configuration.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/Configuration.java index 2c9fdac7..f554a219 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/model/Configuration.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/Configuration.java @@ -17,6 +17,7 @@ package org.apache.camel.karavan.model; import java.util.List; +import java.util.Map; public class Configuration { private String title; @@ -26,17 +27,20 @@ public class Configuration { private List<String> environments; private List<String> configFilenames; private List<Object> status; + private Map<String, String> advanced; public Configuration() { } - public Configuration(String title, String version, String infrastructure, String environment, List<String> environments, List<String> configFilenames) { + public Configuration(String title, String version, String infrastructure, String environment, List<String> environments, List<String> configFilenames, + Map<String, String> advanced) { this.title = title; this.version = version; this.infrastructure = infrastructure; this.environment = environment; this.environments = environments; this.configFilenames = configFilenames; + this.advanced = advanced; } public String getTitle() { @@ -94,4 +98,12 @@ public class Configuration { public void setConfigFilenames(List<String> configFilenames) { this.configFilenames = configFilenames; } + + public Map<String, String> getAdvanced() { + return advanced; + } + + public void setAdvanced(Map<String, String> advanced) { + this.advanced = advanced; + } } diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/ContainerImage.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/ContainerImage.java new file mode 100644 index 00000000..25966f40 --- /dev/null +++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/ContainerImage.java @@ -0,0 +1,68 @@ +/* + * 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. + */ + +package org.apache.camel.karavan.model; + +public class ContainerImage { + + private String id; + private String tag; + private Long created; + private Long size; + + public ContainerImage() { + } + + public ContainerImage(String id, String tag, Long created, Long size) { + this.id = id; + this.tag = tag; + this.created = created; + this.size = size; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public Long getCreated() { + return created; + } + + public void setCreated(Long created) { + this.created = created; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } +} \ No newline at end of file diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java index e820091d..0e16df47 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java @@ -44,8 +44,7 @@ import java.time.Instant; import java.util.*; import java.util.stream.Collectors; -import static org.apache.camel.karavan.KaravanConstants.DEVMODE_IMAGE; -import static org.apache.camel.karavan.KaravanConstants.DEV_ENVIRONMENT; +import static org.apache.camel.karavan.KaravanConstants.*; @ApplicationScoped public class CodeService { @@ -72,6 +71,9 @@ public class CodeService { @ConfigProperty(name = "karavan.environment") String environment; + @ConfigProperty(name = "karavan.gav") + Optional<String> gav; + @Inject KaravanCache karavanCache; @@ -280,9 +282,9 @@ public class CodeService { .replace(prefix, ""); } - public static String getValueForProperty(String line, String property) { - String prefix = property + "="; - return line.replace(prefix, ""); + public static String getPropertyName(String line) { + var parts = line.indexOf("="); + return line.substring(0, parts).trim(); } public String getProjectName(String file) { @@ -290,6 +292,21 @@ public class CodeService { return name != null && !name.isBlank() ? name : getProperty(file, PROPERTY_PROJECT_NAME_OLD); } + public static String replaceProperty(String file, String property, String value) { + return file.lines().map(line -> { + if (line.startsWith(property)) { + return property + "=" + value; + } else { + return line; + } + }).collect(Collectors.joining(System.lineSeparator())); + } + + public static String removePropertiesStartWith(String file, String startWith) { + return file.lines().filter(line -> !line.startsWith(startWith)) + .collect(Collectors.joining(System.lineSeparator())); + } + public ProjectFile createInitialProjectCompose(Project project, int nextAvailablePort) { String template = getTemplateText(PROJECT_COMPOSE_FILENAME); String code = substituteVariables(template, Map.of( @@ -421,4 +438,8 @@ public class CodeService { public String getFileString(String fullName) { return vertx.fileSystem().readFileBlocking(fullName).toString(); } + + public String getGavFormatter() { + return PROPERTY_NAME_GAV + "=" + gav.orElse("org.camel.karavan.demo") + ":%s:1"; + } } 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 58cd1bc3..a4cb1e4d 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 @@ -17,6 +17,7 @@ package org.apache.camel.karavan.service; import io.quarkus.runtime.StartupEvent; +import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; @@ -72,19 +73,23 @@ public class ConfigService { private static Boolean inKubernetes; private static Boolean inDocker; - void onStart(@Observes StartupEvent ev) { - var configFilenames = codeService.getConfigurationList(); - configuration = new Configuration( - title, - version, - inKubernetes() ? "kubernetes" : "docker", - environment, - getEnvs(), - configFilenames - ); + void onStart(@Observes @Priority(10) StartupEvent ev) { + getConfiguration(null); } - public Configuration getConfiguration() { + public Configuration getConfiguration(Map<String, String> advanced) { + if (configuration == null) { + var configFilenames = codeService.getConfigurationList(); + configuration = new Configuration( + title, + version, + inKubernetes() ? "kubernetes" : "docker", + environment, + getEnvs(), + configFilenames, + advanced + ); + } return configuration; } @@ -162,8 +167,5 @@ public class ConfigService { return ConfigProvider.getConfig().getOptionalValue("karavan.appName", String.class).orElse("karavan"); } - public static String getParamWithAppPrefix(String param) { - return getAppName() + "-" + param; - } } \ No newline at end of file diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java index 8ef63cc7..2b47c1e5 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java @@ -326,8 +326,6 @@ public class GitService { LOGGER.infof("Temp folder %s is created for deletion of project %s", folder, projectId); try { Git git = getGit(true, folder); -// git = clone(folder, gitConfig.getUri(), gitConfig.getBranch()); -// checkout(git, false, null, null, gitConfig.getBranch()); addDeletedFolderToIndex(git, folder, projectId, files); commitAddedAndPush(git, gitConfig.getBranch(), commitMessage); LOGGER.info("Delete Temp folder " + folder); 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 8ff10c83..18a6b2f2 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 @@ -36,7 +36,7 @@ import java.time.Instant; import java.util.*; import java.util.stream.Collectors; -import static org.apache.camel.karavan.KaravanConstants.DEV_ENVIRONMENT; +import static org.apache.camel.karavan.KaravanConstants.*; import static org.apache.camel.karavan.KaravanEvents.CMD_PUSH_PROJECT; import static org.apache.camel.karavan.KaravanEvents.POD_CONTAINER_UPDATED; import static org.apache.camel.karavan.service.CodeService.*; @@ -183,15 +183,15 @@ public class ProjectService { private void modifyPropertyFileOnProjectCopy(ProjectFile propertyFile, Project sourceProject, Project project) { String fileContent = propertyFile.getCode(); - String sourceProjectIdProperty = String.format(Property.PROJECT_ID.getKeyValueFormatter(), sourceProject.getProjectId()); - String sourceProjectNameProperty = String.format(Property.PROJECT_NAME.getKeyValueFormatter(), sourceProject.getName()); - String sourceGavProperty = String.format(Property.GAV.getKeyValueFormatter(), sourceProject.getProjectId()); + String sourceProjectIdProperty = String.format(PROPERTY_FORMATTER_PROJECT_ID, sourceProject.getProjectId()); + String sourceProjectNameProperty = String.format(PROPERTY_FORMATTER_PROJECT_NAME, sourceProject.getName()); + String sourceGavProperty = String.format(codeService.getGavFormatter(), sourceProject.getProjectId()); String[] searchValues = {sourceProjectIdProperty, sourceProjectNameProperty, sourceGavProperty}; - String updatedProjectIdProperty = String.format(Property.PROJECT_ID.getKeyValueFormatter(), project.getProjectId()); - String updatedProjectNameProperty = String.format(Property.PROJECT_NAME.getKeyValueFormatter(), project.getName()); - String updatedGavProperty = String.format(Property.GAV.getKeyValueFormatter(), project.getProjectId()); + String updatedProjectIdProperty = String.format(PROPERTY_FORMATTER_PROJECT_ID, project.getProjectId()); + String updatedProjectNameProperty = String.format(PROPERTY_FORMATTER_PROJECT_NAME, project.getName()); + String updatedGavProperty = String.format(codeService.getGavFormatter(), project.getProjectId()); String[] replacementValues = {updatedProjectIdProperty, updatedProjectNameProperty, updatedGavProperty}; @@ -305,20 +305,4 @@ public class ProjectService { return INTERNAL_PORT; } } - - public enum Property { - PROJECT_ID("camel.karavan.projectId=%s"), - PROJECT_NAME("camel.karavan.projectName=%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/webui/src/api/KaravanApi.tsx b/karavan-app/src/main/webui/src/api/KaravanApi.tsx index 99763ee5..cc2b1d6b 100644 --- a/karavan-app/src/main/webui/src/api/KaravanApi.tsx +++ b/karavan-app/src/main/webui/src/api/KaravanApi.tsx @@ -484,14 +484,7 @@ export class KaravanApi { }); } - static async setProjectImage(projectId: string, imageName: string, commit: boolean, message: string, after: (res: AxiosResponse<any>) => void) { - instance.post('/ui/image/' + projectId, {imageName: imageName, commit: commit, message: message}) - .then(res => { - after(res); - }).catch(err => { - after(err); - }); - } + static async stopBuild(environment: string, buildName: string, after: (res: AxiosResponse<any>) => void) { instance.delete('/ui/project/build/' + environment + "/" + buildName) @@ -621,7 +614,7 @@ export class KaravanApi { } static async getImages(projectId: string, after: (string: []) => void) { - instance.get('/ui/image/' + projectId) + instance.get('/ui/image/project/' + projectId) .then(res => { if (res.status === 200) { after(res.data); @@ -631,8 +624,17 @@ export class KaravanApi { }); } + static async setProjectImage(projectId: string, imageName: string, commit: boolean, message: string, after: (res: AxiosResponse<any>) => void) { + instance.post('/ui/image/project/' + projectId, {imageName: imageName, commit: commit, message: message}) + .then(res => { + after(res); + }).catch(err => { + after(err); + }); + } + static async deleteImage(imageName: string, after: () => void) { - instance.delete('/ui/image/' + Buffer.from(imageName).toString('base64')) + instance.delete('/ui/image/project/' + Buffer.from(imageName).toString('base64')) .then(res => { if (res.status === 200) { after(); @@ -642,6 +644,19 @@ export class KaravanApi { }); } + static async pullProjectImages(projectId: string, after: (res: AxiosResponse<any>) => void) { + const params = { + 'projectId': projectId, + 'userId': KaravanApi.getUserId() + }; + instance.post('/ui/image/pull/', params) + .then(res => { + after(res); + }).catch(err => { + after(err); + }); + } + static async getSecrets(after: (any: []) => void) { instance.get('/ui/infrastructure/secrets') .then(res => { diff --git a/karavan-app/src/main/webui/src/api/NotificationService.ts b/karavan-app/src/main/webui/src/api/NotificationService.ts index cd610df1..75fe5553 100644 --- a/karavan-app/src/main/webui/src/api/NotificationService.ts +++ b/karavan-app/src/main/webui/src/api/NotificationService.ts @@ -53,6 +53,9 @@ const sub = NotificationEventBus.onEvent()?.subscribe((event: KaravanEvent) => { ProjectService.refreshProjectData(projectId); }); } + } else if (event.event === 'imagesLoaded') { + const projectId = event.data?.projectId; + EventBus.sendAlert('Success', 'Image loaded for ' + projectId); } else if (event.event === 'error') { const error = event.data?.error; EventBus.sendAlert('Error', error, "danger"); diff --git a/karavan-app/src/main/webui/src/api/ProjectModels.ts b/karavan-app/src/main/webui/src/api/ProjectModels.ts index ccea5426..7431521e 100644 --- a/karavan-app/src/main/webui/src/api/ProjectModels.ts +++ b/karavan-app/src/main/webui/src/api/ProjectModels.ts @@ -23,6 +23,7 @@ export class AppConfig { environments: string[] = []; status: any[] = []; configFilenames: any[] = []; + advanced: any = {} } export enum ProjectType { @@ -151,6 +152,13 @@ export class ProjectFileType { } } +export class ContainerImage { + id: string = ''; + tag: string = ''; + size: number = 0; + created: number = 0; +} + export const ProjectFileTypes: ProjectFileType[] = [ new ProjectFileType("INTEGRATION", "Integration", "camel.yaml"), new ProjectFileType("KAMELET", "Kamelet", "kamelet.yaml"), diff --git a/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-app/src/main/webui/src/api/ProjectService.ts index bce8a5fa..f936e578 100644 --- a/karavan-app/src/main/webui/src/api/ProjectService.ts +++ b/karavan-app/src/main/webui/src/api/ProjectService.ts @@ -16,7 +16,7 @@ */ import {KaravanApi} from './KaravanApi'; -import {DeploymentStatus, ContainerStatus, Project, ProjectFile, ServiceStatus, CamelStatus} from './ProjectModels'; +import {DeploymentStatus, ContainerStatus, Project, ProjectFile, ServiceStatus, CamelStatus, ContainerImage} from './ProjectModels'; import {TemplateApi} from 'karavan-core/lib/api/TemplateApi'; import {InfrastructureAPI} from '../designer/utils/InfrastructureAPI'; import {unstable_batchedUpdates} from 'react-dom' @@ -234,7 +234,7 @@ export class ProjectService { } public static refreshImages(projectId: string) { - KaravanApi.getImages(projectId, (res: any) => { + KaravanApi.getImages(projectId, (res: ContainerImage[]) => { useProjectStore.setState({images: res}); }); } diff --git a/karavan-app/src/main/webui/src/api/ProjectStore.ts b/karavan-app/src/main/webui/src/api/ProjectStore.ts index 07875a26..87b6bd68 100644 --- a/karavan-app/src/main/webui/src/api/ProjectStore.ts +++ b/karavan-app/src/main/webui/src/api/ProjectStore.ts @@ -22,7 +22,7 @@ import { Project, ProjectFile, ServiceStatus, - CamelStatus, + CamelStatus, ContainerImage, } from "./ProjectModels"; import {ProjectEventBus} from "./ProjectEventBus"; import {unstable_batchedUpdates} from "react-dom"; @@ -121,8 +121,8 @@ interface ProjectState { isPulling: boolean, isPushing: boolean, isRunning: boolean, - images: string [], - setImages: (images: string []) => void; + images: ContainerImage [], + setImages: (images: ContainerImage []) => void; project: Project; setProject: (project: Project, operation: "create" | "select" | "delete"| "none" | "copy") => void; operation: "create" | "select" | "delete" | "none" | "copy"; @@ -167,7 +167,7 @@ export const useProjectStore = createWithEqualityFn<ProjectState>((set) => ({ return {tabIndex: tabIndex}; }); }, - setImages: (images: string[]) => { + setImages: (images: ContainerImage[]) => { set((state: ProjectState) => { state.images.length = 0; state.images.push(...images); diff --git a/karavan-app/src/main/webui/src/log/ProjectLogPanel.tsx b/karavan-app/src/main/webui/src/log/ProjectLogPanel.tsx index afaf4933..42ecea1c 100644 --- a/karavan-app/src/main/webui/src/log/ProjectLogPanel.tsx +++ b/karavan-app/src/main/webui/src/log/ProjectLogPanel.tsx @@ -17,13 +17,12 @@ import React, {useEffect, useState} from 'react'; import {Button, Checkbox, Label, PageSection, Tooltip, TooltipPosition} from '@patternfly/react-core'; -import '../designer/karavan.css'; +import './ProjectLog.css'; import CloseIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-icon'; import CleanIcon from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon'; -import {useLogStore, useStatusesStore} from "../api/ProjectStore"; -import {KaravanApi} from "../api/KaravanApi"; +import {useLogStore} from "../api/ProjectStore"; import {shallow} from "zustand/shallow"; import {ProjectEventBus} from "../api/ProjectEventBus"; import {ProjectLog} from "./ProjectLog"; @@ -35,23 +34,23 @@ export function ProjectLogPanel () { const [showLog, type, setShowLog, podName] = useLogStore( (state) => [state.showLog, state.type, state.setShowLog, state.podName], shallow) - const [containers] = useStatusesStore((state) => [state.containers], shallow); const [height, setHeight] = useState(INITIAL_LOG_HEIGHT); const [isTextWrapped, setIsTextWrapped] = useState(true); const [autoScroll, setAutoScroll] = useState(true); - const [fetch, setFetch] = useState<Promise<void> | undefined>(undefined); + const [controller, setController] = React.useState(new AbortController()); const [currentPodName, setCurrentPodName] = useState<string | undefined>(undefined); useEffect(() => { - const controller = new AbortController(); + controller.abort() + const c = new AbortController(); + setController(c); if (showLog && type !== 'none' && podName !== undefined) { - const f = LogWatchApi.fetchData(type, podName, controller).then(value => { - console.log("Fetch Started for: " + podName) + const f = LogWatchApi.fetchData(type, podName, c).then(value => { + console.log("Fetch Started for: " + podName); }); - setFetch(f); } return () => { - controller.abort(); + c.abort(); }; }, [showLog, type, podName]); diff --git a/karavan-app/src/main/webui/src/main/Main.tsx b/karavan-app/src/main/webui/src/main/Main.tsx index 4e0d75de..d29961b2 100644 --- a/karavan-app/src/main/webui/src/main/Main.tsx +++ b/karavan-app/src/main/webui/src/main/Main.tsx @@ -89,15 +89,12 @@ export function Main() { <Page className="karavan"> {!showMain() && <MainLoader/>} {showMain() && - <Flex direction={{default: "row"}} style={{width: "100%", height: "100%"}} - alignItems={{default: "alignItemsStretch"}} spaceItems={{default: 'spaceItemsNone'}}> - <FlexItem> - {<PageNavigation/>} - </FlexItem> - <FlexItem flex={{default: "flex_2"}} style={{height: "100%"}}> + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'stretch', gap: '0', width: "100%", height: "100%"}}> + {<PageNavigation/>} + <div style={{height: "100%", flexGrow: '2'}}> {<MainRoutes/>} - </FlexItem> - </Flex> + </div> + </div> } <Notification/> </Page> diff --git a/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx b/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx index 8df9a64e..84f3eff4 100644 --- a/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx +++ b/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx @@ -83,7 +83,6 @@ export function DevModeToolbar(props: Props) { function refreshContainer(){ ProjectService.refreshContainerStatus(project.projectId, config.environment); ProjectService.refreshCamelStatus(project.projectId, config.environment); - ProjectService.refreshImages(project.projectId); if (refreshTrace) { ProjectService.refreshCamelTraces(project.projectId, config.environment); } 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 1ead482f..a41a6bd9 100644 --- a/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx +++ b/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx @@ -115,7 +115,7 @@ export function BeanWizard() { useEffect(() => { getBeans.filter(b => b.name === templateBeanName).forEach(b => { - setBean(new BeanFactoryDefinition({...b})) + setBean(new BeanFactoryDefinition({...b})) }); }, [templateBeanName]); diff --git a/karavan-app/src/main/webui/src/project/builder/ImagesPanel.tsx b/karavan-app/src/main/webui/src/project/builder/ImagesPanel.tsx index 886b2dd3..b4dea27d 100644 --- a/karavan-app/src/main/webui/src/project/builder/ImagesPanel.tsx +++ b/karavan-app/src/main/webui/src/project/builder/ImagesPanel.tsx @@ -22,8 +22,6 @@ import { Flex, FlexItem, Modal, - Panel, - PanelHeader, TextContent, Text, TextVariants, @@ -36,7 +34,7 @@ import { Switch, TextInput, Card, - CardBody, CardHeader + CardBody, CardHeader, HelperTextItem, HelperText } from '@patternfly/react-core'; import '../../designer/karavan.css'; import {useFilesStore, useProjectStore} from "../../api/ProjectStore"; @@ -50,6 +48,8 @@ import {ProjectService} from "../../api/ProjectService"; import {ServicesYaml} from "../../api/ServiceModels"; import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon"; import {EventBus} from "../../designer/utils/EventBus"; +import {getMegabytes} from "../../util/StringUtils"; +import PullIcon from "@patternfly/react-icons/dist/esm/icons/cloud-download-alt-icon"; export function ImagesPanel() { @@ -57,6 +57,7 @@ export function ImagesPanel() { const [files] = useFilesStore((s) => [s.files], shallow); const [showSetConfirmation, setShowSetConfirmation] = useState<boolean>(false); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false); + const [showPullConfirmation, setShowPullConfirmation] = useState<boolean>(false); const [imageName, setImageName] = useState<string>(); const [commitChanges, setCommitChanges] = useState<boolean>(false); const [commitMessage, setCommitMessage] = useState(''); @@ -130,12 +131,12 @@ export function ImagesPanel() { function getDeleteConfirmation() { return (<Modal - className="modal-delete" title="Confirmation" + variant='medium' isOpen={showDeleteConfirmation} onClose={() => setShowDeleteConfirmation(false)} actions={[ - <Button key="confirm" variant="primary" onClick={e => { + <Button key="confirm" variant="danger" onClick={e => { if (imageName) { KaravanApi.deleteImage(imageName, () => { EventBus.sendAlert("Image deleted", "Image " + imageName + " deleted", 'info'); @@ -148,8 +149,45 @@ export function ImagesPanel() { onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button> ]} onEscapePress={e => setShowDeleteConfirmation(false)}> - <div>{"Delete image:"}</div> - <div>{imageName}</div> + <TextContent> + <Text component='p'> + {"Delete image: "}<b>{imageName}</b> + </Text> + <HelperText> + <HelperTextItem variant="warning" hasIcon> + Container Image will be deleted from Docker Engine only! + </HelperTextItem> + </HelperText> + </TextContent> + </Modal>) + } + + function getPullConfirmation() { + return (<Modal + title="Confirmation" + variant='medium' + isOpen={showPullConfirmation} + onClose={() => setShowPullConfirmation(false)} + actions={[ + <Button key="confirm" variant="primary" onClick={e => { + KaravanApi.pullProjectImages(project.projectId, () => { + setShowPullConfirmation(false); + }); + }}>Pull + </Button>, + <Button key="cancel" variant="link" onClick={_ => setShowPullConfirmation(false)}>Cancel</Button> + ]} + onEscapePress={e => setShowPullConfirmation(false)}> + <TextContent> + <Text component='p'> + {"Pull all images from Registry for project: "}<b>{project.projectId}</b> + </Text> + <HelperText> + <HelperTextItem variant="warning" hasIcon> + Pull is a background process that might take some time! + </HelperTextItem> + </HelperText> + </TextContent> </Modal>) } @@ -158,41 +196,53 @@ export function ImagesPanel() { <PageSection className="project-tab-panel project-images-panel" padding={{default: "padding"}}> <Card> <CardHeader> - <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexStart"}}> + <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentSpaceBetween"}}> <FlexItem> <TextContent> <Text component={TextVariants.h6}>Images</Text> </TextContent> </FlexItem> + <FlexItem> + <Tooltip content="Pull all images from registry" position={"bottom-end"}> + <Button variant={"secondary"} className="dev-action-button" icon={<PullIcon/>} + onClick={() => setShowPullConfirmation(true)}> + Pull + </Button> + </Tooltip> + </FlexItem> </Flex> </CardHeader> <CardBody className='table-card-body'> <Table aria-label="Images" variant={"compact"} className={"table"}> <Thead> <Tr> - <Th key='status' width={10}></Th> + <Th key='status' modifier={"fitContent"}>Status</Th> <Th key='image' width={20}>Image</Th> <Th key='tag' width={10}>Tag</Th> - <Th key='actions' width={10}></Th> + <Th key='size' width={10}>Size</Th> + <Th key='created' width={10}>Created</Th> + <Th key='actions' width={20}></Th> </Tr> </Thead> <Tbody> {images.map(image => { - const index = image.lastIndexOf(":"); - const name = image.substring(0, index); - const tag = image.substring(index + 1); - return <Tr key={image}> + const fullName = image.tag; + const index = fullName.lastIndexOf(":"); + const name = fullName.substring(0, index); + const tag = fullName.substring(index + 1); + const created = new Date(image.created * 1000); + const size = getMegabytes(image.size)?.toFixed(0); + return <Tr key={image.id}> <Td modifier={"fitContent"}> - {image === projectImage ? <SetIcon/> : <div/>} - </Td> - <Td> - {name} - </Td> - <Td> - {tag} + {fullName === projectImage ? <SetIcon/> : <div/>} </Td> + <Td>{name}</Td> + <Td>{tag}</Td> + <Td>{size} MB</Td> + <Td>{created.toISOString()}</Td> <Td modifier={"fitContent"} isActionCell> <Flex direction={{default: "row"}} + flexWrap={{default:'nowrap'}} justifyContent={{default: "justifyContentFlexEnd"}} spaceItems={{default: 'spaceItemsNone'}}> <FlexItem> @@ -200,9 +250,9 @@ export function ImagesPanel() { <Button variant={"plain"} className='dev-action-button' icon={<DeleteIcon/>} - isDisabled={image === projectImage} + isDisabled={fullName === projectImage} onClick={e => { - setImageName(image); + setImageName(fullName); setShowDeleteConfirmation(true); }}> </Button> @@ -210,12 +260,11 @@ export function ImagesPanel() { </FlexItem> <FlexItem> <Tooltip content="Set project image" position={"bottom"}> - <Button style={{padding: '0'}} - variant={"plain"} + <Button variant={"plain"} className='dev-action-button' - isDisabled={image === projectImage} + isDisabled={fullName === projectImage} onClick={e => { - setImageName(image); + setImageName(fullName); setCommitMessage(commitMessage === '' ? new Date().toLocaleString() : commitMessage); setShowSetConfirmation(true); }}> @@ -244,6 +293,7 @@ export function ImagesPanel() { </Table> {showSetConfirmation && getSetConfirmation()} {showDeleteConfirmation && getDeleteConfirmation()} + {showPullConfirmation && getPullConfirmation()} </CardBody> </Card> </PageSection> diff --git a/karavan-app/src/main/webui/src/project/container/ProjectContainerTab.tsx b/karavan-app/src/main/webui/src/project/container/ProjectContainerTab.tsx index a4f525da..702c34f9 100644 --- a/karavan-app/src/main/webui/src/project/container/ProjectContainerTab.tsx +++ b/karavan-app/src/main/webui/src/project/container/ProjectContainerTab.tsx @@ -30,37 +30,36 @@ export function ProjectContainerTab() { const {config} = useAppConfigStore(); + const env = config.environment; return ( <PageSection className="project-tab-panel project-build-panel" padding={{default: "padding"}}> <div> - {config.environments.map(env => - <Card key={env} className="project-status"> - <CardBody> - <DescriptionList isHorizontal horizontalTermWidthModifier={{default: '20ch'}}> + <Card key={env} className="project-status"> + <CardBody> + <DescriptionList isHorizontal horizontalTermWidthModifier={{default: '20ch'}}> + <DescriptionListGroup> + <DescriptionListTerm>Environment</DescriptionListTerm> + <DescriptionListDescription> + <Badge className="badge">{env}</Badge> + </DescriptionListDescription> + </DescriptionListGroup> + {config.infrastructure === 'kubernetes' && <DescriptionListGroup> - <DescriptionListTerm>Environment</DescriptionListTerm> + <DescriptionListTerm>Deployment</DescriptionListTerm> <DescriptionListDescription> - <Badge className="badge">{env}</Badge> + <DeploymentPanel env={env}/> </DescriptionListDescription> </DescriptionListGroup> - {config.infrastructure === 'kubernetes' && - <DescriptionListGroup> - <DescriptionListTerm>Deployment</DescriptionListTerm> - <DescriptionListDescription> - <DeploymentPanel env={env}/> - </DescriptionListDescription> - </DescriptionListGroup> - } - <DescriptionListGroup> - <DescriptionListTerm>Containers</DescriptionListTerm> - <DescriptionListDescription> - <ContainerPanel env={env}/> - </DescriptionListDescription> - </DescriptionListGroup> - </DescriptionList> - </CardBody> - </Card> - )} + } + <DescriptionListGroup> + <DescriptionListTerm>Containers</DescriptionListTerm> + <DescriptionListDescription> + <ContainerPanel env={env}/> + </DescriptionListDescription> + </DescriptionListGroup> + </DescriptionList> + </CardBody> + </Card> </div> </PageSection> ) 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 81e8da38..da6ab20b 100644 --- a/karavan-app/src/main/webui/src/project/files/FilesTab.tsx +++ b/karavan-app/src/main/webui/src/project/files/FilesTab.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import { Badge, Bullseye, @@ -101,14 +101,16 @@ export function FilesTab () { const currentEnv = config.environment; const envs = config.environments; - const parts = filename.split('.'); - const prefix = parts[0] && envs.includes(parts[0]) ? parts[0] : undefined; - if (prefix && envs.includes(prefix) && prefix !== currentEnv) { - return true; - } - if (!prefix) { - const prefixedFilename = `${currentEnv}.${filename}`; - return allFiles.map(f => f.name).includes(prefixedFilename); + if (filename.endsWith(".jkube.yaml") || filename.endsWith(".docker-compose.yaml")) { + const parts = filename.split('.'); + const prefix = parts[0] && envs.includes(parts[0]) ? parts[0] : undefined; + if (prefix && envs.includes(prefix) && prefix !== currentEnv) { + return true; + } + if (!prefix) { + const prefixedFilename = `${currentEnv}.${filename}`; + return allFiles.map(f => f.name).includes(prefixedFilename); + } } return false; } diff --git a/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx b/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx index 4ce38392..a00aa8b3 100644 --- a/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx +++ b/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx @@ -24,7 +24,7 @@ import { ModalVariant, } from '@patternfly/react-core'; import '../designer/karavan.css'; -import {useProjectStore} from "../api/ProjectStore"; +import {useProjectsStore, useProjectStore} from "../api/ProjectStore"; import {ProjectService} from "../api/ProjectService"; import {Project} from "../api/ProjectModels"; import {isValidProjectId} from "../util/StringUtils"; @@ -33,10 +33,12 @@ import {SubmitHandler, useForm} from "react-hook-form"; import {useFormUtil} from "../util/useFormUtil"; import {KaravanApi} from "../api/KaravanApi"; import {AxiosResponse} from "axios"; +import {shallow} from "zustand/shallow"; export function CreateProjectModal() { - const {project, operation, setOperation} = useProjectStore(); + const [project, operation, setOperation] = useProjectStore((s) => [s.project, s.operation, s.setOperation], shallow); + const [projects] = useProjectsStore((s) => [s.projects], shallow); const [isReset, setReset] = React.useState(false); const [backendError, setBackendError] = React.useState<string>(); const formContext = useForm<Project>({mode: "all"}); @@ -115,6 +117,7 @@ export function CreateProjectModal() { 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", + uniques: v => !projects.map(p=> p.name).includes(v) || "Project already exists!", })} {getTextField('name', 'Name', { length: v => v.length > 5 || 'Project name should be longer that 5 characters', diff --git a/karavan-app/src/main/webui/src/projects/DeleteProjectModal.tsx b/karavan-app/src/main/webui/src/projects/DeleteProjectModal.tsx index 3e10d7d0..5403cead 100644 --- a/karavan-app/src/main/webui/src/projects/DeleteProjectModal.tsx +++ b/karavan-app/src/main/webui/src/projects/DeleteProjectModal.tsx @@ -24,10 +24,11 @@ import { import '../designer/karavan.css'; import {useProjectStore} from "../api/ProjectStore"; import {ProjectService} from "../api/ProjectService"; +import {shallow} from "zustand/shallow"; export function DeleteProjectModal () { - const {project, operation} = useProjectStore(); + const [project, operation] = useProjectStore((s) => [s.project, s.operation], shallow); const [deleteContainers, setDeleteContainers] = useState(false); function closeModal () { diff --git a/karavan-app/src/main/webui/src/util/StringUtils.ts b/karavan-app/src/main/webui/src/util/StringUtils.ts index ec04e358..e6ad8644 100644 --- a/karavan-app/src/main/webui/src/util/StringUtils.ts +++ b/karavan-app/src/main/webui/src/util/StringUtils.ts @@ -57,3 +57,7 @@ export function isValidPassword(password: string): boolean { hasSpecialCharacter(password) && hasMinimumLength(password); } + +export function getMegabytes(bytes?: number): number { + return (bytes ? (bytes / 1024 / 1024) : 0); +}