schyma
Version:
JSON Schemas Visualizer React component
236 lines • 13 kB
JavaScript
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