From a903d8ca468fa988ddab5df0bba353ca826e777a Mon Sep 17 00:00:00 2001 From: "Ricardo M." Date: Thu, 8 Feb 2024 17:00:34 +0100 Subject: [PATCH] feat(canvas): Support root OnException This PR adds support for the root `OnException` entity. This entity has a root configuration that is linked through the `CanvasForm` component but also has a `steps` property, effectively turning it into a `VisualEntity`. The `getVizNodeFromProcessor` method was extracted from the `AbstractCamelVisualEntity` class in order to reuse it in the `OnExceptionVisualEntity` class. In an upcoming PR, more methods will be extracted in order to avoid duplication as much as possible. There are more methods that could be extracted to avoid duplication, for instance: 1. `getNodeLabel()` 2. `getTooltipContent()` 3. `getComponentSchema()` 4. `updateModel()` To make it possible, we would need to change how the `CamelRouteVisualEntity` is created, as currently it requires a root `RouteDefinition` object, ideally, it should receive a `{ route: RouteDefinition }` object instead. fix: https://github.com/KaotoIO/kaoto-next/issues/561 --- .../ui/src/components/Form/schema.service.ts | 1 + .../src/models/camel/camel-route-resource.ts | 15 +- .../src/models/camel/entities/base-entity.ts | 1 + .../visualization/base-visual-entity.ts | 3 - .../flows/abstract-camel-visual-entity.ts | 116 ++------ .../camel-on-exception-visual-entity.test.ts | 34 +++ .../flows/camel-on-exception-visual-entity.ts | 261 ++++++++++++++++++ .../flows/camel-route-visual-entity.test.ts | 70 ----- .../kamelet-binding-visual-entity.test.ts | 30 -- .../flows/kamelet-binding-visual-entity.ts | 2 +- .../flows/pipe-visual-entity.test.ts | 27 -- .../visualization/flows/pipe-visual-entity.ts | 16 +- .../support/camel-component-schema.service.ts | 5 +- .../flows/support/camel-steps.service.test.ts | 75 +++++ .../flows/support/camel-steps.service.ts | 97 +++++++ packages/ui/src/utils/node-icon-resolver.ts | 6 +- 16 files changed, 513 insertions(+), 246 deletions(-) create mode 100644 packages/ui/src/models/visualization/flows/camel-on-exception-visual-entity.test.ts create mode 100644 packages/ui/src/models/visualization/flows/camel-on-exception-visual-entity.ts delete mode 100644 packages/ui/src/models/visualization/flows/kamelet-binding-visual-entity.test.ts create mode 100644 packages/ui/src/models/visualization/flows/support/camel-steps.service.test.ts create mode 100644 packages/ui/src/models/visualization/flows/support/camel-steps.service.ts diff --git a/packages/ui/src/components/Form/schema.service.ts b/packages/ui/src/components/Form/schema.service.ts index 611bf6fc3..0d50d4201 100644 --- a/packages/ui/src/components/Form/schema.service.ts +++ b/packages/ui/src/components/Form/schema.service.ts @@ -11,6 +11,7 @@ export class SchemaService { 'dataFormatType', 'outputs', 'steps', + 'onWhen', 'when', 'otherwise', 'doCatch', diff --git a/packages/ui/src/models/camel/camel-route-resource.ts b/packages/ui/src/models/camel/camel-route-resource.ts index 654353964..6784079d1 100644 --- a/packages/ui/src/models/camel/camel-route-resource.ts +++ b/packages/ui/src/models/camel/camel-route-resource.ts @@ -4,15 +4,17 @@ import { createCamelPropertiesSorter, isDefined } from '../../utils'; import { AddStepMode } from '../visualization/base-visual-entity'; import { CamelRouteVisualEntity, isCamelFrom, isCamelRoute } from '../visualization/flows'; import { FlowTemplateService } from '../visualization/flows/flow-templates-service'; +import { NonVisualEntity } from '../visualization/flows/non-visual-entity'; +import { CamelOnExceptionVisualEntity } from '../visualization/flows/camel-on-exception-visual-entity'; import { CamelComponentFilterService } from '../visualization/flows/support/camel-component-filter.service'; import { CamelRouteVisualEntityData } from '../visualization/flows/support/camel-component-types'; import { BeansEntity, isBeans } from '../visualization/metadata'; import { BeansAwareResource, CamelResource } from './camel-resource'; import { BaseCamelEntity } from './entities'; import { SourceSchemaType } from './source-schema-type'; -import { NonVisualEntity } from '../visualization/flows/non-visual-entity'; export class CamelRouteResource implements CamelResource, BeansAwareResource { + static readonly SUPPORTED_ENTITIES = [CamelOnExceptionVisualEntity]; static readonly PARAMETERS_ORDER = ['id', 'description', 'uri', 'parameters', 'steps']; readonly sortFn = createCamelPropertiesSorter(CamelRouteResource.PARAMETERS_ORDER) as ( a: unknown, @@ -50,7 +52,9 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { } getVisualEntities(): CamelRouteVisualEntity[] { - return this.entities.filter((entity) => entity instanceof CamelRouteVisualEntity) as CamelRouteVisualEntity[]; + return this.entities.filter( + (entity) => entity instanceof CamelRouteVisualEntity || entity instanceof CamelOnExceptionVisualEntity, + ) as CamelRouteVisualEntity[]; } getEntities(): BaseCamelEntity[] { @@ -110,6 +114,13 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { } else if (isBeans(rawItem)) { return new BeansEntity(rawItem); } + + for (const Entity of CamelRouteResource.SUPPORTED_ENTITIES) { + if (Entity.isApplicable(rawItem)) { + return new Entity(rawItem); + } + } + return new NonVisualEntity(rawItem as string); } } diff --git a/packages/ui/src/models/camel/entities/base-entity.ts b/packages/ui/src/models/camel/entities/base-entity.ts index e60e2d023..d35c6597b 100644 --- a/packages/ui/src/models/camel/entities/base-entity.ts +++ b/packages/ui/src/models/camel/entities/base-entity.ts @@ -1,6 +1,7 @@ /** This is the enum with the registered Camel entities supported by Kaoto */ export const enum EntityType { Route = 'route', + OnException = 'onException', Integration = 'integration', Kamelet = 'kamelet', KameletBinding = 'kameletBinding', diff --git a/packages/ui/src/models/visualization/base-visual-entity.ts b/packages/ui/src/models/visualization/base-visual-entity.ts index 55296dee0..317e72566 100644 --- a/packages/ui/src/models/visualization/base-visual-entity.ts +++ b/packages/ui/src/models/visualization/base-visual-entity.ts @@ -29,9 +29,6 @@ export interface BaseVisualCamelEntity extends BaseCamelEntity { /** Given a path, update the model */ updateModel(path: string | undefined, value: unknown): void; - /** Retrieve the steps from the underlying Camel entity */ - getSteps: () => unknown[]; - /** Add a step to the underlying Camel entity */ addStep: (options: { definedComponent: DefinedComponent; diff --git a/packages/ui/src/models/visualization/flows/abstract-camel-visual-entity.ts b/packages/ui/src/models/visualization/flows/abstract-camel-visual-entity.ts index e8a4050e5..88dc5d05b 100644 --- a/packages/ui/src/models/visualization/flows/abstract-camel-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/abstract-camel-visual-entity.ts @@ -1,6 +1,6 @@ /* eslint-disable no-case-declarations */ -import { DoCatch, ProcessorDefinition, RouteDefinition, When1 } from '@kaoto-next/camel-catalog/types'; -import { getArrayProperty, getValue, isDefined, setValue } from '../../../utils'; +import { ProcessorDefinition, RouteDefinition } from '@kaoto-next/camel-catalog/types'; +import { ROOT_PATH, getArrayProperty, getValue, isDefined, setValue } from '../../../utils'; import { NodeIconResolver } from '../../../utils/node-icon-resolver'; import { DefinedComponent } from '../../camel-catalog-index'; import { EntityType } from '../../camel/entities'; @@ -15,11 +15,8 @@ import { import { createVisualizationNode } from '../visualization-node'; import { CamelComponentDefaultService } from './support/camel-component-default.service'; import { CamelComponentSchemaService } from './support/camel-component-schema.service'; -import { - CamelProcessorStepsProperties, - CamelRouteVisualEntityData, - ICamelElementLookupResult, -} from './support/camel-component-types'; +import { CamelProcessorStepsProperties, CamelRouteVisualEntityData } from './support/camel-component-types'; +import { CamelStepsService } from './support/camel-steps.service'; import { ModelValidationService } from './support/validators/model-validation.service'; export abstract class AbstractCamelVisualEntity implements BaseVisualCamelEntity { @@ -76,10 +73,6 @@ export abstract class AbstractCamelVisualEntity implements BaseVisualCamelEntity setValue(this.route, path, value); } - getSteps(): ProcessorDefinition[] { - return this.route.from?.steps ?? []; - } - /** * Add a step to the route * @@ -225,99 +218,28 @@ export abstract class AbstractCamelVisualEntity implements BaseVisualCamelEntity } toVizNode(): IVisualizationNode { - const fromNode = this.getVizNodeFromProcessor('from', { - processorName: 'from' as keyof ProcessorDefinition, - componentName: CamelComponentSchemaService.getComponentNameFromUri(this.getRootUri()!), - }); - - if (!this.getRootUri()) { - fromNode.data.icon = NodeIconResolver.getPlaceholderIcon(); - } - - const routeNode = createVisualizationNode(this.id, { - path: '#', + const routeGroupNode = createVisualizationNode(this.id, { + path: ROOT_PATH, entity: this, isGroup: true, - icon: NodeIconResolver.getIcon('route'), + icon: NodeIconResolver.getIcon(this.type), }); - routeNode.addChild(fromNode); - return routeNode; - } - - private getVizNodeFromProcessor(path: string, componentLookup: ICamelElementLookupResult): IVisualizationNode { - const data: CamelRouteVisualEntityData = { - path, - icon: NodeIconResolver.getIcon(CamelComponentSchemaService.getIconName(componentLookup)), - processorName: componentLookup.processorName, - componentName: componentLookup.componentName, - }; - - const vizNode = createVisualizationNode(componentLookup.componentName ?? componentLookup.processorName, data); - - const childrenStepsProperties = CamelComponentSchemaService.getProcessorStepsProperties( - componentLookup.processorName as keyof ProcessorDefinition, + const fromNode = CamelStepsService.getVizNodeFromProcessor( + 'from', + { + processorName: 'from' as keyof ProcessorDefinition, + componentName: CamelComponentSchemaService.getComponentNameFromUri(this.getRootUri()!), + }, + this.route, ); - childrenStepsProperties.forEach((stepsProperty) => { - const childrenVizNodes = this.getVizNodesFromChildren(path, stepsProperty); - childrenVizNodes.forEach((childVizNode) => vizNode.addChild(childVizNode)); - }); - - return vizNode; - } - - private getVizNodesFromChildren(path: string, stepsProperty: CamelProcessorStepsProperties): IVisualizationNode[] { - let singlePath: string; - - switch (stepsProperty.type) { - case 'branch': - singlePath = `${path}.${stepsProperty.name}`; - const stepsList = getValue(this.route, singlePath, []) as ProcessorDefinition[]; - - return stepsList.reduce((accStepsNodes, step, index) => { - const singlePropertyName = Object.keys(step)[0]; - const childPath = `${singlePath}.${index}.${singlePropertyName}`; - const childComponentLookup = CamelComponentSchemaService.getCamelComponentLookup( - childPath, - getValue(step, singlePropertyName), - ); - - const vizNode = this.getVizNodeFromProcessor(childPath, childComponentLookup); - - const previousVizNode = accStepsNodes[accStepsNodes.length - 1]; - if (previousVizNode !== undefined) { - previousVizNode.setNextNode(vizNode); - vizNode.setPreviousNode(previousVizNode); - } - - accStepsNodes.push(vizNode); - return accStepsNodes; - }, [] as IVisualizationNode[]); - - case 'single-clause': - const childPath = `${path}.${stepsProperty.name}`; - const childComponentLookup = CamelComponentSchemaService.getCamelComponentLookup(childPath, this.route); - - /** If the single-clause property is not defined, we don't create a IVisualizationNode for it */ - if (getValue(this.route, childPath) === undefined) return []; - - return [this.getVizNodeFromProcessor(childPath, childComponentLookup)]; - - case 'clause-list': - singlePath = `${path}.${stepsProperty.name}`; - const expressionList = getValue(this.route, singlePath, []) as When1[] | DoCatch[]; - - return expressionList.map((_step, index) => { - const childPath = `${singlePath}.${index}`; - const childComponentLookup = { processorName: stepsProperty.name as keyof ProcessorDefinition }; // when, doCatch - - return this.getVizNodeFromProcessor(childPath, childComponentLookup); - }); - - default: - return []; + if (!this.getRootUri()) { + fromNode.data.icon = NodeIconResolver.getPlaceholderIcon(); } + routeGroupNode.addChild(fromNode); + + return routeGroupNode; } private insertChildStep( diff --git a/packages/ui/src/models/visualization/flows/camel-on-exception-visual-entity.test.ts b/packages/ui/src/models/visualization/flows/camel-on-exception-visual-entity.test.ts new file mode 100644 index 000000000..2a59cdca6 --- /dev/null +++ b/packages/ui/src/models/visualization/flows/camel-on-exception-visual-entity.test.ts @@ -0,0 +1,34 @@ +import { CamelOnExceptionVisualEntity } from './camel-on-exception-visual-entity'; +import { OnException } from '@kaoto-next/camel-catalog/types'; + +describe('CamelOnExceptionVisualEntity', () => { + const ONEXCEPTION_ID_REGEXP = /^onException-[a-zA-Z0-9]{4}$/; + + describe('isApplicable', () => { + it.each([ + [true, { onException: { id: 'onExceptionId' } }], + [false, { from: { id: 'from-1234', steps: [] } }], + [false, { onException: { id: 'onExceptionId' }, anotherProperty: true }], + ])('should return %s for %s', (result, definition) => { + expect(CamelOnExceptionVisualEntity.isApplicable(definition)).toEqual(result); + }); + }); + + describe('constructor', () => { + it('should set id to onExceptionDef.onException.id if it is defined', () => { + const onExceptionDef: { onException: OnException } = { onException: { id: 'onExceptionId' } }; + const entity = new CamelOnExceptionVisualEntity(onExceptionDef); + + expect(entity.id).toEqual('onExceptionId'); + expect(onExceptionDef.onException.id).toEqual('onExceptionId'); + }); + + it('should set id to generated id if onExceptionDef.onException.id is not defined', () => { + const onExceptionDef = { onException: {} as OnException }; + const entity = new CamelOnExceptionVisualEntity(onExceptionDef); + + expect(entity.id).toMatch(ONEXCEPTION_ID_REGEXP); + expect(onExceptionDef.onException.id).toEqual(entity.id); + }); + }); +}); diff --git a/packages/ui/src/models/visualization/flows/camel-on-exception-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-on-exception-visual-entity.ts new file mode 100644 index 000000000..90da5b952 --- /dev/null +++ b/packages/ui/src/models/visualization/flows/camel-on-exception-visual-entity.ts @@ -0,0 +1,261 @@ +import { OnException, ProcessorDefinition } from '@kaoto-next/camel-catalog/types'; +import { getCamelRandomId } from '../../../camel-utils/camel-random-id'; +import { getArrayProperty, getValue, isDefined, setValue } from '../../../utils'; +import { DefinedComponent } from '../../camel-catalog-index'; +import { EntityType } from '../../camel/entities/base-entity'; +import { + AddStepMode, + BaseVisualCamelEntity, + IVisualizationNode, + IVisualizationNodeData, + NodeInteraction, + VisualComponentSchema, +} from '../base-visual-entity'; +import { CamelComponentSchemaService } from './support/camel-component-schema.service'; +import { CamelStepsService } from './support/camel-steps.service'; +import { ModelValidationService } from './support/validators/model-validation.service'; +import { CamelProcessorStepsProperties, CamelRouteVisualEntityData } from './support/camel-component-types'; +import { AbstractCamelVisualEntity } from './abstract-camel-visual-entity'; +import { CamelComponentDefaultService } from './support/camel-component-default.service'; + +export class CamelOnExceptionVisualEntity implements BaseVisualCamelEntity { + id: string; + readonly type = EntityType.ErrorHandler; + + constructor(public onExceptionDef: { onException: OnException }) { + const id = onExceptionDef.onException.id ?? getCamelRandomId('onException'); + this.id = id; + onExceptionDef.onException.id = id; + } + + static isApplicable(onExceptionDef: unknown): onExceptionDef is { onException: OnException } { + if (!isDefined(onExceptionDef) || Array.isArray(onExceptionDef) || typeof onExceptionDef !== 'object') { + return false; + } + + const objectKeys = Object.keys(onExceptionDef!); + + return ( + objectKeys.length === 1 && 'onException' in onExceptionDef! && typeof onExceptionDef.onException === 'object' + ); + } + + getId(): string { + return this.id; + } + + setId(id: string): void { + this.id = id; + } + + getNodeLabel(path?: string): string { + if (!path) return ''; + + const componentModel = getValue(this.onExceptionDef, path); + const label = CamelComponentSchemaService.getNodeLabel( + CamelComponentSchemaService.getCamelComponentLookup(path, componentModel), + componentModel, + ); + + return label; + } + + getTooltipContent(path?: string): string { + if (!path) return ''; + const componentModel = getValue(this.onExceptionDef, path); + + const content = CamelComponentSchemaService.getTooltipContent( + CamelComponentSchemaService.getCamelComponentLookup(path, componentModel), + ); + + return content; + } + + getComponentSchema(path?: string | undefined): VisualComponentSchema | undefined { + if (!path) return undefined; + + const componentModel = getValue(this.onExceptionDef, path); + const visualComponentSchema = CamelComponentSchemaService.getVisualComponentSchema(path, componentModel); + + return visualComponentSchema; + } + + updateModel(path: string | undefined, value: unknown): void { + if (!path) return; + + setValue(this.onExceptionDef, path, value); + } + + /** + * Add a step to the route + * + * path examples: + * from + * from.steps.0.setHeader + * from.steps.1.choice.when.0 + * from.steps.1.choice.when.0.steps.0.setHeader + * from.steps.1.choice.otherwise + * from.steps.1.choice.otherwise.steps.0.setHeader + * from.steps.2.doTry.doCatch.0 + * from.steps.2.doTry.doCatch.0.steps.0.setHeader + */ + addStep(options: { + definedComponent: DefinedComponent; + mode: AddStepMode; + data: IVisualizationNodeData; + targetProperty?: string; + }) { + if (options.data.path === undefined) return; + const defaultValue = CamelComponentDefaultService.getDefaultNodeDefinitionValue(options.definedComponent); + const stepsProperties = CamelComponentSchemaService.getProcessorStepsProperties( + (options.data as CamelRouteVisualEntityData).processorName as keyof ProcessorDefinition, + ); + + if (options.mode === AddStepMode.InsertChildStep || options.mode === AddStepMode.InsertSpecialChildStep) { + this.insertChildStep(options, stepsProperties, defaultValue); + return; + } + + const pathArray = options.data.path.split('.'); + const last = pathArray[pathArray.length - 1]; + const penultimate = pathArray[pathArray.length - 2]; + + /** + * If the last segment is a string and the penultimate is a number, it means the target is member of an array + * therefore we need to look for the array and insert the element at the given index + 1 + * + * f.i. from.steps.0.setHeader + * penultimate: 0 + * last: setHeader + */ + if (!Number.isInteger(Number(last)) && Number.isInteger(Number(penultimate))) { + /** If we're in Append mode, we need to insert the step after the selected index hence `Number(penultimate) + 1` */ + const desiredStartIndex = options.mode === AddStepMode.AppendStep ? Number(penultimate) + 1 : Number(penultimate); + + /** If we're in Replace mode, we need to delete the existing step */ + const deleteCount = options.mode === AddStepMode.ReplaceStep ? 1 : 0; + + const stepsArray: ProcessorDefinition[] = getValue(this.onExceptionDef, pathArray.slice(0, -2), []); + stepsArray.splice(desiredStartIndex, deleteCount, defaultValue); + + return; + } + } + + removeStep(path?: string): void { + if (!path) return; + /** + * If there's only one path segment, it means the target is the `from` property of the route + * therefore we replace it with an empty object + */ + if (path === 'from') { + setValue(this.onExceptionDef, 'from.uri', ''); + return; + } + + const pathArray = path.split('.'); + const last = pathArray[pathArray.length - 1]; + const penultimate = pathArray[pathArray.length - 2]; + + /** + * If the last segment is a number, it means the target object is a member of an array + * therefore we need to look for the array and remove the element at the given index + * + * f.i. from.steps.1.choice.when.0 + * last: 0 + */ + let array = getValue(this.onExceptionDef, pathArray.slice(0, -1), []); + if (Number.isInteger(Number(last)) && Array.isArray(array)) { + array.splice(Number(last), 1); + + return; + } + + /** + * If the last segment is a word and the penultimate is a number, it means the target is an object + * potentially a Processor, that belongs to an array, therefore we remove it entirely + * + * f.i. from.steps.1.choice + * last: choice + * penultimate: 1 + */ + array = getValue(this.onExceptionDef, pathArray.slice(0, -2), []); + if (!Number.isInteger(Number(last)) && Number.isInteger(Number(penultimate)) && Array.isArray(array)) { + array.splice(Number(penultimate), 1); + + return; + } + + /** + * If both the last and penultimate segment are words, it means the target is a property of an object + * therefore we delete it + * + * f.i. from.steps.1.choice.otherwise + * last: otherwise + * penultimate: choice + */ + const object = getValue(this.onExceptionDef, pathArray.slice(0, -1), {}); + if (!Number.isInteger(Number(last)) && !Number.isInteger(Number(penultimate)) && typeof object === 'object') { + delete object[last]; + } + } + + getNodeInteraction(data: IVisualizationNodeData): NodeInteraction { + const stepsProperties = CamelComponentSchemaService.getProcessorStepsProperties( + (data as CamelRouteVisualEntityData).processorName as keyof ProcessorDefinition, + ); + const canHavePreviousStep = CamelComponentSchemaService.canHavePreviousStep( + (data as CamelRouteVisualEntityData).processorName, + ); + const canHaveChildren = stepsProperties.find((property) => property.type === 'branch') !== undefined; + const canHaveSpecialChildren = Object.keys(stepsProperties).length > 1; + + return { + canHavePreviousStep, + canHaveNextStep: canHavePreviousStep, + canHaveChildren, + canHaveSpecialChildren, + }; + } + + getNodeValidationText(path?: string | undefined): string | undefined { + const componentVisualSchema = this.getComponentSchema(path); + if (!componentVisualSchema) return undefined; + + return ModelValidationService.validateNodeStatus(componentVisualSchema); + } + + toVizNode(): IVisualizationNode { + const onExceptionGroupNode = CamelStepsService.getVizNodeFromProcessor( + 'onException', + { processorName: 'onException' as keyof ProcessorDefinition }, + this.onExceptionDef, + ); + onExceptionGroupNode.data.entity = this; + onExceptionGroupNode.data.isGroup = true; + + return onExceptionGroupNode; + } + + toJSON(): unknown { + return this.onExceptionDef; + } + + private insertChildStep( + options: Parameters[0], + stepsProperties: CamelProcessorStepsProperties[], + defaultValue: ProcessorDefinition = {}, + ) { + const property = stepsProperties.find((property) => + options.mode === AddStepMode.InsertChildStep ? 'steps' : options.definedComponent.name === property.name, + ); + if (property === undefined) return; + + if (property.type === 'single-clause') { + setValue(this.onExceptionDef, `${options.data.path}.${property.name}`, defaultValue); + } else { + const arrayPath = getArrayProperty(this.onExceptionDef, `${options.data.path}.${property.name}`); + arrayPath.unshift(defaultValue); + } + } +} diff --git a/packages/ui/src/models/visualization/flows/camel-route-visual-entity.test.ts b/packages/ui/src/models/visualization/flows/camel-route-visual-entity.test.ts index 53f438fd3..1aa405978 100644 --- a/packages/ui/src/models/visualization/flows/camel-route-visual-entity.test.ts +++ b/packages/ui/src/models/visualization/flows/camel-route-visual-entity.test.ts @@ -149,76 +149,6 @@ describe('Camel Route', () => { }); }); - describe('getSteps', () => { - it('should return an empty array if there is no route', () => { - const route = new CamelRouteVisualEntity({ from: {} } as RouteDefinition); - - expect(route.getSteps()).toEqual([]); - }); - - it('should return an empty array if there is no steps', () => { - const route = new CamelRouteVisualEntity({ from: {} } as RouteDefinition); - - expect(route.getSteps()).toEqual([]); - }); - - it('should return the steps', () => { - expect(camelEntity.getSteps()).toEqual([ - { - 'set-header': { - name: 'myChoice', - simple: '${random(2)}', - }, - }, - { - choice: { - otherwise: { - steps: [ - { - to: { - uri: 'amqp:queue:', - }, - }, - { - to: { - uri: 'amqp:queue:', - }, - }, - { - log: { - id: 'log-2', - message: 'We got a ${body}', - }, - }, - ], - }, - when: [ - { - simple: '${header.myChoice} == 1', - steps: [ - { - log: { - id: 'log-1', - message: 'We got a one.', - }, - }, - ], - }, - ], - }, - }, - { - to: { - parameters: { - bridgeErrorHandler: true, - }, - uri: 'direct:my-route', - }, - }, - ]); - }); - }); - describe('removeStep', () => { it('should not remove any step if no path is provided', () => { const originalObject = cloneDeep(camelRouteJson.route); diff --git a/packages/ui/src/models/visualization/flows/kamelet-binding-visual-entity.test.ts b/packages/ui/src/models/visualization/flows/kamelet-binding-visual-entity.test.ts deleted file mode 100644 index 2688e2e11..000000000 --- a/packages/ui/src/models/visualization/flows/kamelet-binding-visual-entity.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { kameletBindingJson } from '../../../stubs/kamelet-binding-route'; -import { KameletBindingVisualEntity } from './kamelet-binding-visual-entity'; - -describe('KameletBinding', () => { - let kameletBinding: KameletBindingVisualEntity; - - beforeEach(() => { - const kameletBindingCR = JSON.parse(JSON.stringify(kameletBindingJson)); - kameletBinding = new KameletBindingVisualEntity(kameletBindingCR.spec); - }); - - it('should return the steps', () => { - expect(kameletBinding.getSteps()).toEqual([ - { - ref: { - apiVersion: 'camel.apache.org/v1', - kind: 'Kamelet', - name: 'delay-action', - }, - }, - { - ref: { - apiVersion: 'camel.apache.org/v1', - kind: 'Kamelet', - name: 'log-sink', - }, - }, - ]); - }); -}); diff --git a/packages/ui/src/models/visualization/flows/kamelet-binding-visual-entity.ts b/packages/ui/src/models/visualization/flows/kamelet-binding-visual-entity.ts index 1d3aa891a..2105c43cb 100644 --- a/packages/ui/src/models/visualization/flows/kamelet-binding-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/kamelet-binding-visual-entity.ts @@ -9,7 +9,7 @@ import { KameletBindingSpec } from '../../camel/entities/kamelet-binding-overrid */ export class KameletBindingVisualEntity extends PipeVisualEntity implements BaseVisualCamelEntity { readonly id = uuidv4(); - type = EntityType.KameletBinding; + type: EntityType = EntityType.KameletBinding; constructor(public spec: KameletBindingSpec) { super(spec); diff --git a/packages/ui/src/models/visualization/flows/pipe-visual-entity.test.ts b/packages/ui/src/models/visualization/flows/pipe-visual-entity.test.ts index 0bb7f24ac..8b97c3cb4 100644 --- a/packages/ui/src/models/visualization/flows/pipe-visual-entity.test.ts +++ b/packages/ui/src/models/visualization/flows/pipe-visual-entity.test.ts @@ -96,33 +96,6 @@ describe('Pipe', () => { }); }); - describe('getSteps', () => { - it('should return an empty array if there is no steps', () => { - const route = new PipeVisualEntity({}); - - expect(route.getSteps()).toEqual([]); - }); - - 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/v1', - kind: 'Kamelet', - name: 'log-sink', - }, - }, - ]); - }); - }); - describe('removeStep', () => { it('should not remove the step if no path is provided', () => { pipe.removeStep(); diff --git a/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts b/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts index 4ec459ff9..f807f6a3c 100644 --- a/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/pipe-visual-entity.ts @@ -5,7 +5,7 @@ import { getCamelRandomId } from '../../../camel-utils/camel-random-id'; import { getArrayProperty, NodeIconResolver } from '../../../utils'; import { DefinedComponent } from '../../camel-catalog-index'; import { EntityType } from '../../camel/entities'; -import { PipeMetadata, PipeSpec, PipeStep, PipeSteps } from '../../camel/entities/pipe-overrides'; +import { PipeMetadata, PipeSpec, PipeStep } from '../../camel/entities/pipe-overrides'; import { AddStepMode, BaseVisualCamelEntity, @@ -20,7 +20,7 @@ import { ModelValidationService } from './support/validators/model-validation.se export class PipeVisualEntity implements BaseVisualCamelEntity { id: string; - type = EntityType.Pipe; + readonly type: EntityType = EntityType.Pipe; spec: PipeSpec; metadata: PipeMetadata; @@ -75,18 +75,6 @@ export class PipeVisualEntity implements BaseVisualCamelEntity { if (stepModel) set(stepModel, 'properties', value); } - getSteps() { - const steps: PipeSteps = this.spec?.steps; - const sink: PipeStep = this.spec?.sink; - let allSteps: Array = []; - if (steps !== undefined) { - allSteps = allSteps.concat(steps); - } - !sink || Object.keys(sink).length === 0 || allSteps.push(sink); - - return allSteps; - } - /** * Add a step to the Pipe * diff --git a/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.ts b/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.ts index 6cc2d6939..34ca626fd 100644 --- a/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.ts +++ b/packages/ui/src/models/visualization/flows/support/camel-component-schema.service.ts @@ -10,7 +10,7 @@ import { IKameletDefinition } from '../../../kamelets-catalog'; import { ICamelComponentDefinition } from '../../../camel-components-catalog'; export class CamelComponentSchemaService { - static DISABLED_SIBLING_STEPS = ['from', 'when', 'otherwise', 'doCatch', 'doFinally']; + static DISABLED_SIBLING_STEPS = ['from', 'onWhen', 'when', 'otherwise', 'doCatch', 'doFinally']; // eslint-disable-next-line @typescript-eslint/no-explicit-any static getVisualComponentSchema(path: string, definition: any): VisualComponentSchema | undefined { @@ -66,6 +66,7 @@ export class CamelComponentSchemaService { const uriString = CamelUriHelper.getUriString(definition); switch (camelElementLookup.processorName) { case 'route' as keyof ProcessorDefinition: + case 'onException' as keyof ProcessorDefinition: return definition?.id ?? ''; case 'from' as keyof ProcessorDefinition: @@ -130,6 +131,7 @@ export class CamelComponentSchemaService { case 'step': case 'whenSkipSendToEndpoint': case 'from' as keyof ProcessorDefinition: + case 'onException' as keyof ProcessorDefinition: return [{ name: 'steps', type: 'branch' }]; case 'choice': @@ -232,6 +234,7 @@ export class CamelComponentSchemaService { let catalogKind: CatalogKind; switch (camelElementLookup.processorName) { case 'route' as keyof ProcessorDefinition: + case 'onException' as keyof ProcessorDefinition: catalogKind = CatalogKind.Entity; break; case 'from' as keyof ProcessorDefinition: diff --git a/packages/ui/src/models/visualization/flows/support/camel-steps.service.test.ts b/packages/ui/src/models/visualization/flows/support/camel-steps.service.test.ts new file mode 100644 index 000000000..e9a5d043d --- /dev/null +++ b/packages/ui/src/models/visualization/flows/support/camel-steps.service.test.ts @@ -0,0 +1,75 @@ +import { ProcessorDefinition, RouteDefinition } from '@kaoto-next/camel-catalog/types'; +import { ICamelElementLookupResult } from './camel-component-types'; +import { CamelStepsService } from './camel-steps.service'; + +describe('CamelStepsService', () => { + let path: string; + let componentLookup: ICamelElementLookupResult; + let entityDefinition: unknown; + + beforeEach(() => { + path = 'from'; + componentLookup = { + processorName: 'from' as keyof ProcessorDefinition, + componentName: 'timer:timerName', + }; + entityDefinition = { uri: 'timer:timerName', steps: [] }; + }); + + describe('getVizNodeFromProcessor', () => { + it('should return a VisualizationNode', () => { + const vizNode = CamelStepsService.getVizNodeFromProcessor(path, componentLookup, entityDefinition); + + expect(vizNode).toBeDefined(); + expect(vizNode.data).toMatchObject({ + path, + icon: expect.any(String), + processorName: 'from', + componentName: 'timer:timerName', + }); + }); + + it('should return a VisualizationNode with children', () => { + const routeDefinition: RouteDefinition = { + from: { + uri: 'timer:timerName', + steps: [{ log: 'logName' }, { to: 'direct:anotherRoute' }], + }, + }; + + const vizNode = CamelStepsService.getVizNodeFromProcessor(path, componentLookup, routeDefinition); + expect(vizNode.getChildren()).toHaveLength(2); + expect(vizNode.getChildren()?.[0].data.path).toBe('from.steps.0.log'); + expect(vizNode.getChildren()?.[1].data.path).toBe('from.steps.1.to'); + }); + + it('should return a VisualizationNode with special children', () => { + const routeDefinition: RouteDefinition = { + from: { + uri: 'timer:timerName', + steps: [ + { + choice: { + when: [ + { expression: { simple: { expression: '${body} == 1' } } }, + { expression: { simple: { expression: '${body} == 2' } } }, + ], + otherwise: { steps: [{ log: 'logName' }] }, + }, + }, + ], + }, + }; + + const vizNode = CamelStepsService.getVizNodeFromProcessor(path, componentLookup, routeDefinition); + expect(vizNode.getChildren()).toHaveLength(1); + expect(vizNode.getChildren()?.[0].data.path).toBe('from.steps.0.choice'); + + const choiceNode = vizNode.getChildren()?.[0]; + expect(choiceNode?.getChildren()).toHaveLength(3); + expect(choiceNode?.getChildren()?.[0].data.path).toBe('from.steps.0.choice.when.0'); + expect(choiceNode?.getChildren()?.[1].data.path).toBe('from.steps.0.choice.when.1'); + expect(choiceNode?.getChildren()?.[2].data.path).toBe('from.steps.0.choice.otherwise'); + }); + }); +}); diff --git a/packages/ui/src/models/visualization/flows/support/camel-steps.service.ts b/packages/ui/src/models/visualization/flows/support/camel-steps.service.ts new file mode 100644 index 000000000..85755e729 --- /dev/null +++ b/packages/ui/src/models/visualization/flows/support/camel-steps.service.ts @@ -0,0 +1,97 @@ +/* eslint-disable no-case-declarations */ +import { DoCatch, ProcessorDefinition, When1 } from '@kaoto-next/camel-catalog/types'; +import { getValue } from '../../../../utils'; +import { NodeIconResolver } from '../../../../utils/node-icon-resolver'; +import { IVisualizationNode } from '../../base-visual-entity'; +import { createVisualizationNode } from '../../visualization-node'; +import { CamelComponentSchemaService } from './camel-component-schema.service'; +import { + CamelProcessorStepsProperties, + CamelRouteVisualEntityData, + ICamelElementLookupResult, +} from './camel-component-types'; + +export class CamelStepsService { + static getVizNodeFromProcessor( + path: string, + componentLookup: ICamelElementLookupResult, + entityDefinition: unknown, + ): IVisualizationNode { + const data: CamelRouteVisualEntityData = { + path, + icon: NodeIconResolver.getIcon(CamelComponentSchemaService.getIconName(componentLookup)), + processorName: componentLookup.processorName, + componentName: componentLookup.componentName, + }; + + const vizNode = createVisualizationNode(componentLookup.componentName ?? componentLookup.processorName, data); + + const childrenStepsProperties = CamelComponentSchemaService.getProcessorStepsProperties( + componentLookup.processorName as keyof ProcessorDefinition, + ); + + childrenStepsProperties.forEach((stepsProperty) => { + const childrenVizNodes = this.getVizNodesFromChildren(path, stepsProperty, entityDefinition); + childrenVizNodes.forEach((childVizNode) => vizNode.addChild(childVizNode)); + }); + + return vizNode; + } + + private static getVizNodesFromChildren( + path: string, + stepsProperty: CamelProcessorStepsProperties, + entityDefinition: unknown, + ): IVisualizationNode[] { + let singlePath: string; + + switch (stepsProperty.type) { + case 'branch': + singlePath = `${path}.${stepsProperty.name}`; + const stepsList = getValue(entityDefinition, singlePath, []) as ProcessorDefinition[]; + + return stepsList.reduce((accStepsNodes, step, index) => { + const singlePropertyName = Object.keys(step)[0]; + const childPath = `${singlePath}.${index}.${singlePropertyName}`; + const childComponentLookup = CamelComponentSchemaService.getCamelComponentLookup( + childPath, + getValue(step, singlePropertyName), + ); + + const vizNode = this.getVizNodeFromProcessor(childPath, childComponentLookup, entityDefinition); + + const previousVizNode = accStepsNodes[accStepsNodes.length - 1]; + if (previousVizNode !== undefined) { + previousVizNode.setNextNode(vizNode); + vizNode.setPreviousNode(previousVizNode); + } + + accStepsNodes.push(vizNode); + return accStepsNodes; + }, [] as IVisualizationNode[]); + + case 'single-clause': + const childPath = `${path}.${stepsProperty.name}`; + const childComponentLookup = CamelComponentSchemaService.getCamelComponentLookup(childPath, entityDefinition); + + /** If the single-clause property is not defined, we don't create a IVisualizationNode for it */ + if (getValue(entityDefinition, childPath) === undefined) return []; + + return [this.getVizNodeFromProcessor(childPath, childComponentLookup, entityDefinition)]; + + case 'clause-list': + singlePath = `${path}.${stepsProperty.name}`; + const expressionList = getValue(entityDefinition, singlePath, []) as When1[] | DoCatch[]; + + return expressionList.map((_step, index) => { + const childPath = `${singlePath}.${index}`; + const childComponentLookup = { processorName: stepsProperty.name as keyof ProcessorDefinition }; // when, doCatch + + return this.getVizNodeFromProcessor(childPath, childComponentLookup, entityDefinition); + }); + + default: + return []; + } + } +} diff --git a/packages/ui/src/utils/node-icon-resolver.ts b/packages/ui/src/utils/node-icon-resolver.ts index bcb696637..324d07e73 100644 --- a/packages/ui/src/utils/node-icon-resolver.ts +++ b/packages/ui/src/utils/node-icon-resolver.ts @@ -20,6 +20,7 @@ import log from '../assets/eip/log.png'; import loop from '../assets/eip/loop.png'; import multicast from '../assets/eip/multicast.png'; import otherwise from '../assets/eip/otherwise.png'; +import throwException from '../assets/eip/throw-exception.png'; import pausable from '../assets/eip/pausable.png'; import pipeline from '../assets/eip/pipeline.png'; import poll_enrich from '../assets/eip/poll-enrich.png'; @@ -110,6 +111,7 @@ import xslt from '../assets/components/xslt2.png'; import { CatalogKind } from '../models/catalog-kind'; import { IKameletDefinition } from '../models/kamelets-catalog'; import { CamelCatalogService } from '../models/visualization/flows/camel-catalog.service'; +import { EntityType } from '../models/camel/entities'; export class NodeIconResolver { static getIcon(elementName: string | undefined): string { @@ -722,8 +724,10 @@ export class NodeIconResolver { private static getVisualEntityIcon(elementName?: string): string | undefined { switch (elementName) { - case 'route': + case EntityType.Route: return route; + case EntityType.OnException: + return throwException; default: return undefined; }