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:16 UTC

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

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>