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 2024/02/02 22:42:13 UTC

(camel-karavan) branch main updated (31ddb809 -> a4620125)

This is an automated email from the ASF dual-hosted git repository.

marat pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git


    from 31ddb809 Fix #1094
     new 9df8d7da Bean config wizard
     new be1aeed6 Wizard  #1097
     new f6a7d87a Wizard file selector  #1097
     new a4620125 Fix #1097

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../camel/karavan/api/ProjectFileResource.java     |  13 +-
 .../org/apache/camel/karavan/code/CodeService.java |  12 +-
 .../camel/karavan/service/ProjectService.java      |   9 +
 .../snippets/database-bean-template.camel.yaml     |  22 ++
 .../snippets/messaging-bean-template.camel.yaml    |   9 +
 .../src/main/webui/src/api/KaravanApi.tsx          |  11 +
 .../src/main/webui/src/api/ProjectService.ts       |   1 -
 .../src/main/webui/src/api/ProjectStore.ts         |  13 +
 .../src/main/webui/src/designer/DesignerStore.ts   |   2 +-
 .../src/main/webui/src/project/ProjectPanel.tsx    |  30 ++-
 .../beans/BeanFilesDropdown.css}                   |  20 +-
 .../webui/src/project/beans/BeanFilesDropdown.tsx  |  85 +++++++
 .../main/webui/src/project/beans/BeanWizard.tsx    | 264 +++++++++++++++++++++
 13 files changed, 464 insertions(+), 27 deletions(-)
 create mode 100644 karavan-web/karavan-app/src/main/resources/snippets/database-bean-template.camel.yaml
 create mode 100644 karavan-web/karavan-app/src/main/resources/snippets/messaging-bean-template.camel.yaml
 copy karavan-web/karavan-app/src/main/webui/src/{containers/ContainerPage.css => project/beans/BeanFilesDropdown.css} (75%)
 create mode 100644 karavan-web/karavan-app/src/main/webui/src/project/beans/BeanFilesDropdown.tsx
 create mode 100644 karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx


(camel-karavan) 03/04: Wizard file selector #1097

Posted by ma...@apache.org.
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

commit f6a7d87a63a9386e5477510cf151fb0622843ef3
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Feb 2 17:38:12 2024 -0500

    Wizard file selector  #1097
---
 .../src/main/webui/src/api/ProjectService.ts       |   1 -
 .../webui/src/project/beans/BeanFilesDropdown.css  |  29 +++++
 .../webui/src/project/beans/BeanFilesDropdown.tsx  |  85 ++++++++++++
 .../main/webui/src/project/beans/BeanWizard.tsx    | 145 ++++++++++++---------
 4 files changed, 198 insertions(+), 62 deletions(-)

diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
index 0876b506..9f9891f4 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
@@ -30,7 +30,6 @@ import {
 import {ProjectEventBus} from './ProjectEventBus';
 import {EventBus} from "../designer/utils/EventBus";
 import {KameletApi} from "karavan-core/lib/api/KameletApi";
-import {AxiosResponse} from "axios";
 
 export class ProjectService {
 
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanFilesDropdown.css b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanFilesDropdown.css
new file mode 100644
index 00000000..d035c7e8
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanFilesDropdown.css
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+.bean-wizard .bean-wizard-toggle {
+    padding-left: 6px;
+    padding-right: 6px;
+}
+
+.bean-wizard .bean-wizard-toggle .pf-v5-c-button__icon.pf-m-start {
+    margin-inline-end: 0;
+}
+
+.bean-wizard .bean-wizard-toggle .pf-v5-c-menu-toggle__controls {
+    display: none;
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanFilesDropdown.tsx b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanFilesDropdown.tsx
new file mode 100644
index 00000000..92dbc396
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanFilesDropdown.tsx
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, {useState} from 'react';
+import {
+    Dropdown,
+    MenuToggleElement,
+    MenuToggle,
+    DropdownList, DropdownItem
+} from '@patternfly/react-core';
+import './BeanFilesDropdown.css';
+import "@patternfly/patternfly/patternfly.css";
+import {shallow} from "zustand/shallow";
+import EllipsisVIcon from "@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon";
+import {useFilesStore} from "../../api/ProjectStore";
+
+const CAMEL_YAML_EXT = ".camel.yaml";
+
+interface Props {
+    onSelect: (filename: string, event?: React.MouseEvent<Element, MouseEvent>) => void;
+}
+
+export function BeanFilesDropdown(props: Props) {
+
+    const [files] = useFilesStore((s) => [s.files], shallow);
+    const [isOpenDropdown, setIsOpenDropdown] = useState<boolean>(false);
+
+    function onMenuToggleClick() {
+        setIsOpenDropdown(!isOpenDropdown)
+    }
+
+    function getToggle(toggleRef: React.Ref<MenuToggleElement>) {
+        return (
+            <MenuToggle className="bean-wizard-toggle"
+                        id={'popoverId'}
+                        ref={toggleRef}
+                        aria-label="placeholder menu"
+                        variant="default"
+                        onClick={() => onMenuToggleClick()}
+                        isExpanded={isOpenDropdown}
+            >
+                <EllipsisVIcon/>
+            </MenuToggle>
+        )
+    }
+
+    const camelYamlFiles = files.filter(f => f.name.endsWith(CAMEL_YAML_EXT)).map(f => f.name);
+
+    return (
+        (files && files.length > 0 ) ?
+            <Dropdown
+                popperProps={{position: "end"}}
+                isOpen={isOpenDropdown}
+                onSelect={(e, value) => {
+                    if (value) {
+                        props.onSelect(value?.toString().replace(CAMEL_YAML_EXT, ''), e);
+                    }
+                    setIsOpenDropdown(false);
+                }}
+                onOpenChange={(isOpen: boolean) => setIsOpenDropdown(isOpen)}
+                toggle={(toggleRef: React.Ref<MenuToggleElement>) => getToggle(toggleRef)}
+                shouldFocusToggleOnSelect
+            >
+                <DropdownList>
+                    {camelYamlFiles.map((pp, index) =>
+                        <DropdownItem value={pp} key={index}>{pp}</DropdownItem>
+                    )}
+                </DropdownList>
+            </Dropdown>
+            : <></>
+    )
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
index 55bbc040..fa7dff0e 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
@@ -18,7 +18,7 @@ import React, {useEffect, useMemo, useState} from 'react';
 import {
     capitalize,
     Flex,
-    Form, FormGroup, FormHelperText, HelperText, HelperTextItem,
+    Form, FormGroup, FormHelperText, HelperText, HelperTextItem, InputGroup, InputGroupItem,
     Modal,
     ModalVariant,
     Radio, Text, TextInput,
@@ -38,8 +38,10 @@ import * as yup from "yup";
 import {ProjectService} from "../../api/ProjectService";
 import {EventBus} from "../../designer/utils/EventBus";
 import {useResponseErrorHandler} from "../../shared/error/UseResponseErrorHandler";
-import {Beans, Integration} from "karavan-core/lib/model/IntegrationDefinition";
+import {Integration} from "karavan-core/lib/model/IntegrationDefinition";
 import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml";
+import {BeanFilesDropdown} from "./BeanFilesDropdown";
+import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt";
 
 const CAMEL_YAML_EXT = ".camel.yaml";
 const EMPTY_BEAN = "empty";
@@ -58,8 +60,9 @@ export function BeanWizard() {
         register,
         setError,
         handleSubmit,
-        formState: { errors },
-        reset
+        formState: {errors},
+        reset,
+        setValue
     } = useForm({
         resolver: yupResolver(formValidationSchema),
         mode: "onChange",
@@ -87,32 +90,40 @@ export function BeanWizard() {
         setError
     );
 
-    function handleOnFormSubmitSuccess (file: ProjectFile) {
+    function handleOnFormSubmitSuccess(file: ProjectFile) {
         const message = "File successfully created.";
-        EventBus.sendAlert( "Success", message, "success");
+        EventBus.sendAlert("Success", message, "success");
         ProjectService.refreshProjectData(file.projectId);
         setFile('select', file, designerTab);
         setShowWizard(false);
     }
 
     function handleFormSubmit() {
-        console.log("!!!", bean)
-        let code = '{}';
-        if (bean !== undefined && templateName !== EMPTY_BEAN) {
-            const i = Integration.createNew("temp");
-            i.spec.flows?.push(new Beans({beans: [bean]}))
-            code = CamelDefinitionYaml.integrationToYaml(i);
+        const file = files.filter(f=> f.name === (filename + CAMEL_YAML_EXT)).at(0);
+        if (file && bean !== undefined) {
+            const i = CamelDefinitionYaml.yamlToIntegration(file.name, file.code);
+            const i2 = CamelDefinitionApiExt.addBeanToIntegration(i, bean);
+            const file2 = {...file} as ProjectFile;
+            file2.code = CamelDefinitionYaml.integrationToYaml(i2);
+            ProjectService.updateFile(file2, false);
+            handleOnFormSubmitSuccess(file2);
+        } else {
+            let code = '{}';
+            if (bean !== undefined && templateName !== EMPTY_BEAN) {
+                const i = Integration.createNew("temp");
+                const i2 = CamelDefinitionApiExt.addBeanToIntegration(i, bean);
+                code = CamelDefinitionYaml.integrationToYaml(i2);
+            }
+            const fullFileName = filename + CAMEL_YAML_EXT;
+            const file = new ProjectFile(fullFileName, project.projectId, code, Date.now());
+            return ProjectService.createFile(file)
+                .then(() => handleOnFormSubmitSuccess(file))
+                .catch((error) => registerResponseErrors(error));
         }
-        const fullFileName = filename + CAMEL_YAML_EXT;
-        const file = new ProjectFile(fullFileName, project.projectId, code, Date.now());
-        return ProjectService.createFile(file)
-            .then(() => handleOnFormSubmitSuccess(file))
-            .catch((error) => registerResponseErrors(error));
     }
 
     useEffect(() => {
         if (showWizard) {
-            console.log("useEffect", "celan")
             reset({filename: ''})
             setFilename('')
             setTemplateName('');
@@ -132,7 +143,7 @@ export function BeanWizard() {
 
     useEffect(() => {
         setBeanName(templateBeanName);
-        getBeans.filter(b=> b.name === templateBeanName).forEach(b => {
+        getBeans.filter(b => b.name === templateBeanName).forEach(b => {
             Object.getOwnPropertyNames(b.properties).forEach(prop => {
                 setBean(new RegistryBeanDefinition({...b}))
             })
@@ -140,7 +151,7 @@ export function BeanWizard() {
     }, [templateBeanName]);
 
 
-    function getRegistryBeanDefinitions():RegistryBeanDefinition[] {
+    function getRegistryBeanDefinitions(): RegistryBeanDefinition[] {
         const fs = templateFiles
             .filter(f => f.name === templateName.concat(BEAN_TEMPLATE_SUFFIX_FILENAME));
         return CodeUtils.getBeans(fs);
@@ -151,24 +162,27 @@ export function BeanWizard() {
     return (
         <Modal title={"Bean"} onClose={_ => setShowWizard(false)}
                variant={ModalVariant.medium} isOpen={showWizard} onEscapePress={() => setShowWizard(false)}>
-            <Wizard height={600} onClose={() => setShowWizard(false)} onSubmit={event => handleFormSubmit()}>
+            <Wizard className="bean-wizard" height={600} onClose={() => setShowWizard(false)} onSubmit={event => handleFormSubmit()}>
                 <WizardStep name={"Type"} id="type"
-                            footer={{ isNextDisabled: !templateNames.includes(templateName) && templateName !== EMPTY_BEAN }}
+                            footer={{isNextDisabled: !templateNames.includes(templateName) && templateName !== EMPTY_BEAN}}
                 >
-                    <Flex direction={{default:"column"}} gap={{default:'gapLg'}}>
-                        <Radio key={EMPTY_BEAN} id={EMPTY_BEAN} label={capitalize(EMPTY_BEAN)} name={EMPTY_BEAN} isChecked={EMPTY_BEAN === templateName} onChange={_ => setTemplateName(EMPTY_BEAN)} />
-                        {templateNames.map(n => <Radio key={n} id={n} label={capitalize(n)} name={n} isChecked={n === templateName}
-                                                       onChange={_ => setTemplateName(n)} />)}
+                    <Flex direction={{default: "column"}} gap={{default: 'gapLg'}}>
+                        <Radio key={EMPTY_BEAN} id={EMPTY_BEAN} label={capitalize(EMPTY_BEAN)} name={EMPTY_BEAN}
+                               isChecked={EMPTY_BEAN === templateName} onChange={_ => setTemplateName(EMPTY_BEAN)}/>
+                        {templateNames.map(n => <Radio key={n} id={n} label={capitalize(n)} name={n}
+                                                       isChecked={n === templateName}
+                                                       onChange={_ => setTemplateName(n)}/>)}
                     </Flex>
                 </WizardStep>
                 <WizardStep name={"Template"} id="template"
                             isHidden={templateName === EMPTY_BEAN}
                             isDisabled={templateName.length == 0}
-                            footer={{ isNextDisabled: !getBeans.map(b=> b.name).includes(templateBeanName) }}
+                            footer={{isNextDisabled: !getBeans.map(b => b.name).includes(templateBeanName)}}
                 >
-                    <Flex direction={{default:"column"}} gap={{default:'gapLg'}}>
-                    {getBeans.map(b => <Radio key={b.name} id={b.name} label={b.name} name={b.name} isChecked={b.name === templateBeanName}
-                                               onChange={_ => setTemplateBeanName(b.name)} />)}
+                    <Flex direction={{default: "column"}} gap={{default: 'gapLg'}}>
+                        {getBeans.map(b => <Radio key={b.name} id={b.name} label={b.name} name={b.name}
+                                                  isChecked={b.name === templateBeanName}
+                                                  onChange={_ => setTemplateBeanName(b.name)}/>)}
                     </Flex>
                 </WizardStep>
                 <WizardStep name="Properties" id="properties"
@@ -185,48 +199,57 @@ export function BeanWizard() {
                             />
                         </FormGroup>
                         <FormGroup label="Properties:" fieldId="properties"/>
-                        {getBeans.filter(b=> b.name === templateBeanName).map(b => (
-                           <div key={b.name}>
-                               {Object.getOwnPropertyNames(b.properties).map(prop => (
-                                   <FormGroup key={prop} label={prop} fieldId={prop}>
-                                       <TextInput
-                                           value={bean?.properties[prop] || ''}
-                                           id={prop}
-                                           aria-describedby={prop}
-                                           onChange={(_, value) => {
-                                               const b = new RegistryBeanDefinition({...bean});
-                                               b.properties[prop] = value;
-                                               setBean(b);
-                                           }}
-                                       />
-                                   </FormGroup>
-                               ))}
-                           </div>
+                        {getBeans.filter(b => b.name === templateBeanName).map(b => (
+                            <div key={b.name}>
+                                {Object.getOwnPropertyNames(b.properties).map(prop => (
+                                    <FormGroup key={prop} label={prop} fieldId={prop}>
+                                        <TextInput
+                                            value={bean?.properties[prop] || ''}
+                                            id={prop}
+                                            aria-describedby={prop}
+                                            onChange={(_, value) => {
+                                                const b = new RegistryBeanDefinition({...bean});
+                                                b.properties[prop] = value;
+                                                setBean(b);
+                                            }}
+                                        />
+                                    </FormGroup>
+                                ))}
+                            </div>
                         ))}
                     </Form>
                 </WizardStep>
                 <WizardStep name={"File"} id={"file"}
-                            footer={{ nextButtonText: 'Save', onNext: handleSubmit(handleFormSubmit) }}
+                            footer={{nextButtonText: 'Save', onNext: handleSubmit(handleFormSubmit)}}
                             isDisabled={(templateName.length == 0 || templateBeanName.length == 0) && templateName !== EMPTY_BEAN}
                 >
                     <Form autoComplete="off">
                         <FormGroup label="Filename" fieldId="filename" isRequired>
-                            <TextInput className="text-field" type="text" id="filename"
-                                       aria-label="filename"
-                                       value={filename}
-                                       customIcon={<Text>{CAMEL_YAML_EXT}</Text>}
-                                       validated={!!errors.filename ? 'error' : 'default'}
-                                       {...register('filename')}
-                                       // validated={!!errors.name ? 'error' : 'default'}
-                                       onChange={(e, value) => {
-                                           setFilename(value);
-                                           register('filename').onChange(e);
-                                       }}
-                            />
+                            <InputGroup>
+                                <InputGroupItem isFill>
+                                    <TextInput className="text-field" type="text" id="filename"
+                                               aria-label="filename"
+                                               value={filename}
+                                               customIcon={<Text>{CAMEL_YAML_EXT}</Text>}
+                                               validated={!!errors.filename ? 'error' : 'default'}
+                                               {...register('filename')}
+                                               onChange={(e, value) => {
+                                                   setFilename(value);
+                                                   register('filename').onChange(e);
+                                               }}
+                                    />
+                                </InputGroupItem>
+                                <InputGroupItem>
+                                    <BeanFilesDropdown {...register('filename')} onSelect={(fn, event) => {
+                                        setFilename(fn);
+                                        setValue('filename', fn, {shouldValidate: true});
+                                    }}/>
+                                </InputGroupItem>
+                            </InputGroup>
                             {!!errors.filename && (
                                 <FormHelperText>
                                     <HelperText>
-                                        <HelperTextItem icon={<ExclamationCircleIcon />} variant={"error"}>
+                                        <HelperTextItem icon={<ExclamationCircleIcon/>} variant={"error"}>
                                             {errors?.filename?.message}
                                         </HelperTextItem>
                                     </HelperText>


(camel-karavan) 04/04: Fix #1097

Posted by ma...@apache.org.
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

commit a4620125151544a5502c5bb7cd0c03b89bb2b627
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Feb 2 17:41:55 2024 -0500

    Fix #1097
---
 .../karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx       | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
index fa7dff0e..896dbe12 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
@@ -239,12 +239,12 @@ export function BeanWizard() {
                                                }}
                                     />
                                 </InputGroupItem>
-                                <InputGroupItem>
+                                {templateName !== EMPTY_BEAN && <InputGroupItem>
                                     <BeanFilesDropdown {...register('filename')} onSelect={(fn, event) => {
                                         setFilename(fn);
                                         setValue('filename', fn, {shouldValidate: true});
                                     }}/>
-                                </InputGroupItem>
+                                </InputGroupItem>}
                             </InputGroup>
                             {!!errors.filename && (
                                 <FormHelperText>


(camel-karavan) 02/04: Wizard #1097

Posted by ma...@apache.org.
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

commit be1aeed6dd10ab80124354a1981aae78d7390026
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Feb 2 15:32:44 2024 -0500

    Wizard  #1097
---
 .../main/webui/src/project/beans/BeanWizard.tsx    | 173 ++++++++++++++++++---
 1 file changed, 148 insertions(+), 25 deletions(-)

diff --git a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
index 1adc5d0f..55bbc040 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
@@ -16,54 +16,132 @@
  */
 import React, {useEffect, useMemo, useState} from 'react';
 import {
-    Badge,
     capitalize,
     Flex,
-    Form, FormGroup,
+    Form, FormGroup, FormHelperText, HelperText, HelperTextItem,
     Modal,
     ModalVariant,
     Radio, Text, TextInput,
     Wizard,
-    WizardHeader,
     WizardStep
 } from '@patternfly/react-core';
 import {KaravanApi} from "../../api/KaravanApi";
 import {RegistryBeanDefinition} from "karavan-core/lib/model/CamelDefinition";
 import {CodeUtils} from "../../util/CodeUtils";
-import {ProjectFile, ProjectType} from "../../api/ProjectModels";
-import {useWizardStore} from "../../api/ProjectStore";
+import {ProjectFile} from "../../api/ProjectModels";
+import {useFilesStore, useFileStore, useProjectStore, useWizardStore} from "../../api/ProjectStore";
 import {shallow} from "zustand/shallow";
+import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
+import {useForm} from "react-hook-form";
+import {yupResolver} from "@hookform/resolvers/yup";
+import * as yup from "yup";
 import {ProjectService} from "../../api/ProjectService";
+import {EventBus} from "../../designer/utils/EventBus";
+import {useResponseErrorHandler} from "../../shared/error/UseResponseErrorHandler";
+import {Beans, Integration} from "karavan-core/lib/model/IntegrationDefinition";
+import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml";
 
+const CAMEL_YAML_EXT = ".camel.yaml";
+const EMPTY_BEAN = "empty";
 const BEAN_TEMPLATE_SUFFIX_FILENAME = "-bean-template.camel.yaml";
 
 export function BeanWizard() {
 
+    const formValidationSchema = yup.object().shape({
+        filename: yup
+            .string()
+            .matches(/^[a-zA-Z0-9_\-.]+$/, 'Incorrect filename')
+            .required("File name is required"),
+    });
+
+    const {
+        register,
+        setError,
+        handleSubmit,
+        formState: { errors },
+        reset
+    } = useForm({
+        resolver: yupResolver(formValidationSchema),
+        mode: "onChange",
+        defaultValues: {filename: ''}
+    });
+
+    const responseToFormErrorFields = new Map<string, string>([
+        ["filename", "filename"]
+    ]);
+
+    const [project] = useProjectStore((s) => [s.project], shallow);
+    const [operation, setFile, designerTab] = useFileStore((s) => [s.operation, s.setFile, s.designerTab], shallow);
+    const [files] = useFilesStore((s) => [s.files], shallow);
     const [showWizard, setShowWizard] = useWizardStore((s) => [s.showWizard, s.setShowWizard], shallow)
-    const [files, setFiles] = useState<ProjectFile[]>([]);
+    const [templateFiles, setTemplateFiles] = useState<ProjectFile[]>([]);
     const [templateNames, setTemplateNames] = useState<string[]>([]);
     const [templateName, setTemplateName] = useState<string>('');
+    const [templateBeanName, setTemplateBeanName] = useState<string>('');
+    const [bean, setBean] = useState<RegistryBeanDefinition | undefined>();
+    const [filename, setFilename] = useState<string>('');
     const [beanName, setBeanName] = useState<string>('');
 
+    const [globalErrors, registerResponseErrors, resetGlobalErrors] = useResponseErrorHandler(
+        responseToFormErrorFields,
+        setError
+    );
+
+    function handleOnFormSubmitSuccess (file: ProjectFile) {
+        const message = "File successfully created.";
+        EventBus.sendAlert( "Success", message, "success");
+        ProjectService.refreshProjectData(file.projectId);
+        setFile('select', file, designerTab);
+        setShowWizard(false);
+    }
+
+    function handleFormSubmit() {
+        console.log("!!!", bean)
+        let code = '{}';
+        if (bean !== undefined && templateName !== EMPTY_BEAN) {
+            const i = Integration.createNew("temp");
+            i.spec.flows?.push(new Beans({beans: [bean]}))
+            code = CamelDefinitionYaml.integrationToYaml(i);
+        }
+        const fullFileName = filename + CAMEL_YAML_EXT;
+        const file = new ProjectFile(fullFileName, project.projectId, code, Date.now());
+        return ProjectService.createFile(file)
+            .then(() => handleOnFormSubmitSuccess(file))
+            .catch((error) => registerResponseErrors(error));
+    }
+
     useEffect(() => {
         if (showWizard) {
+            console.log("useEffect", "celan")
+            reset({filename: ''})
+            setFilename('')
+            setTemplateName('');
+            setTemplateBeanName('');
+            setBean(undefined);
             KaravanApi.getBeanTemplatesFiles(files => {
                 const templateNames = files.map(file => file.name.replace(BEAN_TEMPLATE_SUFFIX_FILENAME, ''));
-                setFiles(prevState => {
+                setTemplateFiles(prevState => {
                     return [...files];
                 });
                 setTemplateNames(prevState => {
                     return [...templateNames];
                 });
-                setTemplateName('');
-                setBeanName('');
             });
         }
     }, [showWizard]);
 
+    useEffect(() => {
+        setBeanName(templateBeanName);
+        getBeans.filter(b=> b.name === templateBeanName).forEach(b => {
+            Object.getOwnPropertyNames(b.properties).forEach(prop => {
+                setBean(new RegistryBeanDefinition({...b}))
+            })
+        });
+    }, [templateBeanName]);
+
 
     function getRegistryBeanDefinitions():RegistryBeanDefinition[] {
-        const fs = files
+        const fs = templateFiles
             .filter(f => f.name === templateName.concat(BEAN_TEMPLATE_SUFFIX_FILENAME));
         return CodeUtils.getBeans(fs);
     }
@@ -73,45 +151,90 @@ export function BeanWizard() {
     return (
         <Modal title={"Bean"} onClose={_ => setShowWizard(false)}
                variant={ModalVariant.medium} isOpen={showWizard} onEscapePress={() => setShowWizard(false)}>
-            <Wizard height={600} title="Bean configuration" onClose={() => setShowWizard(false)}>
+            <Wizard height={600} onClose={() => setShowWizard(false)} onSubmit={event => handleFormSubmit()}>
                 <WizardStep name={"Type"} id="type"
-                            footer={{ isNextDisabled: !templateNames.includes(templateName) }}
+                            footer={{ isNextDisabled: !templateNames.includes(templateName) && templateName !== EMPTY_BEAN }}
                 >
                     <Flex direction={{default:"column"}} gap={{default:'gapLg'}}>
-                        {templateNames.map(n => <Radio id={n} label={capitalize(n)} name={n} isChecked={n === templateName}
+                        <Radio key={EMPTY_BEAN} id={EMPTY_BEAN} label={capitalize(EMPTY_BEAN)} name={EMPTY_BEAN} isChecked={EMPTY_BEAN === templateName} onChange={_ => setTemplateName(EMPTY_BEAN)} />
+                        {templateNames.map(n => <Radio key={n} id={n} label={capitalize(n)} name={n} isChecked={n === templateName}
                                                        onChange={_ => setTemplateName(n)} />)}
                     </Flex>
                 </WizardStep>
                 <WizardStep name={"Template"} id="template"
+                            isHidden={templateName === EMPTY_BEAN}
                             isDisabled={templateName.length == 0}
-                            footer={{ isNextDisabled: !getBeans.map(b=> b.name).includes(beanName) }}
+                            footer={{ isNextDisabled: !getBeans.map(b=> b.name).includes(templateBeanName) }}
                 >
                     <Flex direction={{default:"column"}} gap={{default:'gapLg'}}>
-                    {getBeans.map(b => <Radio id={b.name} label={b.name} name={b.name} isChecked={b.name === beanName}
-                                               onChange={_ => setBeanName(b.name)} />)}
+                    {getBeans.map(b => <Radio key={b.name} id={b.name} label={b.name} name={b.name} isChecked={b.name === templateBeanName}
+                                               onChange={_ => setTemplateBeanName(b.name)} />)}
                     </Flex>
                 </WizardStep>
                 <WizardStep name="Properties" id="properties"
-                            isDisabled={templateName.length == 0 || beanName.length == 0}
-                            footer={{ nextButtonText: 'Add bean' }}
+                            isHidden={templateName === EMPTY_BEAN}
+                            isDisabled={templateName.length == 0 || templateBeanName.length == 0}
                 >
-                    <Form>
-                        {getBeans.filter(b=> b.name === beanName).map(b => (
-                           <>
+                    <Form autoComplete="off">
+                        <FormGroup key={"beanName"} label={"Name"} fieldId={"beanName"}>
+                            <TextInput
+                                value={beanName}
+                                id={"beanName"}
+                                aria-describedby={'beanName'}
+                                onChange={(_, value) => setBeanName(value)}
+                            />
+                        </FormGroup>
+                        <FormGroup label="Properties:" fieldId="properties"/>
+                        {getBeans.filter(b=> b.name === templateBeanName).map(b => (
+                           <div key={b.name}>
                                {Object.getOwnPropertyNames(b.properties).map(prop => (
-                                   <FormGroup label={prop} fieldId={prop}>
+                                   <FormGroup key={prop} label={prop} fieldId={prop}>
                                        <TextInput
-                                           value={b.properties[prop]}
+                                           value={bean?.properties[prop] || ''}
                                            id={prop}
                                            aria-describedby={prop}
-                                           onChange={(_, value) => {}}
+                                           onChange={(_, value) => {
+                                               const b = new RegistryBeanDefinition({...bean});
+                                               b.properties[prop] = value;
+                                               setBean(b);
+                                           }}
                                        />
                                    </FormGroup>
                                ))}
-                           </>
+                           </div>
                         ))}
                     </Form>
                 </WizardStep>
+                <WizardStep name={"File"} id={"file"}
+                            footer={{ nextButtonText: 'Save', onNext: handleSubmit(handleFormSubmit) }}
+                            isDisabled={(templateName.length == 0 || templateBeanName.length == 0) && templateName !== EMPTY_BEAN}
+                >
+                    <Form autoComplete="off">
+                        <FormGroup label="Filename" fieldId="filename" isRequired>
+                            <TextInput className="text-field" type="text" id="filename"
+                                       aria-label="filename"
+                                       value={filename}
+                                       customIcon={<Text>{CAMEL_YAML_EXT}</Text>}
+                                       validated={!!errors.filename ? 'error' : 'default'}
+                                       {...register('filename')}
+                                       // validated={!!errors.name ? 'error' : 'default'}
+                                       onChange={(e, value) => {
+                                           setFilename(value);
+                                           register('filename').onChange(e);
+                                       }}
+                            />
+                            {!!errors.filename && (
+                                <FormHelperText>
+                                    <HelperText>
+                                        <HelperTextItem icon={<ExclamationCircleIcon />} variant={"error"}>
+                                            {errors?.filename?.message}
+                                        </HelperTextItem>
+                                    </HelperText>
+                                </FormHelperText>
+                            )}
+                        </FormGroup>
+                    </Form>
+                </WizardStep>
             </Wizard>
         </Modal>
     )


(camel-karavan) 01/04: Bean config wizard

Posted by ma...@apache.org.
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

commit 9df8d7da7e673f5a4abd6302f2cefd644eb1cf62
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Thu Feb 1 20:00:51 2024 -0500

    Bean config wizard
---
 .../camel/karavan/api/ProjectFileResource.java     |  13 ++-
 .../org/apache/camel/karavan/code/CodeService.java |  12 ++-
 .../camel/karavan/service/ProjectService.java      |   9 ++
 .../snippets/database-bean-template.camel.yaml     |  22 ++++
 .../snippets/messaging-bean-template.camel.yaml    |   9 ++
 .../src/main/webui/src/api/KaravanApi.tsx          |  11 ++
 .../src/main/webui/src/api/ProjectStore.ts         |  13 +++
 .../src/main/webui/src/designer/DesignerStore.ts   |   2 +-
 .../src/main/webui/src/project/ProjectPanel.tsx    |  30 +++---
 .../main/webui/src/project/beans/BeanWizard.tsx    | 118 +++++++++++++++++++++
 10 files changed, 223 insertions(+), 16 deletions(-)

diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
index 77776f6c..41b78850 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
@@ -21,6 +21,7 @@ import jakarta.ws.rs.*;
 import jakarta.ws.rs.core.MediaType;
 import org.apache.camel.karavan.code.CodeService;
 import org.apache.camel.karavan.infinispan.InfinispanService;
+import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.infinispan.model.ProjectFile;
 import org.apache.camel.karavan.validation.project.ProjectFileCreateValidator;
 
@@ -46,13 +47,21 @@ public class ProjectFileResource {
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/{projectId}")
-    public List<ProjectFile> get(@HeaderParam("username") String username,
-                                 @PathParam("projectId") String projectId) throws Exception {
+    public List<ProjectFile> get(@PathParam("projectId") String projectId) throws Exception {
         return infinispanService.getProjectFiles(projectId).stream()
                 .sorted(Comparator.comparing(ProjectFile::getName))
                 .collect(Collectors.toList());
     }
 
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/templates/beans")
+    public List<ProjectFile> getBeanTemplates() throws Exception {
+        return  codeService.getBeanTemplateNames().stream()
+                .map(s -> infinispanService.getProjectFile(Project.Type.templates.name(), s))
+                .toList();
+    }
+
     @POST
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java
index e519b5a4..12644148 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java
@@ -58,6 +58,7 @@ public class CodeService {
     private static final Logger LOGGER = Logger.getLogger(CodeService.class.getName());
     public static final String APPLICATION_PROPERTIES_FILENAME = "application.properties";
     public static final String BUILD_SCRIPT_FILENAME = "build.sh";
+    public static final String BEAN_TEMPLATE_SUFFIX_FILENAME = "-bean-template.camel.yaml";
     public static final String DEV_SERVICES_FILENAME = "devservices.yaml";
     public static final String PROJECT_COMPOSE_FILENAME = "docker-compose.yaml";
     public static final String MARKDOWN_EXTENSION = ".md";
@@ -86,6 +87,7 @@ public class CodeService {
     @Inject
     Vertx vertx;
 
+    List<String> beansTemplates = List.of("database", "messaging");
     List<String> targets = List.of("openshift", "kubernetes", "docker");
     List<String> interfaces = List.of("org.apache.camel.AggregationStrategy.java", "org.apache.camel.Processor.java");
 
@@ -185,6 +187,10 @@ public class CodeService {
         return null;
     }
 
+    public List<String> getBeanTemplateNames(){
+        return beansTemplates.stream().map(name -> name + BEAN_TEMPLATE_SUFFIX_FILENAME).toList();
+    }
+
     public Map<String, String> getTemplates() {
         Map<String, String> result = new HashMap<>();
 
@@ -192,10 +198,14 @@ public class CodeService {
         files.addAll(targets.stream().map(target -> target + "-" + APPLICATION_PROPERTIES_FILENAME).toList());
         files.addAll(targets.stream().map(target ->  target + "-" + BUILD_SCRIPT_FILENAME).toList());
 
+        files.addAll(getBeanTemplateNames());
+
         files.forEach(file -> {
             String templatePath = SNIPPETS_PATH + file;
             String templateText = getResourceFile(templatePath);
-            result.put(file, templateText);
+            if (templateText != null) {
+                result.put(file, templateText);
+            }
         });
 
         result.put(PROJECT_COMPOSE_FILENAME, getResourceFile(SNIPPETS_PATH + PROJECT_COMPOSE_FILENAME));
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
index 826990ec..aaebf8ae 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
@@ -386,6 +386,15 @@ public class ProjectService implements HealthCheck {
                     infinispanService.saveProjectFile(file);
                 });
                 commitAndPushProject(Project.Type.templates.name(), "Add default templates");
+            } else {
+                LOGGER.info("Add new templates if any");
+                codeService.getTemplates().forEach((name, value) -> {
+                    ProjectFile f = infinispanService.getProjectFile(Project.Type.templates.name(), name);
+                    if (f == null) {
+                        ProjectFile file = new ProjectFile(name, value, Project.Type.templates.name(), Instant.now().toEpochMilli());
+                        infinispanService.saveProjectFile(file);
+                    }
+                });
             }
         } catch (Exception e) {
             LOGGER.error("Error during templates project creation", e);
diff --git a/karavan-web/karavan-app/src/main/resources/snippets/database-bean-template.camel.yaml b/karavan-web/karavan-app/src/main/resources/snippets/database-bean-template.camel.yaml
new file mode 100644
index 00000000..b2e8bc61
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/resources/snippets/database-bean-template.camel.yaml
@@ -0,0 +1,22 @@
+- beans:
+    - name: PostgresDatabase
+      properties:
+        username: 'username'
+        password: 'password'
+        url: 'jdbc:postgresql://serverName:serverPort/databaseName'
+        driverClassName: org.postgresql.Driver
+      type: '#class:org.apache.commons.dbcp2.BasicDataSource'
+    - name: MySqlDatabase
+      properties:
+        username: 'username'
+        password: 'password'
+        url: 'jdbc:mysql://serverName:serverPort/databaseName'
+        driverClassName: com.mysql.cj.jdbc.Driver
+      type: '#class:org.apache.commons.dbcp2.BasicDataSource'
+    - name: OracleDatabase
+      properties:
+        username: 'username'
+        password: 'password'
+        url: 'jdbc:oracle:thin:@serverName:serverPort/databaseName'
+        driverClassName: oracle.jdbc.driver.OracleDriver
+      type: '#class:org.apache.commons.dbcp2.BasicDataSource'
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/resources/snippets/messaging-bean-template.camel.yaml b/karavan-web/karavan-app/src/main/resources/snippets/messaging-bean-template.camel.yaml
new file mode 100644
index 00000000..ee05cbff
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/resources/snippets/messaging-bean-template.camel.yaml
@@ -0,0 +1,9 @@
+- beans:
+    - name: JMSArtemis
+      properties:
+        brokerURL: 'tcp://serverHost:serverPort'
+      type: '#class:org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory'
+    - name: AMQP
+      properties:
+        brokerURL: 'amqp://serverHost:serverPort'
+      type: "#class:org.apache.qpid.jms.JmsConnectionFactory"
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
index 4cef25c0..0e963238 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
@@ -318,6 +318,17 @@ export class KaravanApi {
         });
     }
 
+    static async getBeanTemplatesFiles( after: (files: ProjectFile []) => void) {
+        instance.get('/api/file/templates/beans')
+            .then(res => {
+                if (res.status === 200) {
+                    after(res.data);
+                }
+            }).catch(err => {
+            console.log(err);
+        });
+    }
+
     static async getDevModePodStatus(projectId: string, after: (res: AxiosResponse<ContainerStatus>) => void) {
         instance.get('/api/devmode/container/' + projectId)
             .then(res => {
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
index d0078549..ccbb0399 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
@@ -239,6 +239,19 @@ export const useFileStore = createWithEqualityFn<FileState>((set) => ({
     },
 }), shallow)
 
+
+
+interface WizardState {
+    showWizard: boolean;
+    setShowWizard: (showWizard: boolean) => void;
+}
+export const useWizardStore = createWithEqualityFn<WizardState>((set) => ({
+    showWizard: false,
+    setShowWizard: (showWizard: boolean)  => {
+        set({showWizard: showWizard})
+    },
+}), shallow)
+
 interface DevModeState {
     podName?: string,
     status: "none" | "wip",
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/DesignerStore.ts b/karavan-web/karavan-app/src/main/webui/src/designer/DesignerStore.ts
index 8a836e5f..ef95b8d4 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/DesignerStore.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/DesignerStore.ts
@@ -19,7 +19,7 @@ import {CamelElement, Integration} from "karavan-core/lib/model/IntegrationDefin
 import {DslPosition, EventBus} from "./utils/EventBus";
 import {createWithEqualityFn} from "zustand/traditional";
 import {shallow} from "zustand/shallow";
-import {RegistryBeanDefinition} from "karavan-core/src/core/model/CamelDefinition";
+import {RegistryBeanDefinition} from "karavan-core/lib/model/CamelDefinition";
 
 interface IntegrationState {
     integration: Integration;
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
index e9c842af..52d14df8 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
@@ -15,14 +15,14 @@
  * limitations under the License.
  */
 
-import React, {useEffect} from 'react';
+import React, {useEffect, useState} from 'react';
 import {
     Flex,
-    FlexItem, PageSection
+    FlexItem, Modal, ModalVariant, PageSection
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {FilesTab} from "./files/FilesTab";
-import {useAppConfigStore, useFilesStore, useFileStore, useProjectStore} from "../api/ProjectStore";
+import {useAppConfigStore, useFilesStore, useFileStore, useProjectStore, useWizardStore} from "../api/ProjectStore";
 import {DashboardTab} from "./dashboard/DashboardTab";
 import {TraceTab} from "./trace/TraceTab";
 import {ProjectBuildTab} from "./builder/ProjectBuildTab";
@@ -36,6 +36,7 @@ import {Buffer} from "buffer";
 import {CreateFileModal} from "./files/CreateFileModal";
 import {ProjectType} from "../api/ProjectModels";
 import {ReadmeTab} from "./readme/ReadmeTab";
+import {BeanWizard} from "./beans/BeanWizard";
 
 export function ProjectPanel() {
 
@@ -43,6 +44,7 @@ export function ProjectPanel() {
     const [project, tab, setTab] = useProjectStore((s) => [s.project, s.tabIndex, s.setTabIndex], shallow);
     const [setFile] = useFileStore((s) => [s.setFile], shallow);
     const [files] = useFilesStore((s) => [s.files], shallow);
+    const [setShowWizard] = useWizardStore((s) => [s.setShowWizard], shallow)
 
     useEffect(() => {
         onRefresh();
@@ -70,21 +72,25 @@ export function ProjectPanel() {
     const isTopology = tab === 'topology';
 
     const iFiles = files.map(f => new IntegrationFile(f.name, f.code));
-    const codes = iFiles.map(f=>f.code).join("");
+    const codes = iFiles.map(f => f.code).join("");
     const key = Buffer.from(codes).toString('base64')
 
     return isTopology
         ? (
             <>
-            <TopologyTab key={key}
-                         hideToolbar={false}
-                         files={files.map(f => new IntegrationFile(f.name, f.code))}
-                         onClickAddRoute={() => setFile('create', undefined, 'routes')}
-                         onClickAddREST={() => setFile('create', undefined, 'rest')}
-                         onClickAddBean={() => setFile('create', undefined, 'beans')}
-                         onSetFile={(fileName) => selectFile(fileName)}
-            />
+                <TopologyTab key={key}
+                             hideToolbar={false}
+                             files={files.map(f => new IntegrationFile(f.name, f.code))}
+                             onClickAddRoute={() => setFile('create', undefined, 'routes')}
+                             onClickAddREST={() => setFile('create', undefined, 'rest')}
+                             onClickAddBean={() => {
+                                 // setFile('create', undefined, 'beans');
+                                 setShowWizard(true)
+                             }}
+                             onSetFile={(fileName) => selectFile(fileName)}
+                />
                 <CreateFileModal types={['INTEGRATION']} isKameletsProject={false}/>
+                <BeanWizard/>
             </>
         )
         : (<PageSection padding={{default: 'noPadding'}} className="scrollable-out">
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
new file mode 100644
index 00000000..1adc5d0f
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, {useEffect, useMemo, useState} from 'react';
+import {
+    Badge,
+    capitalize,
+    Flex,
+    Form, FormGroup,
+    Modal,
+    ModalVariant,
+    Radio, Text, TextInput,
+    Wizard,
+    WizardHeader,
+    WizardStep
+} from '@patternfly/react-core';
+import {KaravanApi} from "../../api/KaravanApi";
+import {RegistryBeanDefinition} from "karavan-core/lib/model/CamelDefinition";
+import {CodeUtils} from "../../util/CodeUtils";
+import {ProjectFile, ProjectType} from "../../api/ProjectModels";
+import {useWizardStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {ProjectService} from "../../api/ProjectService";
+
+const BEAN_TEMPLATE_SUFFIX_FILENAME = "-bean-template.camel.yaml";
+
+export function BeanWizard() {
+
+    const [showWizard, setShowWizard] = useWizardStore((s) => [s.showWizard, s.setShowWizard], shallow)
+    const [files, setFiles] = useState<ProjectFile[]>([]);
+    const [templateNames, setTemplateNames] = useState<string[]>([]);
+    const [templateName, setTemplateName] = useState<string>('');
+    const [beanName, setBeanName] = useState<string>('');
+
+    useEffect(() => {
+        if (showWizard) {
+            KaravanApi.getBeanTemplatesFiles(files => {
+                const templateNames = files.map(file => file.name.replace(BEAN_TEMPLATE_SUFFIX_FILENAME, ''));
+                setFiles(prevState => {
+                    return [...files];
+                });
+                setTemplateNames(prevState => {
+                    return [...templateNames];
+                });
+                setTemplateName('');
+                setBeanName('');
+            });
+        }
+    }, [showWizard]);
+
+
+    function getRegistryBeanDefinitions():RegistryBeanDefinition[] {
+        const fs = files
+            .filter(f => f.name === templateName.concat(BEAN_TEMPLATE_SUFFIX_FILENAME));
+        return CodeUtils.getBeans(fs);
+    }
+
+    const getBeans = useMemo(() => getRegistryBeanDefinitions(), [templateName]);
+
+    return (
+        <Modal title={"Bean"} onClose={_ => setShowWizard(false)}
+               variant={ModalVariant.medium} isOpen={showWizard} onEscapePress={() => setShowWizard(false)}>
+            <Wizard height={600} title="Bean configuration" onClose={() => setShowWizard(false)}>
+                <WizardStep name={"Type"} id="type"
+                            footer={{ isNextDisabled: !templateNames.includes(templateName) }}
+                >
+                    <Flex direction={{default:"column"}} gap={{default:'gapLg'}}>
+                        {templateNames.map(n => <Radio id={n} label={capitalize(n)} name={n} isChecked={n === templateName}
+                                                       onChange={_ => setTemplateName(n)} />)}
+                    </Flex>
+                </WizardStep>
+                <WizardStep name={"Template"} id="template"
+                            isDisabled={templateName.length == 0}
+                            footer={{ isNextDisabled: !getBeans.map(b=> b.name).includes(beanName) }}
+                >
+                    <Flex direction={{default:"column"}} gap={{default:'gapLg'}}>
+                    {getBeans.map(b => <Radio id={b.name} label={b.name} name={b.name} isChecked={b.name === beanName}
+                                               onChange={_ => setBeanName(b.name)} />)}
+                    </Flex>
+                </WizardStep>
+                <WizardStep name="Properties" id="properties"
+                            isDisabled={templateName.length == 0 || beanName.length == 0}
+                            footer={{ nextButtonText: 'Add bean' }}
+                >
+                    <Form>
+                        {getBeans.filter(b=> b.name === beanName).map(b => (
+                           <>
+                               {Object.getOwnPropertyNames(b.properties).map(prop => (
+                                   <FormGroup label={prop} fieldId={prop}>
+                                       <TextInput
+                                           value={b.properties[prop]}
+                                           id={prop}
+                                           aria-describedby={prop}
+                                           onChange={(_, value) => {}}
+                                       />
+                                   </FormGroup>
+                               ))}
+                           </>
+                        ))}
+                    </Form>
+                </WizardStep>
+            </Wizard>
+        </Modal>
+    )
+}
\ No newline at end of file