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 34353942 Fix #943 34353942 is described below commit 34353942bbcfe2bbcbccde40256c48db26183959 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Fri Oct 20 15:32:32 2023 -0400 Fix #943 --- karavan-core/src/core/api/KameletApi.ts | 1 + karavan-core/src/core/model/KameletModels.ts | 1 + karavan-designer/public/example/demo.camel.yaml | 5 +++ .../route/property/KameletPropertyField.tsx | 52 +++++++++++++++++++--- .../route/property/KameletPropertyField.tsx | 52 +++++++++++++++++++--- karavan-vscode/webview/topology/CustomNode.tsx | 7 ++- karavan-vscode/webview/topology/TopologyApi.tsx | 31 ++++++++++--- karavan-vscode/webview/topology/TopologyTab.tsx | 1 - .../webui/src/designer/beans/BeanProperties.tsx | 6 +-- .../webui/src/designer/beans/BeansDesigner.tsx | 2 +- .../webui/src/designer/kamelet/KameletDesigner.tsx | 2 +- .../route/property/KameletPropertyField.tsx | 52 +++++++++++++++++++--- 12 files changed, 185 insertions(+), 27 deletions(-) diff --git a/karavan-core/src/core/api/KameletApi.ts b/karavan-core/src/core/api/KameletApi.ts index d8bdef4d..62ecb4f2 100644 --- a/karavan-core/src/core/api/KameletApi.ts +++ b/karavan-core/src/core/api/KameletApi.ts @@ -49,6 +49,7 @@ export class KameletApi { prop.format = value.format; prop.example = value.example; prop.type = value.type; + prop.enum = value.enum; if (value.default) prop.value = value.default; prop['x-descriptors'] = value['x-descriptors']; properties.push(prop); diff --git a/karavan-core/src/core/model/KameletModels.ts b/karavan-core/src/core/model/KameletModels.ts index b6066320..e5c9855e 100644 --- a/karavan-core/src/core/model/KameletModels.ts +++ b/karavan-core/src/core/model/KameletModels.ts @@ -24,6 +24,7 @@ export class Property { example: string = ''; 'x-descriptors': string = ''; value: string | number | boolean = ''; + enum?: string[]; } export class Definition { diff --git a/karavan-designer/public/example/demo.camel.yaml b/karavan-designer/public/example/demo.camel.yaml index 69ff1495..82e33ec2 100644 --- a/karavan-designer/public/example/demo.camel.yaml +++ b/karavan-designer/public/example/demo.camel.yaml @@ -1,3 +1,8 @@ +- route: + id: route-612d + from: + uri: kamelet:aws-s3-cdc-source + id: from-e37e - route: id: route-605c from: diff --git a/karavan-designer/src/designer/route/property/KameletPropertyField.tsx b/karavan-designer/src/designer/route/property/KameletPropertyField.tsx index 69446142..0bac1c32 100644 --- a/karavan-designer/src/designer/route/property/KameletPropertyField.tsx +++ b/karavan-designer/src/designer/route/property/KameletPropertyField.tsx @@ -34,6 +34,8 @@ import ShowIcon from "@patternfly/react-icons/dist/js/icons/eye-icon"; import HideIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon"; import DockerIcon from "@patternfly/react-icons/dist/js/icons/docker-icon"; import {usePropertiesHook} from "../usePropertiesHook"; +import {CamelUi} from "../../utils/CamelUi"; +import {Select, SelectDirection, SelectOption, SelectVariant} from "@patternfly/react-core/deprecated"; interface Props { property: Property, @@ -50,12 +52,22 @@ export function KameletPropertyField(props: Props) { const [showPassword, setShowPassword] = useState<boolean>(false); const [infrastructureSelector, setInfrastructureSelector] = useState<boolean>(false); const [infrastructureSelectorProperty, setInfrastructureSelectorProperty] = useState<string | undefined>(undefined); + const [selectStatus, setSelectStatus] = useState<Map<string, boolean>>(new Map<string, boolean>()); const ref = useRef<any>(null); function parametersChanged (parameter: string, value: string | number | boolean | any, pathParameter?: boolean) { onParametersChange(parameter, value, pathParameter); setSelectIsOpen(false); + setSelectStatus(new Map<string, boolean>([[parameter, false]])) + } + + function openSelect(propertyName: string, isExpanded: boolean) { + setSelectStatus(new Map<string, boolean>([[propertyName, isExpanded]])) + } + + function isSelectOpen(propertyName: string): boolean { + return selectStatus.has(propertyName) && selectStatus.get(propertyName) === true; } function selectInfrastructure (value: string) { @@ -98,7 +110,13 @@ export function KameletPropertyField(props: Props) { const inInfrastructure = InfrastructureAPI.infrastructure !== 'local'; const noInfraSelectorButton = ["uri", "id", "description", "group"].includes(property.id); const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? <KubernetesIcon/> : <DockerIcon/> - const showInfraSelectorButton = inInfrastructure && !showEditor && !noInfraSelectorButton + const showInfraSelectorButton = inInfrastructure && !showEditor && !noInfraSelectorButton; + const selectFromList: boolean = property.enum !== undefined && property?.enum?.length > 0; + const selectOptions: JSX.Element[] = []; + if (selectFromList && property.enum) { + selectOptions.push(...property.enum.map((value: string) => + <SelectOption key={value} value={value ? value.trim() : value}/>)); + } return <InputGroup> {showInfraSelectorButton && <Tooltip position="bottom-end" content={"Select from " + capitalize(InfrastructureAPI.infrastructure)}> @@ -106,21 +124,45 @@ export function KameletPropertyField(props: Props) { {icon} </Button> </Tooltip>} - {(!showEditor || property.format === "password") && + {selectFromList && + <Select + id={id} name={id} + placeholderText="Select or type an URI" + variant={SelectVariant.typeahead} + aria-label={property.id} + onToggle={(_event, isExpanded) => { + openSelect(property.id, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => { + parametersChanged(property.id, value); + }} + selections={value} + isOpen={isSelectOpen(property.id)} + isCreatable={true} + createText="" + isInputFilterPersisted={true} + aria-labelledby={property.id} + direction={SelectDirection.down}> + {selectOptions} + </Select> + } + {((!selectFromList && !showEditor) || property.format === "password") && <TextInput ref={ref} className="text-field" isRequired type={property.format && !showPassword ? "password" : "text"} id={id} name={id} value={value} - onChange={(e, value) => parametersChanged(property.id, value)}/>} - {showEditor && property.format !== "password" && + onChange={(e, value) => parametersChanged(property.id, value)}/> + } + {(!selectFromList && showEditor) && property.format !== "password" && <TextArea autoResize={true} className="text-field" isRequired type="text" id={id} name={id} value={value} - onChange={(e, value) => parametersChanged(property.id, value)}/>} + onChange={(e, value) => parametersChanged(property.id, value)}/> + } {property.format !== "password" && <Tooltip position="bottom-end" content={showEditor ? "Change to TextField" : "Change to Text Area"}> <Button variant="control" onClick={e => setShowEditor(!showEditor)}> diff --git a/karavan-space/src/designer/route/property/KameletPropertyField.tsx b/karavan-space/src/designer/route/property/KameletPropertyField.tsx index 69446142..0bac1c32 100644 --- a/karavan-space/src/designer/route/property/KameletPropertyField.tsx +++ b/karavan-space/src/designer/route/property/KameletPropertyField.tsx @@ -34,6 +34,8 @@ import ShowIcon from "@patternfly/react-icons/dist/js/icons/eye-icon"; import HideIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon"; import DockerIcon from "@patternfly/react-icons/dist/js/icons/docker-icon"; import {usePropertiesHook} from "../usePropertiesHook"; +import {CamelUi} from "../../utils/CamelUi"; +import {Select, SelectDirection, SelectOption, SelectVariant} from "@patternfly/react-core/deprecated"; interface Props { property: Property, @@ -50,12 +52,22 @@ export function KameletPropertyField(props: Props) { const [showPassword, setShowPassword] = useState<boolean>(false); const [infrastructureSelector, setInfrastructureSelector] = useState<boolean>(false); const [infrastructureSelectorProperty, setInfrastructureSelectorProperty] = useState<string | undefined>(undefined); + const [selectStatus, setSelectStatus] = useState<Map<string, boolean>>(new Map<string, boolean>()); const ref = useRef<any>(null); function parametersChanged (parameter: string, value: string | number | boolean | any, pathParameter?: boolean) { onParametersChange(parameter, value, pathParameter); setSelectIsOpen(false); + setSelectStatus(new Map<string, boolean>([[parameter, false]])) + } + + function openSelect(propertyName: string, isExpanded: boolean) { + setSelectStatus(new Map<string, boolean>([[propertyName, isExpanded]])) + } + + function isSelectOpen(propertyName: string): boolean { + return selectStatus.has(propertyName) && selectStatus.get(propertyName) === true; } function selectInfrastructure (value: string) { @@ -98,7 +110,13 @@ export function KameletPropertyField(props: Props) { const inInfrastructure = InfrastructureAPI.infrastructure !== 'local'; const noInfraSelectorButton = ["uri", "id", "description", "group"].includes(property.id); const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? <KubernetesIcon/> : <DockerIcon/> - const showInfraSelectorButton = inInfrastructure && !showEditor && !noInfraSelectorButton + const showInfraSelectorButton = inInfrastructure && !showEditor && !noInfraSelectorButton; + const selectFromList: boolean = property.enum !== undefined && property?.enum?.length > 0; + const selectOptions: JSX.Element[] = []; + if (selectFromList && property.enum) { + selectOptions.push(...property.enum.map((value: string) => + <SelectOption key={value} value={value ? value.trim() : value}/>)); + } return <InputGroup> {showInfraSelectorButton && <Tooltip position="bottom-end" content={"Select from " + capitalize(InfrastructureAPI.infrastructure)}> @@ -106,21 +124,45 @@ export function KameletPropertyField(props: Props) { {icon} </Button> </Tooltip>} - {(!showEditor || property.format === "password") && + {selectFromList && + <Select + id={id} name={id} + placeholderText="Select or type an URI" + variant={SelectVariant.typeahead} + aria-label={property.id} + onToggle={(_event, isExpanded) => { + openSelect(property.id, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => { + parametersChanged(property.id, value); + }} + selections={value} + isOpen={isSelectOpen(property.id)} + isCreatable={true} + createText="" + isInputFilterPersisted={true} + aria-labelledby={property.id} + direction={SelectDirection.down}> + {selectOptions} + </Select> + } + {((!selectFromList && !showEditor) || property.format === "password") && <TextInput ref={ref} className="text-field" isRequired type={property.format && !showPassword ? "password" : "text"} id={id} name={id} value={value} - onChange={(e, value) => parametersChanged(property.id, value)}/>} - {showEditor && property.format !== "password" && + onChange={(e, value) => parametersChanged(property.id, value)}/> + } + {(!selectFromList && showEditor) && property.format !== "password" && <TextArea autoResize={true} className="text-field" isRequired type="text" id={id} name={id} value={value} - onChange={(e, value) => parametersChanged(property.id, value)}/>} + onChange={(e, value) => parametersChanged(property.id, value)}/> + } {property.format !== "password" && <Tooltip position="bottom-end" content={showEditor ? "Change to TextField" : "Change to Text Area"}> <Button variant="control" onClick={e => setShowEditor(!showEditor)}> diff --git a/karavan-vscode/webview/topology/CustomNode.tsx b/karavan-vscode/webview/topology/CustomNode.tsx index fb517ae9..d683d04f 100644 --- a/karavan-vscode/webview/topology/CustomNode.tsx +++ b/karavan-vscode/webview/topology/CustomNode.tsx @@ -43,11 +43,16 @@ function getIcon(data: any) { const CustomNode: React.FC<any> = observer(({ element, ...rest }) => { const data = element.getData(); + const badge:string = data.badge?.substring(0,1).toUpperCase(); return ( <DefaultNode + badge={badge} + showStatusDecorator className="common-node" - element={element} {...rest} + scaleLabel={false} + element={element} + {...rest} > {getIcon(data)} </DefaultNode> diff --git a/karavan-vscode/webview/topology/TopologyApi.tsx b/karavan-vscode/webview/topology/TopologyApi.tsx index 9655968d..edff33e8 100644 --- a/karavan-vscode/webview/topology/TopologyApi.tsx +++ b/karavan-vscode/webview/topology/TopologyApi.tsx @@ -38,9 +38,9 @@ import { TopologyRestNode, TopologyRouteNode } from "core/model/TopologyDefinition"; -import CustomGroup from "./CustomGroup"; import CustomEdge from "./CustomEdge"; import {IntegrationFile} from "./TopologyStore"; +import CustomGroup from "./CustomGroup"; const NODE_DIAMETER = 60; @@ -62,7 +62,7 @@ export function getIncomingNodes(tins: TopologyIncomingNode[]): NodeModel[] { status: NodeStatus.default, data: { isAlternate: false, - badge: tin.type, + badge: tin.connectorType, icon: 'element', type: 'step', step: tin.from, @@ -110,7 +110,7 @@ export function getOutgoingNodes(tons: TopologyOutgoingNode[]): NodeModel[] { icon: 'element', type: 'step', step: tin.step, - badge: tin.type, + badge: tin.connectorType, fileName: tin.fileName } } @@ -146,6 +146,26 @@ export function getOutgoingEdges(tons: TopologyOutgoingNode[]): EdgeModel[] { }); } +export function getExternalEdges(tons: TopologyOutgoingNode[], tins: TopologyIncomingNode[]): EdgeModel[] { + const result: EdgeModel[]= []; + tons.filter(ton => ton.type === 'external').forEach((ton, index) => { + const uniqueUri = ton.uniqueUri; + if (uniqueUri) { + const target = TopologyUtils.getNodeIdByUniqueUri(tins, uniqueUri); + const node: EdgeModel = { + id: 'external-' + ton.id + '-' + index, + type: 'edge', + source: ton.id, + target: target, + edgeStyle: EdgeStyle.dotted, + animationSpeed: EdgeAnimationSpeed.slow + } + if (target) result.push(node); + } + }); + return result; +} + export function getRestNodes(tins: TopologyRestNode[]): NodeModel[] { return tins.map(tin => { return { @@ -217,8 +237,8 @@ export function getModel(files: IntegrationFile[]): Model { const nodes: NodeModel[] = []; const groups: NodeModel[] = troutes.map(r => { const children = [r.id] - children.push(... tins.filter(i => i.routeId === r.routeId && i.type === 'external').map(i => i.id)); - children.push(... tons.filter(i => i.routeId === r.routeId && i.type === 'external').map(i => i.id)); + children.push(...tins.filter(i => i.routeId === r.routeId && i.type === 'external').map(i => i.id)); + children.push(...tons.filter(i => i.routeId === r.routeId && i.type === 'external').map(i => i.id)); return { id: 'group-' + r.routeId, children: children, @@ -242,6 +262,7 @@ export function getModel(files: IntegrationFile[]): Model { edges.push(...getOutgoingEdges(tons)); edges.push(...getRestEdges(trestns, tins)); edges.push(...getInternalEdges(tons, tins)); + edges.push(...getExternalEdges(tons,tins)); return {nodes: nodes, edges: edges, graph: {id: 'g1', type: 'graph', layout: 'Dagre'}}; } diff --git a/karavan-vscode/webview/topology/TopologyTab.tsx b/karavan-vscode/webview/topology/TopologyTab.tsx index 306ca8fb..520bf8d2 100644 --- a/karavan-vscode/webview/topology/TopologyTab.tsx +++ b/karavan-vscode/webview/topology/TopologyTab.tsx @@ -68,7 +68,6 @@ export function TopologyTab (props: Props) { } const controller = React.useMemo(() => { - console.log(props.files) const model = getModel(props.files); const newController = new Visualization(); newController.registerLayoutFactory((_, graph) => new DagreLayout(graph)); diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/beans/BeanProperties.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/beans/BeanProperties.tsx index fdb73d2a..455291c4 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/beans/BeanProperties.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/beans/BeanProperties.tsx @@ -73,7 +73,7 @@ export function BeanProperties (props: Props) { function onBeanPropertyUpdate () { if (selectedStep) { - const bean = CamelUtil.cloneBean(selectedStep); + const bean = CamelUtil.cloneBean(selectedStep as RegistryBeanDefinition); const beanProperties: any = {}; properties.forEach((p: any) => beanProperties[p[0]] = p[1]); bean.properties = beanProperties; @@ -83,7 +83,7 @@ export function BeanProperties (props: Props) { function beanFieldChanged (fieldId: string, value: string) { if (selectedStep) { - const bean = CamelUtil.cloneBean(selectedStep); + const bean = CamelUtil.cloneBean(selectedStep as RegistryBeanDefinition); (bean as any)[fieldId] = value; props.onChange(bean); } @@ -137,7 +137,7 @@ export function BeanProperties (props: Props) { function cloneBean () { if (selectedStep) { - const bean = CamelUtil.cloneBean(selectedStep); + const bean = CamelUtil.cloneBean(selectedStep as RegistryBeanDefinition); bean.uuid = uuidv4(); props.onClone(bean); } diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/beans/BeansDesigner.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/beans/BeansDesigner.tsx index 64bac96f..8cd99f25 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/beans/BeansDesigner.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/beans/BeansDesigner.tsx @@ -49,7 +49,7 @@ export function BeansDesigner() { } function deleteBean() { - const i = CamelDefinitionApiExt.deleteBeanFromIntegration(integration, selectedStep); + const i = CamelDefinitionApiExt.deleteBeanFromIntegration(integration, selectedStep as RegistryBeanDefinition); setIntegration(i, false); setShowDeleteConfirmation(false); setSelectedStep(undefined); diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDesigner.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDesigner.tsx index dd889b92..c235ef9b 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDesigner.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDesigner.tsx @@ -52,7 +52,7 @@ export function KameletDesigner() { } function deleteBean() { - const i = CamelDefinitionApiExt.deleteBeanFromIntegration(integration, selectedStep); + const i = CamelDefinitionApiExt.deleteBeanFromIntegration(integration, selectedStep as RegistryBeanDefinition); setIntegration(i, false); setShowDeleteConfirmation(false); setSelectedStep(undefined); diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/route/property/KameletPropertyField.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/route/property/KameletPropertyField.tsx index 69446142..0bac1c32 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/route/property/KameletPropertyField.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/route/property/KameletPropertyField.tsx @@ -34,6 +34,8 @@ import ShowIcon from "@patternfly/react-icons/dist/js/icons/eye-icon"; import HideIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon"; import DockerIcon from "@patternfly/react-icons/dist/js/icons/docker-icon"; import {usePropertiesHook} from "../usePropertiesHook"; +import {CamelUi} from "../../utils/CamelUi"; +import {Select, SelectDirection, SelectOption, SelectVariant} from "@patternfly/react-core/deprecated"; interface Props { property: Property, @@ -50,12 +52,22 @@ export function KameletPropertyField(props: Props) { const [showPassword, setShowPassword] = useState<boolean>(false); const [infrastructureSelector, setInfrastructureSelector] = useState<boolean>(false); const [infrastructureSelectorProperty, setInfrastructureSelectorProperty] = useState<string | undefined>(undefined); + const [selectStatus, setSelectStatus] = useState<Map<string, boolean>>(new Map<string, boolean>()); const ref = useRef<any>(null); function parametersChanged (parameter: string, value: string | number | boolean | any, pathParameter?: boolean) { onParametersChange(parameter, value, pathParameter); setSelectIsOpen(false); + setSelectStatus(new Map<string, boolean>([[parameter, false]])) + } + + function openSelect(propertyName: string, isExpanded: boolean) { + setSelectStatus(new Map<string, boolean>([[propertyName, isExpanded]])) + } + + function isSelectOpen(propertyName: string): boolean { + return selectStatus.has(propertyName) && selectStatus.get(propertyName) === true; } function selectInfrastructure (value: string) { @@ -98,7 +110,13 @@ export function KameletPropertyField(props: Props) { const inInfrastructure = InfrastructureAPI.infrastructure !== 'local'; const noInfraSelectorButton = ["uri", "id", "description", "group"].includes(property.id); const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? <KubernetesIcon/> : <DockerIcon/> - const showInfraSelectorButton = inInfrastructure && !showEditor && !noInfraSelectorButton + const showInfraSelectorButton = inInfrastructure && !showEditor && !noInfraSelectorButton; + const selectFromList: boolean = property.enum !== undefined && property?.enum?.length > 0; + const selectOptions: JSX.Element[] = []; + if (selectFromList && property.enum) { + selectOptions.push(...property.enum.map((value: string) => + <SelectOption key={value} value={value ? value.trim() : value}/>)); + } return <InputGroup> {showInfraSelectorButton && <Tooltip position="bottom-end" content={"Select from " + capitalize(InfrastructureAPI.infrastructure)}> @@ -106,21 +124,45 @@ export function KameletPropertyField(props: Props) { {icon} </Button> </Tooltip>} - {(!showEditor || property.format === "password") && + {selectFromList && + <Select + id={id} name={id} + placeholderText="Select or type an URI" + variant={SelectVariant.typeahead} + aria-label={property.id} + onToggle={(_event, isExpanded) => { + openSelect(property.id, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => { + parametersChanged(property.id, value); + }} + selections={value} + isOpen={isSelectOpen(property.id)} + isCreatable={true} + createText="" + isInputFilterPersisted={true} + aria-labelledby={property.id} + direction={SelectDirection.down}> + {selectOptions} + </Select> + } + {((!selectFromList && !showEditor) || property.format === "password") && <TextInput ref={ref} className="text-field" isRequired type={property.format && !showPassword ? "password" : "text"} id={id} name={id} value={value} - onChange={(e, value) => parametersChanged(property.id, value)}/>} - {showEditor && property.format !== "password" && + onChange={(e, value) => parametersChanged(property.id, value)}/> + } + {(!selectFromList && showEditor) && property.format !== "password" && <TextArea autoResize={true} className="text-field" isRequired type="text" id={id} name={id} value={value} - onChange={(e, value) => parametersChanged(property.id, value)}/>} + onChange={(e, value) => parametersChanged(property.id, value)}/> + } {property.format !== "password" && <Tooltip position="bottom-end" content={showEditor ? "Change to TextField" : "Change to Text Area"}> <Button variant="control" onClick={e => setShowEditor(!showEditor)}>