Skip to content

Commit

Permalink
feat(Canvas): Add new Entities to the canvas
Browse files Browse the repository at this point in the history
Currently, setting the current DSL and creating a new entity are tied
together in a single step in the NewFlow component.

This commit split that functionality in two separate buttons:
1. DSL Selector: Meant to choose the current DSL and therefore determine
   which type of entities can be added into the Canvas
2. NewEntity: This is for adding new entities, supported by the current
   DSL, or said in a different way, allows to add entities from the
current CamelResource registry.

The existing functionality from the CanvasEmptyState is preserved for
now.

In addition to that, when removing all routes, a new route won't be
created by default.

fix: #1030
  • Loading branch information
lordrip committed May 2, 2024
1 parent 5fecb04 commit 7f4d23c
Show file tree
Hide file tree
Showing 28 changed files with 789 additions and 340 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ describe('Test for Multi route actions from the canvas', () => {

it('User changes route type in the canvas', () => {
cy.switchIntegrationType('Kamelet');
cy.get('[data-testid="toolbar-current-dsl"]').contains('Kamelet');
cy.get('[data-testid="dsl-list-dropdown"]').contains('Kamelet');
cy.switchIntegrationType('camelYamlDsl');
cy.get('[data-testid="toolbar-current-dsl"]').contains('Camel Route');
cy.get('[data-testid="dsl-list-dropdown"]').contains('Camel Route');
cy.switchIntegrationType('Pipe');
cy.get('[data-testid="toolbar-current-dsl"]').contains('Pipe');
cy.get('[data-testid="dsl-list-dropdown"]').contains('Pipe');
});

it('User shows and hides a route', () => {
Expand Down Expand Up @@ -55,8 +55,7 @@ describe('Test for Multi route actions from the canvas', () => {
cy.checkCodeSpanLine('id: route-1234', 0);
});

// Blocked ATM by https://github.com/KaotoIO/kaoto/issues/301
it.skip('User deletes routes in the canvas till there are no routes', () => {
it('User deletes routes in the canvas till there are no routes', () => {
cy.openDesignPage();
cy.addNewRoute();
cy.addNewRoute();
Expand All @@ -67,13 +66,17 @@ describe('Test for Multi route actions from the canvas', () => {
cy.deleteRoute(0);
cy.deleteRoute(0);
cy.deleteRoute(0);

cy.toggleFlowsList();

cy.get('[data-testid^="rf__node-node_0"]').should('have.length', 0);
cy.get('[data-testid="flows-list-empty-state"]').should('have.length', 1);
cy.get('[data-testid="flows-list-route-count"]').should('have.text', '0/0');

cy.get('[data-testid="flows-list-empty-state"]').within(() => {
cy.get('h4.pf-c-title').should('have.text', "There's no routes to show");
cy.get('[data-testid="flows-list-route-count"]').should('have.text', '0/0');
cy.get('#flows-list-select').within(() => {
cy.get('h4.pf-v5-c-empty-state__title-text').should('have.text', "There's no routes to show");
});

cy.get('[data-testid="visualization-empty-state"]').should('be.visible');
});

const testData = ['Pipe', 'Kamelet'];
Expand All @@ -83,7 +86,7 @@ describe('Test for Multi route actions from the canvas', () => {
cy.switchIntegrationType(data);
cy.get('[data-testid="dsl-list-dropdown"]').click({ force: true });
cy.get('.pf-v5-c-menu__item-text').contains(data).closest('button').should('be.disabled');
cy.get('[data-testid="dsl-list-btn"]').should('be.disabled');
cy.get('[data-testid="new-entity-list-dropdown"]').should('not.exist');

cy.get('[data-testid="flows-list-route-count"]').should('have.text', '1/1');
});
Expand All @@ -94,6 +97,7 @@ describe('Test for Multi route actions from the canvas', () => {
cy.deleteRoute(0);
cy.addNewRoute();
cy.addNewRoute();
cy.addNewRoute();

cy.showAllRoutes();
cy.get('[data-id^="log"]').should('have.length', 3);
Expand Down
3 changes: 2 additions & 1 deletion packages/ui-tests/cypress/support/next-commands/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ Cypress.Commands.add('switchIntegrationType', (type: string) => {
});

Cypress.Commands.add('addNewRoute', () => {
cy.get('[data-testid="dsl-list-btn"]').click();
cy.get('[data-testid="new-entity-list-dropdown"]').click();
cy.get('[data-testid="new-entity-route"]').click();
});

Cypress.Commands.add('toggleRouteVisibility', (index) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/ui-tests/stories/canvas/CanvasSideBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const selectedNode: CanvasNode = {
},
} as VisualComponentSchema;
},
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route),
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson),
} as IVisualizationNode,
},
};
Expand All @@ -79,7 +79,7 @@ const unknownSelectedNode: CanvasNode = {
definition: null,
} as VisualComponentSchema;
},
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route),
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson),
} as IVisualizationNode,
},
};
Expand Down
127 changes: 56 additions & 71 deletions packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,42 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { CamelRouteResource, KameletResource } from '../../../models/camel';
import { CamelRouteVisualEntity } from '../../../models/visualization/flows';
import { EntitiesContext } from '../../../providers';
import { CatalogModalContext } from '../../../providers/catalog-modal.provider';
import { VisibleFLowsContextResult, VisibleFlowsContext } from '../../../providers/visible-flows.provider';
import { VisibleFLowsContextResult } from '../../../providers/visible-flows.provider';
import { TestProvidersWrapper } from '../../../stubs';
import { camelRouteJson } from '../../../stubs/camel-route';
import { kameletJson } from '../../../stubs/kamelet-route';
import { Canvas } from './Canvas';

describe('Canvas', () => {
const entity = new CamelRouteVisualEntity(camelRouteJson.route);
const entity = new CamelRouteVisualEntity(camelRouteJson);
const entity2 = { ...entity, id: 'route-9999' } as CamelRouteVisualEntity;

it('should render correctly', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult,
});
const result = render(
<TestProvidersWrapper
visibleFlows={{ visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult}
>
<Provider>
<Canvas entities={[entity]} />
</TestProvidersWrapper>,
</Provider>,
);

await waitFor(async () => expect(result.container.querySelector('#fit-to-screen')).toBeInTheDocument());
expect(result.container).toMatchSnapshot();
});

it('should render correctly with more routes ', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: {
visibleFlows: { ['route-8888']: true, ['route-9999']: false },
} as unknown as VisibleFLowsContextResult,
});
const result = render(
<TestProvidersWrapper
visibleFlows={
{ visibleFlows: { ['route-8888']: true, ['route-9999']: false } } as unknown as VisibleFLowsContextResult
}
>
<Provider>
<Canvas entities={[entity, entity2]} />
</TestProvidersWrapper>,
</Provider>,
);

await waitFor(async () => expect(result.container.querySelector('#fit-to-screen')).toBeInTheDocument());
Expand All @@ -45,34 +46,23 @@ describe('Canvas', () => {

it('should be able to delete the routes', async () => {
const camelResource = new CamelRouteResource(camelRouteJson);
const routeEntity = camelResource.getVisualEntities();
const routeEntities = camelResource.getVisualEntities();
const removeSpy = jest.spyOn(camelResource, 'removeEntity');
const setCurrentSchemaTypeSpy = jest.fn();
const updateEntitiesFromCamelResourceSpy = jest.fn();
const updateSourceCodeFromEntitiesSpy = jest.fn();

render(
<EntitiesContext.Provider
value={{
camelResource,
entities: camelResource.getEntities(),
visualEntities: camelResource.getVisualEntities(),
currentSchemaType: camelResource.getType(),
setCurrentSchemaType: setCurrentSchemaTypeSpy,
updateEntitiesFromCamelResource: updateEntitiesFromCamelResourceSpy,
updateSourceCodeFromEntities: updateSourceCodeFromEntitiesSpy,
}}
>
<VisibleFlowsContext.Provider
value={{ visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult}
>
<Canvas entities={routeEntity} />
</VisibleFlowsContext.Provider>
</EntitiesContext.Provider>,

const { Provider } = TestProvidersWrapper({
camelResource,
visibleFlowsContext: {
visibleFlows: { ['route-8888']: true },
} as unknown as VisibleFLowsContextResult,
});
const wrapper = render(
<Provider>
<Canvas entities={routeEntities} />
</Provider>,
);

// Right click anywhere on the container label
const route = screen.getByText('route-8888');
const route = wrapper.getByText('route-8888');
await act(async () => {
fireEvent.contextMenu(route);
});
Expand All @@ -92,34 +82,24 @@ describe('Canvas', () => {

it('should be able to delete the kamelets', async () => {
const kameletResource = new KameletResource(kameletJson);
const kameletEntity = kameletResource.getVisualEntities();
const kameletEntities = kameletResource.getVisualEntities();
const removeSpy = jest.spyOn(kameletResource, 'removeEntity');
const setCurrentSchemaTypeSpy = jest.fn();
const updateEntitiesFromCamelResourceSpy = jest.fn();
const updateSourceCodeFromEntitiesSpy = jest.fn();

render(
<EntitiesContext.Provider
value={{
camelResource: kameletResource,
entities: kameletResource.getEntities(),
visualEntities: kameletResource.getVisualEntities(),
currentSchemaType: kameletResource.getType(),
setCurrentSchemaType: setCurrentSchemaTypeSpy,
updateEntitiesFromCamelResource: updateEntitiesFromCamelResourceSpy,
updateSourceCodeFromEntities: updateSourceCodeFromEntitiesSpy,
}}
>
<VisibleFlowsContext.Provider
value={{ visibleFlows: { ['user-source']: true } } as unknown as VisibleFLowsContextResult}
>
<Canvas entities={kameletEntity} />
</VisibleFlowsContext.Provider>
</EntitiesContext.Provider>,

const { Provider } = TestProvidersWrapper({
camelResource: kameletResource,
visibleFlowsContext: {
visibleFlows: { ['user-source']: true },
} as unknown as VisibleFLowsContextResult,
});

const wrapper = render(
<Provider>
<Canvas entities={kameletEntities} />
</Provider>,
);

// Right click anywhere on the container label
const kamelet = screen.getByText('user-source');
const kamelet = wrapper.getByText('user-source');
// const route = document.querySelectorAll('.pf-topology__group');
await act(async () => {
fireEvent.contextMenu(kamelet);
Expand All @@ -138,13 +118,14 @@ describe('Canvas', () => {
});

it('should render the Catalog button if `CatalogModalContext` is provided', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult,
});
const result = render(
<CatalogModalContext.Provider value={{ getNewComponent: jest.fn(), setIsModalOpen: jest.fn() }}>
<TestProvidersWrapper
visibleFlows={{ visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult}
>
<Provider>
<Canvas entities={[entity]} />
</TestProvidersWrapper>
</Provider>
</CatalogModalContext.Provider>,
);

Expand All @@ -156,23 +137,27 @@ describe('Canvas', () => {

describe('Empty state', () => {
it('should render empty state when there is no visual entity', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: {} } as unknown as VisibleFLowsContextResult,
});
const result = render(
<TestProvidersWrapper visibleFlows={{ visibleFlows: {} } as unknown as VisibleFLowsContextResult}>
<Provider>
<Canvas entities={[]} />
</TestProvidersWrapper>,
</Provider>,
);

await waitFor(async () => expect(result.getByTestId('visualization-empty-state')).toBeInTheDocument());
expect(result.container).toMatchSnapshot();
});

it('should render empty state when there is no visible flows', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: { ['route-8888']: false } } as unknown as VisibleFLowsContextResult,
});
const result = render(
<TestProvidersWrapper
visibleFlows={{ visibleFlows: { ['route-8888']: false } } as unknown as VisibleFLowsContextResult}
>
<Provider>
<Canvas entities={[entity]} />
</TestProvidersWrapper>,
</Provider>,
);

await waitFor(async () => expect(result.getByTestId('visualization-empty-state')).toBeInTheDocument());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('CanvasForm', () => {
});

beforeEach(() => {
camelRouteVisualEntity = new CamelRouteVisualEntity(camelRouteJson.route);
camelRouteVisualEntity = new CamelRouteVisualEntity(camelRouteJson);
const { nodes } = CanvasService.getFlowDiagram(camelRouteVisualEntity.toVizNode());
selectedNode = nodes[2]; // choice
});
Expand Down Expand Up @@ -112,7 +112,7 @@ describe('CanvasForm', () => {
data: {
vizNode: {
getComponentSchema: () => undefined,
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route),
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson),
} as unknown as IVisualizationNode,
},
};
Expand Down Expand Up @@ -141,7 +141,7 @@ describe('CanvasForm', () => {
data: {
vizNode: {
getComponentSchema: () => visualComponentSchema,
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route),
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson),
} as unknown as IVisualizationNode,
},
};
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('CanvasForm', () => {
data: {
vizNode: {
getComponentSchema: () => visualComponentSchema,
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson.route),
getBaseEntity: () => new CamelRouteVisualEntity(camelRouteJson),
} as unknown as IVisualizationNode,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 '../EmptyState/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 [
<ToolbarItem className="current-dsl-tag" key="toolbar-current-dsl">
<span data-testid="toolbar-current-dsl">{sourceSchemaConfig.config[currentSchemaType].name || 'None'}</span>
const toolbarItems: JSX.Element[] = [
<ToolbarItem key="toolbar-dsl-selector">
<DSLSelector />
</ToolbarItem>,
<ToolbarItem key="toolbar-flows-list">
<FlowsMenu />
</ToolbarItem>,
<ToolbarItem key="toolbar-new-route">
<NewFlow />
</ToolbarItem>,
];

if (isMultipleRoutes) {
toolbarItems.push(
<ToolbarItem key="toolbar-new-route">
<NewEntity />
</ToolbarItem>,
);
}

return toolbarItems.concat([
<ToolbarItem key="toolbar-clipboard">
<FlowClipboard />
</ToolbarItem>,
<ToolbarItem key={'toolbar-export-image'}>
<FlowExportImage />
</ToolbarItem>,
];
]);
};

0 comments on commit 7f4d23c

Please sign in to comment.