This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
The following commit(s) were added to refs/heads/main by this push: new edb92d34 Bean Wizard edb92d34 is described below commit edb92d343f6cd8bf3ec121aa412451d82ea26be2 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Thu Apr 25 11:22:46 2024 -0400 Bean Wizard --- .../main/webui/src/designer/KaravanDesigner.tsx | 9 +- .../main/webui/src/designer/editor/CodeEditor.tsx | 4 +- .../webui/src/designer/property/DslProperties.tsx | 2 +- .../webui/src/designer/route/DslConnections.tsx | 92 ++++++++-------- .../main/webui/src/project/beans/BeanWizard.tsx | 64 ++++------- .../src/main/webui/src/util/useFormUtil.tsx | 119 ++++++++++++++------- 6 files changed, 161 insertions(+), 129 deletions(-) diff --git a/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx b/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx index cee6fbc4..ea97a7e1 100644 --- a/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx +++ b/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx @@ -128,8 +128,13 @@ export function KaravanDesigner(props: Props) { } function getCode(integration: Integration): string { - const clone = CamelUtil.cloneIntegration(integration); - return CamelDefinitionYaml.integrationToYaml(clone); + try { + const clone = CamelUtil.cloneIntegration(integration); + return CamelDefinitionYaml.integrationToYaml(clone); + } catch (e) { + EventBus.sendAlert('Error parsing Yaml', (e as Error).message, 'danger'); + return ''; + } } function getTab(title: string, tooltip: string, icon: string, showBadge: boolean = false) { diff --git a/karavan-app/src/main/webui/src/designer/editor/CodeEditor.tsx b/karavan-app/src/main/webui/src/designer/editor/CodeEditor.tsx index d833148e..4bb4e3c5 100644 --- a/karavan-app/src/main/webui/src/designer/editor/CodeEditor.tsx +++ b/karavan-app/src/main/webui/src/designer/editor/CodeEditor.tsx @@ -20,6 +20,7 @@ import Editor from "@monaco-editor/react"; import {shallow} from "zustand/shallow"; import {useDesignerStore, useIntegrationStore} from "../DesignerStore"; import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; export function CodeEditor () { @@ -29,7 +30,8 @@ export function CodeEditor () { useEffect(() => { try { - const c = CamelDefinitionYaml.integrationToYaml(integration); + const clone = CamelUtil.cloneIntegration(integration); + const c = CamelDefinitionYaml.integrationToYaml(clone); setCode(c); } catch (e: any) { const message: string = e?.message ? e.message : e.reason; diff --git a/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx b/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx index 7448351c..a8ce0076 100644 --- a/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx +++ b/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx @@ -62,7 +62,7 @@ export function DslProperties(props: Props) { const [showAdvanced, setShowAdvanced] = useState<boolean>(false); - function getClonableElementHeader(): JSX.Element { + function getClonableElementHeader(): React.JSX.Element { const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep); const description = selectedStep?.dslName ? CamelMetadataApi.getCamelModelMetadataByClassName(selectedStep?.dslName)?.description : title; const descriptionLines: string [] = description ? description?.split("\n") : [""]; diff --git a/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx b/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx index a711780b..0b5dead4 100644 --- a/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx +++ b/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx @@ -47,7 +47,7 @@ export function DslConnections() { setTons(prevState => { const data = new Map<string, string[]>(); TopologyUtils.findTopologyOutgoingNodes(integrations).forEach(t => { - const key = (t.step as any)?.uri + ':' + (t.step as any)?.parameters.name; + const key = (t.step as any)?.uri + ':' + (t.step as any)?.parameters?.name; if (data.has(key)) { const list = data.get(key) || []; list.push(t.routeId); @@ -253,7 +253,7 @@ export function DslConnections() { {name !== undefined && <Tooltip content={`Go to ${uri}:${name}`} position={"left"}> <Button style={{position: 'absolute', right: 27, top: -12, whiteSpace: 'nowrap', zIndex: 300, padding: 0}} - variant={'link'} + variant={'link'} aria-label="Goto" onClick={_ => InfrastructureAPI.onInternalConsumerClick(uri, name, undefined)}> {name} @@ -306,7 +306,7 @@ export function DslConnections() { function getArrow(pos: DslPosition): JSX.Element[] { const list: JSX.Element[] = []; - if (pos.parent && pos.parent.dslName === 'TryDefinition' && pos.position === 0) { + if (pos.parent && pos.parent.dslName === 'TryDefinition' && pos.position === 0) { const parent = steps.get(pos.parent.uuid); list.push(...addArrowToList(list, parent, pos, true, false)) } else if (pos.parent && ['RouteConfigurationDefinition', 'MulticastDefinition'].includes(pos.parent.dslName)) { @@ -320,7 +320,7 @@ export function DslConnections() { const parent = steps.get(pos.parent.uuid); list.push(...addArrowToList(list, parent, pos, true, false)) } else if (pos.parent && ['WhenDefinition', 'OtherwiseDefinition', 'CatchDefinition', 'FinallyDefinition', 'TryDefinition'].includes(pos.parent.dslName)) { - if (pos.position === 0) { + if (pos.position === 0) { const parent = steps.get(pos.parent.uuid); list.push(...addArrowToList(list, parent, pos, true, false)) } @@ -395,45 +395,45 @@ export function DslConnections() { } function getComplexArrow(key: string, rect1: DOMRect, rect2: DOMRect, toHeader: boolean) { - const startX = rect1.x + rect1.width / 2 - left; - const startY = rect1.y + rect1.height - top - 2; - const endX = rect2.x + rect2.width / 2 - left; - const endTempY = rect2.y - top - 9; - - const gapX = Math.abs(endX - startX); - const gapY = Math.abs(endTempY - startY); - - const radX = gapX > 30 ? 20 : gapX/2; - const radY = gapY > 30 ? 20 : gapY/2; - const endY = rect2.y - top - radY - (toHeader ? 9 : 6); - - const iRadX = startX > endX ? -1 * radX : radX; - const iRadY = startY > endY ? -1 * radY : radY; - - const LX1 = startX; - const LY1 = endY - radY; - - const Q1_X1 = startX; - const Q1_Y1 = LY1 + radY; - const Q1_X2 = startX + iRadX; - const Q1_Y2 = LY1 + radY; - - const LX2 = startX + (endX - startX) - iRadX; - const LY2 = LY1 + radY; - - const Q2_X1 = LX2 + iRadX; - const Q2_Y1 = endY; - const Q2_X2 = LX2 + iRadX; - const Q2_Y2 = endY + radY; - - const path = `M ${startX} ${startY}` - + ` L ${LX1} ${LY1} ` - + ` Q ${Q1_X1} ${Q1_Y1} ${Q1_X2} ${Q1_Y2}` - + ` L ${LX2} ${LY2}` - + ` Q ${Q2_X1} ${Q2_Y1} ${Q2_X2} ${Q2_Y2}` - return ( - <path key={uuidv4()} name={key} d={path} className="path" markerEnd="url(#arrowhead)"/> - ) + const startX = rect1.x + rect1.width / 2 - left; + const startY = rect1.y + rect1.height - top - 2; + const endX = rect2.x + rect2.width / 2 - left; + const endTempY = rect2.y - top - 9; + + const gapX = Math.abs(endX - startX); + const gapY = Math.abs(endTempY - startY); + + const radX = gapX > 30 ? 20 : gapX/2; + const radY = gapY > 30 ? 20 : gapY/2; + const endY = rect2.y - top - radY - (toHeader ? 9 : 6); + + const iRadX = startX > endX ? -1 * radX : radX; + const iRadY = startY > endY ? -1 * radY : radY; + + const LX1 = startX; + const LY1 = endY - radY; + + const Q1_X1 = startX; + const Q1_Y1 = LY1 + radY; + const Q1_X2 = startX + iRadX; + const Q1_Y2 = LY1 + radY; + + const LX2 = startX + (endX - startX) - iRadX; + const LY2 = LY1 + radY; + + const Q2_X1 = LX2 + iRadX; + const Q2_Y1 = endY; + const Q2_X2 = LX2 + iRadX; + const Q2_Y2 = endY + radY; + + const path = `M ${startX} ${startY}` + + ` L ${LX1} ${LY1} ` + + ` Q ${Q1_X1} ${Q1_Y1} ${Q1_X2} ${Q1_Y2}` + + ` L ${LX2} ${LY2}` + + ` Q ${Q2_X1} ${Q2_Y1} ${Q2_X2} ${Q2_Y2}` + return ( + <path key={uuidv4()} name={key} d={path} className="path" markerEnd="url(#arrowhead)"/> + ) } function getSvg() { @@ -442,8 +442,8 @@ export function DslConnections() { const uniqueArrows = [...new Map(arrows.map(item => [(item as any).key, item])).values()] return ( <svg key={svgKey} - style={{width: width, height: height, position: "absolute", left: 0, top: 0}} - viewBox={"0 0 " + (width) + " " + (height)}> + style={{width: width, height: height, position: "absolute", left: 0, top: 0}} + viewBox={"0 0 " + (width) + " " + (height)}> <defs> <marker id="arrowhead" markerWidth="9" markerHeight="6" refX="0" refY="3" orient="auto" className="arrow"> <polygon points="0 0, 9 3, 0 6"/> @@ -464,4 +464,4 @@ export function DslConnections() { {getOutgoings().map(p => getOutgoingIcons(p))} </div> ) -} +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx b/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx index 9c599762..8f13949d 100644 --- a/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx +++ b/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx @@ -16,9 +16,10 @@ */ import React, {useEffect, useMemo, useState} from 'react'; import { + Alert, capitalize, Flex, - Form, FormGroup, FormHelperText, HelperText, HelperTextItem, InputGroup, InputGroupItem, + Form, FormAlert, FormGroup, FormHelperText, HelperText, HelperTextItem, InputGroup, InputGroupItem, Modal, ModalVariant, Radio, Text, TextInput, @@ -47,21 +48,6 @@ const BEAN_TEMPLATE_SUFFIX_FILENAME = "-bean-template.camel.yaml"; export function BeanWizard() { - const { - register, - setError, - handleSubmit, - formState: {errors}, - reset, - setValue - } = useForm({ - mode: "onChange", - defaultValues: {filename: ''} - }); - - const responseToFormErrorFields = new Map<string, string>([ - ["filename", "filename"] - ]); const [project] = useProjectStore((s) => [s.project], shallow); const [setFile, designerTab] = useFileStore((s) => [s.setFile, s.designerTab], shallow); @@ -74,11 +60,7 @@ export function BeanWizard() { const [bean, setBean] = useState<RegistryBeanDefinition | undefined>(); const [filename, setFilename] = useState<string>(''); const [beanName, setBeanName] = useState<string>(''); - - const [globalErrors, registerResponseErrors, resetGlobalErrors] = useResponseErrorHandler( - responseToFormErrorFields, - setError - ); + const [backendError, setBackendError] = React.useState<string>(); function handleOnFormSubmitSuccess(file: ProjectFile) { const message = "File successfully created."; @@ -106,15 +88,19 @@ export function BeanWizard() { } 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)); + KaravanApi.saveProjectFile(file, (result, file) => { + if (result) { + handleOnFormSubmitSuccess(file); + } else { + setBackendError(file?.response?.data); + } + }) } } useEffect(() => { if (showWizard) { - reset({filename: ''}) + setBackendError(undefined); setFilename('') setTemplateName(''); setTemplateBeanName(''); @@ -210,7 +196,7 @@ export function BeanWizard() { </Form> </WizardStep> <WizardStep name={"File"} id={"file"} - footer={{nextButtonText: 'Save', onNext: handleSubmit(handleFormSubmit)}} + footer={{nextButtonText: 'Save', onNext: event => handleFormSubmit()}} isDisabled={(templateName.length == 0 || templateBeanName.length == 0) && templateName !== EMPTY_BEAN} > <Form autoComplete="off"> @@ -221,30 +207,24 @@ export function BeanWizard() { 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> {templateName !== EMPTY_BEAN && <InputGroupItem> - <BeanFilesDropdown {...register('filename')} onSelect={(fn, event) => { - setFilename(fn); - setValue('filename', fn, {shouldValidate: true}); - }}/> + <BeanFilesDropdown + onSelect={(fn, event) => { + setFilename(fn); + }} + /> </InputGroupItem>} </InputGroup> - {!!errors.filename && ( - <FormHelperText> - <HelperText> - <HelperTextItem icon={<ExclamationCircleIcon/>} variant={"error"}> - {errors?.filename?.message} - </HelperTextItem> - </HelperText> - </FormHelperText> - )} + {backendError && + <FormAlert> + <Alert variant="danger" title={backendError} aria-live="polite" isInline/> + </FormAlert> + } </FormGroup> </Form> </WizardStep> diff --git a/karavan-app/src/main/webui/src/util/useFormUtil.tsx b/karavan-app/src/main/webui/src/util/useFormUtil.tsx index 6e14da3e..7b06e843 100644 --- a/karavan-app/src/main/webui/src/util/useFormUtil.tsx +++ b/karavan-app/src/main/webui/src/util/useFormUtil.tsx @@ -1,5 +1,12 @@ import React from 'react'; -import {Controller, FieldError, UseFormReturn} from "react-hook-form"; +import { + Controller, + ControllerFieldState, ControllerRenderProps, + FieldError, + FieldValues, + UseFormReturn, + UseFormStateReturn +} from "react-hook-form"; import { Flex, FormGroup, @@ -7,7 +14,7 @@ import { FormSelect, FormSelectOption, HelperText, - HelperTextItem, Switch, Text, + HelperTextItem, Switch, Text, TextArea, TextInput, TextInputGroup, TextInputGroupMain, TextVariants } from "@patternfly/react-core"; import "./form-util.css" @@ -29,16 +36,48 @@ export function useFormUtil(formContext: UseFormReturn<any>) { } function getTextField(fieldName: string, label: string, validate?: ((value: string, formValues: any) => boolean | string) | Record<string, (value: string, formValues: any) => boolean | string>) { - const {setValue, getValues, register, formState: {errors}} = formContext; + const {control, setValue, getValues, formState: {errors}} = formContext; + return ( + <FormGroup label={label} fieldId={fieldName} isRequired> + <Controller + rules={{required: "Required field", validate: validate}} + control={control} + name={fieldName} + render={() => ( + <TextInput className="text-field" type="text" id={fieldName} + value={getValues(fieldName)} + validated={!!errors[fieldName] ? 'error' : 'default'} + onChange={(_, v) => { + setValue(fieldName, v, {shouldValidate: true}); + }} + /> + )} + /> + {getHelper((errors as any)[fieldName])} + </FormGroup> + ) + } + + function getTextArea(fieldName: string, label: string, validate?: ((value: string, formValues: any) => boolean | string) | Record<string, (value: string, formValues: any) => boolean | string>) { + const {setValue, getValues, control, formState: {errors}} = formContext; return ( <FormGroup label={label} fieldId={fieldName} isRequired> - <TextInput className="text-field" type="text" id={fieldName} - value={getValues()[fieldName]} - validated={!!errors[fieldName] ? 'error' : 'default'} - {...register(fieldName, {required: "Required field", validate: validate})} - onChange={(e, v) => { - setValue(fieldName, v, {shouldValidate: true}); - }} + <Controller + rules={{required: "Required field", validate: validate}} + control={control} + name={fieldName} + render={() => ( + <TextArea type="text" + id={fieldName} + value={getValues(fieldName)} + validated={!!errors[fieldName] ? 'error' : 'default'} + // ref={ref} + onChange={(e, v) => { + setValue(fieldName, v, {shouldValidate: true}); + }} + autoResize + /> + )} /> {getHelper((errors as any)[fieldName])} </FormGroup> @@ -53,8 +92,11 @@ export function useFormUtil(formContext: UseFormReturn<any>) { <FormGroup label={label} fieldId={fieldName} isRequired> <TextInputGroup> <TextInputGroupMain className="text-field-with-prefix" type="text" id={fieldName} - value={getValues()[fieldName]} - {...register(fieldName, {required: (required ? "Required field" : false), validate: validate})} + value={getValues(fieldName)} + {...register(fieldName, { + required: (required ? "Required field" : false), + validate: validate + })} onChange={(e, v) => { setValue(fieldName, v, {shouldValidate: true}); }} @@ -75,8 +117,11 @@ export function useFormUtil(formContext: UseFormReturn<any>) { <FormGroup label={label} fieldId={fieldName} isRequired> <TextInputGroup className="text-field-with-suffix"> <TextInputGroupMain type="text" id={fieldName} - value={getValues()[fieldName]} - {...register(fieldName, {required: (required ? "Required field" : false), validate: validate})} + value={getValues(fieldName)} + {...register(fieldName, { + required: (required ? "Required field" : false), + validate: validate + })} onChange={(e, v) => { setValue(fieldName, v, {shouldValidate: true}); }} @@ -119,33 +164,33 @@ export function useFormUtil(formContext: UseFormReturn<any>) { return ( <FormGroup label={label} fieldId={fieldName} isRequired {...register(fieldName)}> <Flex direction={{default: 'column'}}> - {options.map((option, index) => { - const key = option[0]; - const label = option[0]; - return (<Switch - id={key} - label={label} - labelOff={label} - isChecked={watch(fieldName) !== undefined && watch(fieldName).includes(key)} - onChange={(e, v) => { - const vals: string[] = watch(fieldName); - const idx = vals.findIndex(x => x === key); - if (idx > -1 && !v) { - vals.splice(idx, 1); - setValue(fieldName, [...vals]); - } else if (idx === -1 && v) { - vals.push(key); - setValue(fieldName, [...vals]); - } - }} - ouiaId={option[0]} - />) - })} + {options.map((option, index) => { + const key = option[0]; + const label = option[0]; + return (<Switch + id={key} + label={label} + labelOff={label} + isChecked={watch(fieldName) !== undefined && watch(fieldName).includes(key)} + onChange={(e, v) => { + const vals: string[] = watch(fieldName); + const idx = vals.findIndex(x => x === key); + if (idx > -1 && !v) { + vals.splice(idx, 1); + setValue(fieldName, [...vals]); + } else if (idx === -1 && v) { + vals.push(key); + setValue(fieldName, [...vals]); + } + }} + ouiaId={option[0]} + />) + })} </Flex> </FormGroup> ) } - return {getFormSelect, getTextField, getSwitches, getTextFieldPrefix, getTextFieldSuffix} + return {getFormSelect, getTextField, getSwitches, getTextFieldPrefix, getTextFieldSuffix, getTextArea} }