-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
51669f6
commit 82a0466
Showing
8 changed files
with
299 additions
and
82 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,97 @@ | ||
import { useMemo, type JSX } from 'react' | ||
import type { Edge, Node } from '../graph' | ||
import { CustomGraphNode } from './CustomGraphNode' | ||
import { Surface } from './Surface' | ||
|
||
export type GraphNode = Node | ||
|
||
export function GraphNode({ | ||
width, | ||
height, | ||
data: { subtitle, title, label }, | ||
}: GraphNode): JSX.Element { | ||
return ( | ||
<div | ||
className="px-4 py-2 shadow-md rounded-md bg-white dark:bg-[#262626] border-2 border-[#ebebeb] dark:border-[#333333]" | ||
style={{ width: `${width}px`, height: `${height}px` }} | ||
> | ||
<div className="flex"> | ||
<div className="rounded-full w-12 h-12 flex justify-center items-center bg-gray-100 dark:bg-[#363a3a] shrink-0"> | ||
{label} | ||
</div> | ||
<div className="ml-2 grow min-w-0"> | ||
<div className="text-lg font-bold truncate">{title}</div> | ||
<div className="text-gray-500 truncate">{subtitle}</div> | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
type EdgesParams = { | ||
edges: Edge[] | ||
} | ||
|
||
function Edges({ edges }: EdgesParams): JSX.Element { | ||
const beziers = useMemo(() => { | ||
const s = 1 | ||
const c = 1 | ||
return edges.map((edge) => { | ||
const pathStart = `M${edge.start.x},${edge.start.y}` | ||
const pathStartControl = `C${edge.start.x + s / 3}, ${edge.start.y + c}` | ||
const pathStartControl = `C${edge.start.x}, ${edge.start.y + 30}` | ||
|
||
const y = 1 | ||
const pathEnd = `${edge.end.x},${edge.end.y}` | ||
const pathEndControl = `${edge.start.x + s},${edge.start.y + y}` | ||
const pathEndControl = `${edge.end.x},${edge.end.y - 30}` | ||
|
||
return `${pathStart}, ${pathStartControl}, ${pathEnd}, ${pathEndControl}` | ||
return `${pathStart}, ${pathStartControl}, ${pathEndControl}, ${pathEnd}, ` | ||
}) | ||
}, [edges]) | ||
|
||
return ( | ||
<svg role="img" aria-label="Graph edge" className="w-full h-full"> | ||
<g> | ||
{/* <path | ||
d="M415,117.5 C415,140 190,140 190,162.5" | ||
className="stroke-black fill-none stroke-2" | ||
/> */} | ||
{beziers.map((bezier) => ( | ||
<path | ||
key={bezier} | ||
d={bezier} | ||
className="stroke-black fill-none stroke-2" | ||
className="fill-none stroke-2 stroke-[#ebebeb] dark:stroke-[#333333]" | ||
/> | ||
))} | ||
</g> | ||
</svg> | ||
) | ||
} | ||
|
||
// ;<svg style="z-index: 0;"> | ||
// <g | ||
// class="react-flow__edge react-flow__edge-default nopan inactive" | ||
// role="img" | ||
// data-id="kubernetes/6afc2100-bf6d-4a93-9238-5a62087178d1->kubernetes/your-spotify" | ||
// data-testid="rf__edge-kubernetes/6afc2100-bf6d-4a93-9238-5a62087178d1->kubernetes/your-spotify" | ||
// aria-label="Edge from kubernetes/your-spotify to kubernetes/6afc2100-bf6d-4a93-9238-5a62087178d1" | ||
// > | ||
// <path | ||
// d="M415,117.5 C415,140 190,140 190,162.5" | ||
// fill="none" | ||
// class="react-flow__edge-path" | ||
// ></path> | ||
// <path | ||
// d="M415,117.5 C415,140 190,140 190,162.5" | ||
// fill="none" | ||
// stroke-opacity="0" | ||
// stroke-width="20" | ||
// class="react-flow__edge-interaction" | ||
// ></path> | ||
// </g> | ||
// </svg> | ||
|
||
type GraphProps = { | ||
nodes: Node[] | ||
edges: Edge[] | ||
bounds: { width: number; height: number } | ||
} | ||
|
||
export function Graph({ nodes, edges }: GraphProps): JSX.Element { | ||
export function Graph({ | ||
nodes, | ||
edges, | ||
bounds: { width, height }, | ||
}: GraphProps): JSX.Element { | ||
return ( | ||
<div className="relative w-full h-full bg-red-400 overflow-hidden"> | ||
<Edges edges={edges} /> | ||
{nodes.map((node) => ( | ||
<div className="w-full h-full"> | ||
<Surface> | ||
<div | ||
key={node.id} | ||
className="absolute" | ||
style={{ top: `${node.position.y}px`, left: `${node.position.x}px` }} | ||
className="relative" | ||
style={{ width: `${width}px`, height: `${height}px` }} | ||
> | ||
<CustomGraphNode {...node.data} /> | ||
<Edges edges={edges} /> | ||
{nodes.map((node) => ( | ||
<div | ||
key={node.id} | ||
className="absolute" | ||
style={{ | ||
top: `${node.position.y}px`, | ||
left: `${node.position.x}px`, | ||
}} | ||
> | ||
<GraphNode {...node} /> | ||
</div> | ||
))} | ||
</div> | ||
))} | ||
</Surface> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { | ||
type JSX, | ||
type PropsWithChildren, | ||
useCallback, | ||
useEffect, | ||
useRef, | ||
useState, | ||
} from 'react' | ||
import { FluentFullScreenMaximize16Regular } from './icons/fluent-full-screen-maximize-16-regular' | ||
import { FluentAdd16Regular } from './icons/fluent-plus-16-regular' | ||
import { FluentSubtract16Regular } from './icons/fluent-subtract-16-regular' | ||
|
||
export function Surface({ | ||
children, | ||
}: PropsWithChildren<Record<never, never>>): JSX.Element { | ||
const surfaceRef = useRef<HTMLDivElement>(null) | ||
const contentRef = useRef<HTMLDivElement>(null) | ||
|
||
const [offset, setOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 }) | ||
const [scale, setScale] = useState<number>(1.0) | ||
const [isDragging, setIsDragging] = useState(false) | ||
|
||
const onMouseMove = useCallback((e: MouseEvent) => { | ||
setOffset((current) => ({ | ||
x: current.x + e.movementX, | ||
y: current.y + e.movementY, | ||
})) | ||
}, []) | ||
const onMouseUp = useCallback( | ||
(e: MouseEvent) => { | ||
document.removeEventListener('mousemove', onMouseMove) | ||
|
||
setIsDragging(false) | ||
}, | ||
[onMouseMove] | ||
) | ||
const onMouseDown = useCallback( | ||
(e: MouseEvent) => { | ||
if (e.buttons !== 1) { | ||
return | ||
} | ||
|
||
document.addEventListener('mousemove', onMouseMove) | ||
document.addEventListener('mouseup', onMouseUp) | ||
|
||
setIsDragging(true) | ||
}, | ||
[onMouseMove, onMouseUp] | ||
) | ||
|
||
const onZoom = useCallback((delta: number) => { | ||
setScale((current) => Math.min(Math.max(current + delta * 0.1, 0.4), 1)) | ||
}, []) | ||
|
||
const onCenter = useCallback(() => { | ||
if (!contentRef.current || !surfaceRef.current) { | ||
return | ||
} | ||
|
||
const surfaceWidth = surfaceRef.current.offsetWidth | ||
const surfaceHeight = surfaceRef.current.offsetHeight | ||
|
||
const contentWidth = contentRef.current.offsetWidth | ||
const contentHeight = contentRef.current.offsetHeight | ||
|
||
const scale = Math.min( | ||
surfaceWidth / contentWidth, | ||
surfaceHeight / contentHeight | ||
) | ||
|
||
setScale(Math.min(Math.max(scale, 0.3), 1)) | ||
setOffset({ | ||
x: surfaceWidth / 2 - contentWidth / 2, | ||
y: surfaceHeight / 2 - contentHeight / 2, | ||
}) | ||
}, []) | ||
|
||
const onWheel = useCallback((e: WheelEvent) => { | ||
setScale((current) => | ||
Math.min(Math.max(current - e.deltaY * 0.001, 0.3), 1) | ||
) | ||
e.preventDefault() | ||
}, []) | ||
|
||
useEffect(() => { | ||
surfaceRef.current?.addEventListener('mousedown', onMouseDown) | ||
surfaceRef.current?.addEventListener('wheel', onWheel) | ||
|
||
return () => { | ||
surfaceRef.current?.removeEventListener('mousedown', onMouseDown) | ||
surfaceRef.current?.removeEventListener('wheel', onWheel) | ||
} | ||
}, [onMouseDown, onWheel]) | ||
|
||
// Center on child re-size (which should only happen a couple of times the | ||
// first few renders) | ||
const onContentResize = useCallback( | ||
(entries: ResizeObserverEntry[], observer: ResizeObserver) => { | ||
onCenter() | ||
}, | ||
[onCenter] | ||
) | ||
|
||
useEffect(() => { | ||
const observer = new ResizeObserver(onContentResize) | ||
if (contentRef.current) { | ||
observer.observe(contentRef.current) | ||
} | ||
return () => { | ||
observer.disconnect() | ||
} | ||
}, [onContentResize]) | ||
|
||
return ( | ||
<div | ||
ref={surfaceRef} | ||
className={`relative w-full h-full overflow-hidden select-none ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`} | ||
> | ||
<div className="absolute left-0 bottom-0 m-2 rounded bg-white dark:bg-[#1e1e1e] flex flex-col p-2 z-50 shadow-md gap-y-2"> | ||
<button type="button" onClick={() => onZoom(1)}> | ||
<FluentAdd16Regular /> | ||
</button> | ||
<button type="button" onClick={() => onZoom(-1)}> | ||
<FluentSubtract16Regular /> | ||
</button> | ||
<button type="button" onClick={() => onCenter()}> | ||
<FluentFullScreenMaximize16Regular /> | ||
</button> | ||
</div> | ||
<div | ||
ref={contentRef} | ||
className="pointer-events-none w-fit h-fit" | ||
style={{ | ||
transform: `translate3d(${offset.x}px, ${offset.y}px, 0) scale(${scale}, ${scale})`, | ||
}} | ||
> | ||
{children} | ||
</div> | ||
</div> | ||
) | ||
} |
22 changes: 22 additions & 0 deletions
22
web/components/icons/fluent-full-screen-maximize-16-regular.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { SVGProps } from 'react' | ||
|
||
export function FluentFullScreenMaximize16Regular( | ||
props: SVGProps<SVGSVGElement> | ||
) { | ||
return ( | ||
<svg | ||
role="img" | ||
aria-label="icon" | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="16px" | ||
height="16px" | ||
viewBox="0 0 16 16" | ||
{...props} | ||
> | ||
<path | ||
fill="currentColor" | ||
d="M3.75 3a.75.75 0 0 0-.75.75V5.5a.5.5 0 0 1-1 0V3.75C2 2.784 2.784 2 3.75 2H5.5a.5.5 0 0 1 0 1zM10 2.5a.5.5 0 0 1 .5-.5h1.75c.966 0 1.75.784 1.75 1.75V5.5a.5.5 0 0 1-1 0V3.75a.75.75 0 0 0-.75-.75H10.5a.5.5 0 0 1-.5-.5M2.5 10a.5.5 0 0 1 .5.5v1.75c0 .414.336.75.75.75H5.5a.5.5 0 0 1 0 1H3.75A1.75 1.75 0 0 1 2 12.25V10.5a.5.5 0 0 1 .5-.5m11 0a.5.5 0 0 1 .5.5v1.75A1.75 1.75 0 0 1 12.25 14H10.5a.5.5 0 0 1 0-1h1.75a.75.75 0 0 0 .75-.75V10.5a.5.5 0 0 1 .5-.5" | ||
/> | ||
</svg> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import type { SVGProps } from 'react' | ||
|
||
export function FluentAdd16Regular(props: SVGProps<SVGSVGElement>) { | ||
return ( | ||
<svg | ||
role="img" | ||
aria-label="icon" | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="16px" | ||
height="16px" | ||
viewBox="0 0 16 16" | ||
{...props} | ||
> | ||
<path | ||
fill="currentColor" | ||
d="M8 2.5a.5.5 0 0 0-1 0V7H2.5a.5.5 0 0 0 0 1H7v4.5a.5.5 0 0 0 1 0V8h4.5a.5.5 0 0 0 0-1H8z" | ||
/> | ||
</svg> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import type { SVGProps } from 'react' | ||
|
||
export function FluentSubtract16Regular(props: SVGProps<SVGSVGElement>) { | ||
return ( | ||
<svg | ||
role="img" | ||
aria-label="icon" | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="16px" | ||
height="16px" | ||
viewBox="0 0 16 16" | ||
{...props} | ||
> | ||
<path | ||
fill="currentColor" | ||
d="M3 8a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9A.5.5 0 0 1 3 8" | ||
/> | ||
</svg> | ||
) | ||
} |
Oops, something went wrong.