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/10/28 00:16:58 UTC

[camel-karavan] 08/08: First version of Dashboard

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 17e5855590bafd18dd80604d25389d2b852513b6
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Thu Oct 27 20:16:37 2022 -0400

    First version of Dashboard
---
 .../camel/karavan/api/ConfigurationResource.java   |  11 +-
 .../camel/karavan/api/KubernetesResource.java      |  19 ++
 .../apache/camel/karavan/api/StatusResource.java   |  14 +-
 .../DeploymentEventHandler.java                    |  19 +-
 .../PipelineRunEventHandler.java                   |   2 +-
 .../{watcher => informer}/PodEventHandler.java     |   8 +-
 .../karavan/informer/ServiceEventHandler.java      |  91 +++++++
 .../camel/karavan/model/DeploymentStatus.java      |  29 ++-
 .../camel/karavan/model/ProjectStoreSchema.java    |   2 +-
 .../apache/camel/karavan/model/ServiceStatus.java  | 111 +++++++++
 .../camel/karavan/service/InfinispanService.java   |  25 +-
 .../camel/karavan/service/KubernetesService.java   |  18 +-
 karavan-app/src/main/webapp/src/Main.tsx           |  47 ++--
 karavan-app/src/main/webapp/src/api/KaravanApi.tsx |  24 +-
 .../main/webapp/src/dashboard/DashboardPage.tsx    | 268 +++++++++++++++++++++
 karavan-app/src/main/webapp/src/index.css          |  50 +++-
 .../main/webapp/src/projects/ProjectDashboard.tsx  |   1 -
 .../src/main/webapp/src/projects/ProjectInfo.tsx   |   3 -
 .../src/main/webapp/src/projects/ProjectModels.ts  |  12 +
 .../src/main/webapp/src/projects/ProjectsPage.tsx  |  21 +-
 20 files changed, 712 insertions(+), 63 deletions(-)

diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java
index 7980e61..0c34dad 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/ConfigurationResource.java
@@ -50,7 +50,16 @@ public class ConfigurationResource {
                 Map.of(
                         "version", version,
                         "environment", environment,
-                        "environments", infinispanService.getEnvironments().stream().map(e -> e.getName()).collect(Collectors.toList()),
+                        "environments", infinispanService.getEnvironments().stream()
+                                .map(e -> e.getName())
+                                        .sorted((o1, o2) -> {
+                                            if (o1.startsWith("dev") && o2.startsWith("test")) return -1;
+                                            if (o1.startsWith("test") && o2.startsWith("dev")) return 1;
+                                            if (o1.startsWith("test") && o2.startsWith("prod")) return -1;
+                                            if (o1.startsWith("prod") && o2.startsWith("test")) return 1;
+                                            return o1.compareTo(o2);
+                                        })
+                                .collect(Collectors.toList()),
                         "runtime", runtime
                 )
         ).build();
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/KubernetesResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/KubernetesResource.java
index ebe0cc1..d1553cb 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/api/KubernetesResource.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/KubernetesResource.java
@@ -22,6 +22,7 @@ import io.vertx.mutiny.core.eventbus.Message;
 import org.apache.camel.karavan.model.DeploymentStatus;
 import org.apache.camel.karavan.model.PodStatus;
 import org.apache.camel.karavan.model.Project;
+import org.apache.camel.karavan.model.ServiceStatus;
 import org.apache.camel.karavan.service.InfinispanService;
 import org.apache.camel.karavan.service.KubernetesService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -98,6 +99,15 @@ public class KubernetesResource {
         return Response.ok(kubernetesService.getContainerLog(name, kubernetesService.getNamespace())).build();
     }
 
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/deployment")
+    public List<DeploymentStatus> getAllDeploymentStatuses() throws Exception {
+        return infinispanService.getDeploymentStatuses().stream()
+                .sorted(Comparator.comparing(DeploymentStatus::getName))
+                .collect(Collectors.toList());
+    }
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/deployment/{env}")
@@ -128,6 +138,15 @@ public class KubernetesResource {
         return Response.ok().build();
     }
 
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/service")
+    public List<ServiceStatus> getAllServiceStatuses() throws Exception {
+        return infinispanService.getServiceStatuses().stream()
+                .sorted(Comparator.comparing(ServiceStatus::getName))
+                .collect(Collectors.toList());
+    }
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/pod/{env}")
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 f869b26..5f88aec 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
@@ -19,6 +19,7 @@ package org.apache.camel.karavan.api;
 import io.vertx.core.eventbus.EventBus;
 import org.apache.camel.karavan.model.CamelStatus;
 import org.apache.camel.karavan.model.DeploymentStatus;
+import org.apache.camel.karavan.model.Environment;
 import org.apache.camel.karavan.model.PipelineStatus;
 import org.apache.camel.karavan.service.InfinispanService;
 import org.apache.camel.karavan.service.StatusService;
@@ -31,6 +32,7 @@ import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import java.util.Optional;
 
 @Path("/api/status")
 public class StatusResource {
@@ -59,12 +61,14 @@ public class StatusResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/deployment/{name}/{env}")
     public Response getDeploymentStatus(@PathParam("name") String name, @PathParam("env") String env) {
-        DeploymentStatus status = infinispanService.getDeploymentStatus(name, env);
-        if (status != null) {
-            return Response.ok(status).build();
-        } else {
-            return Response.noContent().build();
+        Optional<Environment> environment = infinispanService.getEnvironments().stream().filter(e -> e.getName().equals(env)).findFirst();
+        if (environment.isPresent()){
+            DeploymentStatus status = infinispanService.getDeploymentStatus(name, environment.get().getNamespace(), environment.get().getCluster());
+            if (status != null) {
+                return Response.ok(status).build();
+            }
         }
+        return Response.noContent().build();
     }
 
     @GET
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/watcher/DeploymentEventHandler.java b/karavan-app/src/main/java/org/apache/camel/karavan/informer/DeploymentEventHandler.java
similarity index 76%
rename from karavan-app/src/main/java/org/apache/camel/karavan/watcher/DeploymentEventHandler.java
rename to karavan-app/src/main/java/org/apache/camel/karavan/informer/DeploymentEventHandler.java
index b4f4323..7ba4793 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/watcher/DeploymentEventHandler.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/informer/DeploymentEventHandler.java
@@ -1,8 +1,9 @@
-package org.apache.camel.karavan.watcher;
+package org.apache.camel.karavan.informer;
 
 import io.fabric8.kubernetes.api.model.apps.Deployment;
 import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
 import org.apache.camel.karavan.model.DeploymentStatus;
+import org.apache.camel.karavan.model.Environment;
 import org.apache.camel.karavan.service.InfinispanService;
 import org.apache.camel.karavan.service.KubernetesService;
 import org.jboss.logging.Logger;
@@ -24,6 +25,14 @@ public class DeploymentEventHandler implements ResourceEventHandler<Deployment>
             LOGGER.info("onAdd " + deployment.getMetadata().getName());
             DeploymentStatus ds = getDeploymentStatus(deployment);
             infinispanService.saveDeploymentStatus(ds);
+            // TODO: Delete after UI design
+            infinispanService.saveEnvironment(new Environment("test", "test", "karavan-test", "test-pipeline"));
+            infinispanService.saveEnvironment(new Environment("prod", "prod", "karavan-prod", "prod-pipeline"));
+            DeploymentStatus ds1 = getDeploymentStatus(deployment);
+            ds1.setEnv("test");
+            ds1.setNamespace("karavan-test");
+            ds1.setCluster("demo.cluster");
+            infinispanService.saveDeploymentStatus(ds1);
         } catch (Exception e){
             LOGGER.error(e.getMessage());
         }
@@ -35,6 +44,11 @@ public class DeploymentEventHandler implements ResourceEventHandler<Deployment>
             LOGGER.info("onUpdate " + newDeployment.getMetadata().getName());
             DeploymentStatus ds = getDeploymentStatus(newDeployment);
             infinispanService.saveDeploymentStatus(ds);
+            // TODO: Delete after UI design
+            DeploymentStatus ds1 = getDeploymentStatus(newDeployment);
+            ds1.setEnv("test");
+            ds1.setCluster("demo.cluster");
+            infinispanService.saveDeploymentStatus(ds1);
         } catch (Exception e){
             LOGGER.error(e.getMessage());
         }
@@ -47,6 +61,7 @@ public class DeploymentEventHandler implements ResourceEventHandler<Deployment>
             DeploymentStatus ds = new DeploymentStatus(
                     deployment.getMetadata().getName(),
                     deployment.getMetadata().getNamespace(),
+                    kubernetesService.getCluster(),
                     kubernetesService.environment);
             infinispanService.deleteDeploymentStatus(ds);
         } catch (Exception e){
@@ -65,6 +80,7 @@ public class DeploymentEventHandler implements ResourceEventHandler<Deployment>
                     deployment.getMetadata().getName(),
                     deployment.getMetadata().getNamespace(),
                     kubernetesService.environment,
+                    kubernetesService.getCluster(),
                     imageName,
                     deployment.getSpec().getReplicas(),
                     deployment.getStatus().getReadyReplicas(),
@@ -75,6 +91,7 @@ public class DeploymentEventHandler implements ResourceEventHandler<Deployment>
             return new DeploymentStatus(
                     deployment.getMetadata().getName(),
                     deployment.getMetadata().getNamespace(),
+                    kubernetesService.getCluster(),
                     kubernetesService.environment);
         }
     }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/watcher/PipelineRunEventHandler.java b/karavan-app/src/main/java/org/apache/camel/karavan/informer/PipelineRunEventHandler.java
similarity index 99%
rename from karavan-app/src/main/java/org/apache/camel/karavan/watcher/PipelineRunEventHandler.java
rename to karavan-app/src/main/java/org/apache/camel/karavan/informer/PipelineRunEventHandler.java
index b5d59e9..3254e26 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/watcher/PipelineRunEventHandler.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/informer/PipelineRunEventHandler.java
@@ -1,4 +1,4 @@
-package org.apache.camel.karavan.watcher;
+package org.apache.camel.karavan.informer;
 
 import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRun;
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/watcher/PodEventHandler.java b/karavan-app/src/main/java/org/apache/camel/karavan/informer/PodEventHandler.java
similarity index 91%
rename from karavan-app/src/main/java/org/apache/camel/karavan/watcher/PodEventHandler.java
rename to karavan-app/src/main/java/org/apache/camel/karavan/informer/PodEventHandler.java
index 8d3af7c..4e68379 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/watcher/PodEventHandler.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/informer/PodEventHandler.java
@@ -1,4 +1,4 @@
-package org.apache.camel.karavan.watcher;
+package org.apache.camel.karavan.informer;
 
 import io.fabric8.kubernetes.api.model.Pod;
 import io.fabric8.kubernetes.api.model.PodCondition;
@@ -27,6 +27,9 @@ public class PodEventHandler implements ResourceEventHandler<Pod> {
             LOGGER.info("onAdd " + pod.getMetadata().getName());
             PodStatus ps = getPodStatus(pod);
             infinispanService.savePodStatus(ps);
+            // TODO: Delete after UI design
+            ps.setEnv("test");
+            infinispanService.savePodStatus(ps);
         } catch (Exception e){
             LOGGER.error(e.getMessage());
         }
@@ -38,6 +41,9 @@ public class PodEventHandler implements ResourceEventHandler<Pod> {
             LOGGER.info("onUpdate " + newPod.getMetadata().getName());
             PodStatus ps = getPodStatus(newPod);
             infinispanService.savePodStatus(ps);
+            // TODO: Delete after UI design
+            ps.setEnv("test");
+            infinispanService.savePodStatus(ps);
         } catch (Exception e){
             LOGGER.error(e.getMessage());
         }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/informer/ServiceEventHandler.java b/karavan-app/src/main/java/org/apache/camel/karavan/informer/ServiceEventHandler.java
new file mode 100644
index 0000000..dc7382a
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/informer/ServiceEventHandler.java
@@ -0,0 +1,91 @@
+package org.apache.camel.karavan.informer;
+
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
+import org.apache.camel.karavan.model.ServiceStatus;
+import org.apache.camel.karavan.model.Environment;
+import org.apache.camel.karavan.service.InfinispanService;
+import org.apache.camel.karavan.service.KubernetesService;
+import org.jboss.logging.Logger;
+
+public class ServiceEventHandler implements ResourceEventHandler<Service> {
+
+    private static final Logger LOGGER = Logger.getLogger(ServiceEventHandler.class.getName());
+    private InfinispanService infinispanService;
+    private KubernetesService kubernetesService;
+
+    public ServiceEventHandler(InfinispanService infinispanService, KubernetesService kubernetesService) {
+        this.infinispanService = infinispanService;
+        this.kubernetesService = kubernetesService;
+    }
+
+    @Override
+    public void onAdd(Service service) {
+        try {
+            LOGGER.info("onAdd " + service.getMetadata().getName());
+            ServiceStatus ds = getServiceStatus(service);
+            infinispanService.saveServiceStatus(ds);
+            // TODO: Delete after UI design
+            infinispanService.saveEnvironment(new Environment("test", "demo", "karavan-test", "test-pipeline"));
+            ServiceStatus ds1 = getServiceStatus(service);
+            ds1.setEnv("test");
+            ds1.setCluster("demo.cluster");
+            infinispanService.saveServiceStatus(ds1);
+        } catch (Exception e){
+            LOGGER.error(e.getMessage());
+        }
+    }
+
+    @Override
+    public void onUpdate(Service oldService, Service newService) {
+        try {
+            LOGGER.info("onUpdate " + newService.getMetadata().getName());
+            ServiceStatus ds = getServiceStatus(newService);
+            infinispanService.saveServiceStatus(ds);
+            // TODO: Delete after UI design
+            ServiceStatus ds1 = getServiceStatus(newService);
+            ds1.setEnv("test");
+            ds1.setCluster("demo.cluster");
+            infinispanService.saveServiceStatus(ds1);
+        } catch (Exception e){
+            LOGGER.error(e.getMessage());
+        }
+    }
+
+    @Override
+    public void onDelete(Service service, boolean deletedFinalStateUnknown) {
+        try {
+            LOGGER.info("onDelete " + service.getMetadata().getName());
+            ServiceStatus ds = new ServiceStatus(
+                    service.getMetadata().getName(),
+                    service.getMetadata().getNamespace(),
+                    kubernetesService.getCluster(),
+                    kubernetesService.environment);
+            infinispanService.deleteServiceStatus(ds);
+        } catch (Exception e){
+            LOGGER.error(e.getMessage());
+        }
+    }
+
+    public ServiceStatus getServiceStatus(Service service) {
+        try {
+            return new ServiceStatus(
+                    service.getMetadata().getName(),
+                    service.getMetadata().getNamespace(),
+                    kubernetesService.environment,
+                    kubernetesService.getCluster(),
+                    service.getSpec().getPorts().get(0).getPort(),
+                    service.getSpec().getPorts().get(0).getTargetPort().getIntVal(),
+                    service.getSpec().getClusterIP(),
+                    service.getSpec().getType()
+            );
+        } catch (Exception ex) {
+            LOGGER.error(ex.getMessage());
+            return new ServiceStatus(
+                    service.getMetadata().getName(),
+                    service.getMetadata().getNamespace(),
+                    kubernetesService.getCluster(),
+                    kubernetesService.environment);
+        }
+    }
+}
\ No newline at end of file
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
index f0baa9e..fe50512 100644
--- 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
@@ -1,6 +1,5 @@
 package org.apache.camel.karavan.model;
 
-import org.infinispan.protostream.annotations.ProtoDoc;
 import org.infinispan.protostream.annotations.ProtoFactory;
 import org.infinispan.protostream.annotations.ProtoField;
 
@@ -13,17 +12,20 @@ public class DeploymentStatus {
     @ProtoField(number = 3)
     String env;
     @ProtoField(number = 4)
-    String image;
+    String cluster;
     @ProtoField(number = 5)
-    Integer replicas;
+    String image;
     @ProtoField(number = 6)
-    Integer readyReplicas;
+    Integer replicas;
     @ProtoField(number = 7)
+    Integer readyReplicas;
+    @ProtoField(number = 8)
     Integer unavailableReplicas;
 
-    public DeploymentStatus(String name, String namespace, String env) {
+    public DeploymentStatus(String name, String namespace, String cluster, String env) {
         this.name = name;
         this.namespace = namespace;
+        this.cluster = cluster;
         this.env = env;
         this.image = "";
         this.replicas = 0;
@@ -32,16 +34,21 @@ public class DeploymentStatus {
     }
 
     @ProtoFactory
-    public DeploymentStatus(String name, String namespace, String env, String image, Integer replicas, Integer readyReplicas, Integer unavailableReplicas) {
+    public DeploymentStatus(String name, String namespace, String env, String cluster, String image, Integer replicas, Integer readyReplicas, Integer unavailableReplicas) {
         this.name = name;
-        this.env = env;
         this.namespace = namespace;
+        this.env = env;
+        this.cluster = cluster;
         this.image = image;
         this.replicas = replicas;
         this.readyReplicas = readyReplicas;
         this.unavailableReplicas = unavailableReplicas;
     }
 
+    public String getId() {
+        return name + ":" + namespace + ":" + cluster;
+    }
+
     public String getName() {
         return name;
     }
@@ -97,4 +104,12 @@ public class DeploymentStatus {
     public void setUnavailableReplicas(Integer unavailableReplicas) {
         this.unavailableReplicas = unavailableReplicas;
     }
+
+    public String getCluster() {
+        return cluster;
+    }
+
+    public void setCluster(String cluster) {
+        this.cluster = cluster;
+    }
 }
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 6a94007..c9b1e7a 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
@@ -6,7 +6,7 @@ import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
 @AutoProtoSchemaBuilder(
         includeClasses = {
                 GroupedKey.class, Project.class, ProjectFile.class, PipelineStatus.class, CamelStatus.class, DeploymentStatus.class,
-                PodStatus.class, Environment.class
+                PodStatus.class, Environment.class, ServiceStatus.class
         },
         schemaPackageName = "karavan")
 public interface ProjectStoreSchema extends GeneratedSchema {
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/ServiceStatus.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/ServiceStatus.java
new file mode 100644
index 0000000..38dbde5
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/ServiceStatus.java
@@ -0,0 +1,111 @@
+package org.apache.camel.karavan.model;
+
+import org.infinispan.protostream.annotations.ProtoFactory;
+import org.infinispan.protostream.annotations.ProtoField;
+
+public class ServiceStatus {
+    public static final String CACHE = "service_statuses";
+    @ProtoField(number = 1)
+    String name;
+    @ProtoField(number = 2)
+    String namespace;
+    @ProtoField(number = 3)
+    String env;
+    @ProtoField(number = 4)
+    String cluster;
+    @ProtoField(number = 5)
+    Integer port;
+    @ProtoField(number = 6)
+    Integer targetPort;
+    @ProtoField(number = 7)
+    String clusterIP;
+    @ProtoField(number = 8)
+    String type;
+
+    @ProtoFactory
+    public ServiceStatus(String name, String namespace, String env, String cluster, Integer port, Integer targetPort, String clusterIP, String type) {
+        this.name = name;
+        this.namespace = namespace;
+        this.env = env;
+        this.cluster = cluster;
+        this.port = port;
+        this.targetPort = targetPort;
+        this.clusterIP = clusterIP;
+        this.type = type;
+    }
+
+    public ServiceStatus(String name, String namespace, String cluster, String env) {
+        this.name = name;
+        this.namespace = namespace;
+        this.env = env;
+        this.cluster = cluster;
+    }
+
+    public String getId() {
+        return name + ":" + namespace + ":" + cluster;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getNamespace() {
+        return namespace;
+    }
+
+    public void setNamespace(String namespace) {
+        this.namespace = namespace;
+    }
+
+    public String getEnv() {
+        return env;
+    }
+
+    public void setEnv(String env) {
+        this.env = env;
+    }
+
+    public String getCluster() {
+        return cluster;
+    }
+
+    public void setCluster(String cluster) {
+        this.cluster = cluster;
+    }
+
+    public Integer getPort() {
+        return port;
+    }
+
+    public void setPort(Integer port) {
+        this.port = port;
+    }
+
+    public Integer getTargetPort() {
+        return targetPort;
+    }
+
+    public void setTargetPort(Integer targetPort) {
+        this.targetPort = targetPort;
+    }
+
+    public String getClusterIP() {
+        return clusterIP;
+    }
+
+    public void setClusterIP(String clusterIP) {
+        this.clusterIP = clusterIP;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+}
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/InfinispanService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/InfinispanService.java
index 3fddb8f..aa4026a 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/service/InfinispanService.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/InfinispanService.java
@@ -25,6 +25,7 @@ import org.apache.camel.karavan.model.PipelineStatus;
 import org.apache.camel.karavan.model.PodStatus;
 import org.apache.camel.karavan.model.Project;
 import org.apache.camel.karavan.model.ProjectFile;
+import org.apache.camel.karavan.model.ServiceStatus;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.infinispan.Cache;
 import org.infinispan.client.hotrod.RemoteCache;
@@ -56,6 +57,7 @@ public class InfinispanService {
     BasicCache<GroupedKey, DeploymentStatus> deploymentStatuses;
     BasicCache<GroupedKey, PodStatus> podStatuses;
     BasicCache<GroupedKey, CamelStatus> camelStatuses;
+    BasicCache<GroupedKey, ServiceStatus> serviceStatuses;
     BasicCache<String, String> kamelets;
     BasicCache<String, Environment> environments;
 
@@ -95,6 +97,7 @@ public class InfinispanService {
             pipelineStatuses = cacheManager.administration().withFlags(CacheContainerAdmin.AdminFlag.VOLATILE).getOrCreateCache(PipelineStatus.CACHE, builder.build());
             deploymentStatuses = cacheManager.administration().withFlags(CacheContainerAdmin.AdminFlag.VOLATILE).getOrCreateCache(DeploymentStatus.CACHE, builder.build());
             podStatuses = cacheManager.administration().withFlags(CacheContainerAdmin.AdminFlag.VOLATILE).getOrCreateCache(PodStatus.CACHE, builder.build());
+            serviceStatuses = cacheManager.administration().withFlags(CacheContainerAdmin.AdminFlag.VOLATILE).getOrCreateCache(ServiceStatus.CACHE, builder.build());
             camelStatuses = cacheManager.administration().withFlags(CacheContainerAdmin.AdminFlag.VOLATILE).getOrCreateCache(CamelStatus.CACHE, builder.build());
             kamelets = cacheManager.administration().withFlags(CacheContainerAdmin.AdminFlag.VOLATILE).getOrCreateCache(Kamelet.CACHE, builder.build());
 
@@ -107,6 +110,7 @@ public class InfinispanService {
             pipelineStatuses = cacheManager.administration().getOrCreateCache(PipelineStatus.CACHE, new XMLStringConfiguration(String.format(CACHE_CONFIG, PipelineStatus.CACHE)));
             deploymentStatuses = cacheManager.administration().getOrCreateCache(DeploymentStatus.CACHE, new XMLStringConfiguration(String.format(CACHE_CONFIG, DeploymentStatus.CACHE)));
             podStatuses = cacheManager.administration().getOrCreateCache(PodStatus.CACHE, new XMLStringConfiguration(String.format(CACHE_CONFIG, PodStatus.CACHE)));
+            serviceStatuses = cacheManager.administration().getOrCreateCache(ServiceStatus.CACHE, new XMLStringConfiguration(String.format(CACHE_CONFIG, ServiceStatus.CACHE)));
             camelStatuses = cacheManager.administration().getOrCreateCache(CamelStatus.CACHE, new XMLStringConfiguration(String.format(CACHE_CONFIG, CamelStatus.CACHE)));
             kamelets = cacheManager.administration().getOrCreateCache(Kamelet.CACHE, new XMLStringConfiguration(String.format(CACHE_CONFIG, Kamelet.CACHE)));
         }
@@ -180,16 +184,17 @@ public class InfinispanService {
         pipelineStatuses.remove(GroupedKey.create(status.getProjectId(), status.getEnv()));
     }
 
-    public DeploymentStatus getDeploymentStatus(String name, String env) {
-        return deploymentStatuses.get(GroupedKey.create(name, env));
+    public DeploymentStatus getDeploymentStatus(String name, String namespace, String cluster) {
+        String deploymentId = name + ":" + namespace + ":" + cluster;
+        return deploymentStatuses.get(GroupedKey.create(name, deploymentId));
     }
 
     public void saveDeploymentStatus(DeploymentStatus status) {
-        deploymentStatuses.put(GroupedKey.create(status.getName(), status.getEnv()), status);
+        deploymentStatuses.put(GroupedKey.create(status.getName(), status.getId()), status);
     }
 
     public void deleteDeploymentStatus(DeploymentStatus status) {
-        deploymentStatuses.remove(GroupedKey.create(status.getName(), status.getEnv()));
+        deploymentStatuses.remove(GroupedKey.create(status.getName(), status.getId()));
     }
 
     public List<DeploymentStatus> getDeploymentStatuses() {
@@ -210,6 +215,18 @@ public class InfinispanService {
         }
     }
 
+    public void saveServiceStatus(ServiceStatus status) {
+        serviceStatuses.put(GroupedKey.create(status.getName(), status.getId()), status);
+    }
+
+    public void deleteServiceStatus(ServiceStatus status) {
+        serviceStatuses.remove(GroupedKey.create(status.getName(), status.getId()));
+    }
+
+    public List<ServiceStatus> getServiceStatuses() {
+        return serviceStatuses.values().stream().collect(Collectors.toList());
+    }
+
     public List<PodStatus> getPodStatuses(String projectId, String env) {
         if (cacheManager == null) {
             QueryFactory queryFactory = org.infinispan.query.Search.getQueryFactory((Cache<?, ?>) podStatuses);
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 b896013..c6fdea4 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
@@ -20,15 +20,12 @@ import io.fabric8.kubernetes.api.model.ObjectMeta;
 import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
 import io.fabric8.kubernetes.api.model.Pod;
 import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.Service;
 import io.fabric8.kubernetes.api.model.apps.Deployment;
 import io.fabric8.kubernetes.client.DefaultKubernetesClient;
 import io.fabric8.kubernetes.client.KubernetesClient;
-import io.fabric8.kubernetes.client.Watch;
-import io.fabric8.kubernetes.client.dsl.Informable;
 import io.fabric8.kubernetes.client.dsl.LogWatch;
 import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
-import io.fabric8.kubernetes.client.informers.SharedInformerEventListener;
-import io.fabric8.kubernetes.client.informers.SharedInformerFactory;
 import io.fabric8.openshift.api.model.ImageStream;
 import io.fabric8.openshift.client.OpenShiftClient;
 import io.fabric8.tekton.client.DefaultTektonClient;
@@ -40,19 +37,18 @@ import io.fabric8.tekton.pipeline.v1beta1.PipelineRunBuilder;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRunSpec;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRunSpecBuilder;
 import io.fabric8.tekton.pipeline.v1beta1.WorkspaceBindingBuilder;
-import io.quarkus.runtime.ShutdownEvent;
 import io.quarkus.vertx.ConsumeEvent;
 import io.vertx.mutiny.core.eventbus.EventBus;
+import org.apache.camel.karavan.informer.ServiceEventHandler;
 import org.apache.camel.karavan.model.PipelineRunLog;
 import org.apache.camel.karavan.model.Project;
-import org.apache.camel.karavan.watcher.DeploymentEventHandler;
-import org.apache.camel.karavan.watcher.PipelineRunEventHandler;
-import org.apache.camel.karavan.watcher.PodEventHandler;
+import org.apache.camel.karavan.informer.DeploymentEventHandler;
+import org.apache.camel.karavan.informer.PipelineRunEventHandler;
+import org.apache.camel.karavan.informer.PodEventHandler;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
 import javax.enterprise.context.ApplicationScoped;
-import javax.enterprise.event.Observes;
 import javax.enterprise.inject.Produces;
 import javax.inject.Inject;
 import java.io.IOException;
@@ -110,6 +106,10 @@ public class KubernetesService {
             deploymentInformer.addEventHandlerWithResyncPeriod(new DeploymentEventHandler(infinispanService, this),30 * 1000L);
             informers.add(deploymentInformer);
 
+            SharedIndexInformer<Service> serviceInformer = kubernetesClient().services().inNamespace(getNamespace()).withLabel(runtimeLabel, "camel").inform();
+            serviceInformer.addEventHandlerWithResyncPeriod(new ServiceEventHandler(infinispanService, this),30 * 1000L);
+            informers.add(serviceInformer);
+
             SharedIndexInformer<PipelineRun> pipelineRunInformer = tektonClient().v1beta1().pipelineRuns().inNamespace(getNamespace()).withLabel(runtimeLabel, "camel").inform();
             pipelineRunInformer.addEventHandlerWithResyncPeriod(new PipelineRunEventHandler(infinispanService, this),30 * 1000L);
             informers.add(pipelineRunInformer);
diff --git a/karavan-app/src/main/webapp/src/Main.tsx b/karavan-app/src/main/webapp/src/Main.tsx
index 4d7acb5..f69f8d8 100644
--- a/karavan-app/src/main/webapp/src/Main.tsx
+++ b/karavan-app/src/main/webapp/src/Main.tsx
@@ -1,9 +1,7 @@
 import React from 'react';
 import {
     Page,
-    ModalVariant,
     Button,
-    Modal,
     Alert,
     AlertActionCloseButton,
     Flex,
@@ -13,7 +11,7 @@ import {
 } from '@patternfly/react-core';
 import {KaravanApi} from "./api/KaravanApi";
 import {SsoApi} from "./api/SsoApi";
-import {KameletApi, Kamelets} from "karavan-core/lib/api/KameletApi";
+import {KameletApi} from "karavan-core/lib/api/KameletApi";
 import './designer/karavan.css';
 import {ConfigurationPage} from "./config/ConfigurationPage";
 import {KameletsPage} from "./kamelets/KameletsPage";
@@ -28,10 +26,12 @@ import {ProjectPage} from "./projects/ProjectPage";
 import UserIcon from "@patternfly/react-icons/dist/js/icons/user-icon";
 import ProjectsIcon from "@patternfly/react-icons/dist/js/icons/repository-icon";
 import KameletsIcon from "@patternfly/react-icons/dist/js/icons/registry-icon";
+import DashboardIcon from "@patternfly/react-icons/dist/js/icons/tachometer-alt-icon";
 import EipIcon from "@patternfly/react-icons/dist/js/icons/topology-icon";
 import ComponentsIcon from "@patternfly/react-icons/dist/js/icons/module-icon";
 import ConfigurationIcon from "@patternfly/react-icons/dist/js/icons/cogs-icon";
 import {MainLogin} from "./MainLogin";
+import {DashboardPage} from "./dashboard/DashboardPage";
 
 class ToastMessage {
     id: string = ''
@@ -124,7 +124,7 @@ export class Main extends React.Component<Props, State> {
 
     getData() {
         KaravanApi.getConfiguration((config: any) => {
-            this.setState({ config: config });
+            this.setState({config: config});
         });
         this.updateKamelets();
         this.updateComponents();
@@ -157,16 +157,18 @@ export class Main extends React.Component<Props, State> {
 
     pageNav = () => {
         const pages: MenuItem[] = [
-            // new MenuItem("dashboard", "Dashboard", <TachometerAltIcon/>),
+            new MenuItem("dashboard", "Dashboard", <DashboardIcon/>),
             new MenuItem("projects", "Projects", <ProjectsIcon/>),
             new MenuItem("eip", "Enterprise Integration Patterns", <EipIcon/>),
             new MenuItem("kamelets", "Kamelets", <KameletsIcon/>),
             new MenuItem("components", "Components", <ComponentsIcon/>),
             new MenuItem("configuration", "Configuration", <ConfigurationIcon/>)
         ]
-        return (<Flex className="nav-buttons" direction={{default: "column"}} style={{height:"100%"}} spaceItems={{default:"spaceItemsNone"}}>
-            <FlexItem alignSelf={{default:"alignSelfCenter"}}>
-                <Tooltip className="logo-tooltip" content={"Apache Camel Karavan " + this.state.config.version} position={"right"}>
+        return (<Flex className="nav-buttons" direction={{default: "column"}} style={{height: "100%"}}
+                      spaceItems={{default: "spaceItemsNone"}}>
+            <FlexItem alignSelf={{default: "alignSelfCenter"}}>
+                <Tooltip className="logo-tooltip" content={"Apache Camel Karavan " + this.state.config.version}
+                         position={"right"}>
                     {Icon()}
                 </Tooltip>
             </FlexItem>
@@ -180,11 +182,11 @@ export class Main extends React.Component<Props, State> {
                     </Tooltip>
                 </FlexItem>
             )}
-            <FlexItem flex={{default:"flex_2"}} alignSelf={{default:"alignSelfCenter"}}>
+            <FlexItem flex={{default: "flex_2"}} alignSelf={{default: "alignSelfCenter"}}>
                 <Divider/>
             </FlexItem>
             {KaravanApi.authType !== 'public' &&
-                <FlexItem alignSelf={{default:"alignSelfCenter"}}>
+                <FlexItem alignSelf={{default: "alignSelfCenter"}}>
                     <Popover
                         aria-label="Current user"
                         position={"right-end"}
@@ -194,7 +196,7 @@ export class Main extends React.Component<Props, State> {
                         shouldOpen={tip => this.setState({showUser: true})}
                         headerContent={<div>{KaravanApi.me.userName}</div>}
                         bodyContent={
-                            <Flex direction={{default:"row"}}>
+                            <Flex direction={{default: "row"}}>
                                 {KaravanApi.me.roles && Array.isArray(KaravanApi.me.roles)
                                     && KaravanApi.me.roles
                                         .filter((r: string) => ['administrator', 'developer', 'viewer'].includes(r))
@@ -221,20 +223,28 @@ export class Main extends React.Component<Props, State> {
     getMain() {
         return (
             <>
-                <Flex direction={{default:"row"}} style={{width: "100%", height:"100%"}} alignItems={{default:"alignItemsStretch"}} spaceItems={{ default: 'spaceItemsNone' }}>
+                <Flex direction={{default: "row"}} style={{width: "100%", height: "100%"}}
+                      alignItems={{default: "alignItemsStretch"}} spaceItems={{default: 'spaceItemsNone'}}>
                     <FlexItem>
                         {this.pageNav()}
                     </FlexItem>
-                    <FlexItem flex={{default:"flex_2"}} style={{height:"100%"}}>
+                    <FlexItem flex={{default: "flex_2"}} style={{height: "100%"}}>
                         {this.state.pageId === 'projects' &&
                             <ProjectsPage key={this.state.request}
                                           onSelect={this.onProjectSelect}
                                           toast={this.toast}
                                           config={this.state.config}/>}
-                        {this.state.pageId === 'project' && this.state.project && <ProjectPage project={this.state.project} config={this.state.config}/>}
+                        {this.state.pageId === 'project' && this.state.project &&
+                            <ProjectPage project={this.state.project} config={this.state.config}/>}
+                        {this.state.pageId === 'dashboard' && <DashboardPage key={this.state.request}
+                                                                             onSelect={this.onProjectSelect}
+                                                                             toast={this.toast}
+                                                                             config={this.state.config}/>}
                         {this.state.pageId === 'configuration' && <ConfigurationPage/>}
-                        {this.state.pageId === 'kamelets' && <KameletsPage dark={false} onRefresh={this.updateKamelets}/>}
-                        {this.state.pageId === 'components' && <ComponentsPage dark={false} onRefresh={this.updateComponents}/>}
+                        {this.state.pageId === 'kamelets' &&
+                            <KameletsPage dark={false} onRefresh={this.updateKamelets}/>}
+                        {this.state.pageId === 'components' &&
+                            <ComponentsPage dark={false} onRefresh={this.updateComponents}/>}
                         {this.state.pageId === 'eip' && <EipPage dark={false}/>}
                     </FlexItem>
                 </Flex>
@@ -246,11 +256,12 @@ export class Main extends React.Component<Props, State> {
         return (
             <Page className="karavan">
                 {KaravanApi.authType === undefined && <Bullseye className="loading-page">
-                    <Spinner className="spinner" isSVG diameter="140px" aria-label="Loading..." />
+                    <Spinner className="spinner" isSVG diameter="140px" aria-label="Loading..."/>
                     <div className="logo-placeholder">{Icon()}</div>
                 </Bullseye>}
                 {(KaravanApi.isAuthorized || KaravanApi.authType === 'public') && this.getMain()}
-                {!KaravanApi.isAuthorized && KaravanApi.authType === 'basic' && <MainLogin config={this.state.config} onLogin={this.onLogin}/>}
+                {!KaravanApi.isAuthorized && KaravanApi.authType === 'basic' &&
+                    <MainLogin config={this.state.config} onLogin={this.onLogin}/>}
                 {this.state.alerts.map((e: ToastMessage) => (
                     <Alert key={e.id} className="main-alert" variant={e.variant} title={e.title}
                            timeout={e.variant === "success" ? 1000 : 2000}
diff --git a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
index ad3b9a6..b2cb1af 100644
--- a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
+++ b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
@@ -5,7 +5,7 @@ import {
     PipelineStatus,
     PodStatus,
     Project,
-    ProjectFile
+    ProjectFile, ServiceStatus
 } from "../projects/ProjectModels";
 import {Buffer} from 'buffer';
 import {SsoApi} from "./SsoApi";
@@ -297,6 +297,28 @@ export class KaravanApi {
         });
     }
 
+    static async getAllServiceStatuses(after: (statuses: ServiceStatus[]) => void) {
+        instance.get('/api/kubernetes/service')
+            .then(res => {
+                if (res.status === 200) {
+                    after(res.data);
+                }
+            }).catch(err => {
+            console.log(err);
+        });
+    }
+
+    static async getAllDeploymentStatuses(after: (statuses: DeploymentStatus[]) => void) {
+        instance.get('/api/kubernetes/deployment')
+            .then(res => {
+                if (res.status === 200) {
+                    after(res.data);
+                }
+            }).catch(err => {
+            console.log(err);
+        });
+    }
+
     static async getDeploymentStatuses(env: string, after: (statuses: DeploymentStatus[]) => void) {
         instance.get('/api/kubernetes/deployment/' + env)
             .then(res => {
diff --git a/karavan-app/src/main/webapp/src/dashboard/DashboardPage.tsx b/karavan-app/src/main/webapp/src/dashboard/DashboardPage.tsx
new file mode 100644
index 0000000..d893bf7
--- /dev/null
+++ b/karavan-app/src/main/webapp/src/dashboard/DashboardPage.tsx
@@ -0,0 +1,268 @@
+import React from 'react';
+import {
+    Badge,
+    Button,
+    Flex,
+    FlexItem, HelperText, HelperTextItem, Label, LabelGroup,
+    OverflowMenu,
+    OverflowMenuContent,
+    OverflowMenuGroup,
+    OverflowMenuItem,
+    PageSection,
+    Text,
+    TextContent,
+    TextInput,
+    Toolbar,
+    ToolbarContent,
+    ToolbarItem, Tooltip
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import {MainToolbar} from "../MainToolbar";
+import RefreshIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
+import {DeploymentStatus, Project, ServiceStatus} from "../projects/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";
+
+interface Props {
+    config: any,
+    onSelect: (project: Project) => void
+    toast: (title: string, text: string, variant: 'success' | 'danger' | 'warning' | 'info' | 'default') => void
+}
+
+interface State {
+    projects: Project[],
+    deploymentStatuses: DeploymentStatus[],
+    serviceStatuses: ServiceStatus[],
+    isCreateModalOpen: boolean,
+    isDeleteModalOpen: boolean,
+    isCopy: boolean,
+    projectToCopy?: Project,
+    projectToDelete?: Project,
+    filter: string,
+    name: string,
+    description: string,
+    projectId: string,
+}
+
+export class DashboardPage extends React.Component<Props, State> {
+
+    public state: State = {
+        projects: [],
+        deploymentStatuses: [],
+        serviceStatuses: [],
+        isCreateModalOpen: false,
+        isDeleteModalOpen: false,
+        isCopy: false,
+        filter: '',
+        name: '',
+        description: '',
+        projectId: '',
+    };
+    interval: any;
+
+    componentDidMount() {
+        this.interval = setInterval(() => this.onGetProjects(), 1300);
+    }
+
+    componentWillUnmount() {
+        clearInterval(this.interval);
+    }
+
+    onGetProjects = () => {
+        KaravanApi.getConfiguration((config: any) => {
+            KaravanApi.getProjects((projects: Project[]) => {
+                this.setState({projects: projects})
+            });
+            KaravanApi.getAllDeploymentStatuses((statuses: DeploymentStatus[]) => {
+                this.setState({deploymentStatuses: statuses});
+            });
+            KaravanApi.getAllServiceStatuses((statuses: ServiceStatus[]) => {
+                this.setState({serviceStatuses: statuses});
+            });
+        });
+
+    }
+
+    tools = () => (<Toolbar id="toolbar-group-types">
+        <ToolbarContent>
+            <ToolbarItem>
+                <Button variant="link" icon={<RefreshIcon/>} onClick={e => this.onGetProjects()}/>
+            </ToolbarItem>
+            <ToolbarItem>
+                <TextInput className="text-field" type="search" id="search" name="search"
+                           autoComplete="off" placeholder="Search by name"
+                           value={this.state.filter}
+                           onChange={e => this.setState({filter: e})}/>
+            </ToolbarItem>
+        </ToolbarContent>
+    </Toolbar>);
+
+    title = () => (<TextContent>
+        <Text component="h1">Dashboard</Text>
+    </TextContent>);
+
+    getEnvironments(): string []{
+        return this.props.config.environments && Array.isArray(this.props.config.environments) ? Array.from(this.props.config.environments) : [];
+    }
+
+    getDeploymentEnvironments(name: string): [string, boolean] [] {
+        const deps = this.state.deploymentStatuses;
+        return this.getEnvironments().map(e => {
+            const env: string = e as string;
+            const dep = deps.find(d => d.name === name && d.env === env);
+            const deployed: boolean = dep !== undefined && dep.replicas > 0 && dep.replicas === dep.readyReplicas;
+            return [env, deployed];
+        });
+    }
+
+    getDeploymentByEnvironments(name: string): [string, DeploymentStatus | undefined] [] {
+        const deps = this.state.deploymentStatuses;
+        return this.getEnvironments().map(e => {
+            const env: string = e as string;
+            const dep = deps.find(d => d.name === name && d.env === env);
+            return [env, dep];
+        });
+    }
+
+    getServiceByEnvironments(name: string): [string, ServiceStatus | undefined] [] {
+        const services = this.state.serviceStatuses;
+        return this.getEnvironments().map(e => {
+            const env: string = e as string;
+            const service = services.find(d => d.name === name && d.env === env);
+            return [env, service];
+        });
+    }
+
+    getProject(name: string): Project | undefined {
+        return this.state.projects.filter(p => p.name === name)?.at(0);
+    }
+
+    isKaravan(name: string): boolean {
+        return this.state.projects.findIndex(p => p.projectId === name) > 0;
+    }
+
+    getReplicasPanel(deploymentStatus?: DeploymentStatus) {
+        if (deploymentStatus) {
+            const readyReplicas = deploymentStatus.readyReplicas ? deploymentStatus.readyReplicas : 0;
+            const ok = (deploymentStatus && readyReplicas > 0
+                && (deploymentStatus.unavailableReplicas === 0 || deploymentStatus.unavailableReplicas === undefined || deploymentStatus.unavailableReplicas === null)
+                && deploymentStatus?.replicas === readyReplicas);
+            return (
+                <Flex justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
+                    <FlexItem>
+                        <LabelGroup numLabels={3}>
+                            <Tooltip content={"Ready Replicas / Replicas"} position={"left"}>
+                                <Label className="table-label" icon={ok ? <UpIcon/> : <DownIcon/>}
+                                       color={ok ? "green" : "grey"}>{"Replicas: " + readyReplicas + " / " + deploymentStatus.replicas}</Label>
+                            </Tooltip>
+                            {deploymentStatus.unavailableReplicas > 0 &&
+                                <Tooltip content={"Unavailable replicas"} position={"right"}>
+                                    <Label icon={<DownIcon/>} color={"red"}>{deploymentStatus.unavailableReplicas}</Label>
+                                </Tooltip>
+                            }
+                        </LabelGroup>
+                    </FlexItem>
+                </Flex>
+            )
+        } else {
+            return (<Label icon={<DownIcon/>} color={"grey"}>n/a</Label>);
+        }
+    }
+
+    render() {
+        const deployments = Array.from(new Set(this.state.deploymentStatuses.filter(d => d.name.toLowerCase().includes(this.state.filter)).map(d => d.name)));
+        return (
+            <PageSection className="kamelet-section dashboard-page" padding={{default: 'noPadding'}}>
+                <PageSection className="tools-section" padding={{default: 'noPadding'}}>
+                    <MainToolbar title={this.title()} tools={this.tools()}/>
+                </PageSection>
+                <PageSection isFilled className="kamelets-page">
+                    <TableComposable aria-label="Projects" variant={TableVariant.compact}>
+                        <Thead>
+                            <Tr>
+                                <Th key='type'>Type</Th>
+                                <Th key='name'>Deployment</Th>
+                                <Th key='description'>Project/Description</Th>
+                                <Th key='environment'>Environment</Th>
+                                <Th key='namespace'>Namespace</Th>
+                                <Th key='replicas'>Replicas</Th>
+                                <Th key='services'>Services</Th>
+                                <Th key='camel'>Camel Health</Th>
+                                {/*<Th key='action'></Th>*/}
+                            </Tr>
+                        </Thead>
+                        <Tbody>
+                            {deployments.map(deployment => (
+                                <Tr key={deployment}>
+                                    <Td  style={{verticalAlign:"middle"}}>
+                                        {this.isKaravan(deployment) ? Icon("icon") : CamelUi.getIconFromSource(camelIcon)}
+                                    </Td>
+                                    <Td  style={{ verticalAlign:"middle"}}>
+                                        <Button style={{padding: '6px'}} variant={"link"}>{deployment}</Button>
+                                    </Td>
+                                    <Td  style={{verticalAlign:"middle"}}>
+                                        <HelperText>
+                                            <HelperTextItem>{this.getProject(deployment)?.name || ""}</HelperTextItem>
+                                            <HelperTextItem>{this.getProject(deployment)?.description || ""}</HelperTextItem>
+                                        </HelperText>
+                                    </Td>
+                                    <Td  >
+                                        <Flex direction={{default: "column"}}>
+                                            {this.getDeploymentEnvironments(deployment).map(value => (
+                                                <FlexItem className="badge-flex-item" key={value[0]}><Badge className="badge"
+                                                    isRead={!value[1]}>{value[0]}</Badge></FlexItem>
+                                            ))}
+                                        </Flex>
+                                    </Td>
+                                    <Td >
+                                        <Flex direction={{default: "column"}}>
+                                            {this.getDeploymentByEnvironments(deployment).map(value => (
+                                                <FlexItem className="badge-flex-item" key={value[0]}>
+                                                    <Label variant={"outline"}>{value[1]?.namespace || "n/a"}</Label>
+                                                </FlexItem>
+                                            ))}
+                                        </Flex>
+                                    </Td>
+                                    <Td >
+                                        <Flex direction={{default: "column"}}>
+                                            {this.getDeploymentByEnvironments(deployment).map(value => (
+                                                <FlexItem className="badge-flex-item" key={value[0]}>{this.getReplicasPanel(value[1])}</FlexItem>
+                                            ))}
+                                        </Flex>
+                                    </Td>
+                                    <Td>
+                                        <Flex direction={{default: "column"}}>
+                                            {this.getServiceByEnvironments(deployment).map(value => (
+                                                <FlexItem className="badge-flex-item" key={value[0]}>
+                                                    <Label variant={"outline"}>{value[1] ? (value[1]?.port + " -> " + value[1]?.targetPort) : "n/a"}</Label>
+                                                </FlexItem>
+                                            ))}
+                                        </Flex>
+                                    </Td>
+                                    <Td modifier={"fitContent"}>
+                                        <Flex direction={{default: "column"}}>
+                                            {this.getServiceByEnvironments(deployment).map(value => (
+                                                <FlexItem key={value[0]}>
+                                                    <LabelGroup numLabels={4} className="camel-label-group">
+                                                        <Label className="table-label" icon={false ? <UpIcon/> : <DownIcon/>}>{"Context"}</Label>
+                                                        <Label className="table-label" icon={false ? <UpIcon/> : <DownIcon/>}>{"Consumer"}</Label>
+                                                        <Label className="table-label" icon={false ? <UpIcon/> : <DownIcon/>}>{"Routes"}</Label>
+                                                        <Label className="table-label" icon={false ? <UpIcon/> : <DownIcon/>}>{"Registry"}</Label>
+                                                    </LabelGroup>
+                                                </FlexItem>
+                                            ))}
+                                        </Flex>
+                                    </Td>
+                                </Tr>
+                            ))}
+                        </Tbody>
+                    </TableComposable>
+                </PageSection>
+            </PageSection>
+        )
+    }
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webapp/src/index.css b/karavan-app/src/main/webapp/src/index.css
index 9ccffd9..f170c70 100644
--- a/karavan-app/src/main/webapp/src/index.css
+++ b/karavan-app/src/main/webapp/src/index.css
@@ -111,6 +111,19 @@
   margin: auto;
 }
 
+.karavan .projects-page .badge {
+  font-size: 14px;
+  font-weight: 400;
+  padding: 4px 8px 4px 8px;
+}
+
+.karavan .projects-page .runtime-badge {
+  min-width: 18px;
+  font-size: 14px;
+  font-weight: 400;
+  padding: 2px 7px 2px 7px;
+}
+
 .karavan .projects-page .pf-m-link {
   font-size: 14px;
 }
@@ -205,11 +218,6 @@
   padding: 0;
 }
 
-.karavan .runtime-badge {
-  min-width: 18px;
-  padding: 0;
-}
-
 .create-file-form .pf-c-form__group {
     grid-template-columns: 80px 1fr !important;
 }
@@ -222,6 +230,38 @@
   overflow-wrap: anywhere;
 }
 
+/*Dashboard*/
+.karavan .dashboard-page .pf-m-link {
+  font-size: 14px;
+}
+
+.karavan .dashboard-page .icon {
+  height: 28px;
+  width: 28px;
+  margin: auto;
+}
+
+.karavan .dashboard-page .badge {
+  font-size: 14px;
+  font-weight: 400;
+  padding: 4px 8px 4px 8px;
+}
+
+.karavan .dashboard-page .table-label {
+  font-size: 14px;
+  padding: 2px 6px 2px 4px;
+}
+
+.karavan .dashboard-page .badge-flex-item {
+  margin-bottom: 3px;
+  margin-top: 3px;
+}
+
+.karavan .dashboard-page .camel-label-group .pf-c-label-group__list {
+    flex-wrap: nowrap;
+}
+
+
 .karavan .loading-page .spinner {
   position: absolute;
 }
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectDashboard.tsx b/karavan-app/src/main/webapp/src/projects/ProjectDashboard.tsx
index b3dffe3..0fb9f80 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectDashboard.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectDashboard.tsx
@@ -8,7 +8,6 @@ import {
 import '../designer/karavan.css';
 import {KaravanApi} from "../api/KaravanApi";
 import {DeploymentStatus, Project, ProjectFileTypes} from "./ProjectModels";
-import {ChartDonutThreshold} from "@patternfly/react-charts";
 
 interface Props {
     project: Project,
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectInfo.tsx b/karavan-app/src/main/webapp/src/projects/ProjectInfo.tsx
index acbb7b2..d8e25fd 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectInfo.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectInfo.tsx
@@ -40,7 +40,6 @@ interface State {
     deleteEntity?: 'pod' | 'deployment',
     deleteEntityName?: string,
     deleteEntityEnv?: string,
-    environments: string[],
     environment: string,
     key?: string,
 }
@@ -54,8 +53,6 @@ export class ProjectInfo extends React.Component<Props, State> {
         isBuilding: false,
         isRolling: false,
         showDeleteConfirmation: false,
-        environments: this.props.config.environments && Array.isArray(this.props.config.environments)
-            ? Array.from(this.props.config.environments) : [],
         environment: this.props.config.environment
     };
     interval: any;
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectModels.ts b/karavan-app/src/main/webapp/src/projects/ProjectModels.ts
index 2f6eeb9..455182e 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectModels.ts
+++ b/karavan-app/src/main/webapp/src/projects/ProjectModels.ts
@@ -24,12 +24,24 @@ export class DeploymentStatus {
     name: string = '';
     env: string = '';
     namespace: string = '';
+    cluster: string = '';
     image: string = '';
     replicas: number = 0;
     readyReplicas: number = 0;
     unavailableReplicas: number = 0;
 }
 
+export class ServiceStatus {
+    name: string = '';
+    env: string = '';
+    namespace: string = '';
+    cluster: string = '';
+    port: string = '';
+    targetPort: string = '';
+    clusterIP: string = '';
+    type: string = '';
+}
+
 export class PodStatus {
     name: string = '';
     phase: string = '';
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx b/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
index 744c19f..3f7560c 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
@@ -214,15 +214,22 @@ export class ProjectsPage extends React.Component<Props, State> {
         )
     }
 
-    isDeployed(projectId: string): boolean{
-        const ds = this.state.deploymentStatuses.find(ds => ds.name === projectId);
-        return ds ? (ds.replicas > 0 && ds.replicas === ds.readyReplicas) : false;
+    getEnvironments(): string []{
+        return this.props.config.environments && Array.isArray(this.props.config.environments) ? Array.from(this.props.config.environments) : [];
+    }
+
+    getDeploymentByEnvironments(name: string): [string, DeploymentStatus | undefined] [] {
+        const deps = this.state.deploymentStatuses;
+        return this.getEnvironments().map(e => {
+            const env: string = e as string;
+            const dep = deps.find(d => d.name === name && d.env === env);
+            return [env, dep];
+        });
     }
 
     render() {
         const runtime = this.props.config?.runtime ? this.props.config.runtime : "QUARKUS";
         const projects = this.state.projects.filter(p => p.name.toLowerCase().includes(this.state.filter) || p.description.toLowerCase().includes(this.state.filter));
-        const environment: string = this.props.config.environment;
         return (
             <PageSection className="kamelet-section projects-page" padding={{default: 'noPadding'}}>
                 <PageSection className="tools-section" padding={{default: 'noPadding'}}>
@@ -263,7 +270,11 @@ export class ProjectsPage extends React.Component<Props, State> {
                                     </Td>
                                     <Td noPadding style={{width:"180px"}}>
                                         <Flex direction={{default: "row"}}>
-                                            <FlexItem key={"dev"}><Badge isRead={!this.isDeployed(project.projectId)}>{"dev"}</Badge></FlexItem>
+                                            {this.getDeploymentByEnvironments(project.projectId).map(value => (
+                                                <FlexItem className="badge-flex-item" key={value[0]}>
+                                                    <Badge className="badge"isRead={!value[1]}>{value[0]}</Badge>
+                                                </FlexItem>
+                                            ))}
                                         </Flex>
                                     </Td>
                                     <Td isActionCell>