diff --git a/packages/ui-tests/cypress/e2e/designer/basicNodeActions/hoverToolbarActions.cy.ts b/packages/ui-tests/cypress/e2e/designer/basicNodeActions/hoverToolbarActions.cy.ts index 8a2e1792f..857edd637 100644 --- a/packages/ui-tests/cypress/e2e/designer/basicNodeActions/hoverToolbarActions.cy.ts +++ b/packages/ui-tests/cypress/e2e/designer/basicNodeActions/hoverToolbarActions.cy.ts @@ -33,21 +33,11 @@ describe('Test toolbar on hover actions', () => { cy.openStepConfigurationTab('setHeader'); cy.get('[data-testid="step-toolbar-button-disable"]').click(); - cy.openStepConfigurationTab('setHeader'); - - // Temporary workaround since the toolbar is updated but the config form is closed - cy.openStepConfigurationTab('setHeader'); - cy.selectFormTab('All'); cy.checkConfigCheckboxObject('disabled', true); cy.get('[data-testid="step-toolbar-button-disable"]').click(); - cy.openStepConfigurationTab('setHeader'); - - // Temporary workaround since the toolbar is updated but the config form is closed - cy.openStepConfigurationTab('setHeader'); - cy.checkConfigCheckboxObject('disabled', false); }); diff --git a/packages/ui-tests/cypress/e2e/designer/propsWarnings/mandatoryPropsWarnings.cy.ts b/packages/ui-tests/cypress/e2e/designer/propsWarnings/mandatoryPropsWarnings.cy.ts index 72289b7ba..1e862f7f9 100644 --- a/packages/ui-tests/cypress/e2e/designer/propsWarnings/mandatoryPropsWarnings.cy.ts +++ b/packages/ui-tests/cypress/e2e/designer/propsWarnings/mandatoryPropsWarnings.cy.ts @@ -18,8 +18,8 @@ describe('Test for missing config props canvas warnings', () => { cy.openStepConfigurationTab('github'); + /** The update should happen automatically after filling a property */ cy.interactWithConfigInputObject('parameters.repoName', 'test'); - cy.closeStepConfigurationTab(); cy.get('[data-id^="camel-route|route.from.steps.1.to"] g') .find('span[data-warning="true"].pf-v5-c-icon') @@ -36,8 +36,8 @@ describe('Test for missing config props canvas warnings', () => { cy.openStepConfigurationTab('delay-action'); + /** The update should happen automatically after filling a property */ cy.interactWithConfigInputObject('milliseconds', '1000'); - cy.closeStepConfigurationTab(); cy.get('[data-id^="webhook-binding|delay-action"] g') .find('span[data-warning="true"].pf-v5-c-icon') diff --git a/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/propertiesFilter.cy.ts b/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/propertiesFilter.cy.ts index 32ba8c828..3d7eaaf9c 100644 --- a/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/propertiesFilter.cy.ts +++ b/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/propertiesFilter.cy.ts @@ -86,6 +86,8 @@ describe('Tests for side panel step filtering', () => { cy.get(`input[name="variableSend"]`).should('exist'); cy.get(`input[name="variableReceive"]`).should('exist'); + cy.checkConfigInputObject('variableSend', 'testVariableSend'); + cy.checkConfigInputObject('variableReceive', 'testVariableReceive'); cy.get(`textarea[name="description"]`).should('not.exist'); cy.get(`input[name="id"]`).should('not.exist'); diff --git a/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/stepConfiguration.cy.ts b/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/stepConfiguration.cy.ts index 24feb6cbb..0b7526939 100644 --- a/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/stepConfiguration.cy.ts +++ b/packages/ui-tests/cypress/e2e/designer/sidepanelConfig/stepConfiguration.cy.ts @@ -35,4 +35,25 @@ describe('Tests for Design page', () => { cy.checkCodeSpanLine('user: user'); cy.checkCodeSpanLine('password: password'); }); + + it('Disable and Enable steps using the step configuration', () => { + cy.uploadFixture('flows/camelRoute/basic.yaml'); + cy.openDesignPage(); + + cy.openStepConfigurationTab('setHeader'); + + /** Check the toolbar's Disable button is on "Disable step" mode now */ + cy.get('[data-testid="step-toolbar-button-disable"]').should('have.attr', 'title', 'Disable step'); + /** Check the node now is in disabled mode */ + cy.get(`g[data-nodelabel="setHeader"]`).should('have.attr', 'data-disabled', 'false'); + + cy.selectFormTab('All'); + cy.get('#expandable-section-toggle-processor-advanced').click(); + cy.interactWithConfigInputObject('disabled'); + + /** Check the toolbar's Disable button is on "Enable step" mode now */ + cy.get('[data-testid="step-toolbar-button-disable"]').should('have.attr', 'title', 'Enable step'); + /** Check the node now is in disabled mode */ + cy.get(`g[data-nodelabel="setHeader"]`).should('have.attr', 'data-disabled', 'true'); + }); }); diff --git a/packages/ui/src/components/Form/properties/PropertiesField.tsx b/packages/ui/src/components/Form/properties/PropertiesField.tsx index 37e856fc1..c00d41ee4 100644 --- a/packages/ui/src/components/Form/properties/PropertiesField.tsx +++ b/packages/ui/src/components/Form/properties/PropertiesField.tsx @@ -18,7 +18,7 @@ import { PropertyRow } from './PropertyRow'; * @constructor */ export const PropertiesField = connectField((props: IPropertiesField) => { - const propertiesModel = props.value ? { ...props.value } : {}; + const propertiesModel = props.value; const [isFieldExpanded, setFieldExpanded] = useState(Object.keys(propertiesModel).length > 0); const [expandedNodes, setExpandedNodes] = useState([]); const [placeholderState, setPlaceholderState] = useState(null); diff --git a/packages/ui/src/components/Form/properties/PropertyRow.tsx b/packages/ui/src/components/Form/properties/PropertyRow.tsx index ae4791bd2..38d67c83a 100644 --- a/packages/ui/src/components/Form/properties/PropertyRow.tsx +++ b/packages/ui/src/components/Form/properties/PropertyRow.tsx @@ -85,9 +85,6 @@ export function PropertyRow({ setUserInputName(nodeName); setUserInputValue(nodeValue); setIsEditing(false); - if (isPlaceholder) { - onChangeModel(); - } } function getKey() { diff --git a/packages/ui/src/components/MetadataEditor/MetadataEditor.test.tsx b/packages/ui/src/components/MetadataEditor/MetadataEditor.test.tsx index ac28412c0..58203456e 100644 --- a/packages/ui/src/components/MetadataEditor/MetadataEditor.test.tsx +++ b/packages/ui/src/components/MetadataEditor/MetadataEditor.test.tsx @@ -195,20 +195,10 @@ describe('MetadataEditor.tsx', () => { }); test('add string property and cancel', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const beans: any[] = cloneDeep(mockModel.beansNoProp); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let changed: any; - render( - { - changed = model; - }} - />, - ); + const onChangeModelSpy = jest.fn(); + const beans = cloneDeep(mockModel.beansNoProp); + + render(); const row = screen.getByTestId('metadata-row-0'); fireEvent.click(row); @@ -221,24 +211,14 @@ describe('MetadataEditor.tsx', () => { const cancelBtn = screen.getByTestId('properties--placeholder-property-edit-cancel--btn'); fireEvent.click(cancelBtn); - expect(changed[0].properties.propStr).toBeFalsy(); + expect(onChangeModelSpy).not.toHaveBeenCalled(); }); test('add object property and cancel', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const beans: any[] = cloneDeep(mockModel.beansNoProp); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let changed: any; - render( - { - changed = model; - }} - />, - ); + const onChangeModelSpy = jest.fn(); + const beans = cloneDeep(mockModel.beansNoProp); + + render(); const row = screen.getByTestId('metadata-row-0'); fireEvent.click(row); @@ -249,7 +229,7 @@ describe('MetadataEditor.tsx', () => { const objCancelBtn = screen.getByTestId('properties--placeholder-property-edit-cancel--btn'); fireEvent.click(objCancelBtn); - expect(changed[0].properties.propObj).toBeFalsy(); + expect(onChangeModelSpy).not.toHaveBeenCalled(); }); test('change string property and confirm', async () => { diff --git a/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.test.tsx b/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.test.tsx index a45dd8aed..a5f88fe94 100644 --- a/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.test.tsx +++ b/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.test.tsx @@ -1,6 +1,7 @@ import catalogLibrary from '@kaoto/camel-catalog/index.json'; -import { CatalogLibrary, RouteDefinition } from '@kaoto/camel-catalog/types'; -import { act, fireEvent, render, screen } from '@testing-library/react'; +import { CatalogLibrary, FromDefinition } from '@kaoto/camel-catalog/types'; +import { act, fireEvent, render } from '@testing-library/react'; +import { FunctionComponent, PropsWithChildren } from 'react'; import { CamelCatalogService, CamelRouteVisualEntity, @@ -9,10 +10,11 @@ import { ICamelProcessorDefinition, IKameletDefinition, } from '../../../../models'; -import { IVisualizationNode } from '../../../../models/visualization/base-visual-entity'; -import { VisibleFlowsProvider, CanvasFormTabsContext } from '../../../../providers'; -import { EntitiesContext } from '../../../../providers/entities.provider'; +import { CamelRouteResource } from '../../../../models/camel/camel-route-resource'; +import { CanvasFormTabsContext, SourceCodeProvider } from '../../../../providers'; +import { TestProvidersWrapper } from '../../../../stubs'; import { getFirstCatalogMap } from '../../../../stubs/test-load-catalog'; +import { EventNotifier } from '../../../../utils'; import { SchemaService } from '../../../Form'; import { CanvasNode } from '../canvas.models'; import { CanvasFormBody } from './CanvasFormBody'; @@ -21,6 +23,9 @@ describe('CanvasFormBody', () => { let componentCatalogMap: Record; let patternCatalogMap: Record; let kameletCatalogMap: Record; + let Provider: FunctionComponent; + let entity: CamelRouteVisualEntity; + let selectedNode: CanvasNode; beforeAll(async () => { const catalogsMap = await getFirstCatalogMap(catalogLibrary as CatalogLibrary); @@ -38,13 +43,32 @@ describe('CanvasFormBody', () => { CamelCatalogService.setCatalogKey(CatalogKind.Entity, catalogsMap.entitiesCatalog); }); + const setupProvider = (camelFrom: { from: FromDefinition }) => { + const camelResource = new CamelRouteResource([camelFrom]); + const result = TestProvidersWrapper({ camelResource }); + Provider = result.Provider; + entity = camelResource.getVisualEntities()[0]; + const rootNode = entity.toVizNode(); + const setHeaderNode = rootNode.getChildren()![1]; + selectedNode = { + id: '1', + type: 'node', + data: { + vizNode: setHeaderNode, + }, + }; + + (result.updateSourceCodeFromEntitiesSpy as jest.Mock).mockImplementation(() => { + const eventNotifier = EventNotifier.getInstance(); + const code = camelResource.toString(); + eventNotifier.next('entities:updated', code); + }); + }; + describe('should persists changes from both expression editor and main form', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - it('expression => main form', async () => { - const camelRoute = { + const camelFrom: { from: FromDefinition } = { from: { uri: 'timer', parameters: { @@ -58,138 +82,97 @@ describe('CanvasFormBody', () => { }, ], }, - } as RouteDefinition; - const entity = new CamelRouteVisualEntity(camelRoute); - const rootNode: IVisualizationNode = entity.toVizNode(); - const setHeaderNode = rootNode.getChildren()![1]; - const selectedNode = { - id: '1', - type: 'node', - data: { - vizNode: setHeaderNode, - }, }; - render( - - - - - - - , + setupProvider(camelFrom); + }); + + it('expression => main form', async () => { + const wrapper = render( + + + + + , ); - const button = screen - .getAllByTestId('typeahead-select-input') - .filter((input) => input.innerHTML.includes(SchemaService.DROPDOWN_PLACEHOLDER)); - act(() => { - fireEvent.click(button[0]); + + await act(async () => { + const expressionDropdownTrigger = wrapper.getByPlaceholderText(SchemaService.DROPDOWN_PLACEHOLDER); + fireEvent.click(expressionDropdownTrigger); }); - const simple = screen.getByTestId('expression-dropdownitem-simple'); - act(() => { + + await act(async () => { + const simple = wrapper.getByTestId('expression-dropdownitem-simple'); fireEvent.click(simple.getElementsByTagName('button')[0]); }); - const expressionInput = screen - .getAllByRole('textbox') - .filter((textbox) => textbox.getAttribute('name') === 'expression'); - act(() => { - fireEvent.input(expressionInput[0], { target: { value: '${header.foo}' } }); + + await act(async () => { + const expressionInput = wrapper.getByRole('textbox', { name: 'expression' }); + fireEvent.input(expressionInput, { target: { value: '${header.foo}' } }); }); + /* eslint-disable @typescript-eslint/no-explicit-any */ - expect((camelRoute.from.steps[0].setHeader!.expression as any).simple.expression).toEqual('${header.foo}'); - expect(camelRoute.from.steps[0].setHeader!.name).toEqual('foo'); + expect((entity.entityDef.route.from.steps[0].setHeader!.expression as any).simple.expression).toEqual( + '${header.foo}', + ); + expect(entity.entityDef.route.from.steps[0].setHeader!.name).toEqual('foo'); - const filtered = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Name'); - act(() => { - fireEvent.input(filtered[0], { target: { value: 'bar' } }); + await act(async () => { + const nameInput = wrapper.getByLabelText(/name \*/i); + fireEvent.input(nameInput, { target: { value: 'bar' } }); }); + /* eslint-disable @typescript-eslint/no-explicit-any */ - expect((camelRoute.from.steps[0].setHeader!.expression as any).simple.expression).toEqual('${header.foo}'); - expect(camelRoute.from.steps[0].setHeader!.name).toEqual('bar'); + expect((entity.entityDef.route.from.steps[0].setHeader!.expression as any).simple.expression).toEqual( + '${header.foo}', + ); + expect(entity.entityDef.route.from.steps[0].setHeader!.name).toEqual('bar'); }); it('main form => expression', async () => { - const camelRoute = { - from: { - uri: 'timer', - parameters: { - timerName: 'tutorial', - }, - steps: [ - { - setHeader: { - name: 'foo', - }, - }, - ], - }, - } as RouteDefinition; - const entity = new CamelRouteVisualEntity(camelRoute); - const rootNode: IVisualizationNode = entity.toVizNode(); - const setHeaderNode = rootNode.getChildren()![1]; - const selectedNode = { - id: '1', - type: 'node', - data: { - vizNode: setHeaderNode, - }, - }; - - render( - - - - - - - , + const wrapper = render( + + + + + , ); - const filtered = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Name'); - act(() => { - fireEvent.input(filtered[0], { target: { value: 'bar' } }); + + await act(async () => { + const nameInput = wrapper.getByLabelText(/name \*/i); + fireEvent.input(nameInput, { target: { value: 'bar' } }); }); - expect(camelRoute.from.steps[0].setHeader!.expression).toBeUndefined(); - expect(camelRoute.from.steps[0].setHeader!.name).toEqual('bar'); - - const button = screen - .getAllByTestId('typeahead-select-input') - .filter((input) => input.innerHTML.includes(SchemaService.DROPDOWN_PLACEHOLDER)); - act(() => { - fireEvent.click(button[0]); + + expect(entity.entityDef.route.from.steps[0].setHeader!.expression).toBeUndefined(); + expect(entity.entityDef.route.from.steps[0].setHeader!.name).toEqual('bar'); + + await act(async () => { + const expressionDropdownTrigger = wrapper.getByPlaceholderText(SchemaService.DROPDOWN_PLACEHOLDER); + fireEvent.click(expressionDropdownTrigger); }); - const simple = screen.getByTestId('expression-dropdownitem-simple'); - act(() => { + + await act(async () => { + const simple = wrapper.getByTestId('expression-dropdownitem-simple'); fireEvent.click(simple.getElementsByTagName('button')[0]); }); - const expressionInput = screen - .getAllByRole('textbox') - .filter((textbox) => textbox.getAttribute('name') === 'expression'); - act(() => { - fireEvent.input(expressionInput[0], { target: { value: '${header.foo}' } }); + + await act(async () => { + const expressionInput = wrapper.getByRole('textbox', { name: 'expression' }); + fireEvent.input(expressionInput, { target: { value: '${header.foo}' } }); }); + /* eslint-disable @typescript-eslint/no-explicit-any */ - expect((camelRoute.from.steps[0].setHeader!.expression as any).simple.expression).toEqual('${header.foo}'); - expect(camelRoute.from.steps[0].setHeader!.name).toEqual('bar'); + expect((entity.entityDef.route.from.steps[0].setHeader!.expression as any).simple.expression).toEqual( + '${header.foo}', + ); + expect(entity.entityDef.route.from.steps[0].setHeader!.name).toEqual('bar'); }); }); describe('should persists changes from both dataformat editor and main form', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - it('dataformat => main form', async () => { - const camelRoute = { + const camelFrom: { from: FromDefinition } = { from: { uri: 'timer', parameters: { @@ -203,121 +186,92 @@ describe('CanvasFormBody', () => { }, ], }, - } as RouteDefinition; - const entity = new CamelRouteVisualEntity(camelRoute); - const rootNode: IVisualizationNode = entity.toVizNode(); - const marshalNode = rootNode.getChildren()![1]; - const selectedNode = { - id: '1', - type: 'node', - data: { - vizNode: marshalNode, - }, }; - render( - - + setupProvider(camelFrom); + }); + + it('dataformat => main form', async () => { + const wrapper = render( + + - + - - , + + , ); - const button = screen.getAllByRole('button', { name: 'Typeahead menu toggle' }); await act(async () => { - fireEvent.click(button[0]); + const dataformatDropdownTrigger = wrapper.getByPlaceholderText(SchemaService.DROPDOWN_PLACEHOLDER); + fireEvent.click(dataformatDropdownTrigger); }); - const avro = screen.getByTestId('dataformat-dropdownitem-avro'); + await act(async () => { + const avro = wrapper.getByTestId('dataformat-dropdownitem-avro'); fireEvent.click(avro.getElementsByTagName('button')[0]); }); - expect(camelRoute.from.steps[0].marshal!.avro).toBeDefined(); - expect(camelRoute.from.steps[0].marshal!.id).toEqual('ms'); - const idInput = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Id'); + expect(entity.entityDef.route.from.steps[0].marshal!.avro).toBeDefined(); + expect(entity.entityDef.route.from.steps[0].marshal!.id).toEqual('ms'); + await act(async () => { - fireEvent.input(idInput[1], { target: { value: 'modified' } }); + const [_avroIdInput, marshalIdInput] = wrapper.getAllByLabelText('Id', { selector: 'input' }); + fireEvent.input(marshalIdInput, { target: { value: 'modified' } }); }); - expect(camelRoute.from.steps[0].marshal!.avro).toBeDefined(); - expect(camelRoute.from.steps[0].marshal!.id).toEqual('modified'); + + expect(entity.entityDef.route.from.steps[0].marshal!.avro).toBeDefined(); + expect(entity.entityDef.route.from.steps[0].marshal!.id).toEqual('modified'); }); it('main form => dataformat', async () => { - const camelRoute = { - from: { - uri: 'timer', - parameters: { - timerName: 'tutorial', - }, - steps: [ - { - marshal: { - id: 'ms', - }, - }, - ], - }, - } as RouteDefinition; - const entity = new CamelRouteVisualEntity(camelRoute); - const rootNode: IVisualizationNode = entity.toVizNode(); - const marshalNode = rootNode.getChildren()![1]; - const selectedNode = { - id: '1', - type: 'node', - data: { - vizNode: marshalNode, - }, - }; - - render( - - + const wrapper = render( + + - + - - , + + , ); - const idInput = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Id'); await act(async () => { - fireEvent.input(idInput[0], { target: { value: 'modified' } }); + const [avroIdInput] = wrapper.getAllByLabelText('Id', { selector: 'input' }); + fireEvent.input(avroIdInput, { target: { value: 'modified' } }); }); - expect(camelRoute.from.steps[0].marshal!.avro).toBeUndefined(); - expect(camelRoute.from.steps[0].marshal!.id).toEqual('modified'); - const button = screen.getAllByRole('button', { name: 'Typeahead menu toggle' }); + expect(entity.entityDef.route.from.steps[0].marshal!.avro).toBeUndefined(); + expect(entity.entityDef.route.from.steps[0].marshal!.id).toEqual('modified'); + await act(async () => { - fireEvent.click(button[0]); + const dataformatDropdownTrigger = wrapper.getByPlaceholderText(SchemaService.DROPDOWN_PLACEHOLDER); + fireEvent.click(dataformatDropdownTrigger); }); - const avro = screen.getByTestId('dataformat-dropdownitem-avro'); + await act(async () => { + const avro = wrapper.getByTestId('dataformat-dropdownitem-avro'); fireEvent.click(avro.getElementsByTagName('button')[0]); }); - expect(camelRoute.from.steps[0].marshal!.avro).toBeDefined(); - expect(camelRoute.from.steps[0].marshal!.id).toEqual('modified'); + + expect(entity.entityDef.route.from.steps[0].marshal!.avro).toBeDefined(); + expect(entity.entityDef.route.from.steps[0].marshal!.id).toEqual('modified'); }); }); describe('should persists changes from both loadbalancer editor and main form', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - it('loadbalancer => main form', async () => { - const camelRoute = { + const camelFrom: { from: FromDefinition } = { from: { uri: 'timer', parameters: { @@ -331,111 +285,93 @@ describe('CanvasFormBody', () => { }, ], }, - } as RouteDefinition; - const entity = new CamelRouteVisualEntity(camelRoute); - const rootNode: IVisualizationNode = entity.toVizNode(); - const loadBalanceNode = rootNode.getChildren()![1]; - const selectedNode = { - id: '1', - type: 'node', - data: { - vizNode: loadBalanceNode, - }, }; - render( - - + setupProvider(camelFrom); + }); + + it('loadbalancer => main form', async () => { + const wrapper = render( + + - + - - , + + , ); - const button = screen.getAllByRole('button', { name: 'Typeahead menu toggle' }); await act(async () => { - fireEvent.click(button[0]); + const expressionDropdownTrigger = wrapper.getByPlaceholderText(SchemaService.DROPDOWN_PLACEHOLDER); + fireEvent.click(expressionDropdownTrigger); }); - const weightedLoadBalancer = screen.getByTestId('loadbalancer-dropdownitem-weightedLoadBalancer'); + await act(async () => { + const weightedLoadBalancer = wrapper.getByTestId('loadbalancer-dropdownitem-weightedLoadBalancer'); fireEvent.click(weightedLoadBalancer.getElementsByTagName('button')[0]); }); - expect(camelRoute.from.steps[0].loadBalance!.weightedLoadBalancer).toBeDefined(); - expect(camelRoute.from.steps[0].loadBalance!.id).toEqual('lb'); - const idInput = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Id'); + expect(entity.entityDef.route.from.steps[0].loadBalance!.weightedLoadBalancer).toBeDefined(); + expect(entity.entityDef.route.from.steps[0].loadBalance!.id).toEqual('lb'); + + const [weightedLBIdInput, loadBalanceIdInput] = wrapper.getAllByLabelText('Id', { selector: 'input' }); + await act(async () => { + fireEvent.input(weightedLBIdInput, { target: { value: 'modified-lb-id' } }); + }); + await act(async () => { - fireEvent.input(idInput[1], { target: { value: 'modified' } }); + fireEvent.input(loadBalanceIdInput, { target: { value: 'modified-id' } }); }); - expect(camelRoute.from.steps[0].loadBalance!.weightedLoadBalancer).toBeDefined(); - expect(camelRoute.from.steps[0].loadBalance!.id).toEqual('modified'); + + expect(entity.entityDef.route.from.steps[0].loadBalance!.weightedLoadBalancer).toBeDefined(); + /* eslint-disable @typescript-eslint/no-explicit-any */ + expect((entity.entityDef.route.from.steps[0].loadBalance!.weightedLoadBalancer as any).id).toEqual( + 'modified-lb-id', + ); + expect(entity.entityDef.route.from.steps[0].loadBalance!.id).toEqual('modified-id'); }); it('main form => loadbalancer', async () => { - const camelRoute = { - from: { - uri: 'timer', - parameters: { - timerName: 'tutorial', - }, - steps: [ - { - loadBalance: { - id: 'lb', - }, - }, - ], - }, - } as RouteDefinition; - const entity = new CamelRouteVisualEntity(camelRoute); - const rootNode: IVisualizationNode = entity.toVizNode(); - const loadBalanceNode = rootNode.getChildren()![1]; - const selectedNode = { - id: '1', - type: 'node', - data: { - vizNode: loadBalanceNode, - }, - }; - - render( - - + const wrapper = render( + + - + - - , + + , ); - const idInput = screen.getAllByRole('textbox').filter((textbox) => textbox.getAttribute('label') === 'Id'); await act(async () => { - fireEvent.input(idInput[0], { target: { value: 'modified' } }); + const loadBalanceIdInput = wrapper.getByLabelText('Id', { selector: 'input' }); + fireEvent.input(loadBalanceIdInput, { target: { value: 'modified' } }); }); - expect(camelRoute.from.steps[0].loadBalance!.weightedLoadBalancer).toBeUndefined(); - expect(camelRoute.from.steps[0].loadBalance!.id).toEqual('modified'); - const button = screen.getAllByRole('button', { name: 'Typeahead menu toggle' }); + expect(entity.entityDef.route.from.steps[0].loadBalance!.weightedLoadBalancer).toBeUndefined(); + expect(entity.entityDef.route.from.steps[0].loadBalance!.id).toEqual('modified'); + await act(async () => { - fireEvent.click(button[0]); + const expressionDropdownTrigger = wrapper.getByPlaceholderText(SchemaService.DROPDOWN_PLACEHOLDER); + fireEvent.click(expressionDropdownTrigger); }); - const weighted = screen.getByTestId('loadbalancer-dropdownitem-weightedLoadBalancer'); + await act(async () => { - fireEvent.click(weighted.getElementsByTagName('button')[0]); + const weightedLoadBalancer = wrapper.getByTestId('loadbalancer-dropdownitem-weightedLoadBalancer'); + fireEvent.click(weightedLoadBalancer.getElementsByTagName('button')[0]); }); - expect(camelRoute.from.steps[0].loadBalance!.weightedLoadBalancer).toBeDefined(); - expect(camelRoute.from.steps[0].loadBalance!.id).toEqual('modified'); + + expect(entity.entityDef.route.from.steps[0].loadBalance!.weightedLoadBalancer).toBeDefined(); + expect(entity.entityDef.route.from.steps[0].loadBalance!.id).toEqual('modified'); }); }); }); diff --git a/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx b/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx index d8f7b9b94..cb3c6d042 100644 --- a/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx +++ b/packages/ui/src/components/Visualization/Canvas/Form/CanvasFormBody.tsx @@ -1,35 +1,37 @@ -import { FunctionComponent, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; -import { EntitiesContext } from '../../../../providers/entities.provider'; +import { FunctionComponent, useContext, useEffect, useRef } from 'react'; +import { useVizNodeModel } from '../../../../hooks'; +import { CanvasFormTabsContext } from '../../../../providers/canvas-form-tabs.provider'; import { SchemaBridgeProvider } from '../../../../providers/schema-bridge.provider'; -import { getUserUpdatedPropertiesSchema, getRequiredPropertiesSchema, isDefined, setValue } from '../../../../utils'; +import { getRequiredPropertiesSchema, getUserUpdatedPropertiesSchema, isDefined, setValue } from '../../../../utils'; import { CustomAutoForm, CustomAutoFormRef } from '../../../Form/CustomAutoForm'; import { DataFormatEditor } from '../../../Form/dataFormat/DataFormatEditor'; import { LoadBalancerEditor } from '../../../Form/loadBalancer/LoadBalancerEditor'; import { StepExpressionEditor } from '../../../Form/stepExpression/StepExpressionEditor'; import { UnknownNode } from '../../Custom/UnknownNode'; import { CanvasNode } from '../canvas.models'; -import { CanvasFormTabsContext } from '../../../../providers/canvas-form-tabs.provider'; interface CanvasFormTabsProps { selectedNode: CanvasNode; } export const CanvasFormBody: FunctionComponent = (props) => { - const entitiesContext = useContext(EntitiesContext); + const vizNode = props.selectedNode.data?.vizNode; + if (!vizNode) { + throw new Error('CanvasFormBody must be used only on Node elements with an available IVisualizationNode'); + } + const { selectedTab } = useContext(CanvasFormTabsContext) ?? { selectedTab: 'Required' }; const divRef = useRef(null); const formRef = useRef(null); - const omitFields = useRef(props.selectedNode.data?.vizNode?.getOmitFormFields() || []); + const omitFields = useRef(vizNode.getOmitFormFields() || []); - const visualComponentSchema = useMemo(() => { - const answer = props.selectedNode.data?.vizNode?.getComponentSchema(); - // Overriding parameters with an empty object When the parameters property is mistakenly set to null - if (answer?.definition?.parameters === null) { - answer!.definition.parameters = {}; - } - return answer; - }, [props.selectedNode.data?.vizNode, selectedTab]); - const model = visualComponentSchema?.definition; + const { model, updateModel } = useVizNodeModel>(vizNode); + // Overriding parameters with an empty object When the parameters property is mistakenly set to null + if (model.parameters === null) { + model.parameters = {}; + } + + const visualComponentSchema = vizNode.getComponentSchema(); let processedSchema = visualComponentSchema?.schema; if (selectedTab === 'Required') { processedSchema = getRequiredPropertiesSchema(visualComponentSchema?.schema ?? {}); @@ -40,68 +42,48 @@ export const CanvasFormBody: FunctionComponent = (props) => }; } - useEffect(() => { - formRef.current?.form.reset(); - }, [props.selectedNode.data?.vizNode, selectedTab]); + const comment = visualComponentSchema?.schema?.['$comment'] ?? ''; + const isExpressionAwareStep = comment.includes('expression'); + const isDataFormatAwareStep = comment.includes('dataformat'); + const isLoadBalanceAwareStep = comment.includes('loadbalance'); + const isUnknownComponent = + !isDefined(visualComponentSchema) || + !isDefined(visualComponentSchema.schema) || + Object.keys(visualComponentSchema.schema).length === 0; - const stepFeatures = useMemo(() => { - const comment = visualComponentSchema?.schema?.['$comment'] ?? ''; - const isExpressionAwareStep = comment.includes('expression'); - const isDataFormatAwareStep = comment.includes('dataformat'); - const isLoadBalanceAwareStep = comment.includes('loadbalance'); - const isUnknownComponent = - !isDefined(visualComponentSchema) || - !isDefined(visualComponentSchema.schema) || - Object.keys(visualComponentSchema.schema).length === 0; - return { isExpressionAwareStep, isDataFormatAwareStep, isLoadBalanceAwareStep, isUnknownComponent }; - }, [visualComponentSchema]); + const handleOnChangeIndividualProp = (path: string, value: unknown) => { + let updatedValue = value; + if (typeof value === 'string' && value.trim() === '') { + updatedValue = undefined; + } - const handleOnChangeIndividualProp = useCallback( - (path: string, value: unknown) => { - if (!props.selectedNode.data?.vizNode) { - return; - } + setValue(model, path, updatedValue); + updateModel(model); + }; - let updatedValue = value; - if (typeof value === 'string' && value.trim() === '') { - updatedValue = undefined; - } + useEffect(() => { + formRef.current?.form.reset(); + }, [vizNode, selectedTab]); - const newModel = props.selectedNode.data.vizNode.getComponentSchema()?.definition || {}; - setValue(newModel, path, updatedValue); - props.selectedNode.data.vizNode.updateModel(newModel); - entitiesContext?.updateSourceCodeFromEntities(); - }, - [entitiesContext, props.selectedNode.data?.vizNode], - ); + if (isUnknownComponent) { + return ; + } return ( - <> - {stepFeatures.isUnknownComponent ? ( - - ) : ( - - {stepFeatures.isExpressionAwareStep && ( - - )} - {stepFeatures.isDataFormatAwareStep && ( - - )} - {stepFeatures.isLoadBalanceAwareStep && ( - - )} - -
- - )} - + + {isExpressionAwareStep && } + {isDataFormatAwareStep && } + {isLoadBalanceAwareStep && } + +
+ ); }; diff --git a/packages/ui/src/components/Visualization/Canvas/Form/__snapshots__/CanvasForm.test.tsx.snap b/packages/ui/src/components/Visualization/Canvas/Form/__snapshots__/CanvasForm.test.tsx.snap index 18b04f08f..9723c2b7a 100644 --- a/packages/ui/src/components/Visualization/Canvas/Form/__snapshots__/CanvasForm.test.tsx.snap +++ b/packages/ui/src/components/Visualization/Canvas/Form/__snapshots__/CanvasForm.test.tsx.snap @@ -474,7 +474,7 @@ exports[`CanvasForm should render nothing if no schema and no definition is avai - null + {} @@ -729,7 +729,10 @@ exports[`CanvasForm should render nothing if no schema is available 1`] = ` > + > + {} + +
diff --git a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx index bbded3312..a0ccc7e78 100644 --- a/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx +++ b/packages/ui/src/components/Visualization/Custom/Node/CustomNode.tsx @@ -41,6 +41,7 @@ import { TargetAnchor } from '../target-anchor'; import './CustomNode.scss'; import { useEntityContext } from '../../../../hooks/useEntityContext/useEntityContext'; import { customNodeDropTargetSpec } from '../customComponentUtils'; +import { useVizNodeModel } from '../../../../hooks'; type DefaultNodeProps = Parameters[0]; @@ -61,10 +62,8 @@ const CustomNode: FunctionComponent = observer( const controller = useVisualizationController(); const settingsAdapter = useContext(SettingsContext); const label = vizNode?.getNodeLabel(settingsAdapter.getSettings().nodeLabel); - const isDisabled = !!vizNode?.getComponentSchema()?.definition?.disabled; const tooltipContent = vizNode?.getTooltipContent(); const validationText = vizNode?.getNodeValidationText(); - const doesHaveWarnings = !isDisabled && !!validationText; const [isSelected, onSelect] = useSelection(); const [isGHover, gHoverRef] = useHover(CanvasDefaults.HOVER_DELAY_IN, CanvasDefaults.HOVER_DELAY_OUT); const [isToolbarHover, toolbarHoverRef] = useHover( @@ -135,6 +134,10 @@ const CustomNode: FunctionComponent = observer( return null; } + const { model } = useVizNodeModel<{ disabled?: boolean }>(vizNode); + const isDisabled = !!model.disabled; + const doesHaveWarnings = !isDisabled && !!validationText; + return ( { - const entitiesContext = useContext(EntitiesContext); - const isDisabled = !!vizNode.getComponentSchema()?.definition?.disabled; + const { model, updateModel } = useVizNodeModel<{ disabled?: boolean }>(vizNode); + const isDisabled = !!model.disabled; const onToggleDisableNode = useCallback(() => { - const newModel = vizNode.getComponentSchema()?.definition || {}; - setValue(newModel, 'disabled', !isDisabled); - vizNode.updateModel(newModel); - - entitiesContext?.updateEntitiesFromCamelResource(); - }, [entitiesContext, isDisabled, vizNode]); + const newModel = { ...model, disabled: !isDisabled }; + updateModel(newModel); + }, [isDisabled, model, updateModel]); const value = useMemo( () => ({ diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts index b1886c631..d95e58dd8 100644 --- a/packages/ui/src/hooks/index.ts +++ b/packages/ui/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from './applied-schema.hook'; export * from './entities'; export * from './local-storage.hook'; export * from './schema-bridge.hook'; +export * from './viznode-model'; diff --git a/packages/ui/src/hooks/viznode-model.ts b/packages/ui/src/hooks/viznode-model.ts new file mode 100644 index 000000000..a51ae3272 --- /dev/null +++ b/packages/ui/src/hooks/viznode-model.ts @@ -0,0 +1,18 @@ +import { useContext } from 'react'; +import { IVisualizationNode } from '../models'; +import { EntitiesContext, SourceCodeContext } from '../providers'; + +export const useVizNodeModel = ( + vizNode: IVisualizationNode, +): { model: T; updateModel: (model: unknown) => void; sourceCode: string } => { + const entitiesContext = useContext(EntitiesContext); + const sourceCode = useContext(SourceCodeContext); + const model = vizNode.getComponentSchema()?.definition ?? {}; + + const updateModel = (newModel: unknown) => { + vizNode.updateModel(newModel); + entitiesContext?.updateSourceCodeFromEntities(); + }; + + return { model, updateModel, sourceCode }; +}; diff --git a/packages/ui/src/stubs/kamelet-route.ts b/packages/ui/src/stubs/kamelet-route.ts index 8ebdd7d34..c8fbbb3d2 100644 --- a/packages/ui/src/stubs/kamelet-route.ts +++ b/packages/ui/src/stubs/kamelet-route.ts @@ -1,4 +1,5 @@ import { parse } from 'yaml'; +import { IKameletDefinition } from '../models/kamelets-catalog'; /** * This is a stub Kamelet in YAML format. @@ -53,4 +54,4 @@ spec: * This is a stub Kamelet in JSON format. * It is used to test the Canvas component. */ -export const kameletJson = parse(kameletYaml); +export const kameletJson: IKameletDefinition = parse(kameletYaml); diff --git a/packages/ui/src/utils/get-custom-schema-from-kamelet.test.ts b/packages/ui/src/utils/get-custom-schema-from-kamelet.test.ts index 9732f1bae..c29f44f56 100644 --- a/packages/ui/src/utils/get-custom-schema-from-kamelet.test.ts +++ b/packages/ui/src/utils/get-custom-schema-from-kamelet.test.ts @@ -2,76 +2,13 @@ import { cloneDeep } from 'lodash'; import { SourceSchemaType } from '../models/camel/source-schema-type'; import { IKameletDefinition, IKameletSpecProperty } from '../models/kamelets-catalog'; import { getCustomSchemaFromKamelet } from './get-custom-schema-from-kamelet'; +import { kameletJson } from '../stubs'; describe('getCustomSchemaFromKamelet', () => { let inputKameletStruct: IKameletDefinition; beforeEach(() => { - inputKameletStruct = { - apiVersion: 'camel.apache.org/v1', - kind: SourceSchemaType.Kamelet, - metadata: { - annotations: { - 'camel.apache.org/catalog.version': 'main-SNAPSHOT', - 'camel.apache.org/kamelet.group': 'Users', - 'camel.apache.org/kamelet.icon': - 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNjAgNjAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYwIDYwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik00OC4wMTQsNDIuODg5bC05LjU1My00Ljc3NkMzNy41NiwzNy42NjIsMzcsMzYuNzU2LDM3LDM1Ljc0OHYtMy4zODFjMC4yMjktMC4yOCwwLjQ3LTAuNTk5LDAuNzE5LTAuOTUxCgljMS4yMzktMS43NSwyLjIzMi0zLjY5OCwyLjk1NC01Ljc5OUM0Mi4wODQsMjQuOTcsNDMsMjMuNTc1LDQzLDIydi00YzAtMC45NjMtMC4zNi0xLjg5Ni0xLTIuNjI1di01LjMxOQoJYzAuMDU2LTAuNTUsMC4yNzYtMy44MjQtMi4wOTItNi41MjVDMzcuODU0LDEuMTg4LDM0LjUyMSwwLDMwLDBzLTcuODU0LDEuMTg4LTkuOTA4LDMuNTNDMTcuNzI0LDYuMjMxLDE3Ljk0NCw5LjUwNiwxOCwxMC4wNTYKCXY1LjMxOWMtMC42NCwwLjcyOS0xLDEuNjYyLTEsMi42MjV2NGMwLDEuMjE3LDAuNTUzLDIuMzUyLDEuNDk3LDMuMTA5YzAuOTE2LDMuNjI3LDIuODMzLDYuMzYsMy41MDMsNy4yMzd2My4zMDkKCWMwLDAuOTY4LTAuNTI4LDEuODU2LTEuMzc3LDIuMzJsLTguOTIxLDQuODY2QzguODAxLDQ0LjQyNCw3LDQ3LjQ1OCw3LDUwLjc2MlY1NGMwLDQuNzQ2LDE1LjA0NSw2LDIzLDZzMjMtMS4yNTQsMjMtNnYtMy4wNDMKCUM1Myw0Ny41MTksNTEuMDg5LDQ0LjQyNyw0OC4wMTQsNDIuODg5eiIvPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K', - 'camel.apache.org/kamelet.namespace': 'test', - 'camel.apache.org/kamelet.support.level': 'Stable', - 'camel.apache.org/provider': 'Apache Software Foundation', - foo: 'bar', - }, - labels: { - 'camel.apache.org/kamelet.type': 'Action', - type: 'Action', - }, - name: 'test', - }, - spec: { - definition: { - description: 'test description!', - properties: { - period: { - default: 5000, - description: 'The time interval between two events', - title: 'Period', - type: 'integer', - }, - }, - title: 'kamelet-35256', - type: 'object', - }, - dependencies: ['camel:timer', 'camel:http', 'camel:kamelet'], - template: { - from: { - steps: [ - { - to: { - uri: 'https', - parameters: { - httpUri: 'random-data-api.com/api/v2/users', - }, - }, - }, - { - to: 'kamelet:sink', - }, - ], - id: 'from-3836', - parameters: { - period: '{{period}}', - timerName: 'user', - }, - uri: 'timer', - }, - }, - types: { - out: { - mediaType: 'application/json', - }, - }, - }, - }; + inputKameletStruct = cloneDeep(kameletJson); }); it('should get a custom kamelet definition from a empty kamelet', () => { @@ -97,22 +34,18 @@ describe('getCustomSchemaFromKamelet', () => { it('should get a custom kamelet definition from a kamelet official spec', () => { const expectedCustomSchema = { - name: 'test', - title: 'kamelet-35256', - description: 'test description!', - type: 'Action', + name: 'user-source', + title: 'User Source', + description: 'Produces periodic events about random users!', + type: 'source', icon: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNjAgNjAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYwIDYwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik00OC4wMTQsNDIuODg5bC05LjU1My00Ljc3NkMzNy41NiwzNy42NjIsMzcsMzYuNzU2LDM3LDM1Ljc0OHYtMy4zODFjMC4yMjktMC4yOCwwLjQ3LTAuNTk5LDAuNzE5LTAuOTUxCgljMS4yMzktMS43NSwyLjIzMi0zLjY5OCwyLjk1NC01Ljc5OUM0Mi4wODQsMjQuOTcsNDMsMjMuNTc1LDQzLDIydi00YzAtMC45NjMtMC4zNi0xLjg5Ni0xLTIuNjI1di01LjMxOQoJYzAuMDU2LTAuNTUsMC4yNzYtMy44MjQtMi4wOTItNi41MjVDMzcuODU0LDEuMTg4LDM0LjUyMSwwLDMwLDBzLTcuODU0LDEuMTg4LTkuOTA4LDMuNTNDMTcuNzI0LDYuMjMxLDE3Ljk0NCw5LjUwNiwxOCwxMC4wNTYKCXY1LjMxOWMtMC42NCwwLjcyOS0xLDEuNjYyLTEsMi42MjV2NGMwLDEuMjE3LDAuNTUzLDIuMzUyLDEuNDk3LDMuMTA5YzAuOTE2LDMuNjI3LDIuODMzLDYuMzYsMy41MDMsNy4yMzd2My4zMDkKCWMwLDAuOTY4LTAuNTI4LDEuODU2LTEuMzc3LDIuMzJsLTguOTIxLDQuODY2QzguODAxLDQ0LjQyNCw3LDQ3LjQ1OCw3LDUwLjc2MlY1NGMwLDQuNzQ2LDE1LjA0NSw2LDIzLDZzMjMtMS4yNTQsMjMtNnYtMy4wNDMKCUM1Myw0Ny41MTksNTEuMDg5LDQ0LjQyNyw0OC4wMTQsNDIuODg5eiIvPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K', supportLevel: 'Stable', catalogVersion: 'main-SNAPSHOT', provider: 'Apache Software Foundation', group: 'Users', - namespace: 'test', - labels: { - type: 'Action', - }, - annotations: { - foo: 'bar', - }, + namespace: undefined, + labels: {}, + annotations: {}, kameletProperties: [ { name: 'period', @@ -124,35 +57,32 @@ describe('getCustomSchemaFromKamelet', () => { ], }; - const customSchema = getCustomSchemaFromKamelet(inputKameletStruct as unknown as IKameletDefinition); + const customSchema = getCustomSchemaFromKamelet(inputKameletStruct); expect(customSchema).toEqual(expectedCustomSchema); }); it('should get a custom kamelet definition from a kamelet with string properties', () => { - const kameletWithStringProperties = cloneDeep(inputKameletStruct); - kameletWithStringProperties.spec.definition.properties = test as unknown as Record; + inputKameletStruct.spec.definition.properties = { + period: 'test', + } as unknown as Record; const expectedCustomSchema = { - name: 'test', - title: 'kamelet-35256', - description: 'test description!', - type: 'Action', + name: 'user-source', + title: 'User Source', + description: 'Produces periodic events about random users!', + type: 'source', icon: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNjAgNjAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYwIDYwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik00OC4wMTQsNDIuODg5bC05LjU1My00Ljc3NkMzNy41NiwzNy42NjIsMzcsMzYuNzU2LDM3LDM1Ljc0OHYtMy4zODFjMC4yMjktMC4yOCwwLjQ3LTAuNTk5LDAuNzE5LTAuOTUxCgljMS4yMzktMS43NSwyLjIzMi0zLjY5OCwyLjk1NC01Ljc5OUM0Mi4wODQsMjQuOTcsNDMsMjMuNTc1LDQzLDIydi00YzAtMC45NjMtMC4zNi0xLjg5Ni0xLTIuNjI1di01LjMxOQoJYzAuMDU2LTAuNTUsMC4yNzYtMy44MjQtMi4wOTItNi41MjVDMzcuODU0LDEuMTg4LDM0LjUyMSwwLDMwLDBzLTcuODU0LDEuMTg4LTkuOTA4LDMuNTNDMTcuNzI0LDYuMjMxLDE3Ljk0NCw5LjUwNiwxOCwxMC4wNTYKCXY1LjMxOWMtMC42NCwwLjcyOS0xLDEuNjYyLTEsMi42MjV2NGMwLDEuMjE3LDAuNTUzLDIuMzUyLDEuNDk3LDMuMTA5YzAuOTE2LDMuNjI3LDIuODMzLDYuMzYsMy41MDMsNy4yMzd2My4zMDkKCWMwLDAuOTY4LTAuNTI4LDEuODU2LTEuMzc3LDIuMzJsLTguOTIxLDQuODY2QzguODAxLDQ0LjQyNCw3LDQ3LjQ1OCw3LDUwLjc2MlY1NGMwLDQuNzQ2LDE1LjA0NSw2LDIzLDZzMjMtMS4yNTQsMjMtNnYtMy4wNDMKCUM1Myw0Ny41MTksNTEuMDg5LDQ0LjQyNyw0OC4wMTQsNDIuODg5eiIvPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K', supportLevel: 'Stable', catalogVersion: 'main-SNAPSHOT', provider: 'Apache Software Foundation', group: 'Users', - namespace: 'test', - labels: { - type: 'Action', - }, - annotations: { - foo: 'bar', - }, + namespace: undefined, + labels: {}, + annotations: {}, kameletProperties: [], }; - const customSchema = getCustomSchemaFromKamelet(kameletWithStringProperties as unknown as IKameletDefinition); + const customSchema = getCustomSchemaFromKamelet(inputKameletStruct); expect(customSchema).toEqual(expectedCustomSchema); }); diff --git a/packages/ui/src/utils/pipe-custom-schema.ts b/packages/ui/src/utils/pipe-custom-schema.ts index d9c60e2c1..ea109ed29 100644 --- a/packages/ui/src/utils/pipe-custom-schema.ts +++ b/packages/ui/src/utils/pipe-custom-schema.ts @@ -1,5 +1,6 @@ import { Pipe } from '@kaoto/camel-catalog/types'; import { getValue } from './get-value'; +import { isDefined } from './is-defined'; import { setValue } from './set-value'; export const getCustomSchemaFromPipe = (pipe: Pipe) => { @@ -17,6 +18,10 @@ export const getCustomSchemaFromPipe = (pipe: Pipe) => { }; export const updatePipeFromCustomSchema = (pipe: Pipe, value: Record): void => { + if (!isDefined(value)) { + return; + } + // Ensure 'labels' and 'annotations' are defined in 'value' if (value && getValue(value, 'labels') === undefined) { value.labels = {}; @@ -24,9 +29,9 @@ export const updatePipeFromCustomSchema = (pipe: Pipe, value: Record = getValue(pipe, 'metadata.annotations', {}); const previousLabels: Record = getValue(pipe, 'metadata.labels', {}); diff --git a/packages/ui/src/utils/update-kamelet-from-custom-schema.ts b/packages/ui/src/utils/update-kamelet-from-custom-schema.ts index 20c5ca589..106cb1512 100644 --- a/packages/ui/src/utils/update-kamelet-from-custom-schema.ts +++ b/packages/ui/src/utils/update-kamelet-from-custom-schema.ts @@ -1,35 +1,26 @@ import { + IKameletCustomProperty, IKameletDefinition, - IKameletMetadataAnnotations, - IKameletMetadataLabels, + IKameletSpecProperty, KameletKnownAnnotations, KameletKnownLabels, - IKameletSpecProperty, - IKameletCustomProperty, } from '../models/kamelets-catalog'; import { getValue } from './get-value'; +import { isDefined } from './is-defined'; import { setValue } from './set-value'; export const updateKameletFromCustomSchema = (kamelet: IKameletDefinition, value: Record): void => { - const previousName = getValue(kamelet, 'metadata.name'); - const previousTitle = getValue(kamelet, 'spec.definition.title'); - const previousDescription = getValue(kamelet, 'spec.definition.description'); + if (!isDefined(value)) { + return; + } const newName: string = getValue(value, 'name'); const newTitle: string = getValue(value, 'title'); const newDescription: string = getValue(value, 'description'); - setValue(kamelet, 'metadata.name', newName ?? previousName); - setValue(kamelet, 'spec.definition.title', newTitle ?? previousTitle); - setValue(kamelet, 'spec.definition.description', newDescription ?? previousDescription); - - const previousAnnotations = getValue(kamelet, 'metadata.annotations', {} as IKameletMetadataAnnotations); - const previousIcon = previousAnnotations[KameletKnownAnnotations.Icon]; - const previousSupportLevel = previousAnnotations[KameletKnownAnnotations.SupportLevel]; - const previousCatalogVersion = previousAnnotations[KameletKnownAnnotations.CatalogVersion]; - const previousProvider = previousAnnotations[KameletKnownAnnotations.Provider]; - const previousGroup = previousAnnotations[KameletKnownAnnotations.Group]; - const previousNamespace = previousAnnotations[KameletKnownAnnotations.Namespace]; + setValue(kamelet, 'metadata.name', newName); + setValue(kamelet, 'spec.definition.title', newTitle); + setValue(kamelet, 'spec.definition.description', newDescription); const newIcon = getValue(value, 'icon'); const newSupportLevel = getValue(value, 'supportLevel'); @@ -39,18 +30,17 @@ export const updateKameletFromCustomSchema = (kamelet: IKameletDefinition, value const newNamespace = getValue(value, 'namespace'); const customAnnotations = { - [KameletKnownAnnotations.Icon]: newIcon ?? previousIcon, - [KameletKnownAnnotations.SupportLevel]: newSupportLevel ?? previousSupportLevel, - [KameletKnownAnnotations.CatalogVersion]: newCatalogVersion ?? previousCatalogVersion, - [KameletKnownAnnotations.Provider]: newProvider ?? previousProvider, - [KameletKnownAnnotations.Group]: newGroup ?? previousGroup, - [KameletKnownAnnotations.Namespace]: newNamespace ?? previousNamespace, + [KameletKnownAnnotations.Icon]: newIcon, + [KameletKnownAnnotations.SupportLevel]: newSupportLevel, + [KameletKnownAnnotations.CatalogVersion]: newCatalogVersion, + [KameletKnownAnnotations.Provider]: newProvider, + [KameletKnownAnnotations.Group]: newGroup, + [KameletKnownAnnotations.Namespace]: newNamespace, }; - const previousType = getValue(kamelet, 'metadata.labels', {} as IKameletMetadataLabels)[KameletKnownLabels.Type]; const incomingLabels = getValue(value, 'labels', {}); const newLabels = Object.assign({}, incomingLabels, { - [KameletKnownLabels.Type]: getValue(value, 'type', previousType), + [KameletKnownLabels.Type]: getValue(value, 'type'), }); const newAnnotations = Object.assign({}, getValue(value, 'annotations', {}), customAnnotations); @@ -58,6 +48,8 @@ export const updateKameletFromCustomSchema = (kamelet: IKameletDefinition, value setValue(kamelet, 'metadata.annotations', newAnnotations); const propertiesArray: IKameletCustomProperty[] = getValue(value, 'kameletProperties'); + // TODO: When deleting the content of the property name, it turns into `undefined` instead of being removed + // TODO: Check how can we skip this issue const newProperties = propertiesArray?.reduce( (acc, property) => { if (property !== undefined) {