This is an automated email from the ASF dual-hosted git repository.

marat pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git

commit 562cf3b940775570001a2f5a863e6e03c9103234
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Sep 8 12:55:59 2023 -0400

    Refactor for #817
---
 .../camel/karavan/api/ComponentResources.java      |   2 +-
 .../camel/karavan/api/ConfigurationResource.java   |   2 +-
 .../apache/camel/karavan/api/DevModeResource.java  |   4 +-
 .../apache/camel/karavan/api/ImagesResource.java   |  20 ++
 .../camel/karavan/api/InfrastructureResource.java  |  11 +-
 .../apache/camel/karavan/api/KameletResources.java |   4 +-
 .../apache/camel/karavan/api/LogWatchResource.java |   5 +-
 .../camel/karavan/api/ProjectFileResource.java     |   2 +-
 .../apache/camel/karavan/api/ProjectResource.java  |  25 +-
 .../karavan/{service => code}/CodeService.java     |  49 +--
 .../camel/karavan/code/DockerComposeConverter.java | 111 +++++++
 .../camel/karavan/code/model/DockerCompose.java    |  29 ++
 .../model/DockerComposeHealthCheck.java}           |   6 +-
 .../model/DockerComposeService.java                |   8 +-
 .../camel/karavan/docker/DockerForGitea.java       |  20 +-
 .../camel/karavan/docker/DockerForInfinispan.java  |   4 +-
 .../camel/karavan/docker/DockerForKaravan.java     |   4 +-
 .../camel/karavan/docker/DockerForRegistry.java    |   2 +-
 .../apache/camel/karavan/docker/DockerService.java |   3 +-
 .../camel/karavan/docker/DockerServiceUtils.java   |   4 +-
 .../camel/karavan/docker/GiteaCheckCallback.java   |  53 ---
 .../camel/karavan/{service => git}/GitService.java |   9 +-
 .../karavan/{service => git}/GiteaService.java     |   6 +-
 .../{infinispan => git}/model/GitConfig.java       |   2 +-
 .../karavan/{infinispan => git}/model/GitRepo.java |   2 +-
 .../{infinispan => git}/model/GitRepoFile.java     |   2 +-
 .../camel/karavan/infinispan/model/Project.java    |  21 ++
 .../karavan/kubernetes/KubernetesService.java      |   6 +-
 .../camel/karavan/kubernetes/PodEventHandler.java  |  14 +-
 .../apache/camel/karavan/service/CamelService.java |  10 +-
 .../karavan/{shared => service}/ConfigService.java |   3 +-
 ...entService.java => ContainerStatusService.java} |   7 +-
 .../camel/karavan/service/KaravanService.java      |   2 +-
 .../camel/karavan/service/ProjectService.java      |  65 +++-
 .../camel/karavan/service/RegistryService.java     |   3 -
 .../camel/karavan/service/ScheduledService.java    |   7 +-
 .../org/apache/camel/karavan/shared/Constants.java |   2 -
 .../org/apache/camel/karavan/shared/EventType.java |  22 --
 .../src/main/webui/src/api/KaravanApi.tsx          |  24 +-
 .../src/main/webui/src/api/ProjectModels.ts        |   7 +-
 .../src/main/webui/src/api/ProjectStore.ts         |  20 +-
 .../karavan-app/src/main/webui/src/index.css       |  13 +-
 .../karavan-app/src/main/webui/src/main/Main.tsx   |  44 ++-
 .../src/main/webui/src/main/MainDataPoller.tsx     |   2 +-
 .../src/main/webui/src/project/BuildToolbar.tsx    |  22 ++
 .../src/main/webui/src/project/DevModeToolbar.tsx  |  24 +-
 .../main/webui/src/project/ProjectDataPoller.tsx   |   7 +-
 .../src/main/webui/src/project/ProjectPage.tsx     |   3 +-
 .../src/main/webui/src/project/ProjectPanel.tsx    |   8 +-
 .../src/main/webui/src/project/ProjectToolbar.tsx  |  10 +-
 .../main/webui/src/project/build/BuildStatus.tsx   | 355 ---------------------
 .../webui/src/project/build/ProjectBuildTab.tsx    |   9 +-
 52 files changed, 454 insertions(+), 645 deletions(-)

diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ComponentResources.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ComponentResources.java
index a1ba443d..a42d1354 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ComponentResources.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ComponentResources.java
@@ -16,7 +16,7 @@
  */
 package org.apache.camel.karavan.api;
 
-import org.apache.camel.karavan.service.CodeService;
+import org.apache.camel.karavan.code.CodeService;
 
 import jakarta.inject.Inject;
 import jakarta.ws.rs.GET;
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java
index 4b43f169..7cdd7012 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java
@@ -22,7 +22,7 @@ import jakarta.ws.rs.Path;
 import jakarta.ws.rs.Produces;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.service.ConfigService;
 
 @Path("/api/configuration")
 public class ConfigurationResource {
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java
index 05aff589..9a49d65b 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java
@@ -30,10 +30,10 @@ import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
 import org.apache.camel.karavan.service.CamelService;
 import org.apache.camel.karavan.service.ProjectService;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 
-import static org.apache.camel.karavan.shared.EventType.CONTAINER_STATUS;
+import static 
org.apache.camel.karavan.service.ContainerStatusService.CONTAINER_STATUS;
 
 @Path("/api/devmode")
 public class DevModeResource {
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
index 882ef5db..ae4ca231 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
@@ -16,10 +16,14 @@
  */
 package org.apache.camel.karavan.api;
 
+import io.vertx.core.json.JsonObject;
 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.infinispan.model.Project;
+import org.apache.camel.karavan.service.ProjectService;
 import org.apache.camel.karavan.service.RegistryService;
 
 import java.util.List;
@@ -33,6 +37,9 @@ public class ImagesResource {
     @Inject
     RegistryService registryService;
 
+    @Inject
+    ProjectService projectService;
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/{projectId}")
@@ -41,4 +48,17 @@ public class ImagesResource {
         String pattern = registryService.getRegistryWithGroup() + "/" + 
projectId;
         return dockerService.getImages().stream().filter(s -> 
s.startsWith(pattern)).toList();
     }
+
+    @POST
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Path("/{projectId}")
+    public Response build(JsonObject imageName, @PathParam("projectId") String 
projectId) throws Exception {
+        try {
+            projectService.setProjectImage(projectId, 
imageName.getString("imageName"));
+            return Response.ok().entity(imageName).build();
+        } catch (Exception e) {
+            return Response.serverError().entity(e.getMessage()).build();
+        }
+    }
 }
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
index 8f94786a..f6408f84 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
@@ -20,18 +20,19 @@ import io.smallrye.mutiny.Multi;
 import io.vertx.core.json.JsonObject;
 import io.vertx.mutiny.core.eventbus.EventBus;
 import io.vertx.mutiny.core.eventbus.Message;
+import org.apache.camel.karavan.code.DockerComposeConverter;
 import org.apache.camel.karavan.docker.DockerForKaravan;
 import org.apache.camel.karavan.docker.DockerService;
-import org.apache.camel.karavan.docker.model.DockerComposeService;
+import org.apache.camel.karavan.code.model.DockerComposeService;
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.apache.camel.karavan.infinispan.model.DeploymentStatus;
 import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.infinispan.model.ServiceStatus;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.service.CodeService;
+import org.apache.camel.karavan.code.CodeService;
 import org.apache.camel.karavan.service.ProjectService;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
@@ -44,7 +45,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
-import static org.apache.camel.karavan.shared.EventType.CONTAINER_STATUS;
+import static 
org.apache.camel.karavan.service.ContainerStatusService.CONTAINER_STATUS;
 
 @Path("/api/infrastructure")
 public class InfrastructureResource {
@@ -199,7 +200,7 @@ public class InfrastructureResource {
                 if (command.getString("command").equalsIgnoreCase("run")) {
                     if (Objects.equals(type, 
ContainerStatus.ContainerType.devservice.name())) {
                         String code = projectService.getDevServiceCode();
-                        DockerComposeService dockerComposeService = 
codeService.convertToDockerComposeService(code, name);
+                        DockerComposeService dockerComposeService = 
DockerComposeConverter.fromCode(code, name);
                         if (dockerComposeService != null) {
                             
dockerForKaravan.createDevserviceContainer(dockerComposeService);
                             
dockerService.runContainer(dockerComposeService.getContainer_name());
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/KameletResources.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/KameletResources.java
index 95c8594d..8a437935 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/KameletResources.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/KameletResources.java
@@ -19,7 +19,7 @@ package org.apache.camel.karavan.api;
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.infinispan.model.ProjectFile;
-import org.apache.camel.karavan.service.CodeService;
+import org.apache.camel.karavan.code.CodeService;
 import org.yaml.snakeyaml.Yaml;
 
 import jakarta.inject.Inject;
@@ -47,7 +47,7 @@ public class KameletResources {
         StringBuilder kamelets = new 
StringBuilder(codeService.getResourceFile("/kamelets/kamelets.yaml"));
         if (infinispanService.isReady()) {
             List<ProjectFile> custom = 
infinispanService.getProjectFiles(Project.Type.kamelets.name());
-            if (custom.size() > 0) {
+            if (!custom.isEmpty()) {
                 kamelets.append("\n---\n");
                 kamelets.append(custom.stream()
                         .map(ProjectFile::getCode)
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/LogWatchResource.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/LogWatchResource.java
index 6f702dd4..78cc5d6e 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/LogWatchResource.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/LogWatchResource.java
@@ -21,11 +21,9 @@ import io.smallrye.context.api.ManagedExecutorConfig;
 import io.smallrye.context.api.NamedInstance;
 import org.apache.camel.karavan.docker.DockerService;
 import org.apache.camel.karavan.docker.LogCallback;
-import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.microprofile.context.ManagedExecutor;
-import org.eclipse.microprofile.context.ThreadContext;
 import org.jboss.logging.Logger;
 
 import jakarta.inject.Inject;
@@ -41,7 +39,6 @@ import jakarta.ws.rs.sse.SseEventSink;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 
 @Path("/api/logwatch")
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
index bd1ade90..ae8e50f0 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
@@ -18,7 +18,7 @@ package org.apache.camel.karavan.api;
 
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.ProjectFile;
-import org.apache.camel.karavan.service.CodeService;
+import org.apache.camel.karavan.code.CodeService;
 
 import jakarta.inject.Inject;
 import jakarta.ws.rs.Consumes;
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
index b0bdf4cd..302d9746 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
@@ -19,30 +19,24 @@ package org.apache.camel.karavan.api;
 import jakarta.ws.rs.core.Response;
 import org.apache.camel.karavan.docker.DockerService;
 import org.apache.camel.karavan.infinispan.InfinispanService;
-import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.apache.camel.karavan.infinispan.model.GroupedKey;
 import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.infinispan.model.ProjectFile;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.service.CodeService;
-import org.apache.camel.karavan.service.GitService;
+import org.apache.camel.karavan.git.GitService;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.*;
 import jakarta.ws.rs.core.MediaType;
 import org.apache.camel.karavan.service.ProjectService;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.stream.Collectors;
 
-import static org.apache.camel.karavan.shared.Constants.BUILDER_SUFFIX;
-
 @Path("/api/project")
 public class ProjectResource {
 
@@ -67,14 +61,7 @@ public class ProjectResource {
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     public List<Project> getAll(@QueryParam("type") String type) {
-        if (infinispanService.isReady()) {
-            return infinispanService.getProjects().stream()
-                    .filter(p -> type == null || 
Objects.equals(p.getType().name(), type))
-                    .sorted(Comparator.comparing(Project::getProjectId))
-                    .collect(Collectors.toList());
-        } else {
-            return List.of();
-        }
+        return projectService.getAllProjects(type);
     }
 
     @GET
@@ -105,10 +92,10 @@ public class ProjectResource {
     @POST
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/build")
-    public Response build(Project project) throws Exception {
+    @Path("/build/{tag}")
+    public Response build(Project project, @PathParam("tag") String tag) 
throws Exception {
         try {
-            projectService.buildProject(project);
+            projectService.buildProject(project, tag);
             return Response.ok().entity(project).build();
         } catch (Exception e) {
             return Response.serverError().entity(e.getMessage()).build();
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java
similarity index 85%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java
index f1e02e2a..c86eefc6 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.karavan.service;
+package org.apache.camel.karavan.code;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -23,8 +23,6 @@ import io.apicurio.datamodels.openapi.models.OasDocument;
 import io.quarkus.qute.Engine;
 import io.quarkus.qute.Template;
 import io.quarkus.qute.TemplateInstance;
-import io.vertx.core.json.JsonArray;
-import io.vertx.core.json.JsonObject;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import org.apache.camel.CamelContext;
@@ -32,14 +30,14 @@ import org.apache.camel.generator.openapi.RestDslGenerator;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.karavan.api.KameletResources;
 import org.apache.camel.karavan.docker.DockerService;
-import org.apache.camel.karavan.docker.model.DockerComposeService;
+import org.apache.camel.karavan.code.model.DockerComposeService;
 import org.apache.camel.karavan.infinispan.InfinispanService;
-import org.apache.camel.karavan.infinispan.model.GitRepo;
-import org.apache.camel.karavan.infinispan.model.GitRepoFile;
+import org.apache.camel.karavan.git.model.GitRepo;
+import org.apache.camel.karavan.git.model.GitRepoFile;
 import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.infinispan.model.ProjectFile;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.service.ConfigService;
 import org.jboss.logging.Logger;
 import org.yaml.snakeyaml.LoaderOptions;
 import org.yaml.snakeyaml.Yaml;
@@ -287,7 +285,7 @@ public class CodeService {
     }
 
     public Integer getProjectPort(ProjectFile composeFile) {
-        DockerComposeService dcs = 
convertToDockerComposeService(composeFile.getCode(), 
composeFile.getProjectId());
+        DockerComposeService dcs = 
DockerComposeConverter.fromCode(composeFile.getCode(), 
composeFile.getProjectId());
         Optional<Integer> port = dcs.getPortsMap().entrySet().stream()
                 .filter(e -> Objects.equals(e.getValue(), 
INTERNAL_PORT)).map(Map.Entry::getKey).findFirst();
         return port.orElse(null);
@@ -296,41 +294,8 @@ public class CodeService {
 
     public DockerComposeService getInternalDockerComposeService (String name) {
         String composeText = getResourceFile("/services/internal.yaml");
-        return convertToDockerComposeService(composeText, name);
+        return DockerComposeConverter.fromCode(composeText, name);
     }
 
-    public DockerComposeService convertToDockerComposeService(String code, 
String name) {
-        Yaml yaml = new Yaml();
-        Map<String, Object> obj = yaml.load(code);
-        JsonObject json = JsonObject.mapFrom(obj);
-        JsonObject services = json.getJsonObject("services");
-        if (services.containsKey(name)) {
-            JsonObject service = services.getJsonObject(name);
-            return convertToDockerComposeService(name, service);
-        } else {
-            Optional<JsonObject> j = services.fieldNames().stream()
-                    .map(services::getJsonObject)
-                    .filter(s -> 
s.getString("container_name").equalsIgnoreCase(name)).findFirst();
-            if (j.isPresent()) {
-                return convertToDockerComposeService(name, j.get());
-            }
-        }
-        return null;
-    }
 
-    public DockerComposeService convertToDockerComposeService(String name, 
JsonObject service) {
-        if (service.containsKey(ENVIRONMENT) && service.getValue(ENVIRONMENT) 
instanceof JsonArray) {
-            JsonObject env = new JsonObject();
-            service.getJsonArray(ENVIRONMENT).forEach(o -> {
-                String[] kv = o.toString().split("=");
-                env.put(kv[0], kv[1]);
-            });
-            service.put(ENVIRONMENT, env);
-        }
-        DockerComposeService ds = service.mapTo(DockerComposeService.class);
-        if (ds.getContainer_name() == null) {
-            ds.setContainer_name(name);
-        }
-        return ds;
-    }
 }
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/DockerComposeConverter.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/DockerComposeConverter.java
new file mode 100644
index 00000000..8424ee53
--- /dev/null
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/DockerComposeConverter.java
@@ -0,0 +1,111 @@
+package org.apache.camel.karavan.code;
+
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import org.apache.camel.karavan.code.model.DockerCompose;
+import org.apache.camel.karavan.code.model.DockerComposeService;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.introspector.Property;
+import org.yaml.snakeyaml.nodes.*;
+import org.yaml.snakeyaml.representer.Representer;
+
+import java.util.Map;
+
+public class DockerComposeConverter {
+
+    private static final String ENVIRONMENT = "environment";
+
+    public static DockerCompose fromCode(String code) {
+        Yaml yaml = new Yaml();
+        Map<String, Object> obj = yaml.load(code);
+        JsonObject json = JsonObject.mapFrom(obj);
+        JsonObject services = json.getJsonObject("services");
+        JsonObject composeServices = new JsonObject();
+
+        services.getMap().forEach((name, value) -> {
+            JsonObject serviceJson = services.getJsonObject(name);
+            DockerComposeService service = convertToDockerComposeService(name, 
serviceJson);
+            composeServices.put(name, service);
+        });
+        json.put("services", composeServices);
+        return json.mapTo(DockerCompose.class);
+    }
+
+    public static DockerComposeService fromCode(String code, String 
serviceName) {
+        DockerCompose compose = fromCode(code);
+        return compose.getServices().get(serviceName);
+    }
+
+    public static String toCode(DockerCompose compose) {
+        Yaml yaml = new Yaml(new ComposeRepresenter());
+        return yaml.dumpAs(compose, Tag.MAP, DumperOptions.FlowStyle.BLOCK);
+    }
+
+    public static String toCode(DockerComposeService service) {
+        DockerCompose dc = DockerCompose.create(service);
+        return toCode(dc);
+    }
+
+    private static DockerComposeService convertToDockerComposeService(String 
name, JsonObject service) {
+        if (service.containsKey(ENVIRONMENT) && service.getValue(ENVIRONMENT) 
instanceof JsonArray) {
+            JsonObject env = new JsonObject();
+            service.getJsonArray(ENVIRONMENT).forEach(o -> {
+                String[] kv = o.toString().split("=");
+                env.put(kv[0], kv[1]);
+            });
+            service.put(ENVIRONMENT, env);
+        }
+        DockerComposeService ds = service.mapTo(DockerComposeService.class);
+        if (ds.getContainer_name() == null) {
+            ds.setContainer_name(name);
+        }
+        return ds;
+    }
+
+    private static class ComposeRepresenter extends Representer {
+
+        public ComposeRepresenter() {
+            super(create());
+        }
+
+        private static DumperOptions create() {
+            DumperOptions options = new DumperOptions();
+            options.setExplicitStart(true);
+            options.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN);
+            options.setLineBreak(DumperOptions.LineBreak.UNIX);
+            return options;
+        }
+
+        @Override
+        protected NodeTuple representJavaBeanProperty(Object javaBean, 
Property property, Object propertyValue, Tag customTag) {
+            if (propertyValue == null) {
+                return null;
+            } else {
+                NodeTuple tuple = super.representJavaBeanProperty(javaBean, 
property, propertyValue, customTag);
+                Node valueNode = tuple.getValueNode();
+                if (Tag.NULL.equals(valueNode.getTag())) {
+                    return null;// skip 'null' values
+                }
+                if (propertyValue instanceof String && (((String) 
propertyValue).isEmpty() || ((String) propertyValue).isBlank()) ) {
+                    return null;// skip '' values
+                }
+                if (valueNode instanceof CollectionNode) {
+                    if (Tag.SEQ.equals(valueNode.getTag())) {
+                        SequenceNode seq = (SequenceNode) valueNode;
+                        if (seq.getValue().isEmpty()) {
+                            return null;// skip empty lists
+                        }
+                    }
+                    if (Tag.MAP.equals(valueNode.getTag())) {
+                        MappingNode seq = (MappingNode) valueNode;
+                        if (seq.getValue().isEmpty()) {
+                            return null;// skip empty maps
+                        }
+                    }
+                }
+                return tuple;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerCompose.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerCompose.java
new file mode 100644
index 00000000..36e505a8
--- /dev/null
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerCompose.java
@@ -0,0 +1,29 @@
+package org.apache.camel.karavan.code.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DockerCompose {
+    private Map<String, DockerComposeService> services;
+
+    public DockerCompose() {
+    }
+
+    public DockerCompose(Map<String, DockerComposeService> services) {
+        this.services = services;
+    }
+
+    public static DockerCompose create(DockerComposeService service) {
+        Map<String, DockerComposeService> map = new HashMap<>();
+        map.put(service.getContainer_name(), service);
+        return new DockerCompose(map);
+    }
+
+    public Map<String, DockerComposeService> getServices() {
+        return services;
+    }
+
+    public void setServices(Map<String, DockerComposeService> services) {
+        this.services = services;
+    }
+}
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/HealthCheckConfig.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerComposeHealthCheck.java
similarity index 91%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/HealthCheckConfig.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerComposeHealthCheck.java
index 4a631414..7cff2a90 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/HealthCheckConfig.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerComposeHealthCheck.java
@@ -1,8 +1,8 @@
-package org.apache.camel.karavan.docker.model;
+package org.apache.camel.karavan.code.model;
 
 import java.util.List;
 
-public class HealthCheckConfig {
+public class DockerComposeHealthCheck {
 
     private String interval;
     private Integer retries;
@@ -10,7 +10,7 @@ public class HealthCheckConfig {
     private String start_period;
     private List<String> test;
 
-    public HealthCheckConfig() {
+    public DockerComposeHealthCheck() {
     }
 
     public String getInterval() {
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DockerComposeService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerComposeService.java
similarity index 93%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DockerComposeService.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerComposeService.java
index 629153e4..b3fa284a 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DockerComposeService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/model/DockerComposeService.java
@@ -1,4 +1,4 @@
-package org.apache.camel.karavan.docker.model;
+package org.apache.camel.karavan.code.model;
 
 import java.util.*;
 import java.util.stream.Collectors;
@@ -12,7 +12,7 @@ public class DockerComposeService {
     private List<String> expose;
     private List<String> depends_on;
     private Map<String,String> environment;
-    private HealthCheckConfig healthcheck;
+    private DockerComposeHealthCheck healthcheck;
 
     public DockerComposeService() {
     }
@@ -96,11 +96,11 @@ public class DockerComposeService {
         this.environment = environment;
     }
 
-    public HealthCheckConfig getHealthcheck() {
+    public DockerComposeHealthCheck getHealthcheck() {
         return healthcheck;
     }
 
-    public void setHealthcheck(HealthCheckConfig healthcheck) {
+    public void setHealthcheck(DockerComposeHealthCheck healthcheck) {
         this.healthcheck = healthcheck;
     }
 
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForGitea.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForGitea.java
index c6386808..d1dd236f 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForGitea.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForGitea.java
@@ -21,10 +21,10 @@ import com.github.dockerjava.api.model.Container;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
-import org.apache.camel.karavan.infinispan.model.GitConfig;
-import org.apache.camel.karavan.service.CodeService;
-import org.apache.camel.karavan.service.GitService;
-import org.apache.camel.karavan.service.GiteaService;
+import org.apache.camel.karavan.git.model.GitConfig;
+import org.apache.camel.karavan.code.CodeService;
+import org.apache.camel.karavan.git.GitService;
+import org.apache.camel.karavan.git.GiteaService;
 import org.jboss.logging.Logger;
 
 @ApplicationScoped
@@ -77,16 +77,4 @@ public class DockerForGitea {
             LOGGER.error(e.getMessage());
         }
     }
-
-    protected void checkGiteaInstance() {
-        try {
-            Container gitea = 
dockerService.getContainerByName(GITEA_CONTAINER_NAME);
-            ExecCreateCmdResponse user = 
dockerService.execCreate(gitea.getId(),
-                    "curl", "-Is", "localhost:3000/user/login");
-
-            dockerService.execStart(user.getId(), new GiteaCheckCallback(o -> 
createGiteaUser(), o -> checkGiteaInstance()));
-        } catch (Exception e) {
-            LOGGER.error(e.getMessage());
-        }
-    }
 }
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForInfinispan.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForInfinispan.java
index de1a33d7..7af32ff3 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForInfinispan.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForInfinispan.java
@@ -19,7 +19,7 @@ package org.apache.camel.karavan.docker;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
-import org.apache.camel.karavan.service.CodeService;
+import org.apache.camel.karavan.code.CodeService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
@@ -51,7 +51,7 @@ public class DockerForInfinispan {
             dockerService.runContainer(INFINISPAN_CONTAINER_NAME);
             LOGGER.info("Infinispan is started");
         } catch (Exception e) {
-            LOGGER.error(e.getCause().getMessage());
+            LOGGER.error(e.getMessage());
         }
     }
 }
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
index 13b4bf76..cfa74f84 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
@@ -20,10 +20,9 @@ import com.github.dockerjava.api.model.Container;
 import com.github.dockerjava.api.model.HealthCheck;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
-import org.apache.camel.karavan.docker.model.DockerComposeService;
+import org.apache.camel.karavan.code.model.DockerComposeService;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.apache.camel.karavan.service.RegistryService;
-import org.apache.camel.karavan.shared.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
@@ -93,7 +92,6 @@ public class DockerForKaravan {
 
     public void syncImage(String projectId, String tag) throws 
InterruptedException {
         String image = registryService.getRegistryWithGroup() + "/" + 
projectId + ":" + tag;
-        System.out.println(image);
         dockerService.pullImage(image);
     }
 }
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForRegistry.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForRegistry.java
index b2d1fa4e..f3fee4c7 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForRegistry.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForRegistry.java
@@ -19,7 +19,7 @@ package org.apache.camel.karavan.docker;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
-import org.apache.camel.karavan.service.CodeService;
+import org.apache.camel.karavan.code.CodeService;
 import org.jboss.logging.Logger;
 
 @ApplicationScoped
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
index 6a06a0c9..0f680ad8 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
@@ -30,7 +30,7 @@ import io.vertx.core.Vertx;
 import io.vertx.core.buffer.Buffer;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
-import org.apache.camel.karavan.docker.model.DockerComposeService;
+import org.apache.camel.karavan.code.model.DockerComposeService;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
 import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
@@ -41,7 +41,6 @@ import org.jboss.logging.Logger;
 import java.io.*;
 import java.nio.file.Files;
 import java.nio.file.Paths;
-import java.time.Instant;
 import java.util.*;
 import java.util.stream.Collectors;
 
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java
index ebbd8403..8224d264 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java
@@ -19,7 +19,7 @@ package org.apache.camel.karavan.docker;
 import com.github.dockerjava.api.model.*;
 import io.smallrye.mutiny.tuples.Tuple2;
 import org.apache.camel.karavan.api.KameletResources;
-import org.apache.camel.karavan.docker.model.HealthCheckConfig;
+import org.apache.camel.karavan.code.model.DockerComposeHealthCheck;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 
 import java.io.BufferedReader;
@@ -62,7 +62,7 @@ public class DockerServiceUtils {
         }
     }
 
-    protected HealthCheck getHealthCheck(HealthCheckConfig config) {
+    protected HealthCheck getHealthCheck(DockerComposeHealthCheck config) {
         if (config != null) {
             HealthCheck healthCheck = new 
HealthCheck().withTest(config.getTest());
             if (config.getInterval() != null) {
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/GiteaCheckCallback.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/GiteaCheckCallback.java
deleted file mode 100644
index 0db0978e..00000000
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/GiteaCheckCallback.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.apache.camel.karavan.docker;
-
-import com.github.dockerjava.api.async.ResultCallback;
-import com.github.dockerjava.api.model.Frame;
-
-import java.io.Closeable;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-
-public class GiteaCheckCallback extends ResultCallback.Adapter<Frame> {
-
-    private static final AtomicBoolean giteaStarted = new AtomicBoolean(false);
-    private static final AtomicInteger giteaCheck = new AtomicInteger(0);
-
-
-    private final Consumer createGiteaUser;
-    private final Consumer checkGiteaInstance;
-
-    public GiteaCheckCallback(Consumer createGiteaUser, Consumer 
checkGiteaInstance) {
-        this.createGiteaUser = createGiteaUser;
-        this.checkGiteaInstance = checkGiteaInstance;
-    }
-
-    @Override
-    public void onStart(Closeable stream) {
-    }
-
-    @Override
-    public void onNext(Frame object) {
-        if (!giteaStarted.get()) {
-            String line = new String(object.getPayload());
-            if (line.startsWith("HTTP/1.1 200")) {
-                giteaStarted.set(true);
-                createGiteaUser.accept(null);
-            }
-        }
-    }
-
-    @Override
-    public void onError(Throwable throwable) {
-
-    }
-
-    @Override
-    public void onComplete() {
-        giteaCheck.incrementAndGet();
-        if (!giteaStarted.get() && giteaCheck.get() < 1000) {
-            checkGiteaInstance.accept(null);
-        }
-    }
-
-}
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/GitService.java
similarity index 98%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/GitService.java
index 7fb99882..534781e6 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/GitService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/GitService.java
@@ -14,14 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.karavan.service;
+package org.apache.camel.karavan.git;
 
 import io.fabric8.kubernetes.api.model.Secret;
 import io.smallrye.mutiny.tuples.Tuple2;
 import io.vertx.core.Vertx;
+import org.apache.camel.karavan.git.model.GitConfig;
+import org.apache.camel.karavan.git.model.GitRepo;
+import org.apache.camel.karavan.git.model.GitRepoFile;
 import org.apache.camel.karavan.infinispan.model.*;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.jgit.api.CheckoutCommand;
 import org.eclipse.jgit.api.CloneCommand;
 import org.eclipse.jgit.api.FetchCommand;
@@ -56,8 +59,6 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.*;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
 
 @ApplicationScoped
 public class GitService {
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/GiteaService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/GiteaService.java
similarity index 97%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/GiteaService.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/GiteaService.java
index 469986e3..7ad208e7 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/GiteaService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/GiteaService.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.karavan.service;
+package org.apache.camel.karavan.git;
 
 import io.vertx.core.json.JsonObject;
 import io.vertx.mutiny.core.Vertx;
@@ -23,8 +23,8 @@ import io.vertx.mutiny.ext.web.client.HttpResponse;
 import io.vertx.mutiny.ext.web.client.WebClient;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
-import org.apache.camel.karavan.infinispan.model.GitConfig;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.git.model.GitConfig;
+import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.microprofile.faulttolerance.Retry;
 import org.jboss.logging.Logger;
 
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitConfig.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitConfig.java
similarity index 94%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitConfig.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitConfig.java
index 580ee799..854f3f28 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitConfig.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitConfig.java
@@ -1,4 +1,4 @@
-package org.apache.camel.karavan.infinispan.model;
+package org.apache.camel.karavan.git.model;
 
 public class GitConfig {
     private String uri;
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitRepo.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitRepo.java
similarity index 95%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitRepo.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitRepo.java
index 4736814f..773baab7 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitRepo.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitRepo.java
@@ -1,4 +1,4 @@
-package org.apache.camel.karavan.infinispan.model;
+package org.apache.camel.karavan.git.model;
 
 import java.util.List;
 
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitRepoFile.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitRepoFile.java
similarity index 93%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitRepoFile.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitRepoFile.java
index a1186efe..ead83e90 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/GitRepoFile.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/git/model/GitRepoFile.java
@@ -1,4 +1,4 @@
-package org.apache.camel.karavan.infinispan.model;
+package org.apache.camel.karavan.git.model;
 
 public class GitRepoFile {
     private String name;
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/Project.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/Project.java
index 9c31e296..22a7bac8 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/Project.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/Project.java
@@ -32,8 +32,21 @@ public class Project {
     Long lastCommitTimestamp;
     @ProtoField(number = 7)
     Type type;
+    @ProtoField(number = 8)
+    String image;
 
     @ProtoFactory
+    public Project(String projectId, String name, String description, String 
runtime, String lastCommit, Long lastCommitTimestamp, Type type, String image) {
+        this.projectId = projectId;
+        this.name = name;
+        this.description = description;
+        this.runtime = runtime;
+        this.lastCommit = lastCommit;
+        this.lastCommitTimestamp = lastCommitTimestamp;
+        this.type = type;
+        this.image = image;
+    }
+
     public Project(String projectId, String name, String description, String 
runtime, String lastCommit, Long lastCommitTimestamp, Type type) {
         this.projectId = projectId;
         this.name = name;
@@ -122,4 +135,12 @@ public class Project {
     public void setType(Type type) {
         this.type = type;
     }
+
+    public String getImage() {
+        return image;
+    }
+
+    public void setImage(String image) {
+        this.image = image;
+    }
 }
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
index abdf4f48..4c84c03d 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
@@ -32,8 +32,8 @@ import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.infinispan.model.ProjectFile;
-import org.apache.camel.karavan.service.CodeService;
-import org.apache.camel.karavan.shared.ConfigService;
+import org.apache.camel.karavan.code.CodeService;
+import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.health.HealthCheck;
 import org.eclipse.microprofile.health.HealthCheckResponse;
@@ -47,7 +47,7 @@ import jakarta.inject.Inject;
 import java.util.*;
 import java.util.stream.Collectors;
 
-import static 
org.apache.camel.karavan.service.CodeService.APPLICATION_PROPERTIES_FILENAME;
+import static 
org.apache.camel.karavan.code.CodeService.APPLICATION_PROPERTIES_FILENAME;
 import static org.apache.camel.karavan.shared.Constants.*;
 
 @Default
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/PodEventHandler.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/PodEventHandler.java
index bdd9a7cd..a783df7c 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/PodEventHandler.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/PodEventHandler.java
@@ -12,13 +12,11 @@ import 
org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.jboss.logging.Logger;
 
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 
-import static 
org.apache.camel.karavan.service.CodeService.DEFAULT_CONTAINER_RESOURCES;
+import static 
org.apache.camel.karavan.code.CodeService.DEFAULT_CONTAINER_RESOURCES;
 import static org.apache.camel.karavan.shared.Constants.LABEL_PROJECT_ID;
-import static org.apache.camel.karavan.shared.Constants.RELOAD_TRY_COUNT;
-import static org.apache.camel.karavan.shared.EventType.CONTAINER_STATUS;
+import static 
org.apache.camel.karavan.service.ContainerStatusService.CONTAINER_STATUS;
 
 public class PodEventHandler implements ResourceEventHandler<Pod> {
 
@@ -55,10 +53,10 @@ public class PodEventHandler implements 
ResourceEventHandler<Pod> {
                 if (ps != null) {
                     eventBus.send(CONTAINER_STATUS, JsonObject.mapFrom(ps));
                     if (Objects.equals(ps.getState(), 
ContainerStatus.State.running.name())) {
-                        Map<String, Object> message = Map.of(
-                                LABEL_PROJECT_ID, ps.getProjectId(),
-                                RELOAD_TRY_COUNT, 1
-                        );
+//                        Map<String, Object> message = Map.of(
+//                                LABEL_PROJECT_ID, ps.getProjectId(),
+//                                RELOAD_TRY_COUNT, 1
+//                        );
 //                        eventBus.publish(DEVMODE_CONTAINER_READY, 
JsonObject.mapFrom(message));
                     }
                 }
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelService.java
index ef96461b..7f054871 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelService.java
@@ -27,7 +27,6 @@ import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.CamelStatus;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.shared.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
 import org.jboss.logging.Logger;
@@ -35,20 +34,14 @@ import org.jboss.logging.Logger;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import java.util.Arrays;
-import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ExecutionException;
 
-import static org.apache.camel.karavan.shared.Constants.LABEL_PROJECT_ID;
-import static org.apache.camel.karavan.shared.Constants.RELOAD_TRY_COUNT;
-import static org.apache.camel.karavan.shared.EventType.*;
-
 @ApplicationScoped
 public class CamelService {
 
     private static final Logger LOGGER = 
Logger.getLogger(CamelService.class.getName());
     public static final String CMD_COLLECT_CAMEL_STATUS = 
"collect-camel-status";
-    public static final String CMD_DELETE_CAMEL_STATUS = "delete-camel-status";
 
     @Inject
     InfinispanService infinispanService;
@@ -96,7 +89,7 @@ public class CamelService {
             reloadRequest(projectId);
             ContainerStatus containerStatus = 
infinispanService.getDevModeContainerStatus(projectId, environment);
             containerStatus.setCodeLoaded(true);
-            eventBus.send(CONTAINER_STATUS, 
JsonObject.mapFrom(containerStatus));
+            eventBus.send(ContainerStatusService.CONTAINER_STATUS, 
JsonObject.mapFrom(containerStatus));
         } catch (Exception ex) {
             LOGGER.error(ex.getMessage());
         }
@@ -143,7 +136,6 @@ public class CamelService {
                             cs.getType() == 
ContainerStatus.ContainerType.project
                             || cs.getType() == 
ContainerStatus.ContainerType.devmode
                     ).forEach(pod -> {
-                System.out.println(pod.getProjectId());
                 CamelStatusRequest csr = new 
CamelStatusRequest(pod.getProjectId(), pod.getContainerName());
                 eventBus.publish(CMD_COLLECT_CAMEL_STATUS, 
JsonObject.mapFrom(csr));
             });
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/ConfigService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ConfigService.java
similarity index 96%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/ConfigService.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ConfigService.java
index 392da8a6..a1e24955 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/ConfigService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ConfigService.java
@@ -14,13 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.karavan.shared;
+package org.apache.camel.karavan.service;
 
 import io.quarkus.runtime.StartupEvent;
 import io.quarkus.runtime.configuration.ConfigUtils;
 import io.quarkus.runtime.configuration.ProfileManager;
 import io.vertx.core.Vertx;
 import jakarta.inject.Inject;
+import org.apache.camel.karavan.shared.Configuration;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 
 import jakarta.enterprise.context.ApplicationScoped;
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/EventService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ContainerStatusService.java
similarity index 91%
rename from 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/EventService.java
rename to 
karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ContainerStatusService.java
index eb7a1302..5fc126eb 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/EventService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ContainerStatusService.java
@@ -11,12 +11,11 @@ import org.jboss.logging.Logger;
 import java.time.Instant;
 import java.util.Objects;
 
-import static org.apache.camel.karavan.shared.EventType.CONTAINER_STATUS;
-
 @ApplicationScoped
-public class EventService {
+public class ContainerStatusService {
 
-    private static final Logger LOGGER = 
Logger.getLogger(EventService.class.getName());
+    public static final String CONTAINER_STATUS = "CONTAINER_STATUS";
+    private static final Logger LOGGER = 
Logger.getLogger(ContainerStatusService.class.getName());
 
     @Inject
     InfinispanService infinispanService;
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/KaravanService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/KaravanService.java
index 1d2270d4..77f536d0 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/KaravanService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/KaravanService.java
@@ -27,9 +27,9 @@ import org.apache.camel.karavan.docker.DockerForGitea;
 import org.apache.camel.karavan.docker.DockerForInfinispan;
 import org.apache.camel.karavan.docker.DockerForRegistry;
 import org.apache.camel.karavan.docker.DockerService;
+import org.apache.camel.karavan.git.GiteaService;
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.shared.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.health.HealthCheck;
 import org.eclipse.microprofile.health.HealthCheckResponse;
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
index 0005395a..8d0ced7d 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
@@ -18,13 +18,16 @@ package org.apache.camel.karavan.service;
 
 import io.vertx.core.json.JsonObject;
 import io.vertx.mutiny.core.eventbus.EventBus;
+import org.apache.camel.karavan.code.CodeService;
+import org.apache.camel.karavan.code.DockerComposeConverter;
 import org.apache.camel.karavan.docker.DockerForKaravan;
-import org.apache.camel.karavan.docker.model.DockerComposeService;
+import org.apache.camel.karavan.code.model.DockerComposeService;
+import org.apache.camel.karavan.git.GitService;
+import org.apache.camel.karavan.git.model.GitConfig;
+import org.apache.camel.karavan.git.model.GitRepo;
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.*;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.shared.ConfigService;
-import org.apache.camel.karavan.shared.EventType;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.faulttolerance.Retry;
@@ -42,8 +45,8 @@ import java.util.*;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
-import static 
org.apache.camel.karavan.service.CodeService.DEV_SERVICES_FILENAME;
-import static 
org.apache.camel.karavan.service.CodeService.PROJECT_COMPOSE_FILENAME;
+import static org.apache.camel.karavan.code.CodeService.DEV_SERVICES_FILENAME;
+import static 
org.apache.camel.karavan.code.CodeService.PROJECT_COMPOSE_FILENAME;
 
 @Default
 @Readiness
@@ -99,7 +102,7 @@ public class ProjectService implements HealthCheck {
 
         if (!Objects.equals(status.getState(), 
ContainerStatus.State.running.name())) {
             status.setInTransit(true);
-            eventBus.send(EventType.CONTAINER_STATUS, 
JsonObject.mapFrom(status));
+            eventBus.send(ContainerStatusService.CONTAINER_STATUS, 
JsonObject.mapFrom(status));
 
             if (ConfigService.inKubernetes()) {
                 kubernetesService.runDevModeContainer(project, jBangOptions);
@@ -108,7 +111,7 @@ public class ProjectService implements HealthCheck {
                         .filter(f -> !Objects.equals(f.getName(), 
PROJECT_COMPOSE_FILENAME))
                         .collect(Collectors.toMap(ProjectFile::getName, 
ProjectFile::getCode));
                 ProjectFile compose = 
infinispanService.getProjectFile(project.getProjectId(), 
PROJECT_COMPOSE_FILENAME);
-                DockerComposeService dcs = 
codeService.convertToDockerComposeService(compose.getCode(), 
project.getProjectId());
+                DockerComposeService dcs = 
DockerComposeConverter.fromCode(compose.getCode(), project.getProjectId());
                 dockerForKaravan.runProjectInDevMode(project.getProjectId(), 
jBangOptions, dcs.getPortsMap(), files);
             }
             return containerName;
@@ -117,7 +120,7 @@ public class ProjectService implements HealthCheck {
         }
     }
 
-    public String buildProject(Project project) throws Exception {
+    public String buildProject(Project project, String tag) throws Exception {
         if (ConfigService.inKubernetes()) {
             return kubernetesService.createPipelineRun(project);
         } else {
@@ -128,7 +131,10 @@ public class ProjectService implements HealthCheck {
             String templateName = project.getRuntime() + 
"-builder-script-docker.sh";
             String script = codeService.getTemplateText(templateName);
 
-            String tag = Instant.now().toString().substring(0, 
19).replace(":", "-");
+            tag = tag != null && !tag.isEmpty() && !tag.isBlank()
+                    ? tag
+                    : Instant.now().toString().substring(0, 19).replace(":", 
"-");
+
             List<String> env = getEnvForBuild(project, tag);
             Map<String, String> volumes = mavenCache
                     .map(s -> Map.of(s, "/root/.m2"))
@@ -154,6 +160,35 @@ public class ProjectService implements HealthCheck {
         return env;
     }
 
+    public List<Project> getAllProjects(String type) {
+        if (infinispanService.isReady()) {
+            List<ProjectFile> files = 
infinispanService.getProjectFilesByName(PROJECT_COMPOSE_FILENAME);
+            return infinispanService.getProjects().stream()
+                    .filter(p -> type == null || 
Objects.equals(p.getType().name(), type))
+                    .sorted(Comparator.comparing(Project::getProjectId))
+                    .map(project -> {
+                        if (Objects.equals(project.getType(), 
Project.Type.normal)) {
+                            project.setImage(getImage(files, 
project.getProjectId()));
+                        }
+                        return project;
+                    })
+                    .collect(Collectors.toList());
+        } else {
+            return List.of();
+        }
+    }
+
+    private String getImage(List<ProjectFile> files, String projectId) {
+        Optional<ProjectFile> file = files.stream().filter(f -> 
Objects.equals(f.getProjectId(), projectId)).findFirst();
+        if (file.isPresent()) {
+            DockerComposeService service = 
DockerComposeConverter.fromCode(file.get().getCode(), projectId);
+            String image =  service.getImage();
+            return Objects.equals(image, projectId) ? null : image;
+        } else {
+            return null;
+        }
+    }
+
     public Project save(Project project) throws Exception {
         boolean isNew = infinispanService.getProject(project.getProjectId()) 
== null;
         infinispanService.saveProject(project);
@@ -343,4 +378,16 @@ public class ProjectService implements HealthCheck {
         Optional<ProjectFile> file = files.stream().filter(f -> 
f.getName().equals(DEV_SERVICES_FILENAME)).findFirst();
         return file.orElse(new ProjectFile()).getCode();
     }
+
+    public void setProjectImage(String projectId, String imageName) {
+        ProjectFile file = infinispanService.getProjectFile(projectId, 
PROJECT_COMPOSE_FILENAME);
+        if (file != null) {
+            DockerComposeService service = 
DockerComposeConverter.fromCode(file.getCode(), projectId);
+            service.setImage(imageName);
+            String code = DockerComposeConverter.toCode(service);
+            System.out.println(code);
+            file.setCode(code);
+            infinispanService.saveProjectFile(file);
+        }
+    }
 }
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/RegistryService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/RegistryService.java
index 33968ed2..287ce35a 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/RegistryService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/RegistryService.java
@@ -17,9 +17,6 @@
 package org.apache.camel.karavan.service;
 
 import jakarta.enterprise.context.ApplicationScoped;
-import org.apache.camel.karavan.infinispan.model.GitConfig;
-import org.apache.camel.karavan.infinispan.model.Project;
-import org.apache.camel.karavan.shared.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java
index c851a93c..1a90dc6c 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java
@@ -23,15 +23,12 @@ import org.apache.camel.karavan.docker.DockerForInfinispan;
 import org.apache.camel.karavan.docker.DockerService;
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
-import org.apache.camel.karavan.shared.ConfigService;
-import org.apache.camel.karavan.shared.EventType;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import java.util.List;
-import java.util.stream.Collectors;
 
 @ApplicationScoped
 public class ScheduledService {
@@ -64,7 +61,7 @@ public class ScheduledService {
         if (infinispanService.isReady()) {
             List<ContainerStatus> statusesInDocker = 
dockerService.collectContainersStatistics();
             statusesInDocker.forEach(containerStatus -> {
-                eventBus.send(EventType.CONTAINER_STATUS, 
JsonObject.mapFrom(containerStatus));
+                eventBus.send(ContainerStatusService.CONTAINER_STATUS, 
JsonObject.mapFrom(containerStatus));
             });
         }
     }
@@ -74,7 +71,7 @@ public class ScheduledService {
         if (infinispanService.isReady()) {
             List<ContainerStatus> statusesInDocker = 
dockerService.collectContainersStatuses();
             statusesInDocker.forEach(containerStatus -> {
-                eventBus.send(EventType.CONTAINER_STATUS, 
JsonObject.mapFrom(containerStatus));
+                eventBus.send(ContainerStatusService.CONTAINER_STATUS, 
JsonObject.mapFrom(containerStatus));
             });
             cleanContainersStatuses(statusesInDocker);
         }
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/Constants.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/Constants.java
index 1e38924c..1b339060 100644
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/Constants.java
+++ 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/Constants.java
@@ -24,7 +24,5 @@ public class Constants {
     public static final String LABEL_PROJECT_ID = 
"org.apache.camel.karavan/projectId";
     public static final String LABEL_TAG = "org.apache.camel.karavan/tag";
 
-    public static final String RELOAD_TRY_COUNT = "reloadTryCount";
-
     public static final String BUILDER_SUFFIX = "-builder";
 }
diff --git 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/EventType.java
 
b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/EventType.java
deleted file mode 100644
index add236cd..00000000
--- 
a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/EventType.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.camel.karavan.shared;
-
-public class EventType {
-
-    public static final String CONTAINER_STATUS = "CONTAINER_STATUS";
-}
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx 
b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
index e9704a20..40601212 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
@@ -257,8 +257,8 @@ export class KaravanApi {
         });
     }
 
-    static async buildProject(project: Project, environment: string, after: 
(res: AxiosResponse<any>) => void) {
-        instance.post('/api/project/build', project)
+    static async buildProject(project: Project, tag: string, after: (res: 
AxiosResponse<any>) => void) {
+        instance.post('/api/project/build/' + tag, project)
             .then(res => {
                 after(res);
             }).catch(err => {
@@ -371,6 +371,15 @@ export class KaravanApi {
         });
     }
 
+    static async setProjectImage(projectId: string, imageName: string, after: 
(res: AxiosResponse<any>) => void) {
+        instance.post('/api/image/' + projectId, {imageName: imageName})
+            .then(res => {
+                after(res);
+            }).catch(err => {
+            after(err);
+        });
+    }
+
     static async pipelineRun(project: Project, environment: string, after: 
(res: AxiosResponse<any>) => void) {
         instance.post('/api/infrastructure/pipeline/' + environment, project)
             .then(res => {
@@ -515,6 +524,17 @@ export class KaravanApi {
         });
     }
 
+    static async getImages(projectId: string, after: (string: []) => void) {
+        instance.get('/api/image/' + projectId)
+            .then(res => {
+                if (res.status === 200) {
+                    after(res.data);
+                }
+            }).catch(err => {
+            console.log(err);
+        });
+    }
+
     static async getSecrets(after: (any: []) => void) {
         instance.get('/api/infrastructure/secrets')
             .then(res => {
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts 
b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
index 29f62995..05d76e69 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
@@ -25,9 +25,10 @@ export class Project {
     runtime: string = '';
     lastCommit: string = '';
     lastCommitTimestamp: number = 0;
-    type: string = ProjectType.normal
+    type: string = ProjectType.normal;
+    image: string = '';
 
-    public constructor(projectId: string, name: string, description: string, 
runtime: string, lastCommit: string, type: string);
+    public constructor(projectId: string, name: string, description: string, 
runtime: string, lastCommit: string, type: string, image: string);
     public constructor(init?: Partial<Project>);
     public constructor(...args: any[]) {
         if (args.length === 1) {
@@ -41,6 +42,7 @@ export class Project {
             this.lastCommit = args[4];
             this.lastCommitTimestamp = args[5];
             this.type = args[6];
+            this.image = args[7];
             return;
         }
     }
@@ -141,6 +143,7 @@ export const ProjectFileTypes: ProjectFileType[] = [
     new ProjectFileType("JSON", "JSON", "json"),
     new ProjectFileType("YAML", "YAML", "yaml"),
     new ProjectFileType("LOG", "Log", "log"),
+    new ProjectFileType("SH", "Script", "sh"),
 ];
 
 
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts 
b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
index d42886ee..42939630 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
@@ -81,9 +81,13 @@ export const useProjectsStore = 
createWithEqualityFn<ProjectsState>((set) => ({
 interface ProjectState {
     isPushing: boolean,
     isRunning: boolean,
+    images: string [],
+    setImages: (images: string []) => void;
     project: Project;
     setProject: (project: Project, operation:  "create" | "select" | "delete"| 
"none" | "copy") => void;
     operation: "create" | "select" | "delete" | "none" | "copy";
+    tabIndex: string | number;
+    setTabIndex: (tabIndex: string | number) => void;
     setOperation: (o: "create" | "select" | "delete"| "none" | "copy") => void;
     memory: any,
     setMemory: (memory: any) => void;
@@ -99,7 +103,9 @@ interface ProjectState {
 
 export const useProjectStore = createWithEqualityFn<ProjectState>((set) => ({
     project: new Project(),
-    operation: "none",
+    images: [],
+    operation: 'none',
+    tabIndex: 'files',
     isPushing: false,
     isRunning: false,
     setProject: (project: Project, operation:  "create" | "select" | "delete"| 
"none" | "copy") => {
@@ -118,6 +124,18 @@ export const useProjectStore = 
createWithEqualityFn<ProjectState>((set) => ({
             operation: o
         }));
     },
+    setTabIndex: (tabIndex: string | number) => {
+        set((state: ProjectState) => ({
+            tabIndex: tabIndex
+        }));
+    },
+    setImages: (images: string[]) => {
+        set((state: ProjectState) => {
+            state.images.length = 0;
+            state.images.push(...images);
+            return {images: state.images};
+        });
+    },
     memory: {},
     setMemory: (memory: boolean)  => {
         set({memory: memory})
diff --git a/karavan-web/karavan-app/src/main/webui/src/index.css 
b/karavan-web/karavan-app/src/main/webui/src/index.css
index 6cf02634..45542add 100644
--- a/karavan-web/karavan-app/src/main/webui/src/index.css
+++ b/karavan-web/karavan-app/src/main/webui/src/index.css
@@ -153,6 +153,7 @@
 
 .karavan .project-page .pf-v5-c-description-list__group {
   min-height: 30px;
+  align-items: center;
 }
 
 .karavan .project-page .pf-v5-c-tabs__link,
@@ -191,10 +192,6 @@
   padding-bottom: 0;
 }
 
-.karavan .project-page .project-operations {
-  margin-bottom: 100px;
-}
-
 .karavan .project-log {
   position: absolute;
   bottom: 0;
@@ -409,6 +406,14 @@
   padding: 0 5px 0 5px;
 }
 
+.karavan .project-page .project-build-panel {
+  padding-bottom: 10px;
+}
+
+.karavan .project-page .project-images-panel {
+  padding-top: 0;
+}
+
 .projects-page tr:hover .project-action-buttons {
   opacity: 1;
 }
diff --git a/karavan-web/karavan-app/src/main/webui/src/main/Main.tsx 
b/karavan-web/karavan-app/src/main/webui/src/main/Main.tsx
index 6e4d70d6..7ae54de6 100644
--- a/karavan-web/karavan-app/src/main/webui/src/main/Main.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/main/Main.tsx
@@ -2,13 +2,14 @@ import {Navigate, Route, Routes} from 'react-router-dom';
 import React, {useEffect, useRef} from "react";
 import {KaravanApi} from "../api/KaravanApi";
 import {
-    Bullseye, capitalize,
+    Bullseye,
     Flex,
     FlexItem,
     Page,
     ProgressStep,
     ProgressStepper,
     Spinner,
+    Text, TextContent, TextVariants,
     Tooltip,
     TooltipPosition
 } from "@patternfly/react-core";
@@ -29,6 +30,7 @@ import {MainDataPoller} from "./MainDataPoller";
 import {TemplatesPage} from "../templates/TemplatesPage";
 import {EventBus} from "../designer/utils/EventBus";
 import {Notification} from "../designer/utils/Notification";
+import CheckIcon from "@patternfly/react-icons/dist/esm/icons/check-icon";
 
 export function Main() {
 
@@ -76,21 +78,35 @@ export function Main() {
     }
 
     function getStepper() {
-        const steps: any[] = Array.isArray(readiness.checks) ? 
readiness.checks : [];
+        const steps: any[] = Array.isArray(readiness?.checks) ? 
readiness.checks : [];
         return (
             <Bullseye className="">
-                <ProgressStepper aria-label="Readiness progress" 
isCenterAligned isVertical >
-                    {steps.map(step => (
-                        <ProgressStep
-                            variant={step.status === 'UP' ? "success" : "info"}
-                            id={step.name}
-                            titleId={step.name}
-                            aria-label={step.name}
-                        >
-                            {step.name}
-                        </ProgressStep>
-                    ))}
-                </ProgressStepper>
+                <Flex direction={{default:"column"}} justifyContent={{default: 
"justifyContentCenter"}}>
+                    <FlexItem style={{textAlign: "center"}}>
+                        {Icon()}
+                        <TextContent>
+                            <Text component={TextVariants.h2}>
+                                Waiting for services
+                            </Text>
+                        </TextContent>
+                    </FlexItem>
+                    <FlexItem>
+                        <ProgressStepper aria-label="Readiness progress" 
isCenterAligned isVertical >
+                            {steps.map(step => (
+                                <ProgressStep
+                                    variant={step.status === 'UP' ? "success" 
: "info"}
+                                    isCurrent={step.status !== 'UP'}
+                                    icon={step.status !== 'UP' ? <Spinner 
isInline aria-label="Loading..."/> : undefined}
+                                    id={step.name}
+                                    titleId={step.name}
+                                    aria-label={step.name}
+                                >
+                                    {step.name}
+                                </ProgressStep>
+                            ))}
+                        </ProgressStepper>
+                    </FlexItem>
+                </Flex>
             </Bullseye>
         )
     }
diff --git a/karavan-web/karavan-app/src/main/webui/src/main/MainDataPoller.tsx 
b/karavan-web/karavan-app/src/main/webui/src/main/MainDataPoller.tsx
index 31885ad0..c371444c 100644
--- a/karavan-web/karavan-app/src/main/webui/src/main/MainDataPoller.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/main/MainDataPoller.tsx
@@ -30,7 +30,7 @@ export function MainDataPoller () {
         return () => {
             clearInterval(interval);
         };
-    }, [project]);
+    }, [project, readiness]);
 
     function getData() {
         KaravanApi.getReadiness((r: any) => {
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
new file mode 100644
index 00000000..1fa7a8f0
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import {Button, Flex, FlexItem} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon";
+import {useAppConfigStore} from "../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+
+interface Props {
+    reloadOnly?: boolean
+}
+
+export function BuildToolbar (props: Props) {
+
+    const [config] = useAppConfigStore((state) => [state.config], shallow)
+
+    return (<Flex className="toolbar" direction={{default: "row"}} 
alignItems={{default: "alignItemsCenter"}}>
+        <FlexItem>
+                <Button style={{visibility:"hidden"}} size="sm" 
variant={"control"} icon={<DeleteIcon/>} onClick={() => {}}>
+                </Button>
+        </FlexItem>
+    </Flex>);
+}
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
index 78877fba..bcb9bbf1 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
@@ -76,18 +76,18 @@ export function DevModeToolbar (props: Props) {
                 </Button>
             </Tooltip>
         </FlexItem>}
-        {config.infrastructure !== 'kubernetes' &&
-            <FlexItem>
-                <Tooltip content="Stop container" 
position={TooltipPosition.bottom}>
-                    <Button size="sm"
-                            isDisabled={!commands.includes('stop') || 
inTransit}
-                            variant={"control"}
-                            icon={<StopIcon/>}
-                            onClick={() => 
ProjectService.stopDevModeContainer(project)}>
-                    </Button>
-                </Tooltip>
-            </FlexItem>
-        }
+        {/*{config.infrastructure !== 'kubernetes' &&*/}
+        {/*    <FlexItem>*/}
+        {/*        <Tooltip content="Stop container" 
position={TooltipPosition.bottom}>*/}
+        {/*            <Button size="sm"*/}
+        {/*                    isDisabled={!commands.includes('stop') || 
inTransit}*/}
+        {/*                    variant={"control"}*/}
+        {/*                    icon={<StopIcon/>}*/}
+        {/*                    onClick={() => 
ProjectService.stopDevModeContainer(project)}>*/}
+        {/*            </Button>*/}
+        {/*        </Tooltip>*/}
+        {/*    </FlexItem>*/}
+        {/*}*/}
         <FlexItem>
             <Tooltip content="Delete container" 
position={TooltipPosition.bottom}>
                 <Button size="sm"
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/ProjectDataPoller.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/ProjectDataPoller.tsx
index 062b53e6..a70205f8 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectDataPoller.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectDataPoller.tsx
@@ -8,8 +8,8 @@ import {shallow} from "zustand/shallow";
 export function ProjectDataPoller () {
 
     const [config] = useAppConfigStore((state) => [state.config], shallow)
-    const [project, setMemory, setJvm, setContext, refreshTrace, setTrace] = 
useProjectStore((s) =>
-        [s.project, s.setMemory, s.setJvm, s.setContext, s.refreshTrace, 
s.setTrace], shallow);
+    const [project, setMemory, setJvm, setContext, refreshTrace, setTrace, 
setImages] = useProjectStore((s) =>
+        [s.project, s.setMemory, s.setJvm, s.setContext, s.refreshTrace, 
s.setTrace, s.setImages], shallow);
 
     useEffect(() => {
         const interval = setInterval(() => onRefreshStatus(), 1000);
@@ -41,6 +41,9 @@ export function ProjectDataPoller () {
                 setContext({});
             }
         })
+        KaravanApi.getImages(project.projectId, (res: any) => {
+            setImages(res)
+        });
         if (refreshTrace) {
             KaravanApi.getDevModeStatus(projectId, "trace", res => {
                 if (res.status === 200) {
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPage.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPage.tsx
index 2101f0e4..c932c5f3 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPage.tsx
@@ -22,8 +22,9 @@ export function ProjectPage () {
     const [mode, setMode] = useState<"design" | "code">("design");
     const [key, setKey] = useState<string>('');
     const [projects] = useProjectsStore((state) => [state.projects], shallow)
-    const [project, setProject] = useProjectStore((state) => [state.project, 
state.setProject], shallow )
+    const [project, setProject] = useProjectStore((s) => [s.project, 
s.setProject], shallow )
     let { projectId } = useParams();
+    const [tab, setTab] = useState<string | number>('files');
 
     useEffect(() => {
         const p = projects.filter(project => project.projectId === 
projectId).at(0);
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
index 94fb1d28..27246306 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useState} from 'react';
+import React, {useEffect} from 'react';
 import {
     Flex,
     FlexItem, Tabs, Tab
@@ -11,11 +11,11 @@ import {TraceTab} from "./trace/TraceTab";
 import {ProjectBuildTab} from "./build/ProjectBuildTab";
 import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
+import {ImagesPanel} from "./build/ImagesPanel";
 
 export function ProjectPanel () {
 
-    const [tab, setTab] = useState<string | number>('files');
-    const [project] = useProjectStore((state) => [state.project], shallow )
+    const [project,tab, setTab] = useProjectStore((s) => [s.project, 
s.tabIndex, s.setTabIndex], shallow );
 
     useEffect(() => {
         onRefresh();
@@ -32,7 +32,6 @@ export function ProjectPanel () {
     }
 
     const buildIn = isBuildIn();
-    const isCustomKamelets = project.projectId === 'kamelets';
     return (
         <Flex direction={{default: "column"}} spaceItems={{default: 
"spaceItemsNone"}}>
             <FlexItem className="project-tabs">
@@ -51,6 +50,7 @@ export function ProjectPanel () {
                         {tab === 'dashboard' && project && <DashboardTab/>}
                         {tab === 'trace' && project && <TraceTab/>}
                         {tab === 'build' && <ProjectBuildTab/>}
+                        {tab === 'build' && <ImagesPanel/>}
                     </>
                 }
             </FlexItem>
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
index 1cd17496..c1741c72 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
@@ -21,11 +21,12 @@ import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
 import {ProjectModelApi} from "karavan-core/lib/api/ProjectModelApi";
 import {ProjectModel, ProjectProperty} from 
"karavan-core/lib/model/ProjectModel";
+import {BuildToolbar} from "./BuildToolbar";
 
 
 export function ProjectToolbar () {
 
-    const [project, isPushing] = useProjectStore((state) => [state.project, 
state.isPushing], shallow )
+    const [project, isPushing, tabIndex] = useProjectStore((s) => [s.project, 
s.isPushing, s.tabIndex], shallow )
     const [file, editAdvancedProperties, setEditAdvancedProperties, 
setAddProperty, mode, setMode]
         = useFileStore((state) =>
         [state.file, state.editAdvancedProperties, 
state.setEditAdvancedProperties, state.setAddProperty, state.mode, 
state.setMode], shallow )
@@ -108,6 +109,7 @@ export function ProjectToolbar () {
         return (<Toolbar id="toolbar-group-types">
             <ToolbarContent>
                 {isRunnable() && <DevModeToolbar/>}
+                {isBuild() && <BuildToolbar/>}
             </ToolbarContent>
         </Toolbar>)
     }
@@ -125,7 +127,11 @@ export function ProjectToolbar () {
     }
 
     function isRunnable(): boolean {
-        return !isKameletsProject() && !isTemplatesProject() && 
!isServicesProject();
+        return !isKameletsProject() && !isTemplatesProject() && 
!isServicesProject() && tabIndex !== 'build';
+    }
+
+    function isBuild(): boolean {
+        return !isKameletsProject() && !isTemplatesProject() && 
!isServicesProject() && tabIndex === 'build';
     }
 
     function allowAddFiles(): boolean {
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/build/BuildStatus.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/build/BuildStatus.tsx
deleted file mode 100644
index e3a29ffc..00000000
--- a/karavan-web/karavan-app/src/main/webui/src/project/build/BuildStatus.tsx
+++ /dev/null
@@ -1,355 +0,0 @@
-import React, {useState} from 'react';
-import {
-    Button,
-    DescriptionList,
-    DescriptionListTerm,
-    DescriptionListGroup,
-    DescriptionListDescription, Spinner, Tooltip, Flex, FlexItem, LabelGroup, 
Label, Modal, Badge, CardBody, Card
-} from '@patternfly/react-core';
-import '../../designer/karavan.css';
-import {KaravanApi} from "../../api/KaravanApi";
-import BuildIcon from "@patternfly/react-icons/dist/esm/icons/build-icon";
-import RolloutIcon from 
"@patternfly/react-icons/dist/esm/icons/process-automation-icon";
-import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
-import DownIcon from 
"@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
-import ClockIcon from "@patternfly/react-icons/dist/esm/icons/clock-icon";
-import DeleteIcon from 
"@patternfly/react-icons/dist/esm/icons/times-circle-icon";
-import {ContainerStatus} from "../../api/ProjectModels";
-import {useLogStore, useProjectStore, useStatusesStore} from 
"../../api/ProjectStore";
-import {shallow} from "zustand/shallow";
-
-interface Props {
-    env: string,
-}
-
-export function BuildStatus (props: Props) {
-
-    const [project] = useProjectStore((s) => [s.project], shallow);
-    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
-    const [containers, deployments, camels, pipelineStatuses] =
-        useStatusesStore((s) => [s.containers, s.deployments, s.camels, 
s.pipelineStatuses], shallow);
-    const [isPushing, setIsPushing] = useState<boolean>(false);
-    const [isBuilding, setIsBuilding] = useState<boolean>(false);
-    const [isRolling, setIsRolling] = useState<boolean>(false);
-    const [showDeleteConfirmation, setShowDeleteConfirmation] = 
useState<boolean>(false);
-    const [deleteEntityType, setDeleteEntityType] = useState<'pod' | 
'deployment' | 'build'>('pod');
-    const [deleteEntityName, setDeleteEntityName] = useState<string>();
-    const [deleteEntityEnv, setDeleteEntityEnv] = useState<string>();
-
-    function deleteEntity(type: 'pod' | 'deployment' | 'build', name: string, 
environment: string) {
-        switch (type) {
-            case "deployment":
-                KaravanApi.deleteDeployment(environment, name, (res: any) => {
-                    // if (Array.isArray(res) && Array.from(res).length > 0)
-                    // onRefresh();
-                });
-                break;
-            case "pod":
-                KaravanApi.deleteContainer(environment, 'project', name, (res: 
any) => {
-                    // if (Array.isArray(res) && Array.from(res).length > 0)
-                    // onRefresh();
-                });
-                break;
-            case "build":
-                KaravanApi.stopBuild(environment, name, (res: any) => {
-                    // if (Array.isArray(res) && Array.from(res).length > 0)
-                    // onRefresh();
-                });
-                break;
-        }
-    }
-
-    function build() {
-        setIsBuilding(true);
-        setShowLog(false,'none')
-        KaravanApi.buildProject(project, env, res => {
-            if (res.status === 200 || res.status === 201) {
-                setIsBuilding(false);
-            } else {
-                // Todo notification
-            }
-        });
-    }
-
-    function rollout() {
-        setIsRolling(true);
-        KaravanApi.rolloutDeployment(project.projectId, env, res => {
-            if (res.status === 200 || res.status === 201) {
-                setIsRolling(false);
-            } else {
-                // Todo notification
-            }
-        });
-    }
-
-    function buildButton(env: string) {
-        const status = pipelineStatuses.filter(p => p.projectId === 
project.projectId).at(0);
-        const isRunning = status?.result === 'Running';
-        return (<Tooltip content="Start build pipeline" position={"left"}>
-            <Button isLoading={isBuilding ? true : undefined}
-                    isDisabled={isBuilding || isRunning || isPushing}
-                    size="sm"
-                    variant="secondary"
-                    className="project-button"
-                    icon={!isBuilding ? <BuildIcon/> : <div></div>}
-                    onClick={e => build()}>
-                {isBuilding ? "..." : "Build"}
-            </Button>
-        </Tooltip>)
-    }
-
-    function rolloutButton() {
-        return (<Tooltip content="Rollout deployment" position={"left"}>
-            <Button isLoading={isRolling ? true : undefined} size="sm" 
variant="secondary"
-                    className="project-button"
-                    icon={!isRolling ? <RolloutIcon/> : <div></div>}
-                    onClick={e => rollout()}>
-                {isRolling ? "..." : "Rollout"}
-            </Button>
-        </Tooltip>)
-    }
-
-    function deleteDeploymentButton(env: string) {
-        return (<Tooltip content="Delete deployment" position={"left"}>
-            <Button size="sm" variant="secondary"
-                    className="project-button"
-                    icon={<DeleteIcon/>}
-                    onClick={e => {
-                        setShowDeleteConfirmation(true);
-                        setDeleteEntityType("deployment");
-                        setDeleteEntityEnv(env);
-                        setDeleteEntityName(project?.projectId);
-                    }}>
-                {"Delete"}
-            </Button>
-        </Tooltip>)
-    }
-
-    function getReplicasPanel(env: string) {
-        const deploymentStatus = deployments.find(d => d.name === 
project?.projectId);
-        const ok = (deploymentStatus && deploymentStatus?.readyReplicas > 0
-            && (deploymentStatus.unavailableReplicas === 0 || 
deploymentStatus.unavailableReplicas === undefined || 
deploymentStatus.unavailableReplicas === null)
-            && deploymentStatus?.replicas === deploymentStatus?.readyReplicas)
-        return (
-            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} 
alignItems={{default: "alignItemsCenter"}}>
-                <FlexItem>
-                    {deploymentStatus && <LabelGroup numLabels={3}>
-                        <Tooltip content={"Ready Replicas / Replicas"} 
position={"left"}>
-                            <Label icon={ok ? <UpIcon/> : <DownIcon/>}
-                                   color={ok ? "green" : "grey"}>{"Replicas: " 
+ deploymentStatus.readyReplicas + " / " + deploymentStatus.replicas}</Label>
-                        </Tooltip>
-                        {deploymentStatus.unavailableReplicas > 0 &&
-                            <Tooltip content={"Unavailable replicas"} 
position={"right"}>
-                                <Label icon={<DownIcon/>} 
color={"red"}>{deploymentStatus.unavailableReplicas}</Label>
-                            </Tooltip>
-                        }
-                    </LabelGroup>}
-                    {deploymentStatus === undefined && <Label 
icon={<DownIcon/>} color={"grey"}>No deployments</Label>}
-                </FlexItem>
-                <FlexItem>{env === "dev" && 
deleteDeploymentButton(env)}</FlexItem>
-            </Flex>
-        )
-    }
-
-    function getPodsPanel(env: string) {
-        const podStatuses = containers.filter(d => d.projectId === 
project?.projectId && d.type === 'project');
-        return (
-            <Flex justifyContent={{default: "justifyContentSpaceBetween"}}
-                  alignItems={{default: "alignItemsFlexStart"}}>
-                <FlexItem>
-                    {podStatuses.length === 0 && <Label icon={<DownIcon/>} 
color={"grey"}>No pods</Label>}
-                    <LabelGroup numLabels={2} isVertical>
-                        {podStatuses.map((pod: ContainerStatus) => {
-                                const ready = pod.state === 'running';
-                                return (
-                                    <Tooltip key={pod.containerName} 
content={pod.state}>
-                                        <Label icon={ready ? <UpIcon/> : 
<DownIcon/>} color={ready ? "green" : "red"}>
-                                            <Button variant="link" 
className="labeled-button"
-                                                    onClick={e => {
-                                                        
setShowLog(true,'container', pod.containerName);
-                                                    }}>
-                                                {pod.containerName}
-                                            </Button>
-                                            <Tooltip content={"Delete Pod"}>
-                                                <Button icon={<DeleteIcon/>}
-                                                        
className="labeled-button"
-                                                        variant="link"
-                                                        onClick={e => {
-                                                            
setShowDeleteConfirmation(true);
-                                                            
setDeleteEntityType("pod");
-                                                            
setDeleteEntityEnv(env);
-                                                            
setDeleteEntityName(pod.containerName);
-                                                        }}></Button>
-                                            </Tooltip>
-                                        </Label>
-                                    </Tooltip>
-                                )
-                            }
-                        )}
-                    </LabelGroup>
-                </FlexItem>
-                <FlexItem>{env === "dev" && rolloutButton()}</FlexItem>
-            </Flex>
-        )
-    }
-
-    function getPipelineState(env: string) {
-        const status = pipelineStatuses.filter(p => p.projectId === 
project.projectId).at(0);
-        const pipeline = status?.pipelineName;
-        const pipelineResult = status?.result;
-        let lastPipelineRunTime = 0;
-        if (status?.startTime) {
-            const start: Date = new Date(status.startTime);
-            const finish: Date = status.completionTime !== undefined && 
status.completionTime !== null ? new Date(status.completionTime) : new Date();
-            lastPipelineRunTime = Math.round((finish.getTime() - 
start.getTime()) / 1000);
-        }
-        const showTime = lastPipelineRunTime && lastPipelineRunTime > 0;
-        const isRunning = pipelineResult === 'Running';
-        const isFailed = pipelineResult === 'Failed';
-        const isSucceeded = pipelineResult === 'Succeeded';
-        const color = isSucceeded ? "green" : (isFailed ? "red" : (isRunning ? 
"blue" : "grey"))
-        const icon = isSucceeded ? <UpIcon className="not-spinner"/> : 
<DownIcon className="not-spinner"/>
-        return (
-            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} 
alignItems={{default: "alignItemsCenter"}}>
-                <FlexItem>
-                    <Tooltip content={pipelineResult} position={"right"}>
-                        <LabelGroup numLabels={2}>
-                            <Label icon={isRunning ? <Spinner diameter="16px" 
className="spinner"/> : icon}
-                                   color={color}>
-                                {pipeline
-                                    ? <Button className='labeled-button' 
variant="link" onClick={e =>
-                                        useLogStore.setState({showLog: true, 
type: 'build', podName: pipeline})
-                                    }>
-                                        {pipeline}
-                                    </Button>
-                                    : "No builder"}
-                                {isRunning && <Tooltip content={"Stop build"}>
-                                    <Button
-                                        icon={<DeleteIcon/>}
-                                        className="labeled-button"
-                                        variant="link" onClick={e => {
-                                        setShowDeleteConfirmation(true);
-                                        setDeleteEntityType("build");
-                                        setDeleteEntityEnv(env);
-                                        setDeleteEntityName(pipeline);
-                                    }}></Button>
-                                </Tooltip>}
-                            </Label>
-                            {pipeline !== undefined && showTime === true && 
lastPipelineRunTime !== undefined &&
-                                <Label icon={<ClockIcon 
className="not-spinner"/>}
-                                       color={color}>{lastPipelineRunTime + 
"s"}</Label>}
-                        </LabelGroup>
-                    </Tooltip>
-                </FlexItem>
-                <FlexItem>{env === "dev" && buildButton(env)}</FlexItem>
-            </Flex>
-        )
-    }
-
-    function getBuildState(env: string) {
-        const status = containers.filter(c => c.projectId === 
project.projectId).at(0);
-        const buildName = status?.containerName;
-        const state = status?.state;
-        let buildTime = 0;
-        if (status?.created) {
-            const start: Date = new Date(status.created);
-            const finish: Date = status.finished !== undefined && 
status.finished !== null ? new Date(status.finished) : new Date();
-            buildTime = Math.round((finish.getTime() - start.getTime()) / 
1000);
-        }
-        const showTime = buildTime && buildTime > 0;
-        const isRunning = state === 'running';
-        const isExited = state === 'exited';
-        const color = isExited ? "grey" : (isRunning ? "blue" : "grey");
-        const icon = isExited ? <UpIcon className="not-spinner"/> : <DownIcon 
className="not-spinner"/>
-        return (
-            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} 
alignItems={{default: "alignItemsCenter"}}>
-                <FlexItem>
-                    <LabelGroup numLabels={2}>
-                        <Label icon={isRunning ? <Spinner diameter="16px" 
className="spinner"/> : icon}
-                               color={color}>
-                            {buildName
-                                ? <Button className='labeled-button' 
variant="link" onClick={e =>
-                                    useLogStore.setState({showLog: true, type: 
'build', podName: buildName})
-                                }>
-                                    {buildName}
-                                </Button>
-                                : "No builder"}
-                            {status !== undefined && <Tooltip content={"Delete 
build"}>
-                                <Button
-                                    icon={<DeleteIcon/>}
-                                    className="labeled-button"
-                                    variant="link" onClick={e => {
-                                    setShowDeleteConfirmation(true);
-                                    setDeleteEntityType("build");
-                                    setDeleteEntityEnv(env);
-                                    setDeleteEntityName(buildName);
-                                }}></Button>
-                            </Tooltip>}
-                        </Label>
-                        {buildName !== undefined && showTime === true && 
buildTime !== undefined &&
-                            <Label icon={<ClockIcon className="not-spinner"/>}
-                                   color={color}>{buildTime + "s"}</Label>}
-                    </LabelGroup>
-                </FlexItem>
-                <FlexItem>{env === "dev" && buildButton(env)}</FlexItem>
-            </Flex>
-        )
-    }
-
-    function getDeleteConfirmation() {
-        return (<Modal
-            className="modal-delete"
-            title="Confirmation"
-            isOpen={showDeleteConfirmation}
-            onClose={() => setShowDeleteConfirmation(false)}
-            actions={[
-                <Button key="confirm" variant="primary" onClick={e => {
-                    if (deleteEntityEnv && deleteEntityName && deleteEntity) {
-                        deleteEntity(deleteEntityType, deleteEntityName, 
deleteEntityEnv);
-                        setShowDeleteConfirmation(false);
-                    }
-                }}>Delete
-                </Button>,
-                <Button key="cancel" variant="link"
-                        onClick={e => 
setShowDeleteConfirmation(false)}>Cancel</Button>
-            ]}
-            onEscapePress={e => setShowDeleteConfirmation(false)}>
-            <div>{"Delete " + deleteEntityType + " " + deleteEntityName + 
"?"}</div>
-        </Modal>)
-    }
-
-    const env = props.env;
-    return (
-        <Card className="project-status">
-            <CardBody>
-                <DescriptionList isHorizontal>
-                    <DescriptionListGroup>
-                        <DescriptionListTerm>Environment</DescriptionListTerm>
-                        <DescriptionListDescription>
-                            <Badge className="badge">{env}</Badge>
-                        </DescriptionListDescription>
-                    </DescriptionListGroup>
-                    <DescriptionListGroup>
-                        <DescriptionListTerm>Build 
container</DescriptionListTerm>
-                        <DescriptionListDescription>
-                            {getBuildState(env)}
-                        </DescriptionListDescription>
-                    </DescriptionListGroup>
-                    <DescriptionListGroup>
-                        <DescriptionListTerm>Deployment</DescriptionListTerm>
-                        <DescriptionListDescription>
-                            {getReplicasPanel(env)}
-                        </DescriptionListDescription>
-                    </DescriptionListGroup>
-                    <DescriptionListGroup>
-                        <DescriptionListTerm>Pods</DescriptionListTerm>
-                        <DescriptionListDescription>
-                            {getPodsPanel(env)}
-                        </DescriptionListDescription>
-                    </DescriptionListGroup>
-                </DescriptionList>
-            </CardBody>
-            {showDeleteConfirmation && getDeleteConfirmation()}
-        </Card>
-    )
-}
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
index 89d4bbbf..6c916da4 100644
--- 
a/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
+++ 
b/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
@@ -1,21 +1,20 @@
 import React from 'react';
 import '../../designer/karavan.css';
-import {BuildStatus} from "./BuildStatus";
+import {BuildPanel} from "./BuildPanel";
 import {PageSection} from "@patternfly/react-core";
 import {useAppConfigStore, useProjectStore} from "../../api/ProjectStore";
 
-
 export function ProjectBuildTab () {
 
     const {config} = useAppConfigStore();
     const {project} = useProjectStore();
 
     return (
-        <PageSection className="project-tab-panel" padding={{default: 
"padding"}}>
-            <div className="project-operations">
+        <PageSection className="project-tab-panel project-build-panel" 
padding={{default: "padding"}}>
+            <div>
                 {/*{["dev", "test", "prod"].map(env =>*/}
                 {config.environments.map(env =>
-                    <BuildStatus env={env}/>
+                    <BuildPanel env={env}/>
                 )}
             </div>
         </PageSection>

Reply via email to