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/27 16:32:16 UTC

[camel-karavan] branch main updated: Saas feature16 (#395)

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 5cbaef6  Saas feature16 (#395)
5cbaef6 is described below

commit 5cbaef6a86491985d25b2248b57c0a6a81a00ea9
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Mon Jun 27 12:32:12 2022 -0400

    Saas feature16 (#395)
    
    * Status collect improvements
    
    * Designer/editor switch
---
 .../apache/camel/karavan/api/StatusResource.java   |  91 +------
 .../camel/karavan/model/KaravanConfiguration.java  |   1 +
 .../camel/karavan/service/StatusService.java       | 127 ++++++++++
 .../src/main/resources/application.properties      |   5 +-
 .../src/main/webapp/src/projects/ProjectHeader.tsx | 249 +++++++++++++++++++
 .../src/main/webapp/src/projects/ProjectPage.tsx   | 272 +++------------------
 karavan-designer/src/designer/KaravanDesigner.tsx  |   6 +-
 7 files changed, 429 insertions(+), 322 deletions(-)

diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/StatusResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/StatusResource.java
index 451786b..02d08c9 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
@@ -16,23 +16,12 @@
  */
 package org.apache.camel.karavan.api;
 
-import io.fabric8.tekton.pipeline.v1beta1.PipelineRun;
-import io.quarkus.runtime.configuration.ProfileManager;
-import io.smallrye.mutiny.Uni;
-import io.vertx.mutiny.core.Vertx;
-import io.vertx.mutiny.core.buffer.Buffer;
-import io.vertx.mutiny.ext.web.client.HttpResponse;
-import io.vertx.mutiny.ext.web.client.WebClient;
+import io.vertx.core.eventbus.EventBus;
 import org.apache.camel.karavan.model.KaravanConfiguration;
-import org.apache.camel.karavan.model.Project;
 import org.apache.camel.karavan.model.ProjectEnvStatus;
 import org.apache.camel.karavan.model.ProjectStatus;
 import org.apache.camel.karavan.service.InfinispanService;
-import org.apache.camel.karavan.service.KubernetesService;
-import org.eclipse.microprofile.faulttolerance.Asynchronous;
-import org.eclipse.microprofile.faulttolerance.Fallback;
-import org.eclipse.microprofile.faulttolerance.Retry;
-import org.eclipse.microprofile.faulttolerance.Timeout;
+import org.apache.camel.karavan.service.StatusService;
 import org.jboss.logging.Logger;
 
 import javax.inject.Inject;
@@ -42,11 +31,6 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.stream.Collectors;
 
 @Path("/status")
@@ -57,79 +41,26 @@ public class StatusResource {
     @Inject
     InfinispanService infinispanService;
 
-    @Inject
-    KubernetesService kubernetesService;
-
     @Inject
     KaravanConfiguration configuration;
 
     @Inject
-    Vertx vertx;
-
-    WebClient webClient;
-
-    public WebClient getWebClient() {
-        if (webClient == null) {
-            webClient = WebClient.create(vertx);
-        }
-        return webClient;
-    }
+    EventBus bus;
 
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/{projectId}")
-    @Timeout(value = 1000)
-    @Retry(maxRetries = 0)
-    @Asynchronous
-    @Fallback(fallbackMethod = "fallbackStatus")
-    public Uni<ProjectStatus> getStatus(@HeaderParam("username") String username, @PathParam("projectId") String projectId) throws Exception {
-        ProjectStatus status = new ProjectStatus();
-        status.setProjectId(projectId);
-        status.setLastUpdate(System.currentTimeMillis());
-        List<ProjectEnvStatus> statuses = new ArrayList<>(configuration.environments().size());
-        configuration.environments().forEach(e -> {
-            String url = ProfileManager.getActiveProfile().equals("dev")
-                    ? String.format("http://%s-%s.%s/q/health", projectId, e.namespace(), e.cluster())
-                    : String.format("http://%s.%s.%s/q/health", projectId, e.namespace(), e.cluster());
-            // TODO: make it reactive
-            try {
-                HttpResponse<Buffer> result = getWebClient().getAbs(url).timeout(1000).send().subscribeAsCompletionStage().toCompletableFuture().get();
-                if (result.bodyAsJsonObject().getString("status").equals("UP")) {
-                    statuses.add(new ProjectEnvStatus(e.name(), ProjectEnvStatus.Status.UP));
-                } else {
-                    statuses.add(new ProjectEnvStatus(e.name(), ProjectEnvStatus.Status.DOWN));
-                }
-            } catch (Exception ex) {
-                statuses.add(new ProjectEnvStatus(e.name(), ProjectEnvStatus.Status.DOWN));
-                LOGGER.error(ex);
-            }
-        });
-        status.setStatuses(statuses);
-
-        Project project = infinispanService.getProject(projectId);
-        PipelineRun pipelineRun = kubernetesService.getPipelineRun(project.getLastPipelineRun(), configuration.environments().get(0).namespace());
-        if (pipelineRun != null) {
-            status.setPipeline(pipelineRun.getStatus().getConditions().get(0).getReason());
-        } else {
-            status.setPipeline("Undefined");
-        }
-
-        LOGGER.info("Storing status in cache for " + projectId);
-        infinispanService.saveProjectStatus(status);
-        return Uni.createFrom().item(status);
-    }
-
-    public Uni<ProjectStatus> fallbackStatus(String username, String projectId) {
-        LOGGER.info("Return cached status for " + projectId);
+    public ProjectStatus getStatus(@HeaderParam("username") String username, @PathParam("projectId") String projectId) throws Exception {
+        bus.publish(StatusService.CMD_COLLECT_STATUSES, projectId);
         ProjectStatus status = infinispanService.getProjectStatus(projectId);
         if (status != null){
-            return Uni.createFrom().item(status);
+            return status;
         } else {
-            return Uni.createFrom().item(new ProjectStatus(
-                    projectId,
-                    configuration.environments().stream().map(e -> new ProjectEnvStatus(e.name(), ProjectEnvStatus.Status.DOWN)).collect(Collectors.toList()),
-                    Long.valueOf(0), "Undefined"
-            ));
+            return new ProjectStatus( projectId,
+                    configuration.environments()
+                            .stream().map(e -> new ProjectEnvStatus(e.name(), ProjectEnvStatus.Status.DOWN))
+                            .collect(Collectors.toList()),
+                    Long.valueOf(0), "Undefined");
         }
     }
 }
\ 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 a843798..3cea180 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
@@ -11,6 +11,7 @@ public interface KaravanConfiguration {
     String imageGroup();
     String runtime();
     String runtimeVersion();
+    Long statusThreshold();
     List<Environment> environments();
 
     interface Environment {
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/StatusService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/StatusService.java
new file mode 100644
index 0000000..074feb8
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/StatusService.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.karavan.service;
+
+import io.fabric8.tekton.pipeline.v1beta1.PipelineRun;
+import io.quarkus.runtime.configuration.ProfileManager;
+import io.quarkus.vertx.ConsumeEvent;
+import io.smallrye.mutiny.tuples.Tuple2;
+import io.vertx.mutiny.core.Vertx;
+import io.vertx.mutiny.core.buffer.Buffer;
+import io.vertx.mutiny.ext.web.client.HttpResponse;
+import io.vertx.mutiny.ext.web.client.WebClient;
+import org.apache.camel.karavan.model.KaravanConfiguration;
+import org.apache.camel.karavan.model.Project;
+import org.apache.camel.karavan.model.ProjectEnvStatus;
+import org.apache.camel.karavan.model.ProjectStatus;
+import org.jboss.logging.Logger;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+
+@ApplicationScoped
+public class StatusService {
+
+    private static final Logger LOGGER = Logger.getLogger(StatusService.class.getName());
+    public static final String CMD_COLLECT_STATUSES = "collect-statuses";
+
+    @Inject
+    InfinispanService infinispanService;
+
+    @Inject
+    KubernetesService kubernetesService;
+
+    @Inject
+    KaravanConfiguration configuration;
+
+    private long lastCollect = 0;
+
+    @Inject
+    Vertx vertx;
+
+    WebClient webClient;
+
+    public WebClient getWebClient() {
+        if (webClient == null) {
+            webClient = WebClient.create(vertx);
+        }
+        return webClient;
+    }
+
+    @ConsumeEvent(value = CMD_COLLECT_STATUSES, blocking = true)
+    public void collectStatuses(String projectId) throws Exception {
+        LOGGER.info("Event received to collect statuses for the project " + projectId);
+        if (System.currentTimeMillis() - lastCollect > configuration.statusThreshold()){
+            getStatuses(projectId);
+        }
+    }
+
+    private void getStatuses(String projectId) throws Exception {
+        LOGGER.info("Start to collect statuses for the project " + projectId);
+        ProjectStatus status = new ProjectStatus();
+        status.setProjectId(projectId);
+        status.setLastUpdate(System.currentTimeMillis());
+        List<ProjectEnvStatus> statuses = new ArrayList<>(configuration.environments().size());
+        configuration.environments().forEach(e -> {
+            String url = ProfileManager.getActiveProfile().equals("dev")
+                    ? String.format("http://%s-%s.%s/q/health", projectId, e.namespace(), e.cluster())
+                    : String.format("http://%s.%s.%s/q/health", projectId, e.namespace(), e.cluster());
+            Tuple2<Boolean, ProjectEnvStatus> s = getProjectEnvStatus(url, e.name());
+            if (s.getItem1()) statuses.add(s.getItem2());
+        });
+        status.setStatuses(statuses);
+
+        Project project = infinispanService.getProject(projectId);
+        Tuple2<Boolean, String> p = getProjectPipelineStatus(project.getLastPipelineRun());
+        if (p.getItem1()) status.setPipeline(p.getItem2());
+
+        LOGGER.info("Storing status in cache for " + projectId);
+        infinispanService.saveProjectStatus(status);
+    }
+
+    private Tuple2<Boolean, String> getProjectPipelineStatus(String lastPipelineRun) {
+        try {
+            PipelineRun pipelineRun = kubernetesService.getPipelineRun(lastPipelineRun, configuration.environments().get(0).namespace());
+            if (pipelineRun != null) {
+                return Tuple2.of(true, pipelineRun.getStatus().getConditions().get(0).getReason());
+            } else {
+                return Tuple2.of(true,"Undefined");
+            }
+        } catch (Exception ex) {
+            LOGGER.error(ex.getMessage());
+            return Tuple2.of(false, "Undefined");
+        }
+    }
+
+    private Tuple2<Boolean, ProjectEnvStatus> getProjectEnvStatus(String url, String env) {
+        // TODO: make it reactive
+        try {
+            HttpResponse<Buffer> result = getWebClient().getAbs(url).timeout(1000).send().subscribeAsCompletionStage().toCompletableFuture().get();
+            if (result.bodyAsJsonObject().getString("status").equals("UP")) {
+                return Tuple2.of(true, new ProjectEnvStatus(env, ProjectEnvStatus.Status.UP));
+            } else {
+                return Tuple2.of(true, new ProjectEnvStatus(env, ProjectEnvStatus.Status.DOWN));
+            }
+        } catch (Exception ex) {
+            LOGGER.error(ex.getMessage());
+            return Tuple2.of(false, new ProjectEnvStatus(env, ProjectEnvStatus.Status.DOWN));
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/resources/application.properties b/karavan-app/src/main/resources/application.properties
index e942344..b230f87 100644
--- a/karavan-app/src/main/resources/application.properties
+++ b/karavan-app/src/main/resources/application.properties
@@ -20,10 +20,10 @@ karavan.config.group-id=org.camel.karavan.demo
 karavan.config.image-group=karavan
 karavan.config.runtime=QUARKUS
 karavan.config.runtime-version=2.10.0.Final
+karavan.config.status-threshold=5000
 karavan.config.environments[0].name=dev
 karavan.config.environments[0].namespace=karavan
 karavan.config.environments[0].cluster=svc.cluster.local
-%dev.karavan.config.environments[0].cluster=apps.cluster-ztffv.ztffv.sandbox55.opentlc.com
 karavan.config.environments[1].name=test
 karavan.config.environments[1].namespace=test
 karavan.config.environments[1].cluster=svc.cluster.local
@@ -31,6 +31,9 @@ karavan.config.environments[2].name=prod
 karavan.config.environments[2].namespace=prod
 karavan.config.environments[2].cluster=svc.cluster.local
 
+%dev.karavan.config.environments[0].cluster=apps.cluster-ztffv.ztffv.sandbox55.opentlc.com
+
+
 # Infinispan Server address
 #quarkus.infinispan-client.server-list=localhost:12345
 quarkus.infinispan-client.devservices.enabled=false
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx b/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx
new file mode 100644
index 0000000..30a2ce0
--- /dev/null
+++ b/karavan-app/src/main/webapp/src/projects/ProjectHeader.tsx
@@ -0,0 +1,249 @@
+import React from 'react';
+import {
+    Badge,
+    Button,
+    Text,
+    DescriptionList,
+    DescriptionListTerm,
+    DescriptionListGroup,
+    DescriptionListDescription,
+    Card,
+    CardBody, Spinner, Tooltip, Flex, FlexItem
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import {KaravanApi} from "../api/KaravanApi";
+import {Project, ProjectFileTypes, ProjectStatus} from "../models/ProjectModels";
+import BuildIcon from "@patternfly/react-icons/dist/esm/icons/build-icon";
+import PushIcon from "@patternfly/react-icons/dist/esm/icons/code-branch-icon";
+
+interface Props {
+    project: Project,
+    config: any,
+}
+
+interface State {
+    project?: Project,
+    status?: ProjectStatus,
+    isPushing: boolean,
+    isBuilding: boolean,
+    environments: string[],
+    environment: string,
+    key?: string,
+}
+
+export class ProjectHeader extends React.Component<Props, State> {
+
+    public state: State = {
+        project: this.props.project,
+        isPushing: false,
+        isBuilding: 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)
+            ? this.props.config.environments[0] : ''
+    };
+    interval: any;
+
+    componentDidMount() {
+        this.onRefresh();
+        this.interval = setInterval(() => this.onRefreshStatus(), 3000);
+    }
+
+    componentWillUnmount() {
+        clearInterval(this.interval);
+    }
+
+    onRefresh = () => {
+        if (this.props.project) {
+            KaravanApi.getProject(this.props.project.projectId, (project: Project) => {
+                this.setState({
+                    project: project
+                })
+            });
+        }
+    }
+
+    onRefreshStatus = () => {
+        if (this.props.project) {
+            KaravanApi.getProjectStatus(this.props.project.projectId, (status: ProjectStatus) => {
+                this.setState({
+                    key: Math.random().toString(),
+                    status: status
+                });
+                // console.log(status);
+            });
+        }
+    }
+
+    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
+            }
+        });
+    }
+
+    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"
+        }
+    }
+
+    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 ? "..." : "Build"}
+            </Button>
+        </Tooltip>)
+    }
+
+    getCurrentStatus() {
+        return (<Text>OK</Text>)
+    }
+
+    getPipelineState() {
+        const {project, status} = this.state;
+        const isRunning = status?.pipeline === 'Running';
+        const isFailed = status?.pipeline === 'Failed';
+        const isSucceeded = status?.pipeline === 'Succeeded';
+        let classname = "pipeline"
+        if (isRunning) classname = classname + " pipeline-running";
+        if (isFailed) classname = classname + " pipeline-running";
+        if (isSucceeded) classname = classname + " pipeline-succeeded";
+        return (
+            <Flex spaceItems={{default: 'spaceItemsNone'}} className={classname} direction={{default: "row"}}
+                  alignItems={{default: "alignItemsCenter"}}>
+                <FlexItem style={{height: "18px"}}>
+                    {isRunning && <Spinner isSVG diameter="16px"/>}
+                </FlexItem>
+                <FlexItem style={{height: "18px"}}>
+                    {project?.lastPipelineRun ? project?.lastPipelineRun : "-"}
+                </FlexItem>
+            </Flex>
+        )
+    }
+
+    isUp(env: string): boolean {
+        if (this.state.status) {
+            return this.state.status.statuses.find(s => s.environment === env)?.status === 'UP';
+        } else {
+            return false;
+        }
+    }
+
+    render() {
+        const {project, environments, status} = this.state;
+        return (
+            <Card>
+                <CardBody isFilled>
+                    <Flex direction={{default: "row"}} alignContent={{default: "alignContentSpaceBetween"}}
+                          style={{width: "100%"}}>
+                        <FlexItem flex={{default: "flex_1"}}>
+                            <DescriptionList isHorizontal>
+                                <DescriptionListGroup>
+                                    <DescriptionListTerm>Project ID</DescriptionListTerm>
+                                    <DescriptionListDescription>{project?.projectId}</DescriptionListDescription>
+                                </DescriptionListGroup>
+                                <DescriptionListGroup>
+                                    <DescriptionListTerm>Name</DescriptionListTerm>
+                                    <DescriptionListDescription>{project?.name}</DescriptionListDescription>
+                                </DescriptionListGroup>
+                                <DescriptionListGroup>
+                                    <DescriptionListTerm>Description</DescriptionListTerm>
+                                    <DescriptionListDescription>{project?.description}</DescriptionListDescription>
+                                </DescriptionListGroup>
+                            </DescriptionList>
+                        </FlexItem>
+                        <FlexItem flex={{default: "flex_1"}}>
+                            <DescriptionList isHorizontal>
+                                <DescriptionListGroup>
+                                    <DescriptionListTerm>Commit</DescriptionListTerm>
+                                    <DescriptionListDescription>
+                                        <Tooltip content={project?.lastCommit} position={"bottom"}>
+                                            <Badge>{project?.lastCommit ? project?.lastCommit?.substr(0, 7) : "-"}</Badge>
+                                        </Tooltip>
+                                    </DescriptionListDescription>
+                                </DescriptionListGroup>
+                                <DescriptionListGroup>
+                                    <DescriptionListTerm>Pipeline Run</DescriptionListTerm>
+                                    <DescriptionListDescription>
+                                        {this.getPipelineState()}
+                                    </DescriptionListDescription>
+                                </DescriptionListGroup>
+                                <DescriptionListGroup key={this.state.key}>
+                                    <DescriptionListTerm>Status</DescriptionListTerm>
+                                    <DescriptionListDescription>
+                                        <Flex direction={{default: "row"}}>
+                                            {environments.filter(e => e !== undefined)
+                                                .map(e =>
+                                                    <FlexItem key={e}>
+                                                        <Tooltip content={"Last update: " + (status ? new Date(status.lastUpdate).toISOString() : "N/A")}
+                                                                 position={"bottom"}>
+                                                            <Badge className={this.isUp(e) ? "badge-env-up" : ""} isRead>{e}</Badge>
+                                                        </Tooltip>
+                                                    </FlexItem>)}
+                                        </Flex>
+                                    </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>
+        )
+    }
+}
diff --git a/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx b/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
index 4e86ce9..022d02e 100644
--- a/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
+++ b/karavan-app/src/main/webapp/src/projects/ProjectPage.tsx
@@ -10,18 +10,12 @@ import {
     Toolbar,
     ToolbarContent,
     ToolbarItem,
-    DescriptionList,
-    DescriptionListTerm,
-    DescriptionListGroup,
-    DescriptionListDescription,
-    Card,
-    CardBody,
     Bullseye,
     EmptyState,
     EmptyStateVariant,
     EmptyStateIcon,
     Title,
-    ModalVariant, Modal, Spinner, Tooltip, Flex, FlexItem, ProgressStep, ProgressStepper
+    ModalVariant, Modal, Spinner, Tooltip, Flex, FlexItem, ToggleGroup, ToggleGroupItem
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {MainToolbar} from "../MainToolbar";
@@ -44,6 +38,7 @@ 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 {
     project: Project,
@@ -58,12 +53,8 @@ interface State {
     isUploadModalOpen: boolean,
     isDeleteModalOpen: boolean,
     isCreateModalOpen: boolean,
-    isPushing: boolean,
-    isBuilding: boolean,
     fileToDelete?: ProjectFile,
-    environments: string[],
-    environment: string,
-    key?: string,
+    mode: "design" | "code";
 }
 
 export class ProjectPage extends React.Component<Props, State> {
@@ -73,23 +64,12 @@ export class ProjectPage extends React.Component<Props, State> {
         isUploadModalOpen: false,
         isCreateModalOpen: false,
         isDeleteModalOpen: false,
-        isPushing: false,
-        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] : ''
+        mode: "design"
     };
-    interval: any;
 
     componentDidMount() {
         this.onRefresh();
-        this.interval = setInterval(() => this.onRefreshStatus(), 3000);
-    }
-
-    componentWillUnmount() {
-        clearInterval(this.interval);
     }
 
     onRefresh = () => {
@@ -107,18 +87,6 @@ export class ProjectPage extends React.Component<Props, State> {
         }
     }
 
-    onRefreshStatus = () => {
-        if (this.props.project) {
-            KaravanApi.getProjectStatus(this.props.project.projectId, (status: ProjectStatus) => {
-                this.setState({
-                    key: Math.random().toString(),
-                    status: status
-                });
-                console.log(status);
-            });
-        }
-    }
-
     post = (file: ProjectFile) => {
         KaravanApi.postProjectFile(file, res => {
             if (res.status === 200) {
@@ -129,20 +97,10 @@ export class ProjectPage extends React.Component<Props, State> {
         })
     }
 
-    publish = () => {
-    }
-
-    copy = () => {
-    }
-
     copyToClipboard = (data: string) => {
         navigator.clipboard.writeText(data);
     }
 
-    changeView = (view: "design" | "code") => {
-        // this.setState({view: view});
-    }
-
     save = (name: string, code: string) => {
         const file = this.state.file;
         if (file) {
@@ -162,23 +120,31 @@ export class ProjectPage extends React.Component<Props, State> {
     }
 
     tools = () => {
-        const isFile = this.state.file !== undefined;
+        const {file, mode} = this.state;
+        const isFile = file !== undefined;
+        const isYaml = file !== undefined && file.name.endsWith("yaml");
         return <Toolbar id="toolbar-group-types">
-            {isFile && <ToolbarContent>
-                <ToolbarItem>
-                    <Button variant="secondary" icon={<DownloadIcon/>} onClick={e => this.download()}>Download</Button>
-                </ToolbarItem>
-            </ToolbarContent>}
-            {!isFile && <ToolbarContent>
-                <ToolbarItem>
-                    <Button variant={"primary"} icon={<PlusIcon/>}
-                            onClick={e => this.setState({isCreateModalOpen: true})}>Create</Button>
-                </ToolbarItem>
-                <ToolbarItem>
-                    <Button variant="secondary" icon={<UploadIcon/>}
-                            onClick={e => this.setState({isUploadModalOpen: true})}>Upload</Button>
-                </ToolbarItem>
-            </ToolbarContent>}
+            <ToolbarContent>
+                <Flex className="toolbar" direction={{default: "row"}}>
+                    {isYaml && <FlexItem>
+                        <ToggleGroup>
+                            <ToggleGroupItem text="Design" buttonId="design" isSelected={mode === "design"} onChange={s => this.setState({mode:"design"})} />
+                            <ToggleGroupItem text="Code" buttonId="code" isSelected={mode === "code"} onChange={s => this.setState({mode:"code"})} />
+                        </ToggleGroup>
+                    </FlexItem>}
+                    {isFile && <FlexItem>
+                        <Button variant="secondary" icon={<DownloadIcon/>} onClick={e => this.download()}>Download</Button>
+                    </FlexItem>}
+                    {!isFile && <FlexItem>
+                        <Button variant={"primary"} icon={<PlusIcon/>}
+                                onClick={e => this.setState({isCreateModalOpen: true})}>Create</Button>
+                    </FlexItem>}
+                    {!isFile && <FlexItem>
+                        <Button variant="secondary" icon={<UploadIcon/>}
+                                onClick={e => this.setState({isUploadModalOpen: true})}>Upload</Button>
+                    </FlexItem>}
+                </Flex>
+            </ToolbarContent>
         </Toolbar>
     };
 
@@ -208,7 +174,6 @@ export class ProjectPage extends React.Component<Props, State> {
         this.setState({
             isUploadModalOpen: false,
             isCreateModalOpen: false,
-            isPushing: isPushing
         });
         this.onRefresh();
     }
@@ -233,32 +198,6 @@ export class ProjectPage extends React.Component<Props, State> {
         }
     }
 
-    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
-            }
-        });
-    }
 
     getType = (name: string) => {
         const extension = name.substring(name.lastIndexOf('.') + 1);
@@ -270,151 +209,6 @@ 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 ? "..." : "Build"}
-            </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>)
-    }
-
-    getPipelineState() {
-        const {project, status} = this.state;
-        const isRunning = status?.pipeline === 'Running';
-        const isFailed = status?.pipeline === 'Failed';
-        const isSucceeded = status?.pipeline === 'Succeeded';
-        let classname = "pipeline"
-        if (isRunning) classname = classname + " pipeline-running";
-        if (isFailed) classname = classname + " pipeline-running";
-        if (isSucceeded) classname = classname + " pipeline-succeeded";
-        return (
-                <Flex spaceItems={{ default: 'spaceItemsNone' }} className={classname} direction={{default:"row"}} alignItems={{default: "alignItemsCenter"}}>
-                    <FlexItem style={{height:"18px"}}>
-                        {isRunning && <Spinner isSVG diameter="16px"/>}
-                    </FlexItem>
-                    <FlexItem style={{height:"18px"}}>
-                        {project?.lastPipelineRun ? project?.lastPipelineRun : "-"}
-                    </FlexItem>
-                </Flex>
-            )
-    }
-
-    isUp(env: string): boolean {
-        if (this.state.status) {
-            return this.state.status.statuses.find(s => s.environment === env)?.status === 'UP';
-        } else {
-            return false;
-        }
-    }
-
-    getProjectForm = () => {
-        const {project, environments} = this.state;
-        return (
-            <Card>
-                <CardBody isFilled>
-                    <Flex direction={{default: "row"}} alignContent={{default: "alignContentSpaceBetween"}}
-                          style={{width: "100%"}}>
-                        <FlexItem flex={{default: "flex_1"}}>
-                            <DescriptionList isHorizontal>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Project ID</DescriptionListTerm>
-                                    <DescriptionListDescription>{project?.projectId}</DescriptionListDescription>
-                                </DescriptionListGroup>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Name</DescriptionListTerm>
-                                    <DescriptionListDescription>{project?.name}</DescriptionListDescription>
-                                </DescriptionListGroup>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Description</DescriptionListTerm>
-                                    <DescriptionListDescription>{project?.description}</DescriptionListDescription>
-                                </DescriptionListGroup>
-                            </DescriptionList>
-                        </FlexItem>
-                        <FlexItem flex={{default: "flex_1"}}>
-                            <DescriptionList isHorizontal>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Commit</DescriptionListTerm>
-                                    <DescriptionListDescription>
-                                        <Tooltip content={project?.lastCommit} position={"bottom"}>
-                                            <Badge>{project?.lastCommit ? project?.lastCommit?.substr(0, 7) : "-"}</Badge>
-                                        </Tooltip>
-                                    </DescriptionListDescription>
-                                </DescriptionListGroup>
-                                <DescriptionListGroup>
-                                    <DescriptionListTerm>Pipeline Run</DescriptionListTerm>
-                                    <DescriptionListDescription>
-                                        {this.getPipelineState()}
-                                    </DescriptionListDescription>
-                                </DescriptionListGroup>
-                                <DescriptionListGroup key={this.state.key}>
-                                    <DescriptionListTerm>Status</DescriptionListTerm>
-                                    <DescriptionListDescription>
-                                        <Flex direction={{default: "row"}}>
-                                            {environments.filter(e => e !== undefined)
-                                                .map(e => <FlexItem key={e}><Badge
-                                                    className={this.isUp(e) ? "badge-env-up" : ""}
-                                                    isRead>{e}</Badge></FlexItem>)}
-                                        </Flex>
-                                    </DescriptionListDescription>
-                                </DescriptionListGroup>
-                            </DescriptionList>
-                        </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>
-        )
-    }
-
     getProjectFiles = () => {
         const files = this.state.files;
         return (
@@ -514,10 +308,12 @@ export class ProjectPage extends React.Component<Props, State> {
     }
 
     render() {
-        const file = this.state.file;
+        const {file, mode} = this.state;
         const isYaml = file !== undefined && file.name.endsWith("yaml");
         const isProperties = file !== undefined && file.name.endsWith("properties");
         const isCode = file !== undefined && (file.name.endsWith("java") || file.name.endsWith("groovy"));
+        const showDesigner = isYaml && mode === 'design';
+        const showEditor = isCode || (isYaml && mode === 'code');
         return (
             <PageSection className="kamelet-section project-page" padding={{default: 'noPadding'}}>
                 <PageSection className="tools-section" padding={{default: 'noPadding'}}>
@@ -526,11 +322,11 @@ export class ProjectPage extends React.Component<Props, State> {
                 {file === undefined &&
                     <PageSection isFilled className="kamelets-page"
                                  padding={{default: file !== undefined ? 'noPadding' : 'padding'}}>
-                        {this.getProjectForm()}
+                        {<ProjectHeader project={this.props.project} config={this.props.config}/>}
                         {this.getProjectFiles()}
                     </PageSection>}
-                {isYaml && this.getDesigner()}
-                {isCode && this.getEditor()}
+                {showDesigner && this.getDesigner()}
+                {showEditor && this.getEditor()}
                 {isProperties && this.getPropertiesEditor()}
                 <CreateFileModal project={this.props.project} isOpen={this.state.isCreateModalOpen}
                                  onClose={this.closeModal}/>
diff --git a/karavan-designer/src/designer/KaravanDesigner.tsx b/karavan-designer/src/designer/KaravanDesigner.tsx
index bc0c755..6eb2ff8 100644
--- a/karavan-designer/src/designer/KaravanDesigner.tsx
+++ b/karavan-designer/src/designer/KaravanDesigner.tsx
@@ -155,9 +155,9 @@ export class KaravanDesigner extends React.Component<Props, State> {
                 {tab === 'beans' && <BeansDesigner integration={this.state.integration}
                                                    onSave={(integration, propertyOnly) => this.save(integration, propertyOnly)}
                                                    dark={this.props.dark}/>}
-                {tab === 'dependencies' && <DependenciesDesigner integration={this.state.integration}
-                                                                 onSave={(integration, propertyOnly) => this.save(integration, propertyOnly)}
-                                                                 dark={this.props.dark}/>}
+                {/*{tab === 'dependencies' && <DependenciesDesigner integration={this.state.integration}*/}
+                {/*                                                 onSave={(integration, propertyOnly) => this.save(integration, propertyOnly)}*/}
+                {/*                                                 dark={this.props.dark}/>}*/}
                 {tab === 'error' && <ErrorDesigner integration={this.state.integration}
                                                    onSave={(integration, propertyOnly) => this.save(integration, propertyOnly)}
                                                    dark={this.props.dark}/>}