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 2022/06/29 22:54:41 UTC

[camel-karavan] branch main updated: Improvements (#402)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 2f90ac1  Improvements (#402)
2f90ac1 is described below

commit 2f90ac1082b21f1e4273c63f3176d5f1d8e40513
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Wed Jun 29 18:54:36 2022 -0400

    Improvements (#402)
---
 .../apache/camel/karavan/api/StatusResource.java   |   5 +-
 .../apache/camel/karavan/api/TektonResource.java   |   4 +-
 .../camel/karavan/model/DeploymentStatus.java      |  47 ++++
 .../camel/karavan/model/KaravanConfiguration.java  |   1 +
 .../org/apache/camel/karavan/model/Project.java    |  12 +-
 .../camel/karavan/model/ProjectEnvStatus.java      |  39 +++-
 .../apache/camel/karavan/model/ProjectStatus.java  |  12 +-
 .../camel/karavan/model/ProjectStoreSchema.java    |   2 +-
 .../camel/karavan/service/KaravanService.java      |  12 +-
 .../camel/karavan/service/KubernetesService.java   |  55 ++++-
 .../camel/karavan/service/StatusService.java       |  68 ++++--
 .../src/main/resources/application.properties      |  10 +-
 .../src/main/webapp/src/builder/BuilderPage.tsx    |   2 +-
 karavan-app/src/main/webapp/src/index.css          |   9 +
 .../src/main/webapp/src/models/ProjectModels.ts    |  17 +-
 .../main/webapp/src/projects/ProjectDashboard.tsx  |  42 ++--
 .../src/main/webapp/src/projects/ProjectHeader.tsx | 257 +++++++++++++++------
 .../src/main/webapp/src/projects/ProjectsPage.tsx  |   2 +-
 .../openshift/karavan-quarkus-task.yaml            |   4 +-
 19 files changed, 431 insertions(+), 169 deletions(-)

diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/StatusResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/StatusResource.java
index 02d08c9..c7820b3 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/api/StatusResource.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/StatusResource.java
@@ -17,6 +17,7 @@
 package org.apache.camel.karavan.api;
 
 import io.vertx.core.eventbus.EventBus;
+import org.apache.camel.karavan.model.DeploymentStatus;
 import org.apache.camel.karavan.model.KaravanConfiguration;
 import org.apache.camel.karavan.model.ProjectEnvStatus;
 import org.apache.camel.karavan.model.ProjectStatus;
@@ -58,9 +59,9 @@ public class StatusResource {
         } else {
             return new ProjectStatus( projectId,
                     configuration.environments()
-                            .stream().map(e -> new ProjectEnvStatus(e.name(), ProjectEnvStatus.Status.DOWN))
+                            .stream().map(e -> new ProjectEnvStatus(e.name(), ProjectEnvStatus.Status.DOWN, "-", "Undefined", new DeploymentStatus()))
                             .collect(Collectors.toList()),
-                    Long.valueOf(0), "Undefined");
+                    Long.valueOf(0));
         }
     }
 }
\ No newline at end of file
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/TektonResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/TektonResource.java
index 8b4777a..297e083 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/api/TektonResource.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/TektonResource.java
@@ -56,9 +56,7 @@ public class TektonResource {
         Project p = infinispanService.getProject(project.getProjectId());
         Optional<KaravanConfiguration.Environment> env = configuration.environments().stream().filter(e -> e.name().equals(environment)).findFirst();
         if (env.isPresent()) {
-            String pipelineRunId = kubernetesService.createPipelineRun(project, env.get().namespace());
-            p.setLastPipelineRun(pipelineRunId);
-            infinispanService.saveProject(p);
+            kubernetesService.createPipelineRun(project, env.get().pipeline(), env.get().namespace());
         }
         return p;
     }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/DeploymentStatus.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/DeploymentStatus.java
new file mode 100644
index 0000000..f55d5b4
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/DeploymentStatus.java
@@ -0,0 +1,47 @@
+package org.apache.camel.karavan.model;
+
+import org.infinispan.protostream.annotations.ProtoFactory;
+import org.infinispan.protostream.annotations.ProtoField;
+
+public class DeploymentStatus {
+    @ProtoField(number = 1)
+    String image;
+    @ProtoField(number = 2)
+    Integer replicas;
+    @ProtoField(number = 3)
+    Integer readyReplicas;
+
+    public DeploymentStatus() {
+    }
+
+    @ProtoFactory
+    public DeploymentStatus(String image, Integer replicas, Integer readyReplicas) {
+        this.image = image;
+        this.replicas = replicas;
+        this.readyReplicas = readyReplicas;
+    }
+
+    public String getImage() {
+        return image;
+    }
+
+    public void setImage(String image) {
+        this.image = image;
+    }
+
+    public Integer getReplicas() {
+        return replicas;
+    }
+
+    public void setReplicas(Integer replicas) {
+        this.replicas = replicas;
+    }
+
+    public Integer getReadyReplicas() {
+        return readyReplicas;
+    }
+
+    public void setReadyReplicas(Integer readyReplicas) {
+        this.readyReplicas = readyReplicas;
+    }
+}
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/KaravanConfiguration.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/KaravanConfiguration.java
index 3cea180..497a2fc 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/model/KaravanConfiguration.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/KaravanConfiguration.java
@@ -18,5 +18,6 @@ public interface KaravanConfiguration {
         String name();
         String cluster();
         String namespace();
+        String pipeline();
     }
 }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java
index 3cd5702..898b0ab 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java
@@ -17,8 +17,6 @@ public class Project {
     Project.CamelRuntime runtime;
     @ProtoField(number = 5)
     String lastCommit;
-    @ProtoField(number = 6)
-    String lastPipelineRun;
 
     public enum CamelRuntime {
         @ProtoEnumValue(number = 0, name = "Quarkus")
@@ -30,13 +28,12 @@ public class Project {
     }
 
     @ProtoFactory
-    public Project(String projectId, String name, String description, CamelRuntime runtime, String lastCommit, String lastPipelineRun) {
+    public Project(String projectId, String name, String description, CamelRuntime runtime, String lastCommit) {
         this.projectId = projectId;
         this.name = name;
         this.description = description;
         this.runtime = runtime;
         this.lastCommit = lastCommit;
-        this.lastPipelineRun = lastPipelineRun;
     }
 
     public Project(String projectId, String name, String description, CamelRuntime runtime) {
@@ -89,11 +86,4 @@ public class Project {
         this.lastCommit = lastCommit;
     }
 
-    public String getLastPipelineRun() {
-        return lastPipelineRun;
-    }
-
-    public void setLastPipelineRun(String lastPipelineRun) {
-        this.lastPipelineRun = lastPipelineRun;
-    }
 }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectEnvStatus.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectEnvStatus.java
index 346a0e6..5aeb0dc 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectEnvStatus.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectEnvStatus.java
@@ -9,6 +9,12 @@ public class ProjectEnvStatus {
     String environment;
     @ProtoField(number = 2)
     Status status;
+    @ProtoField(number = 3)
+    String lastPipelineRun;
+    @ProtoField(number = 4)
+    String lastPipelineRunResult;
+    @ProtoField(number = 5)
+    DeploymentStatus deploymentStatus;
 
     public enum Status {
         @ProtoEnumValue(number = 0, name = "DOWN")
@@ -18,9 +24,16 @@ public class ProjectEnvStatus {
     }
 
     @ProtoFactory
-    public ProjectEnvStatus(String environment, Status status) {
+    public ProjectEnvStatus(String environment, Status status, String lastPipelineRun, String lastPipelineRunResult, DeploymentStatus deploymentStatus) {
         this.environment = environment;
         this.status = status;
+        this.lastPipelineRun = lastPipelineRun;
+        this.lastPipelineRunResult = lastPipelineRunResult;
+        this.deploymentStatus = deploymentStatus;
+    }
+
+    public ProjectEnvStatus(String environment) {
+        this.environment = environment;
     }
 
     public String getEnvironment() {
@@ -38,4 +51,28 @@ public class ProjectEnvStatus {
     public void setStatus(Status status) {
         this.status = status;
     }
+
+    public String getLastPipelineRun() {
+        return lastPipelineRun;
+    }
+
+    public void setLastPipelineRun(String lastPipelineRun) {
+        this.lastPipelineRun = lastPipelineRun;
+    }
+
+    public String getLastPipelineRunResult() {
+        return lastPipelineRunResult;
+    }
+
+    public void setLastPipelineRunResult(String lastPipelineRunResult) {
+        this.lastPipelineRunResult = lastPipelineRunResult;
+    }
+
+    public DeploymentStatus getDeploymentStatus() {
+        return deploymentStatus;
+    }
+
+    public void setDeploymentStatus(DeploymentStatus deploymentStatus) {
+        this.deploymentStatus = deploymentStatus;
+    }
 }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectStatus.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectStatus.java
index 4dd7efc..e7145ea 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectStatus.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectStatus.java
@@ -15,8 +15,6 @@ public class ProjectStatus {
     List<ProjectEnvStatus> statuses;
     @ProtoField(number = 3)
     Long lastUpdate;
-    @ProtoField(number = 4)
-    String pipeline;
 
     public enum Status {
         @ProtoEnumValue(number = 0, name = "DOWN")
@@ -26,11 +24,10 @@ public class ProjectStatus {
     }
 
     @ProtoFactory
-    public ProjectStatus(String projectId, List<ProjectEnvStatus> statuses, Long lastUpdate, String pipeline) {
+    public ProjectStatus(String projectId, List<ProjectEnvStatus> statuses, Long lastUpdate) {
         this.projectId = projectId;
         this.statuses = statuses;
         this.lastUpdate = lastUpdate;
-        this.pipeline = pipeline;
     }
 
     public ProjectStatus() {
@@ -60,11 +57,4 @@ public class ProjectStatus {
         this.lastUpdate = lastUpdate;
     }
 
-    public String getPipeline() {
-        return pipeline;
-    }
-
-    public void setPipeline(String pipeline) {
-        this.pipeline = pipeline;
-    }
 }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectStoreSchema.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectStoreSchema.java
index cf01d06..b5fa110 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectStoreSchema.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/ProjectStoreSchema.java
@@ -7,7 +7,7 @@ import java.util.HashMap;
 
 @AutoProtoSchemaBuilder(
         includeClasses = {
-                GroupedKey.class, Project.class, ProjectFile.class, ProjectStatus.class, ProjectEnvStatus.class
+                GroupedKey.class, Project.class, ProjectFile.class, ProjectStatus.class, ProjectEnvStatus.class, DeploymentStatus.class
         },
         schemaPackageName = "karavan")
 public interface ProjectStoreSchema extends GeneratedSchema {
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/KaravanService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/KaravanService.java
index ac1b04f..8f334c3 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/service/KaravanService.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/KaravanService.java
@@ -16,14 +16,17 @@
  */
 package org.apache.camel.karavan.service;
 
+import io.quarkus.runtime.StartupEvent;
 import io.quarkus.vertx.ConsumeEvent;
 import io.smallrye.mutiny.tuples.Tuple2;
+import org.apache.camel.karavan.model.KaravanConfiguration;
 import org.apache.camel.karavan.model.Project;
 import org.apache.camel.karavan.model.ProjectFile;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
 import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.event.Observes;
 import javax.inject.Inject;
 import java.util.Arrays;
 import java.util.List;
@@ -45,6 +48,13 @@ public class KaravanService {
     @ConfigProperty(name = "karavan.config.runtime")
     String runtime;
 
+    @Inject
+    KaravanConfiguration configuration;
+
+    void onStart(@Observes StartupEvent ev) {
+        configuration.environments().forEach(environment -> System.out.println(environment.name()));
+    }
+
     @ConsumeEvent(value = IMPORT_PROJECTS, blocking = true)
     void importProjects(String data) {
         LOGGER.info("Import projects from Git");
@@ -53,7 +63,7 @@ public class KaravanService {
             repo.forEach(p -> {
                 String folderName = p.getItem1();
                 String name = Arrays.stream(folderName.split("-")).map(s -> capitalize(s)).collect(Collectors.joining(" "));
-                Project project = new Project(folderName, name, name, Project.CamelRuntime.valueOf(runtime.toUpperCase()), "", "");
+                Project project = new Project(folderName, name, name, Project.CamelRuntime.valueOf(runtime.toUpperCase()), "");
                 infinispanService.saveProject(project);
                 p.getItem2().forEach((key, value) -> {
                     ProjectFile file = new ProjectFile(key, value, folderName);
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/KubernetesService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/KubernetesService.java
index d48e1a7..299d244 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/service/KubernetesService.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/KubernetesService.java
@@ -31,12 +31,16 @@ import io.fabric8.tekton.pipeline.v1beta1.PipelineRun;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRunBuilder;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRunSpec;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRunSpecBuilder;
+import io.smallrye.mutiny.tuples.Tuple2;
+import io.smallrye.mutiny.tuples.Tuple3;
+import org.apache.camel.karavan.model.DeploymentStatus;
 import org.apache.camel.karavan.model.Project;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
 import javax.enterprise.context.ApplicationScoped;
 import javax.enterprise.inject.Produces;
+import java.util.Comparator;
 import java.util.Map;
 import java.util.Objects;
 
@@ -63,12 +67,12 @@ public class KubernetesService {
 
     private static final Logger LOGGER = Logger.getLogger(KubernetesService.class.getName());
 
-    public String createPipelineRun(Project project, String namespace) throws Exception {
+    public String createPipelineRun(Project project, String pipelineName, String namespace) throws Exception {
         LOGGER.info("Pipeline is creating for " + project.getProjectId());
 
         Map<String, String> labels = Map.of(
                 "karavan-project-id", project.getProjectId(),
-                "tekton.dev/pipeline", "karavan-quarkus"
+                "tekton.dev/pipeline", pipelineName
         );
 
         ObjectMeta meta = new ObjectMetaBuilder()
@@ -94,21 +98,52 @@ public class KubernetesService {
         return pipelineRun.getMetadata().getName();
     }
 
-    public PipelineRun getPipelineRun(String name, String namespace)  {
-        return tektonClient().v1beta1().pipelineRuns().inNamespace(namespace).withName(name).get();
+    public PipelineRun getPipelineRun(String pipelineRuneName, String namespace)  {
+        return tektonClient().v1beta1().pipelineRuns().inNamespace(namespace).withName(pipelineRuneName).get();
     }
 
-    public String getReplicas(String name, String namespace)  {
+    public PipelineRun getLastPipelineRun(String projectId, String pipelineName, String namespace)  {
+
+        tektonClient().v1beta1().pipelineRuns().inNamespace(namespace)
+                .withLabel("karavan-project-id", projectId)
+                .withLabel("tekton.dev/pipeline", pipelineName)
+                .list().getItems().sort(Comparator.comparing(o -> o.getMetadata().getCreationTimestamp()));
+
+
+        return tektonClient().v1beta1().pipelineRuns().inNamespace(namespace)
+                .withLabel("karavan-project-id", projectId)
+                .withLabel("tekton.dev/pipeline", pipelineName)
+                .list().getItems().stream().sorted((o1, o2) -> o2.getMetadata().getCreationTimestamp().compareTo(o1.getMetadata().getCreationTimestamp()))
+                .findFirst().get();
+    }
+
+
+    public Tuple2<Boolean, DeploymentStatus> getDeploymentStatus(String name, String namespace)  {
         if (kubernetesClient().isAdaptable(OpenShiftClient.class)) {
-            DeploymentConfig dc = openshiftClient().deploymentConfigs().inNamespace(namespace).withName(name).get();
-            dc.getSpec().getReplicas();
-            dc.getStatus().getReadyReplicas();
+            try {
+                DeploymentConfig dc = openshiftClient().deploymentConfigs().inNamespace(namespace).withName(name).get();
+                String dsImage = dc.getSpec().getTemplate().getSpec().getContainers().get(0).getImage();
+                String imageName = dsImage.startsWith("image-registry.openshift-image-registry.svc")
+                        ? dsImage.replace("image-registry.openshift-image-registry.svc:5000/", "")
+                        : dsImage;
+                return Tuple2.of(true,
+                        new DeploymentStatus(
+                                imageName,
+                                dc.getSpec().getReplicas(),
+                                dc.getStatus().getReadyReplicas()
+                        )
+                        );
+            } catch (Exception ex){
+                LOGGER.error(ex.getMessage());
+                return Tuple2.of(false, new DeploymentStatus());
+            }
         } else {
-//            throw new Exception("Adapting to OpenShiftClient not support. Check if adapter is present, and that env provides /oapi root path.");
+            // TODO: Implement Deployment for Kubernetes/Minikube
+            return  Tuple2.of(true, new DeploymentStatus());
         }
-        return "";
     }
 
+
     public Secret getKaravanSecret() {
         return kubernetesClient().secrets().inNamespace(namespace).withName("karavan").get();
     }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/StatusService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/StatusService.java
index 3742873..7177e47 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/service/StatusService.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/StatusService.java
@@ -20,12 +20,13 @@ import io.fabric8.tekton.pipeline.v1beta1.PipelineRun;
 import io.quarkus.runtime.configuration.ProfileManager;
 import io.quarkus.vertx.ConsumeEvent;
 import io.smallrye.mutiny.tuples.Tuple2;
+import io.smallrye.mutiny.tuples.Tuple3;
 import io.vertx.mutiny.core.Vertx;
 import io.vertx.mutiny.core.buffer.Buffer;
 import io.vertx.mutiny.ext.web.client.HttpResponse;
 import io.vertx.mutiny.ext.web.client.WebClient;
+import org.apache.camel.karavan.model.DeploymentStatus;
 import org.apache.camel.karavan.model.KaravanConfiguration;
-import org.apache.camel.karavan.model.Project;
 import org.apache.camel.karavan.model.ProjectEnvStatus;
 import org.apache.camel.karavan.model.ProjectStatus;
 import org.jboss.logging.Logger;
@@ -34,6 +35,7 @@ import javax.enterprise.context.ApplicationScoped;
 import javax.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 @ApplicationScoped
 public class StatusService {
@@ -64,10 +66,10 @@ public class StatusService {
         return webClient;
     }
 
-    @ConsumeEvent(value = CMD_COLLECT_STATUSES, blocking = true)
+    @ConsumeEvent(value = CMD_COLLECT_STATUSES, blocking = true, ordered = true)
     public void collectStatuses(String projectId) throws Exception {
-        LOGGER.info("Event received to collect statuses for the project " + projectId);
-        if (System.currentTimeMillis() - lastCollect > configuration.statusThreshold()){
+//        LOGGER.info("Event received to collect statuses for the project " + projectId);
+        if ((System.currentTimeMillis() - lastCollect) > configuration.statusThreshold()){
             getStatuses(projectId);
             lastCollect = System.currentTimeMillis();
         }
@@ -75,6 +77,7 @@ public class StatusService {
 
     private void getStatuses(String projectId) throws Exception {
         LOGGER.info("Start to collect statuses for the project " + projectId);
+        ProjectStatus old = infinispanService.getProjectStatus(projectId);
         ProjectStatus status = new ProjectStatus();
         status.setProjectId(projectId);
         status.setLastUpdate(System.currentTimeMillis());
@@ -83,46 +86,69 @@ public class StatusService {
             String url = ProfileManager.getActiveProfile().equals("dev")
                     ? String.format("http://%s-%s.%s/q/health", projectId, e.namespace(), e.cluster())
                     : String.format("http://%s.%s.%s/q/health", projectId, e.namespace(), e.cluster());
-            Tuple2<Boolean, ProjectEnvStatus> s = getProjectEnvStatus(url, e.name());
-            if (s.getItem1()) statuses.add(s.getItem2());
+            Tuple2<Boolean, ProjectEnvStatus.Status> health = getProjectEnvStatus(url, e.name());
+            Tuple2<Boolean, DeploymentStatus> ds = kubernetesService.getDeploymentStatus(projectId, e.namespace());
+            Tuple3<Boolean, String, String> pipeline = getProjectPipelineStatus(projectId, e.pipeline(), e.namespace());
+            ProjectEnvStatus pes = new ProjectEnvStatus(e.name());
+
+            if (health.getItem1()){
+                pes.setStatus(health.getItem2());
+            } else if (old != null){
+                Optional<ProjectEnvStatus> opes = old.getStatuses().stream().filter(x -> x.getEnvironment().equals(e.name())).findFirst();
+                if (opes.isPresent()) pes.setStatus(opes.get().getStatus());
+            }
+
+            if (ds.getItem1()){
+                pes.setDeploymentStatus(ds.getItem2());
+            } else if (old != null){
+                Optional<ProjectEnvStatus> opes = old.getStatuses().stream().filter(x -> x.getEnvironment().equals(e.name())).findFirst();
+                if (opes.isPresent()) pes.setDeploymentStatus(opes.get().getDeploymentStatus());
+            }
+
+            if (pipeline.getItem1()){
+                pes.setLastPipelineRun(pipeline.getItem2());
+                pes.setLastPipelineRunResult(pipeline.getItem3());
+            } else if (old != null){
+                Optional<ProjectEnvStatus> opes = old.getStatuses().stream().filter(x -> x.getEnvironment().equals(e.name())).findFirst();
+                if (opes.isPresent()) {
+                    pes.setLastPipelineRun(opes.get().getLastPipelineRun());
+                    pes.setLastPipelineRunResult(opes.get().getLastPipelineRunResult());
+                }
+            }
+            statuses.add(pes);
         });
         status.setStatuses(statuses);
 
-        Project project = infinispanService.getProject(projectId);
-        Tuple2<Boolean, String> p = getProjectPipelineStatus(project.getLastPipelineRun());
-        System.out.println(p.getItem2());
-        if (p.getItem1()) status.setPipeline(p.getItem2());
-
         LOGGER.info("Storing status in cache for " + projectId);
         infinispanService.saveProjectStatus(status);
     }
 
-    private Tuple2<Boolean, String> getProjectPipelineStatus(String lastPipelineRun) {
+    private Tuple3<Boolean, String, String> getProjectPipelineStatus(String projectId, String pipelineName, String namespace) {
         try {
-            PipelineRun pipelineRun = kubernetesService.getPipelineRun(lastPipelineRun, configuration.environments().get(0).namespace());
+            PipelineRun pipelineRun = kubernetesService.getLastPipelineRun(projectId, pipelineName, namespace);
             if (pipelineRun != null) {
-                return Tuple2.of(true, pipelineRun.getStatus().getConditions().get(0).getReason());
+                return Tuple3.of(true, pipelineRun.getMetadata().getName(), pipelineRun.getStatus().getConditions().get(0).getReason());
             } else {
-                return Tuple2.of(true,"Undefined");
+                return Tuple3.of(true,"","Undefined");
             }
         } catch (Exception ex) {
             LOGGER.error(ex.getMessage());
-            return Tuple2.of(false, "Undefined");
+            return Tuple3.of(false, "", "Undefined");
         }
     }
 
-    private Tuple2<Boolean, ProjectEnvStatus> getProjectEnvStatus(String url, String env) {
+    private Tuple2<Boolean, ProjectEnvStatus.Status> getProjectEnvStatus(String url, String env) {
         // TODO: make it reactive
         try {
             HttpResponse<Buffer> result = getWebClient().getAbs(url).timeout(1000).send().subscribeAsCompletionStage().toCompletableFuture().get();
-            if (result.bodyAsJsonObject().getString("status").equals("UP")) {
-                return Tuple2.of(true, new ProjectEnvStatus(env, ProjectEnvStatus.Status.UP));
+            if (result.statusCode() == 200 && result.bodyAsJsonObject().getString("status").equals("UP")) {
+                return Tuple2.of(true, ProjectEnvStatus.Status.UP);
             } else {
-                return Tuple2.of(true, new ProjectEnvStatus(env, ProjectEnvStatus.Status.DOWN));
+                return Tuple2.of(true, ProjectEnvStatus.Status.DOWN);
             }
         } catch (Exception ex) {
             LOGGER.error(ex.getMessage());
-            return Tuple2.of(false, new ProjectEnvStatus(env, ProjectEnvStatus.Status.DOWN));
+            return Tuple2.of(false, ProjectEnvStatus.Status.DOWN);
         }
     }
 
diff --git a/karavan-app/src/main/resources/application.properties b/karavan-app/src/main/resources/application.properties
index 421b93d..0c0680a 100644
--- a/karavan-app/src/main/resources/application.properties
+++ b/karavan-app/src/main/resources/application.properties
@@ -14,17 +14,25 @@ karavan.config.image-group=karavan
 karavan.config.runtime=QUARKUS
 karavan.config.runtime-version=2.10.0.Final
 karavan.config.status-threshold=1000
+
 karavan.config.environments[0].name=dev
 karavan.config.environments[0].namespace=karavan
+karavan.config.environments[0].pipeline=karavan-quarkus
 karavan.config.environments[0].cluster=svc.cluster.local
+
 karavan.config.environments[1].name=test
 karavan.config.environments[1].namespace=test
+karavan.config.environments[1].pipeline=karavan-quarkus
 karavan.config.environments[1].cluster=svc.cluster.local
+
 karavan.config.environments[2].name=prod
 karavan.config.environments[2].namespace=prod
+karavan.config.environments[2].pipeline=karavan-quarkus
 karavan.config.environments[2].cluster=svc.cluster.local
 
-%dev.karavan.config.environments[0].cluster=apps.cluster-ztffv.ztffv.sandbox55.opentlc.com
+%dev.karavan.config.environments[0].cluster=apps.cluster-m8k78.m8k78.sandbox206.opentlc.com
+%dev.karavan.config.environments[1].cluster=apps.cluster-m8k78.m8k78.sandbox206.opentlc.com
+%dev.karavan.config.environments[2].cluster=apps.cluster-m8k78.m8k78.sandbox206.opentlc.com
 
 
 # Infinispan Server address
diff --git a/karavan-app/src/main/webapp/src/builder/BuilderPage.tsx b/karavan-app/src/main/webapp/src/builder/BuilderPage.tsx
index 803c9c4..605521c 100644
--- a/karavan-app/src/main/webapp/src/builder/BuilderPage.tsx
+++ b/karavan-app/src/main/webapp/src/builder/BuilderPage.tsx
@@ -53,7 +53,7 @@ import ProjectIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
 import ClipboardIcon from '@patternfly/react-icons/dist/esm/icons/clipboard-icon';
 import RunIcon from '@patternfly/react-icons/dist/esm/icons/play-circle-icon';
 import {ProjectModel, StepStatus} from "karavan-core/lib/model/ProjectModel";
-import {PropertiesTable} from "../projects/PropertiesTable";
+import {PropertiesTable} from "./PropertiesTable";
 
 interface Props {
     project: ProjectModel,
diff --git a/karavan-app/src/main/webapp/src/index.css b/karavan-app/src/main/webapp/src/index.css
index 1641e8f..521203e 100644
--- a/karavan-app/src/main/webapp/src/index.css
+++ b/karavan-app/src/main/webapp/src/index.css
@@ -140,11 +140,20 @@
   color: white;
 }
 
+.karavan .project-page .env-chart {
+  font-size: 18px;
+}
+
 .karavan .project-page .pipeline-failed {
   background-color: #C9190B;
   color: white;
 }
 
+.karavan .project-page .replicas-ready {
+  background-color: rgb(56, 129, 47);
+  color: white;
+}
+
 .karavan .project-page .pipeline .pf-c-progress-stepper__step-main {
   display: none;
 }
diff --git a/karavan-app/src/main/webapp/src/models/ProjectModels.ts b/karavan-app/src/main/webapp/src/models/ProjectModels.ts
index 28496f1..99f4e4f 100644
--- a/karavan-app/src/main/webapp/src/models/ProjectModels.ts
+++ b/karavan-app/src/main/webapp/src/models/ProjectModels.ts
@@ -3,9 +3,8 @@ export class Project {
     name: string = '';
     description: string = '';
     lastCommit: string = '';
-    lastPipelineRun: string = '';
 
-    public constructor(projectId: string, name: string, description: string, lastCommit: string, lastPipelineRun: string);
+    public constructor(projectId: string, name: string, description: string, lastCommit: string);
     public constructor(init?: Partial<Project>);
     public constructor(...args: any[]) {
         if (args.length === 1){
@@ -16,7 +15,6 @@ export class Project {
             this.name = args[1];
             this.description = args[2];
             this.lastCommit = args[3];
-            this.lastPipelineRun = args[4];
             return;
         }
     }
@@ -25,18 +23,21 @@ export class Project {
 export class ProjectEnvStatus {
     environment: string = '';
     status: string = '';
+    lastPipelineRun: string = '';
+    lastPipelineRunResult: string = '';
+    deploymentStatus: DeploymentStatus = new DeploymentStatus();
+}
 
-    constructor(environment: string, status: string) {
-        this.environment = environment;
-        this.status = status;
-    }
+export class DeploymentStatus {
+    image: string = '';
+    replicas: number = 0;
+    readyReplicas: number = 0;
 }
 
 export class ProjectStatus {
     projectId: string = '';
     lastUpdate: number = 0;
     statuses: ProjectEnvStatus[] = [];
-    pipeline: string = 'Running'
 }
 
 export class ProjectFile {
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectDashboard.tsx b/karavan-app/src/main/webapp/src/projects/ProjectDashboard.tsx
index a49f2bb..6f92e77 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectDashboard.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectDashboard.tsx
@@ -86,27 +86,27 @@ export class ProjectDashboard extends React.Component<Props, State> {
         return (<Text>OK</Text>)
     }
 
-    getPipelineState() {
-        const {project, status} = this.state;
-        const isRunning = status?.pipeline === 'Running';
-        const isFailed = status?.pipeline === 'Failed';
-        const isSucceeded = status?.pipeline === 'Succeeded';
-        let classname = "pipeline"
-        if (isRunning) classname = classname + " pipeline-running";
-        if (isFailed) classname = classname + " pipeline-running";
-        if (isSucceeded) classname = classname + " pipeline-succeeded";
-        return (
-            <Flex spaceItems={{default: 'spaceItemsNone'}} className={classname} direction={{default: "row"}}
-                  alignItems={{default: "alignItemsCenter"}}>
-                <FlexItem style={{height: "18px"}}>
-                    {isRunning && <Spinner isSVG diameter="16px"/>}
-                </FlexItem>
-                <FlexItem style={{height: "18px"}}>
-                    {project?.lastPipelineRun ? project?.lastPipelineRun : "-"}
-                </FlexItem>
-            </Flex>
-        )
-    }
+    // getPipelineState() {
+    //     const {project, status} = this.state;
+    //     const isRunning = status?.pipeline === 'Running';
+    //     const isFailed = status?.pipeline === 'Failed';
+    //     const isSucceeded = status?.pipeline === 'Succeeded';
+    //     let classname = "pipeline"
+    //     if (isRunning) classname = classname + " pipeline-running";
+    //     if (isFailed) classname = classname + " pipeline-running";
+    //     if (isSucceeded) classname = classname + " pipeline-succeeded";
+    //     return (
+    //         <Flex spaceItems={{default: 'spaceItemsNone'}} className={classname} direction={{default: "row"}}
+    //               alignItems={{default: "alignItemsCenter"}}>
+    //             <FlexItem style={{height: "18px"}}>
+    //                 {isRunning && <Spinner isSVG diameter="16px"/>}
+    //             </FlexItem>
+    //             <FlexItem style={{height: "18px"}}>
+    //                 {project?.lastPipelineRun ? project?.lastPipelineRun : "-"}
+    //             </FlexItem>
+    //         </Flex>
+    //     )
+    // }
 
     isUp(env: string): boolean {
         if (this.state.status) {
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx b/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx
index efb4e2a..64610c8 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx
@@ -8,14 +8,18 @@ import {
     DescriptionListGroup,
     DescriptionListDescription,
     Card,
-    CardBody, Spinner, Tooltip, Flex, FlexItem, Tabs, Tab, PageSection
+    CardBody, Tooltip, Flex, FlexItem, Tabs, Tab, PageSection, TextVariants, Spinner, Divider
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {KaravanApi} from "../api/KaravanApi";
 import {Project, ProjectFileTypes, ProjectStatus} from "../models/ProjectModels";
 import BuildIcon from "@patternfly/react-icons/dist/esm/icons/build-icon";
 import PushIcon from "@patternfly/react-icons/dist/esm/icons/code-branch-icon";
+import DeployIcon from "@patternfly/react-icons/dist/esm/icons/cluster-icon";
 import {ProjectDashboard} from "./ProjectDashboard";
+import {ChartDonut, ChartLabel} from "@patternfly/react-charts";
+import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
 
 interface Props {
     project: Project,
@@ -141,32 +145,158 @@ export class ProjectHeader extends React.Component<Props, State> {
         </Tooltip>)
     }
 
+    deployButton = () => {
+        const isDeploying = this.state.isBuilding;
+        return (<Tooltip content="Deploy" position={"left"}>
+            <Button isLoading={isDeploying ? true : undefined} isSmall variant="secondary"
+                    className="project-button"
+                    icon={!isDeploying ? <DeployIcon/> : <div></div>}
+                    onClick={e => {
+                        this.push(() => this.build());
+                    }}>
+                {isDeploying ? "..." : "Deploy"}
+            </Button>
+        </Tooltip>)
+    }
+
     getCurrentStatus() {
         return (<Text>OK</Text>)
     }
 
-    getPipelineState() {
-        const {project, status} = this.state;
-        const isRunning = status?.pipeline === 'Running';
-        const isFailed = status?.pipeline === 'Failed';
-        const isSucceeded = status?.pipeline === 'Succeeded';
+    getCommitPanel() {
+        const {project, environments, status} = this.state;
+        return (<Flex justifyContent={{default:"justifyContentSpaceBetween"}}>
+            <FlexItem>
+                <Tooltip content={project?.lastCommit} position={"right"}>
+                    <Badge>{project?.lastCommit ? project?.lastCommit?.substr(0, 7) : "-"}</Badge>
+                </Tooltip>
+            </FlexItem>
+            <FlexItem>
+                {this.pushButton()}
+            </FlexItem>
+        </Flex>)
+    }
+
+
+    getEnvDonut(env: string) {
+        return (
+            <div style={{height: '50px', width: '50px'}}>
+                    <ChartDonut
+                        constrainToVisibleArea={true}
+                        data={[{x: "", y: 100}]}
+                        colorScale={[this.isUp(env) ? "rgb(56, 129, 47)" : "#8bc1f7"]}
+                        labels={({datum}) => datum.x ? datum.x : null}
+                        title={env}
+                        titleComponent={<ChartLabel style={{fontSize: "56px"}}/>}
+                        radius={100}
+                        innerRadius={80}
+                        style={{border: {width: "20px"}}}
+                    >
+                    </ChartDonut>
+                </div>)
+    }
+
+    getEnvChartPanel(env: string) {
+        return (<div style={{height: '60px', width: '60px'}}>
+            <ChartDonut
+                constrainToVisibleArea={true}
+                data={[{x: "", y: 100}]}
+                colorScale={[this.isUp(env) ? "rgb(56, 129, 47)" : "#8bc1f7"]}
+                labels={({datum}) => datum.x ? datum.x : null}
+                title={env}
+                titleComponent={<ChartLabel style={{fontSize: "56px"}}/>}
+                radius={100}
+                innerRadius={80}
+                style={{border: {width: "20px"}}}
+            >
+            </ChartDonut>
+        </div>)
+    }
+
+    getEnvPanel(env: string) {
+        const {status} = this.state;
+        const deploymentStatus = status?.statuses.find(s => s.environment === env)?.deploymentStatus;
+        const ok = (deploymentStatus && deploymentStatus?.readyReplicas > 0 && deploymentStatus?.replicas === deploymentStatus?.readyReplicas);
+        return (<DescriptionList isHorizontal>
+            <Text style={{fontSize:"1.2em", fontWeight:"bold"}}>Deployment</Text>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Pipeline</DescriptionListTerm>
+                <DescriptionListDescription>{this.getPipelineState(env)}</DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Image</DescriptionListTerm>
+                <DescriptionListDescription>
+                    <Badge>{deploymentStatus ? deploymentStatus.image : "-"}</Badge>
+                </DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Replicas</DescriptionListTerm>
+                <DescriptionListDescription><Badge isRead={!ok}>{deploymentStatus ? deploymentStatus.replicas : "-"}</Badge>
+                </DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Ready replicas</DescriptionListTerm>
+                <DescriptionListDescription><Badge isRead={!ok}>{deploymentStatus ? deploymentStatus.readyReplicas : "-"}</Badge>
+                </DescriptionListDescription>
+            </DescriptionListGroup>
+        </DescriptionList>)
+    }
+
+    getHealthPanel(env: string) {
+        const {status} = this.state;
+        const camelStatus: string = "UP";
+        const routesStatus: string = "UP";
+        const consumersStatus: string = "UP";
+        const contextStatus: string = "UP";
+        return (<DescriptionList isHorizontal>
+            <Text style={{fontSize:"1.2em", fontWeight:"bold"}}>Health</Text>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Status</DescriptionListTerm>
+                <DescriptionListDescription><Badge isRead={camelStatus === "DOWN"}>{camelStatus}</Badge></DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Consumers</DescriptionListTerm>
+                <DescriptionListDescription><Badge isRead={consumersStatus === "DOWN"}>{consumersStatus}</Badge></DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Routes</DescriptionListTerm>
+                <DescriptionListDescription><Badge isRead={routesStatus === "DOWN"}>{routesStatus}</Badge></DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Context</DescriptionListTerm>
+                <DescriptionListDescription><Badge isRead={contextStatus === "DOWN"}>{contextStatus}</Badge></DescriptionListDescription>
+            </DescriptionListGroup>
+        </DescriptionList>)
+    }
+
+    getPipelineState(env: string) {
+        const {status} = this.state;
+        const pipeline = status?.statuses.find(s => s.environment === env)?.lastPipelineRun;
+        const pipelineResult = status?.statuses.find(s => s.environment === env)?.lastPipelineRunResult;
+        const isRunning = pipelineResult === 'Running';
+        const isFailed = pipelineResult === 'Failed';
+        const isSucceeded = pipelineResult === 'Succeeded';
         let classname = "pipeline"
         if (isRunning) classname = classname + " pipeline-running";
         if (isFailed) classname = classname + " pipeline-failed";
         if (isSucceeded) classname = classname + " pipeline-succeeded";
         return (
-            <Tooltip content={status?.pipeline} position={"right"}>
-                <Flex spaceItems={{default: 'spaceItemsNone'}} className={classname} direction={{default: "row"}}
-                      alignItems={{default: "alignItemsCenter"}}>
-                    <FlexItem style={{height: "18px"}}>
-                        {isRunning && <Spinner isSVG diameter="16px"/>}
-                    </FlexItem>
-                    <FlexItem style={{height: "18px"}}>
-                        {project?.lastPipelineRun ? project?.lastPipelineRun : "-"}
-                    </FlexItem>
-                </Flex>
-            </Tooltip>
-
+            <Flex justifyContent={{default:"justifyContentSpaceBetween"}}>
+                <FlexItem>
+                    <Tooltip content={pipelineResult} position={"right"}>
+                        <Flex spaceItems={{default: 'spaceItemsNone'}} className={classname} direction={{default: "row"}}
+                              alignItems={{default: "alignItemsCenter"}}>
+                            <FlexItem style={{height: "18px"}}>
+                                {isRunning && <Spinner isSVG diameter="16px"/>}
+                            </FlexItem>
+                            <FlexItem style={{height: "18px"}}>
+                                <Text>{pipeline ? pipeline : "-"}</Text>
+                            </FlexItem>
+                        </Flex>
+                    </Tooltip>
+                </FlexItem>
+                <FlexItem>{env === "dev" && this.buildButton()}</FlexItem>
+            </Flex>
         )
     }
 
@@ -178,72 +308,49 @@ export class ProjectHeader extends React.Component<Props, State> {
         }
     }
 
+    getProjectDescription() {
+        const {project} = this.state;
+        return (<DescriptionList isHorizontal>
+            <Text style={{fontSize:"1.2em", fontWeight:"bold"}}>Details</Text>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Project ID</DescriptionListTerm>
+                <DescriptionListDescription>{project?.projectId}</DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Name</DescriptionListTerm>
+                <DescriptionListDescription>{project?.name}</DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Description</DescriptionListTerm>
+                <DescriptionListDescription>{project?.description}</DescriptionListDescription>
+            </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Last commit</DescriptionListTerm>
+                <DescriptionListDescription>
+                    {this.getCommitPanel()}
+                </DescriptionListDescription>
+            </DescriptionListGroup>
+        </DescriptionList>)
+    }
+
     getProjectInfo() {
-        const {project, environments, status} = this.state;
         return (
             <Card>
                 <CardBody>
-                    <Flex direction={{default: "row"}} alignContent={{default: "alignContentSpaceBetween"}}
+                    <Flex direction={{default: "row"}}
+                          justifyContent={{default: "justifyContentSpaceBetween"}}
+                          alignItems={{default: "alignItemsFlexStart"}}
                           className="project-details">
                         <FlexItem flex={{default: "flex_1"}}>
-                            <DescriptionList isHorizontal>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Project ID</DescriptionListTerm>
-                                    <DescriptionListDescription>{project?.projectId}</DescriptionListDescription>
-                                </DescriptionListGroup>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Name</DescriptionListTerm>
-                                    <DescriptionListDescription>{project?.name}</DescriptionListDescription>
-                                </DescriptionListGroup>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Description</DescriptionListTerm>
-                                    <DescriptionListDescription>{project?.description}</DescriptionListDescription>
-                                </DescriptionListGroup>
-                            </DescriptionList>
+                                {this.getProjectDescription()}
                         </FlexItem>
+                        <Divider orientation={{default:"vertical"}}/>
                         <FlexItem flex={{default: "flex_1"}}>
-                            <DescriptionList isHorizontal>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Commit</DescriptionListTerm>
-                                    <DescriptionListDescription>
-                                        <Tooltip content={project?.lastCommit} position={"right"}>
-                                            <Badge>{project?.lastCommit ? project?.lastCommit?.substr(0, 7) : "-"}</Badge>
-                                        </Tooltip>
-                                    </DescriptionListDescription>
-                                </DescriptionListGroup>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Pipeline Run</DescriptionListTerm>
-                                    <DescriptionListDescription>
-                                        {this.getPipelineState()}
-                                    </DescriptionListDescription>
-                                </DescriptionListGroup>
-                                <DescriptionListGroup key={this.state.key}>
-                                    <DescriptionListTerm>Status</DescriptionListTerm>
-                                    <DescriptionListDescription>
-                                        <Flex direction={{default: "row"}}>
-                                            {environments.filter(e => e !== undefined)
-                                                .map(e =>
-                                                    <FlexItem key={e}>
-                                                        <Badge className={this.isUp(e) ? "badge-env-up" : ""}
-                                                               isRead>{e}</Badge>
-                                                    </FlexItem>)}
-                                        </Flex>
-                                    </DescriptionListDescription>
-                                </DescriptionListGroup>
-                            </DescriptionList>
+                            {this.getHealthPanel("dev")}
                         </FlexItem>
-                        <FlexItem>
-                            <Flex direction={{default: "column"}}>
-                                <FlexItem>
-                                    {this.pushButton()}
-                                </FlexItem>
-                                <FlexItem>
-                                    {this.buildButton()}
-                                </FlexItem>
-                                <FlexItem>
-                                    <Button isSmall style={{visibility: "hidden"}}>Refresh</Button>
-                                </FlexItem>
-                            </Flex>
+                        <Divider orientation={{default:"vertical"}}/>
+                        <FlexItem flex={{default: "flex_1"}}>
+                            {this.getEnvPanel("dev")}
                         </FlexItem>
                     </Flex>
                 </CardBody>
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx b/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
index 2e348d3..1e0d65a 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
@@ -99,7 +99,7 @@ export class ProjectsPage extends React.Component<Props, State> {
 
     saveAndCloseCreateModal = () => {
         const {name, description, projectId} = this.state;
-        const p = new Project(projectId, name, description, '', '');
+        const p = new Project(projectId, name, description, '');
         this.props.onCreate.call(this, p);
         this.setState({isCreateModalOpen: false, isCopy: false, name: this.props.config.groupId, description: '',  projectId: ''});
     }
diff --git a/karavan-builder/openshift/karavan-quarkus-task.yaml b/karavan-builder/openshift/karavan-quarkus-task.yaml
index 7cc945c..0984d70 100644
--- a/karavan-builder/openshift/karavan-quarkus-task.yaml
+++ b/karavan-builder/openshift/karavan-quarkus-task.yaml
@@ -27,6 +27,7 @@ spec:
           entrypoint -Dcamel.jbang.version=3.18.0-SNAPSHOT camel@apache/camel export
       
           export LAST_COMMIT=$(git rev-parse --short HEAD)
+          export DATE=$(date '+%Y%m%d%H%M%S')
           export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
           export NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
 
@@ -41,7 +42,8 @@ spec:
             -Dquarkus.kubernetes-client.master-url=kubernetes.default.svc \
             -Dquarkus.kubernetes-client.token=${TOKEN} \
             -Dquarkus.kubernetes.deploy=true \
-            -Dquarkus.container-image.group=${NAMESPACE}
+            -Dquarkus.container-image.group=${NAMESPACE} \
+            -Dquarkus.container-image.tag=${DATE}
       image: 'ghcr.io/apache/camel-karavan-builder:0.0.16'
       volumeMounts:
         - mountPath: /root/.m2