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 cdde30d412b7aed8477f5902167cda3ef744da2e Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Tue Oct 3 20:06:52 2023 -0400 Kamelets Dependencies UI #315 --- .../src/core/model/IntegrationDefinition.ts | 21 ++- karavan-core/test/kamelet.spec.ts | 3 +- .../public/example/aws-s3-source.kamelet.yaml | 190 +++++++++++++++++++ .../src/designer/kamelet/KameletTypesOutCard.tsx | 113 +++++++++++ .../src/main/resources/application.properties | 2 +- .../kamelet/KameletDefinitionPropertyCard.tsx | 125 +++++++++++- .../designer/kamelet/KameletDefinitionsPanel.tsx | 59 +++++- .../designer/kamelet/KameletDependenciesCard.tsx | 113 +++++++++++ .../src/designer/kamelet/KameletProperties.tsx | 209 +-------------------- .../src/designer/kamelet/KameletTypesOutCard.tsx | 113 +++++++++++ .../main/webui/src/designer/kamelet/kamelet.css | 1 + 11 files changed, 723 insertions(+), 226 deletions(-) diff --git a/karavan-core/src/core/model/IntegrationDefinition.ts b/karavan-core/src/core/model/IntegrationDefinition.ts index 974f8e5e..fb1c11f3 100644 --- a/karavan-core/src/core/model/IntegrationDefinition.ts +++ b/karavan-core/src/core/model/IntegrationDefinition.ts @@ -32,6 +32,23 @@ export class DefinitionProperty { } } +export class MediaType { + mediaType: string = ''; + + public constructor(init?: Partial<MediaType>) { + Object.assign(this, init); + } +} + +export class Types { + in?: MediaType = new MediaType(); + out?: MediaType = new MediaType(); + + public constructor(init?: Partial<Types>) { + Object.assign(this, init); + } +} + export class Definition { title: string = ''; description: string = ''; @@ -46,7 +63,7 @@ export class Definition { export class Spec { definition?: Definition; - types?: any; + types?: Types; flows?: any[] = []; template?: any; dependencies?: string[]; @@ -107,7 +124,7 @@ export class Integration { if (type === 'kamelet') { i.metadata.annotations = new MetadataAnnotations({}) i.spec.definition = new Definition({}) - i.spec.types = {} + i.spec.types = new Types(); } return i; diff --git a/karavan-core/test/kamelet.spec.ts b/karavan-core/test/kamelet.spec.ts index 65e94987..750d9295 100644 --- a/karavan-core/test/kamelet.spec.ts +++ b/karavan-core/test/kamelet.spec.ts @@ -28,7 +28,6 @@ describe('Kamelet <=> YAML', () => { it('Yaml to Kamelet', () => { const yaml = fs.readFileSync('test/postgresql-source.kamelet.yaml',{encoding:'utf8', flag:'r'}); const i = CamelDefinitionYaml.yamlToIntegration("postgresql-source.kamelet.yaml", yaml); - // console.log(i) }); it('Kamelet to YAML with beans', () => { @@ -44,6 +43,8 @@ describe('Kamelet <=> YAML', () => { i.spec.flows?.push(b); const a = new MetadataAnnotations({"camel.apache.org/kamelet.group" : "hello world"}) i.metadata.annotations = a + + // console.log(CamelDefinitionYaml.integrationToYaml(i)) }); it('Kamelet to YAML without beans', () => { diff --git a/karavan-designer/public/example/aws-s3-source.kamelet.yaml b/karavan-designer/public/example/aws-s3-source.kamelet.yaml new file mode 100644 index 00000000..e20d11d7 --- /dev/null +++ b/karavan-designer/public/example/aws-s3-source.kamelet.yaml @@ -0,0 +1,190 @@ +apiVersion: camel.apache.org/v1 +kind: Kamelet +metadata: + name: aws-s3-source + annotations: + camel.apache.org/kamelet.support.level: "Stable" + camel.apache.org/catalog.version: "4.1.0-SNAPSHOT" + camel.apache.org/kamelet.icon: "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAyNDguMiAzMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxzdHlsZT4uc3QyOHtmaWxsOiM4YzMxMjN9LnN0Mjl7ZmlsbDojZTA1MjQzfTwvc3R5bGU+PHBhdGggY2xhc3M9InN0MjgiIGQ9Ik0yMCA1Mi4xTDAgNjJ2MTc1LjVsMjAgOS45LjEtLjFWNTIuMmwtLjEtLjEiLz48cGF0aCBjbGFzcz0ic3QyOSIgZD0iTTEyNyAyMjJMMjAgMjQ3LjVWNTIuMUwxMjcgNzd2MTQ1Ii8+PHBhdGggY2xhc3M9InN0MjgiIG [...] + camel.apache.org/provider: "Apache Software Foundation" + camel.apache.org/kamelet.group: "AWS S3" + camel.apache.org/kamelet.namespace: "AWS" + labels: + camel.apache.org/kamelet.type: "source" +spec: + definition: + title: "AWS S3 Source" + description: |- + Receive data from an Amazon S3 Bucket. + + The basic authentication method for the S3 service is to specify an access key and a secret key. These parameters are optional because the Kamelet provides a default credentials provider. + + If you use the default credentials provider, the S3 client loads the credentials through this provider and doesn't use the basic authentication method. + + Two headers will be duplicated with different names for clarity at sink level, CamelAwsS3Key will be duplicated into aws.s3.key and CamelAwsS3BucketName will be duplicated in aws.s3.bucket.name + required: + - bucketNameOrArn + - region + type: object + properties: + bucketNameOrArn: + title: Bucket Name + description: The S3 Bucket name or Amazon Resource Name (ARN). + type: string + deleteAfterRead: + title: Auto-delete Objects + description: Specifies to delete objects 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 + 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 + region: + title: AWS Region + description: The AWS region to access. + type: string + 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", "a [...] + autoCreateBucket: + title: Autocreate Bucket + description: Specifies to automatically create the S3 bucket. + type: boolean + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:checkbox' + default: false + prefix: + title: Prefix + description: The AWS S3 bucket prefix to consider while searching. + type: string + example: 'folder/' + ignoreBody: + title: Ignore Body + description: If true, the S3 Object body is ignored. Setting this to true overrides any behavior defined by the `includeBody` option. If false, the S3 object is put in the body. + type: boolean + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:checkbox' + default: false + useDefaultCredentialsProvider: + title: Default Credentials Provider + description: If true, the S3 client loads credentials through a default credentials provider. If false, it uses the basic authentication method (access key and secret key). + type: boolean + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:checkbox' + default: false + 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 bucket. + type: integer + default: 500 + dataTypes: + out: + default: binary + headers: + CamelAwsS3BucketName: + title: S3 Bucket Name + description: The bucket name which has been used to retrieve objects + type: string + CamelAwsS3Key: + title: S3 Key + description: The key under which the retrieved object is stored + type: string + CamelAwsS3ContentType: + title: Content Type + description: The content type of the retrieved object. + default: application/octet-stream + type: string + CamelAwsS3ETag: + title: ETag Value + description: |- + The hex encoded 128-bit MD5 digest of the associated object according to RFC 1864. + This data is used as an integrity check to verify that the data received by the caller is the same data that was sent by Amazon S3. + type: string + types: + binary: + format: "application-octet-stream" + description: Default binary representation of the S3 object retrieved from the bucket. + mediaType: application/octet-stream + cloudevents: + format: "aws2-s3:application-cloudevents" + description: |- + Output data type represents AWS S3 get object response as CloudEvent V1. The data type sets Camel specific CloudEvent headers on the exchange with + respective data from the S3 bucket and its derived object. + headers: + CamelCloudEventID: + title: CloudEvent ID + description: The Camel exchange id set as event id + type: string + CamelCloudEventType: + title: CloudEvent Type + description: The event type + default: "org.apache.camel.event.aws.s3.getObject" + type: string + CamelCloudEventSource: + title: CloudEvent Source + description: The event source. By default, the S3 bucket name with prefix "aws.s3.bucket.". + type: string + CamelCloudEventSubject: + title: CloudEvent Subject + description: The event subject. Usually the S3 key. + type: string + CamelCloudEventTime: + title: CloudEvent Time + description: The exchange creation timestamp as event time. + type: string + dependencies: + - "camel:core" + - "camel:aws2-s3" + - "mvn:org.apache.camel.kamelets:camel-kamelets-utils:4.1.0-SNAPSHOT" + - "camel:kamelet" + template: + beans: + - name: renameHeaders + type: "#class:org.apache.camel.kamelets.utils.headers.DuplicateNamingHeaders" + properties: + prefix: 'CamelAwsS3' + renamingPrefix: 'aws.s3.' + mode: 'filtering' + selectedHeaders: 'CamelAwsS3Key,CamelAwsS3BucketName' + from: + uri: "aws2-s3:{{bucketNameOrArn}}" + parameters: + autoCreateBucket: "{{autoCreateBucket}}" + secretKey: "{{?secretKey}}" + accessKey: "{{?accessKey}}" + region: "{{region}}" + ignoreBody: "{{ignoreBody}}" + deleteAfterRead: "{{deleteAfterRead}}" + prefix: "{{?prefix}}" + useDefaultCredentialsProvider: "{{useDefaultCredentialsProvider}}" + uriEndpointOverride: "{{?uriEndpointOverride}}" + overrideEndpoint: "{{overrideEndpoint}}" + delay: "{{delay}}" + steps: + - process: + ref: "{{renameHeaders}}" + - to: "kamelet:sink" \ No newline at end of file diff --git a/karavan-designer/src/designer/kamelet/KameletTypesOutCard.tsx b/karavan-designer/src/designer/kamelet/KameletTypesOutCard.tsx new file mode 100644 index 00000000..76b590ee --- /dev/null +++ b/karavan-designer/src/designer/kamelet/KameletTypesOutCard.tsx @@ -0,0 +1,113 @@ +/* + * 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 { + Button, + Card, + CardBody, + CardTitle, + FormGroup, FormHelperText, HelperText, HelperTextItem, + Label, + LabelGroup, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './kamelet.css'; +import {useIntegrationStore} from "../DesignerStore"; +import {shallow} from "zustand/shallow"; +import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; + +export function KameletTypesOutCard() { + + const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) + + const dependencies: string[] = [...(integration.spec.dependencies || [])]; + + + function setDependencies(deps: string[]) { + const i = CamelUtil.cloneIntegration(integration); + i.spec.dependencies = deps; + setIntegration(i, true); + } + + function addDepencency() { + dependencies.push("dependency") + setDependencies(dependencies); + } + + function deleteDependency(val: string) { + setDependencies(dependencies.filter(e => e !== val)); + } + + function renameDependency(index: number, newVal: string) { + dependencies[index] = newVal; + setDependencies(dependencies); + } + + return ( + <Card isClickable isCompact isFlat ouiaId="PropertyCard" className="property-card"> + <CardTitle> + Dependencies + </CardTitle> + <CardBody> + <FormHelperText> + <HelperText> + <HelperTextItem>Dependencies required, ex: camel:component or mvn:groupId:artifactId:version</HelperTextItem> + </HelperText> + </FormHelperText> + </CardBody> + <CardBody> + <FormGroup fieldId={'dependencies'}> + <LabelGroup + // categoryName={"Dependencies"} + numLabels={dependencies.length} + isEditable + addLabelControl={ + <Button variant="link" icon={<AddIcon/>} onClick={event => addDepencency()}> + Add + </Button> + } + > + {dependencies.map((val: string, index: number) => ( + <Label + key={val} + id={val} + color="grey" + isEditable + onClose={() => deleteDependency(val)} + onEditCancel={(_event, prevText) => {}} + onEditComplete={(event, newText) => { + if (event.type === 'mousedown') { + renameDependency(index, val) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Tab') { + renameDependency(index, newText) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Enter') { + renameDependency(index, newText) + } else { + renameDependency(index, val) + } + }} + > + {val} + </Label> + ))} + </LabelGroup> + </FormGroup> + </CardBody> + </Card> + ) +} diff --git a/karavan-web/karavan-app/src/main/resources/application.properties b/karavan-web/karavan-app/src/main/resources/application.properties index 67990ecb..13a50355 100644 --- a/karavan-web/karavan-app/src/main/resources/application.properties +++ b/karavan-web/karavan-app/src/main/resources/application.properties @@ -121,7 +121,7 @@ quarkus.quinoa.enable-spa-routing=true quarkus.http.enable-compression=true quarkus.log.level=INFO quarkus.banner.enabled=false -quarkus.package.type=uber-jar +quarkus.package.type=fast-jar quarkus.docker.dockerfile-jvm-path=src/main/docker/Dockerfile.jdk #quarkus.docker.dockerfile-jvm-path=src/main/docker/Dockerfile.minimal quarkus.container-image.builder=docker diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDefinitionPropertyCard.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDefinitionPropertyCard.tsx index d8933df4..cf42ef6c 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDefinitionPropertyCard.tsx +++ b/karavan-web/karavan-app/src/main/webui/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="grey" + 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-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDefinitionsPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDefinitionsPanel.tsx index 009a54ac..0d3f04a4 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDefinitionsPanel.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDefinitionsPanel.tsx @@ -19,11 +19,13 @@ import { Button, Card, CardBody, - CardTitle, Flex, FlexItem, + CardTitle, + Flex, + FlexItem, Form, FormGroup, Grid, - GridItem, + GridItem, TextArea, TextInput, } from '@patternfly/react-core'; import '../karavan.css'; @@ -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 {KameletDependenciesCard} from "./KameletDependenciesCard"; export function KameletDefinitionsPanel() { @@ -53,7 +58,7 @@ export function KameletDefinitionsPanel() { } } - function getElement(key: string, label: string, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + function getElementTextInput(key: string, label: string, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { return ( <GridItem span={span}> <FormGroup label={label} fieldId={key} isRequired> @@ -65,7 +70,43 @@ export function KameletDefinitionsPanel() { ) } + function getElementTextArea(key: string, label: string, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + return ( + <GridItem span={span}> + <FormGroup label={label} fieldId={key} isRequired> + <TextArea type="text" id={key} name={key} autoResize + onChange={(_, value) => setValue(key, value)} + value={getValue(key)}/> + </FormGroup> + </GridItem> + ) + } + 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"> @@ -73,9 +114,9 @@ export function KameletDefinitionsPanel() { <CardBody> <Form> <Grid hasGutter> - {getElement('title', 'Title', 4)} - {getElement('description', 'Description', 6)} - {getElement('type', 'Type', 2)} + {getElementTextInput('title', 'Title', 3)} + {getElementTextArea('description', 'Description', 9)} + {/*{getElementTextInput('type', 'Type', 2)}*/} </Grid> </Form> </CardBody> @@ -86,7 +127,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> @@ -102,6 +145,8 @@ export function KameletDefinitionsPanel() { </Form> </CardBody> </Card> + <div style={{height: "20px"}}/> + <KameletDependenciesCard/> </> ) diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDependenciesCard.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDependenciesCard.tsx new file mode 100644 index 00000000..893c897b --- /dev/null +++ b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletDependenciesCard.tsx @@ -0,0 +1,113 @@ +/* + * 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 { + Button, + Card, + CardBody, + CardTitle, + FormGroup, FormHelperText, HelperText, HelperTextItem, + Label, + LabelGroup, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './kamelet.css'; +import {useIntegrationStore} from "../DesignerStore"; +import {shallow} from "zustand/shallow"; +import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; + +export function KameletDependenciesCard() { + + const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) + + const dependencies: string[] = [...(integration.spec.dependencies || [])]; + + + function setDependencies(deps: string[]) { + const i = CamelUtil.cloneIntegration(integration); + i.spec.dependencies = deps; + setIntegration(i, true); + } + + function addDepencency() { + dependencies.push("dependency") + setDependencies(dependencies); + } + + function deleteDependency(val: string) { + setDependencies(dependencies.filter(e => e !== val)); + } + + function renameDependency(index: number, newVal: string) { + dependencies[index] = newVal; + setDependencies(dependencies); + } + + return ( + <Card isClickable isCompact isFlat ouiaId="PropertyCard" className="property-card"> + <CardTitle> + Dependencies + </CardTitle> + <CardBody> + <FormHelperText> + <HelperText> + <HelperTextItem>Dependencies required, ex: camel:component or mvn:groupId:artifactId:version</HelperTextItem> + </HelperText> + </FormHelperText> + </CardBody> + <CardBody> + <FormGroup fieldId={'dependencies'}> + <LabelGroup + // categoryName={"Dependencies"} + numLabels={dependencies.length} + isEditable + addLabelControl={ + <Button variant="link" icon={<AddIcon/>} onClick={event => addDepencency()}> + Add + </Button> + } + > + {dependencies.map((val: string, index: number) => ( + <Label + key={val} + id={val} + color="grey" + isEditable + onClose={() => deleteDependency(val)} + onEditCancel={(_event, prevText) => {}} + onEditComplete={(event, newText) => { + if (event.type === 'mousedown') { + renameDependency(index, val) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Tab') { + renameDependency(index, newText) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Enter') { + renameDependency(index, newText) + } else { + renameDependency(index, val) + } + }} + > + {val} + </Label> + ))} + </LabelGroup> + </FormGroup> + </CardBody> + </Card> + ) +} diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletProperties.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletProperties.tsx index fc8f188f..e5a8cfc5 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletProperties.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletProperties.tsx @@ -14,11 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, {useEffect, useState} from 'react'; +import React from 'react'; import { Form, - FormGroup, - TextInput, Button, Title, Tooltip, Popover, InputGroup, InputGroupItem, } from '@patternfly/react-core'; import '../karavan.css'; import "@patternfly/patternfly/patternfly.css"; @@ -26,21 +24,6 @@ import { RegistryBeanDefinition, } from "karavan-core/lib/model/CamelDefinition"; import {Integration} from "karavan-core/lib/model/IntegrationDefinition"; -import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; -import {SensitiveKeys} from "karavan-core/lib/model/CamelMetadata"; -import {v4 as uuidv4} from "uuid"; -import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon"; -import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; -import CloneIcon from '@patternfly/react-icons/dist/esm/icons/clone-icon' -import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; -import {InfrastructureSelector} from "../route/property/InfrastructureSelector"; -import KubernetesIcon from "@patternfly/react-icons/dist/js/icons/openshift-icon"; -import {InfrastructureAPI} from "../utils/InfrastructureAPI"; -import ShowIcon from "@patternfly/react-icons/dist/js/icons/eye-icon"; -import HideIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon"; -import DockerIcon from "@patternfly/react-icons/dist/js/icons/docker-icon"; -import {useDesignerStore} from "../DesignerStore"; -import {shallow} from "zustand/shallow"; import {IntegrationHeader} from "../utils/IntegrationHeader"; @@ -53,197 +36,11 @@ interface Props { export function KameletProperties (props: Props) { - const [selectedStep] = useDesignerStore((s) => [s.selectedStep], shallow); - const [infrastructureSelector, setInfrastructureSelector] = useState<boolean>(false); - const [infrastructureSelectorProperty, setInfrastructureSelectorProperty] = useState<string | undefined>(undefined); - const [infrastructureSelectorUuid, setInfrastructureSelectorUuid] = useState<string | undefined>(undefined); - const [properties, setProperties] = useState<Map<string, [string, string, boolean]>>(new Map<string, [string, string, boolean]>()); - - useEffect(()=> { - setProperties(preparePropertiesMap((selectedStep as RegistryBeanDefinition)?.properties)) - }, [selectedStep?.uuid]) - - function preparePropertiesMap (properties: any): Map<string, [string, string, boolean]> { - const result = new Map<string, [string, string, boolean]>(); - if (properties) { - Object.keys(properties).forEach((k, i, a) => result.set(uuidv4(), [k, properties[k], false])); - } - return result; - } - - function onBeanPropertyUpdate () { - if (selectedStep) { - const bean = CamelUtil.cloneBean(selectedStep); - const beanProperties: any = {}; - properties.forEach((p: any) => beanProperties[p[0]] = p[1]); - bean.properties = beanProperties; - props.onChange(bean); - } - } - - function beanFieldChanged (fieldId: string, value: string) { - if (selectedStep) { - const bean = CamelUtil.cloneBean(selectedStep); - (bean as any)[fieldId] = value; - props.onChange(bean); - } - } - - function propertyChanged (uuid: string, key: string, value: string, showPassword: boolean) { - setProperties(prevState => { - prevState.set(uuid, [key, value, showPassword]); - return prevState; - }); - onBeanPropertyUpdate(); - } - - function propertyDeleted (uuid: string) { - setProperties(prevState => { - prevState.delete(uuid); - return prevState; - }) - onBeanPropertyUpdate(); - } - - function selectInfrastructure (value: string) { - const propertyId = infrastructureSelectorProperty; - const uuid = infrastructureSelectorUuid; - if (propertyId && uuid){ - if (value.startsWith("config") || value.startsWith("secret")) value = "{{" + value + "}}"; - propertyChanged(uuid, propertyId, value, false); - setInfrastructureSelector(false); - setInfrastructureSelectorProperty(undefined); - } - } - - function openInfrastructureSelector (uuid: string, propertyName: string) { - setInfrastructureSelector(true); - setInfrastructureSelectorProperty(propertyName); - setInfrastructureSelectorUuid(uuid); - } - - function closeInfrastructureSelector () { - setInfrastructureSelector(false); - } - - function getInfrastructureSelectorModal() { - return ( - <InfrastructureSelector - dark={false} - isOpen={infrastructureSelector} - onClose={() => closeInfrastructureSelector()} - onSelect={selectInfrastructure}/>) - } - - function cloneBean () { - if (selectedStep) { - const bean = CamelUtil.cloneBean(selectedStep); - bean.uuid = uuidv4(); - props.onClone(bean); - } - } - - function getLabelIcon (displayName: string, description: string) { - return ( - <Popover - position={"left"} - headerContent={displayName} - bodyContent={description} - footerContent={ - <div> - <b>Required</b> - </div> - }> - <button type="button" aria-label="More info" onClick={e => { - e.preventDefault(); - e.stopPropagation(); - }} className="pf-v5-c-form__group-label-help"> - <HelpIcon /> - </button> - </Popover> - ) - } - function getBeanForm() { - const bean = (selectedStep as RegistryBeanDefinition); - return ( - <> - <div className="headers"> - <div className="top"> - <Title headingLevel="h1" size="md">Bean</Title> - <Tooltip content="Clone bean" position="bottom"> - <Button variant="link" onClick={() => cloneBean()} icon={<CloneIcon/>}/> - </Tooltip> - </div> - </div> - <FormGroup label="Name" fieldId="name" isRequired labelIcon={getLabelIcon("Name", "Bean name used as a reference ex: myBean")}> - <TextInput className="text-field" isRequired type="text" id="name" name="name" value={bean?.name} - onChange={(_, value)=> beanFieldChanged("name", value)}/> - </FormGroup> - <FormGroup label="Type" fieldId="type" isRequired labelIcon={getLabelIcon("Type", "Bean class Canonical Name ex: org.demo.MyBean")}> - <TextInput className="text-field" isRequired type="text" id="type" name="type" value={bean?.type} - onChange={(_, value) => beanFieldChanged("type", value)}/> - </FormGroup> - <FormGroup label="Properties" fieldId="properties" className="bean-properties"> - {Array.from(properties.entries()).map((v, index, array) => { - const i = v[0]; - const key = v[1][0]; - const value = v[1][1]; - const showPassword = v[1][2]; - const isSecret = key !== undefined && SensitiveKeys.includes(key.toLowerCase()); - const inInfrastructure = InfrastructureAPI.infrastructure !== 'local'; - const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? <KubernetesIcon/> : <DockerIcon/> - return ( - <div key={"key-" + i} className="bean-property"> - <TextInput placeholder="Bean Field Name" className="text-field" isRequired type="text" id={"key-" + i} - name={"key-" + i} value={key} - onChange={(_, beanFieldName) => { - propertyChanged(i, beanFieldName, value, showPassword) - }}/> - <InputGroup> - {inInfrastructure && - <Tooltip position="bottom-end" content="Select value from Infrastructure"> - <Button variant="control" onClick={e => openInfrastructureSelector(i, key)}> - {icon} - </Button> - </Tooltip>} - <InputGroupItem isFill> - <TextInput - placeholder="Bean Field Value" - type={isSecret && !showPassword ? "password" : "text"} - className="text-field" - isRequired - id={"value-" + i} - name={"value-" + i} - value={value} - onChange={(_, value) => { - propertyChanged(i, key, value, showPassword) - }}/> - </InputGroupItem> - {isSecret && <Tooltip position="bottom-end" content={showPassword ? "Hide" : "Show"}> - <Button variant="control" onClick={e => propertyChanged(i, key, value, !showPassword)}> - {showPassword ? <ShowIcon/> : <HideIcon/>} - </Button> - </Tooltip>} - </InputGroup> - <Button variant="link" className="delete-button" onClick={e => propertyDeleted(i)}><DeleteIcon/></Button> - </div> - ) - })} - <Button variant="link" className="add-button" onClick={e => propertyChanged(uuidv4(), '', '', false)}> - <AddIcon/>Add property</Button> - </FormGroup> - </> - ) - } - - const bean = (selectedStep as RegistryBeanDefinition); return ( - <div className='properties' key={bean ? bean.uuid : 'integration'}> + <div className='properties' key={'integration'}> <Form autoComplete="off" onSubmit={event => event.preventDefault()}> - {bean === undefined && <IntegrationHeader/>} - {bean !== undefined && getBeanForm()} + <IntegrationHeader/> </Form> - {getInfrastructureSelectorModal()} </div> ) } diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletTypesOutCard.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletTypesOutCard.tsx new file mode 100644 index 00000000..76b590ee --- /dev/null +++ b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/KameletTypesOutCard.tsx @@ -0,0 +1,113 @@ +/* + * 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 { + Button, + Card, + CardBody, + CardTitle, + FormGroup, FormHelperText, HelperText, HelperTextItem, + Label, + LabelGroup, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './kamelet.css'; +import {useIntegrationStore} from "../DesignerStore"; +import {shallow} from "zustand/shallow"; +import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; + +export function KameletTypesOutCard() { + + const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) + + const dependencies: string[] = [...(integration.spec.dependencies || [])]; + + + function setDependencies(deps: string[]) { + const i = CamelUtil.cloneIntegration(integration); + i.spec.dependencies = deps; + setIntegration(i, true); + } + + function addDepencency() { + dependencies.push("dependency") + setDependencies(dependencies); + } + + function deleteDependency(val: string) { + setDependencies(dependencies.filter(e => e !== val)); + } + + function renameDependency(index: number, newVal: string) { + dependencies[index] = newVal; + setDependencies(dependencies); + } + + return ( + <Card isClickable isCompact isFlat ouiaId="PropertyCard" className="property-card"> + <CardTitle> + Dependencies + </CardTitle> + <CardBody> + <FormHelperText> + <HelperText> + <HelperTextItem>Dependencies required, ex: camel:component or mvn:groupId:artifactId:version</HelperTextItem> + </HelperText> + </FormHelperText> + </CardBody> + <CardBody> + <FormGroup fieldId={'dependencies'}> + <LabelGroup + // categoryName={"Dependencies"} + numLabels={dependencies.length} + isEditable + addLabelControl={ + <Button variant="link" icon={<AddIcon/>} onClick={event => addDepencency()}> + Add + </Button> + } + > + {dependencies.map((val: string, index: number) => ( + <Label + key={val} + id={val} + color="grey" + isEditable + onClose={() => deleteDependency(val)} + onEditCancel={(_event, prevText) => {}} + onEditComplete={(event, newText) => { + if (event.type === 'mousedown') { + renameDependency(index, val) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Tab') { + renameDependency(index, newText) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Enter') { + renameDependency(index, newText) + } else { + renameDependency(index, val) + } + }} + > + {val} + </Label> + ))} + </LabelGroup> + </FormGroup> + </CardBody> + </Card> + ) +} diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/kamelet.css b/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/kamelet.css index bbdf28a9..c72acad2 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/kamelet/kamelet.css +++ b/karavan-web/karavan-app/src/main/webui/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); }