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 869f26a528434b821679054134a1c55b6f833474 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Thu Apr 11 11:10:45 2024 -0400 Fix #1133 --- .../webui/src/designer/property/DslProperties.tsx | 144 +------------ .../src/designer/property/PropertiesHeader.tsx | 239 +++++++++++++++++++++ .../webui/src/designer/utils/IntegrationHeader.tsx | 16 +- .../webui/src/expression/ExpressionModalEditor.css | 9 +- .../webui/src/expression/ExpressionModalEditor.tsx | 25 ++- karavan-core/src/core/model/CamelMetadata.ts | 4 + karavan-designer/public/example/demo.camel.yaml | 201 +---------------- .../src/designer/property/DslProperties.tsx | 144 +------------ .../src/designer/property/PropertiesHeader.tsx | 239 +++++++++++++++++++++ .../src/designer/utils/IntegrationHeader.tsx | 16 +- .../src/main/resources/CamelMetadata.header.ts | 4 + .../src/designer/property/DslProperties.tsx | 144 +------------ .../src/designer/property/PropertiesHeader.tsx | 239 +++++++++++++++++++++ .../src/designer/utils/IntegrationHeader.tsx | 16 +- .../src/expression/ExpressionModalEditor.css | 9 +- .../src/expression/ExpressionModalEditor.tsx | 25 ++- 16 files changed, 791 insertions(+), 683 deletions(-) diff --git a/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx b/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx index 656d816c..7448351c 100644 --- a/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx +++ b/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import { Form, Text, @@ -23,11 +23,6 @@ import { ExpandableSection, Button, Tooltip, - Dropdown, - MenuToggleElement, - MenuToggle, - DropdownList, - DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -43,9 +38,7 @@ import {useDesignerStore, useIntegrationStore} from "../DesignerStore"; 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 {ComponentApi} from "karavan-core/lib/api/ComponentApi"; -import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {PropertiesHeader} from "./PropertiesHeader"; interface Props { designerType: 'routes' | 'rest' | 'beans' @@ -56,8 +49,6 @@ export function DslProperties(props: Props) { const [integration] = useIntegrationStore((s) => [s.integration], shallow) const { - saveAsRoute, - convertStep, cloneElement, onDataFormatChange, onPropertyChange, @@ -70,135 +61,6 @@ export function DslProperties(props: Props) { = useDesignerStore((s) => [s.selectedStep, s.dark], shallow) const [showAdvanced, setShowAdvanced] = useState<boolean>(false); - const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); - const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - - useEffect(() => { - setMenuOpen(false) - }, [selectedStep]) - - function getHeaderMenu(): JSX.Element { - const hasSteps = selectedStep?.hasSteps(); - const targetDsl = CamelUi.getConvertTargetDsl(selectedStep?.dslName); - const targetDslTitle = targetDsl?.replace("Definition", ""); - const showMenu = hasSteps || targetDsl !== undefined; - return showMenu ? - <Dropdown - popperProps={{position: "end"}} - isOpen={isMenuOpen} - onSelect={() => { - }} - onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} - toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( - <MenuToggle - className="header-menu-toggle" - ref={toggleRef} - aria-label="menu" - variant="plain" - onClick={() => setMenuOpen(!isMenuOpen)} - isExpanded={isMenuOpen} - > - <EllipsisVIcon/> - </MenuToggle> - )} - > - <DropdownList> - {hasSteps && - <DropdownItem key="saveRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, true); - setMenuOpen(false); - } - }}> - Save Steps to Route - </DropdownItem>} - {hasSteps && - <DropdownItem key="saveRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, false); - setMenuOpen(false); - } - }}> - Save Element to Route - </DropdownItem>} - {targetDsl && - <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} - </DropdownList> - </Dropdown> : <></>; - } - - function getRouteHeader(): JSX.Element { - 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"> - <Title headingLevel="h1" size="md">{title}</Title> - {getHeaderMenu()} - </div> - <Text component={TextVariants.p}>{descriptionLines.at(0)}</Text> - {descriptionLines.length > 1 && - <ExpandableSection toggleText={isDescriptionExpanded ? 'Show less' : 'Show more'} - onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} - isExpanded={isDescriptionExpanded}> - {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 className='component-headers' direction={{default:"column"}}> - {headers.filter((header) => groups.includes(header.group)) - .map((header, index, array) => - <Flex key={index}> - <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" isCode> - {header.name} - </ClipboardCopy> - <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> - ) - } function getClonableElementHeader(): JSX.Element { const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep); @@ -219,7 +81,7 @@ export function DslProperties(props: Props) { } function getComponentHeader(): JSX.Element { - if (props.designerType === 'routes') return getRouteHeader() + if (props.designerType === 'routes') return <PropertiesHeader designerType={props.designerType} /> else return getClonableElementHeader(); } diff --git a/karavan-app/src/main/webui/src/designer/property/PropertiesHeader.tsx b/karavan-app/src/main/webui/src/designer/property/PropertiesHeader.tsx new file mode 100644 index 00000000..9a38fa13 --- /dev/null +++ b/karavan-app/src/main/webui/src/designer/property/PropertiesHeader.tsx @@ -0,0 +1,239 @@ +/* + * 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, {useEffect, useState} from 'react'; +import { + Text, + Title, + TextVariants, + ExpandableSection, + Dropdown, + MenuToggleElement, + MenuToggle, + DropdownList, + DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './DslProperties.css'; +import "@patternfly/patternfly/patternfly.css"; +import {CamelUi} from "../utils/CamelUi"; +import {useDesignerStore} from "../DesignerStore"; +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 {ComponentApi} from "karavan-core/lib/api/ComponentApi"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {CamelMetadataApi} from "karavan-core/lib/model/CamelMetadata"; + +interface Props { + designerType: 'routes' | 'rest' | 'beans' +} + +export function PropertiesHeader(props: Props) { + + const { + saveAsRoute, + convertStep, + } = + usePropertiesHook(props.designerType); + + const [selectedStep, dark] + = useDesignerStore((s) => [s.selectedStep, s.dark], shallow) + + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); + const [isHeadersExpanded, setIsHeadersExpanded] = useState<boolean>(false); + const [isExchangePropertiesExpanded, setIsExchangePropertiesExpanded] = useState<boolean>(false); + const [isMenuOpen, setMenuOpen] = useState<boolean>(false); + + useEffect(() => { + setMenuOpen(false) + }, [selectedStep]) + + function getHeaderMenu(): React.JSX.Element { + const hasSteps = selectedStep?.hasSteps(); + const targetDsl = CamelUi.getConvertTargetDsl(selectedStep?.dslName); + const targetDslTitle = targetDsl?.replace("Definition", ""); + const showMenu = hasSteps || targetDsl !== undefined; + return showMenu ? + <Dropdown + popperProps={{position: "end"}} + isOpen={isMenuOpen} + onSelect={() => { + }} + onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} + toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( + <MenuToggle + className="header-menu-toggle" + ref={toggleRef} + aria-label="menu" + variant="plain" + onClick={() => setMenuOpen(!isMenuOpen)} + isExpanded={isMenuOpen} + > + <EllipsisVIcon/> + </MenuToggle> + )} + > + <DropdownList> + {hasSteps && + <DropdownItem key="saveRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, true); + setMenuOpen(false); + } + }}> + Save Steps to Route + </DropdownItem>} + {hasSteps && + <DropdownItem key="saveRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, false); + setMenuOpen(false); + } + }}> + Save Element to Route + </DropdownItem>} + {targetDsl && + <DropdownItem key="convert" + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} + </DropdownList> + </Dropdown> : <></>; + } + + function getExchangePropertiesSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText='Exchange Properties' + onToggle={(_event, isExpanded) => setIsExchangePropertiesExpanded(!isExchangePropertiesExpanded)} + isExpanded={isExchangePropertiesExpanded}> + <Flex className='component-headers' direction={{default: "column"}}> + {exchangeProperties.map((header, index, array) => + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <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.label}</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> + ) + } + + function getComponentHeadersSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText='Headers' + onToggle={(_event, isExpanded) => setIsHeadersExpanded(!isHeadersExpanded)} + isExpanded={isHeadersExpanded}> + <Flex className='component-headers' direction={{default: "column"}}> + {headers.filter((header) => groups.includes(header.group)) + .map((header, index, array) => + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <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> + ) + } + + function getDescriptionSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText={isDescriptionExpanded ? 'Show less' : 'Show more'} + onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} + isExpanded={isDescriptionExpanded}> + {descriptionLines.filter((value, index) => index > 0) + .map((desc, index, array) => <Text key={index} component={TextVariants.p}>{desc}</Text>)} + </ExpandableSection> + ) + } + + 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 exchangeProperties = selectedStep ? CamelMetadataApi.getExchangeProperties(selectedStep.dslName) : []; + const groups = selectedStep?.dslName === 'FromDefinition' ? ['consumer', 'common'] : ['producer', 'common'] + return ( + <div className="headers"> + <div className="top"> + <Title headingLevel="h1" size="md">{title}</Title> + {getHeaderMenu()} + </div> + <Text component={TextVariants.p}>{descriptionLines.at(0)}</Text> + {descriptionLines.length > 1 && getDescriptionSection()} + {headers.length > 0 && getComponentHeadersSection()} + {exchangeProperties.length > 0 && getExchangePropertiesSection()} + </div> + ) +} diff --git a/karavan-app/src/main/webui/src/designer/utils/IntegrationHeader.tsx b/karavan-app/src/main/webui/src/designer/utils/IntegrationHeader.tsx index 9b75fd5e..be1f998f 100644 --- a/karavan-app/src/main/webui/src/designer/utils/IntegrationHeader.tsx +++ b/karavan-app/src/main/webui/src/designer/utils/IntegrationHeader.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ import React from 'react'; -import {FormGroup, TextInput, Title} from "@patternfly/react-core"; +import {FormGroup, TextInput} from "@patternfly/react-core"; import {useIntegrationStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; @@ -25,22 +25,8 @@ export function IntegrationHeader () { const isKamelet = integration.type === 'kamelet'; - function getKameletType(): string { - // const labels = integration.metadata.labels; - // if (labels && labels.l) - // "camel.apache.org/kamelet.type" - return ''; - } - return ( <div className="headers"> - {/*<Title headingLevel="h1" size="md">Integration</Title>*/} - {/*<FormGroup label="Title" fieldId="title" isRequired>*/} - {/* <TextInput className="text-field" type="text" id="title" name="title" isReadOnly*/} - {/* value={*/} - {/* CamelUi.titleFromName(this.props.integration.metadata.name)*/} - {/* }/>*/} - {/*</FormGroup>*/} <FormGroup label="Kind" fieldId="kind" isRequired> <TextInput className="text-field" type="text" id="kind" name="kind" value={integration.kind} readOnlyVariant="default"/> diff --git a/karavan-app/src/main/webui/src/expression/ExpressionModalEditor.css b/karavan-app/src/main/webui/src/expression/ExpressionModalEditor.css index 732d3abd..c06a3a60 100644 --- a/karavan-app/src/main/webui/src/expression/ExpressionModalEditor.css +++ b/karavan-app/src/main/webui/src/expression/ExpressionModalEditor.css @@ -22,7 +22,6 @@ height: 100%; display: flex; flex-direction: column; - gap: 16px; justify-content: space-between; align-items: stretch; } @@ -36,11 +35,19 @@ .panel-middle { /*height: 64px;*/ } +@keyframes smooth-appear { + to { + bottom: 20px; + opacity:1; + } +} .panel-bottom { flex: 1; height: 100%; overflow-y: auto; + opacity:0; + animation: smooth-appear 1s ease forwards; } .context { diff --git a/karavan-app/src/main/webui/src/expression/ExpressionModalEditor.tsx b/karavan-app/src/main/webui/src/expression/ExpressionModalEditor.tsx index 038c4010..127cbda9 100644 --- a/karavan-app/src/main/webui/src/expression/ExpressionModalEditor.tsx +++ b/karavan-app/src/main/webui/src/expression/ExpressionModalEditor.tsx @@ -16,12 +16,14 @@ */ import React, {useEffect, useState} from 'react'; import { - Button, Modal, Title, TitleSizes + Button, Modal, Switch, Title, TitleSizes } from '@patternfly/react-core'; import Editor from "@monaco-editor/react"; import {ExpressionBottomPanel} from "./ExpressionBottomPanel"; import './ExpressionModalEditor.css' -import {Context, ExpressionFunctions, ExpressionVariables} from "./ExpressionContextModel"; +import {ExpressionFunctions, ExpressionVariables} from "./ExpressionContextModel"; +import ArrowDown from "@patternfly/react-icons/dist/esm/icons/angle-down-icon"; +import ArrowUp from "@patternfly/react-icons/dist/esm/icons/angle-up-icon"; interface Props { name: string, @@ -37,9 +39,10 @@ interface Props { export function ExpressionModalEditor(props: Props) { const [customCode, setCustomCode] = useState<string | undefined>(); + const [showVariables, setShowVariables] = useState<boolean>(true); + const [key, setKey] = useState<string>(''); useEffect(() => { - console.log(title, dslLanguage) setCustomCode(props.customCode) },[]); @@ -80,7 +83,8 @@ export function ExpressionModalEditor(props: Props) { <div className='container'> <div className='panel-top'> <Editor - height="100%" + key={key} + height={"100%"} width="100%" defaultLanguage={'java'} language={'java'} @@ -98,9 +102,16 @@ export function ExpressionModalEditor(props: Props) { onChange={(value, _) => setCustomCode(value)} /> </div> - {show && <div className='panel-bottom'> - {dslLanguage && <ExpressionBottomPanel dslLanguage={dslLanguage}/>} - </div>} + <Button style={{padding:"0"}} variant="link" icon={showVariables ? <ArrowDown/> : <ArrowUp/>} onClick={e => { + setShowVariables(!showVariables); + setKey(Math.random().toString()); + }} + /> + {show && showVariables && + <div className='panel-bottom'> + {dslLanguage && <ExpressionBottomPanel dslLanguage={dslLanguage}/>} + </div> + } </div> </Modal> ) diff --git a/karavan-core/src/core/model/CamelMetadata.ts b/karavan-core/src/core/model/CamelMetadata.ts index 64794b7d..8b477919 100644 --- a/karavan-core/src/core/model/CamelMetadata.ts +++ b/karavan-core/src/core/model/CamelMetadata.ts @@ -119,6 +119,10 @@ export class CamelMetadataApi { static hasLanguage = (name: string): boolean | undefined => { return Languages.filter(value => value[0] === name).length > 0; } + + static getExchangeProperties = (className: string): ExchangePropertyMeta [] => { + return CamelModelMetadata.find(e => e.className === className)?.exchangeProperties || []; + } } export const DataFormats: [string, string, string][] = [ diff --git a/karavan-designer/public/example/demo.camel.yaml b/karavan-designer/public/example/demo.camel.yaml index c02b8092..fc1cbb19 100644 --- a/karavan-designer/public/example/demo.camel.yaml +++ b/karavan-designer/public/example/demo.camel.yaml @@ -1,68 +1,3 @@ -#- route: -# id: route-a947 -# nodePrefixId: route-a32 -# from: -# id: from-c489 -# description: Receive Data -# uri: kamelet:aws-ddb-streams-source -# steps: -# - setHeader: -# id: setBody-4434 -# expression: -# simple: -# id: simple-abd8 -# expression: dddddd -# - filter: -# id: filter-929b -# expression: -# simple: -# id: simple-d932 -# expression: Hello -# steps: -# - bean: -# id: bean-120b -# ref: xxx -# - log: -# id: log-d86e -# message: ${body}!!! -# - to: -# id: to-9f7d -# description: Send notification -# uri: kafka -# - to: -# id: to-53a3 -# description: Send payments -# uri: amqp -#- route: -# id: route-8609 -# nodePrefixId: route-ae2 -# from: -# id: from-6aee -# uri: timer -# steps: -# - to: -# id: to-2df4 -# uri: direct -# parameters: -# name: second_direct -# - to: -# id: to-e017 -# uri: direct -# parameters: -# name: second_direct -#- route: -# id: first-firect -# from: -# id: from-f155 -# uri: direct -# parameters: -# name: first-firect -# steps: -# - to: -# id: to-5c86 -# uri: direct -# parameters: -# name: second_direct - route: id: second_direct from: @@ -84,6 +19,8 @@ - to: id: to-4711 uri: metrics + - aggregate: + id: aggregate-2870 - route: id: route-18e5 nodePrefixId: route-656 @@ -126,137 +63,3 @@ variableSend: hello variableReceive: world uri: amqp - - -# steps: -# - marshal: -# id: marshal-b68c -# - filter: -# expression: -# simple: -# id: simple-1465 -# id: filter-b351 -# - choice: -# when: -# - expression: -# simple: -# id: simple-99bf -# id: when-ab5e -# steps: -# - to: -# uri: arangodb -# id: to-f70a -# - removeProperties: -# id: removeProperties-344a -# - expression: -# simple: -# id: simple-d199 -# id: when-37cd -# steps: -# - to: -# uri: amqp -# id: to-fbfe -# - choice: -# when: -# - expression: -# simple: -# id: simple-e78b -# id: when-b7d0 -# otherwise: -# id: otherwise-40d0 -# id: choice-8f6b -# otherwise: -# id: otherwise-382c -# steps: -# - log: -# message: ${body} -# id: log-6831 -# id: choice-c1db -# - saga: -# id: saga-8f2c -# steps: -# - to: -# uri: kamelet:azure-cosmosdb-sink -# id: to-1394 -#- route: -# nodePrefixId: route-171 -# id: route-99f9 -# from: -# uri: kamelet:azure-storage-blob-source -# id: from-f8e9 -# steps: -# - multicast: -# id: multicast-6a53 -# steps: -# - log: -# message: ${body} -# id: log-799d -# - log: -# message: ${body} -# id: log-fc8e -# - log: -# message: ${body} -# id: log-1e42 -# - filter: -# expression: -# simple: -# id: simple-7ff9 -# id: filter-8c99 -# steps: -# - process: -# id: process-e1c1 -# description: Call cutom java bean -# - delay: -# expression: -# simple: -# id: simple-64a6 -# id: delay-b1ec -# - doTry: -# id: doTry-46cd -# doCatch: -# - id: doCatch-c6e7 -# steps: -# - log: -# message: ${body} -# id: log-77df -# - choice: -# when: -# - expression: -# simple: -# id: simple-c7db -# id: when-f058 -# otherwise: -# id: otherwise-1e11 -# id: choice-8374 -# - wireTap: -# id: wireTap-a25e -# doFinally: -# id: doFinally-0a65 -# steps: -# - log: -# message: ${body} -# id: log-f4fa -# - log: -# message: ${body} -# id: log-cd30 -# steps: -# - pollEnrich: -# expression: -# simple: -# id: simple-6181 -# id: pollEnrich-a41b -# - filter: -# expression: -# simple: -# id: simple-a69b -# id: filter-07cf -# steps: -# - setBody: -# expression: -# simple: -# id: simple-f0dc -# id: setBody-3c0c -# - process: -# id: process-6d06 -# - circuitBreaker: -# id: circuitBreaker-4af8 \ 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 656d816c..7448351c 100644 --- a/karavan-designer/src/designer/property/DslProperties.tsx +++ b/karavan-designer/src/designer/property/DslProperties.tsx @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import { Form, Text, @@ -23,11 +23,6 @@ import { ExpandableSection, Button, Tooltip, - Dropdown, - MenuToggleElement, - MenuToggle, - DropdownList, - DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -43,9 +38,7 @@ import {useDesignerStore, useIntegrationStore} from "../DesignerStore"; 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 {ComponentApi} from "karavan-core/lib/api/ComponentApi"; -import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {PropertiesHeader} from "./PropertiesHeader"; interface Props { designerType: 'routes' | 'rest' | 'beans' @@ -56,8 +49,6 @@ export function DslProperties(props: Props) { const [integration] = useIntegrationStore((s) => [s.integration], shallow) const { - saveAsRoute, - convertStep, cloneElement, onDataFormatChange, onPropertyChange, @@ -70,135 +61,6 @@ export function DslProperties(props: Props) { = useDesignerStore((s) => [s.selectedStep, s.dark], shallow) const [showAdvanced, setShowAdvanced] = useState<boolean>(false); - const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); - const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - - useEffect(() => { - setMenuOpen(false) - }, [selectedStep]) - - function getHeaderMenu(): JSX.Element { - const hasSteps = selectedStep?.hasSteps(); - const targetDsl = CamelUi.getConvertTargetDsl(selectedStep?.dslName); - const targetDslTitle = targetDsl?.replace("Definition", ""); - const showMenu = hasSteps || targetDsl !== undefined; - return showMenu ? - <Dropdown - popperProps={{position: "end"}} - isOpen={isMenuOpen} - onSelect={() => { - }} - onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} - toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( - <MenuToggle - className="header-menu-toggle" - ref={toggleRef} - aria-label="menu" - variant="plain" - onClick={() => setMenuOpen(!isMenuOpen)} - isExpanded={isMenuOpen} - > - <EllipsisVIcon/> - </MenuToggle> - )} - > - <DropdownList> - {hasSteps && - <DropdownItem key="saveRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, true); - setMenuOpen(false); - } - }}> - Save Steps to Route - </DropdownItem>} - {hasSteps && - <DropdownItem key="saveRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, false); - setMenuOpen(false); - } - }}> - Save Element to Route - </DropdownItem>} - {targetDsl && - <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} - </DropdownList> - </Dropdown> : <></>; - } - - function getRouteHeader(): JSX.Element { - 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"> - <Title headingLevel="h1" size="md">{title}</Title> - {getHeaderMenu()} - </div> - <Text component={TextVariants.p}>{descriptionLines.at(0)}</Text> - {descriptionLines.length > 1 && - <ExpandableSection toggleText={isDescriptionExpanded ? 'Show less' : 'Show more'} - onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} - isExpanded={isDescriptionExpanded}> - {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 className='component-headers' direction={{default:"column"}}> - {headers.filter((header) => groups.includes(header.group)) - .map((header, index, array) => - <Flex key={index}> - <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" isCode> - {header.name} - </ClipboardCopy> - <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> - ) - } function getClonableElementHeader(): JSX.Element { const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep); @@ -219,7 +81,7 @@ export function DslProperties(props: Props) { } function getComponentHeader(): JSX.Element { - if (props.designerType === 'routes') return getRouteHeader() + if (props.designerType === 'routes') return <PropertiesHeader designerType={props.designerType} /> else return getClonableElementHeader(); } diff --git a/karavan-designer/src/designer/property/PropertiesHeader.tsx b/karavan-designer/src/designer/property/PropertiesHeader.tsx new file mode 100644 index 00000000..9a38fa13 --- /dev/null +++ b/karavan-designer/src/designer/property/PropertiesHeader.tsx @@ -0,0 +1,239 @@ +/* + * 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, {useEffect, useState} from 'react'; +import { + Text, + Title, + TextVariants, + ExpandableSection, + Dropdown, + MenuToggleElement, + MenuToggle, + DropdownList, + DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './DslProperties.css'; +import "@patternfly/patternfly/patternfly.css"; +import {CamelUi} from "../utils/CamelUi"; +import {useDesignerStore} from "../DesignerStore"; +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 {ComponentApi} from "karavan-core/lib/api/ComponentApi"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {CamelMetadataApi} from "karavan-core/lib/model/CamelMetadata"; + +interface Props { + designerType: 'routes' | 'rest' | 'beans' +} + +export function PropertiesHeader(props: Props) { + + const { + saveAsRoute, + convertStep, + } = + usePropertiesHook(props.designerType); + + const [selectedStep, dark] + = useDesignerStore((s) => [s.selectedStep, s.dark], shallow) + + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); + const [isHeadersExpanded, setIsHeadersExpanded] = useState<boolean>(false); + const [isExchangePropertiesExpanded, setIsExchangePropertiesExpanded] = useState<boolean>(false); + const [isMenuOpen, setMenuOpen] = useState<boolean>(false); + + useEffect(() => { + setMenuOpen(false) + }, [selectedStep]) + + function getHeaderMenu(): React.JSX.Element { + const hasSteps = selectedStep?.hasSteps(); + const targetDsl = CamelUi.getConvertTargetDsl(selectedStep?.dslName); + const targetDslTitle = targetDsl?.replace("Definition", ""); + const showMenu = hasSteps || targetDsl !== undefined; + return showMenu ? + <Dropdown + popperProps={{position: "end"}} + isOpen={isMenuOpen} + onSelect={() => { + }} + onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} + toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( + <MenuToggle + className="header-menu-toggle" + ref={toggleRef} + aria-label="menu" + variant="plain" + onClick={() => setMenuOpen(!isMenuOpen)} + isExpanded={isMenuOpen} + > + <EllipsisVIcon/> + </MenuToggle> + )} + > + <DropdownList> + {hasSteps && + <DropdownItem key="saveRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, true); + setMenuOpen(false); + } + }}> + Save Steps to Route + </DropdownItem>} + {hasSteps && + <DropdownItem key="saveRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, false); + setMenuOpen(false); + } + }}> + Save Element to Route + </DropdownItem>} + {targetDsl && + <DropdownItem key="convert" + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} + </DropdownList> + </Dropdown> : <></>; + } + + function getExchangePropertiesSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText='Exchange Properties' + onToggle={(_event, isExpanded) => setIsExchangePropertiesExpanded(!isExchangePropertiesExpanded)} + isExpanded={isExchangePropertiesExpanded}> + <Flex className='component-headers' direction={{default: "column"}}> + {exchangeProperties.map((header, index, array) => + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <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.label}</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> + ) + } + + function getComponentHeadersSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText='Headers' + onToggle={(_event, isExpanded) => setIsHeadersExpanded(!isHeadersExpanded)} + isExpanded={isHeadersExpanded}> + <Flex className='component-headers' direction={{default: "column"}}> + {headers.filter((header) => groups.includes(header.group)) + .map((header, index, array) => + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <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> + ) + } + + function getDescriptionSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText={isDescriptionExpanded ? 'Show less' : 'Show more'} + onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} + isExpanded={isDescriptionExpanded}> + {descriptionLines.filter((value, index) => index > 0) + .map((desc, index, array) => <Text key={index} component={TextVariants.p}>{desc}</Text>)} + </ExpandableSection> + ) + } + + 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 exchangeProperties = selectedStep ? CamelMetadataApi.getExchangeProperties(selectedStep.dslName) : []; + const groups = selectedStep?.dslName === 'FromDefinition' ? ['consumer', 'common'] : ['producer', 'common'] + return ( + <div className="headers"> + <div className="top"> + <Title headingLevel="h1" size="md">{title}</Title> + {getHeaderMenu()} + </div> + <Text component={TextVariants.p}>{descriptionLines.at(0)}</Text> + {descriptionLines.length > 1 && getDescriptionSection()} + {headers.length > 0 && getComponentHeadersSection()} + {exchangeProperties.length > 0 && getExchangePropertiesSection()} + </div> + ) +} diff --git a/karavan-designer/src/designer/utils/IntegrationHeader.tsx b/karavan-designer/src/designer/utils/IntegrationHeader.tsx index 9b75fd5e..be1f998f 100644 --- a/karavan-designer/src/designer/utils/IntegrationHeader.tsx +++ b/karavan-designer/src/designer/utils/IntegrationHeader.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ import React from 'react'; -import {FormGroup, TextInput, Title} from "@patternfly/react-core"; +import {FormGroup, TextInput} from "@patternfly/react-core"; import {useIntegrationStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; @@ -25,22 +25,8 @@ export function IntegrationHeader () { const isKamelet = integration.type === 'kamelet'; - function getKameletType(): string { - // const labels = integration.metadata.labels; - // if (labels && labels.l) - // "camel.apache.org/kamelet.type" - return ''; - } - return ( <div className="headers"> - {/*<Title headingLevel="h1" size="md">Integration</Title>*/} - {/*<FormGroup label="Title" fieldId="title" isRequired>*/} - {/* <TextInput className="text-field" type="text" id="title" name="title" isReadOnly*/} - {/* value={*/} - {/* CamelUi.titleFromName(this.props.integration.metadata.name)*/} - {/* }/>*/} - {/*</FormGroup>*/} <FormGroup label="Kind" fieldId="kind" isRequired> <TextInput className="text-field" type="text" id="kind" name="kind" value={integration.kind} readOnlyVariant="default"/> diff --git a/karavan-generator/src/main/resources/CamelMetadata.header.ts b/karavan-generator/src/main/resources/CamelMetadata.header.ts index 89e7eaa2..266481d7 100644 --- a/karavan-generator/src/main/resources/CamelMetadata.header.ts +++ b/karavan-generator/src/main/resources/CamelMetadata.header.ts @@ -119,4 +119,8 @@ export class CamelMetadataApi { static hasLanguage = (name: string): boolean | undefined => { return Languages.filter(value => value[0] === name).length > 0; } + + static getExchangeProperties = (className: string): ExchangePropertyMeta [] => { + return CamelModelMetadata.find(e => e.className === className)?.exchangeProperties || []; + } } diff --git a/karavan-space/src/designer/property/DslProperties.tsx b/karavan-space/src/designer/property/DslProperties.tsx index 656d816c..7448351c 100644 --- a/karavan-space/src/designer/property/DslProperties.tsx +++ b/karavan-space/src/designer/property/DslProperties.tsx @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import { Form, Text, @@ -23,11 +23,6 @@ import { ExpandableSection, Button, Tooltip, - Dropdown, - MenuToggleElement, - MenuToggle, - DropdownList, - DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; @@ -43,9 +38,7 @@ import {useDesignerStore, useIntegrationStore} from "../DesignerStore"; 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 {ComponentApi} from "karavan-core/lib/api/ComponentApi"; -import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {PropertiesHeader} from "./PropertiesHeader"; interface Props { designerType: 'routes' | 'rest' | 'beans' @@ -56,8 +49,6 @@ export function DslProperties(props: Props) { const [integration] = useIntegrationStore((s) => [s.integration], shallow) const { - saveAsRoute, - convertStep, cloneElement, onDataFormatChange, onPropertyChange, @@ -70,135 +61,6 @@ export function DslProperties(props: Props) { = useDesignerStore((s) => [s.selectedStep, s.dark], shallow) const [showAdvanced, setShowAdvanced] = useState<boolean>(false); - const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); - const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - - useEffect(() => { - setMenuOpen(false) - }, [selectedStep]) - - function getHeaderMenu(): JSX.Element { - const hasSteps = selectedStep?.hasSteps(); - const targetDsl = CamelUi.getConvertTargetDsl(selectedStep?.dslName); - const targetDslTitle = targetDsl?.replace("Definition", ""); - const showMenu = hasSteps || targetDsl !== undefined; - return showMenu ? - <Dropdown - popperProps={{position: "end"}} - isOpen={isMenuOpen} - onSelect={() => { - }} - onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} - toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( - <MenuToggle - className="header-menu-toggle" - ref={toggleRef} - aria-label="menu" - variant="plain" - onClick={() => setMenuOpen(!isMenuOpen)} - isExpanded={isMenuOpen} - > - <EllipsisVIcon/> - </MenuToggle> - )} - > - <DropdownList> - {hasSteps && - <DropdownItem key="saveRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, true); - setMenuOpen(false); - } - }}> - Save Steps to Route - </DropdownItem>} - {hasSteps && - <DropdownItem key="saveRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, false); - setMenuOpen(false); - } - }}> - Save Element to Route - </DropdownItem>} - {targetDsl && - <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} - </DropdownList> - </Dropdown> : <></>; - } - - function getRouteHeader(): JSX.Element { - 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"> - <Title headingLevel="h1" size="md">{title}</Title> - {getHeaderMenu()} - </div> - <Text component={TextVariants.p}>{descriptionLines.at(0)}</Text> - {descriptionLines.length > 1 && - <ExpandableSection toggleText={isDescriptionExpanded ? 'Show less' : 'Show more'} - onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} - isExpanded={isDescriptionExpanded}> - {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 className='component-headers' direction={{default:"column"}}> - {headers.filter((header) => groups.includes(header.group)) - .map((header, index, array) => - <Flex key={index}> - <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" isCode> - {header.name} - </ClipboardCopy> - <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> - ) - } function getClonableElementHeader(): JSX.Element { const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep); @@ -219,7 +81,7 @@ export function DslProperties(props: Props) { } function getComponentHeader(): JSX.Element { - if (props.designerType === 'routes') return getRouteHeader() + if (props.designerType === 'routes') return <PropertiesHeader designerType={props.designerType} /> else return getClonableElementHeader(); } diff --git a/karavan-space/src/designer/property/PropertiesHeader.tsx b/karavan-space/src/designer/property/PropertiesHeader.tsx new file mode 100644 index 00000000..9a38fa13 --- /dev/null +++ b/karavan-space/src/designer/property/PropertiesHeader.tsx @@ -0,0 +1,239 @@ +/* + * 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, {useEffect, useState} from 'react'; +import { + Text, + Title, + TextVariants, + ExpandableSection, + Dropdown, + MenuToggleElement, + MenuToggle, + DropdownList, + DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './DslProperties.css'; +import "@patternfly/patternfly/patternfly.css"; +import {CamelUi} from "../utils/CamelUi"; +import {useDesignerStore} from "../DesignerStore"; +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 {ComponentApi} from "karavan-core/lib/api/ComponentApi"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {CamelMetadataApi} from "karavan-core/lib/model/CamelMetadata"; + +interface Props { + designerType: 'routes' | 'rest' | 'beans' +} + +export function PropertiesHeader(props: Props) { + + const { + saveAsRoute, + convertStep, + } = + usePropertiesHook(props.designerType); + + const [selectedStep, dark] + = useDesignerStore((s) => [s.selectedStep, s.dark], shallow) + + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); + const [isHeadersExpanded, setIsHeadersExpanded] = useState<boolean>(false); + const [isExchangePropertiesExpanded, setIsExchangePropertiesExpanded] = useState<boolean>(false); + const [isMenuOpen, setMenuOpen] = useState<boolean>(false); + + useEffect(() => { + setMenuOpen(false) + }, [selectedStep]) + + function getHeaderMenu(): React.JSX.Element { + const hasSteps = selectedStep?.hasSteps(); + const targetDsl = CamelUi.getConvertTargetDsl(selectedStep?.dslName); + const targetDslTitle = targetDsl?.replace("Definition", ""); + const showMenu = hasSteps || targetDsl !== undefined; + return showMenu ? + <Dropdown + popperProps={{position: "end"}} + isOpen={isMenuOpen} + onSelect={() => { + }} + onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} + toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( + <MenuToggle + className="header-menu-toggle" + ref={toggleRef} + aria-label="menu" + variant="plain" + onClick={() => setMenuOpen(!isMenuOpen)} + isExpanded={isMenuOpen} + > + <EllipsisVIcon/> + </MenuToggle> + )} + > + <DropdownList> + {hasSteps && + <DropdownItem key="saveRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, true); + setMenuOpen(false); + } + }}> + Save Steps to Route + </DropdownItem>} + {hasSteps && + <DropdownItem key="saveRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, false); + setMenuOpen(false); + } + }}> + Save Element to Route + </DropdownItem>} + {targetDsl && + <DropdownItem key="convert" + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} + </DropdownList> + </Dropdown> : <></>; + } + + function getExchangePropertiesSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText='Exchange Properties' + onToggle={(_event, isExpanded) => setIsExchangePropertiesExpanded(!isExchangePropertiesExpanded)} + isExpanded={isExchangePropertiesExpanded}> + <Flex className='component-headers' direction={{default: "column"}}> + {exchangeProperties.map((header, index, array) => + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <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.label}</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> + ) + } + + function getComponentHeadersSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText='Headers' + onToggle={(_event, isExpanded) => setIsHeadersExpanded(!isHeadersExpanded)} + isExpanded={isHeadersExpanded}> + <Flex className='component-headers' direction={{default: "column"}}> + {headers.filter((header) => groups.includes(header.group)) + .map((header, index, array) => + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <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> + ) + } + + function getDescriptionSection(): React.JSX.Element { + return ( + <ExpandableSection toggleText={isDescriptionExpanded ? 'Show less' : 'Show more'} + onToggle={(_event, isExpanded) => setIsDescriptionExpanded(!isDescriptionExpanded)} + isExpanded={isDescriptionExpanded}> + {descriptionLines.filter((value, index) => index > 0) + .map((desc, index, array) => <Text key={index} component={TextVariants.p}>{desc}</Text>)} + </ExpandableSection> + ) + } + + 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 exchangeProperties = selectedStep ? CamelMetadataApi.getExchangeProperties(selectedStep.dslName) : []; + const groups = selectedStep?.dslName === 'FromDefinition' ? ['consumer', 'common'] : ['producer', 'common'] + return ( + <div className="headers"> + <div className="top"> + <Title headingLevel="h1" size="md">{title}</Title> + {getHeaderMenu()} + </div> + <Text component={TextVariants.p}>{descriptionLines.at(0)}</Text> + {descriptionLines.length > 1 && getDescriptionSection()} + {headers.length > 0 && getComponentHeadersSection()} + {exchangeProperties.length > 0 && getExchangePropertiesSection()} + </div> + ) +} diff --git a/karavan-space/src/designer/utils/IntegrationHeader.tsx b/karavan-space/src/designer/utils/IntegrationHeader.tsx index 9b75fd5e..be1f998f 100644 --- a/karavan-space/src/designer/utils/IntegrationHeader.tsx +++ b/karavan-space/src/designer/utils/IntegrationHeader.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ import React from 'react'; -import {FormGroup, TextInput, Title} from "@patternfly/react-core"; +import {FormGroup, TextInput} from "@patternfly/react-core"; import {useIntegrationStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; @@ -25,22 +25,8 @@ export function IntegrationHeader () { const isKamelet = integration.type === 'kamelet'; - function getKameletType(): string { - // const labels = integration.metadata.labels; - // if (labels && labels.l) - // "camel.apache.org/kamelet.type" - return ''; - } - return ( <div className="headers"> - {/*<Title headingLevel="h1" size="md">Integration</Title>*/} - {/*<FormGroup label="Title" fieldId="title" isRequired>*/} - {/* <TextInput className="text-field" type="text" id="title" name="title" isReadOnly*/} - {/* value={*/} - {/* CamelUi.titleFromName(this.props.integration.metadata.name)*/} - {/* }/>*/} - {/*</FormGroup>*/} <FormGroup label="Kind" fieldId="kind" isRequired> <TextInput className="text-field" type="text" id="kind" name="kind" value={integration.kind} readOnlyVariant="default"/> diff --git a/karavan-space/src/expression/ExpressionModalEditor.css b/karavan-space/src/expression/ExpressionModalEditor.css index 732d3abd..c06a3a60 100644 --- a/karavan-space/src/expression/ExpressionModalEditor.css +++ b/karavan-space/src/expression/ExpressionModalEditor.css @@ -22,7 +22,6 @@ height: 100%; display: flex; flex-direction: column; - gap: 16px; justify-content: space-between; align-items: stretch; } @@ -36,11 +35,19 @@ .panel-middle { /*height: 64px;*/ } +@keyframes smooth-appear { + to { + bottom: 20px; + opacity:1; + } +} .panel-bottom { flex: 1; height: 100%; overflow-y: auto; + opacity:0; + animation: smooth-appear 1s ease forwards; } .context { diff --git a/karavan-space/src/expression/ExpressionModalEditor.tsx b/karavan-space/src/expression/ExpressionModalEditor.tsx index 038c4010..127cbda9 100644 --- a/karavan-space/src/expression/ExpressionModalEditor.tsx +++ b/karavan-space/src/expression/ExpressionModalEditor.tsx @@ -16,12 +16,14 @@ */ import React, {useEffect, useState} from 'react'; import { - Button, Modal, Title, TitleSizes + Button, Modal, Switch, Title, TitleSizes } from '@patternfly/react-core'; import Editor from "@monaco-editor/react"; import {ExpressionBottomPanel} from "./ExpressionBottomPanel"; import './ExpressionModalEditor.css' -import {Context, ExpressionFunctions, ExpressionVariables} from "./ExpressionContextModel"; +import {ExpressionFunctions, ExpressionVariables} from "./ExpressionContextModel"; +import ArrowDown from "@patternfly/react-icons/dist/esm/icons/angle-down-icon"; +import ArrowUp from "@patternfly/react-icons/dist/esm/icons/angle-up-icon"; interface Props { name: string, @@ -37,9 +39,10 @@ interface Props { export function ExpressionModalEditor(props: Props) { const [customCode, setCustomCode] = useState<string | undefined>(); + const [showVariables, setShowVariables] = useState<boolean>(true); + const [key, setKey] = useState<string>(''); useEffect(() => { - console.log(title, dslLanguage) setCustomCode(props.customCode) },[]); @@ -80,7 +83,8 @@ export function ExpressionModalEditor(props: Props) { <div className='container'> <div className='panel-top'> <Editor - height="100%" + key={key} + height={"100%"} width="100%" defaultLanguage={'java'} language={'java'} @@ -98,9 +102,16 @@ export function ExpressionModalEditor(props: Props) { onChange={(value, _) => setCustomCode(value)} /> </div> - {show && <div className='panel-bottom'> - {dslLanguage && <ExpressionBottomPanel dslLanguage={dslLanguage}/>} - </div>} + <Button style={{padding:"0"}} variant="link" icon={showVariables ? <ArrowDown/> : <ArrowUp/>} onClick={e => { + setShowVariables(!showVariables); + setKey(Math.random().toString()); + }} + /> + {show && showVariables && + <div className='panel-bottom'> + {dslLanguage && <ExpressionBottomPanel dslLanguage={dslLanguage}/>} + </div> + } </div> </Modal> )