diff --git a/packages/ui/src/components/Visualization/ContextToolbar/ContextToolbar.tsx b/packages/ui/src/components/Visualization/ContextToolbar/ContextToolbar.tsx index 9adfbe9b2..2fd73c4ed 100644 --- a/packages/ui/src/components/Visualization/ContextToolbar/ContextToolbar.tsx +++ b/packages/ui/src/components/Visualization/ContextToolbar/ContextToolbar.tsx @@ -3,29 +3,39 @@ import { FunctionComponent, useContext } from 'react'; import { sourceSchemaConfig } from '../../../models/camel'; import { EntitiesContext } from '../../../providers/entities.provider'; import './ContextToolbar.scss'; +import { DSLSelector } from './DSLSelector/DSLSelector'; import { FlowClipboard } from './FlowClipboard/FlowClipboard'; import { FlowExportImage } from './FlowExportImage/FlowExportImage'; -import { NewFlow } from './FlowType/NewFlow'; import { FlowsMenu } from './Flows/FlowsMenu'; +import { NewEntity } from './NewEntity/NewEntity'; export const ContextToolbar: FunctionComponent = () => { const { currentSchemaType } = useContext(EntitiesContext)!; + const isMultipleRoutes = sourceSchemaConfig.config[currentSchemaType].multipleRoute; - return [ - - {sourceSchemaConfig.config[currentSchemaType].name || 'None'} + const toolbarItems: JSX.Element[] = [ + + , , - - - , + ]; + + if (isMultipleRoutes) { + toolbarItems.push( + + + , + ); + } + + return toolbarItems.concat([ , , - ]; + ]); }; diff --git a/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/ChangeDSLModal/ChangeDSLModal.test.tsx b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/ChangeDSLModal/ChangeDSLModal.test.tsx new file mode 100644 index 000000000..36892c690 --- /dev/null +++ b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/ChangeDSLModal/ChangeDSLModal.test.tsx @@ -0,0 +1,43 @@ +import { fireEvent, render } from '@testing-library/react'; +import { ChangeDSLModal } from './ChangeDSLModal'; + +describe('ChangeDSLModal', () => { + it('should be hidden when isOpen is false', () => { + const wrapper = render(); + + expect(wrapper.queryByTestId('confirmation-modal')).not.toBeInTheDocument(); + }); + + it('should be visible when isOpen is true', () => { + const wrapper = render(); + + expect(wrapper.queryByTestId('confirmation-modal')).toBeInTheDocument(); + }); + + it('should call onConfirm when confirm button is clicked', () => { + const onConfirm = jest.fn(); + const wrapper = render(); + + fireEvent.click(wrapper.getByTestId('confirmation-modal-confirm')); + + expect(onConfirm).toBeCalled(); + }); + + it('should call onCancel when cancel button is clicked', () => { + const onCancel = jest.fn(); + const wrapper = render(); + + fireEvent.click(wrapper.getByTestId('confirmation-modal-cancel')); + + expect(onCancel).toBeCalled(); + }); + + it('should call onCancel when close button is clicked', () => { + const onCancel = jest.fn(); + const wrapper = render(); + + fireEvent.click(wrapper.getByLabelText('Close')); + + expect(onCancel).toBeCalled(); + }); +}); diff --git a/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/ChangeDSLModal/ChangeDSLModal.tsx b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/ChangeDSLModal/ChangeDSLModal.tsx new file mode 100644 index 000000000..321e137d7 --- /dev/null +++ b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/ChangeDSLModal/ChangeDSLModal.tsx @@ -0,0 +1,34 @@ +import { Button, Modal, ModalVariant } from '@patternfly/react-core'; +import { FunctionComponent } from 'react'; + +interface ChangeDSLModalProps { + isOpen: boolean; + onConfirm: () => void; + onCancel: () => void; +} + +export const ChangeDSLModal: FunctionComponent = (props) => { + return ( + + Confirm + , + , + ]} + isOpen={props.isOpen} + > +

+ This will remove any existing integration and you will lose your current work. Are you sure you would like to + proceed? +

+
+ ); +}; diff --git a/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelector.test.tsx b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelector.test.tsx new file mode 100644 index 000000000..5a2528278 --- /dev/null +++ b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelector.test.tsx @@ -0,0 +1,81 @@ +import { act, fireEvent, render } from '@testing-library/react'; +import { EntitiesContextResult } from '../../../../hooks'; +import { KaotoSchemaDefinition } from '../../../../models'; +import { SourceSchemaType, sourceSchemaConfig } from '../../../../models/camel'; +import { EntitiesContext } from '../../../../providers/entities.provider'; +import { SourceCodeApiContext } from '../../../../providers/source-code.provider'; +import { DSLSelector } from './DSLSelector'; + +describe('DSLSelector.tsx', () => { + const config = sourceSchemaConfig; + config.config[SourceSchemaType.Integration].schema = { + schema: { name: 'Integration', description: 'desc' } as KaotoSchemaDefinition['schema'], + } as KaotoSchemaDefinition; + config.config[SourceSchemaType.Pipe].schema = { + schema: { name: 'Pipe', description: 'desc' } as KaotoSchemaDefinition['schema'], + } as KaotoSchemaDefinition; + config.config[SourceSchemaType.Kamelet].schema = { + schema: { name: 'Kamelet', description: 'desc' } as KaotoSchemaDefinition['schema'], + } as KaotoSchemaDefinition; + config.config[SourceSchemaType.KameletBinding].schema = { + name: 'kameletBinding', + schema: { description: 'desc' }, + } as KaotoSchemaDefinition; + config.config[SourceSchemaType.Route].schema = { + schema: { name: 'route', description: 'desc' } as KaotoSchemaDefinition['schema'], + } as KaotoSchemaDefinition; + + const renderWithContext = () => { + return render( + + + + + , + ); + }; + + it('should render all of the types', async () => { + const wrapper = renderWithContext(); + const trigger = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Open Select */ + act(() => { + fireEvent.click(trigger); + }); + + for (const name of ['Pipe', 'Camel Route']) { + const element = await wrapper.findByText(name); + expect(element).toBeInTheDocument(); + } + }); + + it('should warn the user when adding a different type of flow', async () => { + const wrapper = renderWithContext(); + const trigger = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Open Select */ + act(() => { + fireEvent.click(trigger); + }); + + /** Select an option */ + act(() => { + const element = wrapper.getByText('Pipe'); + fireEvent.click(element); + }); + + const modal = await wrapper.findByTestId('confirmation-modal'); + expect(modal).toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelector.tsx b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelector.tsx new file mode 100644 index 000000000..f5b7cc6fb --- /dev/null +++ b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelector.tsx @@ -0,0 +1,39 @@ +import { FunctionComponent, PropsWithChildren, useCallback, useContext, useState } from 'react'; +import { SourceSchemaType } from '../../../../models/camel'; +import { FlowTemplateService } from '../../../../models/visualization/flows/support/flow-templates-service'; +import { SourceCodeApiContext } from '../../../../providers'; +import { ChangeDSLModal } from './ChangeDSLModal/ChangeDSLModal'; +import { DSLSelectorToggle } from './DSLSelectorToggle/DSLSelectorToggle'; + +export const DSLSelector: FunctionComponent = () => { + const sourceCodeContextApi = useContext(SourceCodeApiContext); + const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); + const [proposedFlowType, setProposedFlowType] = useState(); + + const checkBeforeAddNewFlow = useCallback((flowType: SourceSchemaType) => { + /** + * If it is not the same DSL, this operation might result in + * removing the existing flows, so then we warn the user first + */ + setProposedFlowType(flowType); + setIsConfirmationModalOpen(true); + }, []); + + const onConfirm = useCallback(() => { + if (proposedFlowType) { + sourceCodeContextApi.setCodeAndNotify(FlowTemplateService.getFlowYamlTemplate(proposedFlowType)); + setIsConfirmationModalOpen(false); + } + }, [proposedFlowType, sourceCodeContextApi]); + + const onCancel = useCallback(() => { + setIsConfirmationModalOpen(false); + }, []); + + return ( + <> + + + + ); +}; diff --git a/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelectorToggle/DSLSelectorToggle.test.tsx b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelectorToggle/DSLSelectorToggle.test.tsx new file mode 100644 index 000000000..d035dd153 --- /dev/null +++ b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelectorToggle/DSLSelectorToggle.test.tsx @@ -0,0 +1,177 @@ +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { FunctionComponent } from 'react'; +import { EntitiesContextResult } from '../../../../../hooks'; +import { KaotoSchemaDefinition } from '../../../../../models'; +import { SourceSchemaType, sourceSchemaConfig } from '../../../../../models/camel'; +import { EntitiesContext } from '../../../../../providers/entities.provider'; +import { DSLSelectorToggle } from './DSLSelectorToggle'; + +const config = sourceSchemaConfig; +config.config[SourceSchemaType.Pipe].schema = { + name: 'Pipe', + schema: { name: 'Pipe', description: 'desc' } as KaotoSchemaDefinition['schema'], +} as KaotoSchemaDefinition; +config.config[SourceSchemaType.Kamelet].schema = { + name: 'Kamelet', + schema: { name: 'Kamelet', description: 'desc' } as KaotoSchemaDefinition['schema'], +} as KaotoSchemaDefinition; +config.config[SourceSchemaType.Route].schema = { + name: 'route', + schema: { name: 'route', description: 'desc' } as KaotoSchemaDefinition['schema'], +} as KaotoSchemaDefinition; + +describe('FlowTypeSelector.tsx', () => { + let onSelect: () => void; + beforeEach(() => { + onSelect = jest.fn(); + }); + + const FlowTypeSelectorWithContext: FunctionComponent<{ currentSchemaType?: SourceSchemaType }> = (props) => { + const currentSchemaType = props.currentSchemaType ?? SourceSchemaType.Route; + return ( + + + + ); + }; + + it('component renders', () => { + const wrapper = render(); + const toggle = wrapper.queryByTestId('dsl-list-dropdown'); + expect(toggle).toBeInTheDocument(); + }); + + it('should call onSelect when clicking on the MenuToggleAction', async () => { + const wrapper = render(); + + /** Click on toggle */ + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + act(() => { + fireEvent.click(toggle); + }); + + /** Click on first element */ + const element = await wrapper.findByText('Pipe'); + act(() => { + fireEvent.click(element); + }); + + await waitFor(() => { + expect(onSelect).toHaveBeenCalled(); + }); + }); + + it('should disable the MenuToggleAction if the DSL is already selected', async () => { + const wrapper = render(); + + /** Click on toggle */ + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + act(() => { + fireEvent.click(toggle); + }); + + /** Click on first element */ + const element = await wrapper.findByText('Camel Route'); + // act(() => { + // fireEvent.click(element); + // }); + + waitFor(() => { + expect(element).toBeDisabled(); + }); + }); + + it('should toggle list of DSLs', async () => { + const wrapper = render(); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Click on toggle */ + act(() => { + fireEvent.click(toggle); + }); + + const element = await wrapper.findByText('Pipe'); + expect(element).toBeInTheDocument(); + + /** Close Select */ + act(() => { + fireEvent.click(toggle); + }); + + waitFor(() => { + expect(element).not.toBeInTheDocument(); + }); + }); + + it('should show selected value', async () => { + const wrapper = render(); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Open Select */ + act(() => { + fireEvent.click(toggle); + }); + + /** Click on first element */ + act(() => { + const element = wrapper.getByText('Camel Route'); + fireEvent.click(element); + }); + + /** Open Select again */ + act(() => { + fireEvent.click(toggle); + }); + + const element = await wrapper.findByRole('option', { selected: true }); + expect(element).toBeInTheDocument(); + expect(element).toHaveTextContent('Camel Route'); + }); + + it('should have selected DSL if provided', async () => { + const wrapper = render(); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Open Select */ + act(() => { + fireEvent.click(toggle); + }); + + waitFor(() => { + const element = wrapper.queryByRole('option', { selected: true }); + expect(element).toBeInTheDocument(); + expect(element).toHaveTextContent('Pipe'); + }); + }); + + it('should close Select when pressing ESC', async () => { + const wrapper = render(); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Open Select */ + act(() => { + fireEvent.click(toggle); + }); + + const menu = await wrapper.findByRole('listbox'); + + expect(menu).toBeInTheDocument(); + + /** Press Escape key to close the menu */ + act(() => { + fireEvent.focus(menu); + fireEvent.keyDown(menu, { key: 'Escape', code: 'Escape', charCode: 27 }); + }); + + waitFor(() => { + /** The close panel is an async process */ + expect(menu).not.toBeInTheDocument(); + }); + + waitFor(() => { + const element = wrapper.queryByRole('option', { selected: true }); + expect(element).toBeInTheDocument(); + expect(element).toHaveTextContent('Camel Route'); + }); + }); +}); diff --git a/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelectorToggle/DSLSelectorToggle.tsx b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelectorToggle/DSLSelectorToggle.tsx new file mode 100644 index 000000000..01228dbdc --- /dev/null +++ b/packages/ui/src/components/Visualization/ContextToolbar/DSLSelector/DSLSelectorToggle/DSLSelectorToggle.tsx @@ -0,0 +1,82 @@ +import { MenuToggle, Select, SelectList, SelectOption } from '@patternfly/react-core'; +import { FunctionComponent, MouseEvent, RefObject, useCallback, useContext, useRef, useState } from 'react'; +import { ISourceSchema, SourceSchemaType, sourceSchemaConfig } from '../../../../../models/camel'; +import { EntitiesContext } from '../../../../../providers/entities.provider'; + +interface ISourceTypeSelector { + onSelect?: (value: SourceSchemaType) => void; +} + +export const DSLSelectorToggle: FunctionComponent = (props) => { + const { currentSchemaType } = useContext(EntitiesContext)!; + const currentFlowType: ISourceSchema = sourceSchemaConfig.config[currentSchemaType]; + const [isOpen, setIsOpen] = useState(false); + const dslEntriesRef = useRef>>({ + [SourceSchemaType.Route]: sourceSchemaConfig.config[SourceSchemaType.Route], + [SourceSchemaType.Kamelet]: sourceSchemaConfig.config[SourceSchemaType.Kamelet], + [SourceSchemaType.Pipe]: sourceSchemaConfig.config[SourceSchemaType.Pipe], + }); + + const onSelect = useCallback( + (_event: MouseEvent | undefined, flowType: string | number | undefined) => { + if (!flowType) { + return; + } + const dsl = sourceSchemaConfig.config[flowType as SourceSchemaType]; + + setIsOpen(false); + if (dsl !== undefined) { + props.onSelect?.(flowType as SourceSchemaType); + } + }, + [props], + ); + + const toggle = (toggleRef: RefObject) => ( + { + setIsOpen(!isOpen); + }} + isExpanded={isOpen} + > + {sourceSchemaConfig.config[currentSchemaType].name} + + ); + + return ( + + ); +}; diff --git a/packages/ui/src/components/Visualization/ContextToolbar/NewEntity/NewEntity.test.tsx b/packages/ui/src/components/Visualization/ContextToolbar/NewEntity/NewEntity.test.tsx new file mode 100644 index 000000000..7f6b42136 --- /dev/null +++ b/packages/ui/src/components/Visualization/ContextToolbar/NewEntity/NewEntity.test.tsx @@ -0,0 +1,203 @@ +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { FunctionComponent } from 'react'; +import { EntitiesContextResult } from '../../../../hooks'; +import { KaotoSchemaDefinition } from '../../../../models'; +import { CamelRouteResource, SourceSchemaType, sourceSchemaConfig } from '../../../../models/camel'; +import { EntitiesContext } from '../../../../providers/entities.provider'; +import { VisibleFLowsContextResult, VisibleFlowsContext } from '../../../../providers/visible-flows.provider'; +import { camelRouteJson } from '../../../../stubs'; +import { NewEntity } from './NewEntity'; + +const config = sourceSchemaConfig; +config.config[SourceSchemaType.Pipe].schema = { + name: 'Pipe', + schema: { name: 'Pipe', description: 'desc' } as KaotoSchemaDefinition['schema'], +} as KaotoSchemaDefinition; +config.config[SourceSchemaType.Kamelet].schema = { + name: 'Kamelet', + schema: { name: 'Kamelet', description: 'desc' } as KaotoSchemaDefinition['schema'], +} as KaotoSchemaDefinition; +config.config[SourceSchemaType.Route].schema = { + name: 'route', + schema: { name: 'route', description: 'desc' } as KaotoSchemaDefinition['schema'], +} as KaotoSchemaDefinition; + +describe('FlowTypeSelector.tsx', () => { + let setCurrentSchemaTypeSpy: jest.Mock; + let updateEntitiesFromCamelResourceSpy: jest.Mock; + let updateSourceCodeFromEntitiesSpy: jest.Mock; + + beforeEach(() => { + setCurrentSchemaTypeSpy = jest.fn(); + updateEntitiesFromCamelResourceSpy = jest.fn(); + updateSourceCodeFromEntitiesSpy = jest.fn(); + }); + + const FlowTypeSelectorWithContext: FunctionComponent<{ currentSchemaType?: SourceSchemaType }> = (props) => { + const camelResource = new CamelRouteResource(camelRouteJson); + const currentSchemaType = props.currentSchemaType ?? camelResource.getType(); + + return ( + + + + + + ); + }; + + it('component renders', () => { + const wrapper = render(); + const toggle = wrapper.queryByTestId('dsl-list-dropdown'); + expect(toggle).toBeInTheDocument(); + }); + + it('should call `updateEntitiesFromCamelResource` when selecting an item', async () => { + const wrapper = render(); + + /** Click on toggle */ + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + act(() => { + fireEvent.click(toggle); + }); + + /** Click on first element */ + const element = await wrapper.findByText('Pipe'); + act(() => { + fireEvent.click(element); + }); + + await waitFor(() => { + expect(updateEntitiesFromCamelResourceSpy).toHaveBeenCalled(); + }); + }); + + it('should disable the MenuToggleAction if the DSL is already selected', async () => { + const wrapper = render(); + + /** Click on toggle */ + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + act(() => { + fireEvent.click(toggle); + }); + + /** Click on first element */ + const element = await wrapper.findByText('Camel Route'); + // act(() => { + // fireEvent.click(element); + // }); + + waitFor(() => { + expect(element).toBeDisabled(); + }); + }); + + it('should toggle list of DSLs', async () => { + const wrapper = render(); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Click on toggle */ + act(() => { + fireEvent.click(toggle); + }); + + const element = await wrapper.findByText('Pipe'); + expect(element).toBeInTheDocument(); + + /** Close Select */ + act(() => { + fireEvent.click(toggle); + }); + + waitFor(() => { + expect(element).not.toBeInTheDocument(); + }); + }); + + it('should show selected value', async () => { + const wrapper = render(); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Open Select */ + act(() => { + fireEvent.click(toggle); + }); + + /** Click on first element */ + act(() => { + const element = wrapper.getByText('Camel Route'); + fireEvent.click(element); + }); + + /** Open Select again */ + act(() => { + fireEvent.click(toggle); + }); + + const element = await wrapper.findByRole('option', { selected: true }); + expect(element).toBeInTheDocument(); + expect(element).toHaveTextContent('Camel Route'); + }); + + it('should have selected DSL if provided', async () => { + const wrapper = render(); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Open Select */ + act(() => { + fireEvent.click(toggle); + }); + + waitFor(() => { + const element = wrapper.queryByRole('option', { selected: true }); + expect(element).toBeInTheDocument(); + expect(element).toHaveTextContent('Pipe'); + }); + }); + + it('should close Select when pressing ESC', async () => { + const wrapper = render(); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); + + /** Open Select */ + act(() => { + fireEvent.click(toggle); + }); + + const menu = await wrapper.findByRole('listbox'); + + expect(menu).toBeInTheDocument(); + + /** Press Escape key to close the menu */ + act(() => { + fireEvent.focus(menu); + fireEvent.keyDown(menu, { key: 'Escape', code: 'Escape', charCode: 27 }); + }); + + waitFor(() => { + /** The close panel is an async process */ + expect(menu).not.toBeInTheDocument(); + }); + + waitFor(() => { + const element = wrapper.queryByRole('option', { selected: true }); + expect(element).toBeInTheDocument(); + expect(element).toHaveTextContent('Camel Route'); + }); + }); +}); diff --git a/packages/ui/src/components/Visualization/ContextToolbar/NewEntity/NewEntity.tsx b/packages/ui/src/components/Visualization/ContextToolbar/NewEntity/NewEntity.tsx new file mode 100644 index 000000000..89af17d94 --- /dev/null +++ b/packages/ui/src/components/Visualization/ContextToolbar/NewEntity/NewEntity.tsx @@ -0,0 +1,101 @@ +import { Menu, MenuContainer, MenuContent, MenuItem, MenuList, MenuToggle } from '@patternfly/react-core'; +import { PlusIcon } from '@patternfly/react-icons'; +import { FunctionComponent, ReactElement, useCallback, useContext, useRef, useState } from 'react'; +import { BaseVisualCamelEntityDefinition } from '../../../../models/camel/camel-resource'; +import { EntityType } from '../../../../models/camel/entities'; +import { EntitiesContext } from '../../../../providers/entities.provider'; +import { VisibleFlowsContext } from '../../../../providers/visible-flows.provider'; + +export const NewEntity: FunctionComponent = () => { + const { camelResource, updateEntitiesFromCamelResource } = useContext(EntitiesContext)!; + const visibleFlowsContext = useContext(VisibleFlowsContext)!; + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + const toggleRef = useRef(null); + const groupedEntities = useRef(camelResource.getCanvasEntityList()); + + const onSelect = useCallback( + (_event: unknown, entityType: string | number | undefined) => { + if (!entityType) { + return; + } + + /** + * If it's the same DSL as we have in the existing Flows list, + * we don't need to do anything special, just add a new flow if + * supported + */ + const newId = camelResource.addNewEntity(entityType as EntityType); + visibleFlowsContext.visualFlowsApi.hideAllFlows(); + visibleFlowsContext.visualFlowsApi.setVisibleFlows([newId]); + updateEntitiesFromCamelResource(); + setIsOpen(false); + }, + [camelResource, updateEntitiesFromCamelResource, visibleFlowsContext.visualFlowsApi], + ); + + const getMenuItem = useCallback( + (entity: { name?: EntityType; title: string; description: string }, flyoutMenu?: ReactElement) => { + return ( + + {entity.description} + + } + flyoutMenu={flyoutMenu} + > + {entity.title} + + ); + }, + [], + ); + + return ( + setIsOpen(isOpen)} + menu={ + // TODO: Workaround for flyout menu being scrollable and packed within the toolbar + + + + {groupedEntities.current.common.map((entityDef) => getMenuItem(entityDef))} + + {Object.entries(groupedEntities.current.groups).map(([group, entities]) => { + const flyoutMenu = ( + + + {entities.map((entityDef) => getMenuItem(entityDef))} + + + ); + + return getMenuItem({ name: undefined, title: group, description: '' }, flyoutMenu); + })} + + + + } + menuRef={menuRef} + toggle={ + { + setIsOpen(!isOpen); + }} + isExpanded={isOpen} + > + + New + + } + toggleRef={toggleRef} + /> + ); +}; diff --git a/packages/ui/src/components/Visualization/ContextToolbar/FlowType/FlowTypeSelector.test.tsx b/packages/ui/src/components/Visualization/EmptyState/FlowType/FlowTypeSelector.test.tsx similarity index 100% rename from packages/ui/src/components/Visualization/ContextToolbar/FlowType/FlowTypeSelector.test.tsx rename to packages/ui/src/components/Visualization/EmptyState/FlowType/FlowTypeSelector.test.tsx diff --git a/packages/ui/src/components/Visualization/ContextToolbar/FlowType/FlowTypeSelector.tsx b/packages/ui/src/components/Visualization/EmptyState/FlowType/FlowTypeSelector.tsx similarity index 96% rename from packages/ui/src/components/Visualization/ContextToolbar/FlowType/FlowTypeSelector.tsx rename to packages/ui/src/components/Visualization/EmptyState/FlowType/FlowTypeSelector.tsx index c67eaa681..70abc2300 100644 --- a/packages/ui/src/components/Visualization/ContextToolbar/FlowType/FlowTypeSelector.tsx +++ b/packages/ui/src/components/Visualization/EmptyState/FlowType/FlowTypeSelector.tsx @@ -1,148 +1,147 @@ -import { - MenuToggle, - MenuToggleAction, - MenuToggleElement, - Select, - SelectList, - SelectOption, - Tooltip, -} from '@patternfly/react-core'; -import { FunctionComponent, MouseEvent, PropsWithChildren, Ref, useCallback, useContext, useState } from 'react'; -import { ISourceSchema, SourceSchemaType, sourceSchemaConfig } from '../../../../models/camel'; -import { EntitiesContext } from '../../../../providers/entities.provider'; - -interface ISourceTypeSelector extends PropsWithChildren { - isStatic?: boolean; - onSelect?: (value: SourceSchemaType) => void; -} - -export const FlowTypeSelector: FunctionComponent = (props) => { - const { currentSchemaType, visualEntities } = useContext(EntitiesContext)!; - const totalFlowsCount = visualEntities.length; - const currentFlowType: ISourceSchema = sourceSchemaConfig.config[currentSchemaType]; - const [isOpen, setIsOpen] = useState(false); - - /** Toggle the DSL dropdown */ - const onToggleClick = () => { - setIsOpen(!isOpen); - }; - - /** Selecting a DSL checking the the existing flows */ - const onSelect = useCallback( - (_event: MouseEvent | undefined, flowType: string | number | undefined) => { - if (flowType) { - const dsl = sourceSchemaConfig.config[flowType as SourceSchemaType]; - - setIsOpen(false); - if (typeof props.onSelect === 'function' && dsl !== undefined) { - props.onSelect(flowType as SourceSchemaType); - } - } - }, - [props], - ); - - /** Selecting the same DSL directly*/ - const onNewSameTypeRoute = useCallback(() => { - onSelect(undefined, currentSchemaType); - }, [onSelect, currentSchemaType]); - - /** Override function to provide more useful help texts than available via schema */ - const getDescriptionForType = (type: string) => { - switch (type) { - case SourceSchemaType.Route: - return 'Defines an executable integration flow by declaring a source (starter) and followed by a sequence of actions (or steps). Actions can include data manipulations, EIPs (integration patterns) and internal or external calls.'; - case SourceSchemaType.Kamelet: - return 'Defines a reusable Camel route as a building block. Kamelets can not be executed on their own, they are used as sources, actions or sinks in Camel Routes or Pipes.'; - case SourceSchemaType.Pipe: - case SourceSchemaType.KameletBinding: - return 'Defines a sequence of concatenated Kamelets to form start to finish integration flows. Pipes are a more abstract level of defining integration flows, by chosing and configuring Kamelets.'; - case SourceSchemaType.Integration: - return 'An integration defines a Camel route in a CRD file.'; - default: - return undefined; - } - }; - - const toggle = (toggleRef: Ref) => ( - Add a new {currentFlowType.name} route

- ) : ( -

The {currentFlowType.name} type doesn't support multiple routes

- ) - } - > - 0} - > - {props.children} - - , - ], - }} - /> - ); - - return ( - - ); -}; +import { + MenuToggle, + MenuToggleAction, + MenuToggleElement, + Select, + SelectList, + SelectOption, + Tooltip, +} from '@patternfly/react-core'; +import { FunctionComponent, MouseEvent, PropsWithChildren, Ref, useCallback, useContext, useState } from 'react'; +import { ISourceSchema, SourceSchemaType, sourceSchemaConfig } from '../../../../models/camel'; +import { EntitiesContext } from '../../../../providers/entities.provider'; + +interface ISourceTypeSelector extends PropsWithChildren { + isStatic?: boolean; + onSelect?: (value: SourceSchemaType) => void; +} + +export const FlowTypeSelector: FunctionComponent = (props) => { + const { currentSchemaType, visualEntities } = useContext(EntitiesContext)!; + const totalFlowsCount = visualEntities.length; + const currentFlowType: ISourceSchema = sourceSchemaConfig.config[currentSchemaType]; + const [isOpen, setIsOpen] = useState(false); + + /** Toggle the DSL dropdown */ + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const onSelect = useCallback( + (_event: MouseEvent | undefined, flowType: string | number | undefined) => { + if (flowType) { + const dsl = sourceSchemaConfig.config[flowType as SourceSchemaType]; + + setIsOpen(false); + if (typeof props.onSelect === 'function' && dsl !== undefined) { + props.onSelect(flowType as SourceSchemaType); + } + } + }, + [props], + ); + + /** Selecting the same DSL directly*/ + const onNewSameTypeRoute = useCallback(() => { + onSelect(undefined, currentSchemaType); + }, [onSelect, currentSchemaType]); + + /** Override function to provide more useful help texts than available via schema */ + const getDescriptionForType = (type: string) => { + switch (type) { + case SourceSchemaType.Route: + return 'Defines an executable integration flow by declaring a source (starter) and followed by a sequence of actions (or steps). Actions can include data manipulations, EIPs (integration patterns) and internal or external calls.'; + case SourceSchemaType.Kamelet: + return 'Defines a reusable Camel route as a building block. Kamelets can not be executed on their own, they are used as sources, actions or sinks in Camel Routes or Pipes.'; + case SourceSchemaType.Pipe: + case SourceSchemaType.KameletBinding: + return 'Defines a sequence of concatenated Kamelets to form start to finish integration flows. Pipes are a more abstract level of defining integration flows, by chosing and configuring Kamelets.'; + case SourceSchemaType.Integration: + return 'An integration defines a Camel route in a CRD file.'; + default: + return undefined; + } + }; + + const toggle = (toggleRef: Ref) => ( + Add a new {currentFlowType.name} route

+ ) : ( +

The {currentFlowType.name} type doesn't support multiple routes

+ ) + } + > + 0} + > + {props.children} + + , + ], + }} + /> + ); + + return ( + + ); +}; diff --git a/packages/ui/src/components/Visualization/ContextToolbar/FlowType/NewFlow.test.tsx b/packages/ui/src/components/Visualization/EmptyState/FlowType/NewFlow.test.tsx similarity index 100% rename from packages/ui/src/components/Visualization/ContextToolbar/FlowType/NewFlow.test.tsx rename to packages/ui/src/components/Visualization/EmptyState/FlowType/NewFlow.test.tsx diff --git a/packages/ui/src/components/Visualization/ContextToolbar/FlowType/NewFlow.tsx b/packages/ui/src/components/Visualization/EmptyState/FlowType/NewFlow.tsx similarity index 97% rename from packages/ui/src/components/Visualization/ContextToolbar/FlowType/NewFlow.tsx rename to packages/ui/src/components/Visualization/EmptyState/FlowType/NewFlow.tsx index 66c935355..c0d6adba8 100644 --- a/packages/ui/src/components/Visualization/ContextToolbar/FlowType/NewFlow.tsx +++ b/packages/ui/src/components/Visualization/EmptyState/FlowType/NewFlow.tsx @@ -1,92 +1,92 @@ -import { Button, Modal, ModalVariant } from '@patternfly/react-core'; -import { PlusIcon } from '@patternfly/react-icons'; -import { FunctionComponent, PropsWithChildren, useCallback, useContext, useState } from 'react'; -import { useEntityContext } from '../../../../hooks/useEntityContext/useEntityContext'; -import { SourceSchemaType } from '../../../../models/camel'; -import { FlowTemplateService } from '../../../../models/visualization/flows/support/flow-templates-service'; -import { SourceCodeApiContext } from '../../../../providers'; -import { VisibleFlowsContext } from '../../../../providers/visible-flows.provider'; -import { FlowTypeSelector } from './FlowTypeSelector'; - -export const NewFlow: FunctionComponent = () => { - const sourceCodeContextApi = useContext(SourceCodeApiContext); - const entitiesContext = useEntityContext(); - const visibleFlowsContext = useContext(VisibleFlowsContext)!; - const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); - const [proposedFlowType, setProposedFlowType] = useState(); - - const checkBeforeAddNewFlow = useCallback( - (flowType: SourceSchemaType) => { - const isSameSourceType = entitiesContext.currentSchemaType === flowType; - - if (isSameSourceType) { - /** - * If it's the same DSL as we have in the existing Flows list, - * we don't need to do anything special, just add a new flow if - * supported - */ - const newId = entitiesContext.camelResource.addNewEntity(); - visibleFlowsContext.visualFlowsApi.hideAllFlows(); - visibleFlowsContext.visualFlowsApi.setVisibleFlows([newId]); - entitiesContext.updateEntitiesFromCamelResource(); - } else { - /** - * If it is not the same DSL, this operation might result in - * removing the existing flows, so then we warn the user first - */ - setProposedFlowType(flowType); - setIsConfirmationModalOpen(true); - } - }, - [entitiesContext, visibleFlowsContext.visualFlowsApi], - ); - - return ( - <> - - - New - - { - setIsConfirmationModalOpen(false); - }} - actions={[ - , - , - ]} - isOpen={isConfirmationModalOpen} - > -

- This will remove any existing integration and you will lose your current work. Are you sure you would like to - proceed? -

-
- - ); -}; +import { Button, Modal, ModalVariant } from '@patternfly/react-core'; +import { PlusIcon } from '@patternfly/react-icons'; +import { FunctionComponent, PropsWithChildren, useCallback, useContext, useState } from 'react'; +import { useEntityContext } from '../../../../hooks/useEntityContext/useEntityContext'; +import { SourceSchemaType } from '../../../../models/camel'; +import { FlowTemplateService } from '../../../../models/visualization/flows/support/flow-templates-service'; +import { SourceCodeApiContext } from '../../../../providers'; +import { VisibleFlowsContext } from '../../../../providers/visible-flows.provider'; +import { FlowTypeSelector } from './FlowTypeSelector'; + +export const NewFlow: FunctionComponent = () => { + const sourceCodeContextApi = useContext(SourceCodeApiContext); + const entitiesContext = useEntityContext(); + const visibleFlowsContext = useContext(VisibleFlowsContext)!; + const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); + const [proposedFlowType, setProposedFlowType] = useState(); + + const checkBeforeAddNewFlow = useCallback( + (flowType: SourceSchemaType) => { + const isSameSourceType = entitiesContext.currentSchemaType === flowType; + + if (isSameSourceType) { + /** + * If it's the same DSL as we have in the existing Flows list, + * we don't need to do anything special, just add a new flow if + * supported + */ + const newId = entitiesContext.camelResource.addNewEntity(); + visibleFlowsContext.visualFlowsApi.hideAllFlows(); + visibleFlowsContext.visualFlowsApi.setVisibleFlows([newId]); + entitiesContext.updateEntitiesFromCamelResource(); + } else { + /** + * If it is not the same DSL, this operation might result in + * removing the existing flows, so then we warn the user first + */ + setProposedFlowType(flowType); + setIsConfirmationModalOpen(true); + } + }, + [entitiesContext, visibleFlowsContext.visualFlowsApi], + ); + + return ( + <> + + + New + + { + setIsConfirmationModalOpen(false); + }} + actions={[ + , + , + ]} + isOpen={isConfirmationModalOpen} + > +

+ This will remove any existing integration and you will lose your current work. Are you sure you would like to + proceed? +

+
+ + ); +}; diff --git a/packages/ui/src/components/Visualization/EmptyState/VisualizationEmptyState.tsx b/packages/ui/src/components/Visualization/EmptyState/VisualizationEmptyState.tsx index 8f4e2ecc1..cca4d3604 100644 --- a/packages/ui/src/components/Visualization/EmptyState/VisualizationEmptyState.tsx +++ b/packages/ui/src/components/Visualization/EmptyState/VisualizationEmptyState.tsx @@ -11,7 +11,7 @@ import { import { CubesIcon as PatternFlyCubesIcon, EyeSlashIcon as PatternFlyEyeSlashIcon } from '@patternfly/react-icons'; import { FunctionComponent, useMemo } from 'react'; import { IDataTestID } from '../../../models'; -import { NewFlow } from '../ContextToolbar/FlowType/NewFlow'; +import { NewFlow } from './FlowType/NewFlow'; const CubesIcon: FunctionComponent = (props) => ; const EyeSlashIcon: FunctionComponent = (props) => ; diff --git a/packages/ui/src/models/camel/camel-k-resource.ts b/packages/ui/src/models/camel/camel-k-resource.ts index edeee8dc3..25f78088c 100644 --- a/packages/ui/src/models/camel/camel-k-resource.ts +++ b/packages/ui/src/models/camel/camel-k-resource.ts @@ -4,13 +4,13 @@ import { Pipe as PipeType, } from '@kaoto-next/camel-catalog/types'; import { TileFilter } from '../../components/Catalog'; +import { createCamelPropertiesSorter } from '../../utils'; import { IKameletDefinition } from '../kamelets-catalog'; import { AddStepMode, BaseVisualCamelEntity, IVisualizationNodeData } from '../visualization/base-visual-entity'; import { MetadataEntity } from '../visualization/metadata'; -import { CamelResource } from './camel-resource'; +import { BaseVisualCamelEntityDefinition, CamelResource } from './camel-resource'; import { BaseCamelEntity } from './entities'; import { SourceSchemaType } from './source-schema-type'; -import { createCamelPropertiesSorter } from '../../utils'; export type CamelKType = IntegrationType | IKameletDefinition | KameletBindingType | PipeType; @@ -41,6 +41,13 @@ export abstract class CamelKResource implements CamelResource { this.metadata = this.resource.metadata && new MetadataEntity(this.resource); } + getCanvasEntityList(): BaseVisualCamelEntityDefinition { + return { + common: [], + groups: {}, + }; + } + removeEntity(_id?: string) {} refreshVisualMetadata() {} createMetadataEntity() { diff --git a/packages/ui/src/models/camel/camel-resource.ts b/packages/ui/src/models/camel/camel-resource.ts index 95f56389b..1b487f434 100644 --- a/packages/ui/src/models/camel/camel-resource.ts +++ b/packages/ui/src/models/camel/camel-resource.ts @@ -7,23 +7,24 @@ import { TileFilter } from '../../components/Catalog'; import { IKameletDefinition } from '../kamelets-catalog'; import { AddStepMode, BaseVisualCamelEntity, IVisualizationNodeData } from '../visualization/base-visual-entity'; import { BeansEntity } from '../visualization/metadata'; +import { RouteTemplateBeansEntity } from '../visualization/metadata/routeTemplateBeansEntity'; import { CamelRouteResource } from './camel-route-resource'; -import { BaseCamelEntity } from './entities'; +import { BaseCamelEntity, EntityType } from './entities'; import { IntegrationResource } from './integration-resource'; import { KameletBindingResource } from './kamelet-binding-resource'; import { KameletResource } from './kamelet-resource'; import { PipeResource } from './pipe-resource'; import { SourceSchemaType } from './source-schema-type'; -import { RouteTemplateBeansEntity } from '../visualization/metadata/routeTemplateBeansEntity'; export interface CamelResource { getVisualEntities(): BaseVisualCamelEntity[]; getEntities(): BaseCamelEntity[]; - addNewEntity(entity?: unknown): string; + addNewEntity(entityType?: EntityType): string; removeEntity(id?: string): void; supportsMultipleVisualEntities(): boolean; toJSON(): unknown; getType(): SourceSchemaType; + getCanvasEntityList(): BaseVisualCamelEntityDefinition; /** Components Catalog related methods */ getCompatibleComponents( @@ -36,6 +37,17 @@ export interface CamelResource { sortFn?: (a: unknown, b: unknown) => number; } +export interface BaseVisualCamelEntityDefinition { + common: BaseVisualCamelEntityDefinitionItem[]; + groups: Record; +} + +export interface BaseVisualCamelEntityDefinitionItem { + name: EntityType; + title: string; + description: string; +} + export interface BeansAwareResource { createBeansEntity(): BeansEntity; deleteBeansEntity(entity: BeansEntity): void; diff --git a/packages/ui/src/models/camel/camel-route-resource.ts b/packages/ui/src/models/camel/camel-route-resource.ts index f79299914..21c77f440 100644 --- a/packages/ui/src/models/camel/camel-route-resource.ts +++ b/packages/ui/src/models/camel/camel-route-resource.ts @@ -1,8 +1,9 @@ import { RouteDefinition } from '@kaoto-next/camel-catalog/types'; import { TileFilter } from '../../components/Catalog'; import { createCamelPropertiesSorter, isDefined } from '../../utils'; -import { AddStepMode } from '../visualization/base-visual-entity'; -import { CamelRouteVisualEntity, isCamelFrom, isCamelRoute } from '../visualization/flows'; +import { CatalogKind } from '../catalog-kind'; +import { AddStepMode, BaseVisualCamelEntityConstructor } from '../visualization/base-visual-entity'; +import { CamelCatalogService, CamelRouteVisualEntity } from '../visualization/flows'; import { CamelErrorHandlerVisualEntity } from '../visualization/flows/camel-error-handler-visual-entity'; import { CamelInterceptFromVisualEntity } from '../visualization/flows/camel-intercept-from-visual-entity'; import { CamelInterceptSendToEndpointVisualEntity } from '../visualization/flows/camel-intercept-send-to-endpoint-visual-entity'; @@ -16,27 +17,34 @@ import { CamelComponentFilterService } from '../visualization/flows/support/came import { CamelRouteVisualEntityData } from '../visualization/flows/support/camel-component-types'; import { FlowTemplateService } from '../visualization/flows/support/flow-templates-service'; import { BeansEntity, isBeans } from '../visualization/metadata'; -import { BeansAwareResource, CamelResource } from './camel-resource'; -import { BaseCamelEntity } from './entities'; +import { BaseVisualCamelEntityDefinition, BeansAwareResource, CamelResource } from './camel-resource'; +import { BaseCamelEntity, EntityType } from './entities'; import { SourceSchemaType } from './source-schema-type'; export class CamelRouteResource implements CamelResource, BeansAwareResource { - static readonly SUPPORTED_ENTITIES = [ - CamelOnExceptionVisualEntity, - CamelOnCompletionVisualEntity, - CamelErrorHandlerVisualEntity, - CamelRestConfigurationVisualEntity, - CamelRouteConfigurationVisualEntity, - CamelInterceptVisualEntity, - CamelInterceptFromVisualEntity, - CamelInterceptSendToEndpointVisualEntity, - ] as const; + static readonly SUPPORTED_ENTITIES: { type: EntityType; group: string; Entity: BaseVisualCamelEntityConstructor }[] = + [ + { type: EntityType.Route, group: '', Entity: CamelRouteVisualEntity }, + { type: EntityType.RouteConfiguration, group: 'Configuration', Entity: CamelRouteConfigurationVisualEntity }, + { type: EntityType.Intercept, group: 'Configuration', Entity: CamelInterceptVisualEntity }, + { type: EntityType.InterceptFrom, group: 'Configuration', Entity: CamelInterceptFromVisualEntity }, + { + type: EntityType.InterceptSendToEndpoint, + group: 'Configuration', + Entity: CamelInterceptSendToEndpointVisualEntity, + }, + { type: EntityType.OnCompletion, group: 'Configuration', Entity: CamelOnCompletionVisualEntity }, + { type: EntityType.OnException, group: 'Error Handling', Entity: CamelOnExceptionVisualEntity }, + { type: EntityType.ErrorHandler, group: 'Error Handling', Entity: CamelErrorHandlerVisualEntity }, + { type: EntityType.RestConfiguration, group: 'Rest', Entity: CamelRestConfigurationVisualEntity }, + ]; static readonly PARAMETERS_ORDER = ['id', 'description', 'uri', 'parameters', 'steps']; readonly sortFn = createCamelPropertiesSorter(CamelRouteResource.PARAMETERS_ORDER) as ( a: unknown, b: unknown, ) => number; private entities: BaseCamelEntity[] = []; + private resolvedEntities: BaseVisualCamelEntityDefinition | undefined; constructor(json?: unknown) { if (!json) return; @@ -50,13 +58,51 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { }, [] as BaseCamelEntity[]); } - addNewEntity(): string { + getCanvasEntityList(): BaseVisualCamelEntityDefinition { + if (isDefined(this.resolvedEntities)) { + return this.resolvedEntities; + } + + this.resolvedEntities = CamelRouteResource.SUPPORTED_ENTITIES.reduce( + (acc, { type, group }) => { + const catalogEntity = CamelCatalogService.getComponent(CatalogKind.Entity, type); + const entityDefinition = { + name: type, + title: catalogEntity?.model.title || type, + description: catalogEntity?.model.description || '', + }; + + if (group === '') { + acc.common.push(entityDefinition); + return acc; + } + + acc.groups[group] ??= []; + acc.groups[group].push(entityDefinition); + return acc; + }, + { common: [], groups: {} } as BaseVisualCamelEntityDefinition, + ); + + return this.resolvedEntities; + } + + addNewEntity(entityType?: EntityType): string { + if (entityType) { + const supportedEntity = CamelRouteResource.SUPPORTED_ENTITIES.find(({ type }) => type === entityType); + if (supportedEntity) { + const entity = new supportedEntity.Entity(); + this.entities.push(entity); + return entity.id; + } + } + const template = FlowTemplateService.getFlowTemplate(this.getType()); const route = template[0].route as RouteDefinition; - const visualEntity = new CamelRouteVisualEntity(route); - this.entities.push(visualEntity); + const entity = new CamelRouteVisualEntity(route); + this.entities.push(entity); - return visualEntity.id; + return entity.id; } getType(): SourceSchemaType { @@ -71,7 +117,7 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { return this.entities.filter( (entity) => entity instanceof CamelRouteVisualEntity || - CamelRouteResource.SUPPORTED_ENTITIES.some((SupportedEntity) => entity instanceof SupportedEntity), + CamelRouteResource.SUPPORTED_ENTITIES.some(({ Entity }) => entity instanceof Entity), ) as CamelRouteVisualEntity[]; } @@ -104,10 +150,6 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { if (index !== -1) { this.entities.splice(index, 1); } - // we don't want to end up with clean entities, so we're adding default one if the list if empty - if (this.entities.length === 0) { - this.addNewEntity(); - } } /** Components Catalog related methods */ @@ -125,17 +167,12 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { return undefined; } - if (isCamelRoute(rawItem)) { - return new CamelRouteVisualEntity(rawItem.route); - } else if (isCamelFrom(rawItem)) { - return new CamelRouteVisualEntity({ from: rawItem.from }); - } else if (isBeans(rawItem)) { + if (isBeans(rawItem)) { return new BeansEntity(rawItem); } - for (const Entity of CamelRouteResource.SUPPORTED_ENTITIES) { + for (const { Entity } of CamelRouteResource.SUPPORTED_ENTITIES) { if (Entity.isApplicable(rawItem)) { - // @ts-expect-error When iterating over the entities, we know that the entity is applicable but TS doesn't, hence causing an error return new Entity(rawItem); } } diff --git a/packages/ui/src/models/camel/source-schema-config.ts b/packages/ui/src/models/camel/source-schema-config.ts index d2f7e2a0b..3064d5949 100644 --- a/packages/ui/src/models/camel/source-schema-config.ts +++ b/packages/ui/src/models/camel/source-schema-config.ts @@ -6,6 +6,7 @@ export interface ISourceSchema { schema: KaotoSchemaDefinition | undefined; name: string; multipleRoute: boolean; + description?: string; } interface IEntitySchemaConfig { @@ -22,26 +23,35 @@ class SourceSchemaConfig { name: 'Camel Route', schema: undefined, multipleRoute: true, + description: + 'Defines an executable integration flow by declaring a source (starter) and followed by a sequence of actions (or steps). Actions can include data manipulations, EIPs (integration patterns) and internal or external calls.', }, [SourceSchemaType.Kamelet]: { name: 'Kamelet', schema: undefined, multipleRoute: false, + description: + 'Defines a reusable Camel route as a building block. Kamelets can not be executed on their own, they are used as sources, actions or sinks in Camel Routes or Pipes.', }, [SourceSchemaType.Pipe]: { name: 'Pipe', schema: undefined, multipleRoute: false, + description: + 'Defines a sequence of concatenated Kamelets to form start to finish integration flows. Pipes are a more abstract level of defining integration flows, by chosing and configuring Kamelets.', }, [SourceSchemaType.KameletBinding]: { name: 'Kamelet Binding', schema: undefined, multipleRoute: false, + description: + 'Defines a sequence of concatenated Kamelets to form start to finish integration flows. Pipes are a more abstract level of defining integration flows, by chosing and configuring Kamelets.', }, [SourceSchemaType.Integration]: { name: 'Integration', schema: undefined, multipleRoute: true, + description: 'An integration defines a Camel route in a CRD file.', }, }; diff --git a/packages/ui/src/models/visualization/base-visual-entity.ts b/packages/ui/src/models/visualization/base-visual-entity.ts index 095e3f935..b25bc7ec3 100644 --- a/packages/ui/src/models/visualization/base-visual-entity.ts +++ b/packages/ui/src/models/visualization/base-visual-entity.ts @@ -53,6 +53,12 @@ export interface BaseVisualCamelEntity extends BaseCamelEntity { toVizNode: () => IVisualizationNode; } +export interface BaseVisualCamelEntityConstructor { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new (...args: any[]): BaseVisualCamelEntity; + isApplicable: (entity: unknown) => boolean; +} + /** * IVisualizationNode * diff --git a/packages/ui/src/models/visualization/flows/camel-error-handler-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-error-handler-visual-entity.ts index 84d4a166c..63923616a 100644 --- a/packages/ui/src/models/visualization/flows/camel-error-handler-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-error-handler-visual-entity.ts @@ -17,7 +17,7 @@ export class CamelErrorHandlerVisualEntity implements BaseVisualCamelEntity { id: string; readonly type = EntityType.ErrorHandler; - constructor(public errorHandlerDef: { errorHandler: ErrorHandlerBuilderDeserializer }) { + constructor(public errorHandlerDef: { errorHandler: ErrorHandlerBuilderDeserializer } = { errorHandler: {} }) { const id = getCamelRandomId('errorHandler'); this.id = id; } diff --git a/packages/ui/src/models/visualization/flows/camel-intercept-from-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-intercept-from-visual-entity.ts index 52176a558..6615e1ebf 100644 --- a/packages/ui/src/models/visualization/flows/camel-intercept-from-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-intercept-from-visual-entity.ts @@ -23,7 +23,7 @@ export class CamelInterceptFromVisualEntity readonly type = EntityType.InterceptFrom; static readonly ROOT_PATH = 'interceptFrom'; - constructor(interceptFromRaw: { interceptFrom: InterceptFrom }) { + constructor(interceptFromRaw: { interceptFrom: InterceptFrom } = { interceptFrom: {} }) { let interceptFromDef: { interceptFrom: Exclude }; if (typeof interceptFromRaw.interceptFrom === 'string') { interceptFromDef = { diff --git a/packages/ui/src/models/visualization/flows/camel-intercept-send-to-endpoint-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-intercept-send-to-endpoint-visual-entity.ts index 064fb64f4..4218caa74 100644 --- a/packages/ui/src/models/visualization/flows/camel-intercept-send-to-endpoint-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-intercept-send-to-endpoint-visual-entity.ts @@ -23,7 +23,9 @@ export class CamelInterceptSendToEndpointVisualEntity readonly type = EntityType.InterceptSendToEndpoint; static readonly ROOT_PATH = 'interceptSendToEndpoint'; - constructor(interceptSendToEndpointRaw: { interceptSendToEndpoint: InterceptSendToEndpoint }) { + constructor( + interceptSendToEndpointRaw: { interceptSendToEndpoint: InterceptSendToEndpoint } = { interceptSendToEndpoint: {} }, + ) { let interceptSendToEndpointDef: { interceptSendToEndpoint: Exclude }; if (typeof interceptSendToEndpointRaw.interceptSendToEndpoint === 'string') { interceptSendToEndpointDef = { diff --git a/packages/ui/src/models/visualization/flows/camel-intercept-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-intercept-visual-entity.ts index b27f63ef7..f2ad3ad41 100644 --- a/packages/ui/src/models/visualization/flows/camel-intercept-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-intercept-visual-entity.ts @@ -22,7 +22,7 @@ export class CamelInterceptVisualEntity readonly type = EntityType.Intercept; static readonly ROOT_PATH = 'intercept'; - constructor(public interceptDef: { intercept: Intercept }) { + constructor(public interceptDef: { intercept: Intercept } = { intercept: {} }) { super(interceptDef); const id = interceptDef.intercept.id ?? getCamelRandomId(CamelInterceptVisualEntity.ROOT_PATH); this.id = id; diff --git a/packages/ui/src/models/visualization/flows/camel-on-completion-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-on-completion-visual-entity.ts index bc52aa6b2..d9192393c 100644 --- a/packages/ui/src/models/visualization/flows/camel-on-completion-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-on-completion-visual-entity.ts @@ -22,7 +22,7 @@ export class CamelOnCompletionVisualEntity readonly type = EntityType.OnCompletion; static readonly ROOT_PATH = 'onCompletion'; - constructor(public onCompletionDef: { onCompletion: OnCompletion }) { + constructor(public onCompletionDef: { onCompletion: OnCompletion } = { onCompletion: {} }) { super(onCompletionDef); const id = onCompletionDef.onCompletion.id ?? getCamelRandomId(CamelOnCompletionVisualEntity.ROOT_PATH); this.id = 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 index f8619bc8e..484588095 100644 --- 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 @@ -22,7 +22,7 @@ export class CamelOnExceptionVisualEntity readonly type = EntityType.OnException; private static readonly ROOT_PATH = 'onException'; - constructor(public onExceptionDef: { onException: OnException }) { + constructor(public onExceptionDef: { onException: OnException } = { onException: {} }) { super(onExceptionDef); const id = onExceptionDef.onException.id ?? getCamelRandomId(CamelOnExceptionVisualEntity.ROOT_PATH); this.id = id; diff --git a/packages/ui/src/models/visualization/flows/camel-rest-configuration-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-rest-configuration-visual-entity.ts index 864a76c6d..c3e37fd6a 100644 --- a/packages/ui/src/models/visualization/flows/camel-rest-configuration-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-rest-configuration-visual-entity.ts @@ -21,7 +21,7 @@ export class CamelRestConfigurationVisualEntity implements BaseVisualCamelEntity readonly type = EntityType.RestConfiguration; private schemaValidator: ValidateFunction | undefined; - constructor(public restConfigurationDef: { restConfiguration: RestConfiguration }) { + constructor(public restConfigurationDef: { restConfiguration: RestConfiguration } = { restConfiguration: {} }) { const id = getCamelRandomId('restConfiguration'); this.id = id; } diff --git a/packages/ui/src/models/visualization/flows/camel-route-configuration-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-route-configuration-visual-entity.ts index 49140e912..8662e81a0 100644 --- a/packages/ui/src/models/visualization/flows/camel-route-configuration-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-route-configuration-visual-entity.ts @@ -37,7 +37,9 @@ export class CamelRouteConfigurationVisualEntity 'onCompletion', ]; - constructor(public routeConfigurationDef: { routeConfiguration: RouteConfigurationDefinition }) { + constructor( + public routeConfigurationDef: { routeConfiguration: RouteConfigurationDefinition } = { routeConfiguration: {} }, + ) { super(routeConfigurationDef); const id = routeConfigurationDef.routeConfiguration.id ?? getCamelRandomId(CamelRouteConfigurationVisualEntity.ROOT_PATH); diff --git a/packages/ui/src/models/visualization/flows/camel-route-visual-entity.ts b/packages/ui/src/models/visualization/flows/camel-route-visual-entity.ts index 2fdb39436..e3c697eb1 100644 --- a/packages/ui/src/models/visualization/flows/camel-route-visual-entity.ts +++ b/packages/ui/src/models/visualization/flows/camel-route-visual-entity.ts @@ -38,14 +38,37 @@ export const isCamelFrom = (rawEntity: unknown): rawEntity is { from: FromDefini export class CamelRouteVisualEntity extends AbstractCamelVisualEntity { id: string; + route: RouteDefinition; readonly type = EntityType.Route; - constructor(public route: RouteDefinition) { + constructor( + routeRaw: RouteDefinition = { + from: { + uri: '', + steps: [], + }, + }, + ) { + let route: RouteDefinition; + if (isCamelFrom(routeRaw)) { + route = { + from: routeRaw.from, + }; + } else { + route = routeRaw; + } + super(route); - this.id = route.id ?? getCamelRandomId('route'); + this.route = route; + const id = routeRaw.id ?? getCamelRandomId('route'); + this.id = id; this.route.id = this.id; } + static isApplicable(routeDef: unknown): routeDef is { route: RouteDefinition } | { from: FromDefinition } { + return isCamelRoute(routeDef) || isCamelFrom(routeDef); + } + /** Internal API methods */ setId(routeId: string): void { this.id = routeId;