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