Skip to content

Commit

Permalink
Open node-detail-preview on node selection
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviajanejohns committed Jan 10, 2025
1 parent eafc9d4 commit fa3ec52
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 187 deletions.
48 changes: 24 additions & 24 deletions calm-visualizer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,47 @@ This template provides a minimal setup to get React working in Vite with HMR and

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:
- Configure the top-level `parserOptions` property like this:

```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
})
```

- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:

```js
// eslint.config.js
import react from 'eslint-plugin-react'

export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
2 changes: 1 addition & 1 deletion calm-visualizer/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
position: relative;
display: flex;
justify-content: center;
}
}
83 changes: 3 additions & 80 deletions calm-visualizer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,12 @@
import { useEffect, useState } from "react";
import './App.css'
import { BrowserJsPlumbInstance, newInstance } from "@jsplumb/browser-ui"
import FileUploader from "./components/fileuploader/FileUploader";
import { NodeLayout, RelationshipLayout } from "./layout";
import Graph from "./components/graph/Graph";
import { saveAs } from 'file-saver';
import { CALMNode } from "./types";

interface LayoutObject {
'unique-id': string,
x: number,
y: number
}
import Drawer from './components/drawer/Drawer'

function App() {
const [nodes, setNodes] = useState<NodeLayout[]>([]);
const [relationships, setRelationships] = useState<RelationshipLayout[]>([]);
const [instance, setInstance] = useState<BrowserJsPlumbInstance>();
const [title, setTitle] = useState('Architecture as Code');

function enhanceNodesWithLayout(nodes: CALMNode[], coords: LayoutObject[]): NodeLayout[] {
return nodes.map(node => {
const coordObject = coords.find((coord) => coord["unique-id"] == node["unique-id"]);
return {
...node,
x: coordObject?.x || 400,
y: coordObject?.y || 400
}
});
}

async function handleFile(instanceFile: File, layoutFile?: File) {
const instanceString = await instanceFile.text();
const calmInstance = JSON.parse(instanceString);
setTitle(instanceFile.name);

const nodePositions: LayoutObject[] = [];
if (layoutFile) {
const layoutString = await layoutFile.text();
const layout = JSON.parse(layoutString);
layout.nodes.forEach((node: LayoutObject) => nodePositions.push(node));
}

setNodes(enhanceNodesWithLayout(calmInstance.nodes, nodePositions));
setRelationships(calmInstance.relationships);
}

function onSave() {
const outputNodes = nodes.map(node => {
const bounds = document.getElementById(node["unique-id"])?.getBoundingClientRect();

return {
"unique-id": node["unique-id"],
"x": bounds?.x,
"y": bounds?.y
}
});

const output = JSON.stringify({
nodes: outputNodes,
relationships
});

const blob = new Blob([output], {type: "text/plain;charset=utf-8"});
saveAs(blob, "layout.json");
}

useEffect(() => {
setInstance(newInstance({
container: document.getElementById('app')!
}));
}, []);

return (
<>
<h1>{title}</h1>

<FileUploader callback={handleFile}/>

<button onClick={onSave}>SaveAs</button>
<div id="app">
{instance && <Graph instance={instance} nodes={nodes} relationships={relationships} />}
</div>
<Drawer />
</>
);
)
}

export default App
130 changes: 130 additions & 0 deletions calm-visualizer/src/components/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import Sidebar from '../sidebar/Sidebar'
import { BrowserJsPlumbInstance, newInstance } from '@jsplumb/browser-ui'
import saveAs from 'file-saver'
import { useEffect, useState } from 'react'
import { NodeLayout, RelationshipLayout } from '../../layout'
import { CALMNode } from '../../types'
import FileUploader from '../fileuploader/FileUploader'
import Graph from '../graph/Graph'

interface LayoutObject {
'unique-id': string
x: number
y: number
}

function Drawer() {
const [nodes, setNodes] = useState<NodeLayout[]>([])
const [relationships, setRelationships] = useState<RelationshipLayout[]>([])
const [instance, setInstance] = useState<BrowserJsPlumbInstance>()
const [title, setTitle] = useState('Architecture as Code')
const [selectedNode, setSelectedNode] = useState(null)

const closeSidebar = () => {
setSelectedNode(null)
}

function enhanceNodesWithLayout(
nodes: CALMNode[],
coords: LayoutObject[]
): NodeLayout[] {
return nodes.map((node) => {
const coordObject = coords.find(
(coord) => coord['unique-id'] == node['unique-id']
)
return {
...node,
x: coordObject?.x || 400,
y: coordObject?.y || 400,
}
})
}

async function handleFile(instanceFile: File, layoutFile?: File) {
const instanceString = await instanceFile.text()
const calmInstance = JSON.parse(instanceString)
setTitle(instanceFile.name)

const nodePositions: LayoutObject[] = []
if (layoutFile) {
const layoutString = await layoutFile.text()
const layout = JSON.parse(layoutString)
layout.nodes.forEach((node: LayoutObject) =>
nodePositions.push(node)
)
}

setNodes(enhanceNodesWithLayout(calmInstance.nodes, nodePositions))
setRelationships(calmInstance.relationships)
}

function onSave() {
const outputNodes = nodes.map((node) => {
const bounds = document
.getElementById(node['unique-id'])
?.getBoundingClientRect()

return {
'unique-id': node['unique-id'],
x: bounds?.x,
y: bounds?.y,
}
})

const output = JSON.stringify({
nodes: outputNodes,
relationships,
})

const blob = new Blob([output], { type: 'text/plain;charset=utf-8' })
saveAs(blob, 'layout.json')
}

useEffect(() => {
setInstance(
newInstance({
container: document.getElementById('app')!,
})
)
}, [])

return (
<>
<div
className={`drawer drawer-end ${selectedNode ? 'drawer-open' : ''}`}
>
<input
type="checkbox"
className="drawer-toggle"
checked={!!selectedNode}
onChange={closeSidebar}
/>
<div className="drawer-content">
<div className="text-xl font-bold">{title}</div>
<FileUploader callback={handleFile} />
<button className="btn" onClick={onSave}>
SaveAs
</button>
<div id="app">
{instance && (
<Graph
instance={instance}
nodes={nodes}
relationships={relationships}
setSelectedNode={setSelectedNode}
/>
)}
</div>
</div>
{selectedNode && (
<Sidebar
selectedNode={selectedNode}
closeSidebar={closeSidebar}
/>
)}
</div>
</>
)
}

export default Drawer
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function FileUploader({ callback }: Props) {
onClick={handleSubmit}
disabled={!filesChanged}
className="submit"
>Visualize</button>
>Upload a file</button>
)}
</>
);
Expand Down
2 changes: 2 additions & 0 deletions calm-visualizer/src/components/graph/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface Props {
relationships: RelationshipLayout[]
}

// const groups: string[] = [];

function createConnectsRelationship(instance: BrowserJsPlumbInstance, relationship: CALMConnectsRelationship) {
const r = relationship["relationship-type"]["connects"];
instance.connect({
Expand Down
34 changes: 34 additions & 0 deletions calm-visualizer/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { IoCloseOutline } from 'react-icons/io5'
import { CALMNode } from '../../types'

interface SidebarProps {
selectedNode: CALMNode
closeSidebar: () => void
}

function Sidebar({ selectedNode, closeSidebar }: SidebarProps) {
return (
<div className="drawer-side">
<label
htmlFor="node-details"
className="drawer-overlay"
onClick={closeSidebar}
></label>
<div className="menu bg-base-200 text-base-content min-h-full w-80 p-4">
<button
onClick={closeSidebar}
className="btn btn-square ml-auto"
>
<IoCloseOutline />
</button>
<div className="text-xl font-bold">Node</div>
<span>unique-id: {selectedNode['unique-id']}</span>
<span>name: {selectedNode.name}</span>
<span>node-type: {selectedNode['node-type']}</span>
<span>description: {selectedNode.description}</span>
</div>
</div>
)
}

export default Sidebar
8 changes: 4 additions & 4 deletions calm-visualizer/src/layout.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CALMNode, CALMRelationship } from "./types"
import { CALMNode, CALMRelationship } from './types'

export interface NodeLayout extends CALMNode {
"x": number,
"y": number
x: number
y: number
}

export type RelationshipLayout = CALMRelationship;
export type RelationshipLayout = CALMRelationship
4 changes: 1 addition & 3 deletions calm-visualizer/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
<App />
)
createRoot(document.getElementById('root')!).render(<App />)
Loading

0 comments on commit fa3ec52

Please sign in to comment.