You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2022/06/22 17:26:21 UTC

[camel-karavan] branch main updated: Start PipelineRUn from UI (#387)

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 fb85e4f  Start PipelineRUn from UI (#387)
fb85e4f is described below

commit fb85e4fdebde2c9ba6650520dc306362522ffc39
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Wed Jun 22 13:26:15 2022 -0400

    Start PipelineRUn from UI (#387)
---
 karavan-app/pom.xml                                |   6 +-
 .../camel/karavan/api/ConfigurationResource.java   |   6 +-
 .../apache/camel/karavan/api/TektonResource.java   |  34 ++++-
 .../camel/karavan/model/KaravanConfiguration.java  |   1 +
 .../org/apache/camel/karavan/model/Project.java    |  13 +-
 .../camel/karavan/service/KubernetesService.java   |  49 ++++---
 .../src/main/resources/application.properties      |   3 +
 karavan-app/src/main/webapp/src/api/KaravanApi.tsx |  10 ++
 karavan-app/src/main/webapp/src/index.css          |   6 +-
 .../src/main/webapp/src/models/ProjectModels.ts    |   4 +-
 .../src/main/webapp/src/projects/ProjectPage.tsx   | 161 +++++++++++++++------
 .../src/main/webapp/src/projects/ProjectsPage.tsx  |   6 +-
 12 files changed, 220 insertions(+), 79 deletions(-)

diff --git a/karavan-app/pom.xml b/karavan-app/pom.xml
index d1a67ed..086b59b 100644
--- a/karavan-app/pom.xml
+++ b/karavan-app/pom.xml
@@ -56,11 +56,7 @@
         </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
-            <artifactId>quarkus-resteasy-reactive</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>io.quarkus</groupId>
-            <artifactId>quarkus-resteasy-reactive-jsonb</artifactId>
+            <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
         </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
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 5e01b29..b2f23ef 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
@@ -18,13 +18,13 @@ package org.apache.camel.karavan.api;
 
 import org.apache.camel.karavan.model.KaravanConfiguration;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.jboss.resteasy.reactive.RestResponse;
 
 import javax.inject.Inject;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
 import java.util.Map;
 import java.util.stream.Collectors;
 
@@ -40,8 +40,8 @@ public class ConfigurationResource {
 
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    public RestResponse<Map<String, Object>> getConfiguration() throws Exception {
-        return RestResponse.ResponseBuilder.ok(
+    public Response getConfiguration() throws Exception {
+        return Response.ok(
                 Map.of(
                         "version", version,
                         "environments", configuration.environments().stream().map(e -> e.name()).collect(Collectors.toList()),
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/TektonResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/TektonResource.java
index aa71762..d2c98ef 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/api/TektonResource.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/TektonResource.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.karavan.api;
 
+import io.fabric8.tekton.pipeline.v1beta1.PipelineRun;
+import org.apache.camel.karavan.model.KaravanConfiguration;
 import org.apache.camel.karavan.model.Project;
 import org.apache.camel.karavan.model.ProjectFile;
 import org.apache.camel.karavan.service.InfinispanService;
@@ -24,12 +26,15 @@ import org.jboss.logging.Logger;
 
 import javax.inject.Inject;
 import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
 import javax.ws.rs.HeaderParam;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
-import java.util.List;
+import javax.ws.rs.core.Response;
+import java.util.Optional;
 
 @Path("/tekton")
 public class TektonResource {
@@ -40,15 +45,36 @@ public class TektonResource {
     @Inject
     KubernetesService kubernetesService;
 
+    @Inject
+    KaravanConfiguration configuration;
+
     private static final Logger LOGGER = Logger.getLogger(TektonResource.class.getName());
 
     @POST
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    public Project push(@HeaderParam("username") String username, Project project) throws Exception {
+    @Path("/{environment}")
+    public Project push(@HeaderParam("username") String username, @PathParam("environment") String environment, Project project) throws Exception {
         Project p = infinispanService.getProject(project.getProjectId());
-        List<ProjectFile> files = infinispanService.getProjectFiles(project.getProjectId());
-        String pipelineRunId = kubernetesService.createPipelineRun(project);
+        Optional<KaravanConfiguration.Environment> env = configuration.environments().stream().filter(e -> e.name().equals(environment)).findFirst();
+        if (env.isPresent()) {
+            String pipelineRunId = kubernetesService.createPipelineRun(project, env.get().namespace());
+            p.setLastPipelineRun(pipelineRunId);
+            infinispanService.saveProject(p);
+        }
         return p;
     }
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/{environment}/{name}")
+    public Response get(@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.getPipelineRun(name, env.get().namespace())).build();
+        } else {
+            return Response.noContent().build();
+        }
+    }
 }
\ No newline at end of file
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/KaravanConfiguration.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/KaravanConfiguration.java
index 0db0b9d..a843798 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/model/KaravanConfiguration.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/KaravanConfiguration.java
@@ -16,5 +16,6 @@ public interface KaravanConfiguration {
     interface Environment {
         String name();
         String cluster();
+        String namespace();
     }
 }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java b/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java
index 6f0b4cb..174d5f6 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/model/Project.java
@@ -17,6 +17,8 @@ public class Project {
     Project.CamelRuntime runtime;
     @ProtoField(number = 5)
     String lastCommit;
+    @ProtoField(number = 6)
+    String lastPipelineRun;
 
     public enum CamelRuntime {
         @ProtoEnumValue(number = 0, name = "Quarkus")
@@ -28,12 +30,13 @@ public class Project {
     }
 
     @ProtoFactory
-    public Project(String projectId, String name, String description, CamelRuntime runtime, String lastCommit) {
+    public Project(String projectId, String name, String description, CamelRuntime runtime, String lastCommit, String lastPipelineRun) {
         this.projectId = projectId;
         this.name = name;
         this.description = description;
         this.runtime = runtime;
         this.lastCommit = lastCommit;
+        this.lastPipelineRun = lastPipelineRun;
     }
 
     public Project(String projectId, String name, String description, CamelRuntime runtime) {
@@ -86,4 +89,12 @@ public class Project {
     public void setLastCommit(String lastCommit) {
         this.lastCommit = lastCommit;
     }
+
+    public String getLastPipelineRun() {
+        return lastPipelineRun;
+    }
+
+    public void setLastPipelineRun(String lastPipelineRun) {
+        this.lastPipelineRun = lastPipelineRun;
+    }
 }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/KubernetesService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/KubernetesService.java
index 1004343..7a5f221 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
@@ -16,12 +16,16 @@
  */
 package org.apache.camel.karavan.service;
 
+import io.fabric8.kubernetes.api.model.ObjectMeta;
 import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
 import io.fabric8.kubernetes.client.DefaultKubernetesClient;
 import io.fabric8.tekton.client.DefaultTektonClient;
 import io.fabric8.tekton.pipeline.v1beta1.ParamBuilder;
+import io.fabric8.tekton.pipeline.v1beta1.PipelineRef;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRefBuilder;
+import io.fabric8.tekton.pipeline.v1beta1.PipelineRun;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRunBuilder;
+import io.fabric8.tekton.pipeline.v1beta1.PipelineRunSpec;
 import io.fabric8.tekton.pipeline.v1beta1.PipelineRunSpecBuilder;
 import org.apache.camel.karavan.model.Project;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -44,26 +48,35 @@ public class KubernetesService {
 
     private static final Logger LOGGER = Logger.getLogger(KubernetesService.class.getName());
 
-    public String createPipelineRun(Project project) throws Exception {
+    public String createPipelineRun(Project project, String namespace) throws Exception {
+
+        Map<String, String> labels = Map.of(
+                "karavan-project-id", project.getProjectId(),
+                "tekton.dev/pipeline", "karavan-quarkus"
+        );
+
+        ObjectMeta meta = new ObjectMetaBuilder()
+                .withGenerateName("karavan-" + project.getProjectId() + "-")
+                .withLabels(labels)
+                .withNamespace(namespace)
+                .build();
+
+        PipelineRef ref = new PipelineRefBuilder().withName("karavan-quarkus").build();
+
+        PipelineRunSpec spec = new PipelineRunSpecBuilder()
+                .withPipelineRef(ref)
+                .withServiceAccountName("pipeline")
+                .withParams(new ParamBuilder().withName("PROJECT_NAME").withNewValue(project.getProjectId()).build())
+                .build();
 
         PipelineRunBuilder pipelineRun = new PipelineRunBuilder()
-                .withMetadata(
-                        new ObjectMetaBuilder()
-                                .withGenerateName("karavan-" + project.getProjectId() + "-")
-                                .withLabels(Map.of(
-                                        "karavan-project-id", project.getProjectId(),
-                                        "tekton.dev/pipeline", "karavan-quarkus"
-                                )).build()
-                )
-                .withSpec(
-                        new PipelineRunSpecBuilder()
-                                .withPipelineRef(
-                                        new PipelineRefBuilder().withName("karavan-quarkus").build()
-                                )
-                                .withServiceAccountName("pipeline")
-                                .withParams(new ParamBuilder().withName("PROJECT_NAME").withNewValue(project.getProjectId()).build())
-                                .build()
-                );
+                .withMetadata(meta)
+                .withSpec(spec);
+
         return tektonClient().v1beta1().pipelineRuns().create(pipelineRun.build()).getMetadata().getName();
     }
+
+    public PipelineRun getPipelineRun(String name, String namespace) throws Exception {
+        return tektonClient().v1beta1().pipelineRuns().inNamespace(namespace).withName(name).get();
+    }
 }
diff --git a/karavan-app/src/main/resources/application.properties b/karavan-app/src/main/resources/application.properties
index 1f5929e..b3724db 100644
--- a/karavan-app/src/main/resources/application.properties
+++ b/karavan-app/src/main/resources/application.properties
@@ -29,10 +29,13 @@ karavan.config.image-group=karavan
 karavan.config.runtime=QUARKUS
 karavan.config.runtime-version=2.9.2.Final
 karavan.config.environments[0].name=dev
+karavan.config.environments[0].namespace=karavan
 karavan.config.environments[0].cluster=kubernetes.default.svc
 karavan.config.environments[1].name=test
+karavan.config.environments[1].namespace=test
 karavan.config.environments[1].cluster=kubernetes.default.svc
 karavan.config.environments[2].name=prod
+karavan.config.environments[2].namespace=prod
 karavan.config.environments[2].cluster=kubernetes.default.svc
 
 # Infinispan Server address
diff --git a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
index b8616ee..46f3d84 100644
--- a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
+++ b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
@@ -111,6 +111,16 @@ export const KaravanApi = {
         });
     },
 
+    tekton: async (project: Project, environment: string, after: (res: AxiosResponse<any>) => void) => {
+        axios.post('/tekton/' + environment, project,
+            {headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'username': 'cameleer'}})
+            .then(res => {
+                after(res);
+            }).catch(err => {
+            after(err);
+        });
+    },
+
     getKameletNames: async (after: (names: []) => void) => {
         axios.get('/kamelet',
             {headers: {'Accept': 'application/json'}})
diff --git a/karavan-app/src/main/webapp/src/index.css b/karavan-app/src/main/webapp/src/index.css
index b2a580b..7f33f3c 100644
--- a/karavan-app/src/main/webapp/src/index.css
+++ b/karavan-app/src/main/webapp/src/index.css
@@ -99,13 +99,17 @@
 .karavan .project-page .table {
   margin-top: 16px;
 }
+.karavan .project-page .project-button {
+  width: 100px;
+}
 
 .karavan .action-cell {
   padding: 0;
 }
 
 .karavan .runtime-badge {
-  width: 75px;
+  min-width: 18px;
+  padding: 0;
 }
 
 .create-file-form .pf-c-form__group {
diff --git a/karavan-app/src/main/webapp/src/models/ProjectModels.ts b/karavan-app/src/main/webapp/src/models/ProjectModels.ts
index 89daf48..51474f8 100644
--- a/karavan-app/src/main/webapp/src/models/ProjectModels.ts
+++ b/karavan-app/src/main/webapp/src/models/ProjectModels.ts
@@ -3,8 +3,9 @@ export class Project {
     name: string = '';
     description: string = '';
     lastCommit: string = '';
+    lastPipelineRun: string = '';
 
-    public constructor(projectId: string, name: string, description: string, lastCommit: string);
+    public constructor(projectId: string, name: string, description: string, lastCommit: string, lastPipelineRun: string);
     public constructor(init?: Partial<Project>);
     public constructor(...args: any[]) {
         if (args.length === 1){
@@ -15,6 +16,7 @@ export class Project {
             this.name = args[1];
             this.description = args[2];
             this.lastCommit = args[3];
+            this.lastPipelineRun = args[4];
             return;
         }
     }
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx b/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
index acdf5ae..2648f41 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
@@ -21,7 +21,7 @@ import {
     EmptyStateVariant,
     EmptyStateIcon,
     Title,
-    ModalVariant, Modal, Spinner, Tooltip, Flex, FlexItem,
+    ModalVariant, Modal, Spinner, Tooltip, Flex, FlexItem, ToggleGroup, ToggleGroupItem,
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {MainToolbar} from "../MainToolbar";
@@ -38,8 +38,13 @@ 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 DeployIcon from "@patternfly/react-icons/dist/esm/icons/process-automation-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";
 
 interface Props {
     project: Project,
@@ -53,9 +58,11 @@ interface State {
     isUploadModalOpen: boolean,
     isDeleteModalOpen: boolean,
     isCreateModalOpen: boolean,
-    isPushModalOpen: boolean,
     isPushing: boolean,
-    fileToDelete?: ProjectFile
+    isBuilding: boolean,
+    fileToDelete?: ProjectFile,
+    environments: string[],
+    environment: string
 }
 
 export class ProjectPage extends React.Component<Props, State> {
@@ -65,9 +72,13 @@ export class ProjectPage extends React.Component<Props, State> {
         isUploadModalOpen: false,
         isCreateModalOpen: false,
         isDeleteModalOpen: false,
-        isPushModalOpen: false,
         isPushing: false,
-        files: []
+        isBuilding: false,
+        files: [],
+        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)
+            ? this.props.config.environments[0] : ''
     };
 
     componentDidMount() {
@@ -118,7 +129,6 @@ export class ProjectPage extends React.Component<Props, State> {
 
     tools = () => {
         const isFile = this.state.file !== undefined;
-        const isPushing = this.state.isPushing;
         return <Toolbar id="toolbar-group-types">
             {isFile && <ToolbarContent>
                 <ToolbarItem>
@@ -127,23 +137,13 @@ export class ProjectPage extends React.Component<Props, State> {
             </ToolbarContent>}
             {!isFile && <ToolbarContent>
                 <ToolbarItem>
-                    {!isPushing && <Button variant={"primary"} icon={<PlusIcon/>}
-                                           onClick={e => this.setState({isCreateModalOpen: true})}>Create</Button>}
-                </ToolbarItem>
-                <ToolbarItem>
-                    {!isPushing && <Button variant="secondary" icon={<UploadIcon/>}
-                                           onClick={e => this.setState({isUploadModalOpen: true})}>Upload</Button>}
+                    <Button variant={"primary"} icon={<PlusIcon/>}
+                            onClick={e => this.setState({isCreateModalOpen: true})}>Create</Button>
                 </ToolbarItem>
                 <ToolbarItem>
-                    {!isPushing && <Button variant="secondary" icon={<PushIcon/>}
-                                           onClick={e => this.setState({isPushModalOpen: true})}>Push</Button>}
+                    <Button variant="secondary" icon={<UploadIcon/>}
+                            onClick={e => this.setState({isUploadModalOpen: true})}>Upload</Button>
                 </ToolbarItem>
-                {isPushing && <ToolbarItem>
-                    <Button variant="link" isDisabled>Pushing...</Button>
-                </ToolbarItem>}
-                {isPushing && <ToolbarItem>
-                    <Spinner isSVG diameter="30px"/>
-                </ToolbarItem>}
             </ToolbarContent>}
         </Toolbar>
     };
@@ -189,7 +189,6 @@ export class ProjectPage extends React.Component<Props, State> {
         this.setState({
             isUploadModalOpen: false,
             isCreateModalOpen: false,
-            isPushModalOpen: false,
             isPushing: isPushing
         });
         this.onRefresh();
@@ -215,12 +214,26 @@ export class ProjectPage extends React.Component<Props, State> {
         }
     }
 
-    push = () => {
-        this.closeModal(true);
+    push = (after?: () => void) => {
+        this.setState({isPushing: true});
         KaravanApi.push(this.props.project, res => {
             console.log(res)
             if (res.status === 200 || res.status === 201) {
                 this.setState({isPushing: false});
+                after?.call(this);
+                this.onRefresh();
+            } else {
+                // Todo notification
+            }
+        });
+    }
+
+    build = () => {
+        this.setState({isBuilding: true});
+        KaravanApi.tekton(this.props.project, this.state.environment, res => {
+            console.log(res)
+            if (res.status === 200 || res.status === 201) {
+                this.setState({isBuilding: false});
                 this.onRefresh();
             } else {
                 // Todo notification
@@ -238,11 +251,53 @@ export class ProjectPage extends React.Component<Props, State> {
         }
     }
 
+    pushButton = () => {
+        const isPushing = this.state.isPushing;
+        return (<Tooltip content="Commit and push to git" position={"left"}>
+            <Button isLoading={isPushing ? true : undefined} isSmall variant="secondary"
+                    className="project-button"
+                    icon={!isPushing ? <PushIcon/> : <div></div>}
+                    onClick={e => this.push()}>
+                {isPushing ? "..." : "Commit"}
+            </Button>
+        </Tooltip>)
+    }
+
+    buildButton = () => {
+        const isDeploying = this.state.isBuilding;
+        return (<Tooltip content="Commit, push, build and deploy" position={"left"}>
+            <Button isLoading={isDeploying ? true : undefined} isSmall variant="secondary"
+                    className="project-button"
+                    icon={!isDeploying ? <BuildIcon/> : <div></div>}
+                    onClick={e => {
+                        this.push(() => this.build());
+                    }}>
+                {isDeploying ? "..." : "Run"}
+            </Button>
+        </Tooltip>)
+    }
+
+    getProgressIcon(status?: 'pending' | 'progress' | 'done' | 'error') {
+        switch (status) {
+            case "pending":
+                return <PendingIcon color={"grey"}/>;
+            case "progress":
+                return <Spinner isSVG size="md"/>
+            case "done":
+                return <CheckCircleIcon color={"green"}/>;
+            case "error":
+                return <ExclamationCircleIcon color={"red"}/>;
+            default:
+                return undefined;
+        }
+    }
+
+    getCurrentStatus() {
+        return (<Text>OK</Text>)
+    }
+
     getProjectForm = () => {
-        const project = this.state.project;
-        const environments: string[] = this.props.config.environments && Array.isArray(this.props.config.environments)
-            ? Array.from(this.props.config.environments)
-            : [];
+        const {project, environments, environment, isBuilding} = this.state;
         return (
             <Card>
                 <CardBody isFilled>
@@ -262,21 +317,28 @@ export class ProjectPage extends React.Component<Props, State> {
                                     <DescriptionListTerm>Description</DescriptionListTerm>
                                     <DescriptionListDescription>{project?.description}</DescriptionListDescription>
                                 </DescriptionListGroup>
-
                             </DescriptionList>
                         </FlexItem>
                         <FlexItem flex={{default: "flex_1"}}>
                             <DescriptionList isHorizontal>
                                 <DescriptionListGroup>
-                                    <DescriptionListTerm>Latest Commit</DescriptionListTerm>
+                                    <DescriptionListTerm>Last Commit</DescriptionListTerm>
                                     <DescriptionListDescription>
                                         <Tooltip content={project?.lastCommit} position={"bottom"}>
-                                            <Badge>{project?.lastCommit?.substr(0, 7)}</Badge>
+                                            <Badge>{project?.lastCommit ? project?.lastCommit?.substr(0, 7) : "-"}</Badge>
                                         </Tooltip>
                                     </DescriptionListDescription>
                                 </DescriptionListGroup>
                                 <DescriptionListGroup>
-                                    <DescriptionListTerm>Deployment</DescriptionListTerm>
+                                    <DescriptionListTerm>Last Pipeline Run</DescriptionListTerm>
+                                    <DescriptionListDescription>
+                                        <Tooltip content={project?.lastPipelineRun} position={"bottom"}>
+                                            <Badge>{project?.lastPipelineRun ? project?.lastPipelineRun : "-"}</Badge>
+                                        </Tooltip>
+                                    </DescriptionListDescription>
+                                </DescriptionListGroup>
+                                <DescriptionListGroup>
+                                    <DescriptionListTerm>Status</DescriptionListTerm>
                                     <DescriptionListDescription>
                                         <Flex direction={{default: "row"}}>
                                             {environments.filter(e => e !== undefined)
@@ -284,8 +346,32 @@ export class ProjectPage extends React.Component<Props, State> {
                                         </Flex>
                                     </DescriptionListDescription>
                                 </DescriptionListGroup>
+                                {/*<DescriptionListGroup>*/}
+                                {/*    <DescriptionListTerm>Environment</DescriptionListTerm>*/}
+                                {/*    <DescriptionListDescription>*/}
+                                {/*        <ToggleGroup isCompact>*/}
+                                {/*            {environments.filter(e => e !== undefined)*/}
+                                {/*                .map(e => <ToggleGroupItem key={e} text={e} isSelected={environment === e}*/}
+                                {/*                                           onChange={s => this.setState({environment: e})}>*/}
+                                {/*                </ToggleGroupItem>)}*/}
+                                {/*        </ToggleGroup>*/}
+                                {/*    </DescriptionListDescription>*/}
+                                {/*</DescriptionListGroup>*/}
                             </DescriptionList>
                         </FlexItem>
+                        <FlexItem >
+                            <Flex direction={{default: "column"}}>
+                                <FlexItem>
+                                    {this.pushButton()}
+                                </FlexItem>
+                                <FlexItem>
+                                    {this.buildButton()}
+                                </FlexItem>
+                                <FlexItem>
+                                    <Button isSmall style={{visibility:"hidden"}}>Refresh</Button>
+                                </FlexItem>
+                            </Flex>
+                        </FlexItem>
                     </Flex>
                 </CardBody>
             </Card>
@@ -423,19 +509,6 @@ export class ProjectPage extends React.Component<Props, State> {
                     onEscapePress={e => this.setState({isDeleteModalOpen: false})}>
                     <div>{"Are you sure you want to delete the file " + this.state.fileToDelete?.name + "?"}</div>
                 </Modal>
-                <Modal
-                    title="Push"
-                    variant={ModalVariant.small}
-                    isOpen={this.state.isPushModalOpen}
-                    onClose={() => this.setState({isPushModalOpen: false})}
-                    actions={[
-                        <Button key="confirm" variant="primary" onClick={e => this.push()}>Push</Button>,
-                        <Button key="cancel" variant="link"
-                                onClick={e => this.setState({isPushModalOpen: false})}>Cancel</Button>
-                    ]}
-                    onEscapePress={e => this.setState({isPushModalOpen: false})}>
-                    <div>{"Push project to repository"}</div>
-                </Modal>
             </PageSection>
         )
     }
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx b/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
index 7f19ac6..836e54a 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectsPage.tsx
@@ -99,7 +99,7 @@ export class ProjectsPage extends React.Component<Props, State> {
 
     saveAndCloseCreateModal = () => {
         const {name, description, projectId} = this.state;
-        const p = new Project(projectId, name, description, '');
+        const p = new Project(projectId, name, description, '', '');
         this.props.onCreate.call(this, p);
         this.setState({isCreateModalOpen: false, isCopy: false, name: this.props.config.groupId, description: '',  projectId: ''});
     }
@@ -173,7 +173,9 @@ export class ProjectsPage extends React.Component<Props, State> {
                             {projects.map(project => (
                                 <Tr key={project.projectId}>
                                     <Td modifier={"fitContent"}>
-                                        <Badge className="runtime-badge">{this.props.config.runtime}</Badge>
+                                        <Tooltip content={this.props.config.runtime} position={"left"}>
+                                            <Badge className="runtime-badge">{this.props.config.runtime.substring(0,1)}</Badge>
+                                        </Tooltip>
                                     </Td>
                                     <Td>
                                         <Button style={{padding: '6px'}} variant={"link"} onClick={e=>this.props.onSelect?.call(this, project)}>