diff --git a/src/components/Catalog.stories.tsx b/src/components/Catalog.stories.tsx index ad542bd4e..7285de946 100644 --- a/src/components/Catalog.stories.tsx +++ b/src/components/Catalog.stories.tsx @@ -1,5 +1,5 @@ import { Catalog } from './Catalog'; -import KaotoDrawer from './KaotoDrawer'; +import { KaotoDrawer } from './KaotoDrawer'; import { DrawerContentBody } from '@patternfly/react-core'; import { StoryFn, Meta } from '@storybook/react'; diff --git a/src/components/DSL/DSLSelector.test.tsx b/src/components/DSL/DSLSelector.test.tsx index 354e48dac..aaf11f099 100644 --- a/src/components/DSL/DSLSelector.test.tsx +++ b/src/components/DSL/DSLSelector.test.tsx @@ -30,13 +30,13 @@ describe('DSLSelector.tsx', () => { test('component renders', () => { const wrapper = render(); - const toggle = wrapper.queryByTestId('dsl-select'); + const toggle = wrapper.queryByTestId('dsl-list-dropdown'); expect(toggle).toBeInTheDocument(); }); test('should toggle list of DSLs', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -56,7 +56,7 @@ describe('DSLSelector.tsx', () => { test('should show list of DSLs', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -69,7 +69,7 @@ describe('DSLSelector.tsx', () => { test('should show selected value', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -94,7 +94,7 @@ describe('DSLSelector.tsx', () => { test('should not have anything selected if "isStatic=true"', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -122,7 +122,7 @@ describe('DSLSelector.tsx', () => { const wrapper = render( , ); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -138,7 +138,7 @@ describe('DSLSelector.tsx', () => { test('should have selected the first DSL if selectedDsl is not provided', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -154,7 +154,7 @@ describe('DSLSelector.tsx', () => { test('should close Select when pressing ESC', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -179,7 +179,7 @@ describe('DSLSelector.tsx', () => { test('should call onSelect callback when provided', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -199,7 +199,7 @@ describe('DSLSelector.tsx', () => { test('should not call onSelect spy when not provided', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { @@ -219,7 +219,7 @@ describe('DSLSelector.tsx', () => { test('should not call onSelect spy when the selected id does not exist', async () => { const wrapper = render(); - const toggle = await wrapper.findByTestId('dsl-select'); + const toggle = await wrapper.findByTestId('dsl-list-dropdown'); /** Open Select */ act(() => { diff --git a/src/components/DSL/DSLSelector.tsx b/src/components/DSL/DSLSelector.tsx index 9d824eac8..a15b017ac 100644 --- a/src/components/DSL/DSLSelector.tsx +++ b/src/components/DSL/DSLSelector.tsx @@ -1,6 +1,6 @@ import { useSettingsStore } from '@kaoto/store'; import { IDsl } from '@kaoto/types'; -import { MenuToggle, MenuToggleElement } from '@patternfly/react-core'; +import { MenuToggle, MenuToggleAction, MenuToggleElement } from '@patternfly/react-core'; import { Select, SelectList, SelectOption } from '@patternfly/react-core/next'; import { FunctionComponent, @@ -19,16 +19,21 @@ interface IDSLSelector extends PropsWithChildren { } export const DSLSelector: FunctionComponent = (props) => { - const capabilities = useSettingsStore((state) => state.settings.capabilities, shallow); + const { capabilities, dsl } = useSettingsStore( + ({ settings }) => ({ capabilities: settings.capabilities, dsl: settings.dsl }), + shallow, + ); const [isOpen, setIsOpen] = useState(false); const [selected, setSelected] = useState( props.isStatic ? undefined : props.selectedDsl ?? capabilities[0], ); + /** Toggle the DSL dropdown */ const onToggleClick = () => { setIsOpen(!isOpen); }; + /** Selecting a DSL checking the the existing flows */ const onSelect = useCallback( ( _: ReactMouseEvent | undefined, @@ -48,21 +53,38 @@ export const DSLSelector: FunctionComponent = (props) => { [capabilities, props], ); + /** Selecting the same DSL directly*/ + const onNewSameTypeRoute = useCallback(() => { + onSelect(undefined, dsl.name); + }, [dsl.name, onSelect]); + const toggle = (toggleRef: Ref) => ( - {props.children} - + splitButtonOptions={{ + variant: 'action', + items: [ + + {props.children} + , + ], + }} + /> ); return ( { + setIsOpen(isOpen); + }} + toggle={toggle} + minWidth="300px" + > + { + setIsOpen(false); + }} + /> + + ); +}; diff --git a/src/components/KaotoDrawer.tsx b/src/components/KaotoDrawer.tsx index 6d84de685..efe0d5d98 100644 --- a/src/components/KaotoDrawer.tsx +++ b/src/components/KaotoDrawer.tsx @@ -73,5 +73,3 @@ export const KaotoDrawer = ({ ); }; - -export default KaotoDrawer; diff --git a/src/components/KaotoToolbar.tsx b/src/components/KaotoToolbar.tsx index bef1dbc9f..800f9cb65 100644 --- a/src/components/KaotoToolbar.tsx +++ b/src/components/KaotoToolbar.tsx @@ -1,14 +1,13 @@ import logo from '../assets/images/logo-kaoto-dark.png'; import { AboutModal } from './AboutModal'; +import { AppearanceModal } from './AppearanceModal'; +import { ConfirmationModal } from './ConfirmationModal'; import { NewFlow } from './DSL/NewFlow'; +import { DeploymentsModal } from './DeploymentsModal'; import { ExportCanvasToPng } from './ExportCanvasToPng'; +import { FlowsMenu } from './Flows/FlowsMenu'; +import { SettingsModal } from './SettingsModal'; import { fetchDefaultNamespace, startDeployment } from '@kaoto/api'; -import { - AppearanceModal, - ConfirmationModal, - DeploymentsModal, - SettingsModal, -} from '@kaoto/components'; import { LOCAL_STORAGE_UI_THEME_KEY, THEME_DARK_CLASS } from '@kaoto/constants'; import { useDeploymentStore, @@ -319,6 +318,11 @@ export const KaotoToolbar = ({ + {/* FLOWS LIST DROPDOWN BUTTON */} + + + + {/* DEPLOYMENT STATUS */} diff --git a/src/components/PlusButtonEdge.tsx b/src/components/PlusButtonEdge.tsx index fcc280470..0371c0364 100644 --- a/src/components/PlusButtonEdge.tsx +++ b/src/components/PlusButtonEdge.tsx @@ -1,5 +1,6 @@ +import { BranchBuilder } from './BranchBuilder'; import './CustomEdge.css'; -import { BranchBuilder, MiniCatalog } from '@kaoto/components'; +import { MiniCatalog } from './MiniCatalog'; import { usePosition, useShowBranchTab } from '@kaoto/hooks'; import { StepsService, ValidationService } from '@kaoto/services'; import { useFlowsStore } from '@kaoto/store'; @@ -55,7 +56,7 @@ const PlusButtonEdge = ({ const views = useFlowsStore((state) => state.views, shallow); const { disableBranchesTab, disableBranchesTabMsg } = useShowBranchTab( sourceNode?.data.step, - views + views, ); const [edgePath, edgeCenterX, edgeCenterY] = getBezierPath({ @@ -106,7 +107,7 @@ const PlusButtonEdge = ({ queryParams={{ type: ValidationService.insertableStepTypes( sourceNode?.data.step, - targetNode?.data.step + targetNode?.data.step, ), previousStep: sourceNode?.data.step.id, followingStep: targetNode?.data.step.id, diff --git a/src/components/SourceCodeEditor.tsx b/src/components/SourceCodeEditor.tsx index cd0b37df1..8b7fa61b7 100644 --- a/src/components/SourceCodeEditor.tsx +++ b/src/components/SourceCodeEditor.tsx @@ -1,5 +1,5 @@ +import { StepErrorBoundary } from './StepErrorBoundary'; import { fetchCapabilities, fetchIntegrationJson, fetchIntegrationSourceCode } from '@kaoto/api'; -import { StepErrorBoundary } from '@kaoto/components'; import { useFlowsStore, useIntegrationSourceStore, useSettingsStore } from '@kaoto/store'; import { CodeEditorMode, ICapabilities, IFlowsWrapper, ISettings } from '@kaoto/types'; import { usePrevious } from '@kaoto/utils'; @@ -15,7 +15,7 @@ interface ISourceCodeEditor { initialData?: string; language?: Language; theme?: string; - mode?: CodeEditorMode | CodeEditorMode.FREE_EDIT; + mode?: CodeEditorMode; schemaUri?: string; editable?: boolean | false; syncAction?: () => {}; @@ -33,7 +33,7 @@ export const SourceCodeEditor = (props: ISourceCodeEditor) => { metadata, setFlowsWrapper, }), - shallow + shallow, ); const previousFlows = usePrevious(flows); @@ -48,7 +48,7 @@ export const SourceCodeEditor = (props: ISourceCodeEditor) => { fetchCapabilities().then((capabilities: ICapabilities) => { capabilities.dsls.forEach((dsl) => { if (dsl.name === flows[0].dsl) { - const tmpSettings = { ...settings, dsl: dsl }; + const tmpSettings = { ...settings, dsl }; setSettings(tmpSettings); fetchTheSourceCode({ flows, properties, metadata }, tmpSettings); } @@ -64,7 +64,6 @@ export const SourceCodeEditor = (props: ISourceCodeEditor) => { ...currentFlowsWrapper, flows: currentFlowsWrapper.flows.map((flow) => ({ ...flow, - metadata: { ...flow.metadata, ...settings }, dsl: settings.dsl.name, })), }; @@ -90,7 +89,6 @@ export const SourceCodeEditor = (props: ISourceCodeEditor) => { settings.name = tmpInt.metadata.name; setSettings({ name: tmpInt.metadata.name }); } - tmpInt.metadata = { ...tmpInt.metadata, ...settings }; setFlowsWrapper(flowsWrapper); }) .catch((e) => { @@ -119,7 +117,7 @@ export const SourceCodeEditor = (props: ISourceCodeEditor) => { editor?.onDidAttemptReadOnlyEdit(() => { messageContribution?.showMessage( 'Cannot edit in read-only editor mode.', - editor.getPosition() + editor.getPosition(), ); }); diff --git a/src/components/Visualization.tsx b/src/components/Visualization.tsx index 2de73f5fd..392519bb5 100644 --- a/src/components/Visualization.tsx +++ b/src/components/Visualization.tsx @@ -1,13 +1,11 @@ +import { DeleteButtonEdge } from './DeleteButtonEdge'; +import { KaotoDrawer } from './KaotoDrawer'; +import { PlusButtonEdge } from './PlusButtonEdge'; +import { StepErrorBoundary } from './StepErrorBoundary'; import './Visualization.css'; -import { - DeleteButtonEdge, - KaotoDrawer, - PlusButtonEdge, - StepErrorBoundary, - VisualizationControls, - VisualizationStep, - VisualizationStepViews, -} from '@kaoto/components'; +import { VisualizationControls } from './VisualizationControls'; +import { VisualizationStep } from './VisualizationStep'; +import { VisualizationStepViews } from './VisualizationStepViews'; import { StepsService, VisualizationService } from '@kaoto/services'; import { useFlowsStore, useVisualizationStore } from '@kaoto/store'; import { HandleDeleteStepFn, IStepProps, IVizStepNode } from '@kaoto/types'; @@ -26,18 +24,21 @@ const Visualization = () => { zoom: 1.2, }); const [isPanelExpanded, setIsPanelExpanded] = useState(false); - const [selectedStep, setSelectedStep] = useState({ - maxBranches: 0, - minBranches: 0, - name: '', - type: '', - UUID: '', - integrationId: '', - }); - - const { selectedStepUuid, setSelectedStepUuid } = useVisualizationStore(({ selectedStepUuid, setSelectedStepUuid }) => ({ selectedStepUuid, setSelectedStepUuid }), shallow); - const { onNodesChange, onEdgesChange } = useVisualizationStore(({ onNodesChange, onEdgesChange }) => ({ onNodesChange, onEdgesChange }), shallow); - const layout = useVisualizationStore((state) => state.layout); + const [selectedStep, setSelectedStep] = useState( + VisualizationService.getEmptySelectedStep(), + ); + const { selectedStepUuid, setSelectedStepUuid } = useVisualizationStore( + ({ selectedStepUuid, setSelectedStepUuid }) => ({ selectedStepUuid, setSelectedStepUuid }), + shallow, + ); + const { onNodesChange, onEdgesChange } = useVisualizationStore( + ({ onNodesChange, onEdgesChange }) => ({ onNodesChange, onEdgesChange }), + shallow, + ); + const { layout, visibleFlows } = useVisualizationStore(({ layout, visibleFlows }) => ({ + layout, + visibleFlows, + })); const nodes = useVisualizationStore((state) => state.nodes); const edges = useVisualizationStore((state) => state.edges); const flows = useFlowsStore((state) => state.flows); @@ -65,27 +66,30 @@ const Visualization = () => { if (step) { setSelectedStep(step); } else { - setSelectedStep({ maxBranches: 0, minBranches: 0, name: '', type: '', UUID: '', integrationId: '' }); + setSelectedStep(VisualizationService.getEmptySelectedStep()); setSelectedStepUuid(''); setIsPanelExpanded(false); } }, [flows, selectedStep.integrationId, selectedStepUuid, setSelectedStepUuid, stepsService]); - const handleDeleteStep: HandleDeleteStepFn = useCallback((integrationId, UUID) => { - if (!integrationId || !UUID) return; + const handleDeleteStep: HandleDeleteStepFn = useCallback( + (integrationId, UUID) => { + if (!integrationId || !UUID) return; - if (selectedStepUuid === UUID) { - setIsPanelExpanded(false); - setSelectedStep({ maxBranches: 0, minBranches: 0, name: '', type: '', UUID: '', integrationId: '' }); - setSelectedStepUuid(''); - } + if (selectedStepUuid === UUID) { + setIsPanelExpanded(false); + setSelectedStep(VisualizationService.getEmptySelectedStep()); + setSelectedStepUuid(''); + } - stepsService.deleteStep(integrationId, UUID); - }, [selectedStepUuid, setSelectedStepUuid, stepsService]); + stepsService.deleteStep(integrationId, UUID); + }, + [selectedStepUuid, setSelectedStepUuid, stepsService], + ); useEffect(() => { - visualizationService.redrawDiagram(handleDeleteStep, true).catch((e) => console.error(e)); - }, [handleDeleteStep, flows, layout, visualizationService]); + visualizationService.redrawDiagram(handleDeleteStep).catch((e) => console.error(e)); + }, [handleDeleteStep, flows, layout, visibleFlows, visualizationService]); const nodeTypes = useMemo(() => ({ step: VisualizationStep }), []); const edgeTypes = useMemo( @@ -93,7 +97,7 @@ const Visualization = () => { delete: DeleteButtonEdge, insert: PlusButtonEdge, }), - [] + [], ); const onClosePanelClick = useCallback(() => { @@ -125,7 +129,10 @@ const Visualization = () => { if (!e.target.classList.contains('stepNode__clickable')) return; if (!node.data.isPlaceholder) { - const step = stepsService.findStepWithUUID(node.data.step.integrationId, node.data.step.UUID); + const step = stepsService.findStepWithUUID( + node.data.step.integrationId, + node.data.step.UUID, + ); if (step) { setSelectedStep(step); setSelectedStepUuid(step.UUID); @@ -143,7 +150,7 @@ const Visualization = () => { } } }, - [isPanelExpanded, selectedStepUuid, setSelectedStepUuid, stepsService] + [isPanelExpanded, selectedStepUuid, setSelectedStepUuid, stepsService], ); /** @@ -151,9 +158,12 @@ const Visualization = () => { * @param step * @param newValues */ - const saveConfig = useCallback((step: IStepProps, newValues: Record) => { - stepsService.updateStepParameters(step, newValues); - }, [stepsService]); + const saveConfig = useCallback( + (step: IStepProps, newValues: Record) => { + stepsService.updateStepParameters(step, newValues); + }, + [stepsService], + ); return ( diff --git a/src/components/VisualizationStep.tsx b/src/components/VisualizationStep.tsx index fcca38159..98eaf52a9 100644 --- a/src/components/VisualizationStep.tsx +++ b/src/components/VisualizationStep.tsx @@ -1,8 +1,8 @@ import { AppendStepButton } from './AppendStepButton'; import { BranchBuilder } from './BranchBuilder'; +import { MiniCatalog } from './MiniCatalog'; import { PrependStepButton } from './PrependStepButton'; import './Visualization.css'; -import { MiniCatalog } from '@kaoto/components'; import { usePosition } from '@kaoto/hooks'; import { StepsService, VisualizationService } from '@kaoto/services'; import { useSettingsStore, useVisualizationStore } from '@kaoto/store'; @@ -104,7 +104,7 @@ const VisualizationStep = ({ data }: NodeProps) => { return VisualizationService.getNodeClass( visualizationStore.selectedStepUuid, data.step.UUID, - ' stepNode__Selected' + ' stepNode__Selected', ); }; @@ -112,7 +112,7 @@ const VisualizationStep = ({ data }: NodeProps) => { return VisualizationService.getNodeClass( visualizationStore.hoverStepUuid, data.branchInfo?.rootStepUuid ?? data.step.UUID, - ' stepNode__Hover' + ' stepNode__Hover', ); }; diff --git a/src/components/VisualizationStepViews.test.tsx b/src/components/VisualizationStepViews.test.tsx index 1fb1377bc..f4c9a7b79 100644 --- a/src/components/VisualizationStepViews.test.tsx +++ b/src/components/VisualizationStepViews.test.tsx @@ -1,11 +1,11 @@ -import { JsonSchemaConfigurator } from '@kaoto/components'; +import { JsonSchemaConfigurator } from './JsonSchemaConfigurator'; import { act, fireEvent, render } from '@testing-library/react'; import { AlertProvider } from '../layout'; import { debeziumMongoDBStep } from '../stubs'; import { VisualizationStepViews } from './VisualizationStepViews'; -jest.mock('@kaoto/components', () => { - const actual = jest.requireActual('@kaoto/components'); +jest.mock('./JsonSchemaConfigurator', () => { + const actual = jest.requireActual('./JsonSchemaConfigurator'); const JsonSchemaConfiguratorMock: typeof JsonSchemaConfigurator = (props) => (<>

JsonSchemaConfigurator mock

diff --git a/src/components/VisualizationStepViews.tsx b/src/components/VisualizationStepViews.tsx index 87bbcc36b..66c983c6b 100644 --- a/src/components/VisualizationStepViews.tsx +++ b/src/components/VisualizationStepViews.tsx @@ -1,5 +1,7 @@ +import { Extension } from './Extension'; +import { JsonSchemaConfigurator } from './JsonSchemaConfigurator'; +import { StepErrorBoundary } from './StepErrorBoundary'; import { dynamicImport } from './import'; -import { Extension, JsonSchemaConfigurator, StepErrorBoundary } from '@kaoto/components'; import { StepsService } from '@kaoto/services'; import { useFlowsStore } from '@kaoto/store'; import { IStepProps, IStepPropsParameters } from '@kaoto/types'; @@ -34,8 +36,14 @@ const VisualizationStepViews = ({ saveConfig, step, }: IStepViewsProps) => { - const debouncedSaveConfig = useDebouncedCallback(saveConfig, 1_000, { leading: false, trailing: true }); - const views = useFlowsStore((state) => state.views.filter((view) => view.step === step.UUID), shallow); + const debouncedSaveConfig = useDebouncedCallback(saveConfig, 1_000, { + leading: false, + trailing: true, + }); + const views = useFlowsStore( + (state) => state.views.filter((view) => view.step === step.UUID), + shallow, + ); const hasDetailView = views?.some((v) => v.id === 'detail-step'); const detailsTabIndex = views?.length! + 1; // provide an index that won't be used by custom views @@ -81,15 +89,14 @@ const VisualizationStepViews = ({ }; useEffect(() => { - setActiveTabKey(()=> - step.parameters?.length ? configTabIndex : detailsTabIndex); + setActiveTabKey(() => (step.parameters?.length ? configTabIndex : detailsTabIndex)); let tempSchemaObject: { [label: string]: { type: string; value?: any; description?: string } } = {}; let tempModelObject = {} as IStepPropsParameters; step.parameters?.forEach((p) => - StepsService.buildStepSchemaAndModel(p, tempModelObject, tempSchemaObject) + StepsService.buildStepSchemaAndModel(p, tempModelObject, tempSchemaObject), ); setStepPropertySchema(tempSchemaObject); @@ -165,7 +172,13 @@ const VisualizationStepViews = ({ {views?.length! > 0 && views?.map((view, index) => { const StepExtension = lazy(() => dynamicImport(view.scope, view.module, view.url)); - const kaotoApi = stepsService.createKaotoApi(step, (values) => {saveConfig(step, values)}, alertKaoto); + const kaotoApi = stepsService.createKaotoApi( + step, + (values) => { + saveConfig(step, values); + }, + alertKaoto, + ); return ( { return this.props.children; } } - -export default ErrorBoundary; diff --git a/src/services/FlowsService.test.ts b/src/services/FlowsService.test.ts index 2d2ff22ac..8ead2268e 100644 --- a/src/services/FlowsService.test.ts +++ b/src/services/FlowsService.test.ts @@ -5,7 +5,7 @@ describe('FlowsService', () => { const newFlow = FlowsService.getNewFlow('Integration'); expect(newFlow).toMatchObject({ - id: /Integration-[0-9]*/, + id: /Integration-\d*/, dsl: 'Integration', description: '', metadata: {}, diff --git a/src/services/FlowsService.ts b/src/services/FlowsService.ts index b3b5809ca..48840c826 100644 --- a/src/services/FlowsService.ts +++ b/src/services/FlowsService.ts @@ -14,7 +14,8 @@ export class FlowsService { flowId?: string, options?: { metadata: IIntegration['metadata'] }, ): IIntegration { - const id = flowId ?? `${dsl}-${getRandomArbitraryNumber()}`; + const randomNumber = getRandomArbitraryNumber(); + const id = flowId ?? `route-${randomNumber.toString(10).slice(0, 4)}`; return { id, diff --git a/src/services/visualizationService.test.ts b/src/services/visualizationService.test.ts index 7490132e7..625857ecc 100644 --- a/src/services/visualizationService.test.ts +++ b/src/services/visualizationService.test.ts @@ -2,7 +2,7 @@ import branchSteps from '../store/data/branchSteps'; import nodes from '../store/data/nodes'; import steps from '../store/data/steps'; import { VisualizationService } from './visualizationService'; -import { useVisualizationStore } from '@kaoto/store'; +import { useFlowsStore, useVisualizationStore } from '@kaoto/store'; import { IStepProps, IStepPropsBranch, @@ -16,7 +16,19 @@ import { Position } from 'reactflow'; describe('visualizationService', () => { const groupWidth = 80; - const baseStep = { UUID: '', name: '', maxBranches: 0, minBranches: 0, type: '', integrationId: 'Camel Route-1' }; + const baseStep = { + UUID: '', + name: '', + maxBranches: 0, + minBranches: 0, + type: '', + integrationId: 'Camel Route-1', + }; + let service: VisualizationService; + + beforeEach(() => { + service = new VisualizationService(); + }); it('buildBranchNodeParams(): should build params for a branch node', () => { const currentStep = steps[3]; @@ -70,7 +82,7 @@ describe('visualizationService', () => { const rootNode = { data: { step: {} } } as IVizStepNode; const rootNodeNext = { data: { step: {} } } as IVizStepNode; expect( - VisualizationService.buildBranchSingleStepEdges(node, rootNode, rootNodeNext) + VisualizationService.buildBranchSingleStepEdges(node, rootNode, rootNodeNext), ).toHaveLength(2); }); @@ -90,7 +102,12 @@ describe('visualizationService', () => { const rootNodeNext = { data: { step: {} } } as IVizStepNode; expect( - VisualizationService.buildBranchSingleStepEdges(node, rootNode, rootNodeNext, 'CUSTOM-NODE') + VisualizationService.buildBranchSingleStepEdges( + node, + rootNode, + rootNodeNext, + 'CUSTOM-NODE', + ), ).toHaveLength(2); }); @@ -113,11 +130,122 @@ describe('visualizationService', () => { const rootNodeNext = { data: { step: {} } } as IVizStepNode; expect( - VisualizationService.buildBranchSingleStepEdges(node, rootNode, rootNodeNext) + VisualizationService.buildBranchSingleStepEdges(node, rootNode, rootNodeNext), ).toHaveLength(2); }); }); + describe('redrawDiagram', () => { + it('should process only visible flows', async () => { + useFlowsStore.getState().deleteAllFlows(); + useFlowsStore.getState().addNewFlow('Integration', 'route-1234'); + useFlowsStore.getState().addNewFlow('Integration', 'route-4321'); + + const getLayoutedElementsSpy = jest.spyOn(VisualizationService, 'getLayoutedElements'); + + await service.redrawDiagram(jest.fn()); + + expect(getLayoutedElementsSpy).toHaveBeenCalledTimes(1); + expect(getLayoutedElementsSpy).toHaveBeenCalledWith( + [ + expect.objectContaining({ + data: { + isPlaceholder: true, + label: 'ADD A STEP', + nextStepUuid: undefined, + step: { + UUID: expect.stringMatching(/placeholder-\d+/), + integrationId: 'route-4321', + name: '', + type: 'START', + }, + }, + draggable: false, + height: 80, + id: expect.stringMatching(/node_0--\d+/), + position: { + x: 0, + y: 0, + }, + sourcePosition: 'right', + targetPosition: 'left', + type: 'step', + width: 80, + }), + ], + [], + 'LR', + ); + }); + + it('should process more than one visible flow', async () => { + useFlowsStore.getState().deleteAllFlows(); + useFlowsStore.getState().addNewFlow('Integration', 'route-1234'); + useFlowsStore.getState().addNewFlow('Integration', 'route-4321'); + useVisualizationStore.getState().showAllFlows(); + + const getLayoutedElementsSpy = jest.spyOn(VisualizationService, 'getLayoutedElements'); + + await service.redrawDiagram(jest.fn()); + + expect(getLayoutedElementsSpy).toHaveBeenCalledTimes(1); + expect(getLayoutedElementsSpy).toHaveBeenCalledWith( + [ + expect.objectContaining({ + data: { + isPlaceholder: true, + label: 'ADD A STEP', + nextStepUuid: undefined, + step: { + UUID: expect.stringMatching(/placeholder-\d+/), + integrationId: 'route-1234', + name: '', + type: 'START', + }, + }, + draggable: false, + height: 80, + id: expect.stringMatching(/node_0--\d+/), + position: { + x: 0, + y: 0, + }, + sourcePosition: 'right', + targetPosition: 'left', + type: 'step', + width: 80, + }), + expect.objectContaining({ + data: { + isPlaceholder: true, + label: 'ADD A STEP', + nextStepUuid: undefined, + step: { + UUID: expect.stringMatching(/placeholder-\d+/), + integrationId: 'route-4321', + name: '', + type: 'START', + }, + }, + draggable: false, + height: 80, + id: expect.stringMatching(/node_0--\d+/), + position: { + x: 0, + y: expect.any(Number), + }, + sourcePosition: 'right', + targetPosition: 'left', + type: 'step', + width: 80, + }), + ], + [], + 'LR', + ); + }); + }); + it("buildEdgeParams(): should build an edge's default parameters for a single given node", () => { const currentStep = nodes[1]; const previousStep = nodes[0]; @@ -137,7 +265,10 @@ describe('visualizationService', () => { position: { x: 720, y: 250 }, }, { - data: { label: 'avro-deserialize-sink', step: { ...baseStep, UUID: 'example-1235', integrationId: 'Camel Route-1' } }, + data: { + label: 'avro-deserialize-sink', + step: { ...baseStep, UUID: 'example-1235', integrationId: 'Camel Route-1' }, + }, id: 'dndnode_2', position: { x: 880, y: 250 }, }, @@ -146,7 +277,11 @@ describe('visualizationService', () => { expect(VisualizationService.buildEdges(nodes)).toHaveLength(1); // let's test that it works for branching too - const stepNodes = VisualizationService.buildNodesFromSteps('Camel Route-1', branchSteps, 'RIGHT'); + const stepNodes = VisualizationService.buildNodesFromSteps( + 'Camel Route-1', + branchSteps, + 'RIGHT', + ); expect(VisualizationService.buildEdges(stepNodes)).toHaveLength(branchSteps.length - 1); }); @@ -184,7 +319,11 @@ describe('visualizationService', () => { }); it.skip('buildNodesFromSteps(): should build visualization nodes from an array of steps with branches', () => { - const stepNodes = VisualizationService.buildNodesFromSteps('Camel Route-1', branchSteps, 'RIGHT'); + const stepNodes = VisualizationService.buildNodesFromSteps( + 'Camel Route-1', + branchSteps, + 'RIGHT', + ); expect(stepNodes[0].data.step.UUID).toBeDefined(); expect(stepNodes).toHaveLength(branchSteps.length); }); @@ -221,7 +360,7 @@ describe('visualizationService', () => { id: 'dndnode_2', position: { x: 660, y: 250 }, }, - ]) + ]), ).toBe(false); }); @@ -386,19 +525,21 @@ describe('visualizationService', () => { const something = 'something'; const nothing = undefined; expect(VisualizationService.getNodeClass('example', 'example', ' stepNode__Hover')).toEqual( - ' stepNode__Hover' + ' stepNode__Hover', ); expect( - VisualizationService.getNodeClass(something, nothing ?? 'example', ' stepNode__Hover') + VisualizationService.getNodeClass(something, nothing ?? 'example', ' stepNode__Hover'), ).toEqual(''); expect( - VisualizationService.getNodeClass(something, nothing ?? something, ' stepNode__Hover') + VisualizationService.getNodeClass(something, nothing ?? something, ' stepNode__Hover'), ).toEqual(' stepNode__Hover'); }); it('insertAddStepPlaceholder(): should add an ADD STEP placeholder to the beginning of the array', () => { const nodes: IVizStepNode[] = []; - VisualizationService.insertAddStepPlaceholder('Camel Route-1', nodes, '', 'START', { nextStepUuid: '' }); + VisualizationService.insertAddStepPlaceholder('Camel Route-1', nodes, '', 'START', { + nextStepUuid: '', + }); expect(nodes).toHaveLength(1); }); @@ -417,7 +558,7 @@ describe('visualizationService', () => { branchInfo: {} as IVizStepNodeDataBranch, nextStepUuid: 'some-next-step', previousStepUuid: 'some-previous-step', - }) + }), ).toBeFalsy(); }); @@ -525,8 +666,8 @@ describe('visualizationService', () => { ...lastStepWithBranch, isLastStep: false, }, - false - ) + false, + ), ).toBeTruthy(); // a trick step at the end of an array, an END step, with a branches array but no min/max branching. @@ -534,8 +675,8 @@ describe('visualizationService', () => { expect( VisualizationService.showAppendStepButton( { isLastStep: true, step: { branches: [{}] as IStepPropsBranch[] } } as IVizStepNodeData, - true - ) + true, + ), ).toBeFalsy(); }); @@ -548,7 +689,7 @@ describe('visualizationService', () => { VisualizationService.showBranchesTab({ ...step, branches: [], - }) + }), ).toBeFalsy(); expect( @@ -557,7 +698,7 @@ describe('visualizationService', () => { branches: [], minBranches: 0, maxBranches: -1, - }) + }), ).toBeTruthy(); // if step has maximum number of branches already @@ -567,7 +708,7 @@ describe('visualizationService', () => { branches: [{}, {}] as IStepPropsBranch[], minBranches: 0, maxBranches: 2, - }) + }), ).toBeFalsy(); }); @@ -601,7 +742,7 @@ describe('visualizationService', () => { // is a first step, is an end step expect( - visualizationService.showPrependStepButton({ ...node, step: { ...node.step, type: 'END' } }) + visualizationService.showPrependStepButton({ ...node, step: { ...node.step, type: 'END' } }), ).toBeFalsy(); // is a first step, is not an end step @@ -609,7 +750,7 @@ describe('visualizationService', () => { visualizationService.showPrependStepButton({ ...node, step: { ...node.step, type: 'MIDDLE' }, - }) + }), ).toBeTruthy(); // is not a first step, is not an end step @@ -618,7 +759,7 @@ describe('visualizationService', () => { ...node, step: { ...node.step, type: 'MIDDLE' }, isFirstStep: false, - }) + }), ).toBeFalsy(); // is not a first step, is an end step @@ -627,7 +768,7 @@ describe('visualizationService', () => { ...node, step: { ...node.step, type: 'END' }, isFirstStep: false, - }) + }), ).toBeFalsy(); // current step is NOT an end step, but its previous step contains a branch @@ -637,7 +778,7 @@ describe('visualizationService', () => { isFirstStep: false, previousStepUuid: 'step-one', step: { ...node.step, type: 'MIDDLE' } as IStepProps, - }) + }), ).toBeTruthy(); }); @@ -655,7 +796,7 @@ describe('visualizationService', () => { VisualizationService.showStepsTab({ ...step, step: { ...step.step, branches: [{} as IStepPropsBranch] }, - }) + }), ).toBeTruthy(); // contains branches, has a next step, should not show steps tab @@ -664,7 +805,91 @@ describe('visualizationService', () => { ...step, step: { ...step.step, branches: [{} as IStepPropsBranch] }, nextStepUuid: 'some-dummy-node-uuid', - }) + }), ).toBeFalsy(); }); + + it('should allow consumers to get a static empty step', () => { + const result = VisualizationService.getEmptySelectedStep(); + + expect(result).toEqual({ + maxBranches: 0, + minBranches: 0, + name: '', + type: '', + UUID: '', + integrationId: '', + }); + }); + + describe('getVisibleFlowsInformation', () => { + it('should return the flow id for a single visible flow', () => { + const result = VisualizationService.getVisibleFlowsInformation({ + 'route-1234': true, + 'route-4321': false, + }); + + expect(result).toMatchObject({ + singleFlowId: 'route-1234', + }); + }); + + it('should return undefined when there are more than one visible flow', () => { + const result = VisualizationService.getVisibleFlowsInformation({ + 'route-1234': true, + 'route-4321': true, + }); + + expect(result).toMatchObject({ + singleFlowId: undefined, + }); + }); + + it('should return undefined when there is any visible flow', () => { + const result = VisualizationService.getVisibleFlowsInformation({ + 'route-1234': false, + 'route-4321': false, + }); + + expect(result).toMatchObject({ + singleFlowId: undefined, + }); + }); + + it('should return the visible flows count - all flows visible', () => { + const result = VisualizationService.getVisibleFlowsInformation({ + 'route-1234': true, + 'route-4321': true, + }); + + expect(result).toMatchObject({ + currentVisible: 2, + flowsCount: 2, + }); + }); + + it('should return the visible flows count - some flows visible', () => { + const result = VisualizationService.getVisibleFlowsInformation({ + 'route-1234': true, + 'route-4321': false, + }); + + expect(result).toMatchObject({ + currentVisible: 1, + flowsCount: 2, + }); + }); + + it('should return the visible flows count - no flow visible', () => { + const result = VisualizationService.getVisibleFlowsInformation({ + 'route-1234': false, + 'route-4321': false, + }); + + expect(result).toMatchObject({ + currentVisible: 0, + flowsCount: 2, + }); + }); + }); }); diff --git a/src/services/visualizationService.ts b/src/services/visualizationService.ts index 8f1a2bcc2..9e4a7c002 100644 --- a/src/services/visualizationService.ts +++ b/src/services/visualizationService.ts @@ -1,9 +1,6 @@ import { StepsService } from './stepsService'; import { ValidationService } from './validationService'; -import { - useFlowsStore, - useVisualizationStore, -} from '@kaoto/store'; +import { useFlowsStore, useVisualizationStore } from '@kaoto/store'; import { HandleDeleteStepFn, IStepProps, @@ -40,7 +37,7 @@ export class VisualizationService { step: IStepProps, nodeId: string, layout: string, - dataProps?: { [prop: string]: any } + dataProps?: { [prop: string]: any }, ): IVizStepNode { return { data: { @@ -71,7 +68,7 @@ export class VisualizationService { node: IVizStepNode, rootNode: IVizStepNode, rootNextNode: IVizStepNode, - edgeType?: string + edgeType?: string, ): IVizStepPropsEdge[] { const branchPlaceholderEdges: IVizStepPropsEdge[] = []; let edgeProps = VisualizationService.buildEdgeParams(rootNode, node, edgeType ?? 'default'); @@ -83,7 +80,7 @@ export class VisualizationService { if (rootNextNode) { branchPlaceholderEdges.push( - VisualizationService.buildEdgeParams(node, rootNextNode, 'default') + VisualizationService.buildEdgeParams(node, rootNextNode, 'default'), ); } @@ -102,7 +99,7 @@ export class VisualizationService { const parentNodeIndex = VisualizationService.findNodeIdxWithUUID( node.data.branchInfo?.parentStepUuid, - stepNodes + stepNodes, ); if (node.data.branchInfo) { @@ -115,11 +112,11 @@ export class VisualizationService { ) { const branchStepNextIdx = VisualizationService.findNodeIdxWithUUID( node.data.nextStepUuid, - stepNodes + stepNodes, ); if (stepNodes[branchStepNextIdx]) { specialEdges.push( - VisualizationService.buildEdgeParams(node, stepNodes[branchStepNextIdx], 'insert') + VisualizationService.buildEdgeParams(node, stepNodes[branchStepNextIdx], 'insert'), ); } } @@ -129,12 +126,12 @@ export class VisualizationService { const parentStepNode: IVizStepNode = stepNodes[parentNodeIndex]; const showDeleteEdge = ValidationService.reachedMinBranches( parentStepNode.data.step.branches.length, - parentStepNode.data.step.minBranches + parentStepNode.data.step.minBranches, ); let edgeProps = VisualizationService.buildEdgeParams( parentStepNode, node, - showDeleteEdge ? 'delete' : 'default' + showDeleteEdge ? 'delete' : 'default', ); if (node.data.branchInfo?.branchIdentifier) @@ -148,11 +145,11 @@ export class VisualizationService { const parentStepNode: IVizStepNode = stepNodes[parentNodeIndex]; const parentStepNextIdx = VisualizationService.findNodeIdxWithUUID( node.data.branchInfo?.parentStepNextUuid, - stepNodes + stepNodes, ); const showDeleteEdge = ValidationService.reachedMinBranches( parentStepNode.data.step.branches.length, - parentStepNode.data.step.minBranches + parentStepNode.data.step.minBranches, ); specialEdges.push( @@ -160,8 +157,8 @@ export class VisualizationService { node, stepNodes[parentNodeIndex], stepNodes[parentStepNextIdx], - showDeleteEdge ? 'delete' : 'default' - ) + showDeleteEdge ? 'delete' : 'default', + ), ); } @@ -169,13 +166,13 @@ export class VisualizationService { if (node.data.isLastStep && !StepsService.isEndStep(node.data.step)) { const parentStepNextIdx = VisualizationService.findNodeIdxWithUUID( node.data.branchInfo?.parentStepNextUuid, - stepNodes + stepNodes, ); if (stepNodes[parentStepNextIdx]) { // it needs to merge back specialEdges.push( - VisualizationService.buildEdgeParams(node, stepNodes[parentStepNextIdx], 'default') + VisualizationService.buildEdgeParams(node, stepNodes[parentStepNextIdx], 'default'), ); } } @@ -193,7 +190,7 @@ export class VisualizationService { static buildEdgeParams( sourceStep: IVizStepNode, targetStep: IVizStepNode, - type?: string + type?: string, ): IVizStepPropsEdge { return { arrowHeadType: 'arrowclosed', @@ -235,8 +232,8 @@ export class VisualizationService { VisualizationService.buildEdgeParams( node, nodes[nextNodeIdx], - node.data.branchInfo || node.data.step.branches?.length > 0 ? 'default' : 'insert' - ) + node.data.branchInfo || node.data.step.branches?.length > 0 ? 'default' : 'insert', + ), ); } }); @@ -248,7 +245,7 @@ export class VisualizationService { step: IStepProps, newId: string, props?: { [prop: string]: any }, - branchInfo?: IVizStepNodeDataBranch + branchInfo?: IVizStepNodeDataBranch, ): IVizStepNode { return { data: { @@ -274,21 +271,22 @@ export class VisualizationService { * Builds React Flow nodes and edges from current integration JSON. * @param handleDeleteStep */ - buildNodesAndEdges(handleDeleteStep: HandleDeleteStepFn) { - const layout = useVisualizationStore.getState().layout; + private buildNodesAndEdges(handleDeleteStep: HandleDeleteStepFn) { + const { layout, visibleFlows } = useVisualizationStore.getState(); // build all nodes let stepNodes: IVizStepNode[] = []; - const integrations = useFlowsStore.getState().flows; - stepNodes = integrations.reduce((acc, currentIntegration) => acc.concat( - VisualizationService.buildNodesFromSteps( - currentIntegration.id, - currentIntegration.steps, - layout, - { handleDeleteStep }, - ), - ), [] as IVizStepNode[]); + const flows = useFlowsStore.getState().flows; + stepNodes = flows.reduce((acc, flow) => { + if (!visibleFlows[flow.id]) { + return acc; + } + + return acc.concat( + VisualizationService.buildNodesFromSteps(flow.id, flow.steps, layout, { handleDeleteStep }), + ); + }, [] as IVizStepNode[]); // build edges only for main nodes const filteredNodes = stepNodes.filter((node) => !node.data.branchInfo); @@ -312,7 +310,7 @@ export class VisualizationService { steps: IStepProps[], layout: string, props?: { [prop: string]: any }, - branchInfo?: IVizStepNodeDataBranch + branchInfo?: IVizStepNodeDataBranch, ): IVizStepNode[] { let stepNodes: IVizStepNode[] = []; let id = 0; @@ -378,7 +376,7 @@ export class VisualizationService { // parentStepUuid is always the parent of the branch step, no matter how nested parentStepNextUuid: steps[index + 1]?.UUID ?? branchInfo?.rootStepNextUuid, parentStepUuid: steps[index].UUID, - }) + }), ); }); } @@ -414,7 +412,7 @@ export class VisualizationService { static async getLayoutedElements( nodes: IVizStepNode[], edges: IVizStepPropsEdge[], - direction: string + direction: string, ) { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); @@ -440,7 +438,7 @@ export class VisualizationService { const sourceNode = nodes.find((node) => node.id === edge.source); const dagreWeightedValues = VisualizationService.getDagreWeightedValues( isHorizontal, - sourceNode + sourceNode, ); dagreGraph.setEdge(edge.source, edge.target, dagreWeightedValues); @@ -469,7 +467,7 @@ export class VisualizationService { static getDagreWeightedValues( isHorizontal: boolean, - sourceNode?: IVizStepNode + sourceNode?: IVizStepNode, ): { minlen: number; weight: number } { return { minlen: isHorizontal ? (sourceNode?.data.step.branches?.length > 1 ? 2 : 1) : 1.5, @@ -496,7 +494,7 @@ export class VisualizationService { stepNodes: IVizStepNode[], id: string, type: string, - props: { [prop: string]: any } + props: { [prop: string]: any }, ) { return stepNodes.unshift({ data: { @@ -524,7 +522,7 @@ export class VisualizationService { position: { x: number; y: number }, groupHeight: number, groupWidth: number, - props?: { [prop: string]: any } + props?: { [prop: string]: any }, ) { return stepNodes.unshift({ id: getRandomArbitraryNumber().toString(), @@ -560,22 +558,13 @@ export class VisualizationService { * @param handleDeleteStep * @param rebuildNodes */ - async redrawDiagram(handleDeleteStep: HandleDeleteStepFn, rebuildNodes: boolean) { - let stepNodes = useVisualizationStore.getState().nodes; - let stepEdges = useVisualizationStore.getState().edges; - if (rebuildNodes) { - const ne = this.buildNodesAndEdges(handleDeleteStep); - stepNodes = ne.stepNodes; - stepEdges = ne.stepEdges; - } - VisualizationService.getLayoutedElements( - stepNodes, - stepEdges, - useVisualizationStore.getState().layout - ).then((res) => { - const { layoutedNodes, layoutedEdges } = res; - useVisualizationStore.getState().setNodes(layoutedNodes); - useVisualizationStore.getState().setEdges(layoutedEdges); + async redrawDiagram(handleDeleteStep: HandleDeleteStepFn): Promise { + const ne = this.buildNodesAndEdges(handleDeleteStep); + const layout = useVisualizationStore.getState().layout; + + VisualizationService.getLayoutedElements(ne.stepNodes, ne.stepEdges, layout).then((res) => { + useVisualizationStore.getState().setNodes(res.layoutedNodes); + useVisualizationStore.getState().setEdges(res.layoutedEdges); }); } @@ -605,7 +594,7 @@ export class VisualizationService { return !( ValidationService.reachedMaxBranches( nodeData.step.branches.length, - nodeData.step.maxBranches + nodeData.step.maxBranches, ) && nodeData.nextStepUuid ); @@ -624,7 +613,7 @@ export class VisualizationService { // check if the previous step contains (nested) branches. const prevNodeIdx = VisualizationService.findNodeIdxWithUUID( nodeData.previousStepUuid, - useVisualizationStore.getState().nodes + useVisualizationStore.getState().nodes, ); return !!( useVisualizationStore.getState().nodes[prevNodeIdx] && @@ -653,4 +642,43 @@ export class VisualizationService { // if it doesn't contain branches, don't show the steps tab return !StepsService.containsBranches(nodeData.step); } + + static getEmptySelectedStep(): IStepProps { + return { + maxBranches: 0, + minBranches: 0, + name: '', + type: '', + UUID: '', + integrationId: '', + }; + } + + static getVisibleFlowsInformation(visibleFlows: Record): { + singleFlowId: string | undefined; + currentVisible: number; + flowsCount: number; + } { + const flowsArray = Object.entries(visibleFlows); + const visibleFlowsIdArray = flowsArray.filter((flow) => flow[1]).map((flow) => flow[0]); + + /** If there's only one flow visible, we return its ID */ + if (visibleFlowsIdArray.length === 1) { + return { + singleFlowId: visibleFlowsIdArray[0], + currentVisible: 1, + flowsCount: flowsArray.length, + }; + } + + /** + * Otherwise, we return undefined to signal the UI that there + * could be more than one or no flow visible + */ + return { + singleFlowId: undefined, + currentVisible: visibleFlowsIdArray.length, + flowsCount: flowsArray.length, + }; + } } diff --git a/src/store/FlowsStore.test.ts b/src/store/FlowsStore.test.ts index 30aa51735..7650d283a 100644 --- a/src/store/FlowsStore.test.ts +++ b/src/store/FlowsStore.test.ts @@ -1,7 +1,9 @@ import { initialFlows, kameletSourceStepStub } from '../stubs'; import { useFlowsStore } from './FlowsStore'; +import { useVisualizationStore } from './visualizationStore'; import { StepsService } from '@kaoto/services'; import { IFlowsWrapper, IViewProps } from '@kaoto/types'; +import { act, renderHook } from '@testing-library/react'; describe('FlowsStoreFacade', () => { it('should start with a default value', () => { @@ -10,7 +12,7 @@ describe('FlowsStoreFacade', () => { expect(initialState).toMatchObject({ flows: [ { - id: /Camel Route-[0-9]*/, + id: /Camel Route-\d*/, dsl: 'Camel Route', description: '', metadata: {}, @@ -31,7 +33,7 @@ describe('FlowsStoreFacade', () => { { description: '', dsl: 'Camel Route', - id: /Camel Route-[0-9]*/, + id: /Camel Route-\d*/, metadata: { name: 'integration', namespace: '', @@ -42,7 +44,7 @@ describe('FlowsStoreFacade', () => { { description: '', dsl: 'Integration', - id: /Integration-[0-9]*/, + id: /Integration-\d*/, metadata: {}, params: [], steps: [], @@ -57,7 +59,7 @@ describe('FlowsStoreFacade', () => { { description: '', dsl: 'Camel Route', - id: /Camel Route-[0-9]*/, + id: /Camel Route-\d*/, metadata: { name: 'integration', namespace: '', @@ -76,6 +78,24 @@ describe('FlowsStoreFacade', () => { ]); }); + it('should hide previous flows and only show the newly created one', () => { + const { result: visualizationStoreRef } = renderHook(() => useVisualizationStore()); + const { result: flowsStoreRef } = renderHook(() => useFlowsStore()); + + act(() => { + flowsStoreRef.current.deleteAllFlows(); + flowsStoreRef.current.addNewFlow('Integration', 'route-1234'); + flowsStoreRef.current.addNewFlow('Integration', 'route-4321'); + }); + + expect(flowsStoreRef.current.flows).toHaveLength(2); + + expect(visualizationStoreRef.current.visibleFlows).toEqual({ + 'route-1234': false, + 'route-4321': true, + }); + }); + it('should allow consumers to update views', () => { useFlowsStore .getState() @@ -99,8 +119,8 @@ describe('FlowsStoreFacade', () => { expect(extractNestedStepsSpy).toHaveBeenCalledWith([ { ...kameletSourceStepStub, - UUID: 'Camel Route-0_timer-source-0', - integrationId: 'Camel Route-0', + UUID: 'Camel Route-1_timer-source-0', + integrationId: 'Camel Route-1', }, ]); }); @@ -115,7 +135,7 @@ describe('FlowsStoreFacade', () => { expect(useFlowsStore.getState().flows).toMatchObject([ { dsl: 'Camel Route', - id: 'Camel Route-0', + id: 'Camel Route-1', metadata: { name: 'integration', namespace: '', @@ -123,13 +143,13 @@ describe('FlowsStoreFacade', () => { params: [], steps: [ { - UUID: 'Camel Route-0_timer-source-0', + UUID: 'Camel Route-1_timer-source-0', branches: [], description: 'Produces periodic messages with a custom payload.', group: 'Timer', icon: 'data:image/svg+xml;base64,', id: 'timer-source', - integrationId: 'Camel Route-0', + integrationId: 'Camel Route-1', kind: 'Kamelet', maxBranches: 1, minBranches: 0, @@ -144,9 +164,26 @@ describe('FlowsStoreFacade', () => { ]); }); + it('should allow consumers to delete a flow', () => { + useFlowsStore.getState().deleteAllFlows(); + useFlowsStore.getState().addNewFlow('Integration', 'route-1234'); + + useFlowsStore.getState().deleteFlow('route-1234'); + + expect(useFlowsStore.getState().flows).toHaveLength(0); + }); + it('should allow consumers to delete all flows', () => { useFlowsStore.getState().deleteAllFlows(); expect(useFlowsStore.getState().flows).toHaveLength(0); }); + + it('should clean useVisualizationStore.visibleFlows after deleting all flows', () => { + useFlowsStore.getState().addNewFlow('Integration'); + useFlowsStore.getState().deleteAllFlows(); + + expect(useFlowsStore.getState().flows).toHaveLength(0); + expect(useVisualizationStore.getState().visibleFlows).toEqual({}); + }); }); diff --git a/src/store/FlowsStore.ts b/src/store/FlowsStore.ts index 11766037a..daf6c5b11 100644 --- a/src/store/FlowsStore.ts +++ b/src/store/FlowsStore.ts @@ -1,5 +1,6 @@ import { useNestedStepsStore } from './nestedStepsStore'; import { initDsl, initialSettings } from './settingsStore'; +import { useVisualizationStore } from './visualizationStore'; import { FlowsService, StepsService } from '@kaoto/services'; import { IFlowsWrapper, IIntegration, IStepProps, IViewProps } from '@kaoto/types'; import { setDeepValue } from '@kaoto/utils'; @@ -38,17 +39,19 @@ export interface IFlowsStore extends IFlowsStoreData { /** General flow management */ addNewFlow: (dsl: string, flowId?: string) => void; + deleteFlow: (flowId: string) => void; deleteAllFlows: () => void; } export const getInitialState = (previousState: Partial = {}): IFlowsStoreData => { + const flow = FlowsService.getNewFlow(initDsl.name, undefined, { + metadata: { name: initialSettings.name, namespace: initialSettings.namespace }, + }); + useVisualizationStore.getState().toggleFlowVisible(flow.id, true); + return { ...previousState, - flows: previousState.flows ?? [ - FlowsService.getNewFlow(initDsl.name, undefined, { - metadata: { name: initialSettings.name, namespace: initialSettings.namespace }, - }), - ], + flows: previousState.flows ?? [flow], properties: {}, views: [], metadata: {}, @@ -133,7 +136,7 @@ export const useFlowsStore = create()( * This is needed until https://github.com/KaotoIO/kaoto-backend/issues/663 it's done */ const flowsWithId = flowsWrapper.flows.map((flow, index) => { - const id = `${flow.dsl}-${index}`; + const id = flow.id ?? `${flow.dsl}-${index}`; const steps = StepsService.regenerateUuids(id, flow.steps); allSteps.push(...steps); @@ -142,6 +145,17 @@ export const useFlowsStore = create()( useNestedStepsStore.getState().updateSteps(StepsService.extractNestedSteps(allSteps)); + /** TODO: Move this to the VisualizationService */ + const visibleFlows = flowsWithId.reduce( + (acc, flow, index) => ({ + ...acc, + /** Make visible only the first flow */ + [flow.id]: index === 0, + }), + {} as Record, + ); + useVisualizationStore.getState().setVisibleFlows(visibleFlows); + return { ...state, flows: flowsWithId, @@ -154,11 +168,26 @@ export const useFlowsStore = create()( /** General flow management */ addNewFlow: (dsl, flowId) => set((state) => { - const flows = state.flows.concat(FlowsService.getNewFlow(dsl, flowId)); + const flow = FlowsService.getNewFlow(dsl, flowId); + const flows = state.flows.concat(flow); + useVisualizationStore.getState().hideAllFlows(); + useVisualizationStore.getState().toggleFlowVisible(flow.id, true); return { ...state, currentDsl: dsl, flows }; }), - deleteAllFlows: () => set((state) => getInitialState({ ...state, flows: [] })), + deleteFlow: (flowId) => + set((state) => { + const filteredFlows = state.flows.filter((flow) => flowId !== flow.id); + + return { + ...state, + flows: filteredFlows, + }; + }), + deleteAllFlows: () => { + set((state) => getInitialState({ ...state, flows: [] })); + useVisualizationStore.getState().setVisibleFlows({}); + }, }), { partialize: (state) => { diff --git a/src/store/visualizationStore.test.tsx b/src/store/visualizationStore.test.tsx index d91ce6bdb..def0dacd4 100644 --- a/src/store/visualizationStore.test.tsx +++ b/src/store/visualizationStore.test.tsx @@ -1,5 +1,5 @@ import { useVisualizationStore } from './visualizationStore'; -import { IStepProps } from '@kaoto/types'; +import { IStepProps, IVizStepPropsEdge } from '@kaoto/types'; import { MarkerType } from '@reactflow/core'; import { act, renderHook } from '@testing-library/react'; @@ -64,6 +64,19 @@ describe('visualizationStore', () => { expect(result.current.edges).toHaveLength(1); }); + it('setEdges', () => { + const { result } = renderHook(() => useVisualizationStore()); + expect(result.current.edges).toEqual([]); + + act(() => { + result.current.setEdges([ + { id: 'id-1234', source: 'step-1', target: 'step-3' }, + ] as IVizStepPropsEdge[]); + }); + + expect(result.current.edges).toEqual([{ id: 'id-1234', source: 'step-1', target: 'step-3' }]); + }); + it('setHoverStepUuid', () => { const { result } = renderHook(() => useVisualizationStore()); expect(result.current.hoverStepUuid).toBe(''); @@ -98,6 +111,17 @@ describe('visualizationStore', () => { expect(result.current.selectedStepUuid).toBe('jackfruit'); }); + it('setLayout', () => { + const { result } = renderHook(() => useVisualizationStore()); + expect(result.current.layout).toBe('LR'); + + act(() => { + result.current.setLayout('TB'); + }); + + expect(result.current.layout).toBe('TB'); + }); + it('updateNode', () => { const { result } = renderHook(() => useVisualizationStore()); act(() => { @@ -111,11 +135,65 @@ describe('visualizationStore', () => { position: { x: 0, y: 0 }, data: { label: '', step: {} as IStepProps }, }, - 0 + 0, ); }); expect(result.current.nodes).toHaveLength(1); expect(result.current.nodes[0].id).toEqual('starfruit'); }); + + describe('Visibility handlers', () => { + beforeEach(() => { + useVisualizationStore.getState().toggleFlowVisible('route-1234', false); + useVisualizationStore.getState().toggleFlowVisible('route-4321', false); + }); + + it('should allow consumers to set the visibility of a given flow', () => { + useVisualizationStore.getState().toggleFlowVisible('route-1234', true); + const visibleFlows = useVisualizationStore.getState().visibleFlows; + + expect(visibleFlows).toEqual({ + 'route-1234': true, + 'route-4321': false, + }); + }); + + it('should allow consumers to toggle the visibility of a given flow', () => { + useVisualizationStore.getState().toggleFlowVisible('route-1234'); + const visibleFlows = useVisualizationStore.getState().visibleFlows; + + expect(visibleFlows).toEqual({ + 'route-1234': true, + 'route-4321': false, + }); + }); + + it('should allow consumers show all flows', () => { + useVisualizationStore.getState().showAllFlows(); + const visibleFlows = useVisualizationStore.getState().visibleFlows; + + expect(visibleFlows).toEqual({ + 'route-1234': true, + 'route-4321': true, + }); + }); + + it('should allow consumers to hide all flows', () => { + useVisualizationStore.getState().hideAllFlows(); + const visibleFlows = useVisualizationStore.getState().visibleFlows; + + expect(visibleFlows).toEqual({ + 'route-1234': false, + 'route-4321': false, + }); + }); + + it('should allow consumers to clear all flows', () => { + useVisualizationStore.getState().setVisibleFlows({}); + const visibleFlows = useVisualizationStore.getState().visibleFlows; + + expect(visibleFlows).toEqual({}); + }); + }); }); diff --git a/src/store/visualizationStore.tsx b/src/store/visualizationStore.tsx index 341b34351..a9229d6ce 100644 --- a/src/store/visualizationStore.tsx +++ b/src/store/visualizationStore.tsx @@ -32,6 +32,13 @@ export type RFState = { * @param nodeToUpdate */ updateNode: (nodeToUpdate: IVizStepNode, nodeIndex: number) => void; + + /** Visibility related handlers */ + visibleFlows: Record; + toggleFlowVisible: (flowId: string, isVisible?: boolean) => void; + showAllFlows: () => void; + hideAllFlows: () => void; + setVisibleFlows: (flows: Record) => void; }; // this is our useStore hook that we can use in our components to get parts of the store and call actions @@ -74,6 +81,51 @@ export const useVisualizationStore = create((set, get) => ({ newNodes[nodeIndex] = newNode; set(() => ({ nodes: newNodes })); }, + + /** Visibility related handlers */ + visibleFlows: {}, + toggleFlowVisible: (flowId, isVisible) => { + set(({ visibleFlows }) => { + const currentVisibility = !!visibleFlows[flowId]; + const isFlowVisible = isVisible === undefined ? !currentVisibility : isVisible; + + return { + visibleFlows: { ...visibleFlows, [flowId]: isFlowVisible }, + }; + }); + }, + showAllFlows: () => { + set(({ visibleFlows }) => ({ + visibleFlows: toggleFlowsVisibility(visibleFlows, true), + })); + }, + hideAllFlows: () => { + set(({ visibleFlows }) => ({ + visibleFlows: toggleFlowsVisibility(visibleFlows, false), + })); + }, + setVisibleFlows: (visibleFlows) => { + set(() => ({ + visibleFlows, + })); + }, })); -export default useVisualizationStore; +/** + * TODO: The following function should be moved + * to the VisualizationService, the problem at the + * moment is that VisualizationService should be split + * into a service that doesn't import the VisualizationStore + * and a VisualizationFacade which does. + * + * This will prevent the circular dependency created by + * importing the Store into the service and the other way around + */ +const toggleFlowsVisibility = ( + visibleFlows: Record, + isVisible: boolean, +): Record => + Object.keys(visibleFlows).reduce((acc, flowId) => { + acc[flowId] = isVisible; + return acc; + }, {} as Record);