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>