UNPKG

schyma

Version:

JSON Schemas Visualizer React component

236 lines 13 kB
import { __awaiter } from "tslib"; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { SmartBezierEdge } from '@tisoap/react-flow-smart-edge'; import { ReactFlow, MiniMap, Controls, Background, useReactFlow, MarkerType, useNodesState, useEdgesState, addEdge, Position, ReactFlowProvider, ConnectionLineType, } from 'reactflow'; import { getCompositionType, propMerge, removeEdgesByParent, removeElementsByParent, resolveRef, } from '../utils/reusables'; import { CompositionType } from '../types'; import { getLayoutedElements } from '../utils/dagreLayout'; import SchemaNode from './CustomNode'; import { compositionEdgeColors, initialEdges, position } from '../constants/node'; // Define node and edge types outside component to prevent re-renders const edgeTypes = { smart: SmartBezierEdge, }; const nodeTypes = { schema: SchemaNode, }; function Flow({ initialNode, nNodes, setnNodes, setCurrentNode, schema, isPanelCollapsed }) { const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements([initialNode], initialEdges); const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges); const [hoveredCompositionNode, setHoveredCompositionNode] = useState(null); const { setCenter, getViewport, setViewport } = useReactFlow(); const onInit = useCallback(() => { setTimeout(() => { if (!isPanelCollapsed) { const viewport = getViewport(); const panelWidth = window.innerWidth * 0.45; const screenOffset = panelWidth / 2; setViewport({ x: viewport.x - screenOffset, y: viewport.y, zoom: viewport.zoom }, { duration: 200 }); } }, 100); }, [isPanelCollapsed, getViewport, setViewport]); const styleCompositionEdges = useMemo(() => { // Skip styling for allOf: (since we are flattening allOf by default, there's no reason to style it) - all properties are just regular required properties if (!hoveredCompositionNode || hoveredCompositionNode.compositionType === CompositionType.AllOf) { return edges; } return edges.map((edge) => { var _a; if (edge.source === hoveredCompositionNode.nodeId) { const targetNode = nodes.find((n) => n.id === edge.target); const targetCompositionSource = (_a = targetNode === null || targetNode === void 0 ? void 0 : targetNode.data) === null || _a === void 0 ? void 0 : _a.compositionSource; if (targetCompositionSource === hoveredCompositionNode.compositionType) { return Object.assign(Object.assign({}, edge), { style: { stroke: compositionEdgeColors[hoveredCompositionNode.compositionType], strokeWidth: 2, }, animated: true }); } } return edge; }); }, [edges, hoveredCompositionNode, nodes]); const onConnect = useCallback((connection) => setEdges((eds) => addEdge(Object.assign(Object.assign({}, connection), { type: ConnectionLineType.SmoothStep, animated: true }), eds)), // eslint-disable-next-line react-hooks/exhaustive-deps []); const extractChildren = (props, parent) => __awaiter(this, void 0, void 0, function* () { const children = []; for (const prop in props) { const id = String(Math.floor(Math.random() * 1000000)); const propData = props[prop]; const compositionSource = propData._compositionSource; const directComposition = getCompositionType(propData); if (propData.$ref) { const res = yield resolveRef(propData.$ref, schema); children.push({ id, type: directComposition ? 'schema' : 'default', data: Object.assign(Object.assign(Object.assign(Object.assign({}, propData), { label: prop, parent: parent.id, relations: Object.assign(Object.assign({}, parent.relations), { [parent.id]: 'node' }) }), res), { children: [], compositionType: directComposition, compositionSource }), position: position, sourcePosition: Position.Right, targetPosition: Position.Left, }); } else { children.push({ id, type: directComposition ? 'schema' : 'default', data: Object.assign(Object.assign({}, propData), { label: prop, id, parent: parent.id, relations: Object.assign(Object.assign({}, parent.relations), { [parent.id]: 'node' }), children: [], compositionType: directComposition, compositionSource }), position: position, sourcePosition: Position.Right, targetPosition: Position.Left, }); } } return children; }); const fetchInitialChildren = () => __awaiter(this, void 0, void 0, function* () { const newNodes = []; const properties = initialNode.data.properties; const children = yield extractChildren(properties, initialNode); const nodeType = initialNode.data.compositionType ? 'schema' : 'input'; newNodes.push({ id: initialNode.id, type: nodeType, data: { children, label: initialNode.data.label, description: initialNode.data.description, properties: initialNode.data.properties, relations: initialNode.data.relations, compositionType: initialNode.data.compositionType, isRoot: true, }, position: { x: 0, y: 0 }, sourcePosition: Position.Right, targetPosition: Position.Left, }); setNodes(newNodes); }); useEffect(() => { fetchInitialChildren(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Adjust viewport when panel collapses/expands useEffect(() => { const viewport = getViewport(); const panelWidth = window.innerWidth * 0.45; const screenOffset = panelWidth / 2; if (isPanelCollapsed) { setViewport({ x: viewport.x + screenOffset, y: viewport.y, zoom: viewport.zoom }, { duration: 200 }); } else { setViewport({ x: viewport.x - screenOffset, y: viewport.y, zoom: viewport.zoom }, { duration: 200 }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPanelCollapsed]); const focusNode = (children, zoom) => { if (children.length === 0) return; let middleChild = children[Math.floor(children.length / 2)]; const middleChildWithLatestPosition = nodes.filter((a) => a.id == middleChild.id)[0]; if (middleChildWithLatestPosition) { middleChild = middleChildWithLatestPosition; } let targetX = middleChild.position.x; if (!isPanelCollapsed) { const panelWidth = window.innerWidth * 0.45; const offsetInFlowCoords = panelWidth / 2 / zoom; targetX = targetX + offsetInFlowCoords; } setCenter(targetX, middleChild.position.y, { zoom, duration: 1000 }); }; const nodeClick = (_event, node) => __awaiter(this, void 0, void 0, function* () { const findChildren = nodes.filter((item) => { var _a; return ((_a = item === null || item === void 0 ? void 0 : item.data) === null || _a === void 0 ? void 0 : _a.parent) === node.id; }); if (!findChildren.length) { const itemChildren = node.data.children; const newEdges = [ ...edges, ...itemChildren.map((item) => { var _a; return { id: String(Math.floor(Math.random() * 1000000)), source: (_a = item === null || item === void 0 ? void 0 : item.data) === null || _a === void 0 ? void 0 : _a.parent, target: item === null || item === void 0 ? void 0 : item.id, markerEnd: { type: MarkerType.ArrowClosed, }, }; }), ]; //TODO: Fix nodes type error const newNodes = nodes.concat(itemChildren); const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(newNodes, newEdges, 'LR'); setNodes([...layoutedNodes]); setEdges([...layoutedEdges]); if (itemChildren.length > 0) { focusNode(itemChildren, 0.9); } } else { const newNodes = removeElementsByParent(nodes, node.id); const newEdges = removeEdgesByParent(edges, node.id); const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(newNodes, newEdges, 'LR'); setNodes([...layoutedNodes]); setEdges([...layoutedEdges]); focusNode([node], 0.9); } }); function handleMouseEnter(_e, node) { return __awaiter(this, void 0, void 0, function* () { if (!nNodes[node.id]) { const itemChildren = []; const nodeChildren = node.data.children; yield Promise.all(nodeChildren.map((item) => __awaiter(this, void 0, void 0, function* () { let children = []; const label = item.data.label; const extractProps = propMerge(item.data, label); const nestedComposition = extractProps._nestedComposition; delete extractProps._nestedComposition; if (Object.keys(extractProps).length > 0) { const res = yield extractChildren(extractProps, item); children = res; } const relations = Object.assign(Object.assign({}, node.data.relations), item.data.relations); // Check for direct composition or nested composition (from items/additionalProperties) const directComposition = getCompositionType(item.data); const compositionType = directComposition || nestedComposition || null; // Get composition source tag if this child came from a composition const compositionSource = item.data._compositionSource; // Use custom schema node type if node has composition, otherwise use default types const nodeType = compositionType ? 'schema' : (children === null || children === void 0 ? void 0 : children.length) > 0 ? 'default' : 'output'; itemChildren.push({ id: item.id, type: nodeType, data: Object.assign(Object.assign({}, item.data), { label: `${item.data.label}`, children: children, relations: relations, compositionType, compositionSource }), position: position, sourcePosition: Position.Right, targetPosition: Position.Left, }); }))); node.data.children = itemChildren; nNodes[node.id] = node; setnNodes(nNodes); } const nodeData = node.data; if (nodeData.compositionType) { setHoveredCompositionNode({ nodeId: node.id, compositionType: nodeData.compositionType, }); } setCurrentNode(node); }); } function handleMouseLeave() { setHoveredCompositionNode(null); } return (React.createElement(ReactFlow, { nodes: nodes, edges: styleCompositionEdges, edgeTypes: edgeTypes, nodeTypes: nodeTypes, onNodesChange: onNodesChange, connectionLineType: ConnectionLineType.SmoothStep, onEdgesChange: onEdgesChange, onConnect: onConnect, onNodeMouseEnter: handleMouseEnter, onNodeMouseLeave: handleMouseLeave, onNodeClick: nodeClick, onInit: onInit, fitView: true, defaultViewport: { x: 1, y: 1, zoom: 0.9 } }, React.createElement(MiniMap, null), React.createElement(Controls, null), React.createElement(Background, null))); } export default ({ setCurrentNode, setnNodes, nNodes, initialNode, schema, isPanelCollapsed }) => (React.createElement(ReactFlowProvider, null, React.createElement(Flow, { setnNodes: setnNodes, nNodes: nNodes, setCurrentNode: setCurrentNode, initialNode: initialNode, schema: schema, isPanelCollapsed: isPanelCollapsed }))); //# sourceMappingURL=Nodes.js.map