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>
- )
+ );
}