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 05a5521  Saas feature23 (#405)
05a5521 is described below

commit 05a5521d483f1d0a33ac0c95d85e436e3e57b520
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Thu Jun 30 19:27:12 2022 -0400

    Saas feature23 (#405)
    
    * Pipeline and Pods logs
    
    * Rollout
---
 .../camel/karavan/api/KubernetesResource.java      | 26 ++++++
 .../camel/karavan/model/DeploymentStatus.java      | 22 ++++-
 .../apache/camel/karavan/model/PipelineRunLog.java | 27 ++++++
 .../org/apache/camel/karavan/model/PodStatus.java  | 74 +++++++++++++++++
 .../camel/karavan/model/ProjectStoreSchema.java    |  3 +-
 .../camel/karavan/service/KubernetesService.java   | 53 +++++++++++-
 karavan-app/src/main/webapp/src/api/KaravanApi.tsx | 38 ++++++++-
 .../src/main/webapp/src/models/ProjectModels.ts    | 10 +++
 .../src/main/webapp/src/projects/ProjectHeader.tsx |  5 +-
 .../src/main/webapp/src/projects/ProjectInfo.tsx   | 88 +++++++++++++++++---
 .../src/main/webapp/src/projects/ProjectPage.tsx   | 96 ++++++++++++++++------
 11 files changed, 398 insertions(+), 44 deletions(-)

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 4fa3b41..fe36476 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
@@ -86,4 +86,30 @@ public class KubernetesResource {
             return Response.noContent().build();
         }
     }
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/container/log/{environment}/{name}")
+    public Response getContainerLog(@HeaderParam("username") String username, 
@PathParam("environment") String environment,
+                                   @PathParam("name") String name) throws 
Exception {
+        Optional<KaravanConfiguration.Environment> env = 
configuration.environments().stream().filter(e -> 
e.name().equals(environment)).findFirst();
+        if (env.isPresent()) {
+            return Response.ok(kubernetesService.getContainerLog(name, 
env.get().namespace())).build();
+        } else {
+            return Response.noContent().build();
+        }
+    }
+
+    @POST
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Path("/deployment/rollout/{environment}/{name}")
+    public Response rollout(@HeaderParam("username") String username, 
@PathParam("environment") String environment, @PathParam("name") String name) 
throws Exception {
+        Optional<KaravanConfiguration.Environment> env = 
configuration.environments().stream().filter(e -> 
e.name().equals(environment)).findFirst();
+        if (env.isPresent()) {
+            kubernetesService.rolloutDeployment(name, env.get().namespace());
+            return Response.ok().build();
+        }
+        return Response.noContent().build();
+    }
 }
\ 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 831a54e..445e5a9 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
@@ -3,6 +3,9 @@ package org.apache.camel.karavan.model;
 import org.infinispan.protostream.annotations.ProtoFactory;
 import org.infinispan.protostream.annotations.ProtoField;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class DeploymentStatus {
     @ProtoField(number = 1)
     String image;
@@ -12,16 +15,25 @@ public class DeploymentStatus {
     Integer readyReplicas;
     @ProtoField(number = 4)
     Integer unavailableReplicas;
+    @ProtoField(number = 5, collectionImplementation = ArrayList.class)
+    List<PodStatus> podStatuses;
+
 
     public DeploymentStatus() {
+        this.image = "";
+        this.replicas = 0;
+        this.readyReplicas = 0;
+        this.unavailableReplicas = 0;
+        this.podStatuses = new ArrayList<>(0);
     }
 
     @ProtoFactory
-    public DeploymentStatus(String image, Integer replicas, Integer 
readyReplicas, Integer unavailableReplicas) {
+    public DeploymentStatus(String image, Integer replicas, Integer 
readyReplicas, Integer unavailableReplicas, List<PodStatus> podStatuses) {
         this.image = image;
         this.replicas = replicas;
         this.readyReplicas = readyReplicas;
         this.unavailableReplicas = unavailableReplicas;
+        this.podStatuses = podStatuses;
     }
 
     public String getImage() {
@@ -55,4 +67,12 @@ public class DeploymentStatus {
     public void setUnavailableReplicas(Integer unavailableReplicas) {
         this.unavailableReplicas = unavailableReplicas;
     }
+
+    public List<PodStatus> getPodStatuses() {
+        return podStatuses;
+    }
+
+    public void setPodStatuses(List<PodStatus> podStatuses) {
+        this.podStatuses = podStatuses;
+    }
 }
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/model/PipelineRunLog.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/model/PipelineRunLog.java
new file mode 100644
index 0000000..307a12e
--- /dev/null
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/model/PipelineRunLog.java
@@ -0,0 +1,27 @@
+package org.apache.camel.karavan.model;
+
+public class PipelineRunLog {
+    private String task;
+    private String log;
+
+    public PipelineRunLog(String task, String log) {
+        this.task = task;
+        this.log = log;
+    }
+
+    public String getTask() {
+        return task;
+    }
+
+    public void setTask(String task) {
+        this.task = task;
+    }
+
+    public String getLog() {
+        return log;
+    }
+
+    public void setLog(String log) {
+        this.log = log;
+    }
+}
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/model/PodStatus.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/model/PodStatus.java
new file mode 100644
index 0000000..7d9c9a4
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/PodStatus.java
@@ -0,0 +1,74 @@
+package org.apache.camel.karavan.model;
+
+import org.infinispan.protostream.annotations.ProtoFactory;
+import org.infinispan.protostream.annotations.ProtoField;
+
+public class PodStatus {
+    @ProtoField(number = 1)
+    String name;
+    @ProtoField(number = 2)
+    Boolean started;
+    @ProtoField(number = 3)
+    Boolean ready;
+    @ProtoField(number = 4)
+    String reason;
+    @ProtoField(number = 5)
+    String deployment;
+
+    public PodStatus() {
+        this.name = "";
+        this.started = false;
+        this.ready = false;
+        this.reason = "";
+        this.deployment = "";
+    }
+
+    @ProtoFactory
+    public PodStatus(String name, Boolean started, Boolean ready, String 
reason, String deployment) {
+        this.name = name;
+        this.started = started;
+        this.ready = ready;
+        this.reason = reason;
+        this.deployment = deployment;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Boolean getStarted() {
+        return started;
+    }
+
+    public void setStarted(Boolean started) {
+        this.started = started;
+    }
+
+    public Boolean getReady() {
+        return ready;
+    }
+
+    public void setReady(Boolean ready) {
+        this.ready = ready;
+    }
+
+    public String getReason() {
+        return reason;
+    }
+
+    public void setReason(String reason) {
+        this.reason = reason;
+    }
+
+    public String getDeployment() {
+        return deployment;
+    }
+
+    public void setDeployment(String deployment) {
+        this.deployment = deployment;
+    }
+}
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 b5fa110..a049831 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,8 @@ import java.util.HashMap;
 
 @AutoProtoSchemaBuilder(
         includeClasses = {
-                GroupedKey.class, Project.class, ProjectFile.class, 
ProjectStatus.class, ProjectEnvStatus.class, DeploymentStatus.class
+                GroupedKey.class, Project.class, ProjectFile.class, 
ProjectStatus.class, ProjectEnvStatus.class, DeploymentStatus.class,
+                PodStatus.class
         },
         schemaPackageName = "karavan")
 public interface ProjectStoreSchema extends GeneratedSchema {
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 e6ea942..dfcda42 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
@@ -18,6 +18,7 @@ package org.apache.camel.karavan.service;
 
 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.client.DefaultKubernetesClient;
 import io.fabric8.kubernetes.client.KubernetesClient;
@@ -34,16 +35,21 @@ 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.PipelineRunLog;
+import org.apache.camel.karavan.model.PodStatus;
 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.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 @ApplicationScoped
 public class KubernetesService {
@@ -103,8 +109,13 @@ public class KubernetesService {
         return 
tektonClient().v1beta1().pipelineRuns().inNamespace(namespace).withName(pipelineRuneName).get();
     }
 
-    public Map<String,String> getPipelineRunLog(String pipelineRuneName, 
String namespace) {
-        Map<String,String> result = new HashMap<>(1);
+    public String getContainerLog(String podName, String namespace) {
+        String logText = 
kubernetesClient().pods().inNamespace(namespace).withName(podName).getLog(true);
+        return logText;
+    }
+
+    public List<PipelineRunLog> getPipelineRunLog(String pipelineRuneName, 
String namespace) {
+        List<PipelineRunLog> result = new ArrayList<>(1);
         PipelineRun pipelineRun = getPipelineRun(pipelineRuneName, namespace);
         pipelineRun.getStatus().getTaskRuns().forEach((s, 
pipelineRunTaskRunStatus) -> {
             String podName = pipelineRunTaskRunStatus.getStatus().getPodName();
@@ -114,7 +125,7 @@ public class KubernetesService {
                 
log.append(stepState.getContainer()).append(System.lineSeparator());
                 log.append(logText).append(System.lineSeparator());
             });
-            result.put(s, log.toString());
+            result.add(new PipelineRunLog(s, log.toString()));
         });
         return result;
     }
@@ -127,6 +138,18 @@ public class KubernetesService {
                 .findFirst().get();
     }
 
+    public void rolloutDeployment(String name, String namespace) {
+        try {
+            if (kubernetesClient().isAdaptable(OpenShiftClient.class)) {
+                
openshiftClient().deploymentConfigs().inNamespace(namespace).withName(name).deployLatest();
+            } else {
+                // TODO: Implement Deployment for Kubernetes/Minikube
+            }
+        } catch (Exception ex) {
+            LOGGER.error(ex.getMessage());
+        }
+    }
+
 
     public DeploymentStatus getDeploymentStatus(String name, String namespace) 
{
         try {
@@ -136,11 +159,26 @@ public class KubernetesService {
                 String imageName = 
dsImage.startsWith("image-registry.openshift-image-registry.svc")
                         ? 
dsImage.replace("image-registry.openshift-image-registry.svc:5000/", "")
                         : dsImage;
+
+                List<Pod> pods = 
openshiftClient().pods().inNamespace(namespace)
+                        .withLabel("app.kubernetes.io/name", name)
+                        .withLabel("deploymentconfig", name)
+                        .list().getItems();
+
+                List<PodStatus> podStatuses = pods.stream().map(pod -> new 
PodStatus(
+                        pod.getMetadata().getName(),
+                        
pod.getStatus().getContainerStatuses().get(0).getStarted(),
+                        
pod.getStatus().getContainerStatuses().get(0).getReady(),
+                        getPodReason(pod),
+                        pod.getMetadata().getLabels().get("deployment")
+                        )).collect(Collectors.toList());
+
                 return new DeploymentStatus(
                         imageName,
                         dc.getSpec().getReplicas(),
                         dc.getStatus().getReadyReplicas(),
-                        dc.getStatus().getUnavailableReplicas()
+                        dc.getStatus().getUnavailableReplicas(),
+                        podStatuses
                 );
             } else {
                 // TODO: Implement Deployment for Kubernetes/Minikube
@@ -152,6 +190,13 @@ public class KubernetesService {
         }
     }
 
+    private String getPodReason (Pod pod){
+        try {
+            return 
pod.getStatus().getContainerStatuses().get(0).getState().getWaiting().getReason();
+        } catch (Exception e){
+            return "";
+        }
+    }
 
     public Secret getKaravanSecret() {
         return 
kubernetesClient().secrets().inNamespace(namespace).withName("karavan").get();
diff --git a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx 
b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
index e3f0e82..b834fd7 100644
--- a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
+++ b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
@@ -123,8 +123,42 @@ export const KaravanApi = {
         });
     },
 
-    tekton: async (project: Project, environment: string, after: (res: 
AxiosResponse<any>) => void) => {
-        axios.post('kubernetes/pipeline/' + environment, project,
+    pipelineRun: async (project: Project, environment: string, after: (res: 
AxiosResponse<any>) => void) => {
+        axios.post('/kubernetes/pipeline/' + environment, project,
+            {headers: {'Accept': 'application/json', 'Content-Type': 
'application/json', 'username': 'cameleer'}})
+            .then(res => {
+                after(res);
+            }).catch(err => {
+            after(err);
+        });
+    },
+
+    getPipelineLog: async (environment: string, pipelineRunName: string, 
after: (res: AxiosResponse<any>) => void) => {
+        axios.get('/kubernetes/pipeline/log/' + environment + "/" + 
pipelineRunName,
+            {headers: {'Accept': 'application/json', 'Content-Type': 
'application/json', 'username': 'cameleer'}})
+            .then(res => {
+                if (res.status === 200) {
+                    after(res.data);
+                }
+            }).catch(err => {
+            console.log(err);
+        });
+    },
+
+    getContainerLog: async (environment: string, name: string, after: (res: 
AxiosResponse<string>) => void) => {
+        axios.get('/kubernetes/container/log/' + environment + "/" + name,
+            {headers: {'Accept': 'application/json', 'Content-Type': 
'application/json', 'username': 'cameleer'}})
+            .then(res => {
+                if (res.status === 200) {
+                    after(res.data);
+                }
+            }).catch(err => {
+            console.log(err);
+        });
+    },
+
+    rolloutDeployment: async (name: string, environment: string, after: (res: 
AxiosResponse<any>) => void) => {
+        axios.post('/kubernetes/deployment/rollout/' + environment + '/' + 
name, "",
             {headers: {'Accept': 'application/json', 'Content-Type': 
'application/json', 'username': 'cameleer'}})
             .then(res => {
                 after(res);
diff --git a/karavan-app/src/main/webapp/src/models/ProjectModels.ts 
b/karavan-app/src/main/webapp/src/models/ProjectModels.ts
index 6c6113d..4d6ae49 100644
--- a/karavan-app/src/main/webapp/src/models/ProjectModels.ts
+++ b/karavan-app/src/main/webapp/src/models/ProjectModels.ts
@@ -39,6 +39,15 @@ export class DeploymentStatus {
     replicas: number = 0;
     readyReplicas: number = 0;
     unavailableReplicas: number = 0;
+    podStatuses: PodStatus[] = []
+}
+
+export class PodStatus {
+    name: string = '';
+    started: boolean = false;
+    ready: boolean = false;
+    reason: string = '';
+    deployment: string = '';
 }
 
 export class ProjectStatus {
@@ -78,4 +87,5 @@ export const ProjectFileTypes: ProjectFileType[] = [
     new ProjectFileType("CODE", "Code", "groovy"),
     new ProjectFileType("PROPERTIES", "Properties", "properties"),
     new ProjectFileType("OPENAPI", "OpenAPI", "json"),
+    new ProjectFileType("LOG", "Log", "log"),
 ];
\ No newline at end of file
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx 
b/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx
index 89aa655..3e26968 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx
@@ -7,13 +7,14 @@ import {
     PageSection,
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
-import {Project, ProjectStatus} from "../models/ProjectModels";
+import {Project, ProjectFile, ProjectStatus} from "../models/ProjectModels";
 import {ProjectDashboard} from "./ProjectDashboard";
 import {ProjectInfo} from "./ProjectInfo";
 
 interface Props {
     project: Project,
     config: any,
+    showLog: (type: 'container' | 'pipeline', name: string, environment: 
string) => void
 }
 
 interface State {
@@ -41,7 +42,7 @@ export class ProjectHeader extends React.Component<Props, 
State> {
                 </FlexItem>
                 <FlexItem>
                     <PageSection padding={{default: "padding"}}>
-                        {tab === 'details' && <ProjectInfo 
project={this.props.project} config={this.props.config}/>}
+                        {tab === 'details' && <ProjectInfo 
project={this.props.project} config={this.props.config} 
showLog={this.props.showLog}/>}
                         {tab === 'dashboard' && <ProjectDashboard 
project={this.props.project} config={this.props.config}/>}
                     </PageSection>
                 </FlexItem>
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectInfo.tsx 
b/karavan-app/src/main/webapp/src/projects/ProjectInfo.tsx
index 6d65f4f..97737c8 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectInfo.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectInfo.tsx
@@ -14,13 +14,16 @@ import '../designer/karavan.css';
 import {KaravanApi} from "../api/KaravanApi";
 import {DeploymentStatus, Project, ProjectStatus} from 
"../models/ProjectModels";
 import BuildIcon from "@patternfly/react-icons/dist/esm/icons/build-icon";
+import RolloutIcon from 
"@patternfly/react-icons/dist/esm/icons/process-automation-icon";
 import PushIcon from "@patternfly/react-icons/dist/esm/icons/code-branch-icon";
 import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
+import DownIcon from 
"@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
 import ClockIcon from "@patternfly/react-icons/dist/esm/icons/clock-icon";
 
 interface Props {
     project: Project,
     config: any,
+    showLog: (type: 'container' | 'pipeline', name: string, environment: 
string) => void
 }
 
 interface State {
@@ -28,6 +31,7 @@ interface State {
     status?: ProjectStatus,
     isPushing: boolean,
     isBuilding: boolean,
+    isRolling: boolean,
     environments: string[],
     environment: string,
     key?: string,
@@ -39,6 +43,7 @@ export class ProjectInfo extends React.Component<Props, 
State> {
         project: this.props.project,
         isPushing: false,
         isBuilding: false,
+        isRolling: false,
         environments: this.props.config.environments && 
Array.isArray(this.props.config.environments)
             ? Array.from(this.props.config.environments) : [],
         environment: this.props.config.environments && 
Array.isArray(this.props.config.environments)
@@ -93,7 +98,7 @@ export class ProjectInfo extends React.Component<Props, 
State> {
 
     build = () => {
         this.setState({isBuilding: true});
-        KaravanApi.tekton(this.props.project, this.state.environment, res => {
+        KaravanApi.pipelineRun(this.props.project, this.state.environment, res 
=> {
             console.log(res)
             if (res.status === 200 || res.status === 201) {
                 this.setState({isBuilding: false});
@@ -104,6 +109,19 @@ export class ProjectInfo extends React.Component<Props, 
State> {
         });
     }
 
+    rollout = () => {
+        this.setState({isRolling: true});
+        KaravanApi.rolloutDeployment(this.props.project.projectId, 
this.state.environment, res => {
+            console.log(res)
+            if (res.status === 200 || res.status === 201) {
+                this.setState({isRolling: false});
+                this.onRefresh();
+            } else {
+                // Todo notification
+            }
+        });
+    }
+
     pushButton = () => {
         const isPushing = this.state.isPushing;
         return (<Tooltip content="Commit and push to git" position={"left"}>
@@ -125,7 +143,19 @@ export class ProjectInfo extends React.Component<Props, 
State> {
                     onClick={e => {
                         this.push(() => this.build());
                     }}>
-                {isDeploying ? "..." : "Build"}
+                {isDeploying ? "..." : "Run"}
+            </Button>
+        </Tooltip>)
+    }
+
+    rolloutButton = () => {
+        const isRolling = this.state.isRolling;
+        return (<Tooltip content="Rollout deployment" position={"left"}>
+            <Button isLoading={isRolling ? true : undefined} isSmall 
variant="secondary"
+                    className="project-button"
+                    icon={!isRolling ? <RolloutIcon/> : <div></div>}
+                    onClick={e => this.rollout()}>
+                {isRolling ? "..." : "Rollout"}
             </Button>
         </Tooltip>)
     }
@@ -150,12 +180,6 @@ export class ProjectInfo extends React.Component<Props, 
State> {
         const deploymentStatus = status?.statuses.find(s => s.environment === 
env)?.deploymentStatus;
         return (
             <DescriptionList isHorizontal>
-                <DescriptionListGroup>
-                    <DescriptionListTerm>Commit</DescriptionListTerm>
-                    <DescriptionListDescription>
-                        {this.getCommitPanel()}
-                    </DescriptionListDescription>
-                </DescriptionListGroup>
                 <DescriptionListGroup>
                     <DescriptionListTerm>Pipeline</DescriptionListTerm>
                     <DescriptionListDescription>
@@ -168,6 +192,12 @@ export class ProjectInfo extends React.Component<Props, 
State> {
                         {deploymentStatus && 
this.getReplicasPanel(deploymentStatus)}
                     </DescriptionListDescription>
                 </DescriptionListGroup>
+                <DescriptionListGroup>
+                    <DescriptionListTerm>Pods</DescriptionListTerm>
+                    <DescriptionListDescription>
+                        {deploymentStatus && 
this.getPodsPanel(deploymentStatus, env)}
+                    </DescriptionListDescription>
+                </DescriptionListGroup>
                 <DescriptionListGroup>
                     <DescriptionListTerm>Camel health</DescriptionListTerm>
                     <DescriptionListDescription>
@@ -196,6 +226,31 @@ export class ProjectInfo extends React.Component<Props, 
State> {
         )
     }
 
+    getPodsPanel(deploymentStatus: DeploymentStatus, env: string) {
+        const podStatuses = deploymentStatus.podStatuses;
+        return (
+        <Flex justifyContent={{default: "justifyContentSpaceBetween"}} 
alignItems={{default: "alignItemsCenter"}}>
+            <FlexItem>
+                <LabelGroup numLabels={3}>
+                    {podStatuses.map(pod => {
+                        const running = pod.started && pod.ready;
+                        return (
+                            <Tooltip content={running ? "Running" : 
pod.reason}>
+                                <Label icon={running ? <UpIcon/> : 
<DownIcon/>} color={running ? "green" : "red"} >
+                                    <Button variant="link" onClick={e => 
this.props.showLog?.call(this, 'container', pod.name, env)}>
+                                        {pod.name}
+                                    </Button>
+                                </Label>
+                            </Tooltip>
+                        )}
+                    )}
+                </LabelGroup>
+            </FlexItem>
+            <FlexItem>{env === "dev" && this.rolloutButton()}</FlexItem>
+        </Flex>
+        )
+    }
+
     getHealthPanel(env: string) {
         const status = this.state.status?.statuses.find(s => s.environment === 
env)
         const registryStatus = status?.registryStatus;
@@ -205,8 +260,8 @@ export class ProjectInfo extends React.Component<Props, 
State> {
         const contextVersion = status?.contextVersion;
         return (
             <LabelGroup numLabels={5}>
+                {contextVersion && <Label icon={<UpIcon/>} 
color={contextStatus === "UP" ? "green" : "grey"}>{contextVersion}</Label>}
                 <Label icon={<UpIcon/>} color={contextStatus === "UP" ? 
"green" : "grey"}>Context</Label>
-                <Label icon={<UpIcon/>} color={contextStatus === "UP" ? 
"green" : "grey"}>{contextVersion}</Label>
                 <Label icon={<UpIcon/>} color={consumersStatus === "UP" ? 
"green" : "grey"}>Consumers</Label>
                 <Label icon={<UpIcon/>} color={routesStatus === "UP" ? "green" 
: "grey"}>Routes</Label>
                 <Label icon={<UpIcon/>} color={registryStatus === "UP" ? 
"green" : "grey"}>Registry</Label>
@@ -228,8 +283,13 @@ export class ProjectInfo extends React.Component<Props, 
State> {
                 <FlexItem>
                     <Tooltip content={pipelineResult} position={"right"}>
                         <LabelGroup numLabels={2}>
-                            <Label icon={isRunning ? <Spinner isSVG 
diameter="16px"/> : <UpIcon/>}
-                                   color={color}>{pipeline ? pipeline : 
"-"}</Label>
+                            <Label icon={isRunning ? <Spinner isSVG 
diameter="16px"/> : <UpIcon/>} color={color}>
+                                <Button variant="link" onClick={e => {
+                                    if (pipeline) 
this.props.showLog?.call(this, 'pipeline', pipeline, env);
+                                }}>
+                                    {pipeline ? pipeline : "-"}
+                                </Button>
+                            </Label>
                             {lastPipelineRunTime && lastPipelineRunTime > 0 &&
                                 <Label icon={<ClockIcon/>} 
color={color}>{lastPipelineRunTime + "s"}</Label>}
                         </LabelGroup>
@@ -263,6 +323,12 @@ export class ProjectInfo extends React.Component<Props, 
State> {
                 <DescriptionListTerm>Description</DescriptionListTerm>
                 
<DescriptionListDescription>{project?.description}</DescriptionListDescription>
             </DescriptionListGroup>
+            <DescriptionListGroup>
+                <DescriptionListTerm>Commit</DescriptionListTerm>
+                <DescriptionListDescription>
+                    {this.getCommitPanel()}
+                </DescriptionListDescription>
+            </DescriptionListGroup>
         </DescriptionList>)
     }
 
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx 
b/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
index abcdc97..d9386f7 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
@@ -9,13 +9,19 @@ import {
     TextContent,
     Toolbar,
     ToolbarContent,
-    ToolbarItem,
     Bullseye,
     EmptyState,
     EmptyStateVariant,
     EmptyStateIcon,
     Title,
-    ModalVariant, Modal, Spinner, Tooltip, Flex, FlexItem, ToggleGroup, 
ToggleGroupItem, Card, CardBody
+    ModalVariant,
+    Modal,
+    Flex,
+    FlexItem,
+    ToggleGroup,
+    ToggleGroupItem,
+    CodeBlockCode,
+    CodeBlock, Skeleton
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {MainToolbar} from "../MainToolbar";
@@ -32,12 +38,7 @@ import Editor from "@monaco-editor/react";
 import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
 import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon";
 import {CreateFileModal} from "./CreateFileModal";
-import BuildIcon from "@patternfly/react-icons/dist/esm/icons/build-icon";
-import PushIcon from "@patternfly/react-icons/dist/esm/icons/code-branch-icon";
 import {PropertiesEditor} from "./PropertiesEditor";
-import PendingIcon from "@patternfly/react-icons/dist/esm/icons/pending-icon";
-import CheckCircleIcon from 
"@patternfly/react-icons/dist/esm/icons/check-circle-icon";
-import ExclamationCircleIcon from 
"@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon";
 import {ProjectHeader} from "./ProjectHeader";
 
 interface Props {
@@ -146,21 +147,34 @@ export class ProjectPage extends React.Component<Props, 
State> {
                 </Flex>
             </ToolbarContent>
         </Toolbar>
-    };
+    }
+
+    getType = (name: string) => {
+        const extension = name.substring(name.lastIndexOf('.') + 1);
+        const type = ProjectFileTypes.filter(p => p.extension === 
extension).map(p => p.title)[0];
+        if (type) {
+            return type
+        } else {
+            return "Unknown"
+        }
+    }
 
     title = () => {
         const file = this.state.file;
         const isFile = file !== undefined;
+        const isLog = file !== undefined && file.name.endsWith("log");
+        const filename = file ? file.name.substring(0, 
file.name.lastIndexOf('.')) : "";
         return (<div className="dsl-title">
             {isFile &&
                 <div>
                     <Breadcrumb>
-                        <BreadcrumbItem to="#"
-                                        onClick={event => this.setState({file: 
undefined})}>{"Project: " + this.props.project?.projectId}</BreadcrumbItem>
+                        <BreadcrumbItem to="#" onClick={event => 
this.setState({file: undefined})}>
+                            {"Project: " + this.props.project?.projectId}
+                        </BreadcrumbItem>
                         <BreadcrumbItem to="#" 
isActive>{this.getType(file?.name)}</BreadcrumbItem>
                     </Breadcrumb>
                     <TextContent className="title">
-                        <Text 
component="h1">{CamelUi.titleFromName(file.name)}</Text>
+                        <Text component="h1">{isLog ? filename : 
CamelUi.titleFromName(file.name)}</Text>
                     </TextContent>
                 </div>
             }
@@ -198,17 +212,6 @@ export class ProjectPage extends React.Component<Props, 
State> {
         }
     }
 
-
-    getType = (name: string) => {
-        const extension = name.substring(name.lastIndexOf('.') + 1);
-        const type = ProjectFileTypes.filter(p => p.extension === 
extension).map(p => p.title)[0];
-        if (type) {
-            return type
-        } else {
-            return "Unknown"
-        }
-    }
-
     getProjectFiles = () => {
         const files = this.state.files;
         return (
@@ -217,6 +220,7 @@ export class ProjectPage extends React.Component<Props, 
State> {
                     <Tr>
                         <Th key='type'>Type</Th>
                         <Th key='name'>Name</Th>
+                        <Th key='filename'>Filename</Th>
                         <Th key='action'></Th>
                     </Tr>
                 </Thead>
@@ -233,6 +237,7 @@ export class ProjectPage extends React.Component<Props, 
State> {
                                     {CamelUi.titleFromName(file.name)}
                                 </Button>
                             </Td>
+                            <Td>{file.name}</Td>
                             <Td modifier={"fitContent"}>
                                 <Button style={{padding: '0'}} 
variant={"plain"}
                                         isDisabled={file.name === 
'application.properties'}
@@ -296,6 +301,49 @@ export class ProjectPage extends React.Component<Props, 
State> {
         )
     }
 
+    showPipelineLog = (type: 'container' | 'pipeline', name: string, 
environment: string) => {
+        const filename = name + ".log";
+        const code = '';
+        this.setState({file: new ProjectFile(filename, 
this.props.project.projectId, code)});
+        if (type === 'pipeline'){
+            KaravanApi.getPipelineLog(environment, name, (res: any) => {
+                if (Array.isArray(res) && Array.from(res).length > 0)
+                    this.setState({file: new ProjectFile(filename, 
this.props.project.projectId, res.at(0).log)});
+            });
+        } else if (type === 'container'){
+            KaravanApi.getContainerLog(environment, name, (res: any) => {
+                this.setState({file: new ProjectFile(filename, 
this.props.project.projectId, res)});
+            });
+        }
+
+    }
+
+    getLogView = () => {
+        const file = this.state.file;
+        return (
+            <div style={{overflowX: "auto"}}>
+                {file !== undefined && file.code.length !== 0 &&
+                    <CodeBlock>
+                        <CodeBlockCode 
id="code-content">{file.code}</CodeBlockCode>
+                    </CodeBlock>}
+                {file === undefined || file.code.length === 0 &&
+                    <div>
+                        <Skeleton width="25%" screenreaderText="Loading 
contents" />
+                        <br />
+                        <Skeleton width="33%" />
+                        <br />
+                        <Skeleton width="50%" />
+                        <br />
+                        <Skeleton width="66%" />
+                        <br />
+                        <Skeleton width="75%" />
+                        <br />
+                        <Skeleton />
+                    </div>}
+            </div>
+        )
+    }
+
     getPropertiesEditor = () => {
         const file = this.state.file;
         return (
@@ -311,6 +359,7 @@ export class ProjectPage extends React.Component<Props, 
State> {
         const {file, mode} = this.state;
         const isYaml = file !== undefined && file.name.endsWith("yaml");
         const isProperties = file !== undefined && 
file.name.endsWith("properties");
+        const isLog = file !== undefined && file.name.endsWith("log");
         const isCode = file !== undefined && (file.name.endsWith("java") || 
file.name.endsWith("groovy"));
         const showDesigner = isYaml && mode === 'design';
         const showEditor = isCode || (isYaml && mode === 'code');
@@ -322,11 +371,12 @@ export class ProjectPage extends React.Component<Props, 
State> {
                 {file === undefined &&
                     <PageSection isFilled className="kamelets-page 
project-page-section"
                                  padding={{default: file !== undefined ? 
'noPadding' : 'noPadding'}}>
-                        {<ProjectHeader project={this.props.project} 
config={this.props.config}/>}
+                        {<ProjectHeader project={this.props.project} 
config={this.props.config} showLog={this.showPipelineLog}/>}
                         {this.getProjectFiles()}
                     </PageSection>}
                 {showDesigner && this.getDesigner()}
                 {showEditor && this.getEditor()}
+                {isLog && this.getLogView()}
                 {isProperties && this.getPropertiesEditor()}
                 <CreateFileModal project={this.props.project} 
isOpen={this.state.isCreateModalOpen}
                                  onClose={this.closeModal}/>

Reply via email to