Skip to content

Commit

Permalink
feat: add visualiser
Browse files Browse the repository at this point in the history
  • Loading branch information
Shurtu-gal committed Jul 10, 2023
1 parent d49e4d9 commit 250fb20
Show file tree
Hide file tree
Showing 24 changed files with 1,129 additions and 87 deletions.
16 changes: 16 additions & 0 deletions apps/studio/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const withTM = require("next-transpile-modules")([
"monaco-editor",
]);

const nodeExternals = require('webpack-node-externals');

// Monaco Editor uses CSS imports internally,
// so we need a separate css-loader for app and monaco-editor packages and other packages
const MONACO_DIR = path.resolve(__dirname, "../../node_modules/");
Expand Down Expand Up @@ -45,19 +47,27 @@ const nextConfig = withTM({
}
// fix import of monaco-editor css files
config.module.rules.push(
// For monaco-editor support
{
test: /\.css$/,
include: MONACO_DIR,
use: ["style-loader", "css-loader"],
},
// For tailwindcss support
{
test: /\.css$/i,
include: path.resolve(__dirname, "src"),
use: ["style-loader", "css-loader", "postcss-loader"],
},
// For yaml support
{
test: /\.yml$/i,
type: "asset/source",
},
// For canvas.node support
{
test: /\.node$/,
use: "node-loader",
}
);

Expand Down Expand Up @@ -135,6 +145,12 @@ const nextConfig = withTM({
/Failed to parse source map/,
];

if(isServer) {
config.externals = [
nodeExternals()
]
}

return config;
},
});
Expand Down
5 changes: 4 additions & 1 deletion apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@monaco-editor/react": "^4.4.6",
"@netlify/plugin-nextjs": "^4.36.1",
"@tippyjs/react": "^4.2.6",
"canvas": "^2.11.2",
"css-loader": "^6.8.1",
"js-base64": "^3.7.3",
"js-file-download": "^0.4.12",
Expand Down Expand Up @@ -117,6 +118,7 @@
"eslint-webpack-plugin": "^4.0.1",
"https-browserify": "^1.0.0",
"markdown-toc": "^1.2.0",
"node-loader": "^2.0.0",
"path-browserify": "^1.0.1",
"postcss": "^8.4.19",
"process": "^0.11.10",
Expand All @@ -129,7 +131,8 @@
"url": "^0.11.0",
"util": "^0.12.5",
"web-vitals": "^3.1.0",
"webpack": "^5.75.0"
"webpack": "^5.75.0",
"webpack-node-externals": "^3.0.0"
},
"jest": {
"transformIgnorePatterns": [
Expand Down
4 changes: 3 additions & 1 deletion apps/studio/src/components/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { usePanelsState } from "../states"
import { Editor } from "./Editor";
import SplitPane from "./SplitPane";
import { Navigation } from "./Navigation";
// import { Template } from "./Template";
import { VisualiserTemplate } from "./Visualiser";

interface ContentProps {}

Expand Down Expand Up @@ -75,7 +77,7 @@ const Content : React.FC<ContentProps> = () => {
>
{navigationAndEditor}
{/* {viewType === 'template' && <Template />} */}
{/* {viewType === 'visualiser' && <VisualiserTemplate />} */}
{viewType === 'visualiser' && <VisualiserTemplate />}
</SplitPane>
</div>
</div>
Expand Down
61 changes: 61 additions & 0 deletions apps/studio/src/components/Template/HTMLWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client'

import React, { useState, useEffect } from 'react';
import { AsyncApiComponentWP } from '@asyncapi/react-component';

import { appState, useDocumentsState, useSettingsState, useOtherState, otherState } from '../../states';

import type { AsyncAPIDocumentInterface } from '@asyncapi/parser/cjs';

interface HTMLWrapperProps {}

export const HTMLWrapper: React.FunctionComponent<HTMLWrapperProps> = () => {
const [parsedSpec, setParsedSpec] = useState<AsyncAPIDocumentInterface | null>(null);
// const { navigationSvc } = useServices();
const document = useDocumentsState(state => state.documents['asyncapi']?.document) || null;
const autoRendering = useSettingsState(state => state.templates.autoRendering);
const templateRerender = useOtherState(state => state.templateRerender);

// useEffect(() => {
// navigationSvc.scrollToHash();
// }, []); // eslint-disable-line

useEffect(() => {
if (autoRendering || parsedSpec === null) {
setParsedSpec(document);
}
}, [document]); // eslint-disable-line

useEffect(() => {
if (templateRerender) {
setParsedSpec(document);
otherState.setState({ templateRerender: false });
}
}, [templateRerender]); // eslint-disable-line

if (!document) {
return (
<div className="flex flex-1 overflow-hidden h-full justify-center items-center text-2xl mx-auto px-6 text-center">
<p>Empty or invalid document. Please fix errors/define AsyncAPI document.</p>
</div>
);
}

return (
parsedSpec && (
<div className="flex flex-1 flex-col h-full overflow-hidden">
<div className="overflow-auto">
{/* <AsyncApiComponentWP
schema={parsedSpec}
config={{
show: {
errors: false,
sidebar: appState.getState().readOnly,
},
}}
/> */}
</div>
</div>
)
);
};
17 changes: 17 additions & 0 deletions apps/studio/src/components/Template/Template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';

import { TemplateSidebar } from './TemplateSidebar';
import { HTMLWrapper } from './HTMLWrapper';

import { appState } from '../../states';

interface TemplateProps {}

export const Template: React.FunctionComponent<TemplateProps> = () => {
return (
<div className="flex flex-1 flex-col h-full overflow-hidden">
{!appState.getState().readOnly && <TemplateSidebar />}
<HTMLWrapper />
</div>
);
};
32 changes: 32 additions & 0 deletions apps/studio/src/components/Template/TemplateSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { VscRefresh } from 'react-icons/vsc';

import { useSettingsState, otherState } from '../../states';

interface TemplateSidebarProps {}

export const TemplateSidebar: React.FunctionComponent<TemplateSidebarProps> = () => {
const autoRendering = useSettingsState(state => state.templates.autoRendering);

return (
<div
className="flex flex-row items justify-between bg-gray-800 border-b border-gray-700 text-sm"
style={{ height: '30px', lineHeight: '30px' }}
>
{autoRendering ? (
<div />
) : (
<div className="ml-2 text-gray-500 text-xs flex" style={{ height: '30px', lineHeight: '30px' }}>
<button type="button" className="text-xs" onClick={() => otherState.setState({ templateRerender: true })}>
<div className="inline-block">
<VscRefresh className="w-4 h-4 mt-1" />
</div>
</button>
<span className="ml-2 italic">
Rerender
</span>
</div>
)}
</div>
);
};
2 changes: 2 additions & 0 deletions apps/studio/src/components/Template/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Template';
export * from './HTMLWrapper';
43 changes: 43 additions & 0 deletions apps/studio/src/components/Visualiser/Controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useState, useEffect } from 'react';
import { useStore, useReactFlow, useNodes, useEdges } from 'reactflow';
import { VscDebugStart, VscDebugPause, VscRefresh } from 'react-icons/vsc';

import { calculateNodesForDynamicLayout } from './utils/node-calculator';

import type { FunctionComponent } from 'react';

interface ControlsProps {}

export const Controls: FunctionComponent<ControlsProps> = () => {
const [animateNodes, setAnimateNodes] = useState(false);

const { fitView } = useReactFlow();
const nodes = useNodes();
const edges = useEdges();
const setNodes = useStore(state => state.setNodes);
const setEdges = useStore(state => state.setEdges);

useEffect(() => {
if (nodes.length > 0) {
const newNodeEdges = edges.map(edge => ({ ...edge, animated: animateNodes }));
setEdges([...newNodeEdges]);
}
}, [animateNodes]);

const reloadInterface = () => {
setNodes(calculateNodesForDynamicLayout(nodes));
fitView();
};

return (
<div className="absolute top-0 right-0 mr-5 mt-5 rounded-lg bg-white z-20 space-x-10 px-4 pt-1 shadow-lg">
<button type="button" className="text-xs" onClick={() => setAnimateNodes(!animateNodes)}>
{animateNodes && <VscDebugPause className="w-4 h-4" />}
{!animateNodes && <VscDebugStart className="w-4 h-4" />}
</button>
<button type="button" className="text-xs" onClick={reloadInterface}>
<VscRefresh className="w-4 h-4" />
</button>
</div>
);
};
79 changes: 79 additions & 0 deletions apps/studio/src/components/Visualiser/FlowDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client';

import { useEffect } from 'react';
import ReactFlow, { Controls as FlowControls, Background, BackgroundVariant, useReactFlow, useStore, useNodesState, useEdgesState, useNodes } from 'reactflow';

import NodeTypes from './Nodes';
import { Controls } from './Controls';
import { getElementsFromAsyncAPISpec } from './utils/node-factory';
import { calculateNodesForDynamicLayout } from './utils/node-calculator';

import type { AsyncAPIDocumentInterface } from '@asyncapi/parser/cjs';
import type { FunctionComponent } from 'react';

interface FlowDiagramProps {
parsedSpec: AsyncAPIDocumentInterface;
}

interface AutoLayoutProps {}

const AutoLayout: FunctionComponent<AutoLayoutProps> = () => {
const { fitView } = useReactFlow();
const nodes = useNodes();
const setNodes = useStore(state => state.setNodes);

useEffect(() => {
if (nodes.length === 0 || !nodes[0].width) {
return;
}

const nodesWithOrginalPosition = nodes.filter(node => node.position.x === 0 && node.position.y === 0);
if (nodesWithOrginalPosition.length > 1) {
const calculatedNodes = calculateNodesForDynamicLayout(nodes);
setNodes(calculatedNodes);
fitView();
}
}, [nodes]);

return null;
};

export const FlowDiagram: FunctionComponent<FlowDiagramProps> = ({ parsedSpec }) => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);

useEffect(() => {
const elements = getElementsFromAsyncAPISpec(parsedSpec);
const newNodes = elements.map(el => el.node).filter(Boolean);
const newEdges = elements.map(el => el.edge).filter(Boolean);

setNodes(newNodes);
setEdges(newEdges);
}, [parsedSpec]);

return (
<div className="h-screen bg-gray-800 relative">
<ReactFlow
nodeTypes={NodeTypes}
nodes={nodes}
edges={edges}
minZoom={0.1}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
fitView={true}
>
<Background variant={BackgroundVariant.Dots} gap={20} size={1} className="bg-gray-200" />
<AutoLayout />
<Controls />
<div className="-mt-20">
<FlowControls style={{ bottom: '95px' }} className='bg-white' />
</div>
</ReactFlow>
<div className="m-4 px-2 text-lg absolute text-gray-800 top-0 left-0 bg-white space-x-2 py-2 border border-gray-100 inline-block">
<span className="font-bold">Event Visualiser</span>
<span className="text-gray-200">|</span>
<span className="font-light capitalize">{parsedSpec.info().title()}</span>
</div>
</div>
);
};
Loading

0 comments on commit 250fb20

Please sign in to comment.