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 f8463a1 Project Builder UI (#333)
f8463a1 is described below
commit f8463a1c1415dbb769478e12a06ce35f3032b0e9
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Thu May 5 12:11:46 2022 -0400
Project Builder UI (#333)
* Project Builder UI
* Builder dark mode vscode
* Properties to project model
* Properties edited
* COnfig
* Package from UI
* First UI that works
* Jbang UI works with Openshift
---
karavan-core/src/core/api/ProjectModelApi.ts | 151 ++++++++++
karavan-core/src/core/model/ProjectModel.ts | 60 ++++
karavan-core/test/application.properties | 26 ++
karavan-core/test/application.spec.ts | 45 +++
karavan-demo/builder/application.properties | 32 ++
karavan-demo/builder/builder-demo.yaml | 9 +
karavan-designer/src/App.tsx | 54 ++--
karavan-designer/src/builder/BuilderPage.tsx | 326 +++++++++++++++++++++
karavan-designer/src/builder/FileSelector.tsx | 81 +++++
karavan-designer/src/designer/KaravanDesigner.tsx | 2 +-
karavan-designer/src/designer/karavan.css | 81 ++++-
karavan-designer/src/designer/route/DslElement.tsx | 8 +
.../src/designer/utils/KaravanIcons.tsx | 117 ++++++++
karavan-designer/src/index.tsx | 1 +
karavan-generator/pom.xml | 5 +
.../karavan/generator/CamelJbangGenerator.java | 70 +++++
karavan-vscode/icons/dark/builder.svg | 61 ++++
karavan-vscode/icons/light/builder.svg | 1 +
karavan-vscode/package.json | 23 +-
karavan-vscode/src/builderView.ts | 208 +++++++++++++
karavan-vscode/src/commands.ts | 157 ++++++++++
karavan-vscode/src/extension.ts | 4 +
karavan-vscode/src/openapiView.ts | 11 +-
karavan-vscode/src/utils.ts | 60 +---
karavan-vscode/webview/App.tsx | 29 +-
karavan-vscode/webview/builder/BuilderPage.tsx | 326 +++++++++++++++++++++
karavan-vscode/webview/builder/FileSelector.tsx | 81 +++++
karavan-vscode/webview/index.css | 52 ++++
28 files changed, 1990 insertions(+), 91 deletions(-)
diff --git a/karavan-core/src/core/api/ProjectModelApi.ts
b/karavan-core/src/core/api/ProjectModelApi.ts
new file mode 100644
index 0000000..93001ed
--- /dev/null
+++ b/karavan-core/src/core/api/ProjectModelApi.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 {ProjectModel, ProjectStatus} from "../model/ProjectModel";
+
+const PREFIX_JBANG = "camel.jbang";
+const PREFIX_MAIN = "camel.main";
+
+export class ProjectModelApi {
+
+ static propertiesToProject = (properties: string): ProjectModel => {
+ const lines = properties.split(/\r?\n/).filter(text =>
text.trim().length > 0 &&
+ (text.startsWith(PREFIX_JBANG) ||
text.startsWith(this.getKeyWithPrefix('routesIncludePattern'))));
+ const map = this.propertiesToMap(lines);
+ const project = new ProjectModel();
+ project.name = this.getValue(map, "project.name");
+ project.version = this.getValue(map, "project.version");
+ project.namespace = this.getValue(map, "project.namespace");
+ project.cleanup = this.getValue(map, "project.cleanup") === "true";
+ const tag = this.getValue(map, "build.image.tag", "deploy.image");
+ project.tag = tag ? tag : project.namespace + "/" + project.name + ":"
+ project.version;
+ project.classpathFiles = this.getValue(map, "classpathFiles");
+ project.routesIncludePattern = this.getValue(map,
"routesIncludePattern");
+ project.sourceImage = this.getValue(map, "build.image.source-image");
+ project.from = this.getValue(map, "build.image.from");
+
+ project.filename = this.getValue(map, "build.image.jar",
"package.uber-jar.jar");
+ project.replicas = this.getValue(map, "deploy.replicas");
+ project.nodePort = this.getValue(map, "deploy.node-port");
+ project.server = this.getValue(map, "deploy.server",
"build.image.server");
+ const openshift = this.getValue(map, "deploy.openshift",
"build.image.openshift") === "true";
+ const minikube = this.getValue(map, "deploy.minikube",
"build.image.minikube") === "true";
+ project.target = openshift ? "openshift" : (minikube ? "minikube" :
"kubernetes");
+ project.deploy = this.getValue(map, "deploy") === "true";
+ project.build = this.getValue(map, "build.image") === "true";
+ project.uberJar = this.getValue(map, "package") === "true";
+ project.manifests = this.getValue(map, "manifests") === "true";
+ project.path = this.getValue(map, "manifests.path");
+ project.status = new ProjectStatus();
+
+ Object.keys(project).forEach(key => {
+ const value = (project as any)[key];
+ if ( value === undefined || value === 'undefined') delete (project
as any)[key];
+ })
+ return new ProjectModel(JSON.parse(JSON.stringify(project)) as
ProjectModel);
+ }
+
+ static getValue = (map: Map<string, any>, ...keys: string[]): any => {
+ for (const key of keys) {
+ const k = this.getKeyWithPrefix(key);
+ if (map.has(k)) return map.get(k);
+ }
+ return undefined;
+ }
+
+ static propertiesToMap = (properties: string[]): Map<string, any> => {
+ const result = new Map<string, any>();
+ properties.forEach(line => {
+ const pair = line.split("=").map(line => line.trim());
+ result.set(pair[0], pair[1]);
+ })
+ return result;
+ }
+
+ static updateProperties = (properties: string, project: ProjectModel):
string => {
+ const linesAll = properties.split(/\r?\n/);
+ const nonPropLines = linesAll.filter(text => text.trim().length === 0
+ || (!text.startsWith(PREFIX_JBANG) &&
!text.startsWith(this.getKeyWithPrefix('routesIncludePattern'))));
+ const propLines = linesAll.filter(text => text.trim().length > 0
+ && (text.startsWith(PREFIX_JBANG) ||
text.startsWith(this.getKeyWithPrefix('routesIncludePattern'))));
+ const mapFromFile = this.propertiesToMap(propLines);
+ const mapFromProject = this.projectToMap(project);
+ const result: string[] = [...nonPropLines];
+ mapFromFile.forEach((value, key) => {
+ if (!mapFromProject.has(key) && mapFromProject.get(key) !==
undefined) result.push(key + "=" + value);
+ })
+ mapFromProject.forEach((value, key) => {
+ if (value !== undefined) result.push(key + "=" + value);
+ })
+ return result.join("\n");
+ }
+
+ static projectToMap = (project: ProjectModel): Map<string, any> => {
+ const map = new Map<string, any>();
+ if (project.tag?.length === 0) project.tag = project.namespace + "/" +
project.name + ":" + project.version;
+ this.setValue(map, "project.name", project.name);
+ this.setValue(map, "project.version", project.version);
+ this.setValue(map, "project.namespace", project.namespace);
+ this.setValue(map, "project.cleanup", project.cleanup);
+ this.setValue(map, "package", project.uberJar);
+ this.setValue(map, "package.uber-jar.jar", project.filename);
+ this.setValue(map, "package.uber-jar.fresh", true);
+ this.setValue(map, "classpathFiles", project.classpathFiles);
+ this.setValue(map, "routesIncludePattern",
project.routesIncludePattern);
+ this.setValue(map, "build.image", project.build);
+ this.setValue(map, "build.image.openshift", project.target ===
'openshift');
+ this.setValue(map, "build.image.minikube", project.target ===
'minikube');
+ this.setValue(map, "build.image.jar", project.filename);
+ this.setValue(map, "build.image.tag", project.tag);
+ this.setValue(map, "build.image.source-image", project.sourceImage);
+ this.setValue(map, "build.image.from", project.from);
+ this.setValue(map, "build.image.server", project.server);
+ this.setValue(map, "deploy", project.deploy);
+ this.setValue(map, "deploy.openshift", project.target === 'openshift');
+ this.setValue(map, "deploy.minikube", project.target === 'minikube');
+ this.setValue(map, "deploy.image", project.tag);
+ this.setValue(map, "deploy.replicas", project.replicas);
+ this.setValue(map, "deploy.node-port", project.nodePort);
+ this.setValue(map, "deploy.server", project.server);
+ this.setValue(map, "undeploy.openshift", project.target ===
'openshift');
+ this.setValue(map, "undeploy.minikube", project.target === 'minikube');
+ this.setValue(map, "undeploy.server", project.server);
+ this.setValue(map, "manifests", project.manifests);
+ this.setValue(map, "manifests.path", project.manifests);
+ this.setValue(map, "manifests.openshift", project.target ===
'openshift');
+ this.setValue(map, "manifests.minikube", project.target ===
'minikube');
+ this.setValue(map, "manifests.image", project.tag);
+ this.setValue(map, "manifests.replicas", project.replicas);
+ this.setValue(map, "manifests.node-port", project.nodePort);
+ this.setValue(map, "manifests.server", project.server);
+ this.setValue(map, "manifests.jar", project.filename);
+
+ return map;
+ }
+
+ static setValue = (map: Map<string, any>, key: string, value: any):
Map<string, any> => {
+ if (value !== undefined && value?.toString().length > 0)
+ map.set(this.getKeyWithPrefix(key), value);
+ else
+ map.set(this.getKeyWithPrefix(key), undefined);
+ return map;
+ }
+
+ static getKeyWithPrefix = (key: string): string => {
+ const prefix = key === 'routesIncludePattern' ? PREFIX_MAIN :
PREFIX_JBANG;
+ return prefix + "." + key;
+ }
+}
diff --git a/karavan-core/src/core/model/ProjectModel.ts
b/karavan-core/src/core/model/ProjectModel.ts
new file mode 100644
index 0000000..e9a4b6f
--- /dev/null
+++ b/karavan-core/src/core/model/ProjectModel.ts
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+export class ProjectStatus {
+ uberJar: 'pending' | 'progress' | 'done' | 'error' = 'pending';
+ build: 'pending' | 'progress' | 'done' | 'error' = 'pending';
+ deploy: 'pending' | 'progress' | 'done'| 'error' = 'pending';
+ undeploy: 'pending' | 'progress' | 'done'| 'error' = 'pending';
+ active: boolean = false;
+
+ public constructor(init?: Partial<ProjectStatus>) {
+ Object.assign(this, init);
+ }
+}
+
+export class ProjectModel {
+ name: string = 'demo'
+ version: string = '1.0.0'
+ filename: string = 'camel-runner.jar'
+ namespace: string = 'default'
+ cleanup: boolean = false
+ tag?: string = this.namespace + "/" + this.name + ":" + this.version
+ sourceImage: string = 'java:openjdk-11-ubi8'
+ from: string = 'gcr.io/distroless/java:11'
+ replicas: number = 1
+ nodePort: number = 30777
+ server?: string
+ token?: string
+ target: 'openshift' | 'minikube' | 'kubernetes' = 'minikube'
+ deploy: boolean = false
+ build: boolean = false
+ uberJar: boolean = true
+ manifests: boolean = true
+ path: string = ''
+ classpathFiles: string = ''
+ routesIncludePattern: string = ''
+ status: ProjectStatus = new ProjectStatus()
+
+ public constructor(init?: Partial<ProjectModel>) {
+ Object.assign(this, init);
+ }
+
+ static createNew(): ProjectModel {
+ return new ProjectModel({})
+ }
+}
\ No newline at end of file
diff --git a/karavan-core/test/application.properties
b/karavan-core/test/application.properties
new file mode 100644
index 0000000..d641244
--- /dev/null
+++ b/karavan-core/test/application.properties
@@ -0,0 +1,26 @@
+# main props
+loggingLevel=info
+camel.main.name=CamelJBang
+camel.main.shutdownTimeout=5
+camel.main.routesReloadEnabled=false
+camel.main.sourceLocationEnabled=true
+camel.main.tracing=false
+camel.main.modeline=true
+camel.main.routesCompileDirectory=.camel-jbang
+camel.jbang.health=false
+camel.jbang.console=false
+camel.main.routesIncludePattern=file:CustomProcessor.java,file:postman.yaml
+camel.jbang.classpathFiles=parcels.png,postgres_db.sql,start.sh
+dependency=mvn:org.apache.camel:camel-kamelet:3.17.0-SNAPSHOT
+dependency=mvn:org.apache.camel:camel-java-joor-dsl:3.17.0-SNAPSHOT
+dependency=mvn:org.apache.camel:camel-rest:3.17.0-SNAPSHOT
+dependency=mvn:org.apache.camel:camel-core-languages:3.17.0-SNAPSHOT
+dependency=mvn:org.apache.camel:camel-direct:3.17.0-SNAPSHOT
+
+# jband build props
+camel.jbang.project.namespace=development
+camel.jbang.project.name=demo
+camel.jbang.project.version=1.0.1
+camel.jbang.build.image.openshift=true
+# camel.jbang.deploy.image=karavan/deploy-demo:1.0.0
+camel.jbang.package=true
diff --git a/karavan-core/test/application.spec.ts
b/karavan-core/test/application.spec.ts
new file mode 100644
index 0000000..6c9bc90
--- /dev/null
+++ b/karavan-core/test/application.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 * as fs from 'fs';
+import 'mocha';
+import {expect} from "chai";
+import {ProjectModelApi} from "../src/core/api/ProjectModelApi";
+
+describe('Project configuration', () => {
+
+ it('Read properties', () => {
+ const props =
fs.readFileSync('test/application.properties',{encoding:'utf8', flag:'r'});
+ const project = ProjectModelApi.propertiesToProject(props);
+ expect(project.name).to.equal('demo');
+ expect(project.namespace).to.equal('development');
+
+ project.tag = 'newtag/proj:latest';
+ let newProperties = ProjectModelApi.updateProperties(props, project);
+ const tag = newProperties.split(/\r?\n/).filter(l =>
l.startsWith("camel.jbang.build.image.tag"))[0].split("=")[1];
+ expect(tag).to.equal(project.tag);
+
+
+ project.routesIncludePattern = "file:x";
+ newProperties = ProjectModelApi.updateProperties(newProperties,
project);
+ project.routesIncludePattern = "file:y";
+ newProperties = ProjectModelApi.updateProperties(newProperties,
project);
+ project.routesIncludePattern = "";
+ newProperties = ProjectModelApi.updateProperties(newProperties,
project);
+ // console.log(newProperties);
+ });
+
+});
diff --git a/karavan-demo/builder/application.properties
b/karavan-demo/builder/application.properties
new file mode 100644
index 0000000..3b9a9b6
--- /dev/null
+++ b/karavan-demo/builder/application.properties
@@ -0,0 +1,32 @@
+
+camel.jbang.project.name=demo
+camel.jbang.project.version=1.0.0
+camel.jbang.project.namespace=default
+camel.jbang.project.cleanup=true
+camel.jbang.package=true
+camel.jbang.package.uber-jar.jar=camel-runner.jar
+camel.jbang.package.uber-jar.fresh=true
+camel.main.routesIncludePattern=file:builder-demo.yaml
+camel.jbang.build.image=true
+camel.jbang.build.image.openshift=false
+camel.jbang.build.image.minikube=true
+camel.jbang.build.image.jar=camel-runner.jar
+camel.jbang.build.image.tag=default/demo:1.0.0
+camel.jbang.build.image.source-image=java:openjdk-11-ubi8
+camel.jbang.build.image.from=gcr.io/distroless/java:11
+camel.jbang.deploy=true
+camel.jbang.deploy.openshift=false
+camel.jbang.deploy.minikube=true
+camel.jbang.deploy.image=default/demo:1.0.0
+camel.jbang.deploy.replicas=1
+camel.jbang.deploy.node-port=30777
+camel.jbang.undeploy.openshift=false
+camel.jbang.undeploy.minikube=true
+camel.jbang.manifests=true
+camel.jbang.manifests.path=true
+camel.jbang.manifests.openshift=false
+camel.jbang.manifests.minikube=true
+camel.jbang.manifests.image=default/demo:1.0.0
+camel.jbang.manifests.replicas=1
+camel.jbang.manifests.node-port=30777
+camel.jbang.manifests.jar=camel-runner.jar
\ No newline at end of file
diff --git a/karavan-demo/builder/builder-demo.yaml
b/karavan-demo/builder/builder-demo.yaml
new file mode 100644
index 0000000..d8763d6
--- /dev/null
+++ b/karavan-demo/builder/builder-demo.yaml
@@ -0,0 +1,9 @@
+- route:
+ from:
+ uri: kamelet:timer-source
+ steps:
+ - log:
+ message: ${body}
+ parameters:
+ period: 2000
+ message: Hello World
diff --git a/karavan-designer/src/App.tsx b/karavan-designer/src/App.tsx
index 67426b7..1a83bcd 100644
--- a/karavan-designer/src/App.tsx
+++ b/karavan-designer/src/App.tsx
@@ -24,9 +24,11 @@ import {KaravanDesigner} from "./designer/KaravanDesigner";
import {KameletsPage} from "./kamelets/KameletsPage";
import {ComponentsPage} from "./components/ComponentsPage";
import {EipPage} from "./eip/EipPage";
+import {BuilderPage} from "./builder/BuilderPage";
+import {ProjectModel} from "karavan-core/lib/model/ProjectModel";
interface Props {
- page: "designer" | "kamelets" | "components" | "eip";
+ page: "designer" | "kamelets" | "components" | "eip" | "builder";
}
interface State {
@@ -41,27 +43,26 @@ class App extends React.Component<Props, State> {
name: 'demo.yaml',
key: '',
yaml:
- // 'apiVersion: camel.apache.org/v1\n' +
- // 'kind: Integration\n' +
- // 'metadata:\n' +
- // ' name: postman.yaml\n' +
- // 'spec:\n' +
- // ' flows:\n' +
- // ' - route:\n' +
- // ' from:\n' +
- // ' uri: direct:post\n' +
- // ' steps:\n' +
- // ' - log:\n' +
- // ' message: \'Received: ${body}\'\n' +
- // ' - log:\n' +
- // ' message: \'Received: ${body}\'\n' +
- // ' - log:\n' +
- // ' message: \'Received: ${body}\'\n' +
- // ' - to:\n' +
- // ' uri: kamelet:kafka-sink\n' +
- // ' parameters:\n' +
- // ' topic: topic1\n' +
- // ' id: post\n' +
+ 'apiVersion: camel.apache.org/v1\n' +
+ 'kind: Integration\n' +
+ 'metadata:\n' +
+ ' name: postman.yaml\n' +
+ 'spec:\n' +
+ ' flows:\n' +
+ ' - route:\n' +
+ ' from:\n' +
+ ' uri: kamelet:timer-source\n' +
+ ' steps:\n' +
+ ' - log:\n' +
+ ' message: ${body}\n' +
+ ' - aggregate: {}\n' +
+ ' - choice: {}\n' +
+ ' - split:\n' +
+ ' expression: {}\n' +
+ ' - saga: {}\n' +
+ ' parameters:\n' +
+ ' period: 2000\n' +
+ ' message: Hello World\n' +
''
};
@@ -113,6 +114,9 @@ class App extends React.Component<Props, State> {
}
public render() {
+ const project = ProjectModel.createNew();
+ project.status.active = true;
+ project.status.uberJar = "progress";
return (
<Page className="karavan">
{this.props.page === "designer" && <KaravanDesigner
key={this.state.key} filename={this.state.name} yaml={this.state.yaml}
@@ -122,6 +126,12 @@ class App extends React.Component<Props, State> {
{this.props.page === "kamelets" && <KameletsPage
dark={document.body.className.includes('vscode-dark')} />}
{this.props.page === "components" && <ComponentsPage
dark={document.body.className.includes('vscode-dark')} />}
{this.props.page === "eip" && <EipPage
dark={document.body.className.includes('vscode-dark')} />}
+ {this.props.page === "builder" && <BuilderPage
dark={document.body.className.includes('vscode-dark')} project={project}
+
onChange={project => {
+
console.log("routesIncludePattern", project.routesIncludePattern);
+
console.log("classpathFiles", project.classpathFiles);
+ }}
+
files={'demo.yaml,CustomProcessor.java,script.groovy,docker-compose.yaml,README.MD'}/>}
</Page>
);
}
diff --git a/karavan-designer/src/builder/BuilderPage.tsx
b/karavan-designer/src/builder/BuilderPage.tsx
new file mode 100644
index 0000000..6b33006
--- /dev/null
+++ b/karavan-designer/src/builder/BuilderPage.tsx
@@ -0,0 +1,326 @@
+import React from 'react';
+import {
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+ TextInput,
+ PageSection,
+ TextContent,
+ Text,
+ PageSectionVariants,
+ Flex,
+ FlexItem,
+ Badge,
+ Button,
+ FormGroup,
+ Form,
+ Card,
+ CardTitle,
+ CardBody,
+ CardFooter,
+ CardHeader,
+ CardHeaderMain,
+ CardActions,
+ Checkbox,
+ Switch,
+ ToggleGroup,
+ ToggleGroupItem,
+ PopoverPosition,
+ Popover,
+ InputGroup,
+ ProgressStep,
+ ProgressStepper,
+ Spinner,
+ HelperTextItem, HelperText
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon";
+import InProgressIcon from
'@patternfly/react-icons/dist/esm/icons/in-progress-icon';
+import AutomationIcon from
'@patternfly/react-icons/dist/esm/icons/bundle-icon';
+import PendingIcon from '@patternfly/react-icons/dist/esm/icons/pending-icon';
+import ExclamationCircleIcon from
'@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
+import CheckCircleIcon from
'@patternfly/react-icons/dist/esm/icons/check-circle-icon';
+import RunIcon from '@patternfly/react-icons/dist/esm/icons/forward-icon';
+import StopIcon from '@patternfly/react-icons/dist/esm/icons/stop-icon';
+import JarIcon from '@patternfly/react-icons/dist/esm/icons/hotjar-icon';
+import ImageIcon from '@patternfly/react-icons/dist/esm/icons/docker-icon';
+import DeployIcon from
'@patternfly/react-icons/dist/esm/icons/cloud-upload-alt-icon';
+import CleanupIcon from '@patternfly/react-icons/dist/esm/icons/remove2-icon';
+import ProjectIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
+import {FileSelector} from "./FileSelector";
+import {ProjectModel, ProjectStatus} from
"karavan-core/lib/model/ProjectModel";
+
+interface Props {
+ dark: boolean
+ project: ProjectModel
+ files: string
+ onChange?: (project: ProjectModel) => void
+ onAction?: (action: "start" | "stop" | "undeploy", project: ProjectModel)
=> void
+}
+
+interface State {
+ name: string,
+ version: string,
+ filename: string,
+ namespace: string,
+ tag?: string,
+ sourceImage: string,
+ from: string,
+ replicas: number,
+ nodePort: number,
+ server?: string,
+ token?: string,
+ target: 'openshift' | 'minikube' | 'kubernetes',
+ deploy: boolean,
+ build: boolean,
+ uberJar: boolean,
+ routesIncludePattern: string,
+ classpathFiles: string,
+ status: ProjectStatus,
+ cleanup: boolean,
+ manifests: boolean,
+ path: string,
+}
+
+export class BuilderPage extends React.Component<Props, State> {
+
+ public state: State = this.props.project;
+
+ componentDidUpdate = (prevProps: Readonly<Props>, prevState:
Readonly<State>, snapshot?: any) => {
+ this.props.onChange?.call(this, this.state);
+ }
+
+ getHelp(text: string) {
+ return <Popover
+ aria-label={text}
+ position={PopoverPosition.left}
+ bodyContent={text}>
+ <Button variant="plain" onClick={e => {
+ }}>
+ <HelpIcon/>
+ </Button>
+ </Popover>
+ }
+
+ getField(name: string, label: string, type: 'text' | 'date' |
'datetime-local' | 'email' | 'month' | 'number' | 'password' | 'search' | 'tel'
| 'time' | 'url',
+ value: any, help: string, onChange: (val: any) => void,
isRequired: boolean = false, enabled: boolean = true) {
+ return <FormGroup label={label} fieldId={name} isRequired={isRequired}>
+ <InputGroup>
+ <TextInput isRequired={isRequired} isDisabled={!enabled}
className="text-field" type={type} id={name} name={name} value={value}
+ onChange={val => onChange?.call(this, val)}/>
+ {this.getHelp(help)}
+ </InputGroup>
+ </FormGroup>
+ }
+
+ getCardHeader(title: string, icon: any, optional: boolean = true, checked:
boolean = false, onCheck?: (check: boolean) => void) {
+ return <CardHeader>
+ <CardHeaderMain>
+ <CardTitle className="card-header">
+ {icon}{title}
+ </CardTitle>
+ </CardHeaderMain>
+ <CardActions hasNoOffset={true}>
+ {optional && <Switch isChecked={checked} onChange={checked =>
onCheck?.call(this, checked)} id={title} name={title} aria-label={"xxx"}/>}
+ </CardActions>
+ </CardHeader>
+ }
+
+ getProjectForm() {
+ return (
+ <Card className="builder-card" isCompact style={{width: "100%"}}>
+ {this.getCardHeader("Project", <ProjectIcon/>, false)}
+ <CardBody>
+ <Form isHorizontal>
+ {this.getField("name", "Name", "text",
this.state.name, "Project name", val => this.setState({name: val}), true)}
+ {this.getField("version", "Version", "text",
this.state.version, "Project version", val => this.setState({version: val}),
true)}
+ </Form>
+ </CardBody>
+ </Card>
+ )
+ }
+
+ getPackageForm() {
+ const {uberJar, classpathFiles, routesIncludePattern} = this.state;
+ return <Card className="builder-card" isCompact style={{width:
"100%"}}>
+ {this.getCardHeader("Package", <JarIcon/>, true,
this.state.uberJar, check => this.setState({uberJar: check}))}
+ <CardBody className={uberJar ? "" : "card-disabled"}>
+ <Form isHorizontal>
+ {this.getField("filename", "Jar", "text",
this.state.filename, "Jar file name", val => this.setState({filename: val}),
true, uberJar)}
+ {this.props.files.length >0 &&
+ <FileSelector source={true} label="Route/source files"
help="Routes and source to build and package" files={this.props.files}
filesSelected={routesIncludePattern} onChange={filesSelected =>
this.setState({routesIncludePattern: filesSelected})}/>}
+ {this.props.files.length >0 &&
+ <FileSelector source={false} label="Resources"
help="Files to package as resources" files={this.props.files}
filesSelected={classpathFiles} onChange={filesSelected =>
this.setState({classpathFiles: filesSelected})}/>}
+ </Form>
+ </CardBody>
+ </Card>
+ }
+
+ getBuildForm() {
+ const {target, namespace, build, tag, sourceImage, server, token,
from} = this.state;
+ return <Card className="builder-card" isCompact style={{width:
"100%"}}>
+ {this.getCardHeader("Build", <ImageIcon/>, true, this.state.build,
check => this.setState({build: check}))}
+ <CardBody className={build ? "" : "card-disabled"}>
+ <Form isHorizontal>
+ <FormGroup label="Target" fieldId="tag" isRequired
disabled={true}>
+ <ToggleGroup aria-label="Select target">
+ <ToggleGroupItem isDisabled={!build}
text="Minikube" buttonId="minikube" isSelected={target === 'minikube'}
+ onChange={selected => selected ?
this.setState({target: 'minikube'}) : {}}/>
+ <ToggleGroupItem isDisabled={!build}
text="Kubernetes" buttonId="kubernetes" isSelected={target === 'kubernetes'}
+ onChange={selected => selected ?
this.setState({target: 'kubernetes'}) : {}}/>
+ <ToggleGroupItem isDisabled={!build}
text="Openshift" buttonId="openshift" isSelected={target === 'openshift'}
+ onChange={selected => selected ?
this.setState({target: 'openshift'}) : {}}/>
+ </ToggleGroup>
+ </FormGroup>
+ {this.getField("namespace", "Namespace", "text",
namespace, "Namespace to build and/or deploy", val => this.setState({namespace:
val}), true, build)}
+ {this.getField("tag", "Image tag", "text", tag, "Image
tag", val => this.setState({tag: val}), true, build)}
+ {target !== 'openshift' && this.getField("from", "Base
Image", "text", from, "Base Image", val => this.setState({from: val}), true,
build)}
+ {target === 'openshift' && this.getField("sourceImage",
"Source tag", "text", sourceImage, "Source image name (for OpenShift
BuildConfig)", val => this.setState({sourceImage: val}), true, build)}
+ {target === 'openshift' && this.getField("server",
"Server", "text", server, "Master URL", val => this.setState({server: val}),
true, build)}
+ {target === 'openshift' && this.getField("token", "Token",
"password", token, "Authentication Token (Token will not be saved)", val =>
this.setState({token: val}), true, build)}
+ </Form>
+ </CardBody>
+ </Card>
+ }
+
+ getDeployForm() {
+ const {target, deploy, build} = this.state;
+ return <Card className="builder-card" isCompact style={{width:
"100%"}}>
+ {this.getCardHeader("Deploy", <DeployIcon/>, true, deploy, check
=> this.setState({deploy: check}))}
+ <CardBody className={build ? "" : "card-disabled"}>
+ <Form isHorizontal>
+ {deploy && this.getField("replicas", "Replicas", "number",
this.state.replicas, "Number of replicas of the application", val =>
this.setState({replicas: val}), true, build)}
+ {deploy && target === 'minikube' &&
this.getField("nodePort", "Node port", "number", this.state.nodePort, "Node
port (minikube)", val => this.setState({nodePort: val}), true, build)}
+ </Form>
+ </CardBody>
+ </Card>
+ }
+
+ getCleanupForm() {
+ const {cleanup} = this.state;
+ return <Card className="builder-card" isCompact style={{width:
"100%"}}>
+ {this.getCardHeader("Cleanup", <CleanupIcon/>, true, cleanup,
check => this.setState({cleanup: check}))}
+ <CardBody className={cleanup ? "" : "card-disabled"}>
+ <HelperText>
+ <HelperTextItem variant="indeterminate">Remove created jar
and .camel-jbang after build and/or deploy.</HelperTextItem>
+ </HelperText>
+ </CardBody>
+ </Card>
+ }
+
+ getProgressIcon(status: 'pending' | 'progress' | 'done' | 'error') {
+ switch (status) {
+ case "pending":
+ return <PendingIcon/>;
+ case "progress":
+ return <Spinner isSVG size="md"/>
+ case "done":
+ return <CheckCircleIcon/>;
+ case "error":
+ return <ExclamationCircleIcon/>;
+ default:
+ return undefined;
+ }
+ }
+
+ getProgress() {
+ const {status, uberJar, build, deploy} = this.state;
+ const undeploying = status.active && status.undeploy === "progress";
+ return (
+ <ProgressStepper isCenterAligned style={{visibility: "visible"}}>
+ {!undeploying && uberJar && <ProgressStep variant="pending"
id="package" titleId="package" aria-label="package"
icon={this.getProgressIcon(status.uberJar)}>Package</ProgressStep>}
+ {!undeploying && build && <ProgressStep variant="pending"
isCurrent id="build" titleId="build" aria-label="build"
icon={this.getProgressIcon(status.build)}>Build</ProgressStep>}
+ {!undeploying && deploy && <ProgressStep variant="pending"
id="deploy" titleId="deploy" aria-label="deploy"
icon={this.getProgressIcon(status.deploy)}>Deploy</ProgressStep>}
+ {undeploying &&
+ <ProgressStep variant="pending" id="undeploy"
titleId="undeploy" aria-label="undeploy"
icon={this.getProgressIcon(status.undeploy)}>Undeploy</ProgressStep>}
+ </ProgressStepper>
+ )
+ }
+
+ getHeader() {
+ return (
+ <PageSection className="tools-section" variant={this.props.dark ?
PageSectionVariants.darker : PageSectionVariants.light}>
+ <Flex className="tools" direction={{default: 'row'}}
justifyContent={{default: 'justifyContentSpaceBetween'}} spaceItems={{default:
'spaceItemsLg'}}>
+ <FlexItem>
+ <TextContent className="header">
+ <Text component="h2">Project Builder</Text>
+ <Badge isRead className="labels">Powered by Camel
JBang</Badge>
+ </TextContent>
+ </FlexItem>
+ <FlexItem>
+ <Toolbar id="toolbar-group-types">
+ <ToolbarContent>
+ <ToolbarItem>
+ <Button variant="plain" onClick={e => {
+ }}><HelpIcon/></Button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </FlexItem>
+ </Flex>
+ </PageSection>
+ )
+ }
+
+ onButtonClick(action: "start" | "stop" | "undeploy") {
+ this.props.onAction?.call(this, action, this.state);
+ }
+
+ getFooter() {
+ const active = this.state.status.active;
+ const label = active ? "Stop" : "Start";
+ const icon = active ? <InProgressIcon/> : <AutomationIcon/>;
+ return <div className="footer">
+ <div className="progress">
+ {active && this.getProgress()}
+ </div>
+ <div className="buttons">
+ <Toolbar id="toolbar-items">
+ <ToolbarContent>
+ {!active && <ToolbarItem>
+ <Button variant="secondary" isSmall
onClick={event => this.onButtonClick("undeploy")}>Undeploy</Button>
+ </ToolbarItem>}
+ <ToolbarItem>
+ <Button variant="primary" isSmall
icon={icon} onClick={event => this.onButtonClick(active ? "stop" :
"start")}>{label}</Button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </div>
+ </div>
+ }
+
+ getCenter() {
+ return (
+ <div className="center">
+ <div className="center-column">
+ {this.getProjectForm()}
+ {this.getPackageForm()}
+ </div>
+ <div className="center-column">
+ {this.getBuildForm()}
+ {this.getDeployForm()}
+ {this.getCleanupForm()}
+ </div>
+ </div>
+ )
+ }
+
+ render() {
+ return (
+ <PageSection className="project-builder" variant={this.props.dark
? PageSectionVariants.darker : PageSectionVariants.light}
+ padding={{default: 'noPadding'}}>
+ <div style={{height: "100%", display: "flex", flexDirection:
"column"}}>
+ <div style={{flexShrink: "0"}}>
+ {this.getHeader()}
+ </div>
+ <div style={{overflow: "auto", flexGrow: 1}}>
+ {this.getCenter()}
+ </div>
+ <div style={{flexShrink: "0"}}>
+ {this.getFooter()}
+ </div>
+ </div>
+ </PageSection>
+ )
+ }
+};
\ No newline at end of file
diff --git a/karavan-designer/src/builder/FileSelector.tsx
b/karavan-designer/src/builder/FileSelector.tsx
new file mode 100644
index 0000000..983a2a6
--- /dev/null
+++ b/karavan-designer/src/builder/FileSelector.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import {
+ Button,
+ FormGroup,
+ Checkbox, PopoverPosition, Popover, InputGroup
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon";
+
+interface Props {
+ label: string
+ help: string
+ files: string
+ filesSelected: string
+ onChange: (files: string) => void
+ source: boolean
+}
+
+interface State {
+ selected: []
+}
+
+export class FileSelector extends React.Component<Props, State> {
+
+ public state: State = {
+ selected: []
+ };
+
+ isChecked(file: string) {
+ const finalFile = this.props.source ? "file:" + file : file;
+ const s = this.props.filesSelected.split(",").map(value =>
value.trim());
+ return s.includes(finalFile);
+ }
+
+ onChange(file: string, checked: boolean) {
+ const finalFile = this.props.source ? "file:" + file : file;
+ const s = this.props.filesSelected.split(",").map(f =>
f.trim()).filter(f => f.length > 0);
+ const already = s.includes(finalFile);
+ if (checked && !already) {
+ s.push(finalFile);
+ this.props.onChange?.call(this, s.join(","));
+ } else if (!checked) {
+ const result = s.filter(f => f !== finalFile);
+ this.props.onChange?.call(this, result.join(","));
+ }
+ }
+
+ getFiles(): string[] {
+ const allFiles = (this.props.files ? this.props.files.split(",") : []);
+ if (this.props.source){
+ const extensions = ['yaml', 'yml', 'java', 'js', 'kt', 'groovy',
'xml'];
+ return allFiles.filter(file => {
+ const extension = file.split(".").pop() || '';
+ return extensions.includes(extension);
+ }).map(file => file.replace("file:", ""))
+ }
+ return allFiles;
+ }
+
+ render() {
+ const files = this.getFiles();
+ return (
+ <FormGroup label={this.props.label} fieldId="files">
+ <InputGroup>
+ <div style={{width:"100%"}}>
+ {files.map(file => {
+ const key = file + this.props.source;
+ return <Checkbox key={key} label={file}
isChecked={this.isChecked(file)} onChange={checked => this.onChange(file,
checked)} id={key} name={key}/>
+ })}
+ </div>
+ <Popover aria-label="files" position={PopoverPosition.left}
+ bodyContent={this.props.help}>
+ <Button variant="plain" onClick={e => {}}>
+ <HelpIcon/>
+ </Button>
+ </Popover>
+ </InputGroup>
+ </FormGroup>
+ )
+ }
+};
\ No newline at end of file
diff --git a/karavan-designer/src/designer/KaravanDesigner.tsx
b/karavan-designer/src/designer/KaravanDesigner.tsx
index 0360cc1..bc0c755 100644
--- a/karavan-designer/src/designer/KaravanDesigner.tsx
+++ b/karavan-designer/src/designer/KaravanDesigner.tsx
@@ -140,7 +140,7 @@ export class KaravanDesigner extends React.Component<Props,
State> {
<Tab eventKey='rest' title={this.getTab("REST", "REST
services", "rest")}></Tab>
<Tab eventKey='beans' title={this.getTab("Beans", "Beans
Configuration", "beans")}></Tab>
<Tab eventKey='dependencies'
title={this.getTab("Dependencies", "Dependencies", "dependencies")}></Tab>
- <Tab eventKey='traits' title={this.getTab("Traits",
"traits configuration", "traits")}></Tab>
+ {/*<Tab eventKey='traits' title={this.getTab("Traits",
"traits configuration", "traits")}></Tab>*/}
<Tab eventKey='error' title={this.getTab("Error", "Error
Handler", "error")}></Tab>
<Tab eventKey='exception' title={this.getTab("Exceptions",
"Exception Clauses per type", "exception")}></Tab>
{/*<Tab eventKey='code' title={this.getTab("Code", "Code",
"code")}></Tab>*/}
diff --git a/karavan-designer/src/designer/karavan.css
b/karavan-designer/src/designer/karavan.css
index 4249a17..1800c04 100644
--- a/karavan-designer/src/designer/karavan.css
+++ b/karavan-designer/src/designer/karavan.css
@@ -1097,4 +1097,83 @@ reactour-portal .reactour__popover button:focus-visible {
.karavan .tools-section .tools .header .labels {
height: fit-content;
margin-left: 3px;
-}
\ No newline at end of file
+}
+
+/* Project Tools */
+.karavan .project-builder {
+ height: 100%;
+ font-size: 14px;
+}
+
+.karavan .project-builder .card-header svg {
+ margin-right: 6px;
+}
+
+.karavan .project-builder .card-disabled {
+ opacity: 0.4;
+}
+
+.karavan .project-builder .pf-c-form__label {
+ font-size: 14px;
+}
+
+.karavan .project-builder .pf-c-check__label {
+ font-size: 14px;
+}
+
+.karavan .project-builder .text-field {
+ font-size: 14px;
+ height: auto;
+}
+
+.karavan .project-builder .pf-c-input-group button {
+ font-size: 14px;
+ height: auto;
+}
+
+.karavan .project-builder .pf-c-form {
+ --pf-c-form--GridGap: 1em;
+}
+
+.karavan .project-builder .pf-c-card__body {
+ --pf-c-card--child--PaddingRight: 0.5em;
+ --pf-c-card--child--PaddingBottom: 1em;
+ --pf-c-card--child--PaddingLeft: 1em;
+}
+
+.karavan .project-builder .pf-c-card__header {
+ --pf-c-card--first-child--PaddingTop: 0.5em;
+}
+
+.karavan .project-builder .center {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ padding: 10px;
+}
+.karavan .project-builder .center-column {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.karavan .project-builder .footer {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 10px;
+}
+
+.karavan .project-builder .footer .progress {
+ flex-grow: 4;
+}
+
+.karavan .project-builder .footer .buttons {
+ height: 60px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+}
diff --git a/karavan-designer/src/designer/route/DslElement.tsx
b/karavan-designer/src/designer/route/DslElement.tsx
index ddc391a..bdfec5b 100644
--- a/karavan-designer/src/designer/route/DslElement.tsx
+++ b/karavan-designer/src/designer/route/DslElement.tsx
@@ -28,6 +28,7 @@ import {EventBus} from "../utils/EventBus";
import {ChildElement, CamelDefinitionApiExt} from
"karavan-core/lib/api/CamelDefinitionApiExt";
import ReactDOM from "react-dom";
import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
+import {AggregateIcon, ChoiceIcon, FilterIcon, SagaIcon, SortIcon, SplitIcon,
TransformIcon} from "../utils/KaravanIcons";
interface Props {
step: CamelElement,
@@ -197,6 +198,13 @@ export class DslElement extends React.Component<Props,
State> {
data-tour={step.dslName + "-icon"}
className={"header-icon"}
style={this.isWide() ? {width: ""} : {}}>
+ {/*{step.dslName === 'AggregateDefinition' &&
<AggregateIcon/>}*/}
+ {/*{step.dslName === 'ChoiceDefinition' &&
<ChoiceIcon/>}*/}
+ {/*{step.dslName === 'SplitDefinition' &&
<SplitIcon/>}*/}
+ {/*{step.dslName === 'SagaDefinition' &&
<SagaIcon/>}*/}
+ {/*{step.dslName === 'TransformDefinition' &&
<TransformIcon/>}*/}
+ {/*{step.dslName === 'FilterDefinition' &&
<FilterIcon/>}*/}
+ {/*{step.dslName === 'SortDefinition' &&
<SortIcon/>}*/}
<img draggable={false} src={CamelUi.getIcon(step)}
className="icon" alt="icon"/>
</div>
}
diff --git a/karavan-designer/src/designer/utils/KaravanIcons.tsx
b/karavan-designer/src/designer/utils/KaravanIcons.tsx
index 2246fee..b40a7a9 100644
--- a/karavan-designer/src/designer/utils/KaravanIcons.tsx
+++ b/karavan-designer/src/designer/utils/KaravanIcons.tsx
@@ -217,4 +217,121 @@ export class ConceptIcon extends React.Component<any> {
</svg>
)
}
+}
+
+export function AggregateIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 700 700">
+ <path d="M661.91 195.36v89.37c0 3.9-3.17 7.08-7.07
7.08H500.48c-3.9 0-7.08-3.18-7.08-7.08v-89.38l66.28 51.46.58.41c5.29 3.56 11.34
5.35 17.4 5.35s12.11-1.79 17.4-5.35l.29-.2 66.56-51.66z"></path>
+ <path d="M640.53 184.98l-57.63 44.74a9.755 9.755 0 01-10.49
0l-57.63-44.74h125.74zM412.03 87.97l-56.86 44.14a9.64 9.64 0 01-10.34
0l-56.86-44.14h124.07z"></path>
+ <path d="M433.12 98.21v88.18c0 3.84-3.13 6.97-6.97
6.97H273.87c-3.84 0-6.98-3.13-6.98-6.97v-88.2l65.39 50.77.57.41c5.22 3.52 11.18
5.28 17.17 5.28s11.95-1.75 17.17-5.28l.28-.2 65.66-50.97zM185.21 195.35l-57.63
44.74a9.755 9.755 0 01-10.49 0l-57.63-44.74H185.2z"></path>
+ <path d="M206.59 205.73v89.37c0 3.9-3.17 7.08-7.07 7.08H45.16c-3.9
0-7.08-3.18-7.08-7.08v-89.38l66.28 51.46.58.41c5.29 3.56 11.34 5.35 17.4
5.35s12.11-1.79 17.4-5.35l.29-.2 66.56-51.66z"></path>
+ <g>
+ <path d="M496.2 395.04l-130.22
101.1c-.19.14-.39.29-.59.42a28.39 28.39 0 01-30.77
0c-.21-.13-.4-.28-.59-.42L203.8 395.04h292.4z"></path>
+ <path d="M516.11 403.56v202.1c0 4.12-3.34 7.46-7.45
7.46h-317.3c-4.11 0-7.46-3.34-7.46-7.46V403.55l138.52 107.53c.68.53 1.31.98
1.94 1.38 7.79 5.04 16.72 7.55 25.66 7.55s17.86-2.52 25.66-7.55c.62-.4 1.25-.85
1.94-1.38l138.5-107.52z"></path>
+ </g>
+ <path d="M381.11 331.09l-25.8
20.35-.05.04-.02.02c-.19.14-.38.28-.58.41s-.4.26-.61.36c-.14.08-.28.16-.43.22-.12.06-.24.11-.36.16-.15.07-.3.12-.45.17-.11.04-.22.08-.33.11a.83.83
0 01-.21.06l-.36.09c-.22.05-.43.1-.65.12-.19.04-.38.06-.57.07a7.961 7.961 0
01-1.39
0c-.24-.02-.47-.05-.7-.09-.26-.04-.51-.09-.77-.16-.2-.05-.4-.11-.6-.18-.69-.24-1.35-.57-1.97-.98-.16-.1-.3-.21-.45-.32-.03-.02-.06-.05-.09-.07l-25.83-20.38a8.496
8.496 0 01-1.41-11.94c2.91-3.68 8.25-4.32 11.94-1.41l12.0 [...]
+ <g>
+ <path d="M617.35 343.67l-51.11 51.11 15.28 1.8c4.66.55 8 4.78
7.45 9.44-.51 4.32-4.19 7.51-8.43 7.51-.34 0-.67-.02-1.01-.06l-32.4-3.83c-.17
0-.33-.03-.5-.06-.11-.01-.22-.03-.32-.06-.19-.03-.38-.07-.57-.12-.22-.05-.43-.11-.64-.18-.5-.17-.98-.39-1.43-.65-.12-.06-.25-.14-.37-.22-.09-.05-.18-.11-.26-.17-.17-.11-.33-.23-.48-.34-.19-.16-.38-.32-.55-.48
0-.01
0-.02-.02-.02-.17-.17-.34-.34-.5-.52-.03-.03-.06-.06-.09-.1-.12-.14-.24-.29-.35-.44a.38.38
0 01-.09-.13c-.27-.36-.5-.74-. [...]
+ <path d="M539.37 401.96a6.487 6.487 0 00.02.2l-.02-.2zM540.25
405.01c.21.4.44.78.71 1.14-.27-.36-.51-.74-.71-1.14zM547.13
409.64l-.29-.03-.21-.03c.17.03.33.05.5.06z"></path>
+ </g>
+ <g>
+ <path d="M160.62 400.04l-.02.19a6.796 6.796 0
00.02-.19z"></path>
+ <path d="M164.46 367.54l-3.84
32.5c-.02.26-.06.51-.11.77-.03.2-.08.41-.14.61-.04.16-.09.32-.14.48-.02.06-.03.11-.06.16-.04.14-.09.27-.15.4-.17.44-.39.86-.64
1.26-.12.19-.24.37-.38.55-.14.21-.3.4-.47.59-.17.19-.34.37-.53.55-.22.22-.46.42-.71.6-.14.11-.29.22-.44.31a.18.18
0
01-.07.05c-.2.13-.4.25-.62.37-.4.21-.82.4-1.26.55-.21.07-.42.13-.64.18-.19.05-.38.09-.57.12-.1.03-.2.05-.31.05-.17.04-.34.06-.52.07l-32.39
3.83a8.5 8.5 0 01-9.44-7.45 8.501 8.501 0 017.45-9.44l15.27-1.8- [...]
+ <path d="M152.86
407.71c.18-.01.35-.03.52-.07-.07.02-.15.03-.22.04l-.3.03z"></path>
+ </g>
+ </svg>
+ );
+}
+
+export function ChoiceIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 700 700">
+ <rect
+ width="142.27"
+ height="505.23"
+ x="270.87"
+ y="80.9"
+ rx="5"
+ ry="5"
+ ></rect>
+ <path d="M662.24 257.74l-60.87 69.08a4.999 4.999 0 01-3.76
1.69H431.14c-2.76 0-5-2.24-5-5V189.69c0-2.76 2.24-5 5-5H597.8c1.38 0 2.7.58
3.65 1.58l60.69 64.75a4.979 4.979 0 01.1 6.72zM257.87 343.51v133.83c0 2.75-2.25
5-5 5H86.21c-1.38 0-2.71-.58-3.65-1.58l-60.69-64.75a4.981 4.981 0
01-.11-6.73l60.88-69.07c.95-1.08 2.31-1.7 3.75-1.7h166.48c2.75 0 5 2.25 5
5z"></path>
+ </svg>
+ );
+}
+
+
+export function SplitIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 700 700">
+ <path d="M496.2 79.83l-130.22 101.1c-.19.14-.39.29-.59.42a28.39
28.39 0 01-30.77 0c-.21-.13-.4-.28-.59-.42L203.8 79.83h292.4zm19.91 210.63c0
4.12-3.34 7.46-7.45 7.46h-317.3c-4.11 0-7.46-3.34-7.46-7.46V88.34l138.52
107.54c.68.53 1.31.98 1.94 1.38 7.79 5.04 16.72 7.55 25.66 7.55s17.86-2.52
25.66-7.55c.62-.4 1.25-.85 1.94-1.38L516.13 88.36v202.1z"></path>
+ <path d="M496.2 79.83l-130.22 101.1c-.19.14-.39.29-.59.42a28.39
28.39 0 01-30.77 0c-.21-.13-.4-.28-.59-.42L203.8 79.83h292.4z"></path>
+ <path d="M516.1 88.35v202.1c0 4.12-3.34 7.46-7.45
7.46H191.36c-4.11 0-7.46-3.34-7.46-7.46V88.34l138.52 107.54c.68.53 1.31.98 1.94
1.38 7.79 5.04 16.72 7.55 25.66 7.55s17.86-2.52 25.66-7.55c.62-.4 1.25-.85
1.94-1.38L516.13 88.36zM640.53 450.59l-57.63 44.74a9.755 9.755 0 01-10.49
0l-57.63-44.74h125.74z"></path>
+ <path d="M661.92 460.96v89.37c0 3.9-3.17 7.08-7.07
7.08H500.48c-3.9 0-7.08-3.18-7.08-7.08v-89.39l66.28 51.46.58.41c5.29 3.56 11.34
5.35 17.4 5.35s12.11-1.79 17.4-5.35l.29-.2 66.56-51.66zM412.88 512.96l-57.64
44.75a9.775 9.775 0 01-10.49 0l-57.64-44.75h125.77z"></path>
+ <path d="M434.26 523.33v89.39c0 3.9-3.17 7.07-7.07
7.07H272.82c-3.9 0-7.08-3.17-7.08-7.07v-89.4l66.28 51.46.58.41c5.29 3.57 11.34
5.35 17.4 5.35s12.11-1.78 17.4-5.35l.29-.2 66.56-51.66z"></path>
+ <g>
+ <path d="M185.21 460.96l-57.63 44.74a9.755 9.755 0 01-10.49
0l-57.63-44.74H185.2z"></path>
+ <path d="M206.6 471.33v89.37c0 3.9-3.17 7.08-7.07
7.08H45.16c-3.9 0-7.08-3.18-7.08-7.08v-89.39l66.28 51.46.58.41c5.29 3.56 11.34
5.35 17.4 5.35s12.11-1.79 17.4-5.35l.29-.2 66.56-51.66z"></path>
+ </g>
+ <path d="M381.11 412.41l-25.8
20.35-.05.04-.02.02c-.19.14-.38.28-.58.41s-.4.26-.61.36c-.14.08-.28.16-.43.22-.12.06-.24.11-.36.16-.15.07-.3.12-.45.17-.11.04-.22.08-.33.11a.83.83
0 01-.21.06l-.36.09c-.22.05-.43.1-.65.12-.19.04-.38.06-.57.07a7.961 7.961 0
01-1.39
0c-.24-.02-.47-.05-.7-.09-.26-.04-.51-.09-.77-.16-.2-.05-.4-.11-.6-.18-.69-.24-1.35-.57-1.97-.98-.16-.1-.3-.21-.45-.32-.03-.02-.06-.05-.09-.07l-25.83-20.38a8.496
8.496 0 01-1.41-11.94c2.91-3.68 8.25-4.32 11.94-1.41l12.0 [...]
+ <g>
+ <path d="M215.62 342.32l-51.11 51.11 15.28 1.8c4.66.55 8 4.78
7.45 9.44-.51 4.32-4.19 7.51-8.43 7.51-.34 0-.67-.02-1.01-.06l-32.4-3.83c-.17
0-.33-.03-.5-.06-.11-.01-.22-.03-.32-.06-.19-.03-.38-.07-.57-.12-.22-.05-.43-.11-.64-.18-.5-.17-.98-.39-1.43-.65-.12-.06-.25-.14-.37-.22-.09-.05-.18-.11-.26-.17-.17-.11-.33-.23-.48-.34-.19-.16-.38-.32-.55-.48
0-.01
0-.02-.02-.02-.17-.17-.34-.34-.5-.52-.03-.03-.06-.06-.09-.1-.12-.14-.24-.29-.35-.44a.38.38
0 01-.09-.13c-.27-.36-.5-.74-. [...]
+ <path d="M137.64 400.61a6.487 6.487 0 00.02.2l-.02-.2zM138.52
403.66c.21.4.44.78.71 1.14-.27-.36-.51-.74-.71-1.14zM145.4
408.29l-.29-.03-.21-.03c.17.03.33.05.5.06z"></path>
+ </g>
+ <g>
+ <path d="M562.44 400.62l-.02.19a6.796 6.796 0
00.02-.19z"></path>
+ <path d="M566.28 368.12l-3.84
32.5c-.02.26-.06.51-.11.77-.03.2-.08.41-.14.61-.04.16-.09.32-.14.48-.02.06-.03.11-.06.16-.04.14-.09.27-.15.4-.17.44-.39.86-.64
1.26-.12.19-.24.37-.38.55-.14.21-.3.4-.47.59-.17.19-.34.37-.53.55-.22.22-.46.42-.71.6-.14.11-.29.22-.44.31a.18.18
0
01-.07.05c-.2.13-.4.25-.62.37-.4.21-.82.4-1.26.55-.21.07-.42.13-.64.18-.19.05-.38.09-.57.12-.1.03-.2.05-.31.05-.17.04-.34.06-.52.07l-32.39
3.83a8.5 8.5 0 01-9.44-7.45 8.501 8.501 0 017.45-9.44l15.27-1.8- [...]
+ <path d="M554.68
408.29c.18-.01.35-.03.52-.07-.07.02-.15.03-.22.04l-.3.03z"></path>
+ </g>
+ </svg>
+ );
+}
+
+export function SagaIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 700">
+ <defs>
+ <style>{".b {fill: #fff;}"}</style>
+ </defs>
+ <path
+
d="M616,263.74c-3.24,4.11-9.18,4.81-13.29,1.57l-31.58-24.92v198.52c0,32.76-13.59,64.51-37.28,87.12-22.2,21.19-50.77,32.66-80.97,32.66-1.86,0-3.73-.03-5.6-.12-63.15-2.95-112.63-54.83-112.63-118.12v-3.02c-44.07-4.73-78.49-42.14-78.49-87.43s34.42-82.69,78.49-87.42v-1.48c0-27.61-11.46-54.37-31.43-73.44-19.79-18.88-45.57-28.59-72.62-27.33-53.05,2.48-94.6,46.06-94.6,99.22v180.9h-.48c20.3,4.17,35.61,22.17,35.61,43.69,0,24.58-20,44.58-44.59,44.58s-44.58-20-44.58-44.58c0-21.52,15.
[...]
+ <path className="b"
+
d="M334.78,376.47h-.09c-2.54-.03-4.96-1.07-6.73-2.9l-16.78-17.45c-3.62-3.77-3.5-9.75,.26-13.37,3.76-3.62,9.75-3.51,13.37,.26l10.09,10.5,28.63-28.63c3.69-3.69,9.68-3.69,13.38,0,3.69,3.69,3.69,9.68,0,13.38l-35.45,35.45c-1.77,1.77-4.18,2.77-6.69,2.77Z"/>
+ </svg>
+ );
+}
+
+export function TransformIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 700">
+ <path d="M441.77 277.51l-82.73 64.23c-.07.05-.13.09-.19.13-5.37
3.48-12.33 3.48-17.69.01-.07-.05-.13-.09-.18-.13l-82.76-64.24h183.54z"></path>
+ <path d="M462.2 287.02V420.7c0 .98-.79 1.77-1.77 1.77H239.57c-.98
0-1.77-.79-1.77-1.77V287.02l90.91 70.56c.54.44 1.06.8 1.57 1.12 5.99 3.88 12.86
5.81 19.72 5.81s13.73-1.94 19.73-5.81c.49-.32 1.01-.68
1.58-1.13l90.89-70.55zM622.28 330.68l-35.89 31.78a1.48 1.48 0 01-1.98
0l-35.89-31.78c-.3-.26-.48-.63-.51-1.03-.02-.4.11-.79.38-1.09l11.28-12.73c.55-.61
1.49-.67 2.11-.12l12.44
11.02c-5.24-51.26-28.18-99.47-64.84-136.12-35.82-35.82-81.13-58.05-131.04-64.27-.1
0-.19-.03-.28-.06v.0 [...]
+ </svg>
+ );
+}
+
+export function FilterIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 700 700">
+ <path d="M565.62 156.56L413.36 350.33a10.032 10.032 0 00-2.14
6.18v190.52c0 19.05-25.01 34.49-55.86
34.49s-55.86-15.44-55.86-34.49V356.51c0-2.24-.75-4.42-2.14-6.18L145.1
156.56c-5.15-6.56-.48-16.18 7.87-16.18h404.79c8.34 0 13.02 9.62 7.86
16.18z"></path>
+ </svg>
+ );
+}
+
+export function SortIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 700 700">
+ <path d="M563.24 203.21H271.33c-5.49
0-9.94-4.45-9.94-9.94s4.45-9.94 9.94-9.94h291.91c5.49 0 9.94 4.45 9.94
9.94s-4.45 9.94-9.94 9.94zM516.63 312.59h-245.3c-5.49
0-9.94-4.45-9.94-9.94s4.45-9.94 9.94-9.94h245.3c5.49 0 9.94 4.45 9.94
9.94s-4.45 9.94-9.94 9.94zM470.02 421.98H271.33c-5.49
0-9.94-4.45-9.94-9.94s4.45-9.94 9.94-9.94h198.69c5.49 0 9.94 4.45 9.94
9.94s-4.45 9.94-9.94 9.94zM423.41 531.36H271.33c-5.49
0-9.94-4.45-9.94-9.94s4.45-9.94 9.94-9.94h152.08c5.49 0 9.94 4.45 9.9 [...]
+ <g>
+ <path d="M174 531.35c.19 0 .37 0
.55-.01-.18.02-.36.02-.54.02-1.65 0-3.3-.41-4.76-1.22 1.47.8 3.11 1.2 4.75
1.2zM175.11 531.3c.55-.07 1.1-.18 1.64-.33-.54.16-1.09.27-1.64.33zM178.34
530.36c-.51.26-1.04.47-1.59.61.54-.15 1.08-.36 1.59-.61z"></path>
+ </g>
+ <g>
+ <path d="M219.05 485.88c3.39 4.31 2.65 10.56-1.65 13.96l-37.24
29.38c-.27.21-.55.41-.84.58-.16.12-.33.21-.49.29-.16.11-.33.19-.49.26-.51.25-1.05.46-1.59.61-.54.15-1.09.26-1.64.33-.19.01-.37.04-.56.05-.18.01-.36.01-.55.01-1.64
0-3.27-.41-4.75-1.2h-.01c-.23-.14-.47-.28-.7-.43-.23-.15-.47-.32-.69-.49-.07-.05-.13-.09-.19-.14l-37.06-29.24c-4.3-3.4-5.04-9.65-1.64-13.96
3.39-4.3 9.65-5.04 13.95-1.64l21.15 16.67V199.08l-21.15 16.68a9.866 9.866 0
01-6.15 2.13c-2.93 0-5.85-1.3-7.8- [...]
+ <path d="M169.24
169.85s.02-.01.04-.01h-.01c-.06.05-.13.08-.19.12-.19.09-.36.2-.53.32.22-.15.46-.29.69-.42zM170.45
169.3s.09-.04.14-.05c-.15.05-.29.11-.43.16.09-.05.19-.08.29-.12zM171.83
168.88c.06-.02.11-.04.16-.04-.32.07-.63.15-.94.25.11-.04.21-.07.33-.11.15-.04.29-.07.44-.11zM174.03
168.63c.16 0 .32 0 .48.02-.18-.01-.34-.01-.51-.01s-.34
0-.51.01c-.16.01-.33.02-.5.04.07-.01.14-.01.21-.02.26-.02.51-.04.77-.04h.07zM174.81
168.67c.07.01.14.01.21.02-.16-.01-.34-.02-.5-.04.0 [...]
+ </g>
+ <g>
+ <path d="M169.24 169.85s.01-.01.02
0c-.06.04-.13.07-.19.11.05-.05.11-.07.16-.11zM170.45
169.3c.2-.08.4-.15.61-.21-.16.05-.32.09-.47.16-.15.05-.29.11-.43.16.09-.05.19-.08.29-.12zM171.83
168.88c.06-.02.11-.04.16-.04-.32.07-.63.15-.94.25.11-.04.21-.07.33-.11.15-.04.29-.07.44-.11zM172
168.85c.22-.06.46-.09.68-.12-.22.02-.44.06-.65.12h-.02zM173.19
168.67c.09-.01.2-.01.29-.01-.16.01-.33.02-.5.04.07-.01.14-.01.21-.02zM174.81
168.67c.07.01.14.01.21.02-.16-.01-.34-.02-.5-.04-.18-. [...]
+ </g>
+ </svg>
+ );
}
\ No newline at end of file
diff --git a/karavan-designer/src/index.tsx b/karavan-designer/src/index.tsx
index a39991f..b8d7e70 100644
--- a/karavan-designer/src/index.tsx
+++ b/karavan-designer/src/index.tsx
@@ -33,6 +33,7 @@ render(
<Route path="kamelets-page" element={<App page="kamelets"/>} />
<Route path="components-page" element={<App page="components"/>} />
<Route path="eip-page" element={<App page="eip"/>} />
+ <Route path="builder" element={<App page="builder"/>} />
</Routes>
</BrowserRouter>,
rootElement
diff --git a/karavan-generator/pom.xml b/karavan-generator/pom.xml
index 845333c..3628a62 100644
--- a/karavan-generator/pom.xml
+++ b/karavan-generator/pom.xml
@@ -74,6 +74,11 @@
<artifactId>camel-kamelets-catalog</artifactId>
<version>${version.camel-kamelet}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-jbang-core</artifactId>
+ <version>3.17.0-SNAPSHOT</version>
+ </dependency>
</dependencies>
<build>
<resources>
diff --git
a/karavan-generator/src/main/java/org/apache/camel/karavan/generator/CamelJbangGenerator.java
b/karavan-generator/src/main/java/org/apache/camel/karavan/generator/CamelJbangGenerator.java
new file mode 100644
index 0000000..0eed94b
--- /dev/null
+++
b/karavan-generator/src/main/java/org/apache/camel/karavan/generator/CamelJbangGenerator.java
@@ -0,0 +1,70 @@
+/*
+ * 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.generator;
+
+import org.apache.camel.dsl.jbang.core.commands.Build;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.CodeGenerator;
+import org.apache.camel.dsl.jbang.core.commands.CodeRestGenerator;
+import org.apache.camel.dsl.jbang.core.commands.Deploy;
+import org.apache.camel.dsl.jbang.core.commands.Image;
+import org.apache.camel.dsl.jbang.core.commands.Manifest;
+import org.apache.camel.dsl.jbang.core.commands.Package;
+import org.apache.camel.dsl.jbang.core.commands.Run;
+import org.apache.camel.dsl.jbang.core.commands.UberJar;
+import org.apache.camel.dsl.jbang.core.commands.Undeploy;
+import picocli.CommandLine;
+
+public final class CamelJbangGenerator extends AbstractGenerator {
+
+ final static String modelHeader =
"karavan-generator/src/main/resources/CamelMetadata.header.ts";
+ final static String targetModel =
"karavan-core/src/core/model/CamelMetadata.ts";
+
+ public static void main(String[] args) throws Exception {
+ CamelJbangGenerator.generate();
+ System.exit(0);
+ }
+
+ public static void generate() throws Exception {
+ CamelJbangGenerator g = new CamelJbangGenerator();
+ g.createJbangDefinitions();
+ }
+
+ private void createJbangDefinitions() throws Exception {
+ StringBuilder camelModel = new StringBuilder();
+ camelModel.append(readFileText(modelHeader));
+
+
+ CommandLine commandLine = new CommandLine(new CamelJBangMain())
+ .addSubcommand("run", new CommandLine(new Run()))
+ .addSubcommand("package", new CommandLine(new Package())
+ .addSubcommand("uber-jar", new UberJar()))
+ .addSubcommand("generate", new CommandLine(new CodeGenerator())
+ .addSubcommand("rest", new CodeRestGenerator()))
+ .addSubcommand("build", new CommandLine(new Build())
+ .addSubcommand("manifests", new Manifest())
+ .addSubcommand("image", new Image()))
+ .addSubcommand("deploy", new CommandLine(new Deploy()))
+ .addSubcommand("undeploy", new CommandLine(new Undeploy()));
+
+
+
+ writeFileText(targetModel, camelModel.toString());
+ }
+
+
+}
diff --git a/karavan-vscode/icons/dark/builder.svg
b/karavan-vscode/icons/dark/builder.svg
new file mode 100644
index 0000000..a1e57ce
--- /dev/null
+++ b/karavan-vscode/icons/dark/builder.svg
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="32px"
+ height="32px"
+ viewBox="0 0 32 32"
+ id="icon"
+ version="1.1"
+ sodipodi:docname="builder.svg"
+ inkscape:version="1.1.2 (b8e25be8, 2022-02-05)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <sodipodi:namedview
+ id="namedview834"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ inkscape:zoom="22.46875"
+ inkscape:cx="13.79694"
+ inkscape:cy="16.534075"
+ inkscape:window-width="1312"
+ inkscape:window-height="969"
+ inkscape:window-x="72"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="icon" />
+ <defs
+ id="defs826">
+ <style
+ id="style824">.cls-1{fill:none;}</style>
+ </defs>
+ <title
+ id="title828">tools</title>
+ <path
+
d="M12.1,2A9.8,9.8,0,0,0,6.7,3.6L13.1,10a2.1,2.1,0,0,1,.2,3,2.1,2.1,0,0,1-3-.2L3.7,6.4A9.84,9.84,0,0,0,2,12.1,10.14,10.14,0,0,0,12.1,22.2a10.9,10.9,0,0,0,2.6-.3l6.7,6.7a5,5,0,0,0,7.1-7.1l-6.7-6.7a10.9,10.9,0,0,0,.3-2.6A10,10,0,0,0,12.1,2Zm8,10.1a7.61,7.61,0,0,1-.3,2.1l-.3,1.1.8.8L27,22.8a2.88,2.88,0,0,1,.9,2.1A2.72,2.72,0,0,1,27,27a2.9,2.9,0,0,1-4.2,0l-6.7-6.7-.8-.8-1.1.3a7.61,7.61,0,0,1-2.1.3,8.27,8.27,0,0,1-5.7-2.3A7.63,7.63,0,0,1,4,12.1a8.33,8.33,0,0,1,.3-2.2l4.4,4.4a4.14,4.14,0,
[...]
+ id="path830"
+ style="stroke:#c5c5c5;stroke-opacity:1;fill:#c5c5c5;fill-opacity:1" />
+ <rect
+ id="_Transparent_Rectangle_"
+ data-name="<Transparent Rectangle>"
+ class="cls-1"
+ width="32"
+ height="32" />
+ <metadata
+ id="metadata916">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:title>tools</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+</svg>
diff --git a/karavan-vscode/icons/light/builder.svg
b/karavan-vscode/icons/light/builder.svg
new file mode 100644
index 0000000..16574e1
--- /dev/null
+++ b/karavan-vscode/icons/light/builder.svg
@@ -0,0 +1 @@
+<svg width="32px" height="32px" viewBox="0 0 32 32" id="icon"
xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><title>tools</title><path
d="M12.1,2A9.8,9.8,0,0,0,6.7,3.6L13.1,10a2.1,2.1,0,0,1,.2,3,2.1,2.1,0,0,1-3-.2L3.7,6.4A9.84,9.84,0,0,0,2,12.1,10.14,10.14,0,0,0,12.1,22.2a10.9,10.9,0,0,0,2.6-.3l6.7,6.7a5,5,0,0,0,7.1-7.1l-6.7-6.7a10.9,10.9,0,0,0,.3-2.6A10,10,0,0,0,12.1,2Zm8,10.1a7.61,7.61,0,0,1-.3,2.1l-.3,1.1.8.8L27,22.8a2.88,2.88,0,0,1,.9,2.1A2.72,2.72,0
[...]
\ No newline at end of file
diff --git a/karavan-vscode/package.json b/karavan-vscode/package.json
index e163ae3..830e325 100644
--- a/karavan-vscode/package.json
+++ b/karavan-vscode/package.json
@@ -53,7 +53,8 @@
"onCommand:karavan.openKamelets",
"onCommand:karavan.openComponents",
"onCommand:karavan.openEip",
- "onCommand:karavan.reportIssue"
+ "onCommand:karavan.reportIssue",
+ "onCommand:karavan.projectBuilder"
],
"main": "./dist/extension.js",
"contributes": {
@@ -119,7 +120,7 @@
},
{
"command": "karavan.create-yaml",
- "title": "Karavan: Create YAML"
+ "title": "Karavan: Create Integration"
},
{
"command": "karavan.open",
@@ -133,6 +134,14 @@
"command": "karavan.open-file",
"title": "Karavan: Open editor"
},
+ {
+ "command": "karavan.projectBuilder",
+ "title": "Karavan: Project Builder",
+ "icon": {
+ "light": "icons/light/builder.svg",
+ "dark": "icons/dark/builder.svg"
+ }
+ },
{
"command": "karavan.jbang-run",
"title": "Karavan: JBang Run",
@@ -199,6 +208,9 @@
{
"command": "karavan.generate-rest",
"when": "resourceExtname == .json"
+ },
+ {
+ "command": "karavan.projectBuilder"
}
],
"editor/title": [
@@ -224,6 +236,11 @@
"when": "view == integrations",
"group": "navigation"
},
+ {
+ "command": "karavan.projectBuilder",
+ "when": "view == integrations",
+ "group": "navigation"
+ },
{
"command": "openapi.refresh",
"when": "view == openapi",
@@ -289,7 +306,7 @@
]
},
"scripts": {
- "copy-designer": "cp -r ../karavan-designer/src/designer webview && cp -r
../karavan-designer/src/kamelets webview && cp -r
../karavan-designer/src/components webview && cp -r ../karavan-designer/src/eip
webview",
+ "copy-designer": "cp -r ../karavan-designer/src/designer webview && cp -r
../karavan-designer/src/kamelets webview && cp -r
../karavan-designer/src/components webview && cp -r ../karavan-designer/src/eip
webview && cp -r ../karavan-designer/src/builder webview",
"vscode:prepublish": "npm run copy-designer && npm run package",
"compile": "npm run copy-designer && cross-env NODE_ENV=development
webpack --progress",
"watch": "npm run copy-designer && cross-env NODE_ENV=development webpack
--progress --watch",
diff --git a/karavan-vscode/src/builderView.ts
b/karavan-vscode/src/builderView.ts
new file mode 100644
index 0000000..63c972d
--- /dev/null
+++ b/karavan-vscode/src/builderView.ts
@@ -0,0 +1,208 @@
+/*
+ * 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 * as vscode from "vscode";
+import * as fs from "fs";
+import * as path from "path";
+import * as utils from "./utils";
+import * as commands from "./commands";
+import { ProjectModel } from "karavan-core/lib/model/ProjectModel";
+import { ProjectModelApi } from "karavan-core/lib/api/ProjectModelApi";
+
+let builderPanel: vscode.WebviewPanel | undefined;
+const filename = "application.properties";
+
+export class BuilderView {
+
+ constructor(private context: vscode.ExtensionContext, private
webviewContent: string, private rootPath?: string) {
+
+ }
+
+ openProject() {
+ if (builderPanel === undefined) {
+ // Karavan webview
+ builderPanel = vscode.window.createWebviewPanel(
+ "karavan",
+ "Builder",
+ vscode.ViewColumn.One,
+ {
+ enableScripts: true,
+ retainContextWhenHidden: true,
+ localResourceRoots: [
+ vscode.Uri.joinPath(this.context.extensionUri, "dist"),
+ ],
+ }
+ );
+ builderPanel.webview.html = this.webviewContent;
+ builderPanel.iconPath = vscode.Uri.joinPath(
+ this.context.extensionUri,
+ "icons/karavan.svg"
+ );
+
+ // Handle messages from the webview
+ builderPanel.webview.onDidReceiveMessage(
+ message => {
+ switch (message.command) {
+ case 'getData':
+ this.sendData("builder");
+ break;
+ case 'saveProject':
+ this.saveProject(message.project);
+ break;
+ case 'action':
+ this.actionProject(message.action,
message.project);
+ break;
+ }
+ },
+ undefined,
+ this.context.subscriptions
+ );
+ // Handle close event
+ builderPanel.onDidDispose(() => {
+ builderPanel = undefined;
+ console.log("dispose");
+ }, null, this.context.subscriptions);
+
+ // Handle reopen
+ builderPanel.onDidChangeViewState((e:
vscode.WebviewPanelOnDidChangeViewStateEvent) => {
+ if (e.webviewPanel.active) {
+ e.webviewPanel.webview.postMessage({ command: 'reread' })
+ }
+ });
+ } else {
+ builderPanel?.reveal(undefined, true);
+ builderPanel?.webview.postMessage({ command: 'activate' });
+ }
+ }
+
+ sendData(page: string) {
+ builderPanel?.webview.postMessage({ command: 'open', page: page });
+ console.log(this.rootPath);
+ if (this.rootPath) {
+ const [project, files] = this.readProjectInfo(this.rootPath);
+ // Send data
+ builderPanel?.webview.postMessage({ command: 'project', files:
files, project: project });
+ }
+ }
+
+ actionProject(action: "start" | "stop" | "undeploy", project:
ProjectModel) {
+ switch (action){
+ case "start" : this.start(project); break;
+ case "stop" : {}; break;
+ case "undeploy" : this.undelpoy(project); break;
+ }
+ }
+
+ start(project: ProjectModel) {
+ const [x, files] = this.readProjectInfo(this.rootPath || '');
+ project.status.active = true;
+ project.status.uberJar = "pending";
+ project.status.build = "pending";
+ project.status.deploy = "pending";
+ project.status.undeploy = "pending";
+ if (project.uberJar) {
+ project.status.uberJar = "progress";
+ builderPanel?.webview.postMessage({ command: 'project', files:
files, project: project });
+ this.package(project, files);
+ }
+ }
+
+ package(project: ProjectModel, files: string) {
+ console.log("package", project);
+ project.status.uberJar = "progress";
+ builderPanel?.webview.postMessage({ command: 'project', files: files,
project: project });
+
+ commands.camelJbangPackage(this.rootPath || "", code => {
+ project.status.uberJar = code === 0 ? "done" : "error";
+ builderPanel?.webview.postMessage({ command: 'project', files:
files, project: project });
+ if (code === 0 && project.build) {
+ this.buildImage(project, files);
+ } else {
+ this.finish(project, files, code);
+ }
+ });
+ }
+
+ buildImage(project: ProjectModel, files: string) {
+ console.log("buildImage", project);
+ project.status.build = "progress";
+ builderPanel?.webview.postMessage({ command: 'project', files: files,
project: project });
+
+ commands.camelJbangBuildImage(this.rootPath || "", project, code => {
+ project.status.build = code === 0 ? "done" : "error";
+ builderPanel?.webview.postMessage({ command: 'project', files:
files, project: project });
+ if (code === 0 && project.deploy) {
+ this.deploy(project, files);
+ } else {
+ this.finish(project, files, code);
+ }
+ });
+ }
+
+ deploy(project: ProjectModel, files: string) {
+ console.log("deploy", project);
+ project.status.deploy = "progress";
+ builderPanel?.webview.postMessage({ command: 'project', files: files,
project: project });
+
+ commands.camelJbangDeploy(this.rootPath || "", project, code => {
+ project.status.deploy = code === 0 ? "done" : "error";
+ builderPanel?.webview.postMessage({ command: 'project', files:
files, project: project });
+ this.finish(project, files, code);
+ });
+ }
+
+
+ finish(project: ProjectModel, files: string, code: number) {
+ console.log("finish", project);
+ if (project.cleanup) commands.cleanup(this.rootPath || "", project, ()
=> {})
+ setTimeout(() => {
+ project.status.active = false;
+ builderPanel?.webview.postMessage({ command: 'project', files:
files, project: project });
+ }, 1000);
+ }
+
+ saveProject(project: ProjectModel) {
+ let properties = ''
+ try {
+ properties = fs.readFileSync(path.resolve(this.rootPath || '',
filename)).toString('utf8');
+ } catch (err: any) {
+ if (err.code !== 'ENOENT') throw err;
+ }
+ const newProperties = ProjectModelApi.updateProperties(properties,
project);
+ utils.save(filename, newProperties);
+ }
+
+ readProjectInfo(rootPath: string): [ProjectModel, string] {
+ const files = utils.getAllFiles(rootPath, []).map(f =>
utils.getRalativePath(f)).join(",");
+ let project = ProjectModel.createNew();
+ try {
+ const properties = fs.readFileSync(path.resolve(rootPath,
filename)).toString('utf8');
+ project = ProjectModelApi.propertiesToProject(properties);
+ } catch (err: any) {
+ if (err.code !== 'ENOENT') throw err;
+ }
+ return [project, files];
+ }
+
+ undelpoy(project: ProjectModel) {
+ const [x, files] = this.readProjectInfo(this.rootPath || '');
+ console.log("undelpoy", project);
+ project.status.active = true;
+ project.status.undeploy = "progress";
+ builderPanel?.webview.postMessage({ command: 'project', files: files,
project: project });
+ commands.camelJbangUndeploy(this.rootPath || '', project, (code) =>
this.finish(project, files, code));
+ }
+}
diff --git a/karavan-vscode/src/commands.ts b/karavan-vscode/src/commands.ts
new file mode 100644
index 0000000..6be1b06
--- /dev/null
+++ b/karavan-vscode/src/commands.ts
@@ -0,0 +1,157 @@
+/*
+ * 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 * as vscode from "vscode";
+import * as fs from "fs";
+import * as path from "path";
+import * as shell from 'shelljs';
+import { CamelDefinitionYaml } from "karavan-core/lib/api/CamelDefinitionYaml";
+import { ProjectModel } from "karavan-core/lib/model/ProjectModel";
+
+export function camelJbangGenerate(rootPath: string, openApiFullPath: string,
fullPath: string, add: boolean, crd?: boolean, generateRoutes?: boolean) {
+ let command = prepareCommand("generate rest -i " + openApiFullPath);
+ if (generateRoutes === true) command = command + " --routes";
+ executeJbangCommand(rootPath, command, (code, stdout, stderr) => {
+ console.log('Exit code:', code);
+ if (code === 0) {
+ // console.log('Program output:', stdout);
+ const filename = path.basename(fullPath);
+ let yaml;
+ if (add) {
+ const camelYaml =
fs.readFileSync(path.resolve(fullPath)).toString('utf8');
+ yaml = createYaml(filename, stdout, camelYaml, undefined);
+ } else {
+ yaml = createYaml(filename, stdout, undefined, crd);
+ }
+ const uriFile: vscode.Uri = vscode.Uri.file(fullPath);
+ fs.writeFile(uriFile.fsPath, yaml, err => {
+ if (err) vscode.window.showErrorMessage("Error: " +
err?.message);
+ else {
+ vscode.commands.executeCommand('integrations.refresh')
+ .then(x =>
vscode.commands.executeCommand('karavan.open', { fsPath: fullPath, tab: 'rest'
}));
+ }
+ });
+ } else {
+ vscode.window.showErrorMessage(stderr);
+ }
+ });
+}
+
+export function createYaml(filename: string, restYaml: string, camelYaml?:
string, crd?: boolean): string {
+ if (camelYaml) {
+ const i = CamelDefinitionYaml.yamlToIntegration(filename, camelYaml);
+ const rest = CamelDefinitionYaml.yamlToIntegration(filename, restYaml);
+ i.spec.flows = i.spec.flows?.filter(f => f.dslName !==
'RestDefinition');
+ i.spec.flows?.push(...rest.spec.flows || []);
+ return CamelDefinitionYaml.integrationToYaml(i);
+ } else if (crd === true) {
+ const i = CamelDefinitionYaml.yamlToIntegration(filename, restYaml);
+ i.crd = true;
+ return CamelDefinitionYaml.integrationToYaml(i);
+ } else {
+ return restYaml;
+ }
+}
+
+export function camelJbangPackage(rootPath: string, callback: (code: number)
=> any) {
+ executeJbangCommand(rootPath, prepareCommand("package uber-jar"), (code,
stdout, stderr) => callback(code));
+}
+
+export function camelJbangBuildImage(rootPath: string, project: ProjectModel,
callback: (code: number) => any) {
+ const munikubeCommand = "minikube -p minikube docker-env";
+ let command = prepareCommand("build image", project);
+ if (project.target === 'minikube') {
+ console.log("Build in minikube")
+ executeJbangCommand(rootPath, munikubeCommand, (code, stdout, stderr)
=> {
+ if (code === 0) {
+ setMinikubeEnvVariables(stdout).forEach((value: string, key:
string) => shell.env[key] = value);
+ executeJbangCommand(rootPath, command, (code, stdout, stderr)
=> callback(code));
+ }
+ })
+ } else {
+ removeMinikubeEnvVariables();
+ console.log(shell.env)
+ executeJbangCommand(rootPath, command, (code, stdout, stderr) =>
callback(code));
+ }
+}
+
+export function camelJbangDeploy(rootPath: string, project: ProjectModel,
callback: (code: number) => any) {
+ executeJbangCommand(rootPath, prepareCommand("deploy", project), (code,
stdout, stderr) => callback(code));
+}
+
+export function camelJbangManifests(rootPath: string, project: ProjectModel,
callback: (code: number) => any) {
+ executeJbangCommand(rootPath, prepareCommand("build manifests", project),
(code, stdout, stderr) => callback(code));
+}
+
+
+export function camelJbangUndeploy(rootPath: string, project: ProjectModel,
callback: (code: number) => any) {
+ executeJbangCommand(rootPath, prepareCommand("undeploy", project), (code,
stdout, stderr) => callback(code));
+}
+
+export function cacheClear(rootPath: string, callback: (code: number) => any) {
+ executeJbangCommand(rootPath, "jbang cache clear", (code, stdout, stderr)
=> callback(code));
+}
+
+function prepareCommand(command: string, project?: ProjectModel): string {
+ const version = vscode.workspace.getConfiguration().get("camel.version");
+ const token = project && project.target === 'openshift' ? " --token " +
project.token : "";
+ return "jbang -Dcamel.jbang.version=" + version + " camel@apache/camel " +
command + token;
+}
+
+function executeJbangCommand(rootPath: string, command: string, callback:
(code: number, stdout: any, stderr: any) => any) {
+ const jbang = shell.which('jbang');
+ if (jbang) {
+ shell.config.execPath = String(jbang);
+ shell.cd(rootPath);
+ shell.exec(command, { async: false }, (code, stdout, stderr) => {
+ if (code === 0) {
+ // vscode.window.showInformationMessage(stdout);
+ } else {
+ vscode.window.showErrorMessage(stderr);
+ }
+ callback(code, stdout, stderr);
+ });
+ } else {
+ vscode.window.showErrorMessage("JBang not found!");
+ }
+}
+
+function setMinikubeEnvVariables(env: string): Map<string, string> {
+ const map = new Map<string, string>();
+ const linesAll = env.split(/\r?\n/);
+ const vars = linesAll.filter(l => l !== undefined &&
l.startsWith("export")).map(line => line.replace("export", ""));
+ vars.forEach(line => {
+ const parts = line.split("=");
+ const key = parts[0].trim();
+ const value = parts[1].replaceAll('"', '').trim();
+ map.set(key, value);
+ })
+ return map;
+}
+
+function removeMinikubeEnvVariables() {
+ delete shell.env['DOCKER_TLS_VERIFY'];
+ delete shell.env['DOCKER_HOST'];
+ delete shell.env['DOCKER_CERT_PATH'];
+ delete shell.env['MINIKUBE_ACTIVE_DOCKERD'];
+}
+
+export function cleanup(rootPath: string, project: ProjectModel, callback:
(code: number, stdout: any, stderr: any) => any) {
+ shell.cd(rootPath);
+ shell.rm('-r', path.resolve(rootPath, ".camel-jbang"));
+ shell.rm(path.resolve(rootPath, project.filename));
+}
+
diff --git a/karavan-vscode/src/extension.ts b/karavan-vscode/src/extension.ts
index 8395223..854688d 100644
--- a/karavan-vscode/src/extension.ts
+++ b/karavan-vscode/src/extension.ts
@@ -20,6 +20,7 @@ import { DesignerView } from "./designerView";
import {IntegrationView} from "./integrationView";
import { HelpView } from "./helpView";
import { selectFileName, inputFileName, OpenApiView, OpenApiItem } from
"./openapiView";
+import { BuilderView } from './builderView';
const KARAVAN_LOADED = "karavan:loaded";
@@ -62,6 +63,9 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('karavan.openComponents', () =>
helpView.openKaravanWebView("components"));
vscode.commands.registerCommand('karavan.openEip', () =>
helpView.openKaravanWebView("eip"));
+ const builderView = new BuilderView(context, webviewContent, rootPath);
+ vscode.commands.registerCommand("karavan.projectBuilder", (...args: any[])
=> builderView.openProject());
+
// Create new Integration CRD command
const createCrd = vscode.commands.registerCommand("karavan.create-crd",
(...args: any[]) => {
if (args.length > 0) designer.createIntegration(true, args[0].fsPath)
diff --git a/karavan-vscode/src/openapiView.ts
b/karavan-vscode/src/openapiView.ts
index eb169dc..f0c34b6 100644
--- a/karavan-vscode/src/openapiView.ts
+++ b/karavan-vscode/src/openapiView.ts
@@ -17,6 +17,7 @@
import vscode, { window } from "vscode";
import * as path from "path";
import * as utils from "./utils";
+import * as commands from "./commands";
import * as fs from "fs";
import { ThemeIcon } from "vscode";
import { CamelDefinitionYaml } from "karavan-core/lib/api/CamelDefinitionYaml";
@@ -74,14 +75,14 @@ export class OpenApiItem extends vscode.TreeItem {
/**
* Select routes generation
*/
-export async function selectRouteGeneration(openApiFullPath: string, fullPath:
string, add: boolean, crd?: boolean) {
+export async function selectRouteGeneration(rootPath: string, openApiFullPath:
string, fullPath: string, add: boolean, crd?: boolean) {
const options = ["Generate REST and Routes", 'Generate REST only'];
await window.showQuickPick(options, {
title: "Generate route stubs for REST API",
placeHolder: 'Select option',
}).then(option => {
const generateRoutes: boolean = option !== undefined && option
=== options[0];
- utils.camelJbangGenerate(openApiFullPath, fullPath, add, crd,
generateRoutes);
+ commands.camelJbangGenerate(rootPath, openApiFullPath,
fullPath, add, crd, generateRoutes);
});
}
@@ -96,7 +97,7 @@ export async function selectFileName(rootPath?: string,
openApi?: OpenApiItem) {
placeHolder: 'Select file',
}).then(fullPath => {
if (fullPath && openApi?.fsPath) {
- selectRouteGeneration(openApi.fsPath, fullPath,
true, undefined);
+ selectRouteGeneration(rootPath, openApi.fsPath,
fullPath, true, undefined);
}
});
}
@@ -118,9 +119,9 @@ export async function inputFileName(crd: boolean,
rootPath?: string, openApi?: O
}
}
}).then(filename => {
- if (filename && openApi?.fsPath) {
+ if (filename && openApi?.fsPath && rootPath) {
const fullPath = rootPath + path.sep + filename;
- selectRouteGeneration(openApi.fsPath, fullPath, false,
crd);
+ selectRouteGeneration(rootPath, openApi.fsPath,
fullPath, false, crd);
}
});
}
\ No newline at end of file
diff --git a/karavan-vscode/src/utils.ts b/karavan-vscode/src/utils.ts
index df000fb..c5c67a3 100644
--- a/karavan-vscode/src/utils.ts
+++ b/karavan-vscode/src/utils.ts
@@ -22,11 +22,11 @@ import { CamelDefinitionYaml } from
"karavan-core/lib/api/CamelDefinitionYaml";
const TERMINALS: Map<string, vscode.Terminal> = new Map<string,
vscode.Terminal>();
-export function save(relativePath: string, yaml: string){
+export function save(relativePath: string, text: string){
if (vscode.workspace.workspaceFolders) {
const uriFolder: vscode.Uri = vscode.workspace.workspaceFolders[0].uri;
const uriFile: vscode.Uri = vscode.Uri.file(path.join(uriFolder.path,
relativePath));
- fs.writeFile(uriFile.fsPath, yaml, err => {
+ fs.writeFile(uriFile.fsPath, text, err => {
if (err) vscode.window.showErrorMessage("Error: " + err?.message);
});
}
@@ -105,7 +105,7 @@ export function nameFromTitle(title: string): string {
return title.replace(/[^a-z0-9+]+/gi, "-").toLowerCase();
}
-function getAllFiles (dirPath, arrayOfFiles: string[]): string[] {
+export function getAllFiles (dirPath, arrayOfFiles: string[]): string[] {
const files = fs.readdirSync(dirPath)
arrayOfFiles = arrayOfFiles || []
@@ -141,56 +141,4 @@ export function getIntegrationFiles(baseDir: string):
string[]{
const yaml = fs.readFileSync(path.resolve(f)).toString('utf8');
return !f.startsWith(baseDir + path.sep + "target") &&
CamelDefinitionYaml.yamlIsIntegration(yaml);
});
-}
-
-export function camelJbangGenerate(openApiFullPath: string, fullPath: string,
add: boolean, crd?: boolean, generateRoutes?: boolean) {
- const version = vscode.workspace.getConfiguration().get("camel.version");
- let command = "jbang -Dcamel.jbang.version=" + version + "
camel@apache/camel generate rest -i " + openApiFullPath;
- if (generateRoutes === true) command = command + " --routes";
- const jbang = shell.which('jbang');
- if (jbang){
- shell.config.execPath = String(jbang);
- shell.exec(command, { async: true }, (code, stdout, stderr) => {
- console.log('Exit code:', code);
- if (code === 0){
- // console.log('Program output:', stdout);
- const filename = path.basename(fullPath);
- let yaml;
- if (add) {
- const camelYaml =
fs.readFileSync(path.resolve(fullPath)).toString('utf8');
- yaml = createYaml(filename, stdout, camelYaml, undefined);
- } else {
- yaml = createYaml(filename, stdout, undefined, crd);
- }
- const uriFile: vscode.Uri = vscode.Uri.file(fullPath);
- fs.writeFile(uriFile.fsPath, yaml, err => {
- if (err) vscode.window.showErrorMessage("Error: " +
err?.message);
- else {
- vscode.commands.executeCommand('integrations.refresh')
- .then(x =>
vscode.commands.executeCommand('karavan.open', {fsPath: fullPath, tab:
'rest'}));
- }
- });
- } else {
- vscode.window.showErrorMessage(stderr);
- }
- });
- }
-}
-
-export function createYaml(filename: string, restYaml: string, camelYaml?:
string, crd?: boolean): string {
- if (camelYaml) {
- const i = CamelDefinitionYaml.yamlToIntegration(filename, camelYaml);
- const rest = CamelDefinitionYaml.yamlToIntegration(filename, restYaml);
- i.spec.flows = i.spec.flows?.filter(f => f.dslName !==
'RestDefinition');
- i.spec.flows?.push(...rest.spec.flows || []);
- return CamelDefinitionYaml.integrationToYaml(i);
- } else if (crd === true){
- const i = CamelDefinitionYaml.yamlToIntegration(filename, restYaml);
- i.crd = true;
- return CamelDefinitionYaml.integrationToYaml(i);
- } else {
- return restYaml;
- }
-}
-
-
+}
\ No newline at end of file
diff --git a/karavan-vscode/webview/App.tsx b/karavan-vscode/webview/App.tsx
index b9b418a..e3cd816 100644
--- a/karavan-vscode/webview/App.tsx
+++ b/karavan-vscode/webview/App.tsx
@@ -25,6 +25,8 @@ import { ComponentApi } from
"karavan-core/lib/api/ComponentApi";
import { KameletsPage } from "./kamelets/KameletsPage";
import { ComponentsPage } from "./components/ComponentsPage";
import { EipPage } from "./eip/EipPage";
+import { BuilderPage } from "./builder/BuilderPage";
+import { ProjectModel } from "karavan-core/lib/model/ProjectModel";
interface Props {
dark: boolean
@@ -40,9 +42,11 @@ interface State {
scheduledYaml: string
hasChanges: boolean
showStartHelp: boolean
- page: "designer" | "kamelets" | "components" | "eip"
+ page: "designer" | "kamelets" | "components" | "eip" | "builder"
active: boolean
tab?: string
+ files: string
+ project: ProjectModel
}
class App extends React.Component<Props, State> {
@@ -57,7 +61,9 @@ class App extends React.Component<Props, State> {
hasChanges: false,
showStartHelp: false,
page: "designer",
- active: false
+ active: false,
+ files: '',
+ project: ProjectModel.createNew()
};
saveScheduledChanges = () => {
@@ -91,6 +97,10 @@ class App extends React.Component<Props, State> {
case 'showStartHelp':
this.setState({ showStartHelp: message.showStartHelp });
break;
+ case 'project':
+ this.setState({ project: message.project, files: message.files, key:
Math.random().toString() });
+ console.log(message.project)
+ break;
case 'open':
if (this.state.filename === '' && this.state.key === '') {
if (message.page !== "designer" && this.state.interval)
clearInterval(this.state.interval);
@@ -102,7 +112,7 @@ class App extends React.Component<Props, State> {
relativePath: message.relativePath,
key: Math.random().toString(),
loaded: true,
- active: true,
+ active: true,
tab: message.tab
});
}
@@ -128,6 +138,14 @@ class App extends React.Component<Props, State> {
}
}
+ saveProject(project: ProjectModel) {
+ vscode.postMessage({ command: 'saveProject', project: project });
+ }
+
+ actionProject(action: "start" | "stop" | "undeploy", project: ProjectModel) {
+ vscode.postMessage({ command: 'action', action: action, project: project
});
+ }
+
disableStartHelp() {
vscode.postMessage({ command: 'disableStartHelp' });
}
@@ -154,6 +172,11 @@ class App extends React.Component<Props, State> {
{this.state.loaded && this.state.page === "kamelets" && <KameletsPage
dark={this.props.dark} />}
{this.state.loaded && this.state.page === "components" &&
<ComponentsPage dark={this.props.dark} />}
{this.state.loaded && this.state.page === "eip" && <EipPage
dark={this.props.dark} />}
+ {this.state.loaded && this.state.page === "builder" &&
+ <BuilderPage key={this.state.key} dark={this.props.dark}
files={this.state.files} project={this.state.project}
+ onChange={project => this.saveProject(project)}
+ onAction={(action, project) => this.actionProject(action, project)}
+ />}
</Page>
)
}
diff --git a/karavan-vscode/webview/builder/BuilderPage.tsx
b/karavan-vscode/webview/builder/BuilderPage.tsx
new file mode 100644
index 0000000..903b909
--- /dev/null
+++ b/karavan-vscode/webview/builder/BuilderPage.tsx
@@ -0,0 +1,326 @@
+import React from 'react';
+import {
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+ TextInput,
+ PageSection,
+ TextContent,
+ Text,
+ PageSectionVariants,
+ Flex,
+ FlexItem,
+ Badge,
+ Button,
+ FormGroup,
+ Form,
+ Card,
+ CardTitle,
+ CardBody,
+ CardFooter,
+ CardHeader,
+ CardHeaderMain,
+ CardActions,
+ Checkbox,
+ Switch,
+ ToggleGroup,
+ ToggleGroupItem,
+ PopoverPosition,
+ Popover,
+ InputGroup,
+ ProgressStep,
+ ProgressStepper,
+ Spinner,
+ HelperTextItem, HelperText
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon";
+import InProgressIcon from
'@patternfly/react-icons/dist/esm/icons/in-progress-icon';
+import AutomationIcon from
'@patternfly/react-icons/dist/esm/icons/bundle-icon';
+import PendingIcon from '@patternfly/react-icons/dist/esm/icons/pending-icon';
+import ExclamationCircleIcon from
'@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
+import CheckCircleIcon from
'@patternfly/react-icons/dist/esm/icons/check-circle-icon';
+import RunIcon from '@patternfly/react-icons/dist/esm/icons/forward-icon';
+import StopIcon from '@patternfly/react-icons/dist/esm/icons/stop-icon';
+import JarIcon from '@patternfly/react-icons/dist/esm/icons/hotjar-icon';
+import ImageIcon from '@patternfly/react-icons/dist/esm/icons/docker-icon';
+import DeployIcon from
'@patternfly/react-icons/dist/esm/icons/cloud-upload-alt-icon';
+import CleanupIcon from '@patternfly/react-icons/dist/esm/icons/remove2-icon';
+import ProjectIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
+import {FileSelector} from "./FileSelector";
+import {ProjectModel, ProjectStatus} from
"karavan-core/lib/model/ProjectModel";
+
+interface Props {
+ dark: boolean
+ project: ProjectModel
+ files: string
+ onChange?: (project: ProjectModel) => void
+ onAction?: (action: "start" | "stop" | "undeploy", project: ProjectModel)
=> void
+}
+
+interface State {
+ name: string,
+ version: string,
+ filename: string,
+ namespace: string,
+ tag?: string,
+ sourceImage: string,
+ from: string,
+ replicas: number,
+ nodePort: number,
+ server?: string,
+ token?: string,
+ target: 'openshift' | 'minikube' | 'kubernetes',
+ deploy: boolean,
+ build: boolean,
+ uberJar: boolean,
+ routesIncludePattern: string,
+ classpathFiles: string,
+ status: ProjectStatus,
+ cleanup: boolean,
+ manifests: boolean,
+ path: string,
+}
+
+export class BuilderPage extends React.Component<Props, State> {
+
+ public state: State = this.props.project;
+
+ componentDidUpdate = (prevProps: Readonly<Props>, prevState:
Readonly<State>, snapshot?: any) => {
+ this.props.onChange?.call(this, this.state);
+ }
+
+ getHelp(text: string) {
+ return <Popover
+ aria-label={text}
+ position={PopoverPosition.left}
+ bodyContent={text}>
+ <Button variant="plain" onClick={e => {
+ }}>
+ <HelpIcon/>
+ </Button>
+ </Popover>
+ }
+
+ getField(name: string, label: string, type: 'text' | 'date' |
'datetime-local' | 'email' | 'month' | 'number' | 'password' | 'search' | 'tel'
| 'time' | 'url',
+ value: any, help: string, onChange: (val: any) => void,
isRequired: boolean = false, enabled: boolean = true) {
+ return <FormGroup label={label} fieldId={name} isRequired={isRequired}>
+ <InputGroup>
+ <TextInput isRequired={isRequired} isDisabled={!enabled}
className="text-field" type={type} id={name} name={name} value={value}
+ onChange={val => onChange?.call(this, val)}/>
+ {this.getHelp(help)}
+ </InputGroup>
+ </FormGroup>
+ }
+
+ getCardHeader(title: string, icon: any, optional: boolean = true, checked:
boolean = false, onCheck?: (check: boolean) => void) {
+ return <CardHeader>
+ <CardHeaderMain>
+ <CardTitle className="card-header">
+ {icon}{title}
+ </CardTitle>
+ </CardHeaderMain>
+ <CardActions hasNoOffset={true}>
+ {optional && <Switch isChecked={checked} onChange={checked =>
onCheck?.call(this, checked)} id={title} name={title} aria-label={"xxx"}/>}
+ </CardActions>
+ </CardHeader>
+ }
+
+ getProjectForm() {
+ return (
+ <Card className="builder-card" isCompact style={{width: "100%"}}>
+ {this.getCardHeader("Project", <ProjectIcon/>, false)}
+ <CardBody>
+ <Form isHorizontal>
+ {this.getField("name", "Name", "text",
this.state.name, "Project name", val => this.setState({name: val}), true)}
+ {this.getField("version", "Version", "text",
this.state.version, "Project version", val => this.setState({version: val}),
true)}
+ </Form>
+ </CardBody>
+ </Card>
+ )
+ }
+
+ getPackageForm() {
+ const {uberJar, classpathFiles, routesIncludePattern} = this.state;
+ return <Card className="builder-card" isCompact style={{width:
"100%"}}>
+ {this.getCardHeader("Package", <JarIcon/>, true,
this.state.uberJar, check => this.setState({uberJar: check}))}
+ <CardBody className={uberJar ? "" : "card-disabled"}>
+ <Form isHorizontal>
+ {this.getField("filename", "Jar", "text",
this.state.filename, "Jar file name", val => this.setState({filename: val}),
true, uberJar)}
+ {this.props.files.length >0 &&
+ <FileSelector source={true} label="Route/source files"
help="Routes and source to build and package" files={this.props.files}
filesSelected={routesIncludePattern} onChange={filesSelected =>
this.setState({routesIncludePattern: filesSelected})}/>}
+ {this.props.files.length >0 &&
+ <FileSelector source={false} label="Resources"
help="Files to package as resources" files={this.props.files}
filesSelected={classpathFiles} onChange={filesSelected =>
this.setState({classpathFiles: filesSelected})}/>}
+ </Form>
+ </CardBody>
+ </Card>
+ }
+
+ getBuildForm() {
+ const {target, namespace, build, tag, sourceImage, server, token,
from} = this.state;
+ return <Card className="builder-card" isCompact style={{width:
"100%"}}>
+ {this.getCardHeader("Build", <ImageIcon/>, true, this.state.build,
check => this.setState({build: check}))}
+ <CardBody className={build ? "" : "card-disabled"}>
+ <Form isHorizontal>
+ <FormGroup label="Target" fieldId="tag" isRequired
disabled={true}>
+ <ToggleGroup aria-label="Select target">
+ <ToggleGroupItem isDisabled={!build}
text="Minikube" buttonId="minikube" isSelected={target === 'minikube'}
+ onChange={selected => selected ?
this.setState({target: 'minikube'}) : {}}/>
+ <ToggleGroupItem isDisabled={!build}
text="Kubernetes" buttonId="kubernetes" isSelected={target === 'kubernetes'}
+ onChange={selected => selected ?
this.setState({target: 'kubernetes'}) : {}}/>
+ <ToggleGroupItem isDisabled={!build}
text="Openshift" buttonId="openshift" isSelected={target === 'openshift'}
+ onChange={selected => selected ?
this.setState({target: 'openshift'}) : {}}/>
+ </ToggleGroup>
+ </FormGroup>
+ {this.getField("namespace", "Namespace", "text",
namespace, "Namespace to build and/or deploy", val => this.setState({namespace:
val}), true, build)}
+ {this.getField("tag", "Image tag", "text", tag, "Image
tag", val => this.setState({tag: val}), true, build)}
+ {target !== 'openshift' && this.getField("from", "Base
Image", "text", from, "Base Image", val => this.setState({from: val}), true,
build)}
+ {target === 'openshift' && this.getField("sourceImage",
"Source tag", "text", sourceImage, "Source image name (for OpenShift
BuildConfig)", val => this.setState({sourceImage: val}), true, build)}
+ {target === 'openshift' && this.getField("server",
"Server", "text", server, "Master URL", val => this.setState({server: val}),
true, build)}
+ {target === 'openshift' && this.getField("token", "Token",
"password", token, "Authentication Token", val => this.setState({token: val}),
true, build)}
+ </Form>
+ </CardBody>
+ </Card>
+ }
+
+ getDeployForm() {
+ const {target, deploy, build} = this.state;
+ return <Card className="builder-card" isCompact style={{width:
"100%"}}>
+ {this.getCardHeader("Deploy", <DeployIcon/>, true, deploy, check
=> this.setState({deploy: check}))}
+ <CardBody className={build ? "" : "card-disabled"}>
+ <Form isHorizontal>
+ {deploy && this.getField("replicas", "Replicas", "number",
this.state.replicas, "Number of replicas of the application", val =>
this.setState({replicas: val}), true, build)}
+ {deploy && target === 'minikube' &&
this.getField("nodePort", "Node port", "number", this.state.nodePort, "Node
port (minikube)", val => this.setState({nodePort: val}), true, build)}
+ </Form>
+ </CardBody>
+ </Card>
+ }
+
+ getCleanupForm() {
+ const {cleanup} = this.state;
+ return <Card className="builder-card" isCompact style={{width:
"100%"}}>
+ {this.getCardHeader("Cleanup", <CleanupIcon/>, true, cleanup,
check => this.setState({cleanup: check}))}
+ <CardBody className={cleanup ? "" : "card-disabled"}>
+ <HelperText>
+ <HelperTextItem variant="indeterminate">Remove created jar
and .camel-jbang after build and/or deploy.</HelperTextItem>
+ </HelperText>
+ </CardBody>
+ </Card>
+ }
+
+ getProgressIcon(status: 'pending' | 'progress' | 'done' | 'error') {
+ switch (status) {
+ case "pending":
+ return <PendingIcon/>;
+ case "progress":
+ return <Spinner isSVG size="md"/>
+ case "done":
+ return <CheckCircleIcon/>;
+ case "error":
+ return <ExclamationCircleIcon/>;
+ default:
+ return undefined;
+ }
+ }
+
+ getProgress() {
+ const {status, uberJar, build, deploy} = this.state;
+ const undeploying = status.active && status.undeploy === "progress";
+ return (
+ <ProgressStepper isCenterAligned style={{visibility: "visible"}}>
+ {!undeploying && uberJar && <ProgressStep variant="pending"
id="package" titleId="package" aria-label="package"
icon={this.getProgressIcon(status.uberJar)}>Package</ProgressStep>}
+ {!undeploying && build && <ProgressStep variant="pending"
isCurrent id="build" titleId="build" aria-label="build"
icon={this.getProgressIcon(status.build)}>Build</ProgressStep>}
+ {!undeploying && deploy && <ProgressStep variant="pending"
id="deploy" titleId="deploy" aria-label="deploy"
icon={this.getProgressIcon(status.deploy)}>Deploy</ProgressStep>}
+ {undeploying &&
+ <ProgressStep variant="pending" id="undeploy"
titleId="undeploy" aria-label="undeploy"
icon={this.getProgressIcon(status.undeploy)}>Undeploy</ProgressStep>}
+ </ProgressStepper>
+ )
+ }
+
+ getHeader() {
+ return (
+ <PageSection className="tools-section" variant={this.props.dark ?
PageSectionVariants.darker : PageSectionVariants.light}>
+ <Flex className="tools" direction={{default: 'row'}}
justifyContent={{default: 'justifyContentSpaceBetween'}} spaceItems={{default:
'spaceItemsLg'}}>
+ <FlexItem>
+ <TextContent className="header">
+ <Text component="h2">Project Builder</Text>
+ <Badge isRead className="labels">Powered by Camel
JBang</Badge>
+ </TextContent>
+ </FlexItem>
+ <FlexItem>
+ <Toolbar id="toolbar-group-types">
+ <ToolbarContent>
+ <ToolbarItem>
+ <Button variant="plain" onClick={e => {
+ }}><HelpIcon/></Button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </FlexItem>
+ </Flex>
+ </PageSection>
+ )
+ }
+
+ onButtonClick(action: "start" | "stop" | "undeploy") {
+ this.props.onAction?.call(this, action, this.state);
+ }
+
+ getFooter() {
+ const active = this.state.status.active;
+ const label = active ? "Stop" : "Start";
+ const icon = active ? <InProgressIcon/> : <AutomationIcon/>;
+ return <div className="footer">
+ <div className="progress">
+ {active && this.getProgress()}
+ </div>
+ <div className="buttons">
+ <Toolbar id="toolbar-items">
+ <ToolbarContent>
+ {!active && <ToolbarItem>
+ <Button variant="secondary" isSmall
onClick={event => this.onButtonClick("undeploy")}>Undeploy</Button>
+ </ToolbarItem>}
+ <ToolbarItem>
+ <Button variant="primary" isSmall
icon={icon} onClick={event => this.onButtonClick(active ? "stop" :
"start")}>{label}</Button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </div>
+ </div>
+ }
+
+ getCenter() {
+ return (
+ <div className="center">
+ <div className="center-column">
+ {this.getProjectForm()}
+ {this.getPackageForm()}
+ </div>
+ <div className="center-column">
+ {this.getBuildForm()}
+ {this.getDeployForm()}
+ {this.getCleanupForm()}
+ </div>
+ </div>
+ )
+ }
+
+ render() {
+ return (
+ <PageSection className="project-builder" variant={this.props.dark
? PageSectionVariants.darker : PageSectionVariants.light}
+ padding={{default: 'noPadding'}}>
+ <div style={{height: "100%", display: "flex", flexDirection:
"column"}}>
+ <div style={{flexShrink: "0"}}>
+ {this.getHeader()}
+ </div>
+ <div style={{overflow: "auto", flexGrow: 1}}>
+ {this.getCenter()}
+ </div>
+ <div style={{flexShrink: "0"}}>
+ {this.getFooter()}
+ </div>
+ </div>
+ </PageSection>
+ )
+ }
+};
\ No newline at end of file
diff --git a/karavan-vscode/webview/builder/FileSelector.tsx
b/karavan-vscode/webview/builder/FileSelector.tsx
new file mode 100644
index 0000000..983a2a6
--- /dev/null
+++ b/karavan-vscode/webview/builder/FileSelector.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import {
+ Button,
+ FormGroup,
+ Checkbox, PopoverPosition, Popover, InputGroup
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon";
+
+interface Props {
+ label: string
+ help: string
+ files: string
+ filesSelected: string
+ onChange: (files: string) => void
+ source: boolean
+}
+
+interface State {
+ selected: []
+}
+
+export class FileSelector extends React.Component<Props, State> {
+
+ public state: State = {
+ selected: []
+ };
+
+ isChecked(file: string) {
+ const finalFile = this.props.source ? "file:" + file : file;
+ const s = this.props.filesSelected.split(",").map(value =>
value.trim());
+ return s.includes(finalFile);
+ }
+
+ onChange(file: string, checked: boolean) {
+ const finalFile = this.props.source ? "file:" + file : file;
+ const s = this.props.filesSelected.split(",").map(f =>
f.trim()).filter(f => f.length > 0);
+ const already = s.includes(finalFile);
+ if (checked && !already) {
+ s.push(finalFile);
+ this.props.onChange?.call(this, s.join(","));
+ } else if (!checked) {
+ const result = s.filter(f => f !== finalFile);
+ this.props.onChange?.call(this, result.join(","));
+ }
+ }
+
+ getFiles(): string[] {
+ const allFiles = (this.props.files ? this.props.files.split(",") : []);
+ if (this.props.source){
+ const extensions = ['yaml', 'yml', 'java', 'js', 'kt', 'groovy',
'xml'];
+ return allFiles.filter(file => {
+ const extension = file.split(".").pop() || '';
+ return extensions.includes(extension);
+ }).map(file => file.replace("file:", ""))
+ }
+ return allFiles;
+ }
+
+ render() {
+ const files = this.getFiles();
+ return (
+ <FormGroup label={this.props.label} fieldId="files">
+ <InputGroup>
+ <div style={{width:"100%"}}>
+ {files.map(file => {
+ const key = file + this.props.source;
+ return <Checkbox key={key} label={file}
isChecked={this.isChecked(file)} onChange={checked => this.onChange(file,
checked)} id={key} name={key}/>
+ })}
+ </div>
+ <Popover aria-label="files" position={PopoverPosition.left}
+ bodyContent={this.props.help}>
+ <Button variant="plain" onClick={e => {}}>
+ <HelpIcon/>
+ </Button>
+ </Popover>
+ </InputGroup>
+ </FormGroup>
+ )
+ }
+};
\ No newline at end of file
diff --git a/karavan-vscode/webview/index.css b/karavan-vscode/webview/index.css
index 39225d0..8fde82f 100644
--- a/karavan-vscode/webview/index.css
+++ b/karavan-vscode/webview/index.css
@@ -335,5 +335,57 @@ color: var(--vscode-editor-foreground);
color: var(--vscode-input-foreground);
}
+/* Builder */
+.vscode-dark .karavan .project-builder {
+ background: var(--vscode-tab-inactiveBackground);
+ border: none;
+}
+.vscode-dark .karavan .project-builder .header .pf-c-badge {
+ --pf-c-badge--BackgroundColor: var(--pf-global--primary-color--200);
+}
+
+
+.vscode-dark .karavan .project-builder h2,
+.vscode-dark .karavan .project-builder .pf-c-card__title,
+.vscode-dark .karavan .project-builder .pf-c-form__label-text {
+ color: var(--vscode-editor-foreground);
+}
+.vscode-dark .karavan .project-builder .builder-card {
+ background-color: var(--vscode-badge-background);
+}
+
+.vscode-dark .karavan .project-builder .builder-card .text-field {
+ background-color: var(--vscode-input-background);
+ border-color: var(--vscode-input-background);
+ color: var(--vscode-input-foreground);
+}
+.vscode-dark .karavan .project-builder .pf-c-input-group {
+ background-color: transparent;
+ border-color: var(--vscode-input-foreground);
+ color: var(--vscode-input-foreground);
+ --pf-c-text-input-group__text--before--BorderColor: transparent;
+ --pf-c-text-input-group__text--after--BorderBottomColor: transparent;
+}
+
+.vscode-dark .karavan .project-builder .pf-c-toggle-group {
+ --pf-c-toggle-group__button--BackgroundColor: transparent;
+ --pf-c-toggle-group__button--Color: var(--vscode-editor-foreground);
+}
+
+.vscode-dark .karavan .project-builder .pf-c-toggle-group
.pf-c-toggle-group__button.pf-m-selected {
+ --pf-c-toggle-group__button--BackgroundColor:
var(--pf-global--primary-color--200);
+}
+
+.vscode-dark .karavan .project-builder .pf-c-toggle-group
.pf-c-toggle-group__button:hover {
+ --pf-c-toggle-group__button--BackgroundColor: var(--vscode-input-background);
+}
+
+.vscode-dark .karavan .project-builder .pf-c-progress-stepper__step-icon {
+ background-color: var(--vscode-input-background);
+}
+
+.vscode-dark .karavan .project-builder .footer .buttons .pf-c-toolbar {
+ background: transparent;
+}