Skip to content

Commit

Permalink
Eirby/drawing modules viz lines (#414)
Browse files Browse the repository at this point in the history
This PR generates a svg we can use to visualize the controllers system.
In subsequent PR's I'll do the following
- Style to match current light and dark theme
- add zooming and panning of the svg
- link sidebar module and verb links to and the zoom and pan feature to
zoom to clicked verbs and modules
- add module and verb node and edge highlighting based on selection
- ...

![Screenshot 2023-09-20 at 4 05 24
PM](https://github.com/TBD54566975/ftl/assets/1058725/61f6a99a-dd76-47b4-b989-76a158683e13)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
EdwardIrby and github-actions[bot] authored Sep 20, 2023
1 parent e046f17 commit fa1582d
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 194 deletions.
13 changes: 13 additions & 0 deletions console/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions console/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@monaco-editor/react": "4.5.2",
"@tailwindcss/forms": "^0.5.6",
"@vitejs/plugin-react": "^4.0.4",
"@viz-js/viz": "3.2.0",
"highlight.js": "^11.8.0",
"json-schema": "0.4.0",
"json-schema-faker": "0.5.0-rcv.46",
Expand All @@ -49,6 +50,7 @@
"@bufbuild/protoc-gen-connect-es": "0.12.0",
"@bufbuild/protoc-gen-es": "1.3.0",
"@jest/globals": "29.6.2",
"@types/p5": "1.7.0",
"@swc/core": "1.3.77",
"@swc/jest": "0.2.29",
"@testing-library/jest-dom": "6.0.0",
Expand Down
63 changes: 21 additions & 42 deletions console/client/src/features/modules/ModulesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,30 @@ import React from 'react'
import { Square3Stack3DIcon } from '@heroicons/react/24/outline'
import { PageHeader } from '../../components/PageHeader'
import { modulesContext } from '../../providers/modules-provider'
import { Disclosure } from '@headlessui/react'
import { createLayoutDataStructure } from './create-layout-data-structure'

import { generateDot } from './generate-dot'
import { dotToSVG } from './dot-to-svg'
import { formatSVG } from './format-svg'
export const ModulesPage = () => {
const modules = React.useContext(modulesContext)
const data = createLayoutDataStructure(modules)

const dot = generateDot(modules)
const ref = React.useRef<HTMLDivElement>(null)
const [viewport, setViewPort] = React.useState<HTMLDivElement>()
React.useEffect(() => {
const cur = ref.current
cur && setViewPort(cur)
}, [])
React.useEffect(() => {
const renderSvg = async () => {
const svg = await dotToSVG(dot)
svg && viewport?.replaceChildren(formatSVG(svg))
}
viewport && void renderSvg()
}, [dot, viewport])
// console.log(generateDotFile(modules))
return (
<>
<div className='h-full w-full flex flex-col'>
<PageHeader icon={<Square3Stack3DIcon />} title='Modules' />
<div role='list' className='flex flex-col space-y-3 p-2'>
{data?.map(({ name, style, verbs, 'data-id': dataId }) => (
<Disclosure
as='div'
key={name}
style={{ ...style }}
data-id={dataId}
className='min-w-fit w-44 border border-gray-100 dark:border-slate-700 rounded overflow-hidden inline-block'
>
<Disclosure.Button
as='button'
className='text-gray-600 dark:text-gray-300 p-1 w-full text-left flex justify-between items-center'
>
{name}
</Disclosure.Button>
<Disclosure.Panel as='ul' className='text-gray-400 dark:text-gray-400 text-xs p-1 space-y-1 list-inside'>
{verbs.map(({ name, 'data-id': dataId }) => (
<li key={name} className='flex items-center text-gray-900 dark:text-gray-400'>
<svg
data-id={dataId}
className='w-3.5 h-3.5 mr-2 text-gray-500 dark:text-gray-400 flex-shrink-0'
aria-hidden='true'
xmlns='http://www.w3.org/2000/svg'
fill='currentColor'
viewBox='0 0 20 20'
>
<circle cx='10' cy='10' r='4.5' />
</svg>
{name}
</li>
))}
</Disclosure.Panel>
</Disclosure>
))}
</div>
</>
<div ref={ref} className='viewport' />
</div>
)
}
11 changes: 11 additions & 0 deletions console/client/src/features/modules/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const callIconID = 'call-icon'

export const callIcon = `<defs>
<symbol id="${callIconID}" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 3.75v4.5m0-4.5h-4.5m4.5 0l-6 6m3 12c-8.284 0-15-6.716-15-15V4.5A2.25 2.25 0 014.5 2.25h1.372c.516 0 .966.351 1.091.852l1.106 4.423c.11.44-.054.902-.417 1.173l-1.293.97a1.062 1.062 0 00-.38 1.21 12.035 12.035 0 007.143 7.143c.441.162.928-.004 1.21-.38l.97-1.293a1.125 1.125 0 011.173-.417l4.423 1.106c.5.125.852.575.852 1.091V19.5a2.25 2.25 0 01-2.25 2.25h-2.25z" />
</symbol>
</defs>
`

export const moduleVerbCls = 'module-verb'
export const moduleTitleCls = 'module-title'
152 changes: 0 additions & 152 deletions console/client/src/features/modules/create-layout-data-structure.ts

This file was deleted.

10 changes: 10 additions & 0 deletions console/client/src/features/modules/dot-to-svg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { instance } from '@viz-js/viz'

export const dotToSVG = async (dot: string) => {
const viz = await instance()
try {
return viz.renderSVGElement(dot)
} catch (e) {
console.error(e)
}
}
75 changes: 75 additions & 0 deletions console/client/src/features/modules/format-svg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { callIcon, moduleVerbCls, callIconID } from './constants'
export const formatSVG = (svg: SVGSVGElement): SVGSVGElement => {
svg.insertAdjacentHTML('afterbegin', callIcon)

for (const $a of svg.querySelectorAll('a')) {
const $g = $a.parentNode! as SVGSVGElement

const $docFrag = document.createDocumentFragment()
while ($a.firstChild) {
const $child = $a.firstChild
$docFrag.appendChild($child)
}

$g.replaceChild($docFrag, $a)

$g.id = $g.id.replace(/^a_/, '')
}

for (const $el of svg.querySelectorAll('title')) {
$el.remove()
}

const edgesSources = new Set<string>()
for (const $edge of svg.querySelectorAll('.edge')) {
const [from, to] = $edge.id.split('=>')
$edge.removeAttribute('id')
$edge.setAttribute('data-from', from)
$edge.setAttribute('data-to', to)
edgesSources.add(from)
}

for (const $el of svg.querySelectorAll('[id*=\\:\\:]')) {
const [tag, id] = $el.id.split('::')
$el.id = id
$el.classList.add(tag)
}

for (const $path of svg.querySelectorAll('g.edge path')) {
const $newPath = $path.cloneNode() as HTMLElement
$newPath.classList.add('hover-path')
$newPath.removeAttribute('stroke-dasharray')
$path.parentNode?.appendChild($newPath)
}

for (const $verb of svg.querySelectorAll(`.${moduleVerbCls}`)) {
const texts = $verb.querySelectorAll('text')
texts[0].classList.add('verb-name')

// Tag verb as a call source
if (edgesSources.has($verb.id)) $verb.classList.add('call-source')

// Replace icon
const length = texts.length
for (let i = 1; i < length; ++i) {
const str = texts[i].innerHTML
if (str === '{R}') {
const $iconPlaceholder = texts[i]
const height = 22
const width = 22
const $useIcon = document.createElementNS('http://www.w3.org/2000/svg', 'use')
$useIcon.setAttributeNS('http://www.w3.org/1999/xlink', 'href', `#${callIconID}`)
$useIcon.setAttribute('width', `${width}px`)
$useIcon.setAttribute('height', `${height}px`)

//FIXME: remove hardcoded offset
const y = parseInt($iconPlaceholder.getAttribute('y')!) - 15
$useIcon.setAttribute('x', $iconPlaceholder.getAttribute('x')!)
$useIcon.setAttribute('y', y.toString())
$verb.replaceChild($useIcon, $iconPlaceholder)
continue
}
}
}
return svg
}
Loading

0 comments on commit fa1582d

Please sign in to comment.