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);
+}

Reply via email to