Skip to content

Commit

Permalink
feat(viz): Create new Camel Route
Browse files Browse the repository at this point in the history
relates to: #34
  • Loading branch information
lordrip committed Aug 24, 2023
1 parent ae4db80 commit e25c30d
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 14 deletions.
15 changes: 11 additions & 4 deletions packages/ui/src/camel-entities/camel-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type CamelEntities = BaseCamelEntity;

export abstract class BaseCamelEntity {
abstract id: string;
abstract name: string;

/** Internal API fields */
abstract readonly _id: string;
Expand All @@ -19,17 +20,23 @@ export abstract class BaseCamelEntity {

export class CamelRoute implements BaseCamelEntity {
id = '';
name = '';

readonly _id = uuidv4();
readonly _type = EntityType.Route;

private _steps: Step[] = [];
steps: Step[] = [];

constructor(flowProps: Partial<CamelRoute> = {}) {
Object.assign(this, flowProps);
updateModel(props: Partial<unknown> = {}) {
Object.assign(this, props);
}

_addStep(step: Step): void {
this.steps.push(step);
}

_getSteps(): Step[] {
return this._steps;
return this.steps;
}
}

Expand Down
34 changes: 34 additions & 0 deletions packages/ui/src/camel-utils/camel-random-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getCamelRandomId } from './camel-random-id';

describe('camel-random-id', () => {
it('should return a random number', () => {
expect(getCamelRandomId('route')).toEqual(expect.any(String));
});

it('should return a random number with a given length', () => {
jest
.spyOn(global, 'crypto', 'get')
.mockImplementationOnce(() => ({ getRandomValues: () => [19508888] }) as unknown as Crypto);
expect(getCamelRandomId('route', 6)).toEqual('route-195088');
});

it('should return a random number using Date.now() if crypto module is not available', () => {
jest.spyOn(global, 'crypto', 'get').mockImplementationOnce(() => undefined as unknown as Crypto);
jest.spyOn(global.Date, 'now').mockReturnValueOnce(888);

const result = getCamelRandomId('route');

expect(result).toEqual('route-888');
});

it('should return a random number using msCrypto if crypto module is not available', () => {
Object.defineProperty(global, 'msCrypto', {
value: global.crypto,
writable: true,
});

jest.spyOn(global, 'crypto', 'get').mockImplementationOnce(() => undefined as unknown as Crypto);

expect(getCamelRandomId('route')).toEqual(expect.any(String));
});
});
6 changes: 6 additions & 0 deletions packages/ui/src/camel-utils/camel-random-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const getCamelRandomId = (kind: string, length = 4): string => {
const cryptoObj = window.crypto || (window as Window & { msCrypto?: Crypto }).msCrypto;
const randomNumber = Math.floor(cryptoObj?.getRandomValues(new Uint32Array(1))[0] ?? Date.now());

return `${kind}-${randomNumber.toString(10).slice(0, length)}`;
};
39 changes: 33 additions & 6 deletions packages/ui/src/components/Visualization/Visualization.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,60 @@
import { FunctionComponent, PropsWithChildren, useState } from 'react';
import { Button, Modal } from '@patternfly/react-core';
import { FunctionComponent, PropsWithChildren, useCallback, useState } from 'react';
import { CamelRoute, Step } from '../../camel-entities';
import { getCamelRandomId } from '../../camel-utils/camel-random-id';
import { Catalog, ITile } from '../Catalog';
import { VisualizationCanvas } from './VisualizationCanvas';
import './VisualizationCanvas.scss';
import { CamelRoute } from '../../camel-entities';
import { Button } from '@patternfly/react-core';

interface CanvasProps {
className?: string;
tiles: Record<string, ITile[]>;
}

export const Visualization: FunctionComponent<PropsWithChildren<CanvasProps>> = (props) => {
const [entities, setEntities] = useState<CamelRoute[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedCamelEntity, setSelectedCamelEntity] = useState<CamelRoute | undefined>(undefined);

const onTileClick = useCallback(
(tile: ITile) => {
if (selectedCamelEntity === undefined) {
return;
}

setIsModalOpen(false);
const firstStep = new Step({ name: tile.name });
selectedCamelEntity._addStep(firstStep);
setEntities([...entities, selectedCamelEntity]);
},
[entities, selectedCamelEntity],
);

const handleModalToggle = useCallback(() => {
setIsModalOpen(!isModalOpen);
}, [isModalOpen]);

return (
<div className={`canvasSurface ${props.className ?? ''}`}>
<VisualizationCanvas
contextToolbar={
<Button
onClick={() => {
setIsModalOpen(true);
const newEntity = new CamelRoute();
newEntity.id = 'new-route';

setEntities([...entities, newEntity]);
newEntity.id = getCamelRandomId('route');
setSelectedCamelEntity(newEntity);
}}
>
New Camel route
</Button>
}
entities={entities}
/>

<Modal title="Catalog browser" isOpen={isModalOpen} onClose={handleModalToggle} ouiaId="BasicModal">
<Catalog tiles={props.tiles} onTileClick={onTileClick} />
</Modal>
</div>
);
};
1 change: 1 addition & 0 deletions packages/ui/src/pages/Catalog/CatalogPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CatalogKind } from '../../models';
import { useCatalogStore } from '../../store';

export const CatalogPage: FunctionComponent = () => {
/** TODO: Extract this logic into a separate provider */
const { catalogs } = useCatalogStore((state) => state);
const [tiles, setTiles] = useState<Record<string, ITile[]>>({});
const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down
20 changes: 18 additions & 2 deletions packages/ui/src/pages/Design/DesignPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { Title } from '@patternfly/react-core';
import { FunctionComponent } from 'react';
import { FunctionComponent, useEffect, useState } from 'react';
import { camelComponentToTile, camelProcessorToTile, kameletToTile } from '../../camel-utils';
import { ITile } from '../../components/Catalog';
import { Visualization } from '../../components/Visualization';
import { CatalogKind } from '../../models';
import { useCatalogStore } from '../../store';
import './DesignPage.scss';

export const DesignPage: FunctionComponent = () => {
/** TODO: Extract this logic into a separate provider */
const catalogs = useCatalogStore((state) => state.catalogs);
const [tiles, setTiles] = useState<Record<string, ITile[]>>({});

useEffect(() => {
setTiles({
Component: Object.values(catalogs[CatalogKind.Component] ?? {}).map(camelComponentToTile),
Processor: Object.values(catalogs[CatalogKind.Processor] ?? {}).map(camelProcessorToTile),
Kamelet: Object.values(catalogs[CatalogKind.Kamelet] ?? {}).map(kameletToTile),
});
}, [catalogs]);

return (
<div className="canvasPage">
<Title headingLevel="h1">Visualization</Title>

<Visualization className="canvasPage__canvas" />
<Visualization className="canvasPage__canvas" tiles={tiles} />
</div>
);
};
4 changes: 2 additions & 2 deletions packages/ui/src/providers/catalog-schema-loader.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const CatalogSchemaLoaderContext = createContext<ComponentsCatalog>({});
* Loader for the components catalog and schemas.
*/
export const CatalogSchemaLoaderProvider: FunctionComponent<PropsWithChildren> = (props) => {
const { setCatalog } = useCatalogStore((state) => state);
const { setSchema } = useSchemasStore((state) => state);
const setCatalog = useCatalogStore((state) => state.setCatalog);
const setSchema = useSchemasStore((state) => state.setSchema);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
Expand Down

0 comments on commit e25c30d

Please sign in to comment.