Skip to content

Commit

Permalink
chore(simulator): added react-flow and updated toolbar (#180)
Browse files Browse the repository at this point in the history
* added react-flow component in the workbench

* added toolbar option for asyncapi file selection

* parse asyncApi file and generate simple flow representation of AsyncApi file
  • Loading branch information
SumantxD committed Aug 13, 2023
1 parent 6b4cca2 commit c8ad640
Show file tree
Hide file tree
Showing 15 changed files with 14,853 additions and 8,750 deletions.
14,099 changes: 14,099 additions & 0 deletions Desktop/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@
"react-icons": "^4.3.1",
"react-router-dom": "^5.3.0",
"react-virtualized": "^9.22.3",
"reactflow": "^11.7.4",
"regenerator-runtime": "^0.13.9"
},
"devEngines": {
Expand Down
36 changes: 35 additions & 1 deletion Desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* `./src/main.js` using webpack. This gives us some performance wins.
*/
import path from 'path';
import { app, BrowserWindow, shell, ipcMain } from 'electron';
import { app, BrowserWindow, shell, ipcMain, dialog } from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
// @ts-ignore
Expand All @@ -20,6 +20,8 @@ import { resolveHtmlPath } from './util';
// eslint-disable-next-line import/extensions
import autoSave from './tempScenarioSave';



export default class AppUpdater {
constructor() {
log.transports.file.level = 'info';
Expand Down Expand Up @@ -92,6 +94,8 @@ const installExtensions = async () => {
.catch(console.log);
};

//------------------------------------------*******---------------------------------

const createWindow = async () => {
if (isDevelopment) {
await installExtensions();
Expand Down Expand Up @@ -158,12 +162,42 @@ app.on('window-all-closed', () => {
}
});


async function handleFileLoad () {
const options = {
title: 'Open File',
defaultPath: app.getPath('documents'),
filters: [
{ name: 'Text Files', extensions: ['yaml'] },
{ name: 'All Files', extensions: ['*'] }
],
properties: ['openFile']
};

let filePath;

dialog.showOpenDialog(options).then(result => {
if (!result.canceled && result.filePaths.length > 0) {
filePath = result.filePaths[0];
console.log('Selected File:', filePath);
mainWindow.webContents.send('asynchronous-message', filePath);
}
}).catch(err => {
console.error('Error:', err);
});

}


app
.whenReady()
.then(() => {
createWindow();
app.on('activate', () => {
if (mainWindow === null) createWindow();
});

ipcMain.on('button-click', handleFileLoad)

})
.catch(console.log);
101 changes: 101 additions & 0 deletions Desktop/src/parser/flowGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { AsyncAPIDocument, Channel, Operation, Message } from '@asyncapi/parser';
import { Edge, Node } from 'reactflow';

interface FileredChannel {
channel: string;
channelModel: Channel;
operationModel: Operation;
messagesModel: Message[];
}

//given the operation publish/subscribe we will extract the channels realted to it from the spec
function getChannelsByOperation(operation: string, spec: AsyncAPIDocument) {
const channels = spec.channels();
return Object.keys(channels).reduce((filteredChannels: FileredChannel[], channel) => {
const operationFn = operation === 'publish' ? 'hasPublish' : 'hasSubscribe';
// eslint-disable-next-line
if (channels[String(channel)][operationFn]()) {
const operationModel = (channels as any)[String(channel)][String(operation)]() as OldOperation;
filteredChannels.push({
channel,
channelModel: channels[String(channel)],
operationModel,
messagesModel: operationModel.messages(),
});
}
return filteredChannels;
}, []);
}

function buildFlowElementsForOperation({ operation, spec, applicationLinkType, data }: { operation: 'publish' | 'subscribe'; spec: AsyncAPIDocument; applicationLinkType: string, data: any }): Array<{ node: Node, edge: Edge }> {
return getChannelsByOperation(operation, spec).reduce((nodes: any, channel) => {
const { channelModel, operationModel, messagesModel } = channel;

const node: Node = {
id: `${operation}-${channel.channel}`,
type: `${operation}Node`,
data: {
title: operationModel.id(),
channel: channel.channel,
tags: operationModel.tags(),
messages: messagesModel.map((message) => ({
title: message.uid(),
description: message.description(),
})),

spec,
description: channelModel.description(),
operationId: operationModel.id(),
elementType: operation,
theme: operation === 'subscribe' ? 'green' : 'blue',
...data
},
position: { x: 0, y: 0 },
};

const edge: Edge = {
id: `${operation}-${channel.channel}-to-application`,
// type: 'smoothstep',
animated: true,
label: messagesModel.map(message => message.uid()).join(','),
style: { stroke: applicationLinkType === 'target' ? '#00A5FA' : '#7ee3be', strokeWidth: 4 },
source: applicationLinkType === 'target' ? `${operation}-${channel.channel}` : 'application',
target: applicationLinkType === 'target' ? 'application' : `${operation}-${channel.channel}`,
};

return [...nodes, { node, edge }];
}, []);
}


//this will be the entry point of the node creation where we will us the specs to create the application, the publish and the subscribe nodes
export function generateFromSpecs(spec: AsyncAPIDocument): Array<{node:Node, edge:Edge}>{
//here we will use the publish operation to generate the nodes and edges for the publish section col-1
const publishNodes = buildFlowElementsForOperation({
operation: 'publish',
spec,
applicationLinkType: 'target',
data: { columnToRenderIn: 'col-1' },
});
//here we will use the subscribe operation to generate the nodes and edges for subscribe section col-3
const subscribeNodes = buildFlowElementsForOperation({
operation: 'subscribe',
spec,
applicationLinkType: 'source',
data: { columnToRenderIn: 'col-3' },
});
//here we will build the application node which will lie in the center of the canvas
const applicationNode = {
id: 'application',
type: 'applicationNode',
data: { spec, elementType: 'application', theme: 'indigo', columnToRenderIn: 'col-2' },
position: { x: 0, y: 0 },
}

return [
...publishNodes,
{ node: applicationNode } as { node: Node, edge: Edge },
...subscribeNodes
];

}
62 changes: 62 additions & 0 deletions Desktop/src/parser/utils/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { isNode, Node } from 'reactflow';

const groupNodesByColumn = (elements: Node[]) => {
return elements.reduce((elementsGrouped: any, element: Node) => {
if (isNode(element)) {
if (elementsGrouped[element?.data.columnToRenderIn]) {
return {
...elementsGrouped,
[element.data.columnToRenderIn]: elementsGrouped[element?.data.columnToRenderIn].concat([element])};
}

return {
...elementsGrouped,
[element.data.columnToRenderIn]: (elementsGrouped[element?.data.groupId] = [element]),
};
}
return elementsGrouped;
}, {});
};

export const calculateNodesForDynamicLayout = (elements: Node[]) => {
const elementsGroupedByColumn = groupNodesByColumn(elements);

const newElements: { nodes: Node[], currentXPosition: number } = Object.keys(elementsGroupedByColumn).reduce(
(data: { nodes: Node[], currentXPosition: number }, group: string) => {
const groupNodes = elementsGroupedByColumn[String(group)];

// eslint-disable-next-line
const maxWidthOfColumn = Math.max.apply(
Math,
groupNodes.map((o: Node) => {
return o.width;
})
);

// For each group (column), render the nodes based on height they require (with some padding)
const { positionedNodes } = groupNodes.reduce(
(groupedNodes: { positionedNodes: Node[], currentYPosition: number }, currentNode: Node) => {
const verticalPadding = 40;

currentNode.position.x = data.currentXPosition;
currentNode.position.y = groupedNodes.currentYPosition;

return {
positionedNodes: groupedNodes.positionedNodes.concat([currentNode]),
currentYPosition: groupedNodes.currentYPosition + (currentNode.height || 0) + verticalPadding,
};
},
{ positionedNodes: [], currentYPosition: 0 }
);

return {
nodes: [...data.nodes, ...positionedNodes],
currentXPosition: data.currentXPosition + maxWidthOfColumn + 100,
};
},
{ nodes: [], currentXPosition: 0 }
);

return newElements.nodes;

};
10 changes: 6 additions & 4 deletions Desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';
import { MemoryRouter as Router, Switch, Route } from 'react-router-dom';
import React from 'react'
import './App.global.css';
import { MemoryRouter as Router, Switch, Route } from 'react-router-dom';
import { Editor } from './containers/Editor';

export default function App() {
const App = () => {
return (
<Router>
<Switch>
<Route path="/" component={Editor} />
</Switch>
</Router>
);
)
}

export default App

0 comments on commit c8ad640

Please sign in to comment.