You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2022/04/22 00:53:37 UTC
[camel-karavan] branch main updated: Generate routes from OpenAPI in app (#325)
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 e4aaee5 Generate routes from OpenAPI in app (#325)
e4aaee5 is described below
commit e4aaee5378a121527af309a494c71da7183fbac1
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Thu Apr 21 20:53:33 2022 -0400
Generate routes from OpenAPI in app (#325)
* generare routes
* Upload OpenApi and Yaml
* REST DSL generator in app
---
karavan-app/pom.xml | 7 +
.../apache/camel/karavan/api/OpenApiResource.java | 92 +++++++++++
.../camel/karavan/service/FileSystemService.java | 14 ++
.../camel/karavan/service/GeneratorService.java | 53 +++++++
karavan-app/src/main/webapp/src/Main.tsx | 68 ++++++--
karavan-app/src/main/webapp/src/api/KaravanApi.tsx | 33 ++++
.../main/webapp/src/components/ComponentsPage.tsx | 7 +-
karavan-app/src/main/webapp/src/eip/EipPage.tsx | 7 +-
.../webapp/src/integrations/IntegrationCard.tsx | 15 +-
.../webapp/src/integrations/IntegrationPage.tsx | 130 ++++++++++------
.../main/webapp/src/integrations/OpenApiPage.tsx | 89 +++++++++++
.../main/webapp/src/integrations/UploadModal.tsx | 171 +++++++++++++++++++++
.../src/main/webapp/src/kamelets/KameletsPage.tsx | 7 +-
13 files changed, 618 insertions(+), 75 deletions(-)
diff --git a/karavan-app/pom.xml b/karavan-app/pom.xml
index 9a72a98..d67d016 100644
--- a/karavan-app/pom.xml
+++ b/karavan-app/pom.xml
@@ -33,6 +33,7 @@
<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
<version.camel-kamelet>0.7.1</version.camel-kamelet>
<version.camel-k-client>5.12.1</version.camel-k-client>
+ <version.camel>3.17.0-SNAPSHOT</version.camel>
</properties>
<dependencyManagement>
<dependencies>
@@ -83,6 +84,12 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jgit</artifactId>
</dependency>
+ <!-- Code generator -->
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-openapi-rest-dsl-generator</artifactId>
+ <version>${version.camel}</version>
+ </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-container-image-docker</artifactId>
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/OpenApiResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/OpenApiResource.java
new file mode 100644
index 0000000..93254e2
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/OpenApiResource.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.karavan.api;
+
+import org.apache.camel.karavan.service.FileSystemService;
+import org.apache.camel.karavan.service.GeneratorService;
+import org.apache.camel.karavan.service.GitService;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import javax.inject.Inject;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Path("/openapi")
+public class OpenApiResource {
+
+ private static final String GITOPS_MODE = "gitops";
+ private static final String SERVERLESS_MODE = "serverless";
+
+ @ConfigProperty(name = "karavan.mode", defaultValue = "local")
+ String mode;
+
+ @Inject
+ FileSystemService fileSystemService;
+
+ @Inject
+ GeneratorService generatorService;
+
+ @Inject
+ GitService gitService;
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public Map<String, String> getList(@HeaderParam("username") String username) throws GitAPIException {
+ if (mode.endsWith(GITOPS_MODE)) {
+ String dir = gitService.pullIntegrations(username);
+ return fileSystemService.getOpenApiList(dir).stream().collect(Collectors.toMap(s -> s, s-> ""));
+ } else {
+ return fileSystemService.getOpenApiList().stream().collect(Collectors.toMap(s -> s, s-> ""));
+ }
+ }
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/{name}")
+ public String getJson(@HeaderParam("username") String username, @PathParam("name") String name) throws GitAPIException {
+ switch (mode){
+ case GITOPS_MODE:
+ String dir = gitService.pullIntegrations(username);
+ return fileSystemService.getFile(dir, name);
+ default:
+ return fileSystemService.getIntegrationsFile(name);
+ }
+ }
+
+ @POST
+ @Produces(MediaType.TEXT_PLAIN)
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Path("/{name}/{generateRest}/{generateRoutes}/{integrationName}")
+ public String save(@HeaderParam("username") String username,
+ @PathParam("name") String name,
+ @PathParam("integrationName") String integrationName,
+ @PathParam("generateRest") boolean generateRest,
+ @PathParam("generateRoutes") boolean generateRoutes,
+ String json) throws Exception {
+ fileSystemService.saveIntegrationsFile(name, json);
+ if (generateRest) {
+ String yaml = generatorService.generate(json, generateRoutes);
+ fileSystemService.saveIntegrationsFile(integrationName, yaml);
+ return yaml;
+ }
+ return json;
+ }
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/FileSystemService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/FileSystemService.java
index d63ec59..36fa9bf 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/service/FileSystemService.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/FileSystemService.java
@@ -74,6 +74,20 @@ public class FileSystemService {
}).collect(Collectors.toList());
}
+ public List<String> getOpenApiList() {
+ return getOpenApiList(integrations);
+ }
+
+ public List<String> getOpenApiList(String folder) {
+ return vertx.fileSystem().readDirBlocking(Paths.get(folder).toString())
+ .stream()
+ .filter(s -> s.endsWith(".json"))
+ .map(s -> {
+ String[] parts = s.split(Pattern.quote(File.separator));
+ return parts[parts.length - 1];
+ }).collect(Collectors.toList());
+ }
+
public String getIntegrationsFile(String name) throws GitAPIException {
return vertx.fileSystem().readFileBlocking(Paths.get(integrations, name).toString()).toString();
}
diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/service/GeneratorService.java b/karavan-app/src/main/java/org/apache/camel/karavan/service/GeneratorService.java
new file mode 100644
index 0000000..3bcf6e7
--- /dev/null
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/service/GeneratorService.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.karavan.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.apicurio.datamodels.Library;
+import io.apicurio.datamodels.openapi.models.OasDocument;
+import io.vertx.core.Vertx;
+import org.apache.camel.CamelContext;
+import org.apache.camel.generator.openapi.RestDslGenerator;
+import org.apache.camel.impl.lw.LightweightCamelContext;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.logging.Logger;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+@ApplicationScoped
+public class GeneratorService {
+
+
+ @ConfigProperty(name = "karavan.folder.integrations")
+ String integrations;
+
+ @Inject
+ Vertx vertx;
+
+ private static final Logger LOGGER = Logger.getLogger(GeneratorService.class.getName());
+
+ public String generate(String openApi, boolean generateRoutes) throws Exception {
+ final ObjectMapper mapper = new ObjectMapper();
+ final JsonNode node = mapper.readTree(openApi);
+ OasDocument document = (OasDocument) Library.readDocument(node);
+ try (CamelContext context = new LightweightCamelContext()) {
+ return RestDslGenerator.toYaml(document).generate(context, generateRoutes);
+ }
+ }
+}
diff --git a/karavan-app/src/main/webapp/src/Main.tsx b/karavan-app/src/main/webapp/src/Main.tsx
index aff65df..2fd200e 100644
--- a/karavan-app/src/main/webapp/src/Main.tsx
+++ b/karavan-app/src/main/webapp/src/Main.tsx
@@ -38,6 +38,7 @@ import {ComponentApi} from "karavan-core/lib/api/ComponentApi";
import Icon from "./Logo";
import {ComponentsPage} from "./components/ComponentsPage";
import {EipPage} from "./eip/EipPage";
+import {OpenApiPage} from "./integrations/OpenApiPage";
class ToastMessage {
id: string = ''
@@ -60,13 +61,16 @@ interface State {
version: string,
mode: 'local' | 'gitops' | 'serverless',
isNavOpen: boolean,
- pageId: 'integrations' | 'configuration' | 'kamelets' | 'designer' | "components" | "eip"
+ pageId: 'integrations' | 'configuration' | 'kamelets' | 'designer' | "components" | "eip" | "openapi"
integrations: Map<string,string>,
+ openapis: Map<string,string>,
integration: Integration,
isModalOpen: boolean,
nameToDelete: string,
+ openapi: string,
alerts: ToastMessage[],
request: string
+ filename: string
}
export class Main extends React.Component<Props, State> {
@@ -77,11 +81,14 @@ export class Main extends React.Component<Props, State> {
isNavOpen: true,
pageId: "integrations",
integrations: new Map<string,string>(),
+ openapis: new Map<string,string>(),
integration: Integration.createNew(),
isModalOpen: false,
nameToDelete: '',
alerts: [],
- request: uuidv4()
+ request: uuidv4(),
+ openapi: '',
+ filename: ''
};
designer = React.createRef();
@@ -100,6 +107,7 @@ export class Main extends React.Component<Props, State> {
KaravanApi.getComponent(name, json => ComponentApi.saveComponent(json))
}));
this.onGetIntegrations();
+ this.onGetOpenApis();
}
onNavToggle = () => {
@@ -189,7 +197,7 @@ export class Main extends React.Component<Props, State> {
sidebar = () => (<PageSidebar nav={this.pageNav()} isNavOpen={this.state.isNavOpen}/>);
- onIntegrationDelete = (name: string) => {
+ onIntegrationDelete = (name: string, type: 'integration' | 'openapi') => {
this.setState({isModalOpen: true, nameToDelete: name})
};
@@ -201,6 +209,7 @@ export class Main extends React.Component<Props, State> {
if (res.status === 204) {
this.toast("Success", "Integration deleted", "success");
this.onGetIntegrations();
+ this.onGetOpenApis();
} else {
this.toast("Error", res.statusText, "danger");
}
@@ -214,16 +223,28 @@ export class Main extends React.Component<Props, State> {
this.setState({alerts: mess})
}
- onIntegrationSelect = (filename: string) => {
- KaravanApi.getIntegration(filename, res => {
- if (res.status === 200) {
- const code: string = res.data;
- const i = CamelDefinitionYaml.yamlToIntegration(filename, code);
- this.setState({isNavOpen: false, pageId: 'designer', integration: i});
- } else {
- this.toast("Error", res.status + ", " + res.statusText, "danger");
- }
- });
+ onIntegrationSelect = (filename: string, type: 'integration' | 'openapi') => {
+ if (type === 'integration') {
+ KaravanApi.getIntegration(filename, res => {
+ if (res.status === 200) {
+ const code: string = res.data;
+ const i = CamelDefinitionYaml.yamlToIntegration(filename, code);
+ this.setState({isNavOpen: false, pageId: 'designer', integration: i, filename: filename});
+ } else {
+ this.toast("Error", res.status + ", " + res.statusText, "danger");
+ }
+ });
+ } else {
+ KaravanApi.getOpenApi(filename, res => {
+ if (res.status === 200) {
+ const code: string = JSON.stringify(res.data, null, 2);
+ console.log(code)
+ this.setState({isNavOpen: true, pageId: 'openapi', openapi: code, filename: filename});
+ } else {
+ this.toast("Error", res.status + ", " + res.statusText, "danger");
+ }
+ });
+ }
};
onIntegrationCreate = (i: Integration) => {
@@ -236,20 +257,35 @@ export class Main extends React.Component<Props, State> {
this.setState({
integrations: map, request: uuidv4()
})});
+ }
+
+ onGetOpenApis() {
+ KaravanApi.getOpenApis((openapis: {}) => {
+ const map:Map<string, string> = new Map(Object.entries(openapis));
+ this.setState({
+ openapis: map, request: uuidv4()
+ })});
};
render() {
return (
<Page className="karavan" header={this.header(this.state.version)} sidebar={this.sidebar()}>
{this.state.pageId === 'integrations' &&
- <IntegrationPage key={this.state.request} integrations={this.state.integrations}
- onRefresh={this.onGetIntegrations}
- onDelete={this.onIntegrationDelete} onSelect={this.onIntegrationSelect}
+ <IntegrationPage key={this.state.request}
+ integrations={this.state.integrations}
+ openapis={this.state.openapis}
+ onRefresh={() => {
+ this.onGetIntegrations();
+ this.onGetOpenApis();
+ }}
+ onDelete={this.onIntegrationDelete}
+ onSelect={this.onIntegrationSelect}
onCreate={this.onIntegrationCreate}/>}
{this.state.pageId === 'configuration' && <ConfigurationPage/>}
{this.state.pageId === 'kamelets' && <KameletsPage dark={false}/>}
{this.state.pageId === 'components' && <ComponentsPage dark={false}/>}
{this.state.pageId === 'eip' && <EipPage dark={false}/>}
+ {this.state.pageId === 'openapi' && <OpenApiPage dark={false} openapi={this.state.openapi} filename={this.state.filename}/>}
{this.state.pageId === 'designer' &&
<DesignerPage mode={this.state.mode} integration={this.state.integration}/>}
<Modal
diff --git a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
index 5c1e43b..8dd5d58 100644
--- a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
+++ b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
@@ -114,4 +114,37 @@ export const KaravanApi = {
console.log(err);
});
},
+
+ getOpenApis: async (after: (openapis: []) => void) => {
+ axios.get('/openapi',
+ {headers: {'Accept': 'application/json', 'username': 'cameleer'}})
+ .then(res => {
+ if (res.status === 200) {
+ after(res.data);
+ }
+ }).catch(err => {
+ console.log(err);
+ });
+ },
+
+ getOpenApi: async (name: string, after: (res: AxiosResponse<any>) => void) => {
+ axios.get('/openapi/' + name,
+ {headers: {'Accept': 'text/plain', 'username': 'cameleer'}})
+ .then(res => {
+ after(res);
+ }).catch(err => {
+ after(err);
+ });
+ },
+
+ postOpenApi: async (name: string, json: string, generateRest: boolean, generateRoutes: boolean, integrationName: string, after: (res: AxiosResponse<any>) => void) => {
+ const uri = `/openapi/${name}/${generateRest}/${generateRoutes}/${integrationName}`;
+ axios.post(encodeURI(uri), json,
+ {headers: {'Accept': 'text/plain', 'Content-Type': 'text/plain', 'username': 'cameleer'}})
+ .then(res => {
+ after(res);
+ }).catch(err => {
+ after(err);
+ });
+ },
}
\ No newline at end of file
diff --git a/karavan-app/src/main/webapp/src/components/ComponentsPage.tsx b/karavan-app/src/main/webapp/src/components/ComponentsPage.tsx
index 203a789..f1b5dc8 100644
--- a/karavan-app/src/main/webapp/src/components/ComponentsPage.tsx
+++ b/karavan-app/src/main/webapp/src/components/ComponentsPage.tsx
@@ -5,7 +5,7 @@ import {
Gallery,
ToolbarItem,
TextInput,
- PageSection, TextContent, Text, PageSectionVariants, Flex, FlexItem
+ PageSection, TextContent, Text, PageSectionVariants, Flex, FlexItem, Badge
} from '@patternfly/react-core';
import '../designer/karavan.css';
import {ComponentCard} from "./ComponentCard";
@@ -62,8 +62,9 @@ export class ComponentsPage extends React.Component<Props, State> {
<PageSection className="tools-section" variant={this.props.dark ? PageSectionVariants.darker : PageSectionVariants.light}>
<Flex className="tools" justifyContent={{default: 'justifyContentSpaceBetween'}}>
<FlexItem>
- <TextContent>
- <Text component="h1">Component Catalog</Text>
+ <TextContent className="header">
+ <Text component="h2">Component Catalog</Text>
+ <Badge isRead className="labels">{components.length}</Badge>
</TextContent>
</FlexItem>
<FlexItem>
diff --git a/karavan-app/src/main/webapp/src/eip/EipPage.tsx b/karavan-app/src/main/webapp/src/eip/EipPage.tsx
index fe5fa2f..dd6e301 100644
--- a/karavan-app/src/main/webapp/src/eip/EipPage.tsx
+++ b/karavan-app/src/main/webapp/src/eip/EipPage.tsx
@@ -5,7 +5,7 @@ import {
Gallery,
ToolbarItem,
TextInput,
- PageSection, TextContent, Text, PageSectionVariants, Flex, FlexItem
+ PageSection, TextContent, Text, PageSectionVariants, Flex, FlexItem, Badge
} from '@patternfly/react-core';
import '../designer/karavan.css';
import {EipCard} from "./EipCard";
@@ -57,8 +57,9 @@ export class EipPage extends React.Component<Props, State> {
<PageSection className="tools-section" variant={this.props.dark ? PageSectionVariants.darker : PageSectionVariants.light}>
<Flex className="tools" justifyContent={{default: 'justifyContentSpaceBetween'}}>
<FlexItem>
- <TextContent>
- <Text component="h1">Enterprise Integration Patterns</Text>
+ <TextContent className="header">
+ <Text component="h2">Enterprise Integration Patterns</Text>
+ <Badge isRead className="labels">{elements.length}</Badge>
</TextContent>
</FlexItem>
<FlexItem>
diff --git a/karavan-app/src/main/webapp/src/integrations/IntegrationCard.tsx b/karavan-app/src/main/webapp/src/integrations/IntegrationCard.tsx
index 7402caf..ef1142f 100644
--- a/karavan-app/src/main/webapp/src/integrations/IntegrationCard.tsx
+++ b/karavan-app/src/main/webapp/src/integrations/IntegrationCard.tsx
@@ -5,12 +5,14 @@ import {
import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
import '../designer/karavan.css';
import {CamelUi} from "../designer/utils/CamelUi";
+import {getDesignerIcon} from "../designer/utils/KaravanIcons";
interface Props {
name: string,
+ type: 'integration' | 'openapi',
status?: string,
- onClick: any
- onDelete: any
+ onClick: (filename: string, type: 'integration' | 'openapi') => void
+ onDelete: (name: string, type: 'integration' | 'openapi') => void
}
interface State {
@@ -23,23 +25,24 @@ export class IntegrationCard extends React.Component<Props, State> {
private click(evt: React.MouseEvent) {
evt.stopPropagation();
- this.props.onClick.call(this, this.props.name)
+ this.props.onClick.call(this, this.props.name, this.props.type)
}
private delete(evt: React.MouseEvent) {
evt.stopPropagation();
- this.props.onDelete.call(this, this.props.name);
+ this.props.onDelete.call(this, this.props.name, this.props.type);
}
render() {
return (
- <Card isHoverable isCompact key={this.props.name} className="integration-card" onClick={event => this.click(event)}>
+ <Card isCompact key={this.props.name} className="integration-card" onClick={event => this.click(event)}>
<CardHeader>
- <img src={CamelUi.getIconForName("camel")} alt='icon' className="icon"/>
+ {getDesignerIcon(this.props.type === 'integration' ? 'routes' : 'rest')}
<CardActions>
<Button variant="link" className="delete-button" onClick={e => this.delete(e)}><DeleteIcon/></Button>
</CardActions>
</CardHeader>
+ <CardTitle>{this.props.type === 'integration' ? 'Integration' : 'OpenAPI'}</CardTitle>
<CardTitle>{CamelUi.titleFromName(this.props.name)}</CardTitle>
<CardBody>{this.props.name}</CardBody>
<CardFooter className={this.props.status === 'Running' ? 'running' : (this.props.status === 'Error' ? 'error' : 'normal')}>
diff --git a/karavan-app/src/main/webapp/src/integrations/IntegrationPage.tsx b/karavan-app/src/main/webapp/src/integrations/IntegrationPage.tsx
index 83d72b7..6fcb2a1 100644
--- a/karavan-app/src/main/webapp/src/integrations/IntegrationPage.tsx
+++ b/karavan-app/src/main/webapp/src/integrations/IntegrationPage.tsx
@@ -8,31 +8,42 @@ import {
PageSection,
TextContent,
Text,
- Button, Modal, FormGroup, ModalVariant, Switch, Form, FormSelect, FormSelectOption
+ Button, Modal, FormGroup, ModalVariant, Switch, Form, FormSelect, FormSelectOption, FileUpload
} from '@patternfly/react-core';
import '../designer/karavan.css';
import {IntegrationCard} from "./IntegrationCard";
import {MainToolbar} from "../MainToolbar";
import RefreshIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon';
+import UploadIcon from '@patternfly/react-icons/dist/esm/icons/upload-icon';
import {Integration} from "karavan-core/lib/model/IntegrationDefinition";
import {CamelUi} from "../designer/utils/CamelUi";
+import {UploadModal} from "./UploadModal";
interface Props {
- integrations: Map<string,string>
- onSelect: any
+ integrations: Map<string, string>
+ openapis: Map<string, string>
+ onSelect: (filename: string, type: 'integration' | 'openapi') => void
onCreate: any
- onDelete: any
+ onDelete: (name: string, type: 'integration' | 'openapi') => void
onRefresh: any
}
interface State {
repository: string,
path: string,
- integrations: Map<string,string>,
- isModalOpen: boolean,
+ integrations: Map<string, string>,
+ openapis: Map<string, string>,
+ isCreateModalOpen: boolean,
+ isUploadModalOpen: boolean,
newName: string
crd: boolean
+ data: string
+ filename: string
+ isLoading: boolean
+ isRejected: boolean
+ generateRest: boolean
+ generateRoutes: boolean
}
export class IntegrationPage extends React.Component<Props, State> {
@@ -41,9 +52,17 @@ export class IntegrationPage extends React.Component<Props, State> {
repository: '',
path: '',
integrations: this.props.integrations,
- isModalOpen: false,
+ openapis: this.props.openapis,
+ isCreateModalOpen: false,
+ isUploadModalOpen: false,
newName: '',
- crd: true
+ crd: true,
+ data: '',
+ filename: '',
+ isLoading: false,
+ isRejected: false,
+ generateRest: true,
+ generateRoutes: true
};
tools = () => (<Toolbar id="toolbar-group-types">
@@ -53,10 +72,15 @@ export class IntegrationPage extends React.Component<Props, State> {
autoComplete="off" placeholder="Search by name"/>
</ToolbarItem>
<ToolbarItem>
- <Button variant="secondary" icon={<RefreshIcon />} onClick={e => this.props.onRefresh.call(this)}>Refresh</Button>
+ <Button variant="secondary" icon={<RefreshIcon/>}
+ onClick={e => this.props.onRefresh.call(this)}>Refresh</Button>
</ToolbarItem>
<ToolbarItem>
- <Button icon={<PlusIcon />} onClick={e => this.setState({isModalOpen:true})}>Create</Button>
+ <Button variant="secondary" icon={<UploadIcon/>}
+ onClick={e => this.setState({isUploadModalOpen: true})}>Upload</Button>
+ </ToolbarItem>
+ <ToolbarItem>
+ <Button icon={<PlusIcon/>} onClick={e => this.setState({isCreateModalOpen: true})}>Create</Button>
</ToolbarItem>
</ToolbarContent>
</Toolbar>);
@@ -66,23 +90,55 @@ export class IntegrationPage extends React.Component<Props, State> {
</TextContent>);
closeModal = () => {
- this.setState({isModalOpen:false, newName:""});
+ this.setState({isCreateModalOpen: false, newName: "", isUploadModalOpen: false});
+ this.props.onRefresh.call(this);
}
- saveAndCloseModal = () => {
+ saveAndCloseCreateModal = () => {
const name = CamelUi.nameFromTitle(this.state.newName) + ".yaml";
const i = Integration.createNew(name);
i.crd = this.state.crd;
this.props.onCreate.call(this, i);
- this.setState({isModalOpen:false, newName:""});
+ this.setState({isCreateModalOpen: false, newName: ""});
}
onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.key === 'Enter' && this.state.newName !== undefined) {
- this.saveAndCloseModal();
+ this.saveAndCloseCreateModal();
}
- }
+ }
+ createModalForm() {
+ return (
+ <Modal
+ title="Create new Integration"
+ variant={ModalVariant.small}
+ isOpen={this.state.isCreateModalOpen}
+ onClose={this.closeModal}
+ onKeyDown={this.onKeyDown}
+ actions={[
+ <Button key="confirm" variant="primary" onClick={this.saveAndCloseCreateModal}>Save</Button>,
+ <Button key="cancel" variant="secondary" onClick={this.closeModal}>Cancel</Button>
+ ]}
+ >
+ <Form isHorizontal={true}>
+ <FormGroup label="Title" fieldId="title" isRequired>
+ <TextInput className="text-field" type="text" id="title" name="title"
+ value={this.state.newName}
+ onChange={e => this.setState({newName: e})}/>
+ </FormGroup>
+ <FormGroup label="Type" fieldId="crd" isRequired>
+ <FormSelect value={this.state.crd}
+ onChange={value => this.setState({crd: Boolean(JSON.parse(value))})}
+ aria-label="FormSelect Input">
+ <FormSelectOption key="crd" value="true" label="Camel-K CRD"/>
+ <FormSelectOption key="plain" value="false" label="Plain YAML"/>
+ </FormSelect>
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+ }
render() {
return (
@@ -91,39 +147,25 @@ export class IntegrationPage extends React.Component<Props, State> {
<PageSection isFilled className="integration-page">
<Gallery hasGutter>
{Array.from(this.state.integrations.keys()).map(key => (
- <IntegrationCard key={key}
- name={key}
- status={this.state.integrations.get(key)}
- onDelete={this.props.onDelete}
+ <IntegrationCard key={key}
+ name={key}
+ type={"integration"}
+ status={this.state.integrations.get(key)}
+ onDelete={this.props.onDelete}
+ onClick={this.props.onSelect}/>
+ ))}
+ {Array.from(this.state.openapis.keys()).map(key => (
+ <IntegrationCard key={key}
+ name={key}
+ type={"openapi"}
+ status={this.state.openapis.get(key)}
+ onDelete={this.props.onDelete}
onClick={this.props.onSelect}/>
))}
</Gallery>
</PageSection>
- <Modal
- title="Create new Integration"
- variant={ModalVariant.small}
- isOpen={this.state.isModalOpen}
- onClose={this.closeModal}
- onKeyDown={this.onKeyDown}
- actions={[
- <Button key="confirm" variant="primary" onClick={this.saveAndCloseModal}>Save</Button>,
- <Button key="cancel" variant="secondary" onClick={this.closeModal}>Cancel</Button>
- ]}
- >
- <Form isHorizontal={true}>
- <FormGroup label="Title" fieldId="title" isRequired>
- <TextInput className="text-field" type="text" id="title" name="title"
- value={this.state.newName}
- onChange={e => this.setState({newName: e})}/>
- </FormGroup>
- <FormGroup label="Type" fieldId="crd" isRequired>
- <FormSelect value={this.state.crd} onChange={value => this.setState({crd: Boolean(JSON.parse(value))})} aria-label="FormSelect Input">
- <FormSelectOption key="crd" value="true" label="Camel-K CRD" />
- <FormSelectOption key="plain" value="false" label="Plain YAML" />
- </FormSelect>
- </FormGroup>
- </Form>
- </Modal>
+ {this.createModalForm()}
+ <UploadModal isOpen={this.state.isUploadModalOpen} onClose={this.closeModal}/>
</PageSection>
);
}
diff --git a/karavan-app/src/main/webapp/src/integrations/OpenApiPage.tsx b/karavan-app/src/main/webapp/src/integrations/OpenApiPage.tsx
new file mode 100644
index 0000000..a976ebf
--- /dev/null
+++ b/karavan-app/src/main/webapp/src/integrations/OpenApiPage.tsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import {
+ Button, CodeBlock, CodeBlockCode,
+ PageSection, Text, TextContent, ToggleGroup, ToggleGroupItem, Toolbar, ToolbarContent, ToolbarItem
+} from '@patternfly/react-core';
+import PublishIcon from '@patternfly/react-icons/dist/esm/icons/openshift-icon';
+import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon';
+import SaveIcon from '@patternfly/react-icons/dist/esm/icons/upload-icon';
+import CopyIcon from '@patternfly/react-icons/dist/esm/icons/copy-icon';
+import '../designer/karavan.css';
+import {MainToolbar} from "../MainToolbar";
+import {Integration} from "karavan-core/lib/model/IntegrationDefinition";
+import {KaravanApi} from "../api/KaravanApi";
+import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml";
+import {KaravanDesigner} from "../designer/KaravanDesigner";
+import FileSaver from "file-saver";
+import Editor from '@monaco-editor/react';
+
+interface Props {
+ openapi: string,
+ filename: string,
+ dark: boolean
+}
+
+interface State {
+}
+
+export class OpenApiPage extends React.Component<Props, State> {
+
+ public state: State = {
+ };
+
+ copy = () => {
+ this.copyToClipboard(this.props.openapi);
+ }
+
+ copyToClipboard = (data: string) => {
+ navigator.clipboard.writeText(data);
+ }
+
+ changeView = (view: "design" | "code") => {
+ this.setState({view: view});
+ }
+
+ save = (name: string, yaml: string) => {
+ this.setState({name: name, yaml: yaml})
+ }
+
+ download = () => {
+ const file = new File([this.props.openapi], this.props.filename, {type: "application/json;charset=utf-8"});
+ FileSaver.saveAs(file);
+ }
+
+ tools = () => (
+ <Toolbar id="toolbar-group-types">
+ <ToolbarContent>
+ <ToolbarItem>
+ <Button variant="secondary" icon={<CopyIcon/>} onClick={e => this.copy()}>Copy</Button>
+ </ToolbarItem>
+ <ToolbarItem>
+ <Button variant="secondary" icon={<DownloadIcon/>} onClick={e => this.download()}>Download</Button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>);
+
+ title = () => (
+ <div className="dsl-title">
+ <TextContent className="title">
+ <Text component="h1">OpenAPI</Text>
+ </TextContent>
+ </div>
+ );
+
+ render() {
+ return (<>
+ <MainToolbar title={this.title()}
+ tools={this.tools()}/>
+ <Editor
+ height="100vh"
+ defaultLanguage={'json'}
+ theme={'light'}
+ value={this.props.openapi}
+ className={'code-editor'}
+ onChange={(value, ev) => {if (value) this.setState({yaml: value})}}
+ />
+ </>
+ );
+ }
+}
diff --git a/karavan-app/src/main/webapp/src/integrations/UploadModal.tsx b/karavan-app/src/main/webapp/src/integrations/UploadModal.tsx
new file mode 100644
index 0000000..691b266
--- /dev/null
+++ b/karavan-app/src/main/webapp/src/integrations/UploadModal.tsx
@@ -0,0 +1,171 @@
+import React from 'react';
+import {
+ Toolbar,
+ ToolbarContent,
+ Gallery,
+ ToolbarItem,
+ TextInput,
+ PageSection,
+ TextContent,
+ Text,
+ Button, Modal, FormGroup, ModalVariant, Switch, Form, FormSelect, FormSelectOption, FileUpload, Wizard, Radio
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import {IntegrationCard} from "./IntegrationCard";
+import {MainToolbar} from "../MainToolbar";
+import RefreshIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
+import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon';
+import UploadIcon from '@patternfly/react-icons/dist/esm/icons/upload-icon';
+import {Integration} from "karavan-core/lib/model/IntegrationDefinition";
+import {CamelUi} from "../designer/utils/CamelUi";
+import {KaravanApi} from "../api/KaravanApi";
+
+interface Props {
+ isOpen: boolean,
+ onClose: any
+}
+
+interface State {
+ type: 'integration' | 'openapi'
+ data: string
+ filename: string
+ integrationName: string
+ isLoading: boolean
+ isRejected: boolean
+ generateRest: boolean
+ generateRoutes: boolean
+}
+
+export class UploadModal extends React.Component<Props, State> {
+
+ public state: State = {
+ type: 'integration',
+ data: '',
+ filename: '',
+ integrationName: '',
+ isLoading: false,
+ isRejected: false,
+ generateRest: true,
+ generateRoutes: true
+ };
+
+ closeModal = () => {
+ this.props.onClose?.call(this);
+ }
+
+ saveAndCloseModal = () => {
+ const state = this.state;
+ if (this.state.type === "integration"){
+ KaravanApi.postIntegrations(state.filename, state.data, res => {
+ if (res.status === 200) {
+ console.log(res) //TODO show notification
+ this.props.onClose?.call(this);
+ } else {
+ console.log(res) //TODO show notification
+ this.props.onClose?.call(this);
+ }
+ })
+ } else {
+ KaravanApi.postOpenApi(state.filename, state.data, state.generateRest, state.generateRoutes, state.integrationName, res => {
+ if (res.status === 200) {
+ console.log(res) //TODO show notification
+ this.props.onClose?.call(this);
+ } else {
+ console.log(res) //TODO show notification
+ this.props.onClose?.call(this);
+ }
+ })
+ }
+ }
+
+ handleFileInputChange = (event: React.ChangeEvent<HTMLInputElement> | React.DragEvent<HTMLElement>, file: File) => this.setState({filename: file.name});
+ handleFileReadStarted = (fileHandle: File) => this.setState({isLoading: true});
+ handleFileReadFinished = (fileHandle: File) => this.setState({isLoading: false});
+ handleTextOrDataChange = (data: string) => this.setState({data: data});
+ handleFileRejected = (acceptedOrRejected: File[], event: React.DragEvent<HTMLElement>) => this.setState({isRejected: true});
+ handleClear = (event: React.MouseEvent<HTMLButtonElement>) => this.setState({
+ filename: '',
+ data: '',
+ isRejected: false
+ });
+
+
+ render() {
+ const fileNotUploaded = (this.state.filename === '' || this.state.data === '');
+ const isDisabled = this.state.type === 'integration'
+ ? fileNotUploaded
+ : !(!fileNotUploaded && this.state.integrationName !== undefined && this.state.integrationName.endsWith(".yaml"));
+ const accept = this.state.type === 'integration' ? '.yaml' : '.json';
+ return (
+ <Modal
+ title="Upload"
+ variant={ModalVariant.small}
+ isOpen={this.props.isOpen}
+ onClose={this.closeModal}
+ actions={[
+ <Button key="confirm" variant="primary" onClick={this.saveAndCloseModal} isDisabled={isDisabled}>Save</Button>,
+ <Button key="cancel" variant="secondary" onClick={this.closeModal}>Cancel</Button>
+ ]}
+ >
+ <Form>
+ <FormGroup fieldId="type">
+ <Radio value="Integration" label="Integration yaml" name="Integration" id="Integration" isChecked={this.state.type === 'integration'}
+ onChange={(_, event) => this.setState({ type: _ ? 'integration': 'openapi' })}
+ />{' '}
+ <Radio value="OpenAPI" label="OpenAPI json" name="OpenAPI" id="OpenAPI" isChecked={this.state.type === 'openapi'}
+ onChange={(_, event) => this.setState({ type: _ ? 'openapi' : 'integration' })}
+ />
+ </FormGroup>
+ <FormGroup fieldId="upload">
+ <FileUpload
+ id="file-upload"
+ value={this.state.data}
+ filename={this.state.filename}
+ type="text"
+ hideDefaultPreview
+ browseButtonText="Upload"
+ isLoading={this.state.isLoading}
+ onFileInputChange={this.handleFileInputChange}
+ onDataChange={data => this.handleTextOrDataChange(data)}
+ onTextChange={text => this.handleTextOrDataChange(text)}
+ onReadStarted={this.handleFileReadStarted}
+ onReadFinished={this.handleFileReadFinished}
+ allowEditingUploadedText={false}
+ onClearClick={this.handleClear}
+ dropzoneProps={{accept: accept, onDropRejected: this.handleFileRejected}}
+ validated={this.state.isRejected ? 'error' : 'default'}
+ />
+ </FormGroup>
+ {this.state.type === 'openapi' && <FormGroup fieldId="generateRest">
+ <Switch
+ id="generate-rest"
+ label="Generate REST DSL"
+ labelOff="Do not generate REST DSL"
+ isChecked={this.state.generateRest}
+ onChange={checked => this.setState({generateRest: checked})}
+ />
+ </FormGroup>}
+ {this.state.type === 'openapi' && this.state.generateRest && <FormGroup fieldId="generateRoutes">
+ <Switch
+ id="generate-routes"
+ label="Generate Routes"
+ labelOff="Do not generate Routes"
+ isChecked={this.state.generateRoutes}
+ onChange={checked => this.setState({generateRoutes: checked})}
+ />
+ </FormGroup>}
+ {this.state.type === 'openapi' && this.state.generateRest && <FormGroup fieldId="integrationName" label="Integration name">
+ <TextInput autoComplete="off"
+ id="integrationName"
+ type="text"
+ placeholder="Integration file name with yaml extension"
+ required
+
+ onChange={value => this.setState({integrationName: value})}
+ />
+ </FormGroup>}
+ </Form>
+ </Modal>
+ )
+ }
+};
\ No newline at end of file
diff --git a/karavan-app/src/main/webapp/src/kamelets/KameletsPage.tsx b/karavan-app/src/main/webapp/src/kamelets/KameletsPage.tsx
index a3e0cd4..f4c26ce 100644
--- a/karavan-app/src/main/webapp/src/kamelets/KameletsPage.tsx
+++ b/karavan-app/src/main/webapp/src/kamelets/KameletsPage.tsx
@@ -5,7 +5,7 @@ import {
Gallery,
ToolbarItem,
TextInput,
- PageSection, TextContent, Text, PageSectionVariants, Flex, FlexItem
+ PageSection, TextContent, Text, PageSectionVariants, Flex, FlexItem, Badge
} from '@patternfly/react-core';
import '../designer/karavan.css';
import {KameletCard} from "./KameletCard";
@@ -60,8 +60,9 @@ export class KameletsPage extends React.Component<Props, State> {
<PageSection className="tools-section" variant={this.props.dark ? PageSectionVariants.darker : PageSectionVariants.light}>
<Flex className="tools" justifyContent={{default: 'justifyContentSpaceBetween'}}>
<FlexItem>
- <TextContent>
- <Text component="h1">Kamelet Catalog</Text>
+ <TextContent className="header">
+ <Text component="h2">Kamelet Catalog</Text>
+ <Badge isRead className="labels">{this.state.kamelets.length}</Badge>
</TextContent>
</FlexItem>
<FlexItem>