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 e246ad5b3bbe1add5eb251f0e5a84d9f6582eb35 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Wed Jan 31 12:04:44 2024 -0500 Fix #1088 --- karavan-core/src/core/api/ComponentApi.ts | 17 + .../src/designer/property/DslProperties.tsx | 86 +++-- karavan-designer/src/topology/TopologyToolbar.tsx | 6 +- .../src/designer/property/DslProperties.tsx | 88 ++++-- .../property/property/ComponentPropertyField.tsx | 349 +++++++++++++++++++++ .../property/property/DslPropertyField.tsx | 26 +- karavan-space/src/topology/TopologyTab.tsx | 1 - karavan-space/src/topology/TopologyToolbar.tsx | 6 +- karavan-vscode/webview/topology/TopologyStore.ts | 10 +- karavan-vscode/webview/topology/TopologyTab.tsx | 78 +++-- .../webview/topology/TopologyToolbar.tsx | 6 +- .../webui/src/designer/property/DslProperties.tsx | 88 ++++-- .../property/property/ComponentPropertyField.tsx | 349 +++++++++++++++++++++ .../property/property/DslPropertyField.tsx | 26 +- .../src/main/webui/src/topology/TopologyTab.tsx | 1 - 15 files changed, 1024 insertions(+), 113 deletions(-) diff --git a/karavan-core/src/core/api/ComponentApi.ts b/karavan-core/src/core/api/ComponentApi.ts index 0de142ad..da9b9be1 100644 --- a/karavan-core/src/core/api/ComponentApi.ts +++ b/karavan-core/src/core/api/ComponentApi.ts @@ -87,6 +87,23 @@ export class ComponentApi { return ComponentApi.getComponents().find((c: Component) => c.component.name === name); }; + static findStepComponent = (step?: CamelElement): Component | undefined => { + return ComponentApi.findByName((step as any)?.uri) + }; + + static getComponentHeadersList = (step?: CamelElement): ComponentHeader [] => { + const component = step && ComponentApi.findStepComponent(step); + if (component && component.headers) { + return Object.getOwnPropertyNames(component.headers).map(n => { + const header = component.headers[n]; + header.name = n; + return header; + }) + } else { + return []; + } + }; + static getComponentNameFromUri = (uri: string): string | undefined => { return uri !== undefined ? uri.split(':')[0] : undefined; }; diff --git a/karavan-designer/src/designer/property/DslProperties.tsx b/karavan-designer/src/designer/property/DslProperties.tsx index fc5879e7..9325ee05 100644 --- a/karavan-designer/src/designer/property/DslProperties.tsx +++ b/karavan-designer/src/designer/property/DslProperties.tsx @@ -27,7 +27,7 @@ import { MenuToggleElement, MenuToggle, DropdownList, - DropdownItem, + DropdownItem, Label, Flex, LabelGroup, Popover, FlexItem, Badge, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -44,6 +44,9 @@ import {shallow} from "zustand/shallow"; import {usePropertiesHook} from "./usePropertiesHook"; import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil"; import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import QuestionIcon from '@patternfly/react-icons/dist/esm/icons/question-icon'; +import {ComponentApi} from "karavan-core/lib/api/ComponentApi"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; interface Props { designerType: 'routes' | 'rest' | 'beans' @@ -53,7 +56,15 @@ export function DslProperties(props: Props) { const [integration] = useIntegrationStore((s) => [s.integration], shallow) - const {saveAsRoute, convertStep, cloneElement, onDataFormatChange, onPropertyChange, onParametersChange, onExpressionChange} = + const { + saveAsRoute, + convertStep, + cloneElement, + onDataFormatChange, + onPropertyChange, + onParametersChange, + onExpressionChange + } = usePropertiesHook(props.designerType); const [selectedStep, dark] @@ -63,7 +74,7 @@ export function DslProperties(props: Props) { const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - useEffect(()=> { + useEffect(() => { setMenuOpen(false) }, [selectedStep]) @@ -77,7 +88,8 @@ export function DslProperties(props: Props) { style={{inset: "0px auto auto -70px important!"}} className={"xxx"} isOpen={isMenuOpen} - onSelect={() => {}} + onSelect={() => { + }} onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( <MenuToggle @@ -89,11 +101,11 @@ export function DslProperties(props: Props) { onClick={() => setMenuOpen(!isMenuOpen)} isExpanded={isMenuOpen} > - <EllipsisVIcon /> + <EllipsisVIcon/> </MenuToggle> )} > - <DropdownList > + <DropdownList> {hasSteps && <DropdownItem key="saveRoute" onClick={(ev) => { ev.preventDefault() @@ -102,8 +114,8 @@ export function DslProperties(props: Props) { setMenuOpen(false); } }}> - Save Steps to Route - </DropdownItem>} + Save Steps to Route + </DropdownItem>} {hasSteps && <DropdownItem key="saveRoute" onClick={(ev) => { ev.preventDefault() @@ -112,19 +124,19 @@ export function DslProperties(props: Props) { setMenuOpen(false); } }}> - Save Element to Route + Save Element to Route </DropdownItem>} {targetDsl && <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} </DropdownList> </Dropdown> : <></>; } @@ -133,6 +145,8 @@ export function DslProperties(props: Props) { const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep) const description = selectedStep && CamelDisplayUtil.getDescription(selectedStep); const descriptionLines: string [] = description ? description?.split("\n") : [""]; + const headers = ComponentApi.getComponentHeadersList(selectedStep) + const groups = selectedStep?.dslName === 'FromDefinition' ? ['consumer', 'common'] : ['producer', 'common'] return ( <div className="headers"> <div className="top"> @@ -147,6 +161,42 @@ export function DslProperties(props: Props) { {descriptionLines.filter((value, index) => index > 0) .map((desc, index, array) => <Text key={index} component={TextVariants.p}>{desc}</Text>)} </ExpandableSection>} + + {headers.length > 0 && + <ExpandableSection toggleText='Headers' + onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} + isExpanded={isDescriptionExpanded}> + <Flex direction={{default:"column"}}> + {headers.filter((header) => groups.includes(header.group)) + .map((header, index, array) => + <Flex key={index}> + <Text style={{marginLeft: "26px"}} component={TextVariants.p}>{header.name}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Popover + position={"left"} + headerContent={header.name} + bodyContent={header.description} + footerContent={ + <Flex> + <Text component={TextVariants.p}>{header.javaType}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Badge isRead>{header.group}</Badge> + </FlexItem> + </Flex> + } + > + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} className="pf-v5-c-form__group-label-help"> + <HelpIcon/> + </button> + </Popover> + </FlexItem> + </Flex> + )} + </Flex> + </ExpandableSection>} </div> ) } diff --git a/karavan-designer/src/topology/TopologyToolbar.tsx b/karavan-designer/src/topology/TopologyToolbar.tsx index 473bb6f9..667cf8f5 100644 --- a/karavan-designer/src/topology/TopologyToolbar.tsx +++ b/karavan-designer/src/topology/TopologyToolbar.tsx @@ -34,7 +34,7 @@ export function TopologyToolbar (props: Props) { <ToolbarContent> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add Integration Route"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddRoute()} @@ -45,7 +45,7 @@ export function TopologyToolbar (props: Props) { </ToolbarItem> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add REST API"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddREST()} @@ -56,7 +56,7 @@ export function TopologyToolbar (props: Props) { </ToolbarItem> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add Bean"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddBean()} diff --git a/karavan-space/src/designer/property/DslProperties.tsx b/karavan-space/src/designer/property/DslProperties.tsx index dfe8ae9d..9325ee05 100644 --- a/karavan-space/src/designer/property/DslProperties.tsx +++ b/karavan-space/src/designer/property/DslProperties.tsx @@ -27,7 +27,7 @@ import { MenuToggleElement, MenuToggle, DropdownList, - DropdownItem, + DropdownItem, Label, Flex, LabelGroup, Popover, FlexItem, Badge, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -44,6 +44,9 @@ import {shallow} from "zustand/shallow"; import {usePropertiesHook} from "./usePropertiesHook"; import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil"; import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import QuestionIcon from '@patternfly/react-icons/dist/esm/icons/question-icon'; +import {ComponentApi} from "karavan-core/lib/api/ComponentApi"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; interface Props { designerType: 'routes' | 'rest' | 'beans' @@ -53,7 +56,15 @@ export function DslProperties(props: Props) { const [integration] = useIntegrationStore((s) => [s.integration], shallow) - const {saveAsRoute, convertStep, cloneElement, onDataFormatChange, onPropertyChange, onParametersChange, onExpressionChange} = + const { + saveAsRoute, + convertStep, + cloneElement, + onDataFormatChange, + onPropertyChange, + onParametersChange, + onExpressionChange + } = usePropertiesHook(props.designerType); const [selectedStep, dark] @@ -63,7 +74,7 @@ export function DslProperties(props: Props) { const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - useEffect(()=> { + useEffect(() => { setMenuOpen(false) }, [selectedStep]) @@ -77,7 +88,8 @@ export function DslProperties(props: Props) { style={{inset: "0px auto auto -70px important!"}} className={"xxx"} isOpen={isMenuOpen} - onSelect={() => {}} + onSelect={() => { + }} onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( <MenuToggle @@ -89,11 +101,11 @@ export function DslProperties(props: Props) { onClick={() => setMenuOpen(!isMenuOpen)} isExpanded={isMenuOpen} > - <EllipsisVIcon /> + <EllipsisVIcon/> </MenuToggle> )} > - <DropdownList > + <DropdownList> {hasSteps && <DropdownItem key="saveRoute" onClick={(ev) => { ev.preventDefault() @@ -102,8 +114,8 @@ export function DslProperties(props: Props) { setMenuOpen(false); } }}> - Save Steps to Route - </DropdownItem>} + Save Steps to Route + </DropdownItem>} {hasSteps && <DropdownItem key="saveRoute" onClick={(ev) => { ev.preventDefault() @@ -112,19 +124,19 @@ export function DslProperties(props: Props) { setMenuOpen(false); } }}> - Save Element to Route + Save Element to Route </DropdownItem>} {targetDsl && <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} </DropdownList> </Dropdown> : <></>; } @@ -133,6 +145,8 @@ export function DslProperties(props: Props) { const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep) const description = selectedStep && CamelDisplayUtil.getDescription(selectedStep); const descriptionLines: string [] = description ? description?.split("\n") : [""]; + const headers = ComponentApi.getComponentHeadersList(selectedStep) + const groups = selectedStep?.dslName === 'FromDefinition' ? ['consumer', 'common'] : ['producer', 'common'] return ( <div className="headers"> <div className="top"> @@ -147,6 +161,42 @@ export function DslProperties(props: Props) { {descriptionLines.filter((value, index) => index > 0) .map((desc, index, array) => <Text key={index} component={TextVariants.p}>{desc}</Text>)} </ExpandableSection>} + + {headers.length > 0 && + <ExpandableSection toggleText='Headers' + onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} + isExpanded={isDescriptionExpanded}> + <Flex direction={{default:"column"}}> + {headers.filter((header) => groups.includes(header.group)) + .map((header, index, array) => + <Flex key={index}> + <Text style={{marginLeft: "26px"}} component={TextVariants.p}>{header.name}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Popover + position={"left"} + headerContent={header.name} + bodyContent={header.description} + footerContent={ + <Flex> + <Text component={TextVariants.p}>{header.javaType}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Badge isRead>{header.group}</Badge> + </FlexItem> + </Flex> + } + > + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} className="pf-v5-c-form__group-label-help"> + <HelpIcon/> + </button> + </Popover> + </FlexItem> + </Flex> + )} + </Flex> + </ExpandableSection>} </div> ) } @@ -216,7 +266,7 @@ export function DslProperties(props: Props) { {selectedStep && !['MarshalDefinition', 'UnmarshalDefinition'].includes(selectedStep.dslName) && propertiesAdvanced.length > 0 && <ExpandableSection - toggleText={'Advanced properties'} + toggleText={'EIP advanced properties'} onToggle={(_event, isExpanded) => setShowAdvanced(!showAdvanced)} isExpanded={showAdvanced}> <div className="parameters"> diff --git a/karavan-space/src/designer/property/property/ComponentPropertyField.tsx b/karavan-space/src/designer/property/property/ComponentPropertyField.tsx new file mode 100644 index 00000000..b98b411d --- /dev/null +++ b/karavan-space/src/designer/property/property/ComponentPropertyField.tsx @@ -0,0 +1,349 @@ +/* + * 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, {useRef, useState} from 'react'; +import { + FormGroup, + TextInput, + Popover, + Switch, + InputGroup, + TextArea, + Tooltip, + Button, + capitalize, InputGroupItem +} from '@patternfly/react-core'; +import { + Select, + SelectVariant, + SelectDirection, + SelectOption +} from '@patternfly/react-core/deprecated'; +import '../../karavan.css'; +import "@patternfly/patternfly/patternfly.css"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {ComponentProperty} from "karavan-core/lib/model/ComponentModels"; +import {CamelUi, RouteToCreate} from "../../utils/CamelUi"; +import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition"; +import {ToDefinition} from "karavan-core/lib/model/CamelDefinition"; +import CompressIcon from "@patternfly/react-icons/dist/js/icons/compress-icon"; +import ExpandIcon from "@patternfly/react-icons/dist/js/icons/expand-icon"; +import {InfrastructureSelector} from "./InfrastructureSelector"; +import {InfrastructureAPI} from "../../utils/InfrastructureAPI"; +import DockerIcon from "@patternfly/react-icons/dist/js/icons/docker-icon"; +import ShowIcon from "@patternfly/react-icons/dist/js/icons/eye-icon"; +import HideIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon"; +import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; +import {usePropertiesHook} from "../usePropertiesHook"; +import {useIntegrationStore} from "../../DesignerStore"; +import {shallow} from "zustand/shallow"; +import {KubernetesIcon} from "../../icons/ComponentIcons"; + +const prefix = "parameters"; +const beanPrefix = "#bean:"; + +interface Props { + property: ComponentProperty, + element?: CamelElement, + value: any, + onParameterChange?: (parameter: string, value: string | number | boolean | any, pathParameter?: boolean, newRoute?: RouteToCreate) => void +} + +export function ComponentPropertyField(props: Props) { + + const {onParametersChange, getInternalComponentName} = usePropertiesHook(); + + const [integration] = useIntegrationStore((state) => [state.integration], shallow) + + const [selectStatus, setSelectStatus] = useState<Map<string, boolean>>(new Map<string, boolean>()); + const [showEditor, setShowEditor] = useState<boolean>(false); + const [showPassword, setShowPassword] = useState<boolean>(false); + const [infrastructureSelector, setInfrastructureSelector] = useState<boolean>(false); + const [infrastructureSelectorProperty, setInfrastructureSelectorProperty] = useState<string | undefined>(undefined); + + const [id, setId] = useState<string>(prefix + "-" + props.property.name); + const ref = useRef<any>(null); + + + function parametersChanged(parameter: string, value: string | number | boolean | any, pathParameter?: boolean, newRoute?: RouteToCreate) { + onParametersChange(parameter, value, pathParameter, newRoute); + 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 getSelectBean(property: ComponentProperty, value: any) { + const selectOptions: JSX.Element[] = []; + const beans = CamelUi.getBeans(integration); + if (beans) { + selectOptions.push(<SelectOption key={0} value={"Select..."} isPlaceholder/>); + selectOptions.push(...beans.map((bean) => <SelectOption key={bean.name} value={beanPrefix + bean.name} + description={bean.type}/>)); + } + return ( + <Select + id={id} name={id} + variant={SelectVariant.typeahead} + aria-label={property.name} + onToggle={(_event, isExpanded) => { + openSelect(property.name, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => parametersChanged(property.name, (!isPlaceholder ? value : undefined))} + selections={value} + isCreatable={true} + createText="" + isOpen={isSelectOpen(property.name)} + aria-labelledby={property.name} + direction={SelectDirection.down} + > + {selectOptions} + </Select> + ) + } + + function canBeInternalUri(property: ComponentProperty): boolean { + if (props.element && props.element.dslName === 'ToDefinition' && property.name === 'name') { + const uri: string = (props.element as ToDefinition).uri || ''; + return uri.startsWith("direct") || uri.startsWith("seda"); + } else { + return false; + } + } + + function getInternalUriSelect(property: ComponentProperty, value: any) { + const selectOptions: JSX.Element[] = []; + const componentName = getInternalComponentName(property.name, props.element); + const internalUris = CamelUi.getInternalRouteUris(integration, componentName, false); + const uris: string [] = []; + uris.push(...internalUris); + if (value && value.length > 0 && !uris.includes(value)) { + uris.unshift(value); + } + if (uris && uris.length > 0) { + selectOptions.push(...uris.map((value: string) => + <SelectOption key={value} value={value ? value.trim() : value}/>)); + } + return <InputGroup id={id} name={id}> + <InputGroupItem isFill> + <Select + id={id} name={id} + placeholderText="Select or type an URI" + variant={SelectVariant.typeahead} + aria-label={property.name} + onToggle={(_event, isExpanded) => { + openSelect(property.name, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => { + parametersChanged(property.name, (!isPlaceholder ? value : undefined), property.kind === 'path', undefined); + }} + selections={value} + isOpen={isSelectOpen(property.name)} + isCreatable={true} + createText="" + isInputFilterPersisted={true} + aria-labelledby={property.name} + direction={SelectDirection.down}> + {selectOptions} + </Select> + </InputGroupItem> + <InputGroupItem> + <Tooltip position="bottom-end" content={"Create route"}> + <Button isDisabled={value === undefined} variant="control" onClick={e => { + if (value) { + const newRoute = !internalUris.includes(value.toString()) + ? CamelUi.createNewInternalRoute(componentName.concat(...':',value.toString())) + : undefined; + parametersChanged(property.name, value, property.kind === 'path', newRoute); + } + }}> + {<PlusIcon/>} + </Button> + </Tooltip> + </InputGroupItem> + </InputGroup> + } + + function selectInfrastructure(value: string) { + // check if there is a selection + const textVal = ref.current; + const cursorStart = textVal.selectionStart; + const cursorEnd = textVal.selectionEnd; + if (cursorStart !== cursorEnd) { + const prevValue = props.value; + const selectedText = prevValue.substring(cursorStart, cursorEnd) + value = prevValue.replace(selectedText, value); + } + const propertyName = infrastructureSelectorProperty; + if (propertyName) { + if (value.startsWith("config") || value.startsWith("secret")) value = "{{" + value + "}}"; + parametersChanged(propertyName, value); + setInfrastructureSelector(false); + setInfrastructureSelectorProperty(undefined); + } + } + + function openInfrastructureSelector(propertyName: string) { + setInfrastructureSelector(true); + setInfrastructureSelectorProperty(propertyName); + } + + + function getInfrastructureSelectorModal() { + return ( + <InfrastructureSelector + dark={false} + isOpen={infrastructureSelector} + onClose={() => setInfrastructureSelector(false)} + onSelect={selectInfrastructure}/>) + } + + function getStringInput(property: ComponentProperty, value: any) { + const inInfrastructure = InfrastructureAPI.infrastructure !== 'local'; + const noInfraSelectorButton = ["uri", "id", "description", "group"].includes(property.name); + const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? KubernetesIcon("infra-button") : <DockerIcon/> + return <InputGroup> + {inInfrastructure && !showEditor && !noInfraSelectorButton && + <Tooltip position="bottom-end" + content={"Select from " + capitalize((InfrastructureAPI.infrastructure))}> + <Button variant="control" onClick={e => openInfrastructureSelector(property.name)}> + {icon} + </Button> + </Tooltip>} + {(!showEditor || property.secret) && + <TextInput className="text-field" isRequired ref={ref} + type={property.secret && !showPassword ? "password" : "text"} + id={id} name={id} + value={value !== undefined ? value : property.defaultValue} + onChange={(e, value) => parametersChanged(property.name, value, property.kind === 'path')}/>} + {showEditor && !property.secret && + <TextArea autoResize={true} ref={ref} + className="text-field" isRequired + type="text" + id={id} name={id} + value={value !== undefined ? value : property.defaultValue} + onChange={(e, value) => parametersChanged(property.name, value, property.kind === 'path')}/>} + {!property.secret && + <Tooltip position="bottom-end" content={showEditor ? "Change to TextField" : "Change to Text Area"}> + <Button variant="control" onClick={e => setShowEditor(!showEditor)}> + {showEditor ? <CompressIcon/> : <ExpandIcon/>} + </Button> + </Tooltip> + } + {property.secret && + <Tooltip position="bottom-end" content={showPassword ? "Hide" : "Show"}> + <Button variant="control" onClick={e => setShowPassword(!showPassword)}> + {showPassword ? <ShowIcon/> : <HideIcon/>} + </Button> + </Tooltip> + } + </InputGroup> + } + + function getTextInput(property: ComponentProperty, value: any) { + return ( + <TextInput + className="text-field" isRequired + type={['integer', 'int', 'number'].includes(property.type) ? 'number' : (property.secret ? "password" : "text")} + id={id} name={id} + value={value !== undefined ? value : property.defaultValue} + onChange={(_, value) => { + parametersChanged(property.name, ['integer', 'int', 'number'].includes(property.type) ? Number(value) : value, property.kind === 'path') + }}/> + ) + } + + function getSelect(property: ComponentProperty, value: any) { + const selectOptions: JSX.Element[] = [] + if (property.enum && property.enum.length > 0) { + selectOptions.push(<SelectOption key={0} value={"Select ..."} isPlaceholder/>); + property.enum.forEach(v => selectOptions.push(<SelectOption key={v} value={v}/>)); + } + return ( + <Select + id={id} name={id} + variant={SelectVariant.single} + aria-label={property.name} + onToggle={(_event, isExpanded) => { + openSelect(property.name, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => parametersChanged(property.name, (!isPlaceholder ? value : undefined), property.kind === 'path')} + selections={value !== undefined ? value.toString() : property.defaultValue} + isOpen={isSelectOpen(property.name)} + aria-labelledby={property.name} + direction={SelectDirection.down} + > + {selectOptions} + </Select> + ) + } + + function getSwitch(property: ComponentProperty, value: any) { + const isChecked = value !== undefined ? Boolean(value) : (property.defaultValue !== undefined && ['true', true].includes(property.defaultValue)) + return ( + <Switch + id={id} name={id} + value={value?.toString()} + aria-label={id} + isChecked={isChecked} + onChange={(e, checked) => parametersChanged(property.name, checked)}/> + ) + } + + const property: ComponentProperty = props.property; + const value = props.value; + return ( + <FormGroup + key={id} + label={property.displayName} + isRequired={property.required} + labelIcon={ + <Popover + position={"left"} + headerContent={property.displayName} + bodyContent={property.description} + footerContent={ + <div> + {property.defaultValue !== undefined && <div>{"Default: " + property.defaultValue}</div>} + {property.required && <div>{property.displayName + " is required"}</div>} + </div> + }> + <button type="button" aria-label="More info" onClick={e => e.preventDefault()} + className="pf-v5-c-form__group-label-help"> + <HelpIcon/> + </button> + </Popover> + }> + {canBeInternalUri(property) && getInternalUriSelect(property, value)} + {property.type === 'string' && property.enum === undefined && !canBeInternalUri(property) + && getStringInput(property, value)} + {['duration', 'integer', 'int', 'number'].includes(property.type) && property.enum === undefined && !canBeInternalUri(property) + && getTextInput(property, value)} + {['object'].includes(property.type) && !property.enum + && getSelectBean(property, value)} + {['string', 'object'].includes(property.type) && property.enum + && getSelect(property, value)} + {property.type === 'boolean' + && getSwitch(property, value)} + {getInfrastructureSelectorModal()} + </FormGroup> + ) +} diff --git a/karavan-space/src/designer/property/property/DslPropertyField.tsx b/karavan-space/src/designer/property/property/DslPropertyField.tsx index e138a88a..b01f6e7c 100644 --- a/karavan-space/src/designer/property/property/DslPropertyField.tsx +++ b/karavan-space/src/designer/property/property/DslPropertyField.tsx @@ -50,7 +50,7 @@ import {PropertyMeta} from "karavan-core/lib/model/CamelMetadata"; import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt"; import {ExpressionField} from "./ExpressionField"; import {CamelUi, RouteToCreate} from "../../utils/CamelUi"; -import {ComponentParameterField} from "./ComponentParameterField"; +import {ComponentPropertyField} from "./ComponentPropertyField"; import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition"; import {KameletPropertyField} from "./KameletPropertyField"; import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; @@ -152,7 +152,11 @@ export function DslPropertyField(props: Props) { arrayChanged(fieldId, ""); } - function getLabel(property: PropertyMeta, value: any) { + function isParameter(property: PropertyMeta): boolean { + return property.name === 'parameters' && property.description === 'parameters'; + } + + function getLabel(property: PropertyMeta, value: any, isKamelet: boolean) { if (!isMultiValueField(property) && property.isObject && !property.isArray && !["ExpressionDefinition"].includes(property.type)) { const tooltip = value ? "Delete " + property.name : "Add " + property.name; const className = value ? "change-button delete-button" : "change-button add-button"; @@ -169,6 +173,8 @@ export function DslPropertyField(props: Props) { </Tooltip> </div> ) + } if (isParameter(property)) { + return isKamelet ? "Kamelet properties:" : "Component properties:"; } else if (!["ExpressionDefinition"].includes(property.type)) { return CamelUtil.capitalizeName(property.displayName); } @@ -684,7 +690,7 @@ export function DslPropertyField(props: Props) { <div className="parameters"> {properties.map(kp => { const value = CamelDefinitionApiExt.getParametersValue(element, kp.name, kp.kind === 'path'); - return (<ComponentParameterField + return (<ComponentPropertyField key={kp.name} property={kp} value={value} @@ -696,7 +702,7 @@ export function DslPropertyField(props: Props) { ) } - function getExpandableComponentParameters(properties: ComponentProperty[], label: string) { + function getExpandableComponentProperties(properties: ComponentProperty[], label: string) { const element = props.element; return ( @@ -715,7 +721,7 @@ export function DslPropertyField(props: Props) { isExpanded={isShowAdvanced.includes(label)}> <div className="parameters"> {properties.map(kp => - <ComponentParameterField + <ComponentPropertyField key={kp.name} property={kp} value={CamelDefinitionApiExt.getParametersValue(element, kp.name, kp.kind === 'path')} @@ -779,11 +785,11 @@ export function DslPropertyField(props: Props) { <> {property.name === 'parameters' && getMainComponentParameters(propertiesMain)} {property.name === 'parameters' && element && propertiesScheduler.length > 0 - && getExpandableComponentParameters(propertiesScheduler, "Scheduler parameters")} + && getExpandableComponentProperties(propertiesScheduler, "Component scheduler properties")} {property.name === 'parameters' && element && propertiesSecurity.length > 0 - && getExpandableComponentParameters(propertiesSecurity, "Security parameters")} + && getExpandableComponentProperties(propertiesSecurity, "Component security properties")} {property.name === 'parameters' && element && propertiesAdvanced.length > 0 - && getExpandableComponentParameters(propertiesAdvanced, "Advanced parameters")} + && getExpandableComponentProperties(propertiesAdvanced, "Component advanced properties")} </> ) } @@ -797,9 +803,9 @@ export function DslPropertyField(props: Props) { return ( <div> <FormGroup - label={props.hideLabel ? undefined : getLabel(property, value)} + label={props.hideLabel ? undefined : getLabel(property, value, isKamelet)} isRequired={property.required} - labelIcon={getLabelIcon(property)}> + labelIcon={isParameter(property) ? undefined : getLabelIcon(property)}> {value !== undefined && ["ExpressionDefinition", "ExpressionSubElementDefinition"].includes(property.type) && getExpressionField(property, value)} {property.isObject && !property.isArray && !["ExpressionDefinition", "ExpressionSubElementDefinition"].includes(property.type) diff --git a/karavan-space/src/topology/TopologyTab.tsx b/karavan-space/src/topology/TopologyTab.tsx index be2e754a..727905cc 100644 --- a/karavan-space/src/topology/TopologyTab.tsx +++ b/karavan-space/src/topology/TopologyTab.tsx @@ -35,7 +35,6 @@ import {IntegrationFile, useTopologyStore} from "./TopologyStore"; import {TopologyPropertiesPanel} from "./TopologyPropertiesPanel"; import {TopologyToolbar} from "./TopologyToolbar"; import {useDesignerStore} from "../designer/DesignerStore"; -import RankerIcon from "@patternfly/react-icons/dist/esm/icons/random-icon"; interface Props { files: IntegrationFile[], diff --git a/karavan-space/src/topology/TopologyToolbar.tsx b/karavan-space/src/topology/TopologyToolbar.tsx index 473bb6f9..667cf8f5 100644 --- a/karavan-space/src/topology/TopologyToolbar.tsx +++ b/karavan-space/src/topology/TopologyToolbar.tsx @@ -34,7 +34,7 @@ export function TopologyToolbar (props: Props) { <ToolbarContent> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add Integration Route"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddRoute()} @@ -45,7 +45,7 @@ export function TopologyToolbar (props: Props) { </ToolbarItem> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add REST API"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddREST()} @@ -56,7 +56,7 @@ export function TopologyToolbar (props: Props) { </ToolbarItem> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add Bean"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddBean()} diff --git a/karavan-vscode/webview/topology/TopologyStore.ts b/karavan-vscode/webview/topology/TopologyStore.ts index 3cf5067b..73aead9f 100644 --- a/karavan-vscode/webview/topology/TopologyStore.ts +++ b/karavan-vscode/webview/topology/TopologyStore.ts @@ -33,6 +33,8 @@ interface TopologyState { fileName?: string setSelectedIds: (selectedIds: string []) => void setFileName: (fileName?: string) => void + ranker: string + setRanker: (ranker: string) => void } export const useTopologyStore = createWithEqualityFn<TopologyState>((set) => ({ @@ -46,5 +48,11 @@ export const useTopologyStore = createWithEqualityFn<TopologyState>((set) => ({ set((state: TopologyState) => { return {fileName: fileName}; }); - } + }, + ranker: 'network-simplex', + setRanker: (ranker: string) => { + set((state: TopologyState) => { + return {ranker: ranker}; + }); + }, }), shallow) diff --git a/karavan-vscode/webview/topology/TopologyTab.tsx b/karavan-vscode/webview/topology/TopologyTab.tsx index 5da3a3c4..727905cc 100644 --- a/karavan-vscode/webview/topology/TopologyTab.tsx +++ b/karavan-vscode/webview/topology/TopologyTab.tsx @@ -45,10 +45,10 @@ interface Props { onClickAddBean: () => void } -export function TopologyTab (props: Props) { +export function TopologyTab(props: Props) { - const [selectedIds, setSelectedIds, setFileName] = useTopologyStore((s) => - [s.selectedIds, s.setSelectedIds, s.setFileName], shallow); + const [selectedIds, setSelectedIds, setFileName, ranker, setRanker] = useTopologyStore((s) => + [s.selectedIds, s.setSelectedIds, s.setFileName, s.ranker, s.setRanker], shallow); const [setSelectedStep] = useDesignerStore((s) => [s.setSelectedStep], shallow) function setTopologySelected(model: Model, ids: string []) { @@ -71,7 +71,15 @@ export function TopologyTab (props: Props) { const controller = React.useMemo(() => { const model = getModel(props.files); const newController = new Visualization(); - newController.registerLayoutFactory((_, graph) => new DagreLayout(graph)); + newController.registerLayoutFactory((_, graph) => + new DagreLayout(graph, { + rankdir: 'TB', + ranker: ranker, + nodesep: 20, + edgesep: 20, + ranksep: 0 + })); + newController.registerComponentFactory(customComponentFactory); newController.addEventListener(SELECTION_EVENT, args => setTopologySelected(model, args)); @@ -84,15 +92,51 @@ export function TopologyTab (props: Props) { newController.fromModel(model, false); return newController; - }, []); + },[]); React.useEffect(() => { setSelectedIds([]) const model = getModel(props.files); controller.fromModel(model, false); - }, []); + }, [ranker, controller, setSelectedIds, props.files]); + + const controlButtons = React.useMemo(() => { + // const customButtons = [ + // { + // id: "change-ranker", + // icon: <RankerIcon />, + // tooltip: 'Change Ranker ' + ranker, + // ariaLabel: '', + // callback: (id: any) => { + // if (ranker === 'network-simplex') { + // setRanker('tight-tree') + // } else { + // setRanker('network-simplex') + // } + // } + // } + // ]; + return createTopologyControlButtons({ + ...defaultControlButtonsOptions, + zoomInCallback: action(() => { + controller.getGraph().scaleBy(4 / 3); + }), + zoomOutCallback: action(() => { + controller.getGraph().scaleBy(0.75); + }), + fitToScreenCallback: action(() => { + controller.getGraph().fit(80); + }), + resetViewCallback: action(() => { + controller.getGraph().reset(); + controller.getGraph().layout(); + }), + legend: false, + // customButtons, + }); + }, [ranker, controller, setRanker]); - return ( + return ( <TopologyView className="topology-panel" contextToolbar={!props.hideToolbar @@ -103,28 +147,12 @@ export function TopologyTab (props: Props) { sideBar={<TopologyPropertiesPanel onSetFile={props.onSetFile}/>} controlBar={ <TopologyControlBar - controlButtons={createTopologyControlButtons({ - ...defaultControlButtonsOptions, - zoomInCallback: action(() => { - controller.getGraph().scaleBy(4 / 3); - }), - zoomOutCallback: action(() => { - controller.getGraph().scaleBy(0.75); - }), - fitToScreenCallback: action(() => { - controller.getGraph().fit(80); - }), - resetViewCallback: action(() => { - controller.getGraph().reset(); - controller.getGraph().layout(); - }), - legend: false - })} + controlButtons={controlButtons} /> } > <VisualizationProvider controller={controller}> - <VisualizationSurface state={{ selectedIds }}/> + <VisualizationSurface state={{selectedIds}}/> </VisualizationProvider> </TopologyView> ); diff --git a/karavan-vscode/webview/topology/TopologyToolbar.tsx b/karavan-vscode/webview/topology/TopologyToolbar.tsx index 473bb6f9..667cf8f5 100644 --- a/karavan-vscode/webview/topology/TopologyToolbar.tsx +++ b/karavan-vscode/webview/topology/TopologyToolbar.tsx @@ -34,7 +34,7 @@ export function TopologyToolbar (props: Props) { <ToolbarContent> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add Integration Route"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddRoute()} @@ -45,7 +45,7 @@ export function TopologyToolbar (props: Props) { </ToolbarItem> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add REST API"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddREST()} @@ -56,7 +56,7 @@ export function TopologyToolbar (props: Props) { </ToolbarItem> <ToolbarItem align={{default:"alignRight"}}> <Tooltip content={"Add Bean"} position={"bottom"}> - <Button size="sm" + <Button className="dev-action-button" size="sm" variant={"primary"} icon={<PlusIcon/>} onClick={e => props.onClickAddBean()} diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx index dfe8ae9d..9325ee05 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx @@ -27,7 +27,7 @@ import { MenuToggleElement, MenuToggle, DropdownList, - DropdownItem, + DropdownItem, Label, Flex, LabelGroup, Popover, FlexItem, Badge, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -44,6 +44,9 @@ import {shallow} from "zustand/shallow"; import {usePropertiesHook} from "./usePropertiesHook"; import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil"; import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import QuestionIcon from '@patternfly/react-icons/dist/esm/icons/question-icon'; +import {ComponentApi} from "karavan-core/lib/api/ComponentApi"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; interface Props { designerType: 'routes' | 'rest' | 'beans' @@ -53,7 +56,15 @@ export function DslProperties(props: Props) { const [integration] = useIntegrationStore((s) => [s.integration], shallow) - const {saveAsRoute, convertStep, cloneElement, onDataFormatChange, onPropertyChange, onParametersChange, onExpressionChange} = + const { + saveAsRoute, + convertStep, + cloneElement, + onDataFormatChange, + onPropertyChange, + onParametersChange, + onExpressionChange + } = usePropertiesHook(props.designerType); const [selectedStep, dark] @@ -63,7 +74,7 @@ export function DslProperties(props: Props) { const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - useEffect(()=> { + useEffect(() => { setMenuOpen(false) }, [selectedStep]) @@ -77,7 +88,8 @@ export function DslProperties(props: Props) { style={{inset: "0px auto auto -70px important!"}} className={"xxx"} isOpen={isMenuOpen} - onSelect={() => {}} + onSelect={() => { + }} onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( <MenuToggle @@ -89,11 +101,11 @@ export function DslProperties(props: Props) { onClick={() => setMenuOpen(!isMenuOpen)} isExpanded={isMenuOpen} > - <EllipsisVIcon /> + <EllipsisVIcon/> </MenuToggle> )} > - <DropdownList > + <DropdownList> {hasSteps && <DropdownItem key="saveRoute" onClick={(ev) => { ev.preventDefault() @@ -102,8 +114,8 @@ export function DslProperties(props: Props) { setMenuOpen(false); } }}> - Save Steps to Route - </DropdownItem>} + Save Steps to Route + </DropdownItem>} {hasSteps && <DropdownItem key="saveRoute" onClick={(ev) => { ev.preventDefault() @@ -112,19 +124,19 @@ export function DslProperties(props: Props) { setMenuOpen(false); } }}> - Save Element to Route + Save Element to Route </DropdownItem>} {targetDsl && <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} </DropdownList> </Dropdown> : <></>; } @@ -133,6 +145,8 @@ export function DslProperties(props: Props) { const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep) const description = selectedStep && CamelDisplayUtil.getDescription(selectedStep); const descriptionLines: string [] = description ? description?.split("\n") : [""]; + const headers = ComponentApi.getComponentHeadersList(selectedStep) + const groups = selectedStep?.dslName === 'FromDefinition' ? ['consumer', 'common'] : ['producer', 'common'] return ( <div className="headers"> <div className="top"> @@ -147,6 +161,42 @@ export function DslProperties(props: Props) { {descriptionLines.filter((value, index) => index > 0) .map((desc, index, array) => <Text key={index} component={TextVariants.p}>{desc}</Text>)} </ExpandableSection>} + + {headers.length > 0 && + <ExpandableSection toggleText='Headers' + onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} + isExpanded={isDescriptionExpanded}> + <Flex direction={{default:"column"}}> + {headers.filter((header) => groups.includes(header.group)) + .map((header, index, array) => + <Flex key={index}> + <Text style={{marginLeft: "26px"}} component={TextVariants.p}>{header.name}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Popover + position={"left"} + headerContent={header.name} + bodyContent={header.description} + footerContent={ + <Flex> + <Text component={TextVariants.p}>{header.javaType}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Badge isRead>{header.group}</Badge> + </FlexItem> + </Flex> + } + > + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} className="pf-v5-c-form__group-label-help"> + <HelpIcon/> + </button> + </Popover> + </FlexItem> + </Flex> + )} + </Flex> + </ExpandableSection>} </div> ) } @@ -216,7 +266,7 @@ export function DslProperties(props: Props) { {selectedStep && !['MarshalDefinition', 'UnmarshalDefinition'].includes(selectedStep.dslName) && propertiesAdvanced.length > 0 && <ExpandableSection - toggleText={'Advanced properties'} + toggleText={'EIP advanced properties'} onToggle={(_event, isExpanded) => setShowAdvanced(!showAdvanced)} isExpanded={showAdvanced}> <div className="parameters"> diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/property/property/ComponentPropertyField.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/property/property/ComponentPropertyField.tsx new file mode 100644 index 00000000..b98b411d --- /dev/null +++ b/karavan-web/karavan-app/src/main/webui/src/designer/property/property/ComponentPropertyField.tsx @@ -0,0 +1,349 @@ +/* + * 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, {useRef, useState} from 'react'; +import { + FormGroup, + TextInput, + Popover, + Switch, + InputGroup, + TextArea, + Tooltip, + Button, + capitalize, InputGroupItem +} from '@patternfly/react-core'; +import { + Select, + SelectVariant, + SelectDirection, + SelectOption +} from '@patternfly/react-core/deprecated'; +import '../../karavan.css'; +import "@patternfly/patternfly/patternfly.css"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {ComponentProperty} from "karavan-core/lib/model/ComponentModels"; +import {CamelUi, RouteToCreate} from "../../utils/CamelUi"; +import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition"; +import {ToDefinition} from "karavan-core/lib/model/CamelDefinition"; +import CompressIcon from "@patternfly/react-icons/dist/js/icons/compress-icon"; +import ExpandIcon from "@patternfly/react-icons/dist/js/icons/expand-icon"; +import {InfrastructureSelector} from "./InfrastructureSelector"; +import {InfrastructureAPI} from "../../utils/InfrastructureAPI"; +import DockerIcon from "@patternfly/react-icons/dist/js/icons/docker-icon"; +import ShowIcon from "@patternfly/react-icons/dist/js/icons/eye-icon"; +import HideIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon"; +import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; +import {usePropertiesHook} from "../usePropertiesHook"; +import {useIntegrationStore} from "../../DesignerStore"; +import {shallow} from "zustand/shallow"; +import {KubernetesIcon} from "../../icons/ComponentIcons"; + +const prefix = "parameters"; +const beanPrefix = "#bean:"; + +interface Props { + property: ComponentProperty, + element?: CamelElement, + value: any, + onParameterChange?: (parameter: string, value: string | number | boolean | any, pathParameter?: boolean, newRoute?: RouteToCreate) => void +} + +export function ComponentPropertyField(props: Props) { + + const {onParametersChange, getInternalComponentName} = usePropertiesHook(); + + const [integration] = useIntegrationStore((state) => [state.integration], shallow) + + const [selectStatus, setSelectStatus] = useState<Map<string, boolean>>(new Map<string, boolean>()); + const [showEditor, setShowEditor] = useState<boolean>(false); + const [showPassword, setShowPassword] = useState<boolean>(false); + const [infrastructureSelector, setInfrastructureSelector] = useState<boolean>(false); + const [infrastructureSelectorProperty, setInfrastructureSelectorProperty] = useState<string | undefined>(undefined); + + const [id, setId] = useState<string>(prefix + "-" + props.property.name); + const ref = useRef<any>(null); + + + function parametersChanged(parameter: string, value: string | number | boolean | any, pathParameter?: boolean, newRoute?: RouteToCreate) { + onParametersChange(parameter, value, pathParameter, newRoute); + 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 getSelectBean(property: ComponentProperty, value: any) { + const selectOptions: JSX.Element[] = []; + const beans = CamelUi.getBeans(integration); + if (beans) { + selectOptions.push(<SelectOption key={0} value={"Select..."} isPlaceholder/>); + selectOptions.push(...beans.map((bean) => <SelectOption key={bean.name} value={beanPrefix + bean.name} + description={bean.type}/>)); + } + return ( + <Select + id={id} name={id} + variant={SelectVariant.typeahead} + aria-label={property.name} + onToggle={(_event, isExpanded) => { + openSelect(property.name, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => parametersChanged(property.name, (!isPlaceholder ? value : undefined))} + selections={value} + isCreatable={true} + createText="" + isOpen={isSelectOpen(property.name)} + aria-labelledby={property.name} + direction={SelectDirection.down} + > + {selectOptions} + </Select> + ) + } + + function canBeInternalUri(property: ComponentProperty): boolean { + if (props.element && props.element.dslName === 'ToDefinition' && property.name === 'name') { + const uri: string = (props.element as ToDefinition).uri || ''; + return uri.startsWith("direct") || uri.startsWith("seda"); + } else { + return false; + } + } + + function getInternalUriSelect(property: ComponentProperty, value: any) { + const selectOptions: JSX.Element[] = []; + const componentName = getInternalComponentName(property.name, props.element); + const internalUris = CamelUi.getInternalRouteUris(integration, componentName, false); + const uris: string [] = []; + uris.push(...internalUris); + if (value && value.length > 0 && !uris.includes(value)) { + uris.unshift(value); + } + if (uris && uris.length > 0) { + selectOptions.push(...uris.map((value: string) => + <SelectOption key={value} value={value ? value.trim() : value}/>)); + } + return <InputGroup id={id} name={id}> + <InputGroupItem isFill> + <Select + id={id} name={id} + placeholderText="Select or type an URI" + variant={SelectVariant.typeahead} + aria-label={property.name} + onToggle={(_event, isExpanded) => { + openSelect(property.name, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => { + parametersChanged(property.name, (!isPlaceholder ? value : undefined), property.kind === 'path', undefined); + }} + selections={value} + isOpen={isSelectOpen(property.name)} + isCreatable={true} + createText="" + isInputFilterPersisted={true} + aria-labelledby={property.name} + direction={SelectDirection.down}> + {selectOptions} + </Select> + </InputGroupItem> + <InputGroupItem> + <Tooltip position="bottom-end" content={"Create route"}> + <Button isDisabled={value === undefined} variant="control" onClick={e => { + if (value) { + const newRoute = !internalUris.includes(value.toString()) + ? CamelUi.createNewInternalRoute(componentName.concat(...':',value.toString())) + : undefined; + parametersChanged(property.name, value, property.kind === 'path', newRoute); + } + }}> + {<PlusIcon/>} + </Button> + </Tooltip> + </InputGroupItem> + </InputGroup> + } + + function selectInfrastructure(value: string) { + // check if there is a selection + const textVal = ref.current; + const cursorStart = textVal.selectionStart; + const cursorEnd = textVal.selectionEnd; + if (cursorStart !== cursorEnd) { + const prevValue = props.value; + const selectedText = prevValue.substring(cursorStart, cursorEnd) + value = prevValue.replace(selectedText, value); + } + const propertyName = infrastructureSelectorProperty; + if (propertyName) { + if (value.startsWith("config") || value.startsWith("secret")) value = "{{" + value + "}}"; + parametersChanged(propertyName, value); + setInfrastructureSelector(false); + setInfrastructureSelectorProperty(undefined); + } + } + + function openInfrastructureSelector(propertyName: string) { + setInfrastructureSelector(true); + setInfrastructureSelectorProperty(propertyName); + } + + + function getInfrastructureSelectorModal() { + return ( + <InfrastructureSelector + dark={false} + isOpen={infrastructureSelector} + onClose={() => setInfrastructureSelector(false)} + onSelect={selectInfrastructure}/>) + } + + function getStringInput(property: ComponentProperty, value: any) { + const inInfrastructure = InfrastructureAPI.infrastructure !== 'local'; + const noInfraSelectorButton = ["uri", "id", "description", "group"].includes(property.name); + const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? KubernetesIcon("infra-button") : <DockerIcon/> + return <InputGroup> + {inInfrastructure && !showEditor && !noInfraSelectorButton && + <Tooltip position="bottom-end" + content={"Select from " + capitalize((InfrastructureAPI.infrastructure))}> + <Button variant="control" onClick={e => openInfrastructureSelector(property.name)}> + {icon} + </Button> + </Tooltip>} + {(!showEditor || property.secret) && + <TextInput className="text-field" isRequired ref={ref} + type={property.secret && !showPassword ? "password" : "text"} + id={id} name={id} + value={value !== undefined ? value : property.defaultValue} + onChange={(e, value) => parametersChanged(property.name, value, property.kind === 'path')}/>} + {showEditor && !property.secret && + <TextArea autoResize={true} ref={ref} + className="text-field" isRequired + type="text" + id={id} name={id} + value={value !== undefined ? value : property.defaultValue} + onChange={(e, value) => parametersChanged(property.name, value, property.kind === 'path')}/>} + {!property.secret && + <Tooltip position="bottom-end" content={showEditor ? "Change to TextField" : "Change to Text Area"}> + <Button variant="control" onClick={e => setShowEditor(!showEditor)}> + {showEditor ? <CompressIcon/> : <ExpandIcon/>} + </Button> + </Tooltip> + } + {property.secret && + <Tooltip position="bottom-end" content={showPassword ? "Hide" : "Show"}> + <Button variant="control" onClick={e => setShowPassword(!showPassword)}> + {showPassword ? <ShowIcon/> : <HideIcon/>} + </Button> + </Tooltip> + } + </InputGroup> + } + + function getTextInput(property: ComponentProperty, value: any) { + return ( + <TextInput + className="text-field" isRequired + type={['integer', 'int', 'number'].includes(property.type) ? 'number' : (property.secret ? "password" : "text")} + id={id} name={id} + value={value !== undefined ? value : property.defaultValue} + onChange={(_, value) => { + parametersChanged(property.name, ['integer', 'int', 'number'].includes(property.type) ? Number(value) : value, property.kind === 'path') + }}/> + ) + } + + function getSelect(property: ComponentProperty, value: any) { + const selectOptions: JSX.Element[] = [] + if (property.enum && property.enum.length > 0) { + selectOptions.push(<SelectOption key={0} value={"Select ..."} isPlaceholder/>); + property.enum.forEach(v => selectOptions.push(<SelectOption key={v} value={v}/>)); + } + return ( + <Select + id={id} name={id} + variant={SelectVariant.single} + aria-label={property.name} + onToggle={(_event, isExpanded) => { + openSelect(property.name, isExpanded) + }} + onSelect={(e, value, isPlaceholder) => parametersChanged(property.name, (!isPlaceholder ? value : undefined), property.kind === 'path')} + selections={value !== undefined ? value.toString() : property.defaultValue} + isOpen={isSelectOpen(property.name)} + aria-labelledby={property.name} + direction={SelectDirection.down} + > + {selectOptions} + </Select> + ) + } + + function getSwitch(property: ComponentProperty, value: any) { + const isChecked = value !== undefined ? Boolean(value) : (property.defaultValue !== undefined && ['true', true].includes(property.defaultValue)) + return ( + <Switch + id={id} name={id} + value={value?.toString()} + aria-label={id} + isChecked={isChecked} + onChange={(e, checked) => parametersChanged(property.name, checked)}/> + ) + } + + const property: ComponentProperty = props.property; + const value = props.value; + return ( + <FormGroup + key={id} + label={property.displayName} + isRequired={property.required} + labelIcon={ + <Popover + position={"left"} + headerContent={property.displayName} + bodyContent={property.description} + footerContent={ + <div> + {property.defaultValue !== undefined && <div>{"Default: " + property.defaultValue}</div>} + {property.required && <div>{property.displayName + " is required"}</div>} + </div> + }> + <button type="button" aria-label="More info" onClick={e => e.preventDefault()} + className="pf-v5-c-form__group-label-help"> + <HelpIcon/> + </button> + </Popover> + }> + {canBeInternalUri(property) && getInternalUriSelect(property, value)} + {property.type === 'string' && property.enum === undefined && !canBeInternalUri(property) + && getStringInput(property, value)} + {['duration', 'integer', 'int', 'number'].includes(property.type) && property.enum === undefined && !canBeInternalUri(property) + && getTextInput(property, value)} + {['object'].includes(property.type) && !property.enum + && getSelectBean(property, value)} + {['string', 'object'].includes(property.type) && property.enum + && getSelect(property, value)} + {property.type === 'boolean' + && getSwitch(property, value)} + {getInfrastructureSelectorModal()} + </FormGroup> + ) +} diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/property/property/DslPropertyField.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/property/property/DslPropertyField.tsx index e138a88a..b01f6e7c 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/property/property/DslPropertyField.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/property/property/DslPropertyField.tsx @@ -50,7 +50,7 @@ import {PropertyMeta} from "karavan-core/lib/model/CamelMetadata"; import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt"; import {ExpressionField} from "./ExpressionField"; import {CamelUi, RouteToCreate} from "../../utils/CamelUi"; -import {ComponentParameterField} from "./ComponentParameterField"; +import {ComponentPropertyField} from "./ComponentPropertyField"; import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition"; import {KameletPropertyField} from "./KameletPropertyField"; import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; @@ -152,7 +152,11 @@ export function DslPropertyField(props: Props) { arrayChanged(fieldId, ""); } - function getLabel(property: PropertyMeta, value: any) { + function isParameter(property: PropertyMeta): boolean { + return property.name === 'parameters' && property.description === 'parameters'; + } + + function getLabel(property: PropertyMeta, value: any, isKamelet: boolean) { if (!isMultiValueField(property) && property.isObject && !property.isArray && !["ExpressionDefinition"].includes(property.type)) { const tooltip = value ? "Delete " + property.name : "Add " + property.name; const className = value ? "change-button delete-button" : "change-button add-button"; @@ -169,6 +173,8 @@ export function DslPropertyField(props: Props) { </Tooltip> </div> ) + } if (isParameter(property)) { + return isKamelet ? "Kamelet properties:" : "Component properties:"; } else if (!["ExpressionDefinition"].includes(property.type)) { return CamelUtil.capitalizeName(property.displayName); } @@ -684,7 +690,7 @@ export function DslPropertyField(props: Props) { <div className="parameters"> {properties.map(kp => { const value = CamelDefinitionApiExt.getParametersValue(element, kp.name, kp.kind === 'path'); - return (<ComponentParameterField + return (<ComponentPropertyField key={kp.name} property={kp} value={value} @@ -696,7 +702,7 @@ export function DslPropertyField(props: Props) { ) } - function getExpandableComponentParameters(properties: ComponentProperty[], label: string) { + function getExpandableComponentProperties(properties: ComponentProperty[], label: string) { const element = props.element; return ( @@ -715,7 +721,7 @@ export function DslPropertyField(props: Props) { isExpanded={isShowAdvanced.includes(label)}> <div className="parameters"> {properties.map(kp => - <ComponentParameterField + <ComponentPropertyField key={kp.name} property={kp} value={CamelDefinitionApiExt.getParametersValue(element, kp.name, kp.kind === 'path')} @@ -779,11 +785,11 @@ export function DslPropertyField(props: Props) { <> {property.name === 'parameters' && getMainComponentParameters(propertiesMain)} {property.name === 'parameters' && element && propertiesScheduler.length > 0 - && getExpandableComponentParameters(propertiesScheduler, "Scheduler parameters")} + && getExpandableComponentProperties(propertiesScheduler, "Component scheduler properties")} {property.name === 'parameters' && element && propertiesSecurity.length > 0 - && getExpandableComponentParameters(propertiesSecurity, "Security parameters")} + && getExpandableComponentProperties(propertiesSecurity, "Component security properties")} {property.name === 'parameters' && element && propertiesAdvanced.length > 0 - && getExpandableComponentParameters(propertiesAdvanced, "Advanced parameters")} + && getExpandableComponentProperties(propertiesAdvanced, "Component advanced properties")} </> ) } @@ -797,9 +803,9 @@ export function DslPropertyField(props: Props) { return ( <div> <FormGroup - label={props.hideLabel ? undefined : getLabel(property, value)} + label={props.hideLabel ? undefined : getLabel(property, value, isKamelet)} isRequired={property.required} - labelIcon={getLabelIcon(property)}> + labelIcon={isParameter(property) ? undefined : getLabelIcon(property)}> {value !== undefined && ["ExpressionDefinition", "ExpressionSubElementDefinition"].includes(property.type) && getExpressionField(property, value)} {property.isObject && !property.isArray && !["ExpressionDefinition", "ExpressionSubElementDefinition"].includes(property.type) diff --git a/karavan-web/karavan-app/src/main/webui/src/topology/TopologyTab.tsx b/karavan-web/karavan-app/src/main/webui/src/topology/TopologyTab.tsx index be2e754a..727905cc 100644 --- a/karavan-web/karavan-app/src/main/webui/src/topology/TopologyTab.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/topology/TopologyTab.tsx @@ -35,7 +35,6 @@ import {IntegrationFile, useTopologyStore} from "./TopologyStore"; import {TopologyPropertiesPanel} from "./TopologyPropertiesPanel"; import {TopologyToolbar} from "./TopologyToolbar"; import {useDesignerStore} from "../designer/DesignerStore"; -import RankerIcon from "@patternfly/react-icons/dist/esm/icons/random-icon"; interface Props { files: IntegrationFile[],