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 2023/07/02 15:46:49 UTC

[camel-karavan] branch main updated: Refactoring for #809

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 ed3b3b04 Refactoring for #809
ed3b3b04 is described below

commit ed3b3b04d81f73d33a39f7cb7906ad29277a11b5
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Sun Jul 2 11:46:39 2023 -0400

    Refactoring for #809
---
 karavan-app/pom.xml                                |   8 ++
 .../camel/karavan/api/KubernetesResource.java      |   1 +
 .../apache/camel/karavan/api/LogWatchResource.java |  22 ++--
 .../apache/camel/karavan/api/RunnerResource.java   |  18 +--
 .../org/apache/camel/karavan/api/SseResource.java  |  76 ++++++++++++
 .../camel/karavan/service/RunnerService.java       |   3 +-
 karavan-app/src/main/webui/package-lock.json       |   6 +
 karavan-app/src/main/webui/package.json            |   1 +
 karavan-app/src/main/webui/src/api/KaravanApi.tsx  |  44 ++++++-
 .../src/main/webui/src/api/ProjectEventBus.ts      |  19 +--
 .../src/main/webui/src/api/ProjectService.ts       |  62 +++++++--
 karavan-app/src/main/webui/src/api/ProjectStore.ts |  97 +++++++++++++--
 .../src/main/webui/src/project/CreateFileModal.tsx |   2 +-
 .../src/main/webui/src/project/ProjectLog.tsx      | 124 ------------------
 .../src/main/webui/src/project/ProjectPage.tsx     |  17 ++-
 .../src/main/webui/src/project/ProjectPanel.tsx    |   6 +-
 .../src/main/webui/src/project/ProjectToolbar.tsx  |  59 +--------
 .../src/main/webui/src/project/RunnerToolbar.tsx   |  83 +++----------
 .../webui/src/project/dashboard/DashboardTab.tsx   |  38 +++---
 .../webui/src/project/dashboard/RunnerInfoPod.tsx  |   7 +-
 .../src/main/webui/src/project/log/ProjectLog.tsx  |  28 +++++
 .../main/webui/src/project/log/ProjectLogPanel.tsx |  74 +++++++++++
 .../webui/src/project/pipeline/ProjectStatus.tsx   |  19 +--
 .../webui/src/project/trace/RunnerInfoTrace.tsx    | 138 ---------------------
 .../src/main/webui/src/project/trace/TraceTab.tsx  | 129 ++++++++++++++++---
 25 files changed, 576 insertions(+), 505 deletions(-)

diff --git a/karavan-app/pom.xml b/karavan-app/pom.xml
index 6f21699e..e4e50272 100644
--- a/karavan-app/pom.xml
+++ b/karavan-app/pom.xml
@@ -54,6 +54,14 @@
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-vertx</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-smallrye-reactive-messaging</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-scheduler</artifactId>
+        </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
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 a04c3cd6..1e7b601b 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
@@ -153,6 +153,7 @@ public class KubernetesResource {
     @Path("/pod/{projectId}/{env}")
     public List<PodStatus> getPodStatusesByProjectAndEnv(@PathParam("projectId") String projectId, @PathParam("env") String env) throws Exception {
         return infinispanService.getPodStatuses(projectId, env).stream()
+                .filter(podStatus -> !podStatus.getRunner())
                 .sorted(Comparator.comparing(PodStatus::getName))
                 .collect(Collectors.toList());
     }
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/LogWatchResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/LogWatchResource.java
index b0cade62..fd1183bd 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/api/LogWatchResource.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/LogWatchResource.java
@@ -17,9 +17,7 @@
 package org.apache.camel.karavan.api;
 
 import io.fabric8.kubernetes.client.dsl.LogWatch;
-import io.smallrye.mutiny.tuples.Tuple2;
 import org.apache.camel.karavan.service.KubernetesService;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.context.ManagedExecutor;
 import org.jboss.logging.Logger;
 
@@ -32,11 +30,15 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.sse.Sse;
 import javax.ws.rs.sse.SseEventSink;
+import javax.xml.crypto.Data;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.util.Date;
 import java.util.concurrent.ConcurrentHashMap;
 
+import static org.apache.camel.karavan.service.RunnerService.RUNNER_SUFFIX;
+
 @Path("/api/logwatch")
 public class LogWatchResource {
 
@@ -46,17 +48,13 @@ public class LogWatchResource {
     @Inject
     KubernetesService kubernetesService;
 
-    @ConfigProperty(name = "karavan.environment")
-    String environment;
-
     @Inject
     ManagedExecutor managedExecutor;
 
     @GET
     @Produces(MediaType.SERVER_SENT_EVENTS)
-    @Path("/{type}/{env}/{name}")
-    public void eventSourcing(@PathParam("env") String env,
-                              @PathParam("type") String type,
+    @Path("/{type}/{name}")
+    public void eventSourcing(@PathParam("type") String type,
                               @PathParam("name") String name,
                               @Context SseEventSink eventSink,
                               @Context Sse sse
@@ -64,6 +62,14 @@ public class LogWatchResource {
         managedExecutor.execute(() -> {
             LOGGER.info("LogWatch for " + name + " starting...");
             try (SseEventSink sink = eventSink) {
+//                    while (true) {
+//                        sink.send(sse.newEvent(new Date().toString()));
+//                        try {
+//                            Thread.sleep(2000);
+//                        } catch (InterruptedException e) {
+//                            throw new RuntimeException(e);
+//                        }
+//                    }
                 LogWatch logWatch = type.equals("container")
                         ? kubernetesService.getContainerLogWatch(name)
                         : kubernetesService.getPipelineRunLogWatch(name);
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/RunnerResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/RunnerResource.java
index 6e506794..52f985bc 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/api/RunnerResource.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/RunnerResource.java
@@ -79,19 +79,21 @@ public class RunnerResource {
     @DELETE
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/{name}/{deletePVC}")
-    public Response deleteRunner(@PathParam("name") String name, @PathParam("deletePVC") boolean deletePVC) {
-        kubernetesService.deleteRunner(name, deletePVC);
-        infinispanService.deleteRunnerStatuses(name);
+    @Path("/{projectId}/{deletePVC}")
+    public Response deleteRunner(@PathParam("projectId") String projectId, @PathParam("deletePVC") boolean deletePVC) {
+        String runnerName = projectId + "-" + RUNNER_SUFFIX;
+        kubernetesService.deleteRunner(runnerName, deletePVC);
+        infinispanService.deleteRunnerStatuses(runnerName);
         return Response.accepted().build();
     }
 
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("/pod/{projectId}/{name}")
-    public Response getPodStatus(@PathParam("projectId") String projectId, @PathParam("name") String name) {
+    @Path("/pod/{projectId}")
+    public Response getPodStatus(@PathParam("projectId") String projectId) {
+        String runnerName = projectId + "-" + RUNNER_SUFFIX;
         Optional<PodStatus> ps =  infinispanService.getPodStatuses(projectId, environment).stream()
-                .filter(podStatus -> podStatus.getName().equals(name))
+                .filter(podStatus -> podStatus.getName().equals(runnerName))
                 .findFirst();
         if (ps.isPresent()) {
             return Response.ok(ps.get()).build();
@@ -102,7 +104,7 @@ public class RunnerResource {
 
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("/console/{statusName}/{projectId}")
+    @Path("/console/{projectId}/{statusName}")
     public Response getCamelStatusByProjectAndEnv(@PathParam("projectId") String projectId, @PathParam("statusName") String statusName) {
         String name = projectId + "-" + RUNNER_SUFFIX;
         String status = infinispanService.getRunnerStatus(name, RunnerStatus.NAME.valueOf(statusName));
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/SseResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/SseResource.java
new file mode 100644
index 00000000..1a916862
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/SseResource.java
@@ -0,0 +1,76 @@
+/*
+ * 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.api;
+
+import io.quarkus.scheduler.Scheduled;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+import io.smallrye.mutiny.operators.multi.multicast.MultiConnectAfter;
+import io.vertx.core.json.JsonObject;
+import io.vertx.mutiny.core.eventbus.EventBus;
+import io.vertx.mutiny.core.eventbus.Message;
+import org.eclipse.microprofile.reactive.messaging.Channel;
+import org.eclipse.microprofile.reactive.messaging.Emitter;
+import org.eclipse.microprofile.reactive.messaging.OnOverflow;
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.reactive.RestStreamElementType;
+import org.reactivestreams.Publisher;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.sse.Sse;
+import javax.ws.rs.sse.SseEventSink;
+import java.util.Date;
+import java.util.Objects;
+
+@Path("/api/sse")
+public class SseResource {
+
+    private static final Logger LOGGER = Logger.getLogger(SseResource.class.getName());
+
+    @Inject
+    @Channel("log")
+    Publisher<JsonObject> publisher;
+
+    @Inject
+    @Channel("log")
+    @OnOverflow(value = OnOverflow.Strategy.BUFFER, bufferSize = 1000)
+    Emitter<JsonObject> emitter;
+
+//    @Scheduled(every="1s")
+//    void increment() {
+//        emitter.send(JsonObject.of("name", "marat1", "marat",  new Date()));
+//        emitter.send(JsonObject.of("name", "marat2", "marat",  new Date()));
+//    }
+
+    @GET
+    @Produces(MediaType.SERVER_SENT_EVENTS)
+    @RestStreamElementType("text/plain")
+    @Path("/{name}")
+    public Publisher<JsonObject> stream(@PathParam("name") String name,
+                                        @Context SseEventSink eventSink,
+                                        @Context Sse sse) {
+        System.out.println("----------");
+        System.out.println(name);
+        return Multi.createFrom().publisher(publisher).filter(e -> Objects.equals(e.getString("name"), name));
+    }
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/RunnerService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/RunnerService.java
index 22b31af8..0c5861f9 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/service/RunnerService.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/RunnerService.java
@@ -117,7 +117,6 @@ public class RunnerService {
         String oldContext = infinispanService.getRunnerStatus(podName, RunnerStatus.NAME.context);
         String newContext = getRunnerStatus(podName, RunnerStatus.NAME.context);
         if (newContext != null) {
-            System.out.println(new JsonObject(newContext).encodePrettily());
             infinispanService.saveRunnerStatus(podName, RunnerStatus.NAME.context, newContext);
             Arrays.stream(RunnerStatus.NAME.values())
                     .filter(name -> !name.equals(RunnerStatus.NAME.context))
@@ -128,6 +127,8 @@ public class RunnerService {
                         }
                     });
             reloadCode(podName, oldContext, newContext);
+        } else {
+            infinispanService.deleteRunnerStatuses(podName);
         }
     }
 
diff --git a/karavan-app/src/main/webui/package-lock.json b/karavan-app/src/main/webui/package-lock.json
index 2e4293f9..2c544d86 100644
--- a/karavan-app/src/main/webui/package-lock.json
+++ b/karavan-app/src/main/webui/package-lock.json
@@ -8,6 +8,7 @@
       "name": "karavan",
       "version": "3.20.2-SNAPSHOT",
       "dependencies": {
+        "@microsoft/fetch-event-source": "^2.0.1",
         "@monaco-editor/react": "4.5.0",
         "@patternfly/patternfly": "4.224.2",
         "@patternfly/react-charts": "6.94.19",
@@ -3187,6 +3188,11 @@
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
       "dev": true
     },
+    "node_modules/@microsoft/fetch-event-source": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
+      "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
+    },
     "node_modules/@monaco-editor/loader": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz",
diff --git a/karavan-app/src/main/webui/package.json b/karavan-app/src/main/webui/package.json
index f3d65b03..237e2dff 100644
--- a/karavan-app/src/main/webui/package.json
+++ b/karavan-app/src/main/webui/package.json
@@ -26,6 +26,7 @@
     ]
   },
   "dependencies": {
+    "@microsoft/fetch-event-source": "^2.0.1",
     "@monaco-editor/react": "4.5.0",
     "@patternfly/patternfly": "4.224.2",
     "@patternfly/react-charts": "6.94.19",
diff --git a/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-app/src/main/webui/src/api/KaravanApi.tsx
index 617b2e96..61e57cdd 100644
--- a/karavan-app/src/main/webui/src/api/KaravanApi.tsx
+++ b/karavan-app/src/main/webui/src/api/KaravanApi.tsx
@@ -10,6 +10,8 @@ import {
 } from "./ProjectModels";
 import {Buffer} from 'buffer';
 import {SsoApi} from "./SsoApi";
+import {EventStreamContentType, fetchEventSource} from "@microsoft/fetch-event-source";
+import {ProjectEventBus} from "./ProjectEventBus";
 
 axios.defaults.headers.common['Accept'] = 'application/json';
 axios.defaults.headers.common['Content-Type'] = 'application/json';
@@ -301,8 +303,8 @@ export class KaravanApi {
         });
     }
 
-    static async getRunnerPodStatus(projectId: string, name: string, after: (res: AxiosResponse<PodStatus>) => void) {
-        instance.get('/api/runner/pod/' + projectId + "/" + name)
+    static async getRunnerPodStatus(projectId: string, after: (res: AxiosResponse<PodStatus>) => void) {
+        instance.get('/api/runner/pod/' + projectId)
             .then(res => {
                 after(res);
             }).catch(err => {
@@ -320,7 +322,7 @@ export class KaravanApi {
     }
 
     static async getRunnerConsoleStatus(projectId: string, statusName: string, after: (res: AxiosResponse<string>) => void) {
-        instance.get('/api/runner/console/' + statusName + "/" + projectId)
+        instance.get('/api/runner/console/' + projectId + "/" + statusName)
             .then(res => {
                 after(res);
             }).catch(err => {
@@ -566,4 +568,38 @@ export class KaravanApi {
             after(err);
         });
     }
-}
\ No newline at end of file
+
+    static async fetchData(type: 'container' | 'pipeline' | 'none', podName: string, controller: AbortController) {
+        const fetchData = async () => {
+            await fetchEventSource("/api/logwatch/" + type + "/" + podName, {
+                method: "GET",
+                headers: {
+                    Accept: "text/event-stream",
+                },
+                signal: controller.signal,
+                async onopen(response) {
+                    if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
+                        return; // everything's good
+                    } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
+                        // client-side errors are usually non-retriable:
+                        console.log("Server side error ", response);
+                    } else {
+                        console.log("Error ", response);
+                    }
+                },
+                onmessage(event) {
+                    console.log(event);
+                    ProjectEventBus.sendLog('add', event.data)
+                },
+                onclose() {
+                    console.log("Connection closed by the server");
+                },
+                onerror(err) {
+                    console.log("There was an error from server", err);
+                },
+            });
+        };
+        return fetchData();
+    }
+}
+
diff --git a/karavan-app/src/main/webui/src/api/ProjectEventBus.ts b/karavan-app/src/main/webui/src/api/ProjectEventBus.ts
index 81e3a944..26190541 100644
--- a/karavan-app/src/main/webui/src/api/ProjectEventBus.ts
+++ b/karavan-app/src/main/webui/src/api/ProjectEventBus.ts
@@ -19,11 +19,8 @@ import {Project} from "./ProjectModels";
 
 const selectedProject = new BehaviorSubject<Project | undefined>(undefined);
 const currentFile = new BehaviorSubject<string | undefined>(undefined);
-const showLog = new BehaviorSubject<ShowLogCommand | undefined>(undefined);
-const refreshTrace = new BehaviorSubject<boolean>(false);
 const mode = new BehaviorSubject<"design" | "code">("design");
-const config = new BehaviorSubject<any>({});
-const showCreateProjectModal = new Subject<boolean>();
+const log = new Subject<["add" | "set", string]>();
 
 export class ShowLogCommand {
     type: 'container' | 'pipeline'
@@ -53,19 +50,9 @@ export const ProjectEventBus = {
     selectProject: (project: Project) => selectedProject.next(project),
     onSelectProject: () => selectedProject.asObservable(),
 
-    showLog: (type: 'container' | 'pipeline', name: string, environment: string, show: boolean = true) =>
-        showLog.next(new ShowLogCommand(type, name, environment, show)),
-    onShowLog: () => showLog.asObservable(),
-
-    refreshTrace: (refresh: boolean) => refreshTrace.next(refresh),
-    onRefreshTrace: () => refreshTrace.asObservable(),
-
     setMode: (m: 'design' | 'code') =>  mode.next(m),
     onSetMode: () => mode.asObservable(),
 
-    setConfig: (c: any) =>  config.next(c),
-    onSetConfig: () => config.asObservable(),
-
-    showCreateProjectModal: (show: boolean) => showCreateProjectModal.next(show),
-    onShowCreateProjectModal: () => showCreateProjectModal.asObservable(),
+    sendLog: (type: "add" | "set", m: string) =>  log.next([type, m]),
+    onLog: () => log.asObservable(),
 }
diff --git a/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-app/src/main/webui/src/api/ProjectService.ts
index 5f3aeccb..e286c59c 100644
--- a/karavan-app/src/main/webui/src/api/ProjectService.ts
+++ b/karavan-app/src/main/webui/src/api/ProjectService.ts
@@ -2,41 +2,78 @@ import {KaravanApi} from "./KaravanApi";
 import {DeploymentStatus, PodStatus, Project, ProjectFile} from "./ProjectModels";
 import {TemplateApi} from "karavan-core/lib/api/TemplateApi";
 import {KubernetesAPI} from "../designer/utils/KubernetesAPI";
-import { unstable_batchedUpdates } from 'react-dom'
+import {unstable_batchedUpdates} from 'react-dom'
+import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source';
 import {
     useAppConfigStore,
     useDeploymentStatusesStore,
     useFilesStore,
-    useFileStore,
+    useFileStore, useLogStore,
     useProjectsStore,
-    useProjectStore
+    useProjectStore, useRunnerStore
 } from "./ProjectStore";
 
 export class ProjectService {
 
-    public static runProject(project: Project) {
+    public static startRunner(project: Project) {
+        useRunnerStore.setState({status: "starting"})
         KaravanApi.runProject(project, res => {
             if (res.status === 200 || res.status === 201) {
+                useRunnerStore.setState({showLog: true})
             } else {
                 // Todo notification
             }
         });
     }
 
+    public static reloadRunner(project: Project) {
+        useRunnerStore.setState({status: "reloading"})
+        KaravanApi.getRunnerReload(project.projectId, res => {
+            if (res.status === 200 || res.status === 201) {
+                // setIsReloadingPod(false);
+            } else {
+                // Todo notification
+                // setIsReloadingPod(false);
+            }
+        });
+    }
+
+    public static deleteRunner(project: Project) {
+        useRunnerStore.setState({status: "deleting"})
+        KaravanApi.deleteRunner(project.projectId, false, res => {
+            if (res.status === 202) {
+                useRunnerStore.setState({showLog: false})
+            } else {
+                // Todo notification
+                // setIsDeletingPod(false);
+            }
+        });
+    }
+
     public static getRunnerPodStatus(project: Project) {
         const projectId = project.projectId;
-        const name = projectId + "-runner";
-        KaravanApi.getRunnerPodStatus(projectId, name, res => {
+        KaravanApi.getRunnerPodStatus(projectId, res => {
             if (res.status === 200) {
-                useProjectStore.setState({podStatus: res.data});
+                unstable_batchedUpdates(() => {
+                    const podStatus = res.data;
+                    if (useRunnerStore.getState().podName !== podStatus.name){
+                        useRunnerStore.setState({podName: podStatus.name})
+                    }
+                    if (useRunnerStore.getState().status !== "running"){
+                        useRunnerStore.setState({status: "running"})
+                    }
+                    useProjectStore.setState({podStatus: res.data});
+                })
             } else {
-                useProjectStore.setState({podStatus: new PodStatus()});
-                // Todo notification
+                unstable_batchedUpdates(() => {
+                    useRunnerStore.setState({status: "none", podName: undefined})
+                    useProjectStore.setState({podStatus: new PodStatus()});
+                })
             }
         });
     }
 
-    public static pushProject (project: Project, commitMessage: string) {
+    public static pushProject(project: Project, commitMessage: string) {
         useProjectStore.setState({isPushing: true})
         const params = {
             "projectId": project.projectId,
@@ -53,8 +90,7 @@ export class ProjectService {
         });
     }
 
-    public static saveFile (file: ProjectFile) {
-        console.log(file)
+    public static saveFile(file: ProjectFile) {
         KaravanApi.postProjectFile(file, res => {
             if (res.status === 200) {
                 const newFile = res.data;
@@ -69,7 +105,7 @@ export class ProjectService {
     }
 
     public static refreshProject(projectId: string) {
-        KaravanApi.getProject(projectId , (project: Project)=> {
+        KaravanApi.getProject(projectId, (project: Project) => {
             useProjectStore.setState({project: project});
             unstable_batchedUpdates(() => {
                 useProjectsStore.getState().upsertProject(project);
diff --git a/karavan-app/src/main/webui/src/api/ProjectStore.ts b/karavan-app/src/main/webui/src/api/ProjectStore.ts
index c323ddac..7644a359 100644
--- a/karavan-app/src/main/webui/src/api/ProjectStore.ts
+++ b/karavan-app/src/main/webui/src/api/ProjectStore.ts
@@ -17,6 +17,8 @@
 
 import {create} from 'zustand'
 import {AppConfig, DeploymentStatus, PodStatus, Project, ProjectFile} from "./ProjectModels";
+import {ProjectEventBus} from "./ProjectEventBus";
+import {unstable_batchedUpdates} from "react-dom";
 
 interface AppConfigState {
     config: AppConfig;
@@ -26,7 +28,7 @@ interface AppConfigState {
 export const useAppConfigStore = create<AppConfigState>((set) => ({
     config: new AppConfig(),
     setConfig: (config: AppConfig)  => {
-        set({config: config})
+        set({config: config}, true)
     },
 }))
 
@@ -48,7 +50,7 @@ export const useProjectsStore = create<ProjectsState>((set) => ({
         set((state: ProjectsState) => ({
             projects: state.projects.find(f => f.projectId === project.projectId) === undefined
                 ? [...state.projects, project]
-                : [...state.projects.filter(f => f.projectId !== project.projectId), project],
+                : [...state.projects.filter(f => f.projectId !== project.projectId), project]
         }), true);
     }
 }))
@@ -60,6 +62,7 @@ interface ProjectState {
     podStatus: PodStatus,
     operation: "create" | "select" | "delete" | "none" | "copy";
     setProject: (project: Project, operation:  "create" | "select" | "delete"| "none" | "copy") => void;
+    setOperation: (o: "create" | "select" | "delete"| "none" | "copy") => void;
 }
 
 export const useProjectStore = create<ProjectState>((set) => ({
@@ -68,10 +71,14 @@ export const useProjectStore = create<ProjectState>((set) => ({
     isPushing: false,
     isRunning: false,
     podStatus: new PodStatus(),
-    setProject: (p: Project, o: "create" | "select" | "delete"| "none" | "copy") => {
+    setProject: (p: Project) => {
         set((state: ProjectState) => ({
-            project: p,
-            operation: o,
+            project: p
+        }), true);
+    },
+    setOperation: (o: "create" | "select" | "delete"| "none" | "copy") => {
+        set((state: ProjectState) => ({
+            operation: o
         }), true);
     },
 }))
@@ -86,14 +93,14 @@ export const useFilesStore = create<FilesState>((set) => ({
     files: [],
     setFiles: (files: ProjectFile[]) => {
         set((state: FilesState) => ({
-            files: files,
+            files: files
         }), true);
     },
     upsertFile: (file: ProjectFile) => {
         set((state: FilesState) => ({
             files: state.files.find(f => f.name === file.name) === undefined
                 ? [...state.files, file]
-                : [...state.files.filter(f => f.name !== file.name), file],
+                : [...state.files.filter(f => f.name !== file.name), file]
         }), true);
     }
 }))
@@ -110,7 +117,7 @@ export const useFileStore = create<FileState>((set) => ({
     setFile: (file: ProjectFile, operation:  "create" | "select" | "delete"| "none" | "copy" | "upload") => {
         set((state: FileState) => ({
             file: file,
-            operation: operation,
+            operation: operation
         }), true);
     },
 }))
@@ -124,7 +131,77 @@ export const useDeploymentStatusesStore = create<DeploymentStatusesState>((set)
     statuses: [],
     setDeploymentStatuses: (statuses: DeploymentStatus[]) => {
         set((state: DeploymentStatusesState) => ({
-            statuses: statuses,
+            statuses: statuses
+        }), true);
+    },
+}))
+
+
+interface RunnerState {
+    podName?: string,
+    status: "none" | "starting" | "deleting"| "reloading" | "running",
+    setStatus: (status: "none" | "starting" | "deleting"| "reloading" | "running") => void,
+    type: 'container' | 'pipeline' | 'none',
+    setType: (type: 'container' | 'pipeline' | 'none') => void,
+    showLog: boolean,
+    setShowLog: (showLog: boolean) => void;
+}
+
+export const useRunnerStore = create<RunnerState>((set) => ({
+    podName: undefined,
+    status: "none",
+    setStatus: (status: "none" | "starting" | "deleting"| "reloading" | "running") =>  {
+        set((state: RunnerState) => ({
+            status: status,
         }), true);
     },
-}))
\ No newline at end of file
+    type: "none",
+    setType: (type: 'container' | 'pipeline' | 'none') =>  {
+        set((state: RunnerState) => ({type: type}), true);
+    },
+    showLog: false,
+    setShowLog: (showLog: boolean) => {
+        set(() => ({showLog: showLog}));
+    }
+}))
+
+interface LogState {
+    data: string;
+    setData: (data: string) => void;
+    addData: (data: string) => void;
+    addDataAsync: (data: string) => void;
+    currentLine: number;
+    setCurrentLine: (currentLine: number) => void;
+}
+
+export const useLogStore = create<LogState>((set) => ({
+    data: '',
+    setData: (data: string)  => {
+        set({data: data}, true)
+    },
+    addData: (data: string)  => {
+        set((state: LogState) => ({data: state.data.concat('\n').concat(data)}), true)
+    },
+    addDataAsync: async (data: string) => {
+        set((state: LogState) => ({data: state.data.concat('\n').concat(data)}), true)
+    },
+    currentLine: 0,
+    setCurrentLine: (currentLine: number)  => {
+        set((state: LogState) => ({currentLine: currentLine}), true)
+    }
+}))
+
+console.log("Start log subscriber");
+const sub = ProjectEventBus.onLog()?.subscribe((result: ["add" | "set", string]) => {
+    if (result[0] === 'add') {
+        unstable_batchedUpdates(() => {
+            useLogStore.setState((state: LogState) =>
+                ({data: state.data ? state.data.concat('\n').concat(result[1]) : result[1], currentLine: state.currentLine+1}), true)
+        })
+    }
+    else if (result[0] === 'set') {
+        unstable_batchedUpdates(() => {
+            useLogStore.setState({data: result[1], currentLine: 0});
+        })
+    }
+});
diff --git a/karavan-app/src/main/webui/src/project/CreateFileModal.tsx b/karavan-app/src/main/webui/src/project/CreateFileModal.tsx
index 79887096..c13ede3e 100644
--- a/karavan-app/src/main/webui/src/project/CreateFileModal.tsx
+++ b/karavan-app/src/main/webui/src/project/CreateFileModal.tsx
@@ -21,7 +21,7 @@ interface Props {
 
 export const CreateFileModal = (props: Props) => {
 
-    const {file, operation} = useFileStore();
+    const {operation} = useFileStore();
     const {project, setProject} = useProjectStore();
     const [name, setName] = useState<string>( '');
     const [fileType, setFileType] = useState<string>(props.types.at(0) || 'INTEGRATION');
diff --git a/karavan-app/src/main/webui/src/project/ProjectLog.tsx b/karavan-app/src/main/webui/src/project/ProjectLog.tsx
deleted file mode 100644
index ba674aaa..00000000
--- a/karavan-app/src/main/webui/src/project/ProjectLog.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import React from 'react';
-import {Button, Checkbox, Label, PageSection, Tooltip, TooltipPosition} from '@patternfly/react-core';
-import '../designer/karavan.css';
-import CloseIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
-import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
-import ScrollIcon from '@patternfly/react-icons/dist/esm/icons/scroll-icon';
-import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-icon';
-import CleanIcon from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon';
-import {LogViewer} from '@patternfly/react-log-viewer';
-import {Subscription} from "rxjs";
-import {ProjectEventBus, ShowLogCommand} from "../api/ProjectEventBus";
-
-const INITIAL_LOG_HEIGHT = "50%";
-
-interface Props {
-
-}
-
-interface State {
-    log?: ShowLogCommand,
-    showLog: boolean,
-    height?: number | string,
-    logViewerRef: any,
-    isTextWrapped: boolean
-    autoScroll: boolean
-    data: string,
-    currentLine: number
-}
-
-export class ProjectLog extends React.Component<Props, State> {
-
-    public state: State = {
-        showLog: false,
-        height: INITIAL_LOG_HEIGHT,
-        logViewerRef: React.createRef(),
-        isTextWrapped: true,
-        autoScroll: true,
-        data: '',
-        currentLine: 0
-    }
-    eventSource?: EventSource;
-    sub?: Subscription;
-
-    componentDidMount() {
-        this.eventSource?.close();
-        this.sub = ProjectEventBus.onShowLog()?.subscribe((log: ShowLogCommand | undefined) => {
-            if (log) {
-                this.setState({showLog: log.show, log: log, data: ''});
-                if (log.show) {
-                    this.showLogs(log.type, log.name, log.environment);
-                } else {
-                    this.eventSource?.close();
-                }
-            }
-        });
-    }
-
-    componentWillUnmount() {
-        this.eventSource?.close();
-        this.sub?.unsubscribe();
-    }
-
-    showLogs = (type: 'container' | 'pipeline', name: string, environment: string) => {
-        this.eventSource?.close();
-        this.eventSource = new EventSource("/api/logwatch/"+type+"/"+environment+"/"+name, { withCredentials: true });
-        this.eventSource.onerror = (event) => {
-            this.eventSource?.close();
-        }
-        this.eventSource.onmessage = (event) => {
-            this.setState((state: Readonly<State>) => {
-                const data = state.data.concat('\n').concat(event.data)
-                return {data: data, currentLine: this.state.currentLine + 1}
-            });
-            if (this.state.autoScroll) {
-                this.state.logViewerRef.current.scrollToBottom();
-            }
-        };
-    }
-
-    getButtons() {
-        const {height, isTextWrapped, logViewerRef, log, data, autoScroll} = this.state;
-        return (<div className="buttons">
-            <Label className="log-name">{log?.type + ": " + log?.name}</Label>
-            <Tooltip content={"Clean log"} position={TooltipPosition.bottom}>
-                <Button variant="plain" onClick={() => this.setState({data: ''})} icon={<CleanIcon/>}/>
-            </Tooltip>
-            <Checkbox label="Wrap text" aria-label="wrap text checkbox" isChecked={isTextWrapped} id="wrap-text-checkbox"
-                      onChange={checked => this.setState({isTextWrapped: checked})} />
-            <Checkbox label="Autoscroll" aria-label="autoscroll checkbox" isChecked={autoScroll} id="autoscroll-checkbox"
-                      onChange={checked => this.setState({autoScroll: checked})} />
-            <Tooltip content={"Scroll to bottom"} position={TooltipPosition.bottom}>
-                <Button variant="plain" onClick={() => logViewerRef.current.scrollToBottom()} icon={<ScrollIcon/>}/>
-            </Tooltip>
-            <Tooltip content={height === "100%" ? "Collapse": "Expand"} position={TooltipPosition.bottom}>
-                <Button variant="plain" onClick={() => {
-                    const h = height === "100%" ? INITIAL_LOG_HEIGHT : "100%";
-                    this.setState({height: h, showLog: true, data: data.concat(' ')});
-                }} icon={height === "100%" ? <CollapseIcon/> : <ExpandIcon/>}/>
-            </Tooltip>
-            <Button variant="plain" onClick={() => {
-                this.eventSource?.close();
-                this.setState({height: INITIAL_LOG_HEIGHT, showLog: false, data: '', currentLine: 0});
-            }} icon={<CloseIcon/>}/>
-        </div>);
-    }
-
-    render() {
-        const {showLog, height, logViewerRef, isTextWrapped, data, currentLine} = this.state;
-        return (showLog ?
-            <PageSection className="project-log" padding={{default: "noPadding"}} style={{height: height}}>
-                <LogViewer
-                    isTextWrapped={isTextWrapped}
-                    innerRef={logViewerRef}
-                    hasLineNumbers={false}
-                    loadingContent={"Loading..."}
-                    header={this.getButtons()}
-                    height={"100vh"}
-                    data={data}
-                    // scrollToRow={currentLine}
-                    theme={'dark'}/>
-            </PageSection>
-            : <></>);
-    }
-}
diff --git a/karavan-app/src/main/webui/src/project/ProjectPage.tsx b/karavan-app/src/main/webui/src/project/ProjectPage.tsx
index d23d86c1..1dd13b8e 100644
--- a/karavan-app/src/main/webui/src/project/ProjectPage.tsx
+++ b/karavan-app/src/main/webui/src/project/ProjectPage.tsx
@@ -6,7 +6,7 @@ import '../designer/karavan.css';
 import {KaravanApi} from "../api/KaravanApi";
 import FileSaver from "file-saver";
 import {ProjectToolbar} from "./ProjectToolbar";
-import {ProjectLog} from "./ProjectLog";
+import {ProjectLogPanel} from "./log/ProjectLogPanel";
 import {ProjectFile, ProjectFileTypes} from "../api/ProjectModels";
 import {useFileStore, useProjectStore} from "../api/ProjectStore";
 import {MainToolbar} from "../common/MainToolbar";
@@ -15,6 +15,8 @@ import {DeleteFileModal} from "./DeleteFileModal";
 import {ProjectTitle} from "./ProjectTitle";
 import {ProjectPanel} from "./ProjectPanel";
 import {FileEditor} from "./file/FileEditor";
+import {ProjectService} from "../api/ProjectService";
+import {shallow} from "zustand/shallow";
 
 export const ProjectPage = () => {
 
@@ -23,7 +25,16 @@ export const ProjectPage = () => {
     const {file, operation} = useFileStore();
     const [mode, setMode] = useState<"design" | "code">("design");
     const [key, setKey] = useState<string>('');
-    const {project} = useProjectStore();
+    const [project] = useProjectStore((state) => [state.project], shallow )
+
+    useEffect(() => {
+        const interval = setInterval(() => {
+            ProjectService.getRunnerPodStatus(project);
+        }, 1000);
+        return () => {
+            clearInterval(interval)
+        };
+    }, []);
 
     function post (file: ProjectFile)  {
         KaravanApi.postProjectFile(file, res => {
@@ -87,9 +98,9 @@ export const ProjectPage = () => {
             </PageSection>
             {file === undefined && operation !== 'select' && <ProjectPanel/>}
             {file !== undefined && operation === 'select' && <FileEditor/>}
-            <ProjectLog/>
             <CreateFileModal types={types}/>
             <DeleteFileModal />
+            <ProjectLogPanel/>
         </PageSection>
     )
 }
diff --git a/karavan-app/src/main/webui/src/project/ProjectPanel.tsx b/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
index fe21736d..ae163a41 100644
--- a/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
+++ b/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
@@ -5,17 +5,17 @@ import {
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {FilesTab} from "./files/FilesTab";
-import {useAppConfigStore, useProjectStore} from "../api/ProjectStore";
+import {useProjectStore} from "../api/ProjectStore";
 import {DashboardTab} from "./dashboard/DashboardTab";
 import {TraceTab} from "./trace/TraceTab";
 import {ProjectPipelineTab} from "./pipeline/ProjectPipelineTab";
 import {ProjectService} from "../api/ProjectService";
+import {shallow} from "zustand/shallow";
 
 export const ProjectPanel = () => {
 
     const [tab, setTab] = useState<string | number>('files');
-    const {project} = useProjectStore();
-    const {config} = useAppConfigStore();
+    const [project] = useProjectStore((state) => [state.project], shallow )
 
     useEffect(() => {
         onRefresh();
diff --git a/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx b/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
index 81368c14..de8a0f72 100644
--- a/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
+++ b/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
@@ -26,14 +26,13 @@ import DownloadImageIcon from "@patternfly/react-icons/dist/esm/icons/image-icon
 import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon";
 import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml";
 import PushIcon from "@patternfly/react-icons/dist/esm/icons/code-branch-icon";
-import {KaravanApi} from "../api/KaravanApi";
 import ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
 import {RunnerToolbar} from "./RunnerToolbar";
-import {Project, ProjectFile} from "../api/ProjectModels";
-import {ProjectEventBus} from "../api/ProjectEventBus";
-import {useAppConfigStore, useFilesStore, useFileStore, useProjectStore} from "../api/ProjectStore";
+import {ProjectFile} from "../api/ProjectModels";
+import {useFilesStore, useProjectStore, useRunnerStore} from "../api/ProjectStore";
 import {EventBus} from "../designer/utils/EventBus";
 import {ProjectService} from "../api/ProjectService";
+import {shallow} from "zustand/shallow";
 
 interface Props {
     file?: ProjectFile,
@@ -52,9 +51,8 @@ export const ProjectToolbar = (props: Props) => {
     const [isYaml, setIsYaml] = useState(false);
     const [isIntegration, setIsIntegration] = useState(false);
     const [isProperties, setIsProperties] = useState(false);
-    const {project, isPushing} = useProjectStore();
+    const [ project, isPushing] = useProjectStore((state) => [state.project, state.isPushing], shallow )
     const {files} = useFilesStore();
-    const {config} = useAppConfigStore();
 
     useEffect(() => {
         console.log("ProjectToolbar useEffect", isPushing, project.lastCommitTimestamp);
@@ -70,10 +68,6 @@ export const ProjectToolbar = (props: Props) => {
         setIsProperties(isProperties);
     });
 
-    function podName() {
-        return project.projectId + '-runner';
-    }
-
     function needCommit(): boolean {
         return project ? files.filter(f => f.lastUpdate > project.lastCommitTimestamp).length > 0 : false;
     }
@@ -128,49 +122,6 @@ export const ProjectToolbar = (props: Props) => {
         )
     }
 
-    function getTemplatesToolbar() {
-        const {file, editAdvancedProperties, setUploadModalOpen} = props;
-        return <Toolbar id="toolbar-group-types">
-            <ToolbarContent>
-                <ToolbarItem>
-                    <Flex className="toolbar" direction={{default: "row"}} justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
-                        {!isFile && <FlexItem>
-                            {getLastUpdatePanel()}
-                        </FlexItem>}
-                        {!isFile && <FlexItem>
-                            <Tooltip content="Commit and push to git" position={"bottom"}>
-                                <Button isLoading={isPushing ? true : undefined}
-                                        isSmall
-                                        variant={needCommit() ? "primary" : "secondary"}
-                                        className="project-button"
-                                        icon={!isPushing ? <PushIcon/> : <div></div>}
-                                        onClick={() => setCommitMessageIsOpen(true)}>
-                                    {isPushing ? "..." : "Commit"}
-                                </Button>
-                            </Tooltip>
-                        </FlexItem>}
-                        {isProperties && <FlexItem>
-                            <Checkbox
-                                id="advanced"
-                                label="Edit advanced"
-                                isChecked={editAdvancedProperties}
-                                onChange={checked => props.setEditAdvancedProperties(checked)}
-                            />
-                        </FlexItem>}
-
-                        {!isFile && <FlexItem>
-                            <Button isSmall variant={"secondary"} icon={<PlusIcon/>}
-                                    onClick={e => ProjectEventBus.showCreateProjectModal(true)}>Create</Button>
-                        </FlexItem>}
-                        {!isFile && <FlexItem>
-                            <Button isSmall variant="secondary" icon={<UploadIcon/>}
-                                    onClick={e => setUploadModalOpen()}>Upload</Button>
-                        </FlexItem>}
-                    </Flex>
-                </ToolbarItem>
-            </ToolbarContent>
-        </Toolbar>
-    }
 
     function getFileToolbar() {
         const {file, mode, editAdvancedProperties,
@@ -236,6 +187,7 @@ export const ProjectToolbar = (props: Props) => {
             <ToolbarContent>
                 <Flex className="toolbar" direction={{default: "row"}} alignItems={{default: "alignItemsCenter"}}>
                     <FlexItem>{getLastUpdatePanel()}</FlexItem>
+                    {isRunnable() && <RunnerToolbar/>}
                     <FlexItem>
                         <Tooltip content="Commit and push to git" position={"bottom-end"}>
                             <Button isLoading={isPushing ? true : undefined}
@@ -251,7 +203,6 @@ export const ProjectToolbar = (props: Props) => {
                             </Button>
                         </Tooltip>
                     </FlexItem>
-                    {isRunnable() && <RunnerToolbar/>}
                 </Flex>
             </ToolbarContent>
         </Toolbar>)
diff --git a/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx b/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx
index 8fc52840..92a99e95 100644
--- a/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx
+++ b/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useState} from 'react';
+import React from 'react';
 import {
     Button, FlexItem,
     Tooltip,
@@ -8,99 +8,52 @@ import '../designer/karavan.css';
 import RocketIcon from "@patternfly/react-icons/dist/esm/icons/rocket-icon";
 import ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
 import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon";
-import {KaravanApi} from "../api/KaravanApi";
-import {useAppConfigStore, useProjectStore} from "../api/ProjectStore";
+import { useProjectStore, useRunnerStore} from "../api/ProjectStore";
 import {ProjectService} from "../api/ProjectService";
-
+import {shallow} from "zustand/shallow";
 
 export const RunnerToolbar = () => {
 
-    const [isStartingPod, setIsStartingPod] = useState(false);
-    const [isDeletingPod, setIsDeletingPod] = useState(false);
-    const [isReloadingPod, setIsReloadingPod] = useState(false);
-    const {config} = useAppConfigStore();
-    const {project, podStatus} = useProjectStore();
-
-    function isRunning() {
-        return podStatus.started;
-    }
-
-    useEffect(() => {
-        console.log("Runner toolbar", podStatus);
-        const interval = setInterval(() => {
-            if (isRunning()) {
-                ProjectService.getRunnerPodStatus(project);
-                if (isStartingPod) setIsStartingPod(false);
-            }
-        }, 1000);
-        return () => {
-            clearInterval(interval)
-        };
-
-    }, []);
-
-    function jbangRun() {
-        setIsStartingPod(true);
-        ProjectService.runProject(project);
-    }
-
-    function reloadRunner() {
-        setIsReloadingPod(true);
-        KaravanApi.getRunnerReload(project.projectId, res => {
-            if (res.status === 200 || res.status === 201) {
-                setIsReloadingPod(false);
-            } else {
-                // Todo notification
-                setIsReloadingPod(false);
-            }
-        });
-    }
-
-    function deleteRunner() {
-        setIsDeletingPod(true);
-        KaravanApi.deleteRunner(project.projectId + "-runner", false, res => {
-            if (res.status === 202) {
-                setIsDeletingPod(false);
-            } else {
-                // Todo notification
-                setIsDeletingPod(false);
-            }
-        });
-    }
+    const [ status] = useRunnerStore((state) => [state.status], shallow )
+    const [ project] = useProjectStore((state) => [state.project], shallow )
 
+    const isRunning = status === "running";
+    const isStartingPod = status === "starting";
+    const isReloadingPod = status === "reloading";
+    const isDeletingPod = status === "deleting";
     return (<>
-        {!isRunning() && <FlexItem>
-            <Tooltip content="Run in development mode" position={TooltipPosition.bottomEnd}>
+        {!isRunning && !isReloadingPod && <FlexItem>
+            <Tooltip content="Run in development mode" position={TooltipPosition.bottom}>
                 <Button isLoading={isStartingPod ? true : undefined}
                         isSmall
                         variant={"primary"}
                         className="project-button"
                         icon={!isStartingPod ? <RocketIcon/> : <div></div>}
-                        onClick={() => jbangRun()}>
+                        onClick={() => ProjectService.startRunner(project)}>
                     {isStartingPod ? "..." : "Run"}
                 </Button>
             </Tooltip>
         </FlexItem>}
-        {isRunning() && <FlexItem>
-            <Tooltip content="Reload" position={TooltipPosition.bottomEnd}>
+        {(isRunning || isReloadingPod) && <FlexItem>
+            <Tooltip content="Reload" position={TooltipPosition.bottom}>
                 <Button isLoading={isReloadingPod ? true : undefined}
                         isSmall
                         variant={"primary"}
                         className="project-button"
                         icon={!isReloadingPod ? <ReloadIcon/> : <div></div>}
-                        onClick={() => reloadRunner()}>
+                        onClick={() => ProjectService.reloadRunner(project)}>
                     {isReloadingPod ? "..." : "Reload"}
                 </Button>
             </Tooltip>
         </FlexItem>}
-        {isRunning() && <FlexItem>
-        <Tooltip content="Stop runner" position={TooltipPosition.bottomEnd}>
+        {(isRunning || isDeletingPod) && !isReloadingPod && <FlexItem>
+        <Tooltip content="Stop runner" position={TooltipPosition.bottom}>
             <Button isLoading={isDeletingPod ? true : undefined}
                     isSmall
                     variant={"secondary"}
                     className="project-button"
                     icon={!isRunning ? <DeleteIcon/> : <div></div>}
-                    onClick={() => deleteRunner()}>
+                    onClick={() => ProjectService.deleteRunner(project)}>
                 {isDeletingPod ? "..." : "Stop"}
             </Button>
         </Tooltip>
diff --git a/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx b/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
index 07bb5541..f43c21af 100644
--- a/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
+++ b/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
@@ -1,3 +1,19 @@
+/*
+ * 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.
+ */
 import React, {useEffect, useRef, useState} from 'react';
 import {
     Card,
@@ -9,8 +25,7 @@ import {RunnerInfoContext} from "./RunnerInfoContext";
 import {RunnerInfoMemory} from "./RunnerInfoMemory";
 import {KaravanApi} from "../../api/KaravanApi";
 import {PodStatus} from "../../api/ProjectModels";
-import {useAppConfigStore, useProjectStore} from "../../api/ProjectStore";
-import {ProjectEventBus} from "../../api/ProjectEventBus";
+import {useAppConfigStore, useProjectStore, useRunnerStore} from "../../api/ProjectStore";
 
 export function isRunning(status: PodStatus): boolean {
     return status.phase === 'Running' && !status.terminating;
@@ -18,16 +33,13 @@ export function isRunning(status: PodStatus): boolean {
 
 export const DashboardTab = () => {
 
-    const {project} = useProjectStore();
-    const [podStatus, setPodStatus] = useState(new PodStatus());
-    const previousValue = useRef(new PodStatus());
+    const {project, podStatus} = useProjectStore();
     const [memory, setMemory] = useState({});
     const [jvm, setJvm] = useState({});
     const [context, setContext] = useState({});
     const {config} = useAppConfigStore();
 
     useEffect(() => {
-        previousValue.current = podStatus;
         const interval = setInterval(() => {
             onRefreshStatus();
         }, 1000);
@@ -39,18 +51,6 @@ export const DashboardTab = () => {
 
     function onRefreshStatus() {
         const projectId = project.projectId;
-        const name = projectId + "-runner";
-        KaravanApi.getRunnerPodStatus(projectId, name, res => {
-            if (res.status === 200) {
-                setPodStatus(res.data);
-                if (isRunning(res.data) && !isRunning(previousValue.current)) {
-                    ProjectEventBus.showLog('container', res.data.name, config.environment);
-                }
-            } else {
-                ProjectEventBus.showLog('container', name, config.environment, false);
-                setPodStatus(new PodStatus({name: name}));
-            }
-        });
         KaravanApi.getRunnerConsoleStatus(projectId, "memory", res => {
             if (res.status === 200) {
                 setMemory(res.data);
@@ -85,7 +85,7 @@ export const DashboardTab = () => {
                     <Flex direction={{default: "row"}}
                           justifyContent={{default: "justifyContentSpaceBetween"}}>
                         <FlexItem flex={{default: "flex_1"}}>
-                            <RunnerInfoPod podStatus={podStatus} config={config}/>
+                            <RunnerInfoPod podStatus={podStatus}/>
                         </FlexItem>
                         <Divider orientation={{default: "vertical"}}/>
                         <FlexItem flex={{default: "flex_1"}}>
diff --git a/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoPod.tsx b/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoPod.tsx
index c55ae090..5b819bfa 100644
--- a/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoPod.tsx
+++ b/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoPod.tsx
@@ -12,7 +12,7 @@ import '../../designer/karavan.css';
 import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
 import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
 import {PodStatus} from "../../api/ProjectModels";
-import {ProjectEventBus} from "../../api/ProjectEventBus";
+import {useRunnerStore} from "../../api/ProjectStore";
 
 
 export function isRunning(status: PodStatus): boolean {
@@ -21,19 +21,18 @@ export function isRunning(status: PodStatus): boolean {
 
 interface Props {
     podStatus: PodStatus,
-    config: any,
 }
 
 export const RunnerInfoPod = (props: Props) => {
 
     function getPodInfo() {
-        const env = props.config.environment;
         const podStatus = props.podStatus;
         return (
             <Label icon={getIcon()} color={getColor()}>
                 <Tooltip content={"Show log"}>
                     <Button variant="link"
-                            onClick={e => ProjectEventBus.showLog('container', podStatus.name, env)}>
+                            onClick={e =>
+                                useRunnerStore.setState({showLog: true, type: 'container', podName: podStatus.name})}>
                         {podStatus.name}
                     </Button>
                 </Tooltip>
diff --git a/karavan-app/src/main/webui/src/project/log/ProjectLog.tsx b/karavan-app/src/main/webui/src/project/log/ProjectLog.tsx
new file mode 100644
index 00000000..61d7280d
--- /dev/null
+++ b/karavan-app/src/main/webui/src/project/log/ProjectLog.tsx
@@ -0,0 +1,28 @@
+import React, {useState} from 'react';
+import '../../designer/karavan.css';
+import {LogViewer} from '@patternfly/react-log-viewer';
+import {useLogStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow"
+
+interface Props {
+    autoScroll: boolean
+    isTextWrapped: boolean
+    header?: React.ReactNode
+}
+
+export const ProjectLog = (props: Props) => {
+
+    const [data, currentLine] = useLogStore((state) => [state.data, state.currentLine], shallow );
+    const [logViewerRef] = useState(React.createRef());
+
+    return (<LogViewer
+                isTextWrapped={props.isTextWrapped}
+                innerRef={logViewerRef}
+                hasLineNumbers={false}
+                loadingContent={"Loading..."}
+                header={props.header}
+                height={"100vh"}
+                data={data}
+                scrollToRow={props.autoScroll ? currentLine : undefined}
+                theme={'dark'}/>);
+}
diff --git a/karavan-app/src/main/webui/src/project/log/ProjectLogPanel.tsx b/karavan-app/src/main/webui/src/project/log/ProjectLogPanel.tsx
new file mode 100644
index 00000000..f17f64ce
--- /dev/null
+++ b/karavan-app/src/main/webui/src/project/log/ProjectLogPanel.tsx
@@ -0,0 +1,74 @@
+import React, {useEffect, useState} from 'react';
+import {Button, Checkbox, Label, PageSection, Tooltip, TooltipPosition} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import CloseIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
+import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-icon';
+import CleanIcon from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon';
+import {useRunnerStore} from "../../api/ProjectStore";
+import {KaravanApi} from "../../api/KaravanApi";
+import {shallow} from "zustand/shallow";
+import {ProjectEventBus} from "../../api/ProjectEventBus";
+import {ProjectLog} from "./ProjectLog";
+
+const INITIAL_LOG_HEIGHT = "50%";
+
+export const ProjectLogPanel = () => {
+    const [showLog, type, setShowLog, podName, status] = useRunnerStore(
+        (state) => [state.showLog, state.type, state.setShowLog, state.podName, state.status], shallow)
+
+    const [height, setHeight] = useState(INITIAL_LOG_HEIGHT);
+    const [isTextWrapped, setIsTextWrapped] = useState(true);
+    const [autoScroll, setAutoScroll] = useState(true);
+    const [fetch, setFetch] = useState<Promise<void> | undefined>(undefined);
+
+    useEffect(() => {
+        console.log("ProjectLogPanel", showLog, type, podName, status);
+        const controller = new AbortController();
+        if (showLog && type !== 'none' && podName !== undefined) {
+            const f = KaravanApi.fetchData(type, podName, controller).then(value => {
+                console.log("Fetch Started for: " + podName)
+            });
+            console.log("new fetch")
+            setFetch(f);
+        }
+        return () => {
+            console.log("end");
+            controller.abort();
+        };
+    }, [showLog, type, podName, status]);
+
+    function getButtons() {
+        return (<div className="buttons">
+            <Label className="log-name">{podName!== undefined ? (type + ": " + podName + " " + status) : ''}</Label>
+            <Tooltip content={"Clean log"} position={TooltipPosition.bottom}>
+                <Button variant="plain" onClick={() => ProjectEventBus.sendLog('set', '')} icon={<CleanIcon/>}/>
+            </Tooltip>
+            <Checkbox label="Wrap text" aria-label="wrap text checkbox" isChecked={isTextWrapped} id="wrap-text-checkbox"
+                      onChange={checked => setIsTextWrapped(checked)} />
+            <Checkbox label="Autoscroll" aria-label="autoscroll checkbox" isChecked={autoScroll} id="autoscroll-checkbox"
+                      onChange={checked => setAutoScroll(checked)} />
+            {/*<Tooltip content={"Scroll to bottom"} position={TooltipPosition.bottom}>*/}
+            {/*    <Button variant="plain" onClick={() => } icon={<ScrollIcon/>}/>*/}
+            {/*</Tooltip>*/}
+            <Tooltip content={height === "100%" ? "Collapse": "Expand"} position={TooltipPosition.bottom}>
+                <Button variant="plain" onClick={() => {
+                    const h = height === "100%" ? INITIAL_LOG_HEIGHT : "100%";
+                    setHeight(h);
+                    ProjectEventBus.sendLog('add', ' ')
+                }} icon={height === "100%" ? <CollapseIcon/> : <ExpandIcon/>}/>
+            </Tooltip>
+            <Button variant="plain" onClick={() => {
+                setShowLog(false);
+                setHeight(INITIAL_LOG_HEIGHT);
+                ProjectEventBus.sendLog('set', '')
+            }} icon={<CloseIcon/>}/>
+        </div>);
+    }
+
+    return (showLog ?
+        <PageSection className="project-log" padding={{default: "noPadding"}} style={{height: height}}>
+            <ProjectLog autoScroll={autoScroll} isTextWrapped={isTextWrapped} header={getButtons()}/>
+        </PageSection>
+        : <></>);
+}
diff --git a/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx b/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
index 27c898dc..18d72f8e 100644
--- a/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
+++ b/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
@@ -15,7 +15,7 @@ import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon
 import ClockIcon from "@patternfly/react-icons/dist/esm/icons/clock-icon";
 import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon";
 import {CamelStatus, DeploymentStatus, PipelineStatus, PodStatus, Project} from "../../api/ProjectModels";
-import {ProjectEventBus} from "../../api/ProjectEventBus";
+import {useRunnerStore} from "../../api/ProjectStore";
 
 interface Props {
     project: Project,
@@ -107,7 +107,7 @@ export class ProjectStatus extends React.Component<Props, State> {
         KaravanApi.pipelineRun(this.props.project, this.props.env, res => {
             if (res.status === 200 || res.status === 201) {
                 this.setState({isBuilding: false});
-                ProjectEventBus.showLog('pipeline', res.data, this.props.env)
+                useRunnerStore.setState({showLog: true, type: 'pipeline', podName:  res.data})
             } else {
                 // Todo notification
             }
@@ -170,16 +170,6 @@ export class ProjectStatus extends React.Component<Props, State> {
         </Tooltip>)
     }
 
-    getDate(lastUpdate: number): string {
-        if (lastUpdate) {
-            const date = new Date(lastUpdate);
-            return date.toDateString() + ' ' + date.toLocaleTimeString();
-        } else {
-            return "N/A"
-        }
-    }
-
-
     getReplicasPanel(env: string, deploymentStatus?: DeploymentStatus) {
         const ok = (deploymentStatus && deploymentStatus?.readyReplicas > 0
             && (deploymentStatus.unavailableReplicas === 0 || deploymentStatus.unavailableReplicas === undefined || deploymentStatus.unavailableReplicas === null)
@@ -218,7 +208,8 @@ export class ProjectStatus extends React.Component<Props, State> {
                                     <Tooltip key={pod.name} content={running ? "Running" : pod.phase}>
                                         <Label icon={running ? <UpIcon/> : <DownIcon/>} color={running ? "green" : "red"}>
                                             <Button variant="link"
-                                                    onClick={e => ProjectEventBus.showLog('container', pod.name, env)}>
+                                                    onClick={e =>
+                                                        useRunnerStore.setState({showLog: true, type: 'container', podName:  pod.name})}>
                                                 {pod.name}
                                             </Button>
                                             <Tooltip content={"Delete Pod"}>
@@ -253,7 +244,7 @@ export class ProjectStatus extends React.Component<Props, State> {
 
     showPipelineLog(pipeline: string, env: string) {
         if (pipeline) {
-            ProjectEventBus.showLog('pipeline', pipeline, env);
+            useRunnerStore.setState({showLog: true, type: 'pipeline', podName:  pipeline})
         }
     }
 
diff --git a/karavan-app/src/main/webui/src/project/trace/RunnerInfoTrace.tsx b/karavan-app/src/main/webui/src/project/trace/RunnerInfoTrace.tsx
deleted file mode 100644
index cf2a37ce..00000000
--- a/karavan-app/src/main/webui/src/project/trace/RunnerInfoTrace.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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.
- */
-import React, {useState} from 'react';
-import {
-    Bullseye,
-    Button,
-    EmptyState,
-    EmptyStateIcon,
-    EmptyStateVariant, Flex, FlexItem,
-    Panel,
-    PanelHeader,
-    Text,
-    Switch, TextContent, TextVariants, Title,
-} from '@patternfly/react-core';
-import '../../designer/karavan.css';
-import {ProjectEventBus} from "../../api/ProjectEventBus";
-import {RunnerInfoTraceModal} from "./RunnerInfoTraceModal";
-import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
-import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon";
-
-
-interface Props {
-    trace: any
-    refreshTrace: boolean
-}
-
-export const RunnerInfoTrace = (props: Props) => {
-
-    const [trace, setTrace] = useState({});
-    const [nodes, setNodes] = useState([{}]);
-    const [isOpen, setIsOpen] = useState(false);
-
-    function closeModal() {
-        setIsOpen(false);
-    }
-
-    function getNodes(exchangeId: string): any[] {
-        const traces: any[] = props.trace?.trace?.traces || [];
-        return traces
-            .filter(t => t.message?.exchangeId === exchangeId)
-            .sort((a, b) => a.uid > b.uid ? 1 : -1);
-    }
-
-    function getNode(exchangeId: string): any {
-        const traces: any[] = props.trace?.trace?.traces || [];
-        return traces
-            .filter(t => t.message?.exchangeId === exchangeId)
-            .sort((a, b) => a.uid > b.uid ? 1 : -1)
-            .at(0);
-    }
-
-    const traces: any[] = (props.trace?.trace?.traces || []).sort((a: any, b: any) => b.uid > a.uid ? 1 : -1);
-    const exchanges: any[] = Array.from(new Set((traces).map((item: any) => item?.message?.exchangeId)));
-    return (
-        <div>
-            {isOpen && <RunnerInfoTraceModal isOpen={isOpen} trace={trace} nodes={nodes} onClose={closeModal}/>}
-            <Panel>
-                <PanelHeader>
-                    <Flex direction={{default: "row"}} justifyContent={{default:"justifyContentFlexEnd"}}>
-                        <FlexItem>
-                            <TextContent>
-                                <Text component={TextVariants.h6}>Auto refresh</Text>
-                            </TextContent>
-                        </FlexItem>
-                        <FlexItem>
-                            <Switch aria-label="refresh"
-                                    id="refresh"
-                                    isChecked={props.refreshTrace}
-                                    onChange={checked => ProjectEventBus.refreshTrace(checked)}
-                            />
-                        </FlexItem>
-                    </Flex>
-                </PanelHeader>
-            </Panel>
-            <TableComposable aria-label="Files" variant={"compact"} className={"table"}>
-                <Thead>
-                    <Tr>
-                        <Th key='uid' width={30}>Type</Th>
-                        <Th key='exchangeId' width={40}>Filename</Th>
-                        <Th key='timestamp' width={30}>Updated</Th>
-                    </Tr>
-                </Thead>
-                <Tbody>
-                    {exchanges.map(exchangeId => {
-                        const node = getNode(exchangeId);
-                        return <Tr key={node?.uid}>
-                            <Td>
-                                {node?.uid}
-                            </Td>
-                            <Td>
-                                <Button style={{padding: '0'}} variant={"link"}
-                                        onClick={e => {
-                                            setTrace(trace);
-                                            setNodes(getNodes(exchangeId));
-                                            setIsOpen(true);
-                                        }}>
-                                    {exchangeId}
-                                </Button>
-                            </Td>
-                            <Td>
-                                {node ? new Date(node?.timestamp).toISOString() : ""}
-                            </Td>
-
-                        </Tr>
-                    })}
-                    {exchanges.length === 0 &&
-                        <Tr>
-                            <Td colSpan={8}>
-                                <Bullseye>
-                                    <EmptyState variant={EmptyStateVariant.small}>
-                                        <EmptyStateIcon icon={SearchIcon}/>
-                                        <Title headingLevel="h2" size="lg">
-                                            No results found
-                                        </Title>
-                                    </EmptyState>
-                                </Bullseye>
-                            </Td>
-                        </Tr>
-                    }
-                </Tbody>
-            </TableComposable>
-        </div>
-    );
-}
diff --git a/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx b/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx
index 1c108f8b..b6e17dbc 100644
--- a/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx
+++ b/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx
@@ -15,43 +15,46 @@
  * limitations under the License.
  */
 import React, {useEffect, useState} from 'react';
-import { PageSection
+import {
+    Bullseye,
+    Button,
+    EmptyState,
+    EmptyStateIcon,
+    EmptyStateVariant, Flex, FlexItem,
+    Panel,
+    PanelHeader,
+    Text,
+    Switch, TextContent, TextVariants, Title, PageSection,
 } from '@patternfly/react-core';
 import '../../designer/karavan.css';
-import {PodStatus} from "../../api/ProjectModels";
+import {RunnerInfoTraceModal} from "./RunnerInfoTraceModal";
+import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon";
 import {KaravanApi} from "../../api/KaravanApi";
-import {ProjectEventBus} from "../../api/ProjectEventBus";
-import {RunnerInfoTrace} from "./RunnerInfoTrace";
-import {useAppConfigStore, useProjectStore} from "../../api/ProjectStore";
+import {useProjectStore} from "../../api/ProjectStore";
 
-export function isRunning(status: PodStatus): boolean {
-    return status.phase === 'Running' && !status.terminating;
-}
 
 export const TraceTab = () => {
 
-    const {project, setProject} = useProjectStore();
-    const [trace, setTrace] = useState({});
-    const [refreshTrace, setRefreshTrace] = useState(true);
-    const {config} = useAppConfigStore();
+    const {project} = useProjectStore();
+    const [trace, setTrace] = useState<any>({});
+    const [nodes, setNodes] = useState([{}]);
+    const [isOpen, setIsOpen] = useState(false);
+    const [refreshTrace, setRefreshTrace] = useState(false);
 
     useEffect(() => {
-        const sub2 = ProjectEventBus.onRefreshTrace()?.subscribe((result: boolean) => {
-            setRefreshTrace(result);
-        });
+        console.log("TraceTab useEffect", refreshTrace, project)
         const interval = setInterval(() => {
             onRefreshStatus();
         }, 1000);
         return () => {
-            sub2.unsubscribe();
             clearInterval(interval)
         };
-
     }, []);
 
+
     function onRefreshStatus() {
         const projectId = project.projectId;
-        const name = projectId + "-runner";
         if (refreshTrace) {
             KaravanApi.getRunnerConsoleStatus(projectId, "trace", res => {
                 if (res.status === 200) {
@@ -63,9 +66,95 @@ export const TraceTab = () => {
         }
     }
 
+    function closeModal() {
+        setIsOpen(false);
+    }
+
+    function getNodes(exchangeId: string): any[] {
+        const traces: any[] = trace?.trace?.traces || [];
+        return traces
+            .filter(t => t.message?.exchangeId === exchangeId)
+            .sort((a, b) => a.uid > b.uid ? 1 : -1);
+    }
+
+    function getNode(exchangeId: string): any {
+        const traces: any[] = trace?.trace?.traces || [];
+        return traces
+            .filter(t => t.message?.exchangeId === exchangeId)
+            .sort((a, b) => a.uid > b.uid ? 1 : -1)
+            .at(0);
+    }
+
+    const traces: any[] = (trace?.trace?.traces || []).sort((a: any, b: any) => b.uid > a.uid ? 1 : -1);
+    const exchanges: any[] = Array.from(new Set((traces).map((item: any) => item?.message?.exchangeId)));
     return (
         <PageSection className="project-tab-panel" padding={{default: "padding"}}>
-            <RunnerInfoTrace trace={trace} refreshTrace={refreshTrace}/>
+            {isOpen && <RunnerInfoTraceModal isOpen={isOpen} trace={trace} nodes={nodes} onClose={closeModal}/>}
+            <Panel>
+                <PanelHeader>
+                    <Flex direction={{default: "row"}} justifyContent={{default:"justifyContentFlexEnd"}}>
+                        <FlexItem>
+                            <TextContent>
+                                <Text component={TextVariants.h6}>Auto refresh</Text>
+                            </TextContent>
+                        </FlexItem>
+                        <FlexItem>
+                            <Switch aria-label="refresh"
+                                    id="refresh"
+                                    isChecked={refreshTrace}
+                                    onChange={checked => setRefreshTrace(checked)}
+                            />
+                        </FlexItem>
+                    </Flex>
+                </PanelHeader>
+            </Panel>
+            <TableComposable aria-label="Files" variant={"compact"} className={"table"}>
+                <Thead>
+                    <Tr>
+                        <Th key='uid' width={30}>Type</Th>
+                        <Th key='exchangeId' width={40}>Filename</Th>
+                        <Th key='timestamp' width={30}>Updated</Th>
+                    </Tr>
+                </Thead>
+                <Tbody>
+                    {exchanges.map(exchangeId => {
+                        const node = getNode(exchangeId);
+                        return <Tr key={node?.uid}>
+                            <Td>
+                                {node?.uid}
+                            </Td>
+                            <Td>
+                                <Button style={{padding: '0'}} variant={"link"}
+                                        onClick={e => {
+                                            setTrace(trace);
+                                            setNodes(getNodes(exchangeId));
+                                            setIsOpen(true);
+                                        }}>
+                                    {exchangeId}
+                                </Button>
+                            </Td>
+                            <Td>
+                                {node ? new Date(node?.timestamp).toISOString() : ""}
+                            </Td>
+
+                        </Tr>
+                    })}
+                    {exchanges.length === 0 &&
+                        <Tr>
+                            <Td colSpan={8}>
+                                <Bullseye>
+                                    <EmptyState variant={EmptyStateVariant.small}>
+                                        <EmptyStateIcon icon={SearchIcon}/>
+                                        <Title headingLevel="h2" size="lg">
+                                            No results found
+                                        </Title>
+                                    </EmptyState>
+                                </Bullseye>
+                            </Td>
+                        </Tr>
+                    }
+                </Tbody>
+            </TableComposable>
         </PageSection>
-    )
+    );
 }