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 0135be74 Kamelets Definition Enum UI #315 0135be74 is described below commit 0135be7402ec837f9ba38fb5c58b4bceeadb4e4a Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Mon Oct 2 15:19:21 2023 -0400 Kamelets Definition Enum UI #315 --- .../src/core/model/IntegrationDefinition.ts | 1 + .../public/example/aws-s3-cdc-source.kamelet.yaml | 219 +++++++++++++++++++++ karavan-designer/src/App.tsx | 4 +- .../kamelet/KameletDefinitionPropertyCard.tsx | 125 +++++++++++- .../designer/kamelet/KameletDefinitionsPanel.tsx | 35 +++- karavan-designer/src/designer/kamelet/kamelet.css | 1 + 6 files changed, 372 insertions(+), 13 deletions(-) diff --git a/karavan-core/src/core/model/IntegrationDefinition.ts b/karavan-core/src/core/model/IntegrationDefinition.ts index 696e2da1..974f8e5e 100644 --- a/karavan-core/src/core/model/IntegrationDefinition.ts +++ b/karavan-core/src/core/model/IntegrationDefinition.ts @@ -25,6 +25,7 @@ export class DefinitionProperty { example?: any; format?: string; "x-descriptors"?: string[]; + enum?: string[]; public constructor(init?: Partial<DefinitionProperty>) { Object.assign(this, init); diff --git a/karavan-designer/public/example/aws-s3-cdc-source.kamelet.yaml b/karavan-designer/public/example/aws-s3-cdc-source.kamelet.yaml new file mode 100644 index 00000000..c093d002 --- /dev/null +++ b/karavan-designer/public/example/aws-s3-cdc-source.kamelet.yaml @@ -0,0 +1,219 @@ +apiVersion: camel.apache.org/v1 +kind: Kamelet +metadata: + name: aws-s3-cdc-source + annotations: + camel.apache.org/kamelet.support.level: Preview + camel.apache.org/catalog.version: "4.1.0-SNAPSHOT" + camel.apache.org/kamelet.icon: >- + data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFRE [...] + camel.apache.org/provider: Apache Software Foundation + camel.apache.org/kamelet.group: AWS S3 CDC + camel.apache.org/kamelet.namespace: "AWS" + camel.apache.org/keda.type: aws-s3-cdc-queue + labels: + camel.apache.org/kamelet.type: source +spec: + definition: + title: AWS S3 CDC Source + description: >- + Receive data from AWS SQS subscribed to Eventbridge Bus reporting events related to an S3 bucket or multiple buckets. + + Access Key/Secret Key are the basic method for authenticating to the AWS + SQS Service. + + To use this Kamelet you'll need to set up Eventbridge on your bucket and subscribe Eventbridge bus to an SQS Queue. + + For doing this you'll need to enable Evenbridge notification on your bucket and creating a rule on Eventbridge console related to all the events on S3 bucket and pointing to the SQS Queue specified as parameter in this Kamelet. + required: + - accessKey + - secretKey + - queueNameOrArn + - region + type: object + properties: + queueNameOrArn: + title: Queue Name + description: The SQS Queue Name or ARN + type: string + deleteAfterRead: + title: Auto-delete Messages + description: Delete messages after consuming them + type: boolean + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:checkbox' + default: true + accessKey: + title: Access Key + description: The access key obtained from AWS. + type: string + format: password + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:password' + - 'urn:camel:group:credentials' + - 'urn:keda:authentication:awsAccessKeyID' + - 'urn:keda:required' + secretKey: + title: Secret Key + description: The secret key obtained from AWS. + type: string + format: password + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:password' + - 'urn:camel:group:credentials' + - 'urn:keda:authentication:awsSecretAccessKey' + - 'urn:keda:required' + region: + title: AWS Region + description: The AWS region to access. + type: string + x-descriptors: + - 'urn:keda:metadata:awsRegion' + - 'urn:keda:required' + enum: + - ap-south-1 + - eu-south-1 + - us-gov-east-1 + - me-central-1 + - ca-central-1 + - eu-central-1 + - us-iso-west-1 + - us-west-1 + - us-west-2 + - af-south-1 + - eu-north-1 + - eu-west-3 + - eu-west-2 + - eu-west-1 + - ap-northeast-3 + - ap-northeast-2 + - ap-northeast-1 + - me-south-1 + - sa-east-1 + - ap-east-1 + - cn-north-1 + - us-gov-west-1 + - ap-southeast-1 + - ap-southeast-2 + - us-iso-east-1 + - ap-southeast-3 + - us-east-1 + - us-east-2 + - cn-northwest-1 + - us-isob-east-1 + - aws-global + - aws-cn-global + - aws-us-gov-global + - aws-iso-global + - aws-iso-b-global + autoCreateQueue: + title: Autocreate Queue + description: Setting the autocreation of the SQS queue. + type: boolean + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:checkbox' + default: false + amazonAWSHost: + title: AWS Host + description: The hostname of the Amazon AWS cloud. + type: string + default: amazonaws.com + protocol: + title: Protocol + description: The underlying protocol used to communicate with SQS + type: string + example: http or https + default: https + queueURL: + title: Queue URL + description: The full SQS Queue URL (required if using KEDA) + type: string + x-descriptors: + - 'urn:keda:metadata:queueURL' + - 'urn:keda:required' + uriEndpointOverride: + title: Overwrite Endpoint URI + description: >- + The overriding endpoint URI. To use this option, you must also select + the `overrideEndpoint` option. + type: string + overrideEndpoint: + title: Endpoint Overwrite + description: >- + Select this option to override the endpoint URI. To use this option, + you must also provide a URI for the `uriEndpointOverride` option. + type: boolean + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:checkbox' + default: false + delay: + title: Delay + description: The number of milliseconds before the next poll of the selected stream + type: integer + default: 500 + greedy: + title: Greedy Scheduler + description: >- + If greedy is enabled, then the polling will happen immediately again, + if the previous run polled 1 or more messages. + type: boolean + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:checkbox' + default: false + getObject: + title: Greedy Object in Bucket + description: >- + If getObject is enabled, then the file created in the bucket will be + get and returned as body, if not only the event will returned as body. + type: boolean + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:checkbox' + default: false + dependencies: + - 'camel:core' + - 'camel:aws2-sqs' + - 'camel:aws2-s3' + - 'camel:jsonpath' + - 'camel:kamelet' + - 'camel:jackson' + template: + from: + uri: 'aws2-sqs:{{queueNameOrArn}}' + parameters: + autoCreateQueue: '{{autoCreateQueue}}' + secretKey: '{{?secretKey}}' + accessKey: '{{?accessKey}}' + region: '{{region}}' + deleteAfterRead: '{{deleteAfterRead}}' + amazonAWSHost: '{{?amazonAWSHost}}' + protocol: '{{?protocol}}' + uriEndpointOverride: '{{?uriEndpointOverride}}' + overrideEndpoint: '{{overrideEndpoint}}' + delay: '{{delay}}' + greedy: '{{greedy}}' + steps: + - choice: + precondition: true + when: + - simple: '${properties:getObject:true}' + steps: + - unmarshal: + json: + library: Jackson + unmarshalType: com.fasterxml.jackson.databind.JsonNode + - set-property: + name: s3-event-name + jsonpath: $.detail.reason + - choice: + when: + - simple: '${exchangeProperty.s3-event-name} == "PutObject"' + steps: + - set-property: + name: aws-s3-name + jsonpath: $.detail.object.key + - set-property: + name: aws-s3-bucket + jsonpath: $.detail.bucket.name + - toD: >- + aws2-s3:${exchangeProperty.aws-s3-bucket}?accessKey={{accessKey}}&secretKey={{secretKey}}®ion={{region}}&operation=getObject&keyName=${exchangeProperty.aws-s3-name} + - to: 'kamelet:sink' diff --git a/karavan-designer/src/App.tsx b/karavan-designer/src/App.tsx index dcea7fc1..f23eb43d 100644 --- a/karavan-designer/src/App.tsx +++ b/karavan-designer/src/App.tsx @@ -70,7 +70,7 @@ class App extends React.Component<Props, State> { fetch("snippets/org.apache.camel.AggregationStrategy"), fetch("snippets/org.apache.camel.Processor"), // fetch("example/demo.camel.yaml") - fetch("example/postgresql-source.kamelet.yaml") + fetch("example/aws-s3-cdc-source.kamelet.yaml") // fetch("components/supported-components.json"), ]).then(responses => Promise.all(responses.map(response => response.text())) @@ -90,7 +90,7 @@ class App extends React.Component<Props, State> { if (data[4]) { // this.setState({yaml: data[4], name: "demo.camel.yaml"}) - this.setState({yaml: data[4], name: "postgresql-source.kamelet.yaml"}) + this.setState({yaml: data[4], name: "aws-s3-cdc-source.kamelet.yaml"}) } if (data[5]) { diff --git a/karavan-designer/src/designer/kamelet/KameletDefinitionPropertyCard.tsx b/karavan-designer/src/designer/kamelet/KameletDefinitionPropertyCard.tsx index d8933df4..47c85e18 100644 --- a/karavan-designer/src/designer/kamelet/KameletDefinitionPropertyCard.tsx +++ b/karavan-designer/src/designer/kamelet/KameletDefinitionPropertyCard.tsx @@ -19,10 +19,18 @@ import { Button, Card, CardBody, - CardTitle, Flex, FlexItem, - FormGroup, FormSelect, FormSelectOption, + CardTitle, + Flex, + FlexItem, + FormGroup, + FormSelect, + FormSelectOption, Grid, - GridItem, Label, Modal, Switch, + GridItem, + Label, + LabelGroup, + Modal, + Switch, TextInput, } from '@patternfly/react-core'; import '../karavan.css'; @@ -30,6 +38,8 @@ import './kamelet.css'; import {useIntegrationStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; import {DefinitionProperty} from "karavan-core/lib/model/IntegrationDefinition"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; +import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; interface Props { index: number @@ -45,7 +55,7 @@ export function KameletDefinitionPropertyCard(props: Props) { const key = props.propKey; const required = integration.spec.definition?.required || []; - function setPropertyValue(field: string, value: string) { + function setPropertyValue(field: string, value: any) { if (integration.spec.definition?.properties) { (integration.spec.definition?.properties as any)[key][field] = value; setIntegration(integration, true); @@ -83,8 +93,9 @@ export function KameletDefinitionPropertyCard(props: Props) { aria-label="FormSelect Input" ouiaId="BasicFormSelect" > - {['string', 'number', 'boolean'].map((option, index) => ( - <FormSelectOption key={option} isDisabled={false} id={key + field} name={key + field} value={option} label={option} /> + {['string', 'number', 'integer', 'boolean'].map((option, index) => ( + <FormSelectOption key={option} isDisabled={false} id={key + field} name={key + field} + value={option} label={option}/> ))} </FormSelect> </FormGroup> @@ -92,9 +103,106 @@ export function KameletDefinitionPropertyCard(props: Props) { ) } + function sortEnum(source: string, dest: string) { + const i = CamelUtil.cloneIntegration(integration); + if (i.spec.definition && integration.spec.definition?.properties[key]) { + const enums: string [] = i.spec.definition.properties[key].enum; + console.log(enums) + if (enums && Array.isArray(enums)) { + console.log("isArray") + const from = enums.findIndex(e => source); + const to = enums.findIndex(e => dest); + if (from > -1 && to > -1) { + console.log("exchange"); + [enums[from], enums[to]] = [enums[to], enums[from]]; + i.spec.definition.properties[key].enum = enums; + console.log("i.spec.definition.properties[key].enum", i.spec.definition.properties[key].enum); + setIntegration(i, true); + } + } + } + } + + function addEnum() { + const i = CamelUtil.cloneIntegration(integration); + if (i.spec.definition && integration.spec.definition?.properties[key]) { + let enums: string [] = i.spec.definition.properties[key].enum; + if (enums && Array.isArray(enums)) { + enums.push("enum") + } else { + enums = ['enum']; + } + i.spec.definition.properties[key].enum = enums; + setIntegration(i, true); + } + } + + function deleteEnum(val: string) { + const enumVal = getPropertyValue('enum'); + const i = CamelUtil.cloneIntegration(integration); + if (enumVal && Array.isArray(enumVal) && i.spec.definition) { + const enums: string[] = [...enumVal]; + setPropertyValue('enum', enums.filter(e => e !== val)); + } + } + + function renameEnum(index: number, newVal: string) { + const enumVal = getPropertyValue('enum'); + const i = CamelUtil.cloneIntegration(integration); + if (enumVal && Array.isArray(enumVal) && i.spec.definition) { + const enums: string[] = [...enumVal]; + enums[index] = newVal; + setPropertyValue('enum', enums); + } + } + + function getPropertyEnumField(field: string, label: string, isRequired: boolean, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + const enumVal = getPropertyValue(field); + return ( + <GridItem span={span}> + <FormGroup fieldId={key + field} isRequired={isRequired}> + <LabelGroup + categoryName={label} + numLabels={enumVal?.length || 0} + isEditable + addLabelControl={ + <Button variant="link" icon={<AddIcon/>} onClick={event => addEnum()}> + Add + </Button> + } + > + {enumVal && enumVal.map((val: string, index: number) => ( + <Label + key={val} + id={val} + color="blue" + isEditable + onClose={() => deleteEnum(val)} + onEditCancel={(_event, prevText) => {}} + onEditComplete={(event, newText) => { + if (event.type === 'mousedown') { + renameEnum(index, val) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Tab') { + renameEnum(index, newText) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Enter') { + renameEnum(index, newText) + } else { + renameEnum(index, val) + } + }} + > + {val} + </Label> + ))} + </LabelGroup> + </FormGroup> + </GridItem> + ) + } + function renameProperty(newKey: string) { const oldKey = key; - newKey = newKey.replace(/[\W_]+/g,''); + newKey = newKey.replace(/[\W_]+/g, ''); if (oldKey !== newKey) { if (integration.spec.definition?.properties) { const o = (integration.spec.definition?.properties as any) @@ -138,7 +246,6 @@ export function KameletDefinitionPropertyCard(props: Props) { } function setRequired(checked: boolean) { - console.log(required, key) const newRequired = [...required]; if (checked && !newRequired.includes(key)) { newRequired.push(key); @@ -146,7 +253,6 @@ export function KameletDefinitionPropertyCard(props: Props) { const index = newRequired.findIndex(r => r === key); newRequired.splice(index, 1); } - // console.log(newRequired) if (integration.spec.definition?.required) { integration.spec.definition.required.length = 0; integration.spec.definition.required.push(...newRequired) @@ -212,6 +318,7 @@ export function KameletDefinitionPropertyCard(props: Props) { {getPropertyField("format", "Format", false, 3)} {getPropertyField("example", "Example", false, 6)} {getPropertyField("default", "Default", false, 3)} + {getPropertyValue('type') === 'string' && getPropertyEnumField("enum", "Enum", true, 12)} {/*{getPropertyField("x-descriptors", "Descriptors", false, 12)}*/} </Grid> </CardBody> diff --git a/karavan-designer/src/designer/kamelet/KameletDefinitionsPanel.tsx b/karavan-designer/src/designer/kamelet/KameletDefinitionsPanel.tsx index 009a54ac..0eb3f521 100644 --- a/karavan-designer/src/designer/kamelet/KameletDefinitionsPanel.tsx +++ b/karavan-designer/src/designer/kamelet/KameletDefinitionsPanel.tsx @@ -19,7 +19,9 @@ import { Button, Card, CardBody, - CardTitle, Flex, FlexItem, + CardTitle, + Flex, + FlexItem, Form, FormGroup, Grid, @@ -32,6 +34,9 @@ import {useIntegrationStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; import {KameletDefinitionPropertyCard} from "./KameletDefinitionPropertyCard"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; +import {DefinitionProperty} from "karavan-core/lib/model/IntegrationDefinition"; +import {Simulate} from "react-dom/test-utils"; export function KameletDefinitionsPanel() { @@ -66,6 +71,30 @@ export function KameletDefinitionsPanel() { } const properties = integration.spec.definition?.properties ? Object.keys(integration.spec.definition?.properties) : []; + + function addNewProperty() { + const i = CamelUtil.cloneIntegration(integration); + if (i.spec.definition && integration.spec.definition?.properties) { + const propertyName = generatePropertyName(); + i.spec.definition.properties = Object.assign({[propertyName]: new DefinitionProperty()}, integration.spec.definition.properties); + setIntegration(i, true); + } + } + + function generatePropertyName(count: number = 0): string { + const prefix = 'property'; + const propName = 'property' + count; + if (integration.spec.definition?.properties) { + const keys = Object.keys(integration.spec.definition?.properties); + if (keys.includes(propName)) { + return generatePropertyName(count + 1); + } else { + return propName; + } + } + return prefix; + } + return ( <> <Card isCompact ouiaId="DefinitionsCard"> @@ -86,7 +115,9 @@ export function KameletDefinitionsPanel() { <Flex> <FlexItem>Properties</FlexItem> <FlexItem align={{default: "alignRight"}}> - <Button variant={"link"} icon={<AddIcon/>}>Add property</Button> + <Button variant={"link"} icon={<AddIcon/>} onClick={event => addNewProperty()}> + Add property + </Button> </FlexItem> </Flex> </CardTitle> diff --git a/karavan-designer/src/designer/kamelet/kamelet.css b/karavan-designer/src/designer/kamelet/kamelet.css index bbdf28a9..c72acad2 100644 --- a/karavan-designer/src/designer/kamelet/kamelet.css +++ b/karavan-designer/src/designer/kamelet/kamelet.css @@ -23,6 +23,7 @@ padding-bottom: 106px; } +.karavan .kamelet-designer .pf-v5-c-drawer__content, .karavan .kamelet-designer .main { background-color: var(--pf-v5-global--BackgroundColor--light-300); }