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>