From 1a7c7341cd7b31a427a4aa8cc939a06d76a2b2d0 Mon Sep 17 00:00:00 2001 From: Tomohisa Igarashi Date: Mon, 2 Oct 2023 08:16:52 -0400 Subject: [PATCH] feat: Configure KameletBinding & Pipe properties Fixes: #149 --- .../flows/kamelet-binding.test.ts | 153 +++++++++++++++++- .../visualization/flows/kamelet-binding.ts | 66 +++++--- .../flows/kamelet-schema.service.ts | 59 +++++++ .../models/visualization/flows/pipe.test.ts | 29 ++++ .../visualization/visualization-node.ts | 1 + packages/ui/src/stubs/kamelet-binding.ts | 79 +++++++++ 6 files changed, 355 insertions(+), 32 deletions(-) create mode 100644 packages/ui/src/models/visualization/flows/kamelet-schema.service.ts create mode 100644 packages/ui/src/models/visualization/flows/pipe.test.ts create mode 100644 packages/ui/src/stubs/kamelet-binding.ts diff --git a/packages/ui/src/models/visualization/flows/kamelet-binding.test.ts b/packages/ui/src/models/visualization/flows/kamelet-binding.test.ts index 56adad8e2..ebae4b08c 100644 --- a/packages/ui/src/models/visualization/flows/kamelet-binding.test.ts +++ b/packages/ui/src/models/visualization/flows/kamelet-binding.test.ts @@ -1,23 +1,160 @@ +import { KameletBinding as KameletBindingModel } from '@kaoto-next/camel-catalog/types'; +import { JSONSchemaType } from 'ajv'; +import { kameletBindingJson } from '../../../stubs/kamelet-binding'; import { EntityType } from '../../camel-entities/base-entity'; import { KameletBinding } from './kamelet-binding'; +import { KameletSchemaService } from './kamelet-schema.service.ts'; describe('Kamelet Binding', () => { let kameletBinding: KameletBinding; beforeEach(() => { - kameletBinding = new KameletBinding(); + kameletBinding = new KameletBinding(JSON.parse(JSON.stringify(kameletBindingJson))); }); - it('should have an uuid', () => { - expect(kameletBinding.id).toBeDefined(); - expect(typeof kameletBinding.id).toBe('string'); + describe('id', () => { + it('should have an uuid', () => { + expect(kameletBinding.id).toBeDefined(); + expect(typeof kameletBinding.id).toBe('string'); + }); + + it('should have a type', () => { + expect(kameletBinding.type).toEqual(EntityType.KameletBinding); + }); + + it('should return the id', () => { + expect(kameletBinding.getId()).toEqual(expect.any(String)); + }); + }); + + describe('getComponentSchema', () => { + it('should return undefined if no path is provided', () => { + expect(kameletBinding.getComponentSchema()).toBeUndefined(); + }); + + it('should return undefined if no component model is found', () => { + const result = kameletBinding.getComponentSchema('test'); + + expect(result).toBeUndefined(); + }); + + it('should return the component schema', () => { + const spy = jest.spyOn(KameletSchemaService, 'getVisualComponentSchema'); + spy.mockReturnValueOnce({ + title: 'test', + schema: {} as JSONSchemaType, + definition: {}, + }); + + kameletBinding.getComponentSchema('source'); + expect(spy).toBeCalledTimes(1); + }); + }); + + it('should return the json', () => { + expect(kameletBinding.toJSON()).toEqual(kameletBindingJson); + }); + + describe('updateModel', () => { + it('should not update the model if no path is provided', () => { + const originalObject = JSON.parse(JSON.stringify(kameletBindingJson)); + + kameletBinding.updateModel(undefined, undefined); + + expect(originalObject).toEqual(kameletBindingJson); + }); + + it('should update the model', () => { + const name = 'timer-source'; + + kameletBinding.updateModel('source.ref.name', name); + + expect(kameletBinding.route.spec?.source?.ref?.name).toEqual(name); + }); }); - it('should have a type', () => { - expect(kameletBinding.type).toEqual(EntityType.KameletBinding); + describe('getSteps', () => { + it('should return an empty array if there is no route', () => { + const route = new KameletBinding(); + + expect(route.getSteps()).toEqual([]); + }); + + it('should return an empty array if there is no steps', () => { + const route = new KameletBinding({ spec: {} } as KameletBindingModel); + + expect(route.getSteps()).toEqual([]); + }); + + it('should return the steps', () => { + expect(kameletBinding.getSteps()).toEqual([ + { + ref: { + apiVersion: 'camel.apache.org/v1', + kind: 'Kamelet', + name: 'log-sink', + properties: { + showHeaders: 'true', + }, + }, + }, + { + ref: { + apiVersion: 'camel.apache.org/v1', + kind: 'Kamelet', + name: 'kafka-sink', + properties: { + bootstrapServers: '192.168.0.1', + password: 'test', + topic: 'myTopic', + user: 'test2', + }, + }, + }, + ]); + }); }); - it('should return the steps', () => { - expect(kameletBinding.getSteps()).toEqual([]); + describe('toVizNode', () => { + it('should return the viz node and set the initial path to `source`', () => { + const vizNode = kameletBinding.toVizNode(); + + expect(vizNode).toBeDefined(); + expect(vizNode.path).toEqual('source'); + }); + + it('should use the uri as the node label', () => { + const vizNode = kameletBinding.toVizNode(); + + expect(vizNode.label).toEqual('timer-source'); + }); + + it('should set an empty label if the uri is not available', () => { + kameletBinding = new KameletBinding({ spec: {} } as KameletBindingModel); + const vizNode = kameletBinding.toVizNode(); + + expect(vizNode.label).toBeUndefined(); + }); + + it('should populate the viz node chain with the steps', () => { + const vizNode = kameletBinding.toVizNode(); + + expect(vizNode.path).toEqual('source'); + expect(vizNode.label).toEqual('timer-source'); + expect(vizNode.getPreviousNode()).toBeUndefined(); + expect(vizNode.getNextNode()).toBeDefined(); + + const steps0 = vizNode.getNextNode()!; + expect(steps0.path).toEqual('steps.0'); + expect(steps0.label).toEqual('log-sink'); + expect(steps0.getPreviousNode()).toBe(vizNode); + expect(steps0.getNextNode()).toBeDefined(); + + const sink = steps0.getNextNode()!; + expect(sink.path).toEqual('sink'); + expect(sink.label).toEqual('kafka-sink'); + expect(sink.getPreviousNode()).toBe(steps0); + expect(sink.getNextNode()).toBeUndefined(); + }); }); }); diff --git a/packages/ui/src/models/visualization/flows/kamelet-binding.ts b/packages/ui/src/models/visualization/flows/kamelet-binding.ts index d3269717f..69bd28fe6 100644 --- a/packages/ui/src/models/visualization/flows/kamelet-binding.ts +++ b/packages/ui/src/models/visualization/flows/kamelet-binding.ts @@ -1,14 +1,12 @@ +import get from 'lodash.get'; +import set from 'lodash.set'; import { KameletBinding as KameletBindingModel } from '@kaoto-next/camel-catalog/types'; import { v4 as uuidv4 } from 'uuid'; -import { EntityType } from '../../camel-entities/base-entity'; -import { - KameletBindingSink, - KameletBindingSource, - KameletBindingStep, - KameletBindingSteps, -} from '../../camel-entities/kamelet-binding-overrides'; +import { EntityType } from '../../camel-entities'; +import { KameletBindingStep, KameletBindingSteps } from '../../camel-entities/kamelet-binding-overrides'; import { BaseVisualCamelEntity, VisualComponentSchema } from '../base-visual-entity'; import { VisualizationNode } from '../visualization-node'; +import { KameletSchemaService } from './kamelet-schema.service'; export class KameletBinding implements BaseVisualCamelEntity { readonly id = uuidv4(); @@ -21,16 +19,21 @@ export class KameletBinding implements BaseVisualCamelEntity { return ''; } - getComponentSchema(): VisualComponentSchema | undefined { - return undefined; + getComponentSchema(path?: string): VisualComponentSchema | undefined { + if (!path) return undefined; + const stepModel = get(this.route.spec, path) as KameletBindingStep; + return stepModel ? KameletSchemaService.getVisualComponentSchema(stepModel) : undefined; } toJSON() { - return { route: this.route }; + return this.route; } - updateModel(): void { - return; + updateModel(path: string | undefined, value: unknown): void { + if (!path) return; + + const stepModel = get(this.route.spec, path) as KameletBindingStep; + if (stepModel) set(stepModel, 'ref.properties', value); } getSteps() { @@ -46,15 +49,27 @@ export class KameletBinding implements BaseVisualCamelEntity { } toVizNode(): VisualizationNode { - const source: KameletBindingSource = this.route.spec?.source; - const rootNode = new VisualizationNode(source?.ref?.name ?? ''); - const vizNodes = this.getVizNodesFromSteps(this.getSteps()); + const rootNode = this.getVizNodeFromStep(this.route.spec?.source, 'source'); + const stepNodes = this.route.spec?.steps ? this.getVizNodesFromSteps(this.route.spec?.steps) : undefined; + const sinkNode = this.getVizNodeFromStep(this.route.spec?.sink, 'sink'); - if (vizNodes !== undefined) { - const firstVizNode = vizNodes[0]; - if (firstVizNode !== undefined) { - rootNode.setNextNode(firstVizNode); - firstVizNode.setPreviousNode(rootNode); + if (stepNodes !== undefined) { + const firstStepNode = stepNodes[0]; + if (firstStepNode !== undefined) { + rootNode.setNextNode(firstStepNode); + firstStepNode.setPreviousNode(rootNode); + } + } + if (sinkNode !== undefined) { + if (stepNodes !== undefined) { + const lastStepNode = stepNodes[stepNodes.length - 1]; + if (lastStepNode !== undefined) { + lastStepNode.setNextNode(sinkNode); + sinkNode.setPreviousNode(lastStepNode); + } + } else { + rootNode.setNextNode(sinkNode); + sinkNode.setPreviousNode(rootNode); } } return rootNode; @@ -63,7 +78,7 @@ export class KameletBinding implements BaseVisualCamelEntity { private getVizNodesFromSteps(steps: Array): VisualizationNode[] { return steps?.reduce((acc, camelRouteStep) => { const previousVizNode = acc[acc.length - 1]; - const vizNode = this.getVizNodeFromStep(camelRouteStep); + const vizNode = this.getVizNodeFromStep(camelRouteStep, 'steps.' + acc.length); if (previousVizNode !== undefined) { previousVizNode.setNextNode(vizNode); @@ -74,9 +89,12 @@ export class KameletBinding implements BaseVisualCamelEntity { }, [] as VisualizationNode[]); } - private getVizNodeFromStep(step: KameletBindingSink): VisualizationNode { + private getVizNodeFromStep(step: KameletBindingStep, path: string): VisualizationNode { const stepName = step?.ref?.name; - const parentStep = new VisualizationNode(stepName!); - return parentStep; + const answer = new VisualizationNode(stepName!, this); + answer.path = path; + const kameletDefinition = KameletSchemaService.getKameletDefinition(step); + answer.iconData = kameletDefinition?.metadata.annotations['camel.apache.org/kamelet.icon']; + return answer; } } diff --git a/packages/ui/src/models/visualization/flows/kamelet-schema.service.ts b/packages/ui/src/models/visualization/flows/kamelet-schema.service.ts new file mode 100644 index 000000000..f679c8715 --- /dev/null +++ b/packages/ui/src/models/visualization/flows/kamelet-schema.service.ts @@ -0,0 +1,59 @@ +import { IKameletDefinition } from '../../kamelets-catalog'; +import { VisualComponentSchema } from '../base-visual-entity'; +import { KameletBindingStep } from '../../camel-entities/kamelet-binding-overrides'; +import { useCatalogStore } from '../../../store'; +import { CatalogKind } from '../../catalog-kind'; +import { JSONSchemaType } from 'ajv'; + +export class KameletSchemaService { + static getVisualComponentSchema(stepModel: KameletBindingStep): VisualComponentSchema | undefined { + if (stepModel === undefined) { + return undefined; + } + const definition = KameletSchemaService.getKameletDefinition(stepModel); + return { + title: definition?.metadata.name || '', + schema: KameletSchemaService.getSchemaFromKameletDefinition(definition), + definition: stepModel?.ref?.properties || {}, + }; + } + + static getSchemaFromKameletDefinition(definition: IKameletDefinition | undefined): JSONSchemaType { + const required: string[] = []; + const schema = { + type: 'object', + properties: {}, + required, + } as unknown as JSONSchemaType; + const properties = definition?.spec.definition.properties; + if (!properties) { + return schema; + } + + Object.keys(properties).forEach((propertyName) => { + const property = properties[propertyName]; + const propertySchema = { + type: property.type, + title: property.title, + description: property.description, + } as unknown as JSONSchemaType; + + schema.properties[propertyName] = propertySchema; + }); + + if (definition.spec.definition.required) { + required.push(...definition.spec.definition.required); + } + + return schema; + } + + static getKameletDefinition(step: KameletBindingStep): IKameletDefinition | undefined { + if (step?.ref?.kind === 'Kamelet') { + const stepName = step?.ref?.name; + const kameletCatalog = useCatalogStore.getState().catalogs[CatalogKind.Kamelet]; + return kameletCatalog && kameletCatalog[stepName!]; + } + return undefined; + } +} diff --git a/packages/ui/src/models/visualization/flows/pipe.test.ts b/packages/ui/src/models/visualization/flows/pipe.test.ts new file mode 100644 index 000000000..90ebeade0 --- /dev/null +++ b/packages/ui/src/models/visualization/flows/pipe.test.ts @@ -0,0 +1,29 @@ +import { pipeJson } from '../../../stubs/pipe-route'; +import { Pipe } from '.'; + +describe('Pipe', () => { + let pipe: Pipe; + + beforeEach(() => { + pipe = new Pipe(JSON.parse(JSON.stringify(pipeJson))); + }); + + it('should return the steps', () => { + expect(pipe.getSteps()).toEqual([ + { + ref: { + apiVersion: 'camel.apache.org/v1', + kind: 'Kamelet', + name: 'delay-action', + }, + }, + { + ref: { + apiVersion: 'camel.apache.org/v1alpha1', + kind: 'Kamelet', + name: 'log-sink', + }, + }, + ]); + }); +}); diff --git a/packages/ui/src/models/visualization/visualization-node.ts b/packages/ui/src/models/visualization/visualization-node.ts index fe4685698..5780d2bce 100644 --- a/packages/ui/src/models/visualization/visualization-node.ts +++ b/packages/ui/src/models/visualization/visualization-node.ts @@ -8,6 +8,7 @@ export class VisualizationNode implements IVisualizationNode { private nextNode: IVisualizationNode | undefined = undefined; private children: IVisualizationNode[] | undefined; path: string | undefined; + iconData: string | undefined; constructor( public label: string, diff --git a/packages/ui/src/stubs/kamelet-binding.ts b/packages/ui/src/stubs/kamelet-binding.ts new file mode 100644 index 000000000..89ed5bc6b --- /dev/null +++ b/packages/ui/src/stubs/kamelet-binding.ts @@ -0,0 +1,79 @@ +export const kameletBindingYaml = ` +- apiVersion: camel.apache.org/v1 + kind: KameletBinding + metadata: + name: redhat-test + spec: + source: + ref: + apiVersion: camel.apache.org/v1 + name: timer-source + kind: Kamelet + properties: + period: "10000" + message: Hello + steps: + - ref: + apiVersion: camel.apache.org/v1 + name: log-action + kind: Kamelet + properties: + showHeaders: "true" + sink: + ref: + apiVersion: camel.apache.org/v1 + name: kafka-sink + kind: Kamelet + properties: + topic: myTopic + bootstrapServers: 192.168.0.1 + user: test2 + password: test + +`; + +export const kameletBindingJson = { + apiVersion: 'camel.apache.org/v1', + kind: 'KameletBinding', + metadata: { + name: 'redhat-test', + }, + spec: { + source: { + ref: { + apiVersion: 'camel.apache.org/v1', + name: 'timer-source', + kind: 'Kamelet', + properties: { + period: '10000', + message: 'Hello', + }, + }, + }, + steps: [ + { + ref: { + apiVersion: 'camel.apache.org/v1', + name: 'log-sink', + kind: 'Kamelet', + properties: { + showHeaders: 'true', + }, + }, + }, + ], + sink: { + ref: { + apiVersion: 'camel.apache.org/v1', + name: 'kafka-sink', + kind: 'Kamelet', + properties: { + topic: 'myTopic', + bootstrapServers: '192.168.0.1', + user: 'test2', + password: 'test', + }, + }, + }, + }, +};