This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
The following commit(s) were added to refs/heads/main by this push: new 5aa32bf Properties UI decomposition (#140) 5aa32bf is described below commit 5aa32bf85aafdc1875ecc9824677ba4fa378dbf7 Author: Marat Gubaidullin <marat.gubaidul...@gmail.com> AuthorDate: Sun Dec 5 15:59:57 2021 -0500 Properties UI decomposition (#140) --- karavan-designer/src/App.tsx | 5 +- karavan-designer/src/designer/api/CamelApiExt.tsx | 16 +- karavan-designer/src/designer/api/CamelUi.tsx | 12 +- karavan-designer/src/designer/ui/DslProperties.tsx | 243 ++------------------- .../designer/ui/field/ComponentParameterField.tsx | 120 ++++++++++ .../src/designer/ui/field/DataFormatField.tsx | 124 +++++++++++ .../src/designer/ui/field/DslPropertyField.tsx | 176 +++++++++++++++ .../src/designer/ui/field/ExpressionField.tsx | 115 ++++++++++ .../src/designer/ui/field/KameletPropertyField.tsx | 99 +++++++++ 9 files changed, 667 insertions(+), 243 deletions(-) diff --git a/karavan-designer/src/App.tsx b/karavan-designer/src/App.tsx index 4d3f39e..df7f5cf 100644 --- a/karavan-designer/src/App.tsx +++ b/karavan-designer/src/App.tsx @@ -53,7 +53,10 @@ class App extends React.Component<Props, State> { ' - pollEnrich:\n' + ' expression: {}\n' + ' - to: \n' + - ' uri: "log:info"\n' + + ' uri: "log:info:xxx"\n' + + ' parameters:\n' + + ' level: \'OFF\'\n' + + ' logMask: true \n' + ' - choice:\n' + ' otherwise: {}\n' + ' when:\n' + diff --git a/karavan-designer/src/designer/api/CamelApiExt.tsx b/karavan-designer/src/designer/api/CamelApiExt.tsx index 58a5143..5c1eec7 100644 --- a/karavan-designer/src/designer/api/CamelApiExt.tsx +++ b/karavan-designer/src/designer/api/CamelApiExt.tsx @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {CamelElement, FromStep, Integration, ProcessorStep} from "../model/CamelModel"; +import {CamelElement, Expression, FromStep, Integration, ProcessorStep} from "../model/CamelModel"; import {CamelMetadataApi, DataFormats, PropertyMeta} from "./CamelMetadata"; import {CamelApi} from "./CamelApi"; import {ComponentApi} from "./ComponentApi"; @@ -63,10 +63,10 @@ export class CamelApiExt { return integration; } - static getExpressionLanguage = (element: CamelElement | undefined): string | undefined => { - const el: any = Object.assign({}, element); - if (el.hasOwnProperty('expression') && el.expression) { - return el.expression.language + static getExpressionLanguage = (expression: Expression | undefined): string | undefined => { + const el: any = Object.assign({}, expression); + if (el.hasOwnProperty('language') && el.language) { + return el.language } else { return undefined; } @@ -78,10 +78,10 @@ export class CamelApiExt { return dataFormat ? [dataFormat, el[dataFormat]] : undefined; } - static getExpressionValue = (element: CamelElement | undefined): string | undefined => { - const language = CamelApiExt.getExpressionLanguage(element); + static getExpressionValue = (expression: Expression | undefined): string | undefined => { + const language = CamelApiExt.getExpressionLanguage(expression); if (language) { - return (element as any).expression[language]; + return (expression as any)[language]; } else { return undefined; } diff --git a/karavan-designer/src/designer/api/CamelUi.tsx b/karavan-designer/src/designer/api/CamelUi.tsx index 79c19de..6d6be18 100644 --- a/karavan-designer/src/designer/api/CamelUi.tsx +++ b/karavan-designer/src/designer/api/CamelUi.tsx @@ -240,8 +240,11 @@ export class CamelUi { }; static isShowExpressionTooltip = (element: CamelElement): boolean => { - const exp = CamelApiExt.getExpressionValue(element); - return element.hasOwnProperty("expression") && (exp !== undefined && exp?.trim().length > 0); + if (element.hasOwnProperty("expression")){ + const exp = CamelApiExt.getExpressionValue((element as any).expression); + return (exp !== undefined && exp?.trim().length > 0); + } + return false; } static isShowUriTooltip = (element: CamelElement): boolean => { @@ -250,8 +253,9 @@ export class CamelUi { } static getExpressionTooltip = (element: CamelElement): string => { - const language = CamelApiExt.getExpressionLanguage(element) || 'simple'; - const value = CamelApiExt.getExpressionValue(element) || ''; + const e = (element as any).expression; + const language = CamelApiExt.getExpressionLanguage(e) || 'simple'; + const value = CamelApiExt.getExpressionValue(e) || ''; return language.concat(": ", value); } diff --git a/karavan-designer/src/designer/ui/DslProperties.tsx b/karavan-designer/src/designer/ui/DslProperties.tsx index 80cd53d..c06f74a 100644 --- a/karavan-designer/src/designer/ui/DslProperties.tsx +++ b/karavan-designer/src/designer/ui/DslProperties.tsx @@ -24,12 +24,6 @@ import { Popover, Switch, TextVariants, - Select, - SelectVariant, - SelectDirection, - SelectOption, - TextArea, - ExpandableSection, } from '@patternfly/react-core'; import '../karavan.css'; import "@patternfly/patternfly/patternfly.css"; @@ -38,11 +32,12 @@ import {Property} from "../model/KameletModels"; import {CamelElement, Expression, Integration} from "../model/CamelModel"; import {CamelApi} from "../api/CamelApi"; import {CamelApiExt} from "../api/CamelApiExt"; -import {CamelMetadataApi, DataFormats, Languages, PropertyMeta} from "../api/CamelMetadata"; +import {CamelMetadataApi, PropertyMeta} from "../api/CamelMetadata"; import {CamelYaml} from "../api/CamelYaml"; import {CamelUi} from "../api/CamelUi"; import {ComponentApi} from "../api/ComponentApi"; -import {ComponentProperty} from "../model/ComponentModels"; +import {DslPropertyField} from "./field/DslPropertyField"; +import {DataFormatField} from "./field/DataFormatField"; interface Props { integration: Integration, @@ -114,7 +109,7 @@ export class DslProperties extends React.Component<Props, State> { this.props.onPropertyUpdate?.call(this, clone, this.state.step.uuid); } } - }; + } componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) => { if (prevProps.step !== this.props.step) { @@ -130,14 +125,6 @@ export class DslProperties extends React.Component<Props, State> { }); } - openSelect = (propertyName: string) => { - this.setState({selectStatus: new Map<string, boolean>([[propertyName, true]])}); - } - - isSelectOpen = (propertyName: string): boolean => { - return this.state.selectStatus.has(propertyName) && this.state.selectStatus.get(propertyName) === true; - } - getIntegrationHeader = (): JSX.Element => { return ( <div className="headers"> @@ -215,225 +202,21 @@ export class DslProperties extends React.Component<Props, State> { ) } - createComponentProperty = (property: ComponentProperty): JSX.Element => { - const prefix = "parameters"; - const id = prefix + "-" + property.name; - const value = CamelApiExt.getParametersValue(this.state.element, property.name, property.kind === 'path'); - const selectOptions: JSX.Element[] = [] - if (property.enum && property.enum.length > 0) { - selectOptions.push(<SelectOption key={0} value={"Select ..."} isPlaceholder/>); - property.enum.forEach(v => selectOptions.push(<SelectOption key={v} value={v}/>)); - } - return ( - <FormGroup - key={id} - label={property.displayName} - fieldId={id} - isRequired={property.kind === 'path' || property.required} - labelIcon={ - <Popover - position={"left"} - headerContent={property.displayName} - bodyContent={property.description} - footerContent={property.defaultValue !== undefined ? "Default: " + property.defaultValue : undefined}> - <button type="button" aria-label="More info" onClick={e => e.preventDefault()} - className="pf-c-form__group-label-help"> - <HelpIcon noVerticalAlign/> - </button> - </Popover> - }> - {['string', 'duration', 'integer', 'int', 'number'].includes(property.type) && property.enum === undefined && - <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={e => this.parametersChanged(property.name, ['integer', 'int', 'number'].includes(property.type) ? Number(e) : e, property.kind === 'path')}/> - } - {property.type === 'string' && property.enum && - <Select - variant={SelectVariant.single} - aria-label={property.name} - onToggle={isExpanded => { - this.openSelect(property.name) - }} - onSelect={(e, value, isPlaceholder) => this.parametersChanged(property.name, (!isPlaceholder ? value : undefined), property.kind === 'path')} - selections={value !== undefined ? value.toString() : property.defaultValue} - isOpen={this.isSelectOpen(property.name)} - aria-labelledby={property.name} - direction={SelectDirection.down} - > - {selectOptions} - </Select> - } - {property.type === 'boolean' && <Switch - id={id} name={id} - value={value?.toString()} - aria-label={id} - isChecked={value !== undefined ? Boolean(value) === true : Boolean(property.defaultValue) === true} - onChange={e => this.parametersChanged(property.name, !Boolean(value))}/> - } - </FormGroup> - ) - } - - createExpressionProperty = (property: PropertyMeta): JSX.Element => { - const prefix = "language"; - const language = CamelApiExt.getExpressionLanguage(this.state.element) || 'Simple' - const dslLanguage = Languages.find((l: [string, string, string]) => l[0] === language); - const value = language ? CamelApiExt.getExpressionValue(this.state.element) : undefined; - const selectOptions: JSX.Element[] = [] - Languages.forEach((lang: [string, string, string]) => { - const s = <SelectOption key={lang[0]} value={lang[0]} description={lang[2]}/>; - selectOptions.push(s); - }) - return ( - <div> - <FormGroup key={prefix + "-" + property.name} fieldId={property.name}> - <Select - variant={SelectVariant.typeahead} - aria-label={property.name} - onToggle={isExpanded => { - this.openSelect(property.name) - }} - onSelect={(e, lang, isPlaceholder) => this.expressionChanged(lang.toString(), value)} - selections={dslLanguage} - isOpen={this.isSelectOpen(property.name)} - aria-labelledby={property.name} - direction={SelectDirection.down} - > - {selectOptions} - </Select> - </FormGroup> - <FormGroup - key={property.name} - fieldId={property.name} - labelIcon={property.description ? - <Popover - position={"left"} - headerContent={property.displayName} - bodyContent={property.description}> - <button type="button" aria-label="More info" onClick={e => { - e.preventDefault(); - e.stopPropagation(); - }} - className="pf-c-form__group-label-help"> - <HelpIcon noVerticalAlign/> - </button> - </Popover> : <div></div> - }> - <TextArea - autoResize - className="text-field" isRequired - type={"text"} - id={property.name} name={property.name} - height={"100px"} - value={value?.toString()} - onChange={e => this.expressionChanged(language, e)}/> - </FormGroup> - </div> - ) - } - - createEipDslProperty = (property: PropertyMeta): JSX.Element => { - const value = this.state.element ? (this.state.element as any)[property.name] : undefined; - const selectOptions: JSX.Element[] = [] - if (property.enumVals && property.enumVals.length > 0) { - selectOptions.push(<SelectOption key={0} value={"Select " + property.name} isPlaceholder/>); - selectOptions.push(...property.enumVals.split(',').map((value: string) => - <SelectOption key={value} value={value.trim()}/>)); - } - return ( - <FormGroup - key={property.name} - label={CamelApi.capitalizeName(property.displayName)} - fieldId={property.name} - labelIcon={property.description ? - <Popover - position={"left"} - headerContent={property.displayName} - bodyContent={property.description} - footerContent={property.defaultValue !== undefined ? "Default: " + property.defaultValue : undefined}> - <button type="button" aria-label="More info" onClick={e => { - e.preventDefault(); - e.stopPropagation(); - }} - className="pf-c-form__group-label-help"> - <HelpIcon noVerticalAlign/> - </button> - </Popover> : <div></div> - }> - {['string', 'duration', 'integer', 'number'].includes(property.type) && !property.enumVals && - <TextInput - isReadOnly={property.name === 'uri' && this.state.element?.dslName !== 'toD'} - className="text-field" isRequired - type={['integer', 'number'].includes(property.type) ? 'number' : (property.secret ? "password" : "text")} - id={property.name} name={property.name} - value={value?.toString()} - onChange={e => this.propertyChanged(property.name, ['integer', 'number'].includes(property.type) ? Number(e) : e)}/> - } - {property.type === 'boolean' && <Switch - id={property.name} name={property.name} - value={this.state.element?.toString()} - aria-label={property.name} - isChecked={Boolean(value) === true} - onChange={e => this.propertyChanged(property.name, !Boolean(value))}/> - } - - {property.enumVals && - <Select - variant={SelectVariant.single} - aria-label={property.name} - onToggle={isExpanded => { - this.openSelect(property.name) - }} - onSelect={(e, value, isPlaceholder) => this.propertyChanged(property.name, (!isPlaceholder ? value : undefined))} - selections={value} - isOpen={this.isSelectOpen(property.name)} - aria-labelledby={property.name} - direction={SelectDirection.down} - > - {selectOptions} - </Select> - } - {property.name === 'expression' && property.type === "Expression" && - <div className="expression"> - {this.createExpressionProperty(property)} - </div> - } - <div className="parameters"> - {property.name === 'parameters' && CamelUi.isKameletComponent(this.state.element) - && CamelUi.getKameletProperties(this.state.element).map(kp => this.createKameletProperty(kp))} - - {property.name === 'parameters' && this.state.element && !CamelUi.isKameletComponent(this.state.element) - && CamelUi.getComponentProperties(this.state.element, false).map(kp => this.createComponentProperty(kp))} - </div> - {property.name === 'parameters' && this.state.element && !CamelUi.isKameletComponent(this.state.element) && CamelUi.getComponentProperties(this.state.element, true).length > 0 && ( - <ExpandableSection - toggleText={'Advanced parameters'} - onToggle={isExpanded => this.setState({isShowAdvanced: !this.state.isShowAdvanced})} - isExpanded={this.state.isShowAdvanced}> - <div className="parameters"> - {CamelUi.getComponentProperties(this.state.element, true).map(kp => this.createComponentProperty(kp))} - </div> - </ExpandableSection> - )} - </FormGroup> - ) - } - - setDataFormat = (dataFormat: string, props: any) => { - console.log(dataFormat); - console.log(props); - } - render() { return ( <div key={this.state.step ? this.state.step.uuid : 'integration'} className='properties'> <Form autoComplete="off"> {this.state.element === undefined && this.getIntegrationHeader()} {this.state.element && this.getComponentHeader()} - {this.state.element && CamelApiExt.getElementProperties(this.state.element.dslName).map((property: PropertyMeta) => this.createEipDslProperty(property))} + {this.state.element && CamelApiExt.getElementProperties(this.state.element.dslName).map((property: PropertyMeta) => + <DslPropertyField property={property} + element={this.state.element} + value={this.state.element ? (this.state.element as any)[property.name] : undefined} + onExpressionChange={this.expressionChanged} + onParameterChange={this.parametersChanged} + onChange={this.propertyChanged} /> + )} + {this.state.element && ['marshal', 'unmarshal'].includes(this.state.element.dslName) && <DataFormatField element={this.state.element}/>} </Form> </div> ) diff --git a/karavan-designer/src/designer/ui/field/ComponentParameterField.tsx b/karavan-designer/src/designer/ui/field/ComponentParameterField.tsx new file mode 100644 index 0000000..f9b6e15 --- /dev/null +++ b/karavan-designer/src/designer/ui/field/ComponentParameterField.tsx @@ -0,0 +1,120 @@ +/* + * 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 from 'react'; +import { + FormGroup, + TextInput, + Popover, + Switch, + Select, + SelectVariant, + SelectDirection, + SelectOption, +} from '@patternfly/react-core'; +import '../../karavan.css'; +import "@patternfly/patternfly/patternfly.css"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {ComponentProperty} from "../../model/ComponentModels"; + +interface Props { + property: ComponentProperty, + value: any, + onParameterChange?: (parameter: string, value: string | number | boolean | any, pathParameter?: boolean) => void +} + +interface State { + selectIsOpen: boolean +} + +export class ComponentParameterField extends React.Component<Props, State> { + + public state: State = { + selectIsOpen: false, + } + + openSelect = () => { + this.setState({selectIsOpen: true}); + } + + parametersChanged = (parameter: string, value: string | number | boolean | any, pathParameter?: boolean) => { + this.props.onParameterChange?.call(this, parameter, value, pathParameter); + this.setState({selectIsOpen: false}); + } + + render() { + const property: ComponentProperty = this.props.property; + const value = this.props.value; + const prefix = "parameters"; + const id = prefix + "-" + property.name; + const selectOptions: JSX.Element[] = [] + if (property.enum && property.enum.length > 0) { + selectOptions.push(<SelectOption key={0} value={"Select ..."} isPlaceholder/>); + property.enum.forEach(v => selectOptions.push(<SelectOption key={v} value={v}/>)); + } + return ( + <FormGroup + key={id} + label={property.displayName} + fieldId={id} + isRequired={property.kind === 'path' || property.required} + labelIcon={ + <Popover + position={"left"} + headerContent={property.displayName} + bodyContent={property.description} + footerContent={property.defaultValue !== undefined ? "Default: " + property.defaultValue : undefined}> + <button type="button" aria-label="More info" onClick={e => e.preventDefault()} + className="pf-c-form__group-label-help"> + <HelpIcon noVerticalAlign/> + </button> + </Popover> + }> + {['string', 'duration', 'integer', 'int', 'number'].includes(property.type) && property.enum === undefined && + <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={e => this.parametersChanged(property.name, ['integer', 'int', 'number'].includes(property.type) ? Number(e) : e, property.kind === 'path')}/> + } + {property.type === 'string' && property.enum && + <Select + variant={SelectVariant.single} + aria-label={property.name} + onToggle={isExpanded => { + this.openSelect() + }} + onSelect={(e, value, isPlaceholder) => this.parametersChanged(property.name, (!isPlaceholder ? value : undefined), property.kind === 'path')} + selections={value !== undefined ? value.toString() : property.defaultValue} + isOpen={this.state.selectIsOpen} + aria-labelledby={property.name} + direction={SelectDirection.down} + > + {selectOptions} + </Select> + } + {property.type === 'boolean' && <Switch + id={id} name={id} + value={value?.toString()} + aria-label={id} + isChecked={value !== undefined ? Boolean(value) === true : Boolean(property.defaultValue) === true} + onChange={e => this.parametersChanged(property.name, !Boolean(value))}/> + } + </FormGroup> + ) + } +} \ No newline at end of file diff --git a/karavan-designer/src/designer/ui/field/DataFormatField.tsx b/karavan-designer/src/designer/ui/field/DataFormatField.tsx new file mode 100644 index 0000000..a4168f2 --- /dev/null +++ b/karavan-designer/src/designer/ui/field/DataFormatField.tsx @@ -0,0 +1,124 @@ +/* + * 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 from 'react'; +import { + FormGroup, + Popover, + Select, + SelectVariant, + SelectDirection, + SelectOption, + TextArea, +} from '@patternfly/react-core'; +import '../../karavan.css'; +import "@patternfly/patternfly/patternfly.css"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {CamelElement} from "../../model/CamelModel"; +import {CamelApiExt} from "../../api/CamelApiExt"; +import {CamelMetadataApi, DataFormats, PropertyMeta} from "../../api/CamelMetadata"; + +interface Props { + element: CamelElement, +} + +interface State { + property: PropertyMeta, + element?: CamelElement, + selectStatus: Map<string, boolean> +} + +export class DataFormatField extends React.Component<Props, State> { + + public state: State = { + property: new PropertyMeta('id', 'Id', "The id of this node", 'string', '', '', false, false, false, false), + selectStatus: new Map<string, boolean>(), + element: this.props.element, + } + + setDataFormat = (dataFormat: string, props: any) => { + console.log(dataFormat); + console.log(props); + } + + openSelect = (propertyName: string) => { + this.setState({selectStatus: new Map<string, boolean>([[propertyName, true]])}); + } + + isSelectOpen = (propertyName: string): boolean => { + return this.state.selectStatus.has(propertyName) && this.state.selectStatus.get(propertyName) === true; + } + + render() { + const fieldId = "dataFormat"; + const dataFormat = CamelApiExt.getDataFormat(this.state.element) + const dataFormatName = dataFormat ? dataFormat[0] : ''; + const value = dataFormat ? CamelApiExt.getExpressionValue(this.state.element) : undefined; + const properties = CamelMetadataApi.getCamelDataFormatMetadata(dataFormatName)?.properties; + const selectOptions: JSX.Element[] = [] + DataFormats.forEach((df: [string, string, string]) => { + const s = <SelectOption key={df[0]} value={df[0]} description={df[2]}/>; + selectOptions.push(s); + }) + return ( + <div> + <FormGroup + key={fieldId} + label="Data Format" + fieldId="dataFormat" + labelIcon={ + <Popover + position={"left"} + headerContent="Data Format" + bodyContent="Specified format for transmission over a transport or component"> + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} + className="pf-c-form__group-label-help"> + <HelpIcon noVerticalAlign/> + </button> + </Popover>} + > + <div className="dataformat"> + <Select + variant={SelectVariant.typeahead} + aria-label="dataFormat" + onToggle={isExpanded => { + this.openSelect(fieldId) + }} + onSelect={(e, df, isPlaceholder) => this.setDataFormat(df.toString(), value)} + selections={dataFormatName} + isOpen={this.isSelectOpen(fieldId)} + aria-labelledby={fieldId} + direction={SelectDirection.down} + > + {selectOptions} + </Select> + <TextArea + autoResize + className="text-field" isRequired + type={"text"} + id={fieldId+"text"} name={fieldId+"text"} + height={"100px"} + value={value?.toString()} + onChange={e => this.setDataFormat(dataFormatName, e)}/> + </div> + </FormGroup> + </div> + ) + } +} \ No newline at end of file diff --git a/karavan-designer/src/designer/ui/field/DslPropertyField.tsx b/karavan-designer/src/designer/ui/field/DslPropertyField.tsx new file mode 100644 index 0000000..073907d --- /dev/null +++ b/karavan-designer/src/designer/ui/field/DslPropertyField.tsx @@ -0,0 +1,176 @@ +/* + * 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 from 'react'; +import { + FormGroup, + TextInput, + Popover, + Switch, + Select, + SelectVariant, + SelectDirection, + SelectOption, ExpandableSection, +} from '@patternfly/react-core'; +import '../../karavan.css'; +import "@patternfly/patternfly/patternfly.css"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {CamelApi} from "../../api/CamelApi"; +import { PropertyMeta} from "../../api/CamelMetadata"; +import {CamelApiExt} from "../../api/CamelApiExt"; +import {ExpressionField} from "./ExpressionField"; +import {CamelUi} from "../../api/CamelUi"; +import {ComponentParameterField} from "./ComponentParameterField"; +import {CamelElement} from "../../model/CamelModel"; +import {KameletPropertyField} from "./KameletPropertyField"; + +interface Props { + property: PropertyMeta, + value: any, + onChange?: (fieldId: string, value: string | number | boolean | any) => void, + onExpressionChange?: (language: string, value: string | undefined) => void, + onParameterChange?: (parameter: string, value: string | number | boolean | any, pathParameter?: boolean) => void, + element?: CamelElement +} + +interface State { + selectStatus: Map<string, boolean>, + isShowAdvanced: boolean +} + +export class DslPropertyField extends React.Component<Props, State> { + + public state: State = { + selectStatus: new Map<string, boolean>(), + isShowAdvanced: false + } + + openSelect = (propertyName: string) => { + this.setState({selectStatus: new Map<string, boolean>([[propertyName, true]])}); + } + + isSelectOpen = (propertyName: string): boolean => { + return this.state.selectStatus.has(propertyName) && this.state.selectStatus.get(propertyName) === true; + } + + propertyChanged = (fieldId: string, value: string | number | boolean | any) => { + this.props.onChange?.call(this, fieldId, value); + } + + render() { + const isKamelet = CamelUi.isKameletComponent(this.props.element); + const property: PropertyMeta = this.props.property; + const value = this.props.value; + const selectOptions: JSX.Element[] = [] + if (property.enumVals && property.enumVals.length > 0) { + selectOptions.push(<SelectOption key={0} value={"Select " + property.name} isPlaceholder/>); + selectOptions.push(...property.enumVals.split(',').map((value: string) => + <SelectOption key={value} value={value.trim()}/>)); + } + return ( + <FormGroup + key={property.name} + label={CamelApi.capitalizeName(property.displayName)} + fieldId={property.name} + labelIcon={property.description ? + <Popover + position={"left"} + headerContent={property.displayName} + bodyContent={property.description} + footerContent={property.defaultValue !== undefined ? "Default: " + property.defaultValue : undefined}> + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} + className="pf-c-form__group-label-help"> + <HelpIcon noVerticalAlign/> + </button> + </Popover> : <div></div> + }> + {['string', 'duration', 'integer', 'number'].includes(property.type) && !property.enumVals && + <TextInput + // isReadOnly={property.name === 'uri' && this.state.element?.dslName !== 'toD'} + className="text-field" isRequired + type={['integer', 'number'].includes(property.type) ? 'number' : (property.secret ? "password" : "text")} + id={property.name} name={property.name} + value={value?.toString()} + onChange={e => this.propertyChanged(property.name, ['integer', 'number'].includes(property.type) ? Number(e) : e)}/> + } + {property.type === 'boolean' && <Switch + id={property.name} name={property.name} + value={value?.toString()} + aria-label={property.name} + isChecked={Boolean(value) === true} + onChange={e => this.propertyChanged(property.name, !Boolean(value))}/> + } + + {property.enumVals && + <Select + variant={SelectVariant.single} + aria-label={property.name} + onToggle={isExpanded => { + this.openSelect(property.name) + }} + onSelect={(e, value, isPlaceholder) => this.propertyChanged(property.name, (!isPlaceholder ? value : undefined))} + selections={value} + isOpen={this.isSelectOpen(property.name)} + aria-labelledby={property.name} + direction={SelectDirection.down} + > + {selectOptions} + </Select> + } + {property.name === 'expression' && property.type === "Expression" && + <div className="expression"> + <ExpressionField property={property} value={value} onExpressionChange={this.props.onExpressionChange} /> + </div> + } + {property.name === 'parameters' && + <div className="parameters"> + {!isKamelet && CamelUi.getComponentProperties(this.props.element, false).map(kp => + <ComponentParameterField + property={kp} + value={CamelApiExt.getParametersValue(this.props.element, kp.name, kp.kind === 'path')} + onParameterChange={this.props.onParameterChange} + />)} + {isKamelet && CamelUi.getKameletProperties(this.props.element).map(property => + <KameletPropertyField + property={property} + value={CamelApiExt.getParametersValue(this.props.element, property.id)} + onParameterChange={this.props.onParameterChange} + />)} + </div> + } + {property.name === 'parameters' && this.props.element && !isKamelet && CamelUi.getComponentProperties(this.props.element, true).length > 0 && ( + <ExpandableSection + toggleText={'Advanced parameters'} + onToggle={isExpanded => this.setState({isShowAdvanced: !this.state.isShowAdvanced})} + isExpanded={this.state.isShowAdvanced}> + <div className="parameters"> + {CamelUi.getComponentProperties(this.props.element, true).map(kp => + <ComponentParameterField + property={kp} + value={CamelApiExt.getParametersValue(this.props.element, kp.name, kp.kind === 'path')} + onParameterChange={this.props.onParameterChange} + /> + )} + </div> + </ExpandableSection> + )} + </FormGroup> + ) + } +} \ No newline at end of file diff --git a/karavan-designer/src/designer/ui/field/ExpressionField.tsx b/karavan-designer/src/designer/ui/field/ExpressionField.tsx new file mode 100644 index 0000000..62eb70c --- /dev/null +++ b/karavan-designer/src/designer/ui/field/ExpressionField.tsx @@ -0,0 +1,115 @@ +/* + * 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 from 'react'; +import { + FormGroup, + Popover, + Select, + SelectVariant, + SelectDirection, + SelectOption, TextArea, +} from '@patternfly/react-core'; +import '../../karavan.css'; +import "@patternfly/patternfly/patternfly.css"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {Languages, PropertyMeta} from "../../api/CamelMetadata"; +import {CamelApiExt} from "../../api/CamelApiExt"; + +interface Props { + property: PropertyMeta, + value: any, + onExpressionChange?: (language: string, value: string | undefined) => void +} + +interface State { + selectIsOpen: boolean; +} + +export class ExpressionField extends React.Component<Props, State> { + + public state: State = { + selectIsOpen: false, + } + + openSelect = () => { + this.setState({selectIsOpen: true}); + } + + expressionChanged = (language: string, value: string | undefined) => { + this.props.onExpressionChange?.call(this, language, value); + this.setState({selectIsOpen: false}); + } + + render() { + const property: PropertyMeta = this.props.property; + const prefix = "language"; + const language = CamelApiExt.getExpressionLanguage(this.props.value) || 'Simple' + const dslLanguage = Languages.find((l: [string, string, string]) => l[0] === language); + const value = language ? CamelApiExt.getExpressionValue(this.props.value) : undefined; + const selectOptions: JSX.Element[] = [] + Languages.forEach((lang: [string, string, string]) => { + const s = <SelectOption key={lang[0]} value={lang[0]} description={lang[2]}/>; + selectOptions.push(s); + }) + return ( + <div> + <FormGroup key={prefix + "-" + property.name} fieldId={property.name}> + <Select + variant={SelectVariant.typeahead} + aria-label={property.name} + onToggle={isExpanded => { + this.openSelect() + }} + onSelect={(e, lang, isPlaceholder) => this.expressionChanged(lang.toString(), value)} + selections={dslLanguage} + isOpen={this.state.selectIsOpen} + aria-labelledby={property.name} + direction={SelectDirection.down} + > + {selectOptions} + </Select> + </FormGroup> + <FormGroup + key={property.name} + fieldId={property.name} + labelIcon={property.description ? + <Popover + position={"left"} + headerContent={property.displayName} + bodyContent={property.description}> + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} + className="pf-c-form__group-label-help"> + <HelpIcon noVerticalAlign/> + </button> + </Popover> : <div></div> + }> + <TextArea + autoResize + className="text-field" isRequired + type={"text"} + id={property.name} name={property.name} + height={"100px"} + value={value?.toString()} + onChange={e => this.expressionChanged(language, e)}/> + </FormGroup> + </div> + ) + } +} \ No newline at end of file diff --git a/karavan-designer/src/designer/ui/field/KameletPropertyField.tsx b/karavan-designer/src/designer/ui/field/KameletPropertyField.tsx new file mode 100644 index 0000000..78ed047 --- /dev/null +++ b/karavan-designer/src/designer/ui/field/KameletPropertyField.tsx @@ -0,0 +1,99 @@ +/* + * 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 from 'react'; +import { + FormGroup, + TextInput, + Popover, + Switch, +} from '@patternfly/react-core'; +import '../../karavan.css'; +import "@patternfly/patternfly/patternfly.css"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import {Property} from "../../model/KameletModels"; + +interface Props { + property: Property, + value: any, + onParameterChange?: (parameter: string, value: string | number | boolean | any, pathParameter?: boolean) => void +} + +interface State { + selectIsOpen: boolean +} + +export class KameletPropertyField extends React.Component<Props, State> { + + public state: State = { + selectIsOpen: false, + } + + openSelect = () => { + this.setState({selectIsOpen: true}); + } + + parametersChanged = (parameter: string, value: string | number | boolean | any, pathParameter?: boolean) => { + this.props.onParameterChange?.call(this, parameter, value, pathParameter); + this.setState({selectIsOpen: false}); + } + + render() { + const property = this.props.property; + const value = this.props.value; + const prefix = "parameters"; + const id = prefix + "-" + property.id; + return ( + <FormGroup + key={id} + label={property.title} + fieldId={id} + labelIcon={ + <Popover + position={"left"} + headerContent={property.title} + bodyContent={property.description} + footerContent={ + <div> + {property.default !== undefined && + <div>Default: {property.default.toString()}</div>} + {property.example !== undefined && <div>Example: {property.example}</div>} + </div> + }> + <button type="button" aria-label="More info" onClick={e => e.preventDefault()} + className="pf-c-form__group-label-help"> + <HelpIcon noVerticalAlign/> + </button> + </Popover> + }> + {['string', 'integer', 'int', 'number'].includes(property.type) && <TextInput + className="text-field" isRequired + type={['integer', 'int', 'number'].includes(property.type) ? 'number' : (property.format ? "password" : "text")} + id={id} name={id} + value={value} + onChange={e => this.parametersChanged(property.id, ['integer', 'int', 'number'].includes(property.type) ? Number(e) : e)}/> + } + {property.type === 'boolean' && <Switch + id={id} name={id} + value={value?.toString()} + aria-label={id} + isChecked={Boolean(value) === true} + onChange={e => this.parametersChanged(property.id, !Boolean(value))}/> + } + </FormGroup> + ) + } +} \ No newline at end of file