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 e1119cbe5f0703dea77c5c88fd01ef08ec7fb836 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Wed Jan 31 16:34:01 2024 -0500 #1091 in Designer --- karavan-designer/src/DesignerPage.tsx | 4 + karavan-designer/src/designer/DesignerStore.ts | 16 ++- karavan-designer/src/designer/KaravanDesigner.tsx | 8 +- .../src/designer/property/DslProperties.css | 7 +- .../src/designer/property/DslProperties.tsx | 10 +- .../property/property/ComponentPropertyField.tsx | 49 ++++++--- .../ComponentPropertyPlaceholderDropdown.css | 24 +++++ .../ComponentPropertyPlaceholderDropdown.tsx | 81 +++++++++++++++ .../property/property/DslPropertyField.tsx | 1 + karavan-space/src/designer/DesignerStore.ts | 16 ++- karavan-space/src/designer/KaravanDesigner.tsx | 8 +- .../src/designer/property/DslProperties.css | 12 +++ .../src/designer/property/DslProperties.tsx | 16 +-- .../property/property/ComponentPropertyField.tsx | 113 ++++++++++++++------- .../ComponentPropertyPlaceholderDropdown.css | 24 +++++ .../ComponentPropertyPlaceholderDropdown.tsx | 81 +++++++++++++++ .../property/property/DslPropertyField.tsx | 70 ++++++------- .../src/designer/property/property/ModalEditor.tsx | 5 +- karavan-space/src/space/SpacePage.tsx | 1 + .../webui/src/designer/property/DslProperties.css | 7 ++ .../webui/src/designer/property/DslProperties.tsx | 8 +- .../property/property/ComponentPropertyField.tsx | 74 +++++++++----- .../property/property/DslPropertyField.tsx | 69 ++++++------- .../src/designer/property/property/ModalEditor.tsx | 5 +- 24 files changed, 532 insertions(+), 177 deletions(-) diff --git a/karavan-designer/src/DesignerPage.tsx b/karavan-designer/src/DesignerPage.tsx index 909b15c5..125e3626 100644 --- a/karavan-designer/src/DesignerPage.tsx +++ b/karavan-designer/src/DesignerPage.tsx @@ -77,6 +77,10 @@ export const DesignerPage = (props: Props) => { onSaveCustomCode={(name1, code) => { console.log(name1, code) }} + propertyPlaceholders={[ + "timer.delay", + "sql.query" + ]} /> ) } diff --git a/karavan-designer/src/designer/DesignerStore.ts b/karavan-designer/src/designer/DesignerStore.ts index 407713a7..fe198a7f 100644 --- a/karavan-designer/src/designer/DesignerStore.ts +++ b/karavan-designer/src/designer/DesignerStore.ts @@ -168,7 +168,8 @@ type DesignerState = { height: number, top: number, left: number, - moveElements: [string | undefined, string | undefined] + moveElements: [string | undefined, string | undefined], + propertyPlaceholders: string[]; } const designerState: DesignerState = { @@ -186,7 +187,8 @@ const designerState: DesignerState = { height: 0, top: 0, left: 0, - moveElements: [undefined, undefined] + moveElements: [undefined, undefined], + propertyPlaceholders: [] }; type DesignerAction = { @@ -203,6 +205,7 @@ type DesignerAction = { reset: () => void; setNotification: (notificationBadge: boolean, notificationMessage: [string, string]) => void; setMoveElements: (moveElements: [string | undefined, string | undefined]) => void; + setPropertyPlaceholders: (propertyPlaceholders: string[]) => void; } export const useDesignerStore = createWithEqualityFn<DesignerState & DesignerAction>((set) => ({ @@ -257,5 +260,12 @@ export const useDesignerStore = createWithEqualityFn<DesignerState & DesignerAct }, setMoveElements: (moveElements: [string | undefined, string | undefined]) => { set({moveElements: moveElements}) - } + }, + setPropertyPlaceholders: (propertyPlaceholders: string[]) => { + set((state: DesignerState) => { + state.propertyPlaceholders.length = 0; + state.propertyPlaceholders.push(...propertyPlaceholders); + return state; + }) + }, }), shallow) \ No newline at end of file diff --git a/karavan-designer/src/designer/KaravanDesigner.tsx b/karavan-designer/src/designer/KaravanDesigner.tsx index 626f984b..349ae548 100644 --- a/karavan-designer/src/designer/KaravanDesigner.tsx +++ b/karavan-designer/src/designer/KaravanDesigner.tsx @@ -41,7 +41,6 @@ import {BeansDesigner} from "./beans/BeansDesigner"; import {CodeEditor} from "./editor/CodeEditor"; import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; import {KameletDesigner} from "./kamelet/KameletDesigner"; -import {v4 as uuidv4} from "uuid"; interface Props { onSave: (filename: string, yaml: string, propertyOnly: boolean) => void @@ -53,13 +52,15 @@ interface Props { hideLogDSL?: boolean showCodeTab: boolean tab?: "routes" | "rest" | "beans" + propertyPlaceholders: string[] } export function KaravanDesigner(props: Props) { const [tab, setTab] = useState<string>('routes'); - const [setDark, hideLogDSL, setHideLogDSL, setSelectedStep, reset, badge, message] = useDesignerStore((s) => - [s.setDark, s.hideLogDSL, s.setHideLogDSL, s.setSelectedStep, s.reset, s.notificationBadge, s.notificationMessage], shallow) + const [setDark, hideLogDSL, setHideLogDSL, setSelectedStep, reset, badge, message, setPropertyPlaceholders] = + useDesignerStore((s) => + [s.setDark, s.hideLogDSL, s.setHideLogDSL, s.setSelectedStep, s.reset, s.notificationBadge, s.notificationMessage, s.setPropertyPlaceholders], shallow) const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) @@ -83,6 +84,7 @@ export function KaravanDesigner(props: Props) { setTab(designerTab || 'routes') reset(); setDark(props.dark); + setPropertyPlaceholders(props.propertyPlaceholders) setHideLogDSL(props.hideLogDSL === true); return () => { sub?.unsubscribe(); diff --git a/karavan-designer/src/designer/property/DslProperties.css b/karavan-designer/src/designer/property/DslProperties.css index dd7cb199..43562bcb 100644 --- a/karavan-designer/src/designer/property/DslProperties.css +++ b/karavan-designer/src/designer/property/DslProperties.css @@ -248,10 +248,15 @@ min-height: 6px; } +.karavan .properties .header-menu-toggle { + padding-left: 6px; + padding-right: 0; +} + .karavan .properties .component-headers { margin-left: 24px; } .karavan .properties .component-headers .pf-v5-c-clipboard-copy.pf-m-inline { background-color: transparent; -} \ No newline at end of file +} diff --git a/karavan-designer/src/designer/property/DslProperties.tsx b/karavan-designer/src/designer/property/DslProperties.tsx index 6169d656..ed56d2eb 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, Label, Flex, LabelGroup, Popover, FlexItem, Badge, ClipboardCopy, ClipboardCopyAction, + DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -85,18 +85,16 @@ export function DslProperties(props: Props) { const showMenu = hasSteps || targetDsl !== undefined; return showMenu ? <Dropdown - style={{inset: "0px auto auto -70px important!"}} - className={"xxx"} + popperProps={{position: "end"}} isOpen={isMenuOpen} onSelect={() => { }} onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( <MenuToggle - style={{width: "240px", display: "flex", flexDirection: "row", justifyContent: "end"}} - className={"zzzz"} + className="header-menu-toggle" ref={toggleRef} - aria-label="kebab dropdown toggle" + aria-label="menu" variant="plain" onClick={() => setMenuOpen(!isMenuOpen)} isExpanded={isMenuOpen} diff --git a/karavan-designer/src/designer/property/property/ComponentPropertyField.tsx b/karavan-designer/src/designer/property/property/ComponentPropertyField.tsx index ae63e323..a25acafc 100644 --- a/karavan-designer/src/designer/property/property/ComponentPropertyField.tsx +++ b/karavan-designer/src/designer/property/property/ComponentPropertyField.tsx @@ -21,10 +21,13 @@ import { Popover, Switch, InputGroup, - TextArea, Tooltip, Button, - capitalize, InputGroupItem, TextInputGroup, TextVariants, Text + capitalize, + InputGroupItem, + TextInputGroup, + TextVariants, + Text, } from '@patternfly/react-core'; import { Select, @@ -51,6 +54,7 @@ import {shallow} from "zustand/shallow"; import {KubernetesIcon} from "../../icons/ComponentIcons"; import EditorIcon from "@patternfly/react-icons/dist/js/icons/code-icon"; import {ModalEditor} from "./ModalEditor"; +import {ComponentPropertyPlaceholderDropdown} from "./ComponentPropertyPlaceholderDropdown"; const prefix = "parameters"; const beanPrefix = "#bean:"; @@ -67,16 +71,18 @@ export function ComponentPropertyField(props: Props) { const {onParametersChange, getInternalComponentName} = usePropertiesHook(); const [integration] = useIntegrationStore((state) => [state.integration], shallow) - const [dark, setSelectedStep] = useDesignerStore((s) => [s.dark, s.setSelectedStep], shallow) + const [dark, setSelectedStep, propertyPlaceholders] = useDesignerStore((s) => + [s.dark, s.setSelectedStep, s.propertyPlaceholders], 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); + const [isOpenPlaceholdersDropdown, setOpenPlaceholdersDropdown] = useState<boolean>(false); function parametersChanged(parameter: string, value: string | number | boolean | any, pathParameter?: boolean, newRoute?: RouteToCreate) { @@ -262,20 +268,30 @@ export function ComponentPropertyField(props: Props) { </Button> </Tooltip> } + <InputGroupItem> + <ComponentPropertyPlaceholderDropdown property={property}/> + </InputGroupItem> </InputGroup> } - function getTextInput(property: ComponentProperty, value: any) { + function getSpecialStringInput(property: ComponentProperty, value: any) { return ( - <TextInput - className="text-field" isRequired - type={(property.secret ? "password" : "text")} - id={id} name={id} - value={value !== undefined ? value : property.defaultValue} - customIcon={<Text component={TextVariants.p}>{property.type}</Text>} - onChange={(_, value) => { - parametersChanged(property.name, value, property.kind === 'path') - }}/> + <InputGroup> + <InputGroupItem isFill> + <TextInput + className="text-field" isRequired + type={(property.secret ? "password" : "text")} + id={id} name={id} + value={value !== undefined ? value : property.defaultValue} + customIcon={<Text component={TextVariants.p}>{property.type}</Text>} + onChange={(_, value) => { + parametersChanged(property.name, value, property.kind === 'path') + }}/> + </InputGroupItem> + <InputGroupItem> + <ComponentPropertyPlaceholderDropdown property={property}/> + </InputGroupItem> + </InputGroup> ) } @@ -330,6 +346,9 @@ export function ComponentPropertyField(props: Props) { onChange={(_, v) => parametersChanged(property.name, v)} /> </InputGroupItem> + <InputGroupItem> + <ComponentPropertyPlaceholderDropdown property={property}/> + </InputGroupItem> </TextInputGroup> ) } @@ -362,7 +381,7 @@ export function ComponentPropertyField(props: Props) { {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)} + && getSpecialStringInput(property, value)} {['object'].includes(property.type) && !property.enum && getSelectBean(property, value)} {['string', 'object'].includes(property.type) && property.enum diff --git a/karavan-designer/src/designer/property/property/ComponentPropertyPlaceholderDropdown.css b/karavan-designer/src/designer/property/property/ComponentPropertyPlaceholderDropdown.css new file mode 100644 index 00000000..7d5d2d43 --- /dev/null +++ b/karavan-designer/src/designer/property/property/ComponentPropertyPlaceholderDropdown.css @@ -0,0 +1,24 @@ +/* + * 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. + */ + +.karavan .properties .property-placeholder-toggle { + padding-left: 6px; + padding-right: 6px; +} +.karavan .properties .property-placeholder-toggle .pf-v5-c-menu-toggle__controls { + display: none; +} \ No newline at end of file diff --git a/karavan-designer/src/designer/property/property/ComponentPropertyPlaceholderDropdown.tsx b/karavan-designer/src/designer/property/property/ComponentPropertyPlaceholderDropdown.tsx new file mode 100644 index 00000000..270f9549 --- /dev/null +++ b/karavan-designer/src/designer/property/property/ComponentPropertyPlaceholderDropdown.tsx @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, {useState} from 'react'; +import { + Dropdown, + MenuToggleElement, + MenuToggle, + DropdownList, DropdownItem +} from '@patternfly/react-core'; +import '../../karavan.css'; +import './ComponentPropertyPlaceholderDropdown.css'; +import "@patternfly/patternfly/patternfly.css"; +import {ComponentProperty} from "karavan-core/lib/model/ComponentModels"; +import {RouteToCreate} from "../../utils/CamelUi"; +import {usePropertiesHook} from "../usePropertiesHook"; +import {useDesignerStore} from "../../DesignerStore"; +import {shallow} from "zustand/shallow"; +import EllipsisVIcon from "@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon"; + + +interface Props { + property: ComponentProperty, +} + +export function ComponentPropertyPlaceholderDropdown(props: Props) { + + const {onParametersChange} = usePropertiesHook(); + const [propertyPlaceholders] = useDesignerStore((s) => [s.propertyPlaceholders], shallow) + const [isOpenPlaceholdersDropdown, setOpenPlaceholdersDropdown] = useState<boolean>(false); + + function parametersChanged(parameter: string, value: string | number | boolean | any, pathParameter?: boolean, newRoute?: RouteToCreate) { + onParametersChange(parameter, value, pathParameter, newRoute); + } + + const property: ComponentProperty = props.property; + return ( + propertyPlaceholders && propertyPlaceholders.length > 0 ? + <Dropdown + popperProps={{position: "end"}} + isOpen={isOpenPlaceholdersDropdown} + onSelect={(_, value) => { + parametersChanged(property.name, `{{${value}}}`, property.kind === 'path') + setOpenPlaceholdersDropdown(false); + }} + onOpenChange={(isOpen: boolean) => setOpenPlaceholdersDropdown(isOpen)} + toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( + <MenuToggle className="property-placeholder-toggle" + ref={toggleRef} + aria-label="placeholder menu" + variant="default" + onClick={() => setOpenPlaceholdersDropdown(!isOpenPlaceholdersDropdown)} + isExpanded={isOpenPlaceholdersDropdown} + > + <EllipsisVIcon/> + </MenuToggle> + )} + shouldFocusToggleOnSelect + > + <DropdownList> + {propertyPlaceholders.map((pp, index) => + <DropdownItem value={pp} key={index}>{pp}</DropdownItem> + )} + </DropdownList> + </Dropdown> + : <></> + ) +} diff --git a/karavan-designer/src/designer/property/property/DslPropertyField.tsx b/karavan-designer/src/designer/property/property/DslPropertyField.tsx index aa093618..59066667 100644 --- a/karavan-designer/src/designer/property/property/DslPropertyField.tsx +++ b/karavan-designer/src/designer/property/property/DslPropertyField.tsx @@ -74,6 +74,7 @@ import { import {TemplateApi} from "karavan-core/lib/api/TemplateApi"; import {KubernetesIcon} from "../../icons/ComponentIcons"; import {BeanProperties} from "./BeanProperties"; +import {ComponentPropertyPlaceholderDropdown} from "./ComponentPropertyPlaceholderDropdown"; interface Props { property: PropertyMeta, diff --git a/karavan-space/src/designer/DesignerStore.ts b/karavan-space/src/designer/DesignerStore.ts index 407713a7..fe198a7f 100644 --- a/karavan-space/src/designer/DesignerStore.ts +++ b/karavan-space/src/designer/DesignerStore.ts @@ -168,7 +168,8 @@ type DesignerState = { height: number, top: number, left: number, - moveElements: [string | undefined, string | undefined] + moveElements: [string | undefined, string | undefined], + propertyPlaceholders: string[]; } const designerState: DesignerState = { @@ -186,7 +187,8 @@ const designerState: DesignerState = { height: 0, top: 0, left: 0, - moveElements: [undefined, undefined] + moveElements: [undefined, undefined], + propertyPlaceholders: [] }; type DesignerAction = { @@ -203,6 +205,7 @@ type DesignerAction = { reset: () => void; setNotification: (notificationBadge: boolean, notificationMessage: [string, string]) => void; setMoveElements: (moveElements: [string | undefined, string | undefined]) => void; + setPropertyPlaceholders: (propertyPlaceholders: string[]) => void; } export const useDesignerStore = createWithEqualityFn<DesignerState & DesignerAction>((set) => ({ @@ -257,5 +260,12 @@ export const useDesignerStore = createWithEqualityFn<DesignerState & DesignerAct }, setMoveElements: (moveElements: [string | undefined, string | undefined]) => { set({moveElements: moveElements}) - } + }, + setPropertyPlaceholders: (propertyPlaceholders: string[]) => { + set((state: DesignerState) => { + state.propertyPlaceholders.length = 0; + state.propertyPlaceholders.push(...propertyPlaceholders); + return state; + }) + }, }), shallow) \ No newline at end of file diff --git a/karavan-space/src/designer/KaravanDesigner.tsx b/karavan-space/src/designer/KaravanDesigner.tsx index 626f984b..349ae548 100644 --- a/karavan-space/src/designer/KaravanDesigner.tsx +++ b/karavan-space/src/designer/KaravanDesigner.tsx @@ -41,7 +41,6 @@ import {BeansDesigner} from "./beans/BeansDesigner"; import {CodeEditor} from "./editor/CodeEditor"; import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; import {KameletDesigner} from "./kamelet/KameletDesigner"; -import {v4 as uuidv4} from "uuid"; interface Props { onSave: (filename: string, yaml: string, propertyOnly: boolean) => void @@ -53,13 +52,15 @@ interface Props { hideLogDSL?: boolean showCodeTab: boolean tab?: "routes" | "rest" | "beans" + propertyPlaceholders: string[] } export function KaravanDesigner(props: Props) { const [tab, setTab] = useState<string>('routes'); - const [setDark, hideLogDSL, setHideLogDSL, setSelectedStep, reset, badge, message] = useDesignerStore((s) => - [s.setDark, s.hideLogDSL, s.setHideLogDSL, s.setSelectedStep, s.reset, s.notificationBadge, s.notificationMessage], shallow) + const [setDark, hideLogDSL, setHideLogDSL, setSelectedStep, reset, badge, message, setPropertyPlaceholders] = + useDesignerStore((s) => + [s.setDark, s.hideLogDSL, s.setHideLogDSL, s.setSelectedStep, s.reset, s.notificationBadge, s.notificationMessage, s.setPropertyPlaceholders], shallow) const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) @@ -83,6 +84,7 @@ export function KaravanDesigner(props: Props) { setTab(designerTab || 'routes') reset(); setDark(props.dark); + setPropertyPlaceholders(props.propertyPlaceholders) setHideLogDSL(props.hideLogDSL === true); return () => { sub?.unsubscribe(); diff --git a/karavan-space/src/designer/property/DslProperties.css b/karavan-space/src/designer/property/DslProperties.css index c681098c..43562bcb 100644 --- a/karavan-space/src/designer/property/DslProperties.css +++ b/karavan-space/src/designer/property/DslProperties.css @@ -248,3 +248,15 @@ min-height: 6px; } +.karavan .properties .header-menu-toggle { + padding-left: 6px; + padding-right: 0; +} + +.karavan .properties .component-headers { + margin-left: 24px; +} + +.karavan .properties .component-headers .pf-v5-c-clipboard-copy.pf-m-inline { + background-color: transparent; +} diff --git a/karavan-space/src/designer/property/DslProperties.tsx b/karavan-space/src/designer/property/DslProperties.tsx index 9325ee05..ed56d2eb 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, Label, Flex, LabelGroup, Popover, FlexItem, Badge, + DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -85,18 +85,16 @@ export function DslProperties(props: Props) { const showMenu = hasSteps || targetDsl !== undefined; return showMenu ? <Dropdown - style={{inset: "0px auto auto -70px important!"}} - className={"xxx"} + popperProps={{position: "end"}} isOpen={isMenuOpen} onSelect={() => { }} onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( <MenuToggle - style={{width: "240px", display: "flex", flexDirection: "row", justifyContent: "end"}} - className={"zzzz"} + className="header-menu-toggle" ref={toggleRef} - aria-label="kebab dropdown toggle" + aria-label="menu" variant="plain" onClick={() => setMenuOpen(!isMenuOpen)} isExpanded={isMenuOpen} @@ -166,11 +164,13 @@ export function DslProperties(props: Props) { <ExpandableSection toggleText='Headers' onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} isExpanded={isDescriptionExpanded}> - <Flex direction={{default:"column"}}> + <Flex className='component-headers' 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> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" isCode> + {header.name} + </ClipboardCopy> <FlexItem align={{default: 'alignRight'}}> <Popover position={"left"} diff --git a/karavan-space/src/designer/property/property/ComponentPropertyField.tsx b/karavan-space/src/designer/property/property/ComponentPropertyField.tsx index b98b411d..a25acafc 100644 --- a/karavan-space/src/designer/property/property/ComponentPropertyField.tsx +++ b/karavan-space/src/designer/property/property/ComponentPropertyField.tsx @@ -21,10 +21,13 @@ import { Popover, Switch, InputGroup, - TextArea, Tooltip, Button, - capitalize, InputGroupItem + capitalize, + InputGroupItem, + TextInputGroup, + TextVariants, + Text, } from '@patternfly/react-core'; import { Select, @@ -39,8 +42,6 @@ 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"; @@ -48,9 +49,12 @@ 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 {useDesignerStore, useIntegrationStore} from "../../DesignerStore"; import {shallow} from "zustand/shallow"; import {KubernetesIcon} from "../../icons/ComponentIcons"; +import EditorIcon from "@patternfly/react-icons/dist/js/icons/code-icon"; +import {ModalEditor} from "./ModalEditor"; +import {ComponentPropertyPlaceholderDropdown} from "./ComponentPropertyPlaceholderDropdown"; const prefix = "parameters"; const beanPrefix = "#bean:"; @@ -67,15 +71,18 @@ export function ComponentPropertyField(props: Props) { const {onParametersChange, getInternalComponentName} = usePropertiesHook(); const [integration] = useIntegrationStore((state) => [state.integration], shallow) + const [dark, setSelectedStep, propertyPlaceholders] = useDesignerStore((s) => + [s.dark, s.setSelectedStep, s.propertyPlaceholders], 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); + const [isOpenPlaceholdersDropdown, setOpenPlaceholdersDropdown] = useState<boolean>(false); function parametersChanged(parameter: string, value: string | number | boolean | any, pathParameter?: boolean, newRoute?: RouteToCreate) { @@ -170,7 +177,7 @@ export function ComponentPropertyField(props: Props) { <Button isDisabled={value === undefined} variant="control" onClick={e => { if (value) { const newRoute = !internalUris.includes(value.toString()) - ? CamelUi.createNewInternalRoute(componentName.concat(...':',value.toString())) + ? CamelUi.createNewInternalRoute(componentName.concat(...':', value.toString())) : undefined; parametersChanged(property.name, value, property.kind === 'path', newRoute); } @@ -234,20 +241,26 @@ export function ComponentPropertyField(props: Props) { 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"}> + <InputGroupItem> + <Tooltip position="bottom-end" content={"Show Editor"}> <Button variant="control" onClick={e => setShowEditor(!showEditor)}> - {showEditor ? <CompressIcon/> : <ExpandIcon/>} + <EditorIcon/> </Button> </Tooltip> - } + </InputGroupItem> + {showEditor && <InputGroupItem> + <ModalEditor name={property.name} + customCode={value} + showEditor={showEditor} + dark={dark} + dslLanguage={undefined} + title={property.displayName} + onClose={() => setShowEditor(false)} + onSave={(fieldId, value1) => { + parametersChanged(property.name, value1, property.kind === 'path') + setShowEditor(false); + }}/> + </InputGroupItem>} {property.secret && <Tooltip position="bottom-end" content={showPassword ? "Hide" : "Show"}> <Button variant="control" onClick={e => setShowPassword(!showPassword)}> @@ -255,19 +268,30 @@ export function ComponentPropertyField(props: Props) { </Button> </Tooltip> } + <InputGroupItem> + <ComponentPropertyPlaceholderDropdown property={property}/> + </InputGroupItem> </InputGroup> } - function getTextInput(property: ComponentProperty, value: any) { + function getSpecialStringInput(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') - }}/> + <InputGroup> + <InputGroupItem isFill> + <TextInput + className="text-field" isRequired + type={(property.secret ? "password" : "text")} + id={id} name={id} + value={value !== undefined ? value : property.defaultValue} + customIcon={<Text component={TextVariants.p}>{property.type}</Text>} + onChange={(_, value) => { + parametersChanged(property.name, value, property.kind === 'path') + }}/> + </InputGroupItem> + <InputGroupItem> + <ComponentPropertyPlaceholderDropdown property={property}/> + </InputGroupItem> + </InputGroup> ) } @@ -297,14 +321,35 @@ export function ComponentPropertyField(props: Props) { } function getSwitch(property: ComponentProperty, value: any) { + const isValueBoolean = (value === true || value === false); + const isDisabled = value !== undefined && !isValueBoolean; 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)}/> + <TextInputGroup className="input-group"> + <InputGroupItem> + <Switch + id={id} name={id} + value={value?.toString()} + isDisabled={isDisabled} + className="switch-placeholder" + aria-label={id} + isChecked={isChecked} + onChange={(e, checked) => parametersChanged(property.name, checked)}/> + </InputGroupItem> + <InputGroupItem isFill> + <TextInput + id={property.name + "-placeholder"} + name={property.name + "-placeholder"} + type="text" + aria-label="placeholder" + value={!isValueBoolean ? value?.toString() : undefined} + onChange={(_, v) => parametersChanged(property.name, v)} + /> + </InputGroupItem> + <InputGroupItem> + <ComponentPropertyPlaceholderDropdown property={property}/> + </InputGroupItem> + </TextInputGroup> ) } @@ -336,7 +381,7 @@ export function ComponentPropertyField(props: Props) { {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)} + && getSpecialStringInput(property, value)} {['object'].includes(property.type) && !property.enum && getSelectBean(property, value)} {['string', 'object'].includes(property.type) && property.enum diff --git a/karavan-space/src/designer/property/property/ComponentPropertyPlaceholderDropdown.css b/karavan-space/src/designer/property/property/ComponentPropertyPlaceholderDropdown.css new file mode 100644 index 00000000..7d5d2d43 --- /dev/null +++ b/karavan-space/src/designer/property/property/ComponentPropertyPlaceholderDropdown.css @@ -0,0 +1,24 @@ +/* + * 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. + */ + +.karavan .properties .property-placeholder-toggle { + padding-left: 6px; + padding-right: 6px; +} +.karavan .properties .property-placeholder-toggle .pf-v5-c-menu-toggle__controls { + display: none; +} \ No newline at end of file diff --git a/karavan-space/src/designer/property/property/ComponentPropertyPlaceholderDropdown.tsx b/karavan-space/src/designer/property/property/ComponentPropertyPlaceholderDropdown.tsx new file mode 100644 index 00000000..270f9549 --- /dev/null +++ b/karavan-space/src/designer/property/property/ComponentPropertyPlaceholderDropdown.tsx @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, {useState} from 'react'; +import { + Dropdown, + MenuToggleElement, + MenuToggle, + DropdownList, DropdownItem +} from '@patternfly/react-core'; +import '../../karavan.css'; +import './ComponentPropertyPlaceholderDropdown.css'; +import "@patternfly/patternfly/patternfly.css"; +import {ComponentProperty} from "karavan-core/lib/model/ComponentModels"; +import {RouteToCreate} from "../../utils/CamelUi"; +import {usePropertiesHook} from "../usePropertiesHook"; +import {useDesignerStore} from "../../DesignerStore"; +import {shallow} from "zustand/shallow"; +import EllipsisVIcon from "@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon"; + + +interface Props { + property: ComponentProperty, +} + +export function ComponentPropertyPlaceholderDropdown(props: Props) { + + const {onParametersChange} = usePropertiesHook(); + const [propertyPlaceholders] = useDesignerStore((s) => [s.propertyPlaceholders], shallow) + const [isOpenPlaceholdersDropdown, setOpenPlaceholdersDropdown] = useState<boolean>(false); + + function parametersChanged(parameter: string, value: string | number | boolean | any, pathParameter?: boolean, newRoute?: RouteToCreate) { + onParametersChange(parameter, value, pathParameter, newRoute); + } + + const property: ComponentProperty = props.property; + return ( + propertyPlaceholders && propertyPlaceholders.length > 0 ? + <Dropdown + popperProps={{position: "end"}} + isOpen={isOpenPlaceholdersDropdown} + onSelect={(_, value) => { + parametersChanged(property.name, `{{${value}}}`, property.kind === 'path') + setOpenPlaceholdersDropdown(false); + }} + onOpenChange={(isOpen: boolean) => setOpenPlaceholdersDropdown(isOpen)} + toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( + <MenuToggle className="property-placeholder-toggle" + ref={toggleRef} + aria-label="placeholder menu" + variant="default" + onClick={() => setOpenPlaceholdersDropdown(!isOpenPlaceholdersDropdown)} + isExpanded={isOpenPlaceholdersDropdown} + > + <EllipsisVIcon/> + </MenuToggle> + )} + shouldFocusToggleOnSelect + > + <DropdownList> + {propertyPlaceholders.map((pp, index) => + <DropdownItem value={pp} key={index}>{pp}</DropdownItem> + )} + </DropdownList> + </Dropdown> + : <></> + ) +} diff --git a/karavan-space/src/designer/property/property/DslPropertyField.tsx b/karavan-space/src/designer/property/property/DslPropertyField.tsx index b01f6e7c..59066667 100644 --- a/karavan-space/src/designer/property/property/DslPropertyField.tsx +++ b/karavan-space/src/designer/property/property/DslPropertyField.tsx @@ -32,7 +32,7 @@ import { Tooltip, Card, InputGroup, - capitalize, InputGroupItem + capitalize, InputGroupItem, TextVariants } from '@patternfly/react-core'; import { Select, @@ -59,8 +59,6 @@ import {CamelDefinitionApi} from "karavan-core/lib/api/CamelDefinitionApi"; import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; import {MediaTypes} from "../../utils/MediaTypes"; import {ComponentProperty} from "karavan-core/lib/model/ComponentModels"; -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 EditorIcon from "@patternfly/react-icons/dist/js/icons/code-icon"; @@ -76,6 +74,7 @@ import { import {TemplateApi} from "karavan-core/lib/api/TemplateApi"; import {KubernetesIcon} from "../../icons/ComponentIcons"; import {BeanProperties} from "./BeanProperties"; +import {ComponentPropertyPlaceholderDropdown} from "./ComponentPropertyPlaceholderDropdown"; interface Props { property: PropertyMeta, @@ -229,6 +228,9 @@ export function DslPropertyField(props: Props) { const inInfrastructure = InfrastructureAPI.infrastructure !== 'local'; const noInfraSelectorButton = ["uri", "id", "description", "group"].includes(property.name); const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? KubernetesIcon("infra-button") : <DockerIcon/> + const isNumber = ['integer', 'number', 'duration'].includes(property.type); + const uriReadOnly = isUriReadOnly(property); + const showEditorButton = !uriReadOnly && !isNumber && !property.secret && !['id', 'description'].includes(property.name); return (<InputGroup> {inInfrastructure && !showEditor && !noInfraSelectorButton && <InputGroupItem> @@ -244,34 +246,35 @@ export function DslPropertyField(props: Props) { <InputGroupItem isFill> <TextInput ref={ref} className="text-field" isRequired - type={['integer', 'number'].includes(property.type) ? 'number' : (property.secret ? "password" : "text")} + type={(property.secret ? "password" : "text")} id={property.name} name={property.name} value={value?.toString()} - onChange={(_, v) => propertyChanged(property.name, ['integer', 'number'].includes(property.type) ? Number(v) : v)} - readOnlyVariant={isUriReadOnly(property) ? "default" : undefined}/> - </InputGroupItem> - } - {showEditor && !property.secret && - <InputGroupItem isFill> - <TextArea ref={ref} - autoResize={true} - className="text-field" isRequired - type="text" - id={property.name} name={property.name} - value={value?.toString()} - onChange={(_, v) => propertyChanged(property.name, ['integer', 'number'].includes(property.type) ? Number(v) : v)} - readOnlyVariant={isUriReadOnly(property) ? "default" : undefined}/> - </InputGroupItem> - } - {!property.secret && - <InputGroupItem> - <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> + customIcon={property.type !== 'string' ? <Text component={TextVariants.p}>{property.type}</Text> : undefined} + onChange={(_, v) => + propertyChanged(property.name, v)} + readOnlyVariant={uriReadOnly? "default" : undefined}/> </InputGroupItem> } + {showEditorButton && <InputGroupItem> + <Tooltip position="bottom-end" content={"Show Editor"}> + <Button variant="control" onClick={e => setShowEditor(!showEditor)}> + <EditorIcon/> + </Button> + </Tooltip> + </InputGroupItem>} + {showEditor && <InputGroupItem> + <ModalEditor name={property.name} + customCode={value} + showEditor={showEditor} + dark={dark} + dslLanguage={undefined} + title={property.displayName} + onClose={() => setShowEditor(false)} + onSave={(fieldId, value1) => { + propertyChanged(property.name, value1) + setShowEditor(false); + }}/> + </InputGroupItem>} </InputGroup>) } @@ -313,7 +316,7 @@ export function DslPropertyField(props: Props) { </Tooltip> </InputGroupItem> {showEditor && <InputGroupItem> - <ModalEditor property={property} + <ModalEditor name={property.name} customCode={customCode} showEditor={showEditor} dark={dark} @@ -351,7 +354,7 @@ export function DslPropertyField(props: Props) { </Tooltip> </InputGroupItem> {showEditor && <InputGroupItem> - <ModalEditor property={property} + <ModalEditor name={property.name} customCode={value} showEditor={showEditor} dark={dark} @@ -388,7 +391,7 @@ export function DslPropertyField(props: Props) { ) } - function getSwitch(property: PropertyMeta, value: any) { + function getBooleanInput(property: PropertyMeta, value: any) { const isValueBoolean = (value === true || value === false); const isDisabled = value !== undefined && !isValueBoolean; let isChecked = false; @@ -410,17 +413,16 @@ export function DslPropertyField(props: Props) { isChecked={isChecked} onChange={(_, v) => propertyChanged(property.name, v)}/> </InputGroupItem> - {property.placeholder && <InputGroupItem isFill> + <InputGroupItem isFill> <TextInput id={property.name + "-placeholder"} name={property.name + "-placeholder"} type="text" aria-label="placeholder" - placeholder="Property placeholder" value={!isValueBoolean ? value?.toString() : undefined} onChange={(_, v) => propertyChanged(property.name, v)} /> - </InputGroupItem>} + </InputGroupItem> </TextInputGroup> ) } @@ -831,7 +833,7 @@ export function DslPropertyField(props: Props) { {isMultiValueField(property) && getMultiValueField(property, value)} {property.type === 'boolean' - && getSwitch(property, value)} + && getBooleanInput(property, value)} {property.enumVals && getSelect(property, value)} {isKamelet && property.name === 'parameters' && getKameletParameters()} diff --git a/karavan-space/src/designer/property/property/ModalEditor.tsx b/karavan-space/src/designer/property/property/ModalEditor.tsx index 3b18975b..d3df2a27 100644 --- a/karavan-space/src/designer/property/property/ModalEditor.tsx +++ b/karavan-space/src/designer/property/property/ModalEditor.tsx @@ -22,11 +22,10 @@ import { } from '@patternfly/react-core'; import '../../karavan.css'; import "@patternfly/patternfly/patternfly.css"; -import {PropertyMeta} from "karavan-core/lib/model/CamelMetadata"; import Editor from "@monaco-editor/react"; interface Props { - property: PropertyMeta, + name: string, customCode: any, onSave: (fieldId: string, value: string | number | boolean | any) => void, onClose: () => void, @@ -49,7 +48,7 @@ export function ModalEditor(props: Props) { } function closeAndSave(){ - props.onSave(props.property.name, customCode); + props.onSave(props.name, customCode); } const {dark, dslLanguage, title, showEditor} = props; diff --git a/karavan-space/src/space/SpacePage.tsx b/karavan-space/src/space/SpacePage.tsx index 3dea0d2a..e878383a 100644 --- a/karavan-space/src/space/SpacePage.tsx +++ b/karavan-space/src/space/SpacePage.tsx @@ -108,6 +108,7 @@ export class SpacePage extends React.Component<Props, State> { onSaveCustomCode={(name1, code) => { console.log(name1, code) }} + propertyPlaceholders={[]} /> ) } diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/property/DslProperties.css b/karavan-web/karavan-app/src/main/webui/src/designer/property/DslProperties.css index c681098c..dd7cb199 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/property/DslProperties.css +++ b/karavan-web/karavan-app/src/main/webui/src/designer/property/DslProperties.css @@ -248,3 +248,10 @@ min-height: 6px; } +.karavan .properties .component-headers { + margin-left: 24px; +} + +.karavan .properties .component-headers .pf-v5-c-clipboard-copy.pf-m-inline { + background-color: transparent; +} \ No newline at end of file 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 9325ee05..6169d656 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, Label, Flex, LabelGroup, Popover, FlexItem, Badge, + DropdownItem, Label, Flex, LabelGroup, Popover, FlexItem, Badge, ClipboardCopy, ClipboardCopyAction, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -166,11 +166,13 @@ export function DslProperties(props: Props) { <ExpandableSection toggleText='Headers' onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} isExpanded={isDescriptionExpanded}> - <Flex direction={{default:"column"}}> + <Flex className='component-headers' 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> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" isCode> + {header.name} + </ClipboardCopy> <FlexItem align={{default: 'alignRight'}}> <Popover position={"left"} 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 index b98b411d..ae63e323 100644 --- 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 @@ -24,7 +24,7 @@ import { TextArea, Tooltip, Button, - capitalize, InputGroupItem + capitalize, InputGroupItem, TextInputGroup, TextVariants, Text } from '@patternfly/react-core'; import { Select, @@ -39,8 +39,6 @@ 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"; @@ -48,9 +46,11 @@ 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 {useDesignerStore, useIntegrationStore} from "../../DesignerStore"; import {shallow} from "zustand/shallow"; import {KubernetesIcon} from "../../icons/ComponentIcons"; +import EditorIcon from "@patternfly/react-icons/dist/js/icons/code-icon"; +import {ModalEditor} from "./ModalEditor"; const prefix = "parameters"; const beanPrefix = "#bean:"; @@ -67,6 +67,7 @@ export function ComponentPropertyField(props: Props) { const {onParametersChange, getInternalComponentName} = usePropertiesHook(); const [integration] = useIntegrationStore((state) => [state.integration], shallow) + const [dark, setSelectedStep] = useDesignerStore((s) => [s.dark, s.setSelectedStep], shallow) const [selectStatus, setSelectStatus] = useState<Map<string, boolean>>(new Map<string, boolean>()); const [showEditor, setShowEditor] = useState<boolean>(false); @@ -170,7 +171,7 @@ export function ComponentPropertyField(props: Props) { <Button isDisabled={value === undefined} variant="control" onClick={e => { if (value) { const newRoute = !internalUris.includes(value.toString()) - ? CamelUi.createNewInternalRoute(componentName.concat(...':',value.toString())) + ? CamelUi.createNewInternalRoute(componentName.concat(...':', value.toString())) : undefined; parametersChanged(property.name, value, property.kind === 'path', newRoute); } @@ -234,20 +235,26 @@ export function ComponentPropertyField(props: Props) { 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"}> + <InputGroupItem> + <Tooltip position="bottom-end" content={"Show Editor"}> <Button variant="control" onClick={e => setShowEditor(!showEditor)}> - {showEditor ? <CompressIcon/> : <ExpandIcon/>} + <EditorIcon/> </Button> </Tooltip> - } + </InputGroupItem> + {showEditor && <InputGroupItem> + <ModalEditor name={property.name} + customCode={value} + showEditor={showEditor} + dark={dark} + dslLanguage={undefined} + title={property.displayName} + onClose={() => setShowEditor(false)} + onSave={(fieldId, value1) => { + parametersChanged(property.name, value1, property.kind === 'path') + setShowEditor(false); + }}/> + </InputGroupItem>} {property.secret && <Tooltip position="bottom-end" content={showPassword ? "Hide" : "Show"}> <Button variant="control" onClick={e => setShowPassword(!showPassword)}> @@ -262,11 +269,12 @@ export function ComponentPropertyField(props: Props) { return ( <TextInput className="text-field" isRequired - type={['integer', 'int', 'number'].includes(property.type) ? 'number' : (property.secret ? "password" : "text")} + type={(property.secret ? "password" : "text")} id={id} name={id} value={value !== undefined ? value : property.defaultValue} + customIcon={<Text component={TextVariants.p}>{property.type}</Text>} onChange={(_, value) => { - parametersChanged(property.name, ['integer', 'int', 'number'].includes(property.type) ? Number(value) : value, property.kind === 'path') + parametersChanged(property.name, value, property.kind === 'path') }}/> ) } @@ -297,14 +305,32 @@ export function ComponentPropertyField(props: Props) { } function getSwitch(property: ComponentProperty, value: any) { + const isValueBoolean = (value === true || value === false); + const isDisabled = value !== undefined && !isValueBoolean; 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)}/> + <TextInputGroup className="input-group"> + <InputGroupItem> + <Switch + id={id} name={id} + value={value?.toString()} + isDisabled={isDisabled} + className="switch-placeholder" + aria-label={id} + isChecked={isChecked} + onChange={(e, checked) => parametersChanged(property.name, checked)}/> + </InputGroupItem> + <InputGroupItem isFill> + <TextInput + id={property.name + "-placeholder"} + name={property.name + "-placeholder"} + type="text" + aria-label="placeholder" + value={!isValueBoolean ? value?.toString() : undefined} + onChange={(_, v) => parametersChanged(property.name, v)} + /> + </InputGroupItem> + </TextInputGroup> ) } 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 b01f6e7c..aa093618 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 @@ -32,7 +32,7 @@ import { Tooltip, Card, InputGroup, - capitalize, InputGroupItem + capitalize, InputGroupItem, TextVariants } from '@patternfly/react-core'; import { Select, @@ -59,8 +59,6 @@ import {CamelDefinitionApi} from "karavan-core/lib/api/CamelDefinitionApi"; import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; import {MediaTypes} from "../../utils/MediaTypes"; import {ComponentProperty} from "karavan-core/lib/model/ComponentModels"; -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 EditorIcon from "@patternfly/react-icons/dist/js/icons/code-icon"; @@ -229,6 +227,9 @@ export function DslPropertyField(props: Props) { const inInfrastructure = InfrastructureAPI.infrastructure !== 'local'; const noInfraSelectorButton = ["uri", "id", "description", "group"].includes(property.name); const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? KubernetesIcon("infra-button") : <DockerIcon/> + const isNumber = ['integer', 'number', 'duration'].includes(property.type); + const uriReadOnly = isUriReadOnly(property); + const showEditorButton = !uriReadOnly && !isNumber && !property.secret && !['id', 'description'].includes(property.name); return (<InputGroup> {inInfrastructure && !showEditor && !noInfraSelectorButton && <InputGroupItem> @@ -244,34 +245,35 @@ export function DslPropertyField(props: Props) { <InputGroupItem isFill> <TextInput ref={ref} className="text-field" isRequired - type={['integer', 'number'].includes(property.type) ? 'number' : (property.secret ? "password" : "text")} + type={(property.secret ? "password" : "text")} id={property.name} name={property.name} value={value?.toString()} - onChange={(_, v) => propertyChanged(property.name, ['integer', 'number'].includes(property.type) ? Number(v) : v)} - readOnlyVariant={isUriReadOnly(property) ? "default" : undefined}/> - </InputGroupItem> - } - {showEditor && !property.secret && - <InputGroupItem isFill> - <TextArea ref={ref} - autoResize={true} - className="text-field" isRequired - type="text" - id={property.name} name={property.name} - value={value?.toString()} - onChange={(_, v) => propertyChanged(property.name, ['integer', 'number'].includes(property.type) ? Number(v) : v)} - readOnlyVariant={isUriReadOnly(property) ? "default" : undefined}/> - </InputGroupItem> - } - {!property.secret && - <InputGroupItem> - <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> + customIcon={property.type !== 'string' ? <Text component={TextVariants.p}>{property.type}</Text> : undefined} + onChange={(_, v) => + propertyChanged(property.name, v)} + readOnlyVariant={uriReadOnly? "default" : undefined}/> </InputGroupItem> } + {showEditorButton && <InputGroupItem> + <Tooltip position="bottom-end" content={"Show Editor"}> + <Button variant="control" onClick={e => setShowEditor(!showEditor)}> + <EditorIcon/> + </Button> + </Tooltip> + </InputGroupItem>} + {showEditor && <InputGroupItem> + <ModalEditor name={property.name} + customCode={value} + showEditor={showEditor} + dark={dark} + dslLanguage={undefined} + title={property.displayName} + onClose={() => setShowEditor(false)} + onSave={(fieldId, value1) => { + propertyChanged(property.name, value1) + setShowEditor(false); + }}/> + </InputGroupItem>} </InputGroup>) } @@ -313,7 +315,7 @@ export function DslPropertyField(props: Props) { </Tooltip> </InputGroupItem> {showEditor && <InputGroupItem> - <ModalEditor property={property} + <ModalEditor name={property.name} customCode={customCode} showEditor={showEditor} dark={dark} @@ -351,7 +353,7 @@ export function DslPropertyField(props: Props) { </Tooltip> </InputGroupItem> {showEditor && <InputGroupItem> - <ModalEditor property={property} + <ModalEditor name={property.name} customCode={value} showEditor={showEditor} dark={dark} @@ -388,7 +390,7 @@ export function DslPropertyField(props: Props) { ) } - function getSwitch(property: PropertyMeta, value: any) { + function getBooleanInput(property: PropertyMeta, value: any) { const isValueBoolean = (value === true || value === false); const isDisabled = value !== undefined && !isValueBoolean; let isChecked = false; @@ -410,17 +412,16 @@ export function DslPropertyField(props: Props) { isChecked={isChecked} onChange={(_, v) => propertyChanged(property.name, v)}/> </InputGroupItem> - {property.placeholder && <InputGroupItem isFill> + <InputGroupItem isFill> <TextInput id={property.name + "-placeholder"} name={property.name + "-placeholder"} type="text" aria-label="placeholder" - placeholder="Property placeholder" value={!isValueBoolean ? value?.toString() : undefined} onChange={(_, v) => propertyChanged(property.name, v)} /> - </InputGroupItem>} + </InputGroupItem> </TextInputGroup> ) } @@ -831,7 +832,7 @@ export function DslPropertyField(props: Props) { {isMultiValueField(property) && getMultiValueField(property, value)} {property.type === 'boolean' - && getSwitch(property, value)} + && getBooleanInput(property, value)} {property.enumVals && getSelect(property, value)} {isKamelet && property.name === 'parameters' && getKameletParameters()} diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/property/property/ModalEditor.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/property/property/ModalEditor.tsx index 3b18975b..d3df2a27 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/property/property/ModalEditor.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/property/property/ModalEditor.tsx @@ -22,11 +22,10 @@ import { } from '@patternfly/react-core'; import '../../karavan.css'; import "@patternfly/patternfly/patternfly.css"; -import {PropertyMeta} from "karavan-core/lib/model/CamelMetadata"; import Editor from "@monaco-editor/react"; interface Props { - property: PropertyMeta, + name: string, customCode: any, onSave: (fieldId: string, value: string | number | boolean | any) => void, onClose: () => void, @@ -49,7 +48,7 @@ export function ModalEditor(props: Props) { } function closeAndSave(){ - props.onSave(props.property.name, customCode); + props.onSave(props.name, customCode); } const {dark, dslLanguage, title, showEditor} = props;