Skip to content

Commit

Permalink
[Temp]: Rework how to add new Entities to the canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
lordrip committed Apr 29, 2024
1 parent 2260f06 commit a493808
Show file tree
Hide file tree
Showing 19 changed files with 463 additions and 87 deletions.
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
12 changes: 6 additions & 6 deletions packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ 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 result = render(
<TestProvidersWrapper
visibleFlows={{ visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult}
visibleFlowsContext={{ visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult}
>
<Canvas entities={[entity]} />
</TestProvidersWrapper>,
Expand All @@ -31,7 +31,7 @@ describe('Canvas', () => {
it('should render correctly with more routes ', async () => {
const result = render(
<TestProvidersWrapper
visibleFlows={
visibleFlowsContext={
{ visibleFlows: { ['route-8888']: true, ['route-9999']: false } } as unknown as VisibleFLowsContextResult
}
>
Expand Down Expand Up @@ -141,7 +141,7 @@ describe('Canvas', () => {
const result = render(
<CatalogModalContext.Provider value={{ getNewComponent: jest.fn(), setIsModalOpen: jest.fn() }}>
<TestProvidersWrapper
visibleFlows={{ visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult}
visibleFlowsContext={{ visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult}
>
<Canvas entities={[entity]} />
</TestProvidersWrapper>
Expand All @@ -157,7 +157,7 @@ describe('Canvas', () => {
describe('Empty state', () => {
it('should render empty state when there is no visual entity', async () => {
const result = render(
<TestProvidersWrapper visibleFlows={{ visibleFlows: {} } as unknown as VisibleFLowsContextResult}>
<TestProvidersWrapper visibleFlowsContext={{ visibleFlows: {} } as unknown as VisibleFLowsContextResult}>
<Canvas entities={[]} />
</TestProvidersWrapper>,
);
Expand All @@ -169,7 +169,7 @@ describe('Canvas', () => {
it('should render empty state when there is no visible flows', async () => {
const result = render(
<TestProvidersWrapper
visibleFlows={{ visibleFlows: { ['route-8888']: false } } as unknown as VisibleFLowsContextResult}
visibleFlowsContext={{ visibleFlows: { ['route-8888']: false } } as unknown as VisibleFLowsContextResult}
>
<Canvas entities={[entity]} />
</TestProvidersWrapper>,
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>,
];
]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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, VisibleFlowsProvider } 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('NewEntity', () => {
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 (
<EntitiesContext.Provider
key={Date.now()}
value={
{
camelResource,
entities: camelResource.getEntities(),
visualEntities: camelResource.getVisualEntities(),
currentSchemaType,
setCurrentSchemaType: setCurrentSchemaTypeSpy,
updateEntitiesFromCamelResource: updateEntitiesFromCamelResourceSpy,
updateSourceCodeFromEntities: updateSourceCodeFromEntitiesSpy,
} as unknown as EntitiesContextResult
}
>
<VisibleFlowsProvider>
<NewEntity />
</VisibleFlowsProvider>
</EntitiesContext.Provider>
);
};

it('component renders', () => {
const wrapper = render(<FlowTypeSelectorWithContext />);
const toggle = wrapper.queryByTestId('new-entity-list-dropdown');
expect(toggle).toBeInTheDocument();
});

it('should call `updateEntitiesFromCamelResource` when selecting an item', async () => {
const wrapper = render(<FlowTypeSelectorWithContext />);

/** Click on toggle */
const toggle = await wrapper.findByTestId('new-entity-list-dropdown');
act(() => {
fireEvent.click(toggle);
});

/** Click on first element */
const element = await wrapper.findAllByRole('menuitem');
act(() => {
fireEvent.click(element[0]);
});

await waitFor(() => {
expect(updateEntitiesFromCamelResourceSpy).toHaveBeenCalled();
});
});

it('should toggle list of DSLs', async () => {
const wrapper = render(<FlowTypeSelectorWithContext />);
const toggle = await wrapper.findByTestId('new-entity-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 close Select when pressing ESC', async () => {
const wrapper = render(<FlowTypeSelectorWithContext />);
const toggle = await wrapper.findByTestId('new-entity-list-dropdown');

/** Open Select */
act(() => {
fireEvent.click(toggle);
});

const menu = await wrapper.findByRole('menu');

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');
});
});
});
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const toggleRef = useRef<HTMLButtonElement>(null);
const groupedEntities = useRef<BaseVisualCamelEntityDefinition>(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 (
<MenuItem
key={`new-entity-${entity.name}`}
data-testid={`new-entity-${entity.name}`}
itemId={entity.name}
description={
<span className="pf-v5-u-text-break-word" style={{ wordBreak: 'keep-all' }}>
{entity.description}
</span>
}
flyoutMenu={flyoutMenu}
>
{entity.title}
</MenuItem>
);
},
[],
);

return (
<MenuContainer
isOpen={isOpen}
onOpenChange={(isOpen) => setIsOpen(isOpen)}
menu={
// TODO: Workaround for flyout menu being scrollable and packed within the toolbar
<Menu ref={menuRef} style={{ overflowY: 'unset' }} containsFlyout onSelect={onSelect}>
<MenuContent>
<MenuList>
{groupedEntities.current.common.map((entityDef) => getMenuItem(entityDef))}

{Object.entries(groupedEntities.current.groups).map(([group, entities]) => {
const flyoutMenu = (
<Menu onSelect={onSelect}>
<MenuContent>
<MenuList>{entities.map((entityDef) => getMenuItem(entityDef))}</MenuList>
</MenuContent>
</Menu>
);

return getMenuItem({ name: undefined, title: group, description: '' }, flyoutMenu);
})}
</MenuList>
</MenuContent>
</Menu>
}
menuRef={menuRef}
toggle={
<MenuToggle
data-testid="new-entity-list-dropdown"
ref={toggleRef}
onClick={() => {
setIsOpen(!isOpen);
}}
isExpanded={isOpen}
>
<PlusIcon />
<span className="pf-v5-u-m-sm">New</span>
</MenuToggle>
}
toggleRef={toggleRef}
/>
);
};

0 comments on commit a493808

Please sign in to comment.