You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2023/07/27 22:22:45 UTC

[camel-karavan] branch main updated (e106d5eb -> 17084c37)

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

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


    from e106d5eb Build git url along the lines of `https://user:password@gitserver.com` (#831)
     new 6d30dbab projects fix for #817
     new 44a06742 devservices #817
     new 17084c37 devservices logs #817

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/camel/karavan/api/DevModeResource.java  |  16 +-
 .../camel/karavan/api/InfrastructureResource.java  |  48 ++++-
 .../apache/camel/karavan/docker/DockerService.java | 216 +++++++------------
 .../camel/karavan/docker/DockerServiceUtils.java   | 229 +++++++++++++++++++++
 .../camel/karavan/docker/model/DevService.java     | 103 +++++++++
 .../karavan/docker/model/HealthCheckConfig.java    |  55 +++++
 .../apache/camel/karavan/service/CodeService.java  |   5 +-
 .../camel/karavan/service/KaravanService.java      |  13 +-
 .../camel/karavan/service/ProjectService.java      |  19 +-
 .../camel/karavan/service/ScheduledService.java    |  11 +-
 .../karavan-app/src/main/webui/src/Main.tsx        |  41 ++--
 .../src/main/webui/src/api/KaravanApi.tsx          |   8 +-
 .../src/main/webui/src/api/ProjectModels.ts        |   2 +-
 .../src/main/webui/src/api/ProjectService.ts       | 112 +++++-----
 .../src/main/webui/src/api/ProjectStore.ts         |   8 -
 .../src/main/webui/src/api/ServiceModels.ts        |  13 +-
 .../webui/src/containers/ContainerTableRow.tsx     |  12 +-
 .../main/webui/src/containers/ContainersPage.tsx   |  49 +----
 .../karavan-app/src/main/webui/src/index.css       |  16 +-
 .../src/main/webui/src/project/DevModeToolbar.tsx  |  26 +--
 .../src/main/webui/src/project/ProjectPage.tsx     |  19 +-
 .../webui/src/project/dashboard/DashboardTab.tsx   |  19 +-
 .../main/webui/src/project/log/ProjectLogPanel.tsx |  14 +-
 .../webui/src/project/pipeline/ProjectStatus.tsx   |   7 +-
 .../src/main/webui/src/projects/ProjectsPage.tsx   |  15 +-
 .../src/main/webui/src/services/ServicesPage.tsx   |  26 ++-
 .../main/webui/src/services/ServicesTableRow.tsx   | 104 ++++++++--
 .../karavan/infinispan/InfinispanService.java      |   6 -
 .../karavan/infinispan/model/ContainerStatus.java  |   4 +
 29 files changed, 815 insertions(+), 401 deletions(-)
 create mode 100644 karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java
 create mode 100644 karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DevService.java
 create mode 100644 karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/HealthCheckConfig.java


[camel-karavan] 01/03: projects fix for #817

Posted by ma...@apache.org.
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 6d30dbabef13e9f27a9ff43241247088970c9bb7
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Wed Jul 26 16:58:30 2023 -0400

    projects fix for #817
---
 .../org/apache/camel/karavan/docker/DockerService.java    | 11 +++++++++++
 .../org/apache/camel/karavan/service/KaravanService.java  | 13 +++++++++----
 .../org/apache/camel/karavan/service/ProjectService.java  |  8 ++++----
 .../src/main/webui/src/project/DevModeToolbar.tsx         |  9 +++++----
 .../src/main/webui/src/projects/ProjectsPage.tsx          | 15 +++++++++------
 5 files changed, 38 insertions(+), 18 deletions(-)

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 b7a6fa5e..af1d67f6 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
@@ -89,6 +89,17 @@ public class DockerService {
 
     private DockerClient dockerClient;
 
+    public boolean checkDocker() {
+        try {
+            getDockerClient().pingCmd().exec();
+            LOGGER.info("Docker is available");
+            return true;
+        } catch (Exception e) {
+            LOGGER.error("Error connecting Docker: " + e.getMessage());
+            return false;
+        }
+    }
+
     public void runDevmodeContainer(Project project, String jBangOptions) throws InterruptedException {
         String projectId = project.getProjectId();
         LOGGER.infof("DevMode starting for %s with JBANG_OPTIONS=%s", projectId, jBangOptions);
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 2f5aa7ad..ee801b94 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
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.karavan.service;
 
+import io.quarkus.runtime.Quarkus;
 import io.quarkus.runtime.ShutdownEvent;
 import io.quarkus.runtime.StartupEvent;
 import io.vertx.core.eventbus.EventBus;
@@ -52,10 +53,14 @@ public class KaravanService {
                 LOGGER.info("Starting Karavan Headless in Docker");
             } else {
                 LOGGER.info("Starting Karavan with Docker");
-                dockerService.createNetwork();
-                dockerService.startListeners();
-                dockerService.startInfinispan();
-                dockerService.checkInfinispanHealth();
+                if (!dockerService.checkDocker()){
+                    Quarkus.asyncExit();
+                } else {
+                    dockerService.createNetwork();
+                    dockerService.startListeners();
+                    dockerService.startInfinispan();
+                    dockerService.checkInfinispanHealth();
+                }
             }
         } else {
             LOGGER.info("Starting Karavan in " + (kubernetesService.isOpenshift() ? "OpenShift" : "Kubernetes"));
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 815eab38..7c05cea0 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
@@ -229,10 +229,10 @@ public class ProjectService implements HealthCheck{
     void addServicesProject() {
         LOGGER.info("Add services project if not exists");
         try {
-            Project templates  = infinispanService.getProject(Project.Type.services.name());
-            if (templates == null) {
-                templates = new Project(Project.Type.services.name(), "Services", "Development Services", "", "", Instant.now().toEpochMilli(), Project.Type.services);
-                infinispanService.saveProject(templates);
+            Project services  = infinispanService.getProject(Project.Type.services.name());
+            if (services == null) {
+                services = new Project(Project.Type.services.name(), "Services", "Development Services", "", "", Instant.now().toEpochMilli(), Project.Type.services);
+                infinispanService.saveProject(services);
 
                 codeService.getServices().forEach((name, value) -> {
                     ProjectFile file = new ProjectFile(name, value, Project.Type.services.name(), Instant.now().toEpochMilli());
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 58dae7ea..9777019b 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
@@ -4,6 +4,7 @@ import '../designer/karavan.css';
 import RocketIcon from "@patternfly/react-icons/dist/esm/icons/rocket-icon";
 import ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
 import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon";
+import PauseIcon from "@patternfly/react-icons/dist/esm/icons/pause-icon";
 import {useDevModeStore, useLogStore, useProjectStore} from "../api/ProjectStore";
 import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
@@ -74,7 +75,7 @@ export const DevModeToolbar = (props: Props) => {
                 </Button>
             </Tooltip>
         </FlexItem>}
-        {<FlexItem>
+        <FlexItem>
             <Tooltip content="Stop container" position={TooltipPosition.bottom}>
                 <Button isSmall
                         isDisabled={!commands.includes('stop') || inTransit}
@@ -83,8 +84,8 @@ export const DevModeToolbar = (props: Props) => {
                         onClick={() => ProjectService.stopDevModeContainer(project)}>
                 </Button>
             </Tooltip>
-        </FlexItem>}
-        {<FlexItem>
+        </FlexItem>
+        <FlexItem>
             <Tooltip content="Delete container" position={TooltipPosition.bottom}>
                 <Button isSmall
                         isDisabled={!commands.includes('delete') || inTransit}
@@ -93,6 +94,6 @@ export const DevModeToolbar = (props: Props) => {
                         onClick={() => ProjectService.deleteDevModeContainer(project)}>
                 </Button>
             </Tooltip>
-        </FlexItem>}
+        </FlexItem>
     </Flex>);
 }
diff --git a/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsPage.tsx b/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsPage.tsx
index dd03c5dc..9b8c0d74 100644
--- a/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsPage.tsx
@@ -40,9 +40,7 @@ export const ProjectsPage = () => {
         const interval = setInterval(() => {
             if (projects.length === 0) setLoading(true);
             if (!["create", "delete", "select", "copy"].includes(operation)) {
-                ProjectService.refreshProjects();
-                ProjectService.refreshAllDeploymentStatuses();
-                ProjectService.refreshAllContainerStatuses();
+                refresh();
             }
         }, 1300);
         return () => {
@@ -50,14 +48,19 @@ export const ProjectsPage = () => {
         };
     }, [operation]);
 
+    function refresh() {
+        ProjectService.refreshProjects();
+        ProjectService.refreshAllDeploymentStatuses();
+        ProjectService.refreshAllContainerStatuses();
+        setLoading(false);
+    }
     function getTools() {
         return <Toolbar id="toolbar-group-types">
             <ToolbarContent>
                 <ToolbarItem>
                     <Button variant="link" icon={<RefreshIcon/>} onClick={e => {
-                        ProjectService.refreshProjects();
-                        ProjectService.refreshAllDeploymentStatuses();
-                        ProjectService.refreshAllContainerStatuses();
+                        setLoading(true);
+                        refresh();
                     }}/>
                 </ToolbarItem>
                 <ToolbarItem>


[camel-karavan] 02/03: devservices #817

Posted by ma...@apache.org.
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 44a0674263bcf7ffb282e783cfac6a5572cc35cc
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Thu Jul 27 16:39:12 2023 -0400

    devservices #817
---
 .../apache/camel/karavan/api/DevModeResource.java  |  16 +-
 .../camel/karavan/api/InfrastructureResource.java  |  48 ++++-
 .../apache/camel/karavan/docker/DockerService.java | 205 ++++++------------
 .../camel/karavan/docker/DockerServiceUtils.java   | 229 +++++++++++++++++++++
 .../camel/karavan/docker/model/DevService.java     | 103 +++++++++
 .../karavan/docker/model/HealthCheckConfig.java    |  55 +++++
 .../apache/camel/karavan/service/CodeService.java  |   5 +-
 .../camel/karavan/service/ProjectService.java      |  11 +-
 .../camel/karavan/service/ScheduledService.java    |  11 +-
 .../src/main/webui/src/api/KaravanApi.tsx          |   8 +-
 .../src/main/webui/src/api/ProjectModels.ts        |   2 +-
 .../src/main/webui/src/api/ProjectService.ts       |  66 +++---
 .../src/main/webui/src/api/ServiceModels.ts        |  13 +-
 .../webui/src/containers/ContainerTableRow.tsx     |  12 +-
 .../main/webui/src/containers/ContainersPage.tsx   |  32 +--
 .../webui/src/project/pipeline/ProjectStatus.tsx   |   2 +-
 .../src/main/webui/src/services/ServicesPage.tsx   |  33 ++-
 .../main/webui/src/services/ServicesTableRow.tsx   |  91 ++++++--
 .../karavan/infinispan/InfinispanService.java      |   6 -
 .../karavan/infinispan/model/ContainerStatus.java  |   4 +
 20 files changed, 680 insertions(+), 272 deletions(-)

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 d6d647c9..8466fe42 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
@@ -35,6 +35,8 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.util.Objects;
 
+import static org.apache.camel.karavan.shared.EventType.CONTAINER_STATUS;
+
 @Path("/api/devmode")
 public class DevModeResource {
 
@@ -74,7 +76,8 @@ public class DevModeResource {
             if (ConfigService.inKubernetes()) {
                 kubernetesService.runDevModeContainer(project, jBangOptions);
             } else {
-                dockerService.runDevmodeContainer(project, jBangOptions);
+                dockerService.createDevmodeContainer(project.getProjectId(), jBangOptions);
+                dockerService.runContainer(project.getProjectId());
             }
             return Response.ok(containerName).build();
         }
@@ -105,7 +108,7 @@ public class DevModeResource {
     @Consumes(MediaType.APPLICATION_JSON)
     @Path("/{projectId}/{deletePVC}")
     public Response deleteDevMode(@PathParam("projectId") String projectId, @PathParam("deletePVC") boolean deletePVC) {
-        infinispanService.setContainerStatusTransit(projectId, environment, projectId);
+        setContainerStatusTransit(projectId, ContainerStatus.ContainerType.devmode.name());
         if (ConfigService.inKubernetes()) {
             kubernetesService.deleteDevModePod(projectId, deletePVC);
         } else {
@@ -114,6 +117,15 @@ public class DevModeResource {
         return Response.accepted().build();
     }
 
+    private void setContainerStatusTransit(String name, String type){
+        ContainerStatus status = infinispanService.getContainerStatus(name, environment, name);
+        if (status == null) {
+            status = ContainerStatus.createByType(name, environment, ContainerStatus.ContainerType.valueOf(type));
+        }
+        status.setInTransit(true);
+        eventBus.send(CONTAINER_STATUS, JsonObject.mapFrom(status));
+    }
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/container/{projectId}")
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 8291c7eb..74ffd6a8 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
@@ -21,12 +21,14 @@ 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.docker.DockerService;
+import org.apache.camel.karavan.docker.model.DevService;
 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.ProjectService;
 import org.apache.camel.karavan.shared.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
@@ -40,6 +42,8 @@ import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+import static org.apache.camel.karavan.shared.EventType.CONTAINER_STATUS;
+
 @Path("/api/infrastructure")
 public class InfrastructureResource {
 
@@ -55,6 +59,9 @@ public class InfrastructureResource {
     @Inject
     DockerService dockerService;
 
+    @Inject
+    ProjectService projectService;
+
     @ConfigProperty(name = "karavan.environment")
     String environment;
 
@@ -166,23 +173,47 @@ public class InfrastructureResource {
     @POST
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/container/{env}/{name}")
-    public Response startContainer(@PathParam("env") String env, @PathParam("name") String name, JsonObject command) throws Exception {
+    @Path("/container/{env}/{type}/{name}")
+    public Response startContainer(@PathParam("env") String env, @PathParam("type") String type, @PathParam("name") String name, JsonObject command) throws Exception {
         if (infinispanService.isReady()) {
-            infinispanService.setContainerStatusTransit(name, env, name);
+            // set container statuses
+            setContainerStatusTransit(name, type);
+            // exec docker commands
             if (command.containsKey("command")) {
-                if (command.getString("command").equalsIgnoreCase("start")) {
-                    dockerService.startContainer(name);
+                if (command.getString("command").equalsIgnoreCase("run")) {
+                    if (Objects.equals(type, ContainerStatus.ContainerType.devservice.name())) {
+                        String code = projectService.getDevServiceCode();
+                        DevService devService = dockerService.getDevService(code, name);
+                        if (devService != null) {
+                            dockerService.createDevserviceContainer(devService);
+                            dockerService.runContainer(devService.getContainer_name());
+                        }
+                    } else if (Objects.equals(type, ContainerStatus.ContainerType.devmode.name())) {
+                        dockerService.createDevmodeContainer(name, "");
+                        dockerService.runContainer(name);
+                    }
                     return Response.ok().build();
                 } else if (command.getString("command").equalsIgnoreCase("stop")) {
                     dockerService.stopContainer(name);
                     return Response.ok().build();
+                } else if (command.getString("command").equalsIgnoreCase("pause")) {
+                    dockerService.pauseContainer(name);
+                    return Response.ok().build();
                 }
             }
         }
         return Response.notModified().build();
     }
 
+    private void setContainerStatusTransit(String name, String type){
+        ContainerStatus status = infinispanService.getContainerStatus(name, environment, name);
+        if (status == null) {
+            status = ContainerStatus.createByType(name, environment, ContainerStatus.ContainerType.valueOf(type));
+        }
+        status.setInTransit(true);
+        eventBus.send(CONTAINER_STATUS, JsonObject.mapFrom(status));
+    }
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/container/{env}")
@@ -205,10 +236,11 @@ public class InfrastructureResource {
     @DELETE
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/container/{env}/{name}")
-    public Response deleteContainer(@PathParam("env") String env, @PathParam("name") String name) {
+    @Path("/container/{env}/{type}/{name}")
+    public Response deleteContainer(@PathParam("env") String env, @PathParam("type") String type, @PathParam("name") String name) {
         if (infinispanService.isReady()) {
-            infinispanService.setContainerStatusTransit(name, env, name);
+            // set container statuses
+            setContainerStatusTransit(name, type);
             try {
                 if (ConfigService.inKubernetes()) {
                     kubernetesService.deletePod(name, kubernetesService.getNamespace());
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 af1d67f6..03e8b378 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
@@ -18,6 +18,7 @@ package org.apache.camel.karavan.docker;
 
 import com.github.dockerjava.api.DockerClient;
 import com.github.dockerjava.api.async.ResultCallback;
+import com.github.dockerjava.api.command.CreateContainerCmd;
 import com.github.dockerjava.api.command.CreateContainerResponse;
 import com.github.dockerjava.api.command.CreateNetworkResponse;
 import com.github.dockerjava.api.command.HealthState;
@@ -30,11 +31,14 @@ import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
 import com.github.dockerjava.transport.DockerHttpClient;
 import io.smallrye.mutiny.tuples.Tuple2;
 import io.vertx.core.eventbus.EventBus;
-import org.apache.camel.karavan.infinispan.InfinispanService;
+import io.vertx.core.json.JsonObject;
+import org.apache.camel.karavan.docker.model.DevService;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.apache.camel.karavan.infinispan.model.Project;
+import org.apache.camel.karavan.service.CodeService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
+import org.yaml.snakeyaml.Yaml;
 
 import javax.enterprise.context.ApplicationScoped;
 import javax.inject.Inject;
@@ -49,7 +53,7 @@ import static org.apache.camel.karavan.shared.Constants.*;
 import static org.apache.camel.karavan.shared.EventType.*;
 
 @ApplicationScoped
-public class DockerService {
+public class DockerService extends DockerServiceUtils {
 
     private static final Logger LOGGER = Logger.getLogger(DockerService.class.getName());
 
@@ -57,10 +61,7 @@ public class DockerService {
     protected static final String KARAVAN_CONTAINER_NAME = "karavan-headless";
 
     protected static final String NETWORK_NAME = "karavan";
-    private static final DecimalFormat formatCpu = new DecimalFormat("0.00");
-    private static final DecimalFormat formatMiB = new DecimalFormat("0.0");
-    private static final DecimalFormat formatGiB = new DecimalFormat("0.00");
-    private static final Map<String, Tuple2<Long, Long>> previousStats = new ConcurrentHashMap<>();
+
     private static final List<String> infinispanHealthCheckCMD = List.of("CMD", "curl", "-f", "http://localhost:11222/rest/v2/cache-managers/default/health/status");
 
     @ConfigProperty(name = "karavan.environment")
@@ -100,25 +101,37 @@ public class DockerService {
         }
     }
 
-    public void runDevmodeContainer(Project project, String jBangOptions) throws InterruptedException {
-        String projectId = project.getProjectId();
+    public void createDevmodeContainer(String projectId, String jBangOptions) throws InterruptedException {
         LOGGER.infof("DevMode starting for %s with JBANG_OPTIONS=%s", projectId, jBangOptions);
 
         HealthCheck healthCheck = new HealthCheck().withTest(List.of("CMD", "curl", "-f", "http://localhost:8080/q/dev/health"))
                 .withInterval(10000000000L).withTimeout(10000000000L).withStartPeriod(10000000000L).withRetries(30);
 
-        List<String> env = jBangOptions !=null && !jBangOptions.trim().isEmpty()
+        List<String> env = jBangOptions != null && !jBangOptions.trim().isEmpty()
                 ? List.of(ENV_VAR_JBANG_OPTIONS + "=" + jBangOptions)
                 : List.of();
 
         createContainer(projectId, devmodeImage,
-                env, null, false, false, healthCheck,
+                env, null, false, List.of(), healthCheck,
                 Map.of(LABEL_TYPE, ContainerStatus.ContainerType.devmode.name(), LABEL_PROJECT_ID, projectId));
 
-        startContainer(projectId);
         LOGGER.infof("DevMode started for %s", projectId);
     }
 
+    public void createDevserviceContainer(DevService devService) throws InterruptedException {
+        LOGGER.infof("DevService starting for ", devService.getContainer_name());
+
+        HealthCheck healthCheck = getHealthCheck(devService.getHealthcheck());
+        List<String> env = devService.getEnvironment() != null ? devService.getEnvironmentList() : List.of();
+        String ports = String.join(",", devService.getPorts());
+
+        createContainer(devService.getContainer_name(), devService.getImage(),
+                env, ports, false, devService.getExpose(), healthCheck,
+                Map.of(LABEL_TYPE, ContainerStatus.ContainerType.devservice.name()));
+
+        LOGGER.infof("DevService started for %s", devService.getContainer_name());
+    }
+
     public void startInfinispan() {
         try {
             LOGGER.info("Infinispan is starting...");
@@ -126,12 +139,14 @@ public class DockerService {
             HealthCheck healthCheck = new HealthCheck().withTest(infinispanHealthCheckCMD)
                     .withInterval(10000000000L).withTimeout(10000000000L).withStartPeriod(10000000000L).withRetries(30);
 
+            List<String> exposedPorts = List.of(infinispanPort.split(":")[0]);
+
             createContainer(INFINISPAN_CONTAINER_NAME, infinispanImage,
                     List.of("USER=" + infinispanUsername, "PASS=" + infinispanPassword),
-                    infinispanPort, false, true, healthCheck,
+                    infinispanPort, false, exposedPorts, healthCheck,
                     Map.of(LABEL_TYPE, ContainerStatus.ContainerType.internal.name()));
 
-            startContainer(INFINISPAN_CONTAINER_NAME);
+            runContainer(INFINISPAN_CONTAINER_NAME);
             LOGGER.info("Infinispan is started");
         } catch (Exception e) {
             LOGGER.error(e.getMessage());
@@ -148,10 +163,10 @@ public class DockerService {
                             "INFINISPAN_USERNAME=" + infinispanUsername,
                             "INFINISPAN_PASSWORD=" + infinispanPassword
                     ),
-                    null, false, false, new HealthCheck(),
+                    null, false, List.of(), new HealthCheck(),
                     Map.of(LABEL_TYPE, ContainerStatus.ContainerType.internal.name()));
 
-            startContainer(KARAVAN_CONTAINER_NAME);
+            runContainer(KARAVAN_CONTAINER_NAME);
             LOGGER.info("Karavan headless is started");
         } catch (Exception e) {
             LOGGER.error(e.getMessage());
@@ -171,7 +186,8 @@ public class DockerService {
         List<ContainerStatus> result = new ArrayList<>();
         getDockerClient().listContainersCmd().withShowAll(true).exec().forEach(container -> {
             ContainerStatus containerStatus = getContainerStatus(container);
-            updateStatistics(containerStatus, container);
+            Statistics stats = getContainerStats(container.getId());
+            updateStatistics(containerStatus, container, stats);
             result.add(containerStatus);
         });
         return result;
@@ -186,16 +202,6 @@ public class DockerService {
         return ContainerStatus.createWithId(name, environment, container.getId(), container.getImage(), ports, type, commands, container.getState(), created);
     }
 
-    private void updateStatistics(ContainerStatus containerStatus, Container container) {
-        Statistics stats = getContainerStats(container.getId());
-        if (stats != null && stats.getMemoryStats() != null) {
-            String memoryUsage = formatMemory(stats.getMemoryStats().getUsage());
-            String memoryLimit = formatMemory(stats.getMemoryStats().getLimit());
-            containerStatus.setMemoryInfo(memoryUsage + " / " + memoryLimit);
-            containerStatus.setCpuInfo(formatCpu(containerStatus.getContainerName(), stats));
-        }
-    }
-
     public void startListeners() {
         getDockerClient().eventsCmd().exec(dockerEventListener);
     }
@@ -254,22 +260,23 @@ public class DockerService {
     }
 
     public Container createContainer(String name, String image, List<String> env, String ports, boolean inRange,
-                                     boolean exposedPort, HealthCheck healthCheck, Map<String, String> labels) throws InterruptedException {
+                                     List<String> exposed, HealthCheck healthCheck, Map<String, String> labels) throws InterruptedException {
         List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec();
         if (containers.size() == 0) {
             pullImage(image);
 
-            List<ExposedPort> exposedPorts = getPortsFromString(ports).values().stream().map(i -> ExposedPort.tcp(i)).collect(Collectors.toList());
-
-            CreateContainerResponse response = getDockerClient().createContainerCmd(image)
-                    .withName(name)
-                    .withLabels(labels)
-                    .withEnv(env)
-                    .withExposedPorts(exposedPorts)
-                    .withHostName(name)
-                    .withHostConfig(getHostConfig(ports, exposedPort, inRange))
-                    .withHealthcheck(healthCheck)
-                    .exec();
+            CreateContainerCmd createContainerCmd = getDockerClient().createContainerCmd(image)
+                    .withName(name).withLabels(labels).withEnv(env).withHostName(name).withHealthcheck(healthCheck);
+
+            if (exposed != null) {
+                List<ExposedPort> exposedPorts = exposed.stream().map(i -> ExposedPort.tcp(Integer.parseInt(i))).collect(Collectors.toList());
+                createContainerCmd.withExposedPorts(exposedPorts);
+                createContainerCmd.withHostConfig(getHostConfig(ports, exposedPorts, inRange, NETWORK_NAME));
+            } else {
+                createContainerCmd.withHostConfig(getHostConfig(ports, List.of(), inRange, NETWORK_NAME));
+            }
+
+            CreateContainerResponse response = createContainerCmd.exec();
             LOGGER.info("Container created: " + response.getId());
             return getDockerClient().listContainersCmd().withShowAll(true)
                     .withIdFilter(Collections.singleton(response.getId())).exec().get(0);
@@ -279,21 +286,18 @@ public class DockerService {
         }
     }
 
-    public void startContainer(String name) throws InterruptedException {
+    public void runContainer(String name) {
         List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec();
         if (containers.size() == 1) {
             Container container = containers.get(0);
-            if (!container.getState().equals("running")) {
+            if (container.getState().equals("paused")) {
+                getDockerClient().unpauseContainerCmd(container.getId()).exec();
+            } else if (!container.getState().equals("running")) {
                 getDockerClient().startContainerCmd(container.getId()).exec();
             }
         }
     }
 
-    public void restartContainer(String name) throws InterruptedException {
-        stopContainer(name);
-        startContainer(name);
-    }
-
     public void logContainer(String containerName, LogCallback callback) {
         try {
             Container container = getContainerByName(containerName);
@@ -312,6 +316,16 @@ public class DockerService {
         }
     }
 
+    public void pauseContainer(String name) {
+        List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec();
+        if (containers.size() == 1) {
+            Container container = containers.get(0);
+            if (container.getState().equals("running")) {
+                getDockerClient().pauseContainerCmd(container.getId()).exec();
+            }
+        }
+    }
+
     public void stopContainer(String name) {
         List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec();
         if (containers.size() == 1) {
@@ -342,31 +356,6 @@ public class DockerService {
         }
     }
 
-    private HostConfig getHostConfig(String ports, boolean exposedPort, boolean inRange) {
-        Ports portBindings = new Ports();
-
-        getPortsFromString(ports).forEach((hostPort, containerPort) -> {
-            Ports.Binding binding = exposedPort
-                    ? (inRange ? Ports.Binding.bindPortRange(hostPort, hostPort + 1000) : Ports.Binding.bindPort(hostPort))
-                    : Ports.Binding.bindPort(hostPort);
-            portBindings.bind(ExposedPort.tcp(containerPort), binding);
-        });
-        return new HostConfig()
-                .withPortBindings(portBindings)
-                .withNetworkMode(NETWORK_NAME);
-    }
-
-    private Map<Integer, Integer> getPortsFromString(String ports) {
-        Map<Integer, Integer> p = new HashMap<>();
-        if (ports != null && !ports.isEmpty()) {
-            Arrays.stream(ports.split(",")).forEach(s -> {
-                String[] values = s.split(":");
-                p.put(Integer.parseInt(values[0]), Integer.parseInt(values[1]));
-            });
-        }
-        return p;
-    }
-
     private DockerClientConfig getDockerClientConfig() {
         return DefaultDockerClientConfig.createDefaultConfigBuilder().build();
     }
@@ -387,80 +376,4 @@ public class DockerService {
         }
         return dockerClient;
     }
-
-    private String formatMemory(Long memory) {
-        try {
-            if (memory < (1073741824)) {
-                return formatMiB.format(memory.doubleValue() / 1048576) + "MiB";
-            } else {
-                return formatGiB.format(memory.doubleValue() / 1073741824) + "GiB";
-            }
-        } catch (Exception e) {
-            return "";
-        }
-    }
-
-    private ContainerStatus.ContainerType getContainerType(Map<String, String> labels) {
-        String type = labels.get(LABEL_TYPE);
-        if (Objects.equals(type, ContainerStatus.ContainerType.devmode.name())) {
-            return ContainerStatus.ContainerType.devmode;
-        } else if (Objects.equals(type, ContainerStatus.ContainerType.devservice.name())) {
-            return ContainerStatus.ContainerType.devservice;
-        } else if (Objects.equals(type, ContainerStatus.ContainerType.project.name())) {
-            return ContainerStatus.ContainerType.project;
-        } else if (Objects.equals(type, ContainerStatus.ContainerType.internal.name())) {
-            return ContainerStatus.ContainerType.internal;
-        }
-        return ContainerStatus.ContainerType.unknown;
-    }
-
-    private List<ContainerStatus.Command> getContainerCommand(String state) {
-        List<ContainerStatus.Command> result = new ArrayList<>();
-        if (Objects.equals(state, ContainerStatus.State.created.name())) {
-            result.add(ContainerStatus.Command.run);
-            result.add(ContainerStatus.Command.delete);
-        } else if (Objects.equals(state, ContainerStatus.State.exited.name())) {
-            result.add(ContainerStatus.Command.run);
-            result.add(ContainerStatus.Command.delete);
-        } else if (Objects.equals(state, ContainerStatus.State.running.name())) {
-            result.add(ContainerStatus.Command.pause);
-            result.add(ContainerStatus.Command.stop);
-            result.add(ContainerStatus.Command.delete);
-        } else if (Objects.equals(state, ContainerStatus.State.paused.name())) {
-            result.add(ContainerStatus.Command.run);
-            result.add(ContainerStatus.Command.stop);
-            result.add(ContainerStatus.Command.delete);
-        } else if (Objects.equals(state, ContainerStatus.State.dead.name())) {
-            result.add(ContainerStatus.Command.delete);
-        }
-        return result;
-    }
-
-    private String formatCpu(String containerName, Statistics stats) {
-        try {
-            double cpuUsage = 0;
-            long previousCpu = previousStats.containsKey(containerName) ? previousStats.get(containerName).getItem1() : -1;
-            long previousSystem = previousStats.containsKey(containerName) ? previousStats.get(containerName).getItem2() : -1;
-
-            CpuStatsConfig cpuStats = stats.getCpuStats();
-            if (cpuStats != null) {
-                CpuUsageConfig cpuUsageConfig = cpuStats.getCpuUsage();
-                long systemUsage = cpuStats.getSystemCpuUsage();
-                long totalUsage = cpuUsageConfig.getTotalUsage();
-
-                if (previousCpu != -1 && previousSystem != -1) {
-                    float cpuDelta = totalUsage - previousCpu;
-                    float systemDelta = systemUsage - previousSystem;
-
-                    if (cpuDelta > 0 && systemDelta > 0) {
-                        cpuUsage = cpuDelta / systemDelta * cpuStats.getOnlineCpus() * 100;
-                    }
-                }
-                previousStats.put(containerName, Tuple2.of(totalUsage, systemUsage));
-            }
-            return formatCpu.format(cpuUsage) + "%";
-        } catch (Exception e) {
-            return "";
-        }
-    }
 }
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
new file mode 100644
index 00000000..748c17bc
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java
@@ -0,0 +1,229 @@
+/*
+ * 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.docker;
+
+import com.github.dockerjava.api.model.*;
+import io.smallrye.mutiny.tuples.Tuple2;
+import io.vertx.core.json.JsonObject;
+import org.apache.camel.karavan.api.KameletResources;
+import org.apache.camel.karavan.docker.model.DevService;
+import org.apache.camel.karavan.docker.model.HealthCheckConfig;
+import org.apache.camel.karavan.infinispan.model.ContainerStatus;
+import org.apache.camel.karavan.service.CodeService;
+import org.yaml.snakeyaml.Yaml;
+
+import javax.inject.Inject;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.text.DecimalFormat;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static org.apache.camel.karavan.shared.Constants.LABEL_TYPE;
+
+public class DockerServiceUtils {
+
+    protected static final DecimalFormat formatCpu = new DecimalFormat("0.00");
+    protected static final DecimalFormat formatMiB = new DecimalFormat("0.0");
+    protected static final DecimalFormat formatGiB = new DecimalFormat("0.00");
+    protected static final Map<String, Tuple2<Long, Long>> previousStats = new ConcurrentHashMap<>();
+
+    @Inject
+    CodeService codeService;
+
+
+    protected ContainerStatus getContainerStatus(Container container, String environment) {
+        String name = container.getNames()[0].replace("/", "");
+        List<Integer> ports = Arrays.stream(container.getPorts()).map(ContainerPort::getPrivatePort).filter(Objects::nonNull).collect(Collectors.toList());
+        List<ContainerStatus.Command> commands = getContainerCommand(container.getState());
+        ContainerStatus.ContainerType type = getContainerType(container.getLabels());
+        String created = Instant.ofEpochSecond(container.getCreated()).toString();
+        return ContainerStatus.createWithId(name, environment, container.getId(), container.getImage(), ports, type, commands, container.getState(), created);
+    }
+
+    protected void updateStatistics(ContainerStatus containerStatus, Container container, Statistics stats) {
+        if (stats != null && stats.getMemoryStats() != null) {
+            String memoryUsage = formatMemory(stats.getMemoryStats().getUsage());
+            String memoryLimit = formatMemory(stats.getMemoryStats().getLimit());
+            containerStatus.setMemoryInfo(memoryUsage + " / " + memoryLimit);
+            containerStatus.setCpuInfo(formatCpu(containerStatus.getContainerName(), stats));
+        }
+    }
+
+    public DevService getDevService(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)) {
+            DevService ds = services.getJsonObject(name).mapTo(DevService.class);
+            if (ds.getContainer_name() == null) {
+                ds.setContainer_name(name);
+            }
+            return ds;
+        } else {
+            Optional<JsonObject> j = services.fieldNames().stream()
+                    .map(services::getJsonObject)
+                    .filter(s -> {
+                        s.getJsonObject("container_name");
+                        return false;
+                    }).findFirst();
+            if (j.isPresent()) {
+                return j.get().mapTo(DevService.class);
+            }
+        }
+        return null;
+    }
+
+    protected HealthCheck getHealthCheck(HealthCheckConfig config) {
+        if (config != null) {
+            HealthCheck healthCheck = new HealthCheck().withTest(config.getTest());
+            if (config.getInterval() != null) {
+                healthCheck.withInterval(convertDuration(config.getInterval()));
+            }
+            if (config.getTimeout() != null) {
+                healthCheck.withTimeout(convertDuration(config.getTimeout()));
+            }
+            if (config.getStart_period() != null) {
+                healthCheck.withStartPeriod(convertDuration(config.getStart_period()));
+            }
+            if (config.getRetries() != null) {
+                healthCheck.withRetries(config.getRetries());
+            }
+            return healthCheck;
+        }
+        return new HealthCheck();
+    }
+
+    protected Long convertDuration(String value) {
+        return Long.parseLong(value.replace("s", "")) * 1000000000L;
+    }
+
+    protected String getResourceFile(String path) {
+        try {
+            InputStream inputStream = KameletResources.class.getResourceAsStream(path);
+            return new BufferedReader(new InputStreamReader(inputStream))
+                    .lines().collect(Collectors.joining(System.getProperty("line.separator")));
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    protected HostConfig getHostConfig(String ports, List<ExposedPort> exposedPorts, boolean inRange, String network) {
+        Ports portBindings = new Ports();
+
+        getPortsFromString(ports).forEach((hostPort, containerPort) -> {
+            Ports.Binding binding = (exposedPorts.stream().anyMatch(e -> e.getPort() == containerPort))
+                    ? (inRange ? Ports.Binding.bindPortRange(hostPort, hostPort + 1000) : Ports.Binding.bindPort(hostPort))
+                    : Ports.Binding.bindPort(hostPort);
+            portBindings.bind(ExposedPort.tcp(containerPort), binding);
+        });
+        return new HostConfig()
+                .withPortBindings(portBindings)
+                .withNetworkMode(network);
+    }
+
+    protected Map<Integer, Integer> getPortsFromString(String ports) {
+        Map<Integer, Integer> p = new HashMap<>();
+        if (ports != null && !ports.isEmpty()) {
+            Arrays.stream(ports.split(",")).forEach(s -> {
+                String[] values = s.split(":");
+                p.put(Integer.parseInt(values[0]), Integer.parseInt(values[1]));
+            });
+        }
+        return p;
+    }
+
+    protected String formatMemory(Long memory) {
+        try {
+            if (memory < (1073741824)) {
+                return formatMiB.format(memory.doubleValue() / 1048576) + "MiB";
+            } else {
+                return formatGiB.format(memory.doubleValue() / 1073741824) + "GiB";
+            }
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+    protected ContainerStatus.ContainerType getContainerType(Map<String, String> labels) {
+        String type = labels.get(LABEL_TYPE);
+        if (Objects.equals(type, ContainerStatus.ContainerType.devmode.name())) {
+            return ContainerStatus.ContainerType.devmode;
+        } else if (Objects.equals(type, ContainerStatus.ContainerType.devservice.name())) {
+            return ContainerStatus.ContainerType.devservice;
+        } else if (Objects.equals(type, ContainerStatus.ContainerType.project.name())) {
+            return ContainerStatus.ContainerType.project;
+        } else if (Objects.equals(type, ContainerStatus.ContainerType.internal.name())) {
+            return ContainerStatus.ContainerType.internal;
+        }
+        return ContainerStatus.ContainerType.unknown;
+    }
+
+    protected List<ContainerStatus.Command> getContainerCommand(String state) {
+        List<ContainerStatus.Command> result = new ArrayList<>();
+        if (Objects.equals(state, ContainerStatus.State.created.name())) {
+            result.add(ContainerStatus.Command.run);
+            result.add(ContainerStatus.Command.delete);
+        } else if (Objects.equals(state, ContainerStatus.State.exited.name())) {
+            result.add(ContainerStatus.Command.run);
+            result.add(ContainerStatus.Command.delete);
+        } else if (Objects.equals(state, ContainerStatus.State.running.name())) {
+            result.add(ContainerStatus.Command.pause);
+            result.add(ContainerStatus.Command.stop);
+            result.add(ContainerStatus.Command.delete);
+        } else if (Objects.equals(state, ContainerStatus.State.paused.name())) {
+            result.add(ContainerStatus.Command.run);
+            result.add(ContainerStatus.Command.stop);
+            result.add(ContainerStatus.Command.delete);
+        } else if (Objects.equals(state, ContainerStatus.State.dead.name())) {
+            result.add(ContainerStatus.Command.delete);
+        }
+        return result;
+    }
+
+    protected String formatCpu(String containerName, Statistics stats) {
+        try {
+            double cpuUsage = 0;
+            long previousCpu = previousStats.containsKey(containerName) ? previousStats.get(containerName).getItem1() : -1;
+            long previousSystem = previousStats.containsKey(containerName) ? previousStats.get(containerName).getItem2() : -1;
+
+            CpuStatsConfig cpuStats = stats.getCpuStats();
+            if (cpuStats != null) {
+                CpuUsageConfig cpuUsageConfig = cpuStats.getCpuUsage();
+                long systemUsage = cpuStats.getSystemCpuUsage();
+                long totalUsage = cpuUsageConfig.getTotalUsage();
+
+                if (previousCpu != -1 && previousSystem != -1) {
+                    float cpuDelta = totalUsage - previousCpu;
+                    float systemDelta = systemUsage - previousSystem;
+
+                    if (cpuDelta > 0 && systemDelta > 0) {
+                        cpuUsage = cpuDelta / systemDelta * cpuStats.getOnlineCpus() * 100;
+                    }
+                }
+                previousStats.put(containerName, Tuple2.of(totalUsage, systemUsage));
+            }
+            return formatCpu.format(cpuUsage) + "%";
+        } catch (Exception e) {
+            return "";
+        }
+    }
+}
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DevService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DevService.java
new file mode 100644
index 00000000..2c0b4e30
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DevService.java
@@ -0,0 +1,103 @@
+package org.apache.camel.karavan.docker.model;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class DevService {
+
+    private String container_name;
+    private String image;
+    private String restart;
+    private List<String> ports;
+    private List<String> expose;
+    private String depends_on;
+    private Map<String,String> environment;
+    private HealthCheckConfig healthcheck;
+
+    public DevService() {
+    }
+
+    public String getContainer_name() {
+        return container_name;
+    }
+
+    public void setContainer_name(String container_name) {
+        this.container_name = container_name;
+    }
+
+    public String getImage() {
+        return image;
+    }
+
+    public void setImage(String image) {
+        this.image = image;
+    }
+
+    public String getRestart() {
+        return restart;
+    }
+
+    public void setRestart(String restart) {
+        this.restart = restart;
+    }
+
+    public List<String> getPorts() {
+        return ports;
+    }
+
+    public void setPorts(List<String> ports) {
+        this.ports = ports;
+    }
+
+    public List<String> getExpose() {
+        return expose;
+    }
+
+    public void setExpose(List<String> expose) {
+        this.expose = expose;
+    }
+
+    public String getDepends_on() {
+        return depends_on;
+    }
+
+    public void setDepends_on(String depends_on) {
+        this.depends_on = depends_on;
+    }
+
+    public Map<String, String> getEnvironment() {
+        return environment;
+    }
+
+    public List<String> getEnvironmentList() {
+        return environment.entrySet().stream()
+                .map(e -> e.getKey().concat("=").concat(e.getValue())).collect(Collectors.toList());
+    }
+
+    public void setEnvironment(Map<String, String> environment) {
+        this.environment = environment;
+    }
+
+    public HealthCheckConfig getHealthcheck() {
+        return healthcheck;
+    }
+
+    public void setHealthcheck(HealthCheckConfig healthcheck) {
+        this.healthcheck = healthcheck;
+    }
+
+    @Override
+    public String toString() {
+        return "DevService{" +
+                "container_name='" + container_name + '\'' +
+                ", image='" + image + '\'' +
+                ", restart='" + restart + '\'' +
+                ", ports=" + ports +
+                ", expose=" + expose +
+                ", depends_on='" + depends_on + '\'' +
+                ", environment=" + environment +
+                ", healthcheck=" + healthcheck +
+                '}';
+    }
+}
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/docker/model/HealthCheckConfig.java
new file mode 100644
index 00000000..46f766df
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/HealthCheckConfig.java
@@ -0,0 +1,55 @@
+package org.apache.camel.karavan.docker.model;
+
+import java.util.List;
+
+public class HealthCheckConfig {
+
+    private String interval;
+    private Integer retries;
+    private String timeout;
+    private String start_period;
+    private List<String> test;
+
+    public HealthCheckConfig() {
+    }
+
+    public String getInterval() {
+        return interval;
+    }
+
+    public void setInterval(String interval) {
+        this.interval = interval;
+    }
+
+    public Integer getRetries() {
+        return retries;
+    }
+
+    public void setRetries(Integer retries) {
+        this.retries = retries;
+    }
+
+    public String getTimeout() {
+        return timeout;
+    }
+
+    public void setTimeout(String timeout) {
+        this.timeout = timeout;
+    }
+
+    public List<String> getTest() {
+        return test;
+    }
+
+    public void setTest(List<String> test) {
+        this.test = test;
+    }
+
+    public String getStart_period() {
+        return start_period;
+    }
+
+    public void setStart_period(String start_period) {
+        this.start_period = start_period;
+    }
+}
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/service/CodeService.java
index a23d93d0..73505065 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/service/CodeService.java
@@ -51,6 +51,7 @@ public class CodeService {
 
     private static final Logger LOGGER = Logger.getLogger(CodeService.class.getName());
     public static final String APPLICATION_PROPERTIES_FILENAME = "application.properties";
+    public static final String DEV_SERVICES_FILENAME = "dev-services.yaml";
 
     @Inject
     KubernetesService kubernetesService;
@@ -133,8 +134,8 @@ public class CodeService {
 
     public Map<String, String> getServices() {
         Map<String, String> result = new HashMap<>();
-        String templateText = getResourceFile("/services/dev-services.yaml");
-        result.put("dev-services.yaml", templateText);
+        String templateText = getResourceFile("/services/" + DEV_SERVICES_FILENAME);
+        result.put(DEV_SERVICES_FILENAME, templateText);
         return result;
     }
 
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 7c05cea0..93ec231f 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
@@ -16,8 +16,6 @@
  */
 package org.apache.camel.karavan.service;
 
-import io.quarkus.scheduler.Scheduled;
-import io.quarkus.vertx.ConsumeEvent;
 import io.smallrye.mutiny.tuples.Tuple2;
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.GitRepo;
@@ -37,9 +35,10 @@ import javax.inject.Inject;
 import java.time.Instant;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-import static org.apache.camel.karavan.shared.EventType.IMPORT_PROJECTS;
+import static org.apache.camel.karavan.service.CodeService.DEV_SERVICES_FILENAME;
 
 @Default
 @Readiness
@@ -263,4 +262,10 @@ public class ProjectService implements HealthCheck{
             LOGGER.error("Error during pipelines project creation", e);
         }
     }
+
+    public String getDevServiceCode() {
+        List <ProjectFile> files = infinispanService.getProjectFiles(Project.Type.services.name());
+        Optional<ProjectFile> file = files.stream().filter(f -> f.getName().equals(DEV_SERVICES_FILENAME)).findFirst();
+        return file.orElse(new ProjectFile()).getCode();
+    }
 }
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 16fcbeb6..a4ba7878 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
@@ -62,10 +62,13 @@ public class ScheduledService {
             List<String> namesInDocker = statusesInDocker.stream().map(ContainerStatus::getContainerName).collect(Collectors.toList());
             List<ContainerStatus> statusesInInfinispan = infinispanService.getContainerStatuses(environment);
             // clean deleted
-            statusesInInfinispan.stream().filter(cs -> !namesInDocker.contains(cs.getContainerName())).forEach(containerStatus -> {
-                infinispanService.deleteContainerStatus(containerStatus);
-                infinispanService.deleteCamelStatuses(containerStatus.getProjectId(), containerStatus.getEnv());
-            });
+            statusesInInfinispan.stream()
+                    .filter(cs -> !(cs.getContainerId() == null && cs.getInTransit()))
+                    .filter(cs -> !namesInDocker.contains(cs.getContainerName()))
+                    .forEach(containerStatus -> {
+                        infinispanService.deleteContainerStatus(containerStatus);
+                        infinispanService.deleteCamelStatuses(containerStatus.getProjectId(), containerStatus.getEnv());
+                    });
             // send statuses to save
             statusesInDocker.forEach(containerStatus -> {
                 eventBus.send(EventType.CONTAINER_STATUS, JsonObject.mapFrom(containerStatus));
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 fc0ed2e8..4c94484a 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
@@ -463,8 +463,8 @@ export class KaravanApi {
         });
     }
 
-    static async manageContainer(environment: string, name: string, command: 'run' | 'pause' | 'stop', after: (res: AxiosResponse<any>) => void) {
-        instance.post('/api/infrastructure/container/' + environment + '/' + name, {command: command})
+    static async manageContainer(environment: string, type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown',  name: string, command: 'run' | 'pause' | 'stop', after: (res: AxiosResponse<any>) => void) {
+        instance.post('/api/infrastructure/container/' + environment + '/' + type + "/" + name, {command: command})
             .then(res => {
                 after(res);
             }).catch(err => {
@@ -472,8 +472,8 @@ export class KaravanApi {
         });
     }
 
-    static async deleteContainer(environment: string, name: string, after: (res: AxiosResponse<any>) => void) {
-        instance.delete('/api/infrastructure/container/' + environment + '/' + name)
+    static async deleteContainer(environment: string, type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown', name: string, after: (res: AxiosResponse<any>) => void) {
+        instance.delete('/api/infrastructure/container/' + environment + '/' + type + "/" + name)
             .then(res => {
                 after(res);
             }).catch(err => {
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 3a4ced02..82a89413 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
@@ -75,7 +75,7 @@ export class ContainerStatus {
     deployment: string = '';
     projectId: string = '';
     env: string = '';
-    type: string = '';
+    type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown' = 'unknown';
     memoryInfo: string = '';
     cpuInfo: string = '';
     created: string = '';
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
index 8b043d61..cdfada6f 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
@@ -1,7 +1,7 @@
-import {KaravanApi} from "./KaravanApi";
-import {DeploymentStatus, ContainerStatus, Project, ProjectFile, ToastMessage} from "./ProjectModels";
-import {TemplateApi} from "karavan-core/lib/api/TemplateApi";
-import {InfrastructureAPI} from "../designer/utils/InfrastructureAPI";
+import {KaravanApi} from './KaravanApi';
+import {DeploymentStatus, ContainerStatus, Project, ProjectFile, ToastMessage} from './ProjectModels';
+import {TemplateApi} from 'karavan-core/lib/api/TemplateApi';
+import {InfrastructureAPI} from '../designer/utils/InfrastructureAPI';
 import {unstable_batchedUpdates} from 'react-dom'
 import {
     useFilesStore,
@@ -9,17 +9,17 @@ import {
     useFileStore, useLogStore,
     useProjectsStore,
     useProjectStore, useDevModeStore
-} from "./ProjectStore";
-import {ProjectEventBus} from "./ProjectEventBus";
+} from './ProjectStore';
+import {ProjectEventBus} from './ProjectEventBus';
 
 export class ProjectService {
 
     public static startDevModeContainer(project: Project, verbose: boolean) {
-        useDevModeStore.setState({status: "wip"})
+        useDevModeStore.setState({status: 'wip'})
         KaravanApi.startDevModeContainer(project, verbose, res => {
-            useDevModeStore.setState({status: "none"})
+            useDevModeStore.setState({status: 'none'})
             if (res.status === 200 || res.status === 201) {
-                ProjectEventBus.sendLog("set", '');
+                ProjectEventBus.sendLog('set', '');
                 useLogStore.setState({showLog: true, type: 'container', podName: res.data})
             } else {
                 // Todo notification
@@ -28,9 +28,9 @@ export class ProjectService {
     }
 
     public static reloadDevModeCode(project: Project) {
-        useDevModeStore.setState({status: "wip"})
+        useDevModeStore.setState({status: 'wip'})
         KaravanApi.reloadDevModeCode(project.projectId, res => {
-            useDevModeStore.setState({status: "none"})
+            useDevModeStore.setState({status: 'none'})
             if (res.status === 200 || res.status === 201) {
                 // setIsReloadingPod(false);
             } else {
@@ -41,38 +41,38 @@ export class ProjectService {
     }
 
     public static stopDevModeContainer(project: Project) {
-        useDevModeStore.setState({status: "wip"})
-        KaravanApi.manageContainer("dev", project.projectId, 'stop', res => {
-            useDevModeStore.setState({status: "none"})
+        useDevModeStore.setState({status: 'wip'})
+        KaravanApi.manageContainer('dev', 'devmove', project.projectId, 'stop', res => {
+            useDevModeStore.setState({status: 'none'})
             if (res.status === 200) {
                 useLogStore.setState({showLog: false, type: 'container', isRunning: false})
             } else {
-                ProjectEventBus.sendAlert(new ToastMessage("Error stopping DevMode container", res.statusText, 'warning'))
+                ProjectEventBus.sendAlert(new ToastMessage('Error stopping DevMode container', res.statusText, 'warning'))
             }
         });
     }
 
     public static pauseDevModeContainer(project: Project) {
-        useDevModeStore.setState({status: "wip"})
-        KaravanApi.manageContainer("dev", project.projectId, 'pause', res => {
-            useDevModeStore.setState({status: "none"})
+        useDevModeStore.setState({status: 'wip'})
+        KaravanApi.manageContainer('dev', 'devmove', project.projectId, 'pause', res => {
+            useDevModeStore.setState({status: 'none'})
             if (res.status === 200) {
                 useLogStore.setState({showLog: false, type: 'container', isRunning: false})
             } else {
-                ProjectEventBus.sendAlert(new ToastMessage("Error stopping DevMode container", res.statusText, 'warning'))
+                ProjectEventBus.sendAlert(new ToastMessage('Error stopping DevMode container', res.statusText, 'warning'))
             }
         });
     }
 
     public static deleteDevModeContainer(project: Project) {
-        useDevModeStore.setState({status: "wip"})
-        ProjectEventBus.sendLog("set", '');
+        useDevModeStore.setState({status: 'wip'})
+        ProjectEventBus.sendLog('set', '');
         KaravanApi.deleteDevModeContainer(project.projectId, false, res => {
-            useDevModeStore.setState({status: "none"})
+            useDevModeStore.setState({status: 'none'})
             if (res.status === 202) {
                 useLogStore.setState({showLog: false, type: 'container', isRunning: false})
             } else {
-                ProjectEventBus.sendAlert(new ToastMessage("Error delete runner", res.statusText, 'warning'))
+                ProjectEventBus.sendAlert(new ToastMessage('Error delete runner', res.statusText, 'warning'))
             }
         });
     }
@@ -86,14 +86,14 @@ export class ProjectService {
                     if (useDevModeStore.getState().podName !== containerStatus.containerName){
                         useDevModeStore.setState({podName: containerStatus.containerName})
                     }
-                    if (useDevModeStore.getState().status !== "wip"){
+                    if (useDevModeStore.getState().status !== 'wip'){
                         useLogStore.setState({isRunning: true})
                     }
                     useProjectStore.setState({containerStatus: containerStatus});
                 })
             } else {
                 unstable_batchedUpdates(() => {
-                    useDevModeStore.setState({status: "none", podName: undefined})
+                    useDevModeStore.setState({status: 'none', podName: undefined})
                     useProjectStore.setState({containerStatus: new ContainerStatus({})});
                 })
             }
@@ -103,8 +103,8 @@ export class ProjectService {
     public static pushProject(project: Project, commitMessage: string) {
         useProjectStore.setState({isPushing: true})
         const params = {
-            "projectId": project.projectId,
-            "message": commitMessage
+            'projectId': project.projectId,
+            'message': commitMessage
         };
         KaravanApi.push(params, res => {
             if (res.status === 200 || res.status === 201) {
@@ -167,10 +167,10 @@ export class ProjectService {
     public static deleteProject(project: Project) {
         KaravanApi.deleteProject(project, res => {
             if (res.status === 204) {
-                // this.props.toast?.call(this, "Success", "Project deleted", "success");
+                // this.props.toast?.call(this, 'Success', 'Project deleted', 'success');
                 ProjectService.refreshProjectData();
             } else {
-                // this.props.toast?.call(this, "Error", res.statusText, "danger");
+                // this.props.toast?.call(this, 'Error', res.statusText, 'danger');
             }
         });
     }
@@ -179,9 +179,9 @@ export class ProjectService {
         KaravanApi.postProject(project, res => {
             if (res.status === 200 || res.status === 201) {
                 ProjectService.refreshProjectData();
-                // this.props.toast?.call(this, "Success", "Project created", "success");
+                // this.props.toast?.call(this, 'Success', 'Project created', 'success');
             } else {
-                // this.props.toast?.call(this, "Error", res.status + ", " + res.statusText, "danger");
+                // this.props.toast?.call(this, 'Error', res.status + ', ' + res.statusText, 'danger');
             }
         });
     }
@@ -211,10 +211,10 @@ export class ProjectService {
         KaravanApi.getProject(project.projectId, (project: Project) => {
             // ProjectEventBus.selectProject(project);
             KaravanApi.getTemplatesFiles((files: ProjectFile[]) => {
-                files.filter(f => f.name.endsWith("java"))
+                files.filter(f => f.name.endsWith('java'))
                     .filter(f => f.name.startsWith(project.runtime))
                     .forEach(f => {
-                        const name = f.name.replace(project.runtime + "-", '').replace(".java", '');
+                        const name = f.name.replace(project.runtime + '-', '').replace('.java', '');
                         TemplateApi.saveTemplate(name, f.code);
                     })
             });
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ServiceModels.ts b/karavan-web/karavan-app/src/main/webui/src/api/ServiceModels.ts
index a24f473d..506301d7 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ServiceModels.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ServiceModels.ts
@@ -11,8 +11,7 @@ export class Healthcheck {
     }
 }
 
-export class Service {
-    name: string = '';
+export class DevService {
     container_name: string = '';
     image: string = '';
     restart: string = '';
@@ -21,14 +20,14 @@ export class Service {
     environment: any = {};
     healthcheck?: Healthcheck;
 
-    public constructor(init?: Partial<Service>) {
+    public constructor(init?: Partial<DevService>) {
         Object.assign(this, init);
     }
 }
 
 export class Services {
     version: string = '';
-    services: Service[] = [];
+    services: DevService[] = [];
 
     public constructor(init?: Partial<Services>) {
         Object.assign(this, init);
@@ -43,8 +42,10 @@ export class ServicesYaml {
         const result: Services = new Services({version: fromYaml.version});
         Object.keys(fromYaml.services).forEach(key => {
             const o = fromYaml.services[key];
-            const service = new Service(o);
-            service.name = key;
+            const service = new DevService(o);
+            if (!service.container_name) {
+                service.container_name = key;
+            }
             result.services.push(service);
         })
         return result;
diff --git a/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx b/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx
index 13d702d2..0e2a1091 100644
--- a/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx
@@ -59,15 +59,15 @@ export const ContainerTableRow = (props: Props) => {
                     {!inTransit && <Label color={color}>{container.state}</Label>}
                     {inTransit && <Spinner isSVG size="lg" aria-label="spinner"/>}
                 </Td>
-                <Td className="project-action-buttons">
+                <Td>
                     {container.type !== 'internal' &&
-                        <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexEnd"}}
+                        <Flex direction={{default: "row"}} flexWrap={{default: "nowrap"}}
                               spaceItems={{default: 'spaceItemsNone'}}>
                             <FlexItem>
                                 <Tooltip content={"Start container"} position={"bottom"}>
                                     <Button variant={"plain"} icon={<PlayIcon/>} isDisabled={!commands.includes('run') || inTransit}
                                             onClick={e => {
-                                                KaravanApi.manageContainer(container.env, container.containerName, 'run', res => {});
+                                                KaravanApi.manageContainer(container.env, container.type, container.containerName, 'run', res => {});
                                             }}></Button>
                                 </Tooltip>
                             </FlexItem>
@@ -75,7 +75,7 @@ export const ContainerTableRow = (props: Props) => {
                                 <Tooltip content={"Pause container"} position={"bottom"}>
                                     <Button variant={"plain"} icon={<PauseIcon/>} isDisabled={!commands.includes('pause') || inTransit}
                                             onClick={e => {
-                                                KaravanApi.manageContainer(container.env, container.containerName, 'pause', res => {});
+                                                KaravanApi.manageContainer(container.env, container.type, container.containerName, 'pause', res => {});
                                             }}></Button>
                                 </Tooltip>
                             </FlexItem>
@@ -83,7 +83,7 @@ export const ContainerTableRow = (props: Props) => {
                                 <Tooltip content={"Stop container"} position={"bottom"}>
                                     <Button variant={"plain"} icon={<StopIcon/>} isDisabled={!commands.includes('stop') || inTransit}
                                             onClick={e => {
-                                                KaravanApi.manageContainer(container.env, container.containerName, 'stop', res => {});
+                                                KaravanApi.manageContainer(container.env, container.type, container.containerName, 'stop', res => {});
                                             }}></Button>
                                 </Tooltip>
                             </FlexItem>
@@ -91,7 +91,7 @@ export const ContainerTableRow = (props: Props) => {
                                 <Tooltip content={"Delete container"} position={"bottom"}>
                                     <Button variant={"plain"} icon={<DeleteIcon/>} isDisabled={!commands.includes('delete') || inTransit}
                                             onClick={e => {
-                                                KaravanApi.deleteContainer(container.env, container.containerName, res => {});
+                                                KaravanApi.deleteContainer(container.env, container.type, container.containerName, res => {});
                                             }}></Button>
                                 </Tooltip>
                             </FlexItem>
diff --git a/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx b/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx
index 9d1f6607..08539413 100644
--- a/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx
@@ -1,32 +1,24 @@
 import React, {useEffect, useState} from 'react';
 import {
-    Badge, Bullseye,
+    Bullseye,
     Button, EmptyState, EmptyStateIcon, EmptyStateVariant,
-    Flex,
-    FlexItem, HelperText, HelperTextItem, Label, LabelGroup,
     PageSection, Spinner,
     Text,
     TextContent,
     TextInput, Title, ToggleGroup, ToggleGroupItem,
     Toolbar,
     ToolbarContent,
-    ToolbarItem, Tooltip
+    ToolbarItem
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
-import {CamelStatus, ContainerStatus, DeploymentStatus, Project, ServiceStatus} from "../api/ProjectModels";
+import {ContainerStatus} from "../api/ProjectModels";
 import {TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
-import {camelIcon, CamelUi} from "../designer/utils/CamelUi";
 import {KaravanApi} from "../api/KaravanApi";
-import Icon from "../Logo";
-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 RefreshIcon from "@patternfly/react-icons/dist/esm/icons/sync-alt-icon";
 import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon";
 import {MainToolbar} from "../designer/MainToolbar";
-import {useAppConfigStore, useProjectsStore, useStatusesStore} from "../api/ProjectStore";
+import {useAppConfigStore, useStatusesStore} from "../api/ProjectStore";
 import {shallow} from "zustand/shallow";
-import {Service} from "../api/ServiceModels";
-import {ServicesTableRow} from "../services/ServicesTableRow";
 import {ContainerTableRow} from "./ContainerTableRow";
 
 export const ContainersPage = () => {
@@ -39,19 +31,17 @@ export const ContainersPage = () => {
 
     useEffect(() => {
         const interval = setInterval(() => {
-            onGetProjects()
+            updateContainerStatuses()
         }, 700);
         return () => {
             clearInterval(interval)
         };
     }, []);
 
-    function onGetProjects() {
-        KaravanApi.getConfiguration((config: any) => {
-            KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => {
-                setContainers(statuses);
-                setLoading(false);
-            });
+    function updateContainerStatuses() {
+        KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => {
+            setContainers(statuses);
+            setLoading(false);
         });
     }
 
@@ -72,7 +62,7 @@ export const ContainersPage = () => {
         return (<Toolbar id="toolbar-group-types">
             <ToolbarContent>
                 <ToolbarItem>
-                    <Button variant="link" icon={<RefreshIcon/>} onClick={e => onGetProjects()}/>
+                    <Button variant="link" icon={<RefreshIcon/>} onClick={e => updateContainerStatuses()}/>
                 </ToolbarItem>
                 <ToolbarItem>
                     <ToggleGroup aria-label="Default with single selectable">
@@ -148,7 +138,7 @@ export const ContainersPage = () => {
                             <Th key='cpuInfo'>CPU</Th>
                             <Th key='memoryInfo'>Memory</Th>
                             <Th key='state'>State</Th>
-                            <Th key='action'></Th>
+                            <Th  key='action'></Th>
                         </Tr>
                     </Thead>
                     {conts?.map((container: ContainerStatus, index: number) => (
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx b/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
index add844b9..14dc1eaa 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
@@ -84,7 +84,7 @@ export class ProjectStatus extends React.Component<Props, State> {
                 });
                 break;
             case "pod":
-                KaravanApi.deleteContainer(environment, name, (res: any) => {
+                KaravanApi.deleteContainer(environment, 'project', name, (res: any) => {
                     // if (Array.isArray(res) && Array.from(res).length > 0)
                     // this.onRefresh();
                 });
diff --git a/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx b/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
index 54a101be..87606ac9 100644
--- a/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
@@ -3,7 +3,6 @@ import {
     Toolbar,
     ToolbarContent,
     ToolbarItem,
-    TextInput,
     PageSection,
     TextContent,
     Text,
@@ -17,28 +16,43 @@ import {
 import '../designer/karavan.css';
 import RefreshIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
 import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon';
-import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import {TableComposable, Td, Th, Thead, Tr} from "@patternfly/react-table";
 import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
 import {ServicesTableRow} from "./ServicesTableRow";
 import {DeleteServiceModal} from "./DeleteServiceModal";
 import {CreateServiceModal} from "./CreateServiceModal";
-import {useProjectStore} from "../api/ProjectStore";
+import {useProjectStore, useStatusesStore} from "../api/ProjectStore";
 import {MainToolbar} from "../designer/MainToolbar";
-import {Project, ProjectFile, ProjectType} from "../api/ProjectModels";
+import {ContainerStatus, Project, ProjectType} from "../api/ProjectModels";
 import {KaravanApi} from "../api/KaravanApi";
-import {Service, Services, ServicesYaml} from "../api/ServiceModels";
+import {DevService, Services, ServicesYaml} from "../api/ServiceModels";
+import {shallow} from "zustand/shallow";
 
 
 export const ServicesPage = () => {
 
     const [services, setServices] = useState<Services>();
+    const [containers, setContainers] = useStatusesStore((state) => [state.containers, state.setContainers], shallow);
     const [operation, setOperation] = useState<'create' | 'delete' | 'none'>('none');
     const [loading, setLoading] = useState<boolean>(false);
 
     useEffect(() => {
         getServices();
+        const interval = setInterval(() => {
+            updateContainerStatuses()
+        }, 700);
+        return () => {
+            clearInterval(interval)
+        };
     }, []);
 
+    function updateContainerStatuses() {
+        KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => {
+            setContainers(statuses);
+            setLoading(false);
+        });
+    }
+
     function getServices() {
         KaravanApi.getFiles(ProjectType.services, files => {
             const file = files.at(0);
@@ -92,6 +106,10 @@ export const ServicesPage = () => {
         )
     }
 
+    function getContainer(name: string) {
+        return containers.filter(c => c.containerName === name).at(0);
+    }
+
     function getServicesTable() {
         return (
             <TableComposable aria-label="Services" variant={"compact"}>
@@ -102,11 +120,12 @@ export const ServicesPage = () => {
                         <Th key='container_name'>Container Name</Th>
                         <Th key='image'>Image</Th>
                         <Th key='ports'>Ports</Th>
+                        <Th key='state'>State</Th>
                         <Th key='action'></Th>
                     </Tr>
                 </Thead>
-                {services?.services.map((service: Service, index: number) => (
-                    <ServicesTableRow key={service.name} index={index} service={service}/>
+                {services?.services.map((service: DevService, index: number) => (
+                    <ServicesTableRow key={service.container_name} index={index} service={service} container={getContainer(service.container_name)}/>
                 ))}
                 {services?.services.length === 0 && getEmptyState()}
             </TableComposable>
diff --git a/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx b/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx
index f6affafe..fab5456d 100644
--- a/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx
@@ -2,35 +2,90 @@ import React, {useState} from 'react';
 import {
     Button,
     Tooltip,
-    Flex, FlexItem, Label
+    Flex, FlexItem, Label, ToolbarContent, Toolbar, ToolbarItem, Spinner
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
-import {ExpandableRowContent, Tbody, Td, Tr} from "@patternfly/react-table";
+import {ActionsColumn, ExpandableRowContent, Tbody, Td, Tr} from "@patternfly/react-table";
 import StopIcon from "@patternfly/react-icons/dist/js/icons/stop-icon";
 import PlayIcon from "@patternfly/react-icons/dist/esm/icons/play-icon";
-import {Service} from "../api/ServiceModels";
+import {DevService} from "../api/ServiceModels";
+import {ContainerStatus} from "../api/ProjectModels";
+import PauseIcon from "@patternfly/react-icons/dist/esm/icons/pause-icon";
+import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
+import {useAppConfigStore} from "../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {KaravanApi} from "../api/KaravanApi";
 
 interface Props {
     index: number
-    service: Service
+    service: DevService
+    container?: ContainerStatus
 }
 
 export const ServicesTableRow = (props: Props) => {
 
+    const [config] = useAppConfigStore((state) => [state.config], shallow)
     const [isExpanded, setIsExpanded] = useState<boolean>(false);
-    const [running, setRunning] = useState<boolean>(false);
+
+
+    function getButtons() {
+        const container = props.container;
+        const commands = container?.commands || ['run'];
+        const inTransit = container?.inTransit;
+        return (
+            <Td noPadding className="project-action-buttons">
+                <Flex direction={{default: "row"}} flexWrap={{default: "nowrap"}}
+                      spaceItems={{default: 'spaceItemsNone'}}>
+                    <FlexItem>
+                        <Tooltip content={"Start container"} position={"bottom"}>
+                            <Button variant={"plain"} icon={<PlayIcon/>} isDisabled={!commands.includes('run') || inTransit}
+                                    onClick={e => {
+                                        KaravanApi.manageContainer(config.environment, 'devservice', service.container_name, 'run', res => {});
+                                    }}></Button>
+                        </Tooltip>
+                    </FlexItem>
+                    <FlexItem>
+                        <Tooltip content={"Pause container"} position={"bottom"}>
+                            <Button variant={"plain"} icon={<PauseIcon/>} isDisabled={!commands.includes('pause') || inTransit}
+                                    onClick={e => {
+                                        // KaravanApi.manageContainer(container.env, container.containerName, 'pause', res => {});
+                                    }}></Button>
+                        </Tooltip>
+                    </FlexItem>
+                    <FlexItem>
+                        <Tooltip content={"Stop container"} position={"bottom"}>
+                            <Button variant={"plain"} icon={<StopIcon/>} isDisabled={!commands.includes('stop') || inTransit}
+                                    onClick={e => {
+                                        KaravanApi.manageContainer(config.environment, 'devservice', service.container_name, 'stop', res => {});
+                                    }}></Button>
+                        </Tooltip>
+                    </FlexItem>
+                    <FlexItem>
+                        <Tooltip content={"Delete container"} position={"bottom"}>
+                            <Button variant={"plain"} icon={<DeleteIcon/>} isDisabled={!commands.includes('delete') || inTransit}
+                                    onClick={e => {
+                                        KaravanApi.deleteContainer(config.environment, 'devservice', service.container_name, res => {});
+                                    }}></Button>
+                        </Tooltip>
+                    </FlexItem>
+                </Flex>
+            </Td>
+        )
+    }
 
     const service = props.service;
     const healthcheck = service.healthcheck;
     const env = service.environment;
     const keys = Object.keys(env);
-    const icon = running ? <StopIcon/> : <PlayIcon/>;
-    const tooltip = running ? "Stop container" : "Start container";
+    const container = props.container;
+    const isRunning = container?.state === 'running';
+    const inTransit = container?.inTransit;
+    const color = container?.state === 'running' ? "green" : "grey";
     return (
         <Tbody isExpanded={isExpanded}>
-            <Tr key={service.name}>
+            <Tr key={service.container_name}>
                 <Td expand={
-                    service.name
+                    service.container_name
                         ? {
                             rowIndex: props.index,
                             isExpanded: isExpanded,
@@ -41,7 +96,7 @@ export const ServicesTableRow = (props: Props) => {
                     modifier={"fitContent"}>
                 </Td>
                 <Td>
-                    <Label color={"grey"}>{service.name}</Label>
+                    <Label color={color}>{service.container_name}</Label>
                 </Td>
                 <Td>{service.container_name}</Td>
                 <Td>{service.image}</Td>
@@ -50,19 +105,11 @@ export const ServicesTableRow = (props: Props) => {
                         {service.ports.map(port => <FlexItem key={port}>{port}</FlexItem>)}
                     </Flex>
                 </Td>
-                {/*<Td>{service.environment}</Td>*/}
-                <Td className="project-action-buttons">
-                    <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexEnd"}}
-                          spaceItems={{default: 'spaceItemsNone'}}>
-                        <FlexItem>
-                            <Tooltip content={tooltip} position={"bottom"}>
-                                <Button variant={"plain"} icon={icon} onClick={e => {
-                                    // setProject(project, "delete");
-                                }}></Button>
-                            </Tooltip>
-                        </FlexItem>
-                    </Flex>
+                <Td>
+                    {!inTransit && container?.state && <Label color={color}>{container?.state}</Label>}
+                    {inTransit && <Spinner isSVG size="lg" aria-label="spinner"/>}
                 </Td>
+                {getButtons()}
             </Tr>
             {keys.length > 0 && <Tr isExpanded={isExpanded}>
                 <Td></Td>
diff --git a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java
index ac905f64..839de24c 100644
--- a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java
+++ b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java
@@ -276,12 +276,6 @@ public class InfinispanService {
                 .execute().list();
     }
 
-    public void setContainerStatusTransit(String projectId, String env, String containerName) {
-        ContainerStatus cs = getContainerStatus(projectId, env, containerName);
-        cs.setInTransit(true);
-        saveContainerStatus(cs);
-    }
-
     public ContainerStatus getContainerStatus(String projectId, String env, String containerName) {
         return containerStatuses.get(GroupedKey.create(projectId, env, containerName));
     }
diff --git a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
index ef7ca779..c9336b8e 100644
--- a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
+++ b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
@@ -105,6 +105,10 @@ public class ContainerStatus {
         return new ContainerStatus(projectId, projectId, null, null, null, env, ContainerType.devmode, null, null, null, List.of(Command.run), null, false, false);
     }
 
+    public static ContainerStatus createByType(String name, String env, ContainerType type) {
+        return new ContainerStatus(name, name, null, null, null, env, type, null, null, null, List.of(Command.run), null, false, false);
+    }
+
     public static ContainerStatus createWithId(String name, String env, String containerId, String image, List<Integer> ports, ContainerType type, List<Command> commands, String status, String created) {
         return new ContainerStatus(name, name, containerId, image, ports, env, type,
                 null, null, created,  commands, status, false, false);


[camel-karavan] 03/03: devservices logs #817

Posted by ma...@apache.org.
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 17084c371fd0d0523463f6b69e88b0a218134942
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Thu Jul 27 18:22:36 2023 -0400

    devservices logs #817
---
 .../karavan-app/src/main/webui/src/Main.tsx        | 41 +++++++++++-------
 .../src/main/webui/src/api/ProjectService.ts       | 50 +++++++++++-----------
 .../src/main/webui/src/api/ProjectStore.ts         |  8 ----
 .../main/webui/src/containers/ContainersPage.tsx   | 29 ++-----------
 .../karavan-app/src/main/webui/src/index.css       | 16 +++----
 .../src/main/webui/src/project/DevModeToolbar.tsx  | 19 ++++----
 .../src/main/webui/src/project/ProjectPage.tsx     | 19 ++++----
 .../webui/src/project/dashboard/DashboardTab.tsx   | 19 ++++----
 .../main/webui/src/project/log/ProjectLogPanel.tsx | 14 +++---
 .../webui/src/project/pipeline/ProjectStatus.tsx   |  5 +--
 .../src/main/webui/src/services/ServicesPage.tsx   | 23 +++-------
 .../main/webui/src/services/ServicesTableRow.tsx   | 21 +++++++--
 12 files changed, 125 insertions(+), 139 deletions(-)

diff --git a/karavan-web/karavan-app/src/main/webui/src/Main.tsx b/karavan-web/karavan-app/src/main/webui/src/Main.tsx
index 8adfe79d..98ca08f1 100644
--- a/karavan-web/karavan-app/src/main/webui/src/Main.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/Main.tsx
@@ -28,12 +28,13 @@ import {ContainersPage} from "./containers/ContainersPage";
 import {ProjectEventBus} from "./api/ProjectEventBus";
 import {AppConfig, ContainerStatus, Project, ToastMessage} from "./api/ProjectModels";
 import {ProjectPage} from "./project/ProjectPage";
-import {useAppConfigStore, useDevModeStore, useFileStore, useProjectStore} from "./api/ProjectStore";
+import {useAppConfigStore, useDevModeStore, useFileStore, useProjectStore, useStatusesStore} from "./api/ProjectStore";
 import {Notification} from "./Notification";
 import {InfrastructureAPI} from "./designer/utils/InfrastructureAPI";
 import {KnowledgebasePage} from "./knowledgebase/KnowledgebasePage";
 import {ServicesPage} from "./services/ServicesPage";
 import {shallow} from "zustand/shallow";
+import {ProjectService} from "./api/ProjectService";
 
 class MenuItem {
     pageId: string = '';
@@ -50,12 +51,16 @@ class MenuItem {
 export const Main = () => {
 
     const [config, setConfig] = useAppConfigStore((state) => [state.config, state.setConfig], shallow)
+    const [setContainers] = useStatusesStore((state) => [state.setContainers], shallow);
     const [pageId, setPageId] = useState<string>('projects');
     const [request, setRequest] = useState<string>(uuidv4());
     const [showUser, setShowUser] = useState<boolean>(false);
 
     useEffect(() => {
         console.log("Main Start");
+        const interval = setInterval(() => {
+            getStatuses();
+        }, 1000);
         const sub = ProjectEventBus.onSelectProject()?.subscribe((project: Project | undefined) => {
             if (project) setPageId("project");
         });
@@ -64,17 +69,15 @@ export const Main = () => {
             if (authType === 'oidc') {
                 SsoApi.auth(() => {
                     KaravanApi.getMe((user: any) => {
-                        console.log("me", user);
                         getData();
                     });
                 });
             }
-            if (KaravanApi.isAuthorized || KaravanApi.authType === 'public') {
-                getData();
-            }
+            getData();
         });
         return () => {
             console.log("Main End");
+            clearInterval(interval);
             sub?.unsubscribe();
         };
     }, []);
@@ -88,17 +91,26 @@ export const Main = () => {
             }
         });
     }
+    function getStatuses() {
+        if (KaravanApi.isAuthorized || KaravanApi.authType === 'public') {
+            KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => {
+                setContainers(statuses);
+            });
+        }
+    }
 
     function getData() {
-        KaravanApi.getConfiguration((config: AppConfig) => {
-            setRequest(uuidv4());
-            setConfig(config);
-            useAppConfigStore.setState({config: config});
-            InfrastructureAPI.infrastructure = config.infrastructure;
-        });
-        updateKamelets();
-        updateComponents();
-        // updateSupportedComponents(); // not implemented yet
+        if (KaravanApi.isAuthorized || KaravanApi.authType === 'public') {
+            KaravanApi.getConfiguration((config: AppConfig) => {
+                setRequest(uuidv4());
+                setConfig(config);
+                useAppConfigStore.setState({config: config});
+                InfrastructureAPI.infrastructure = config.infrastructure;
+            });
+            updateKamelets();
+            updateComponents();
+            // updateSupportedComponents(); // not implemented yet
+        }
     }
 
     async function updateKamelets(): Promise<void> {
@@ -165,7 +177,6 @@ export const Main = () => {
                                 onClick={event => {
                                     useFileStore.setState({operation: 'none', file: undefined})
                                     useDevModeStore.setState({podName: undefined, status: "none"})
-                                    useProjectStore.setState({containerStatus: new ContainerStatus({}),})
                                     setPageId(page.pageId);
                                 }}
                         />
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
index cdfada6f..14f42cbb 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
@@ -45,7 +45,7 @@ export class ProjectService {
         KaravanApi.manageContainer('dev', 'devmove', project.projectId, 'stop', res => {
             useDevModeStore.setState({status: 'none'})
             if (res.status === 200) {
-                useLogStore.setState({showLog: false, type: 'container', isRunning: false})
+                useLogStore.setState({showLog: false, type: 'container'})
             } else {
                 ProjectEventBus.sendAlert(new ToastMessage('Error stopping DevMode container', res.statusText, 'warning'))
             }
@@ -57,7 +57,7 @@ export class ProjectService {
         KaravanApi.manageContainer('dev', 'devmove', project.projectId, 'pause', res => {
             useDevModeStore.setState({status: 'none'})
             if (res.status === 200) {
-                useLogStore.setState({showLog: false, type: 'container', isRunning: false})
+                useLogStore.setState({showLog: false, type: 'container'})
             } else {
                 ProjectEventBus.sendAlert(new ToastMessage('Error stopping DevMode container', res.statusText, 'warning'))
             }
@@ -70,35 +70,35 @@ export class ProjectService {
         KaravanApi.deleteDevModeContainer(project.projectId, false, res => {
             useDevModeStore.setState({status: 'none'})
             if (res.status === 202) {
-                useLogStore.setState({showLog: false, type: 'container', isRunning: false})
+                useLogStore.setState({showLog: false, type: 'container'})
             } else {
                 ProjectEventBus.sendAlert(new ToastMessage('Error delete runner', res.statusText, 'warning'))
             }
         });
     }
 
-    public static getDevModeStatus(project: Project) {
-        const projectId = project.projectId;
-        KaravanApi.getDevModePodStatus(projectId, res => {
-            if (res.status === 200) {
-                unstable_batchedUpdates(() => {
-                    const containerStatus = res.data;
-                    if (useDevModeStore.getState().podName !== containerStatus.containerName){
-                        useDevModeStore.setState({podName: containerStatus.containerName})
-                    }
-                    if (useDevModeStore.getState().status !== 'wip'){
-                        useLogStore.setState({isRunning: true})
-                    }
-                    useProjectStore.setState({containerStatus: containerStatus});
-                })
-            } else {
-                unstable_batchedUpdates(() => {
-                    useDevModeStore.setState({status: 'none', podName: undefined})
-                    useProjectStore.setState({containerStatus: new ContainerStatus({})});
-                })
-            }
-        });
-    }
+    // public static getDevModeStatus(project: Project) {
+    //     const projectId = project.projectId;
+    //     KaravanApi.getDevModePodStatus(projectId, res => {
+    //         if (res.status === 200) {
+    //             unstable_batchedUpdates(() => {
+    //                 const containerStatus = res.data;
+    //                 if (useDevModeStore.getState().podName !== containerStatus.containerName){
+    //                     useDevModeStore.setState({podName: containerStatus.containerName})
+    //                 }
+    //                 if (useDevModeStore.getState().status !== 'wip'){
+    //                     useLogStore.setState({isRunning: true})
+    //                 }
+    //                 useStatusesStore.setState({containerStatus: containerStatus});
+    //             })
+    //         } else {
+    //             unstable_batchedUpdates(() => {
+    //                 useDevModeStore.setState({status: 'none', podName: undefined})
+    //                 useStatusesStore.setState({containerStatus: new ContainerStatus({})});
+    //             })
+    //         }
+    //     });
+    // }
 
     public static pushProject(project: Project, commitMessage: string) {
         useProjectStore.setState({isPushing: true})
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 80c99593..4c554926 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
@@ -59,7 +59,6 @@ interface ProjectState {
     project: Project;
     isPushing: boolean,
     isRunning: boolean,
-    containerStatus: ContainerStatus,
     operation: "create" | "select" | "delete" | "none" | "copy";
     setProject: (project: Project, operation:  "create" | "select" | "delete"| "none" | "copy") => void;
     setOperation: (o: "create" | "select" | "delete"| "none" | "copy") => void;
@@ -70,7 +69,6 @@ export const useProjectStore = create<ProjectState>((set) => ({
     operation: "none",
     isPushing: false,
     isRunning: false,
-    containerStatus: new ContainerStatus(),
     setProject: (project: Project, operation:  "create" | "select" | "delete"| "none" | "copy") => {
         set((state: ProjectState) => ({
             project: project,
@@ -191,7 +189,6 @@ export const useStatusesStore = create<StatusesState>((set) => ({
 
 interface LogState {
     podName?: string,
-    isRunning: boolean,
     data: string;
     setData: (data: string) => void;
     addData: (data: string) => void;
@@ -202,13 +199,11 @@ interface LogState {
     setShowLog: (showLog: boolean) => void;
     type: 'container' | 'pipeline' | 'none',
     setType: (type: 'container' | 'pipeline' | 'none') => void,
-    setIsRunning: (isRunning: boolean) => void;
 }
 
 export const useLogStore = create<LogState>((set) => ({
     podName: undefined,
     data: '',
-    isRunning: false,
     setData: (data: string)  => {
         set({data: data})
     },
@@ -232,9 +227,6 @@ export const useLogStore = create<LogState>((set) => ({
     setShowLog: (showLog: boolean) => {
         set(() => ({showLog: showLog}));
     },
-    setIsRunning: (isRunning: boolean) => {
-        set(() => ({isRunning: isRunning}));
-    },
     type: "none",
     setType: (type: 'container' | 'pipeline' | 'none') =>  {
         set((state: LogState) => ({type: type}));
diff --git a/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx b/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx
index 08539413..a2ae7686 100644
--- a/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx
@@ -1,7 +1,7 @@
-import React, {useEffect, useState} from 'react';
+import React, {useState} from 'react';
 import {
     Bullseye,
-    Button, EmptyState, EmptyStateIcon, EmptyStateVariant,
+    EmptyState, EmptyStateIcon, EmptyStateVariant,
     PageSection, Spinner,
     Text,
     TextContent,
@@ -13,8 +13,6 @@ import {
 import '../designer/karavan.css';
 import {ContainerStatus} from "../api/ProjectModels";
 import {TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
-import {KaravanApi} from "../api/KaravanApi";
-import RefreshIcon from "@patternfly/react-icons/dist/esm/icons/sync-alt-icon";
 import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon";
 import {MainToolbar} from "../designer/MainToolbar";
 import {useAppConfigStore, useStatusesStore} from "../api/ProjectStore";
@@ -24,27 +22,11 @@ import {ContainerTableRow} from "./ContainerTableRow";
 export const ContainersPage = () => {
 
     const [config] = useAppConfigStore((state) => [state.config], shallow)
-    const [containers, setContainers] = useStatusesStore((state) => [state.containers, state.setContainers], shallow);
+    const [containers] = useStatusesStore((state) => [state.containers, state.setContainers], shallow);
     const [filter, setFilter] = useState<string>('');
-    const [loading, setLoading] = useState<boolean>(true);
+    const [loading] = useState<boolean>(true);
     const [selectedEnv, setSelectedEnv] = useState<string[]>([config.environment]);
 
-    useEffect(() => {
-        const interval = setInterval(() => {
-            updateContainerStatuses()
-        }, 700);
-        return () => {
-            clearInterval(interval)
-        };
-    }, []);
-
-    function updateContainerStatuses() {
-        KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => {
-            setContainers(statuses);
-            setLoading(false);
-        });
-    }
-
     function selectEnvironment(name: string, selected: boolean) {
         if (selected && !selectedEnv.includes(name)) {
             setSelectedEnv((state: string[]) => {
@@ -61,9 +43,6 @@ export const ContainersPage = () => {
     function tools() {
         return (<Toolbar id="toolbar-group-types">
             <ToolbarContent>
-                <ToolbarItem>
-                    <Button variant="link" icon={<RefreshIcon/>} onClick={e => updateContainerStatuses()}/>
-                </ToolbarItem>
                 <ToolbarItem>
                     <ToggleGroup aria-label="Default with single selectable">
                         {config.environments.map(env => (
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 54dbc583..0367351c 100644
--- a/karavan-web/karavan-app/src/main/webui/src/index.css
+++ b/karavan-web/karavan-app/src/main/webui/src/index.css
@@ -180,7 +180,7 @@
   margin-bottom: 100px;
 }
 
-.karavan .project-page .project-log {
+.karavan .project-log {
   position: absolute;
   bottom: 0;
   right: 0;
@@ -191,7 +191,7 @@
   align-items: stretch;
 }
 
-.karavan .project-page .project-log .buttons {
+.karavan .project-log .buttons {
   display: flex;
   flex-direction: row;
   justify-content: flex-end;
@@ -199,26 +199,26 @@
   padding-right: 6px;
 }
 
-.karavan .project-page .project-log .buttons button,
-.karavan .project-page .project-log .buttons .pf-c-check {
+.karavan .project-log .buttons button,
+.karavan .project-log .buttons .pf-c-check {
   padding: 8px;
 }
 
-.karavan .project-page .project-log .buttons .pf-c-check .pf-c-check__label{
+.karavan .project-log .buttons .pf-c-check .pf-c-check__label{
   font-size: 12px;
   line-height: 20px;
   padding: 0;
 }
 
-.karavan .project-page .project-log .pf-c-log-viewer__scroll-container {
+.karavan .project-log .pf-c-log-viewer__scroll-container {
   /*height: 100% !important;*/
 }
 
-.karavan .project-page .project-log .pf-c-log-viewer__text {
+.karavan .project-log .pf-c-log-viewer__text {
   font-size: 12px;
 }
 
-.karavan .project-page .project-log .log-name {
+.karavan .project-log .log-name {
   --pf-c-label__content--before--BorderWidth: 0;
   --pf-c-label--BackgroundColor: transparent;
   margin-right: auto;
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 9777019b..9401aeb0 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
@@ -4,8 +4,7 @@ import '../designer/karavan.css';
 import RocketIcon from "@patternfly/react-icons/dist/esm/icons/rocket-icon";
 import ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
 import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon";
-import PauseIcon from "@patternfly/react-icons/dist/esm/icons/pause-icon";
-import {useDevModeStore, useLogStore, useProjectStore} from "../api/ProjectStore";
+import {useDevModeStore, useLogStore, useProjectStore, useStatusesStore} from "../api/ProjectStore";
 import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
 import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
@@ -20,23 +19,25 @@ interface Props {
 export const DevModeToolbar = (props: Props) => {
 
     const [status] = useDevModeStore((state) => [state.status], shallow)
-    const [project, containerStatus ] = useProjectStore((state) => [state.project, state.containerStatus], shallow)
+    const [project ] = useProjectStore((state) => [state.project], shallow)
+    const [containers] = useStatusesStore((state) => [state.containers], shallow);
     const [verbose, setVerbose] = useState(false);
 
-    const commands = containerStatus.commands;
-    const isRunning = containerStatus.state === 'running';
-    const inTransit = containerStatus.inTransit;
+    const containerStatus = containers.filter(c => c.containerName === project.projectId).at(0);
+    const commands = containerStatus?.commands || ['run'];
+    const isRunning = containerStatus?.state === 'running';
+    const inTransit = containerStatus?.inTransit;
     const isLoading= status === 'wip';
-    const color = containerStatus.state === 'running' ? "green" : "grey";
+    const color = containerStatus?.state === 'running' ? "green" : "grey";
     const icon = isRunning ? <UpIcon/> : <DownIcon/>;
     return (<Flex className="toolbar" direction={{default: "row"}} alignItems={{default: "alignItemsCenter"}}>
         <FlexItem>
             {(inTransit || isLoading) && <Spinner isSVG size="lg" aria-label="spinner"/>}
         </FlexItem>
-        {containerStatus.containerId && <FlexItem>
+        {containerStatus?.containerId && <FlexItem>
             <Label icon={icon} color={color}>
                 <Tooltip content={"Show log"} position={TooltipPosition.bottom}>
-                    <Button variant="link"
+                    <Button variant="link" isDisabled={!isRunning}
                             onClick={e =>
                                 useLogStore.setState({showLog: true, type: 'container', podName: containerStatus.containerName})}>
                         {containerStatus.containerName}
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 cab863b6..976123ef 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
@@ -13,7 +13,6 @@ import {MainToolbar} from "../designer/MainToolbar";
 import {ProjectTitle} from "./ProjectTitle";
 import {ProjectPanel} from "./ProjectPanel";
 import {FileEditor} from "./file/FileEditor";
-import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
 
 export const ProjectPage = () => {
@@ -23,15 +22,15 @@ export const ProjectPage = () => {
     const [key, setKey] = useState<string>('');
     const [project] = useProjectStore((state) => [state.project], shallow )
 
-    useEffect(() => {
-        // TODO: make status request only when started or just opened
-        const interval = setInterval(() => {
-            ProjectService.getDevModeStatus(project);
-        }, 1000);
-        return () => {
-            clearInterval(interval)
-        };
-    }, []);
+    // useEffect(() => {
+    //     // TODO: make status request only when started or just opened
+    //     const interval = setInterval(() => {
+    //         ProjectService.getDevModeStatus(project);
+    //     }, 1000);
+    //     return () => {
+    //         clearInterval(interval)
+    //     };
+    // }, []);
 
     function post (file: ProjectFile)  {
         KaravanApi.postProjectFile(file, res => {
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx b/karavan-web/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
index 9aa0a0f8..beceb5c8 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
@@ -24,11 +24,14 @@ import {InfoContainer} from "./InfoContainer";
 import {InfoContext} from "./InfoContext";
 import {InfoMemory} from "./InfoMemory";
 import {KaravanApi} from "../../api/KaravanApi";
-import {useProjectStore} from "../../api/ProjectStore";
+import {useProjectStore, useStatusesStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {ContainerStatus} from "../../api/ProjectModels";
 
 export const DashboardTab = () => {
 
-    const {project, containerStatus} = useProjectStore();
+    const [project] = useProjectStore((state) => [state.project], shallow);
+    const [containers] = useStatusesStore((state) => [state.containers], shallow);
     const [memory, setMemory] = useState({});
     const [jvm, setJvm] = useState({});
     const [context, setContext] = useState({});
@@ -67,10 +70,8 @@ export const DashboardTab = () => {
         })
     }
 
-    function showConsole(): boolean {
-        return containerStatus.lifeCycle === 'ready';
-    }
-
+    const containerStatus = containers.filter(c => c.containerName === project.projectId).at(0);
+    const showConsole = containerStatus?.state === 'running'
     return (
         <PageSection className="project-tab-panel" padding={{default: "padding"}}>
             <Card className="project-development">
@@ -78,15 +79,15 @@ export const DashboardTab = () => {
                     <Flex direction={{default: "row"}}
                           justifyContent={{default: "justifyContentSpaceBetween"}}>
                         <FlexItem flex={{default: "flex_1"}}>
-                            <InfoContainer containerStatus={containerStatus}/>
+                            <InfoContainer containerStatus={containerStatus || new ContainerStatus()}/>
                         </FlexItem>
                         <Divider orientation={{default: "vertical"}}/>
                         <FlexItem flex={{default: "flex_1"}}>
-                            <InfoMemory jvm={jvm} memory={memory} showConsole={showConsole()}/>
+                            <InfoMemory jvm={jvm} memory={memory} showConsole={showConsole}/>
                         </FlexItem>
                         <Divider orientation={{default: "vertical"}}/>
                         <FlexItem flex={{default: "flex_1"}}>
-                            <InfoContext context={context} showConsole={showConsole()}/>
+                            <InfoContext context={context} showConsole={showConsole}/>
                         </FlexItem>
                     </Flex>
                 </CardBody>
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/log/ProjectLogPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/log/ProjectLogPanel.tsx
index 619b1ff7..d02ee9ac 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/log/ProjectLogPanel.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/log/ProjectLogPanel.tsx
@@ -5,7 +5,7 @@ import CloseIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
 import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
 import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-icon';
 import CleanIcon from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon';
-import {useLogStore} from "../../api/ProjectStore";
+import {useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
 import {KaravanApi} from "../../api/KaravanApi";
 import {shallow} from "zustand/shallow";
 import {ProjectEventBus} from "../../api/ProjectEventBus";
@@ -14,9 +14,10 @@ import {ProjectLog} from "./ProjectLog";
 const INITIAL_LOG_HEIGHT = "50%";
 
 export const ProjectLogPanel = () => {
-    const [showLog, type, setShowLog, podName, isRunning] = useLogStore(
-        (state) => [state.showLog, state.type, state.setShowLog, state.podName, state.isRunning], shallow)
+    const [showLog, type, setShowLog, podName] = useLogStore(
+        (state) => [state.showLog, state.type, state.setShowLog, state.podName], shallow)
 
+    const [containers] = useStatusesStore((state) => [state.containers], shallow);
     const [height, setHeight] = useState(INITIAL_LOG_HEIGHT);
     const [isTextWrapped, setIsTextWrapped] = useState(true);
     const [autoScroll, setAutoScroll] = useState(true);
@@ -24,9 +25,10 @@ export const ProjectLogPanel = () => {
     const [currentPodName, setCurrentPodName] = useState<string | undefined>(undefined);
 
     useEffect(() => {
-        console.log("ProjectLogPanel", showLog, type, podName, isRunning);
+        const containerStatus = containers.filter(c => c.containerName === podName).at(0);
+        console.log("ProjectLogPanel", showLog, type, podName, containerStatus);
         const controller = new AbortController();
-        if (showLog && type !== 'none' && podName !== undefined && isRunning) {
+        if (showLog && type !== 'none' && podName !== undefined) {
             const f = KaravanApi.fetchData(type, podName, controller).then(value => {
                 console.log("Fetch Started for: " + podName)
             });
@@ -37,7 +39,7 @@ export const ProjectLogPanel = () => {
             console.log("end");
             controller.abort();
         };
-    }, [showLog, type, podName, isRunning]);
+    }, [showLog, type, podName]);
 
     useEffect(() => {
         if (currentPodName !== podName) {
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx b/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
index 14dc1eaa..ba26da23 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
@@ -207,8 +207,7 @@ export class ProjectStatus extends React.Component<Props, State> {
                                                         useLogStore.setState({
                                                             showLog: true,
                                                             type: 'container',
-                                                            podName: pod.containerName,
-                                                            isRunning: true
+                                                            podName: pod.containerName
                                                         });
                                                     }}>
                                                 {pod.containerName}
@@ -287,7 +286,7 @@ export class ProjectStatus extends React.Component<Props, State> {
                             <Label icon={isRunning ? <Spinner isSVG diameter="16px" className="spinner"/> : icon} color={color}>
                                 {pipeline
                                     ? <Button variant="link" onClick={e =>
-                                        useLogStore.setState({showLog: true, type: 'pipeline', podName: pipeline, isRunning: true})
+                                        useLogStore.setState({showLog: true, type: 'pipeline', podName: pipeline})
                                     }>
                                         {pipeline}
                                     </Button>
diff --git a/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx b/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
index 87606ac9..b7dd8fde 100644
--- a/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
@@ -23,36 +23,24 @@ import {DeleteServiceModal} from "./DeleteServiceModal";
 import {CreateServiceModal} from "./CreateServiceModal";
 import {useProjectStore, useStatusesStore} from "../api/ProjectStore";
 import {MainToolbar} from "../designer/MainToolbar";
-import {ContainerStatus, Project, ProjectType} from "../api/ProjectModels";
+import {Project, ProjectType} from "../api/ProjectModels";
 import {KaravanApi} from "../api/KaravanApi";
 import {DevService, Services, ServicesYaml} from "../api/ServiceModels";
 import {shallow} from "zustand/shallow";
+import {ProjectLogPanel} from "../project/log/ProjectLogPanel";
 
 
 export const ServicesPage = () => {
 
     const [services, setServices] = useState<Services>();
-    const [containers, setContainers] = useStatusesStore((state) => [state.containers, state.setContainers], shallow);
-    const [operation, setOperation] = useState<'create' | 'delete' | 'none'>('none');
-    const [loading, setLoading] = useState<boolean>(false);
+    const [containers] = useStatusesStore((state) => [state.containers, state.setContainers], shallow);
+    const [operation] = useState<'create' | 'delete' | 'none'>('none');
+    const [loading] = useState<boolean>(false);
 
     useEffect(() => {
         getServices();
-        const interval = setInterval(() => {
-            updateContainerStatuses()
-        }, 700);
-        return () => {
-            clearInterval(interval)
-        };
     }, []);
 
-    function updateContainerStatuses() {
-        KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => {
-            setContainers(statuses);
-            setLoading(false);
-        });
-    }
-
     function getServices() {
         KaravanApi.getFiles(ProjectType.services, files => {
             const file = files.at(0);
@@ -142,6 +130,7 @@ export const ServicesPage = () => {
             </PageSection>
             {["create"].includes(operation) && <CreateServiceModal/>}
             {["delete"].includes(operation) && <DeleteServiceModal/>}
+            <ProjectLogPanel/>
         </PageSection>
     )
 }
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx b/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx
index fab5456d..c5bc953a 100644
--- a/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx
@@ -2,7 +2,7 @@ import React, {useState} from 'react';
 import {
     Button,
     Tooltip,
-    Flex, FlexItem, Label, ToolbarContent, Toolbar, ToolbarItem, Spinner
+    Flex, FlexItem, Label, ToolbarContent, Toolbar, ToolbarItem, Spinner, TooltipPosition
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {ActionsColumn, ExpandableRowContent, Tbody, Td, Tr} from "@patternfly/react-table";
@@ -12,9 +12,11 @@ import {DevService} from "../api/ServiceModels";
 import {ContainerStatus} from "../api/ProjectModels";
 import PauseIcon from "@patternfly/react-icons/dist/esm/icons/pause-icon";
 import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
-import {useAppConfigStore} from "../api/ProjectStore";
+import {useAppConfigStore, useLogStore} from "../api/ProjectStore";
 import {shallow} from "zustand/shallow";
 import {KaravanApi} from "../api/KaravanApi";
+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";
 
 interface Props {
     index: number
@@ -80,7 +82,8 @@ export const ServicesTableRow = (props: Props) => {
     const container = props.container;
     const isRunning = container?.state === 'running';
     const inTransit = container?.inTransit;
-    const color = container?.state === 'running' ? "green" : "grey";
+    const color = isRunning ? "green" : "grey";
+    const icon = isRunning ? <UpIcon/> : <DownIcon/>;
     return (
         <Tbody isExpanded={isExpanded}>
             <Tr key={service.container_name}>
@@ -96,7 +99,17 @@ export const ServicesTableRow = (props: Props) => {
                     modifier={"fitContent"}>
                 </Td>
                 <Td>
-                    <Label color={color}>{service.container_name}</Label>
+                    {container && <Label icon={icon} color={color}>
+                        <Tooltip content={"Show log"} position={TooltipPosition.bottom}>
+                            <Button variant="link" isDisabled={!isRunning}
+                                    onClick={e => {
+                                        useLogStore.setState({showLog: true, type: 'container', podName: container.containerName});
+                                    }}>
+                                {service.container_name}
+                            </Button>
+                        </Tooltip>
+                    </Label>}
+                    {!container && <Label color={color}>{service.container_name}</Label>}
                 </Td>
                 <Td>{service.container_name}</Td>
                 <Td>{service.image}</Td>