diff --git a/.gitignore b/.gitignore index f55eeef..b33a96a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules/ /.idea /wwwroot +/.playwright-cli/ diff --git a/index.html b/index.html index a4eca61..3de5e39 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,7 @@ + ECAP diff --git a/package-lock.json b/package-lock.json index aad06e1..7d9143c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "@ant-design/icons": "^6.1.0", - "@antv/g6": "^5.1.1", + "@dagrejs/dagre": "^3.1.1", + "@xyflow/react": "^12.11.2", "antd": "^5.29.1", "axios": "^1.8.4", "cesium": "^1.143.0", @@ -154,241 +155,6 @@ "react": ">=16.9.0" } }, - "node_modules/@antv/algorithm": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@antv/algorithm/-/algorithm-0.1.26.tgz", - "integrity": "sha512-DVhcFSQ8YQnMNW34Mk8BSsfc61iC1sAnmcfYoXTAshYHuU50p/6b7x3QYaGctDNKWGvi1ub7mPcSY0bK+aN0qg==", - "license": "MIT", - "dependencies": { - "@antv/util": "^2.0.13", - "tslib": "^2.0.0" - } - }, - "node_modules/@antv/algorithm/node_modules/@antv/util": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz", - "integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==", - "license": "ISC", - "dependencies": { - "csstype": "^3.0.8", - "tslib": "^2.0.3" - } - }, - "node_modules/@antv/component": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.11.tgz", - "integrity": "sha512-dTdz8VAd3rpjOaGEZTluz82mtzrP4XCtNlNQyrxY7VNRNcjtvpTLDn57bUL2lRu1T+iklKvgbE2llMriWkq9vQ==", - "license": "MIT", - "dependencies": { - "@antv/g": "^6.1.11", - "@antv/scale": "^0.4.16", - "@antv/util": "^3.3.10", - "svg-path-parser": "^1.1.0" - } - }, - "node_modules/@antv/event-emitter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@antv/event-emitter/-/event-emitter-0.1.3.tgz", - "integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==", - "license": "MIT" - }, - "node_modules/@antv/expr": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@antv/expr/-/expr-1.0.2.tgz", - "integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==", - "license": "MIT" - }, - "node_modules/@antv/g": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@antv/g/-/g-6.3.1.tgz", - "integrity": "sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==", - "license": "MIT", - "dependencies": { - "@antv/g-lite": "2.7.0", - "@antv/util": "^3.3.5", - "@babel/runtime": "^7.25.6", - "gl-matrix": "^3.4.3", - "html2canvas": "^1.4.1" - } - }, - "node_modules/@antv/g-canvas": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@antv/g-canvas/-/g-canvas-2.2.0.tgz", - "integrity": "sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==", - "license": "MIT", - "dependencies": { - "@antv/g-lite": "2.7.0", - "@antv/g-math": "3.1.0", - "@antv/util": "^3.3.5", - "@babel/runtime": "^7.25.6", - "gl-matrix": "^3.4.3", - "tslib": "^2.5.3" - } - }, - "node_modules/@antv/g-lite": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@antv/g-lite/-/g-lite-2.7.0.tgz", - "integrity": "sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==", - "license": "MIT", - "dependencies": { - "@antv/g-math": "3.1.0", - "@antv/util": "^3.3.5", - "@antv/vendor": "^1.0.3", - "@babel/runtime": "^7.25.6", - "eventemitter3": "^5.0.1", - "gl-matrix": "^3.4.3", - "tslib": "^2.5.3" - } - }, - "node_modules/@antv/g-math": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@antv/g-math/-/g-math-3.1.0.tgz", - "integrity": "sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==", - "license": "MIT", - "dependencies": { - "@antv/util": "^3.3.5", - "@babel/runtime": "^7.25.6", - "gl-matrix": "^3.4.3", - "tslib": "^2.5.3" - } - }, - "node_modules/@antv/g-plugin-dragndrop": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@antv/g-plugin-dragndrop/-/g-plugin-dragndrop-2.1.1.tgz", - "integrity": "sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==", - "license": "MIT", - "dependencies": { - "@antv/g-lite": "2.7.0", - "@antv/util": "^3.3.5", - "@babel/runtime": "^7.25.6", - "tslib": "^2.5.3" - } - }, - "node_modules/@antv/g6": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@antv/g6/-/g6-5.1.1.tgz", - "integrity": "sha512-50bXxMUf4mChyOv4ePVeWZLwotih9VunKfp0a++Wofv/wCyY8fb9+CV2wouIBCOZnd5ydBRA4NNaX9yLJzqa2w==", - "license": "MIT", - "dependencies": { - "@antv/algorithm": "^0.1.26", - "@antv/component": "^2.1.7", - "@antv/event-emitter": "^0.1.3", - "@antv/g": "^6.1.28", - "@antv/g-canvas": "^2.0.48", - "@antv/g-plugin-dragndrop": "^2.0.38", - "@antv/graphlib": "^2.0.4", - "@antv/hierarchy": "^0.7.1", - "@antv/layout": "^2.0.0", - "@antv/util": "^3.3.11", - "bubblesets-js": "^2.3.4" - } - }, - "node_modules/@antv/graphlib": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@antv/graphlib/-/graphlib-2.0.4.tgz", - "integrity": "sha512-zc/5oQlsdk42Z0ib1mGklwzhJ5vczLFiPa1v7DgJkTbgJ2YxRh9xdarf86zI49sKVJmgbweRpJs7Nu5bIiwv4w==", - "license": "MIT", - "dependencies": { - "@antv/event-emitter": "^0.1.3" - } - }, - "node_modules/@antv/hierarchy": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@antv/hierarchy/-/hierarchy-0.7.1.tgz", - "integrity": "sha512-7r22r+HxfcRZp79ZjGmsn97zgC1Iajrv0Mm9DIgx3lPfk+Kme2MG/+EKdZj1iEBsN0rJRzjWVPGL5YrBdVHchw==", - "license": "MIT" - }, - "node_modules/@antv/layout": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@antv/layout/-/layout-2.0.0.tgz", - "integrity": "sha512-aCZ3UdNc40SfT7meFV7QTADY2HCnc0DShVw56CJNTI6oExUIVU736grPuL5Dhb8/JrVaU4Y83QPN/P7KafBzlw==", - "license": "MIT", - "dependencies": { - "@antv/event-emitter": "^0.1.3", - "@antv/expr": "^1.0.2", - "@antv/graphlib": "^2.0.0", - "@antv/util": "^3.3.2", - "comlink": "^4.4.1", - "d3-force": "^3.0.0", - "d3-force-3d": "^3.0.5", - "d3-octree": "^1.0.2", - "d3-quadtree": "^3.0.1", - "dagre": "^0.8.5", - "ml-matrix": "^6.10.4", - "tslib": "^2.8.1" - } - }, - "node_modules/@antv/scale": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz", - "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==", - "license": "MIT", - "dependencies": { - "@antv/util": "^3.3.7", - "color-string": "^1.5.5", - "fecha": "^4.2.1" - } - }, - "node_modules/@antv/util": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz", - "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "gl-matrix": "^3.3.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@antv/vendor": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@antv/vendor/-/vendor-1.0.11.tgz", - "integrity": "sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.2.1", - "@types/d3-color": "^3.1.3", - "@types/d3-dispatch": "^3.0.6", - "@types/d3-dsv": "^3.0.7", - "@types/d3-ease": "^3.0.2", - "@types/d3-fetch": "^3.0.7", - "@types/d3-force": "^3.0.10", - "@types/d3-format": "^3.0.4", - "@types/d3-geo": "^3.1.0", - "@types/d3-hierarchy": "^3.1.7", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-path": "^3.1.0", - "@types/d3-quadtree": "^3.0.6", - "@types/d3-random": "^3.0.3", - "@types/d3-scale": "^4.0.9", - "@types/d3-scale-chromatic": "^3.1.0", - "@types/d3-shape": "^3.1.7", - "@types/d3-time": "^3.0.4", - "@types/d3-timer": "^3.0.2", - "d3-array": "^3.2.4", - "d3-color": "^3.1.0", - "d3-dispatch": "^3.0.1", - "d3-dsv": "^3.0.1", - "d3-ease": "^3.0.1", - "d3-fetch": "^3.0.1", - "d3-force": "^3.0.0", - "d3-force-3d": "^3.0.5", - "d3-format": "^3.1.0", - "d3-geo": "^3.1.1", - "d3-geo-projection": "^4.0.0", - "d3-hierarchy": "^3.1.2", - "d3-interpolate": "^3.0.1", - "d3-path": "^3.1.0", - "d3-quadtree": "^3.0.1", - "d3-random": "^3.0.1", - "d3-regression": "^1.3.10", - "d3-scale": "^4.0.2", - "d3-scale-chromatic": "^3.1.0", - "d3-shape": "^3.2.0", - "d3-time": "^3.1.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -739,6 +505,21 @@ "node": ">=22.0.0" } }, + "node_modules/@dagrejs/dagre": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-3.1.1.tgz", + "integrity": "sha512-zroZB1dFOFiGgv4Xcrn1DckB1o4aOikPqD2NDQPV0WM//CXGcS6xiD0rNkqHmw6FEg4tabt4nxPLwgCWT+Vb2A==", + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "4.0.5" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-4.0.5.tgz", + "integrity": "sha512-7xrBTqIts3o+PMUZX97wSc+7TUbW+/rULzGNCTP6yooNVDXbzw4Wutg/H/xOutTB/c/k0YqOAavgPh4/Zk9PFA==", + "license": "MIT" + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -1955,72 +1736,21 @@ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, - "node_modules/@types/d3-dispatch": { + "node_modules/@types/d3-drag": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", - "license": "MIT" - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", "dependencies": { - "@types/d3-dsv": "*" + "@types/d3-selection": "*" } }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT" - }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", @@ -2030,60 +1760,31 @@ "@types/d3-color": "*" } }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT" }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", - "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "license": "MIT", "dependencies": { - "@types/d3-time": "*" + "@types/d3-selection": "*" } }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "license": "MIT", "dependencies": { - "@types/d3-path": "*" + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" } }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2093,7 +1794,8 @@ "node_modules/@types/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -2115,14 +1817,14 @@ "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2133,7 +1835,7 @@ "version": "18.3.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -2461,6 +2163,48 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@xyflow/react": { + "version": "12.11.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz", + "integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.79", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.79", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz", + "integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/@zip.js/zip.js": { "version": "2.8.34", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.34.tgz", @@ -2654,15 +2398,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/bitmap-sdf": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", @@ -2713,12 +2448,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bubblesets-js": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/bubblesets-js/-/bubblesets-js-2.3.4.tgz", - "integrity": "sha512-DyMjHmpkS2+xcFNtyN00apJYL3ESdp9fTrkDr5+9Qg/GPqFmcWgGsK1akZnttE1XFxJ/VMy4DNNGMGYtmFp1Sg==", - "license": "MIT" - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2799,6 +2528,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -2831,18 +2566,9 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2855,12 +2581,6 @@ "node": ">= 0.8" } }, - "node_modules/comlink": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", - "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", - "license": "Apache-2.0" - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2917,39 +2637,12 @@ "node": ">= 8" } }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-binarytree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", - "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", - "license": "MIT" - }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -2968,40 +2661,19 @@ "node": ">=12" } }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "license": "ISC", "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" + "d3-dispatch": "1 - 3", + "d3-selection": "3" }, "engines": { "node": ">=12" } }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -3011,108 +2683,6 @@ "node": ">=12" } }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force-3d": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", - "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", - "license": "MIT", - "dependencies": { - "d3-binarytree": "1", - "d3-dispatch": "1 - 3", - "d3-octree": "1", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo-projection": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", - "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", - "license": "ISC", - "dependencies": { - "commander": "7", - "d3-array": "1 - 3", - "d3-geo": "1.12.0 - 3" - }, - "bin": { - "geo2svg": "bin/geo2svg.js", - "geograticule": "bin/geograticule.js", - "geoproject": "bin/geoproject.js", - "geoquantize": "bin/geoquantize.js", - "geostitch": "bin/geostitch.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo-projection/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -3125,110 +2695,15 @@ "node": ">=12" } }, - "node_modules/d3-octree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", - "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", - "license": "MIT" - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-regression": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/d3-regression/-/d3-regression-1.3.10.tgz", - "integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==", - "license": "BSD-3-Clause" - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", @@ -3238,14 +2713,39 @@ "node": ">=12" } }, - "node_modules/dagre": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", - "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", - "license": "MIT", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { - "graphlib": "^2.1.8", - "lodash": "^4.17.15" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/dayjs": { @@ -3610,16 +3110,11 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { @@ -3653,12 +3148,6 @@ } } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3815,12 +3304,6 @@ "node": ">= 0.4" } }, - "node_modules/gl-matrix": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", - "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", - "license": "MIT" - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3865,15 +3348,6 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "license": "MIT" }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3923,31 +3397,6 @@ "node": ">= 0.4" } }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "license": "MIT", - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3985,27 +3434,6 @@ "node": ">=0.8.19" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-any-array": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz", - "integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==", - "license": "MIT" - }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "license": "MIT" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4189,12 +3617,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4284,45 +3706,6 @@ "node": "*" } }, - "node_modules/ml-array-max": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-2.0.0.tgz", - "integrity": "sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==", - "license": "MIT", - "dependencies": { - "is-any-array": "^3.0.0" - } - }, - "node_modules/ml-array-min": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-2.0.0.tgz", - "integrity": "sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==", - "license": "MIT", - "dependencies": { - "is-any-array": "^3.0.0" - } - }, - "node_modules/ml-array-rescale": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-2.0.0.tgz", - "integrity": "sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==", - "license": "MIT", - "dependencies": { - "is-any-array": "^3.0.0", - "ml-array-max": "^2.0.0", - "ml-array-min": "^2.0.0" - } - }, - "node_modules/ml-matrix": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.15.0.tgz", - "integrity": "sha512-wFa1v6KP8bKp+fj0nYmRs1Pb5K4zRkXGKsOvLinvILENFIADncm4XlOI+S1M7yuACMGfI6cfk0IifDgd4j5xmw==", - "license": "MIT", - "dependencies": { - "is-any-array": "^3.0.0", - "ml-array-rescale": "^2.0.0" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5278,18 +4661,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -5344,15 +4715,6 @@ "node": ">=8" } }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5398,21 +4760,6 @@ "node": ">=8" } }, - "node_modules/svg-path-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz", - "integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==", - "license": "MIT" - }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -5578,13 +4925,13 @@ "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", "license": "MIT" }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "dependencies": { - "base64-arraybuffer": "^1.0.2" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/vite": { @@ -5706,6 +5053,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 2c65711..ce9fc38 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ }, "dependencies": { "@ant-design/icons": "^6.1.0", - "@antv/g6": "^5.1.1", + "@dagrejs/dagre": "^3.1.1", + "@xyflow/react": "^12.11.2", "antd": "^5.29.1", "axios": "^1.8.4", "cesium": "^1.143.0", diff --git a/src/Adminive/AmisPanel.tsx b/src/Adminive/AmisPanel.tsx index 73dc8a8..248dfc0 100644 --- a/src/Adminive/AmisPanel.tsx +++ b/src/Adminive/AmisPanel.tsx @@ -1,10 +1,12 @@ import {useEffect, useRef, useState} from "react" import {amis_fetcher} from "./amis_fetcher.ts" -import {embed_amis} from "./amis_runtime.ts" +import {embed_amis, type Amis_Fetcher} from "./amis_runtime.ts" type Amis_Panel_Props = { schema: Record + data?: Record + fetcher?: Amis_Fetcher } -export function AmisPanel({schema}: Amis_Panel_Props) { +export function AmisPanel({schema, data, fetcher = amis_fetcher}: Amis_Panel_Props) { const container = useRef(null) const [error, set_error] = useState("") useEffect(() => { @@ -13,11 +15,11 @@ export function AmisPanel({schema}: Amis_Panel_Props) { } set_error("") try { - const scoped = embed_amis(container.current, schema, amis_fetcher) + const scoped = embed_amis(container.current, schema, fetcher, data) return () => scoped.unmount() } catch (reason) { set_error(reason instanceof Error ? reason.message : String(reason)) } - }, [schema]) + }, [data, fetcher, schema]) return error ?
{error}
:
} diff --git a/src/Adminive/DataTopology.tsx b/src/Adminive/DataTopology.tsx index ecf4127..36e7ddb 100644 --- a/src/Adminive/DataTopology.tsx +++ b/src/Adminive/DataTopology.tsx @@ -1,9 +1,17 @@ -import {CanvasEvent, Graph, GraphEvent, NodeEvent} from "@antv/g6" -import {Alert, Button, Card, Collapse, Descriptions, Drawer, Empty, Space, Spin, Tag, Typography} from "antd" +import {CaretDownOutlined, CaretRightOutlined, DeleteOutlined, DisconnectOutlined, EditOutlined, LinkOutlined, PlusOutlined, SaveOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons" +import dagre from "@dagrejs/dagre" +import {Background, BackgroundVariant, BaseEdge, Controls, EdgeLabelRenderer, Handle, MarkerType, Position, ReactFlow, applyNodeChanges, getSmoothStepPath, type Connection, type Edge, type EdgeMouseHandler, type EdgeProps, type Node, type NodeMouseHandler, type NodeProps, type OnNodesChange, type ReactFlowInstance} from "@xyflow/react" +import "@xyflow/react/dist/style.css" +import {Alert, Button, Card, Menu, type MenuProps, Modal, Select, Space, Spin, Tag, Typography, message} from "antd" import axios from "axios" -import {useCallback, useEffect, useMemo, useRef, useState} from "react" +import {useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as React_Mouse_Event, type MutableRefObject} from "react" import {AmisPanel} from "./AmisPanel.tsx" - +import {amis_fetcher} from "./amis_fetcher.ts" +import type {Amis_Fetcher, Amis_Request} from "./amis_runtime.ts" +type Topology_Detail = { + label: string + value: unknown +} type Topology_Node = { id: string kind: string @@ -12,26 +20,29 @@ type Topology_Node = { label: string type: string enabled: boolean + config?: Record + details: Topology_Detail[] status_api?: string } - type Topology_Edge = { id: string + kind: "configuration" | "runtime" source: string target: string active: boolean + details?: Topology_Detail[] + status_api?: string } - type Topology_View = { protocol: "ecap.data-topology-view" protocol_version: 1 initialized: boolean + layout_revision: number zoom: number pan_x: number pan_y: number node_positions: Record } - type Topology_Data = { protocol: "ecap.data-topology" protocol_version: 1 @@ -39,21 +50,58 @@ type Topology_Data = { edges: Topology_Edge[] view: Topology_View } - type Node_Kind = { kind: string title: string fill: string stroke: string } - type Topology_Editor = { kind: string title: string - button_label: string - schema: Record + create_data: Record + create_schema: Record + edit_schema: Record } - +type Editor_State = { + kind: string + mode: "create" | "edit" + node_id?: string +} +type Context_Menu_State = { + x: number + y: number + target: "canvas" | "node" | "edge" + id?: string +} +type Connection_State = { + feed_id?: string +} +type Topology_Status_Config = { + title: string + poll_interval_ms: number + collapsed_hint: string +} +type Context_Event = { + clientX: number + clientY: number + preventDefault(): void + stopPropagation(): void +} +type Flow_Node_Data = { + topology_node: Topology_Node + kind?: Node_Kind + status: Topology_Status_Config + status_expansion: MutableRefObject> +} +type Flow_Node = Node +type Flow_Edge_Data = { + topology_edge: Topology_Edge + status: Topology_Status_Config + status_expansion: MutableRefObject> + on_context_menu(event: React_Mouse_Event): void +} +type Flow_Edge = Edge export type Data_Topology_Schema = { title: string data_api: string @@ -61,279 +109,681 @@ export type Data_Topology_Schema = { graph: { height: number layout: Record + layout_revision: number + poll_interval_ms: number behaviors: string[] active_edge_color: string inactive_edge_color: string } node_kinds: Node_Kind[] - status: { - title: string - poll_interval_ms: number - collapsed_hint: string - } + status: Topology_Status_Config editors: Topology_Editor[] } - type Adminive_Response = { status: number msg: string data: T } - const topology_changed_event = "ecap-data-topology-changed" - -function status_text(value: unknown): string { - if (value === null) return "null" - if (typeof value === "object") return JSON.stringify(value, null, 2) - return String(value) +const data_sources_api = "/api/adminive/config/data_sources" +const data_feeds_api = "/api/adminive/config/data_feeds" +const draft_submit_api = "ecap://data-topology/draft" +const node_schema = { + type: "container", + className: "p-0", + body: [{type: "tpl", tpl: "
${label}
${type}
"}, {type: "each", name: "details", items: {type: "tpl", tpl: "
${label}:${value}
"}}] } - -function Node_Status({node, schema}: {node: Topology_Node, schema: Data_Topology_Schema["status"]}) { - const [expanded, set_expanded] = useState(false) +function clone_data(value: T): T { + return structuredClone(value) +} +function context_position(event: Context_Event, element: HTMLElement): {x: number, y: number} { + event.preventDefault() + event.stopPropagation() + const bounds = element.getBoundingClientRect() + return {x: Math.max(8, Math.min(event.clientX - bounds.left, element.clientWidth - 218)), y: Math.max(8, Math.min(event.clientY - bounds.top, element.clientHeight - 178))} +} +function node_size(node: Topology_Node): [number, number] { + return node.kind === "client" ? [246, 122] : [282, 166] +} +function node_enabled(config: Record): boolean { + return Boolean(config.enable ?? config.enabled ?? true) +} +function node_details(kind: string, config: Record): Topology_Detail[] { + const details: Topology_Detail[] = [{label: "配置状态", value: "待保存"}] + if(kind === "data_source") { + if(config.ip !== undefined || config.port !== undefined) + details.push({label: "目标", value: `${String(config.ip ?? "")}:${String(config.port ?? "")}`}) + else if(config.port_name !== undefined) + details.push({label: "串口", value: config.port_name}) + else if(config.file_path !== undefined) + details.push({label: "文件", value: config.file_path}) + else if(config.library_path !== undefined) + details.push({label: "动态库", value: config.library_path}) + else if(config.shared_memory_name !== undefined) + details.push({label: "共享内存", value: config.shared_memory_name}) + } else { + const output_format = config.output_format as Record | undefined + details.push({label: "数据格式", value: output_format?.type ?? config.format ?? ""}) + if(config.port !== undefined) + details.push({label: String(config.type).includes("Server") ? "监听端口" : "目标", value: String(config.type).includes("Server") ? config.port : `${String(config.url)}:${String(config.port)}`}) + } + return details +} +function draft_signature(data: Topology_Data | null): string { + if(!data) + return "" + return JSON.stringify({nodes: data.nodes.filter(node => node.kind !== "client").map(node => ({id: node.id, kind: node.kind, entity_id: node.entity_id, config: node.config})).sort((left, right) => left.id.localeCompare(right.id)), edges: data.edges.filter(edge => edge.kind === "configuration").map(edge => ({source: edge.source, target: edge.target})).sort((left, right) => left.target.localeCompare(right.target))}) +} +function changed_config(previous: Record | undefined, next: Record | undefined): Record { + const changes: Record = {} + for(const [key, value] of Object.entries(next ?? {})) { + if(JSON.stringify(previous?.[key]) !== JSON.stringify(value)) + changes[key] = value + } + return changes +} +function runtime_signature(data: Topology_Data): string { + return JSON.stringify({nodes: data.nodes.map(node => node.kind === "client" ? node : {id: node.id, enabled: node.enabled, details: node.details, status_api: node.status_api}).sort((left, right) => left.id.localeCompare(right.id)), edges: data.edges.map(edge => edge.kind === "runtime" ? edge : {id: edge.id, active: edge.active, details: edge.details, status_api: edge.status_api}).sort((left, right) => left.id.localeCompare(right.id))}) +} +function merge_runtime(current: Topology_Data, incoming: Topology_Data): Topology_Data { + const configured_ids = new Set(current.nodes.filter(node => node.kind !== "client").map(node => node.id)) + const incoming_nodes = new Map(incoming.nodes.map(node => [node.id, node])) + const incoming_edges = new Map(incoming.edges.filter(edge => edge.kind === "configuration").map(edge => [edge.id, edge])) + const nodes = current.nodes.filter(node => node.kind !== "client").map(node => { + const status = incoming_nodes.get(node.id) + return status ? {...node, enabled: status.enabled, details: status.details, status_api: status.status_api} : node + }) + const edges = current.edges.filter(edge => edge.kind !== "runtime").map(edge => { + const status = incoming_edges.get(edge.id) + return status && status.source === edge.source && status.target === edge.target ? {...edge, active: status.active, details: status.details, status_api: status.status_api} : edge + }) + nodes.push(...incoming.nodes.filter(node => node.kind === "client" && configured_ids.has(incoming.edges.find(edge => edge.kind === "runtime" && edge.target === node.id)?.source ?? ""))) + edges.push(...incoming.edges.filter(edge => edge.kind === "runtime" && configured_ids.has(edge.source))) + return {...current, nodes, edges} +} +function strip_editor_field(value: unknown, field_name: string): unknown { + if(Array.isArray(value)) + return value.map(item => strip_editor_field(item, field_name)).filter(item => item !== undefined) + if(!value || typeof value !== "object") + return value + const object = value as Record + if(object.name === field_name) + return undefined + const result: Record = {} + for(const [key, child] of Object.entries(object)) + result[key] = strip_editor_field(child, field_name) + return result +} +function draft_editor_schema(editor: Topology_Editor, mode: Editor_State["mode"]): Record { + const source = mode === "create" ? editor.create_schema : editor.edit_schema + const schema = (editor.kind === "data_feed" ? strip_editor_field(clone_data(source), "source_key") : clone_data(source)) as Record + delete schema.initApi + delete schema.reload + schema.actions = [] + schema.api = {method: "post", url: draft_submit_api} + return schema +} +function topology_layout(data: Topology_Data, layout: Record): Map { + const graph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})) + graph.setGraph({rankdir: "TB", align: "UL", nodesep: Number(layout.nodesep ?? 52), ranksep: Number(layout.ranksep ?? 96), marginx: 32, marginy: 32}) + for(const node of data.nodes) { + const [width, height] = node_size(node) + graph.setNode(node.id, {width, height}) + } + for(const edge of data.edges) + graph.setEdge(edge.source, edge.target) + dagre.layout(graph) + return new Map(data.nodes.map(node => { + const position = graph.node(node.id) + const [width, height] = node_size(node) + return [node.id, {x: position.x - width / 2, y: position.y - height / 2}] + })) +} +function canonical_view(view: Topology_View, nodes: Topology_Node[]): Topology_View { + const node_positions: Topology_View["node_positions"] = {} + for(const node of nodes) { + if(node.kind === "client") + continue + const position = view.node_positions[node.id] + if(position) + node_positions[`${node.kind}:${node.key}`] = position + } + return {...view, node_positions} +} +function capture_flow_view(instance: ReactFlowInstance, data: Topology_Data, layout_revision: number): Topology_View { + const node_positions: Topology_View["node_positions"] = {} + for(const node of instance.getNodes()) { + if(node.data.topology_node.kind !== "client") + node_positions[node.id] = {x: node.position.x, y: node.position.y} + } + const viewport = instance.getViewport() + return {protocol: "ecap.data-topology-view", protocol_version: 1, initialized: true, layout_revision, zoom: viewport.zoom, pan_x: viewport.x, pan_y: viewport.y, node_positions} +} +function create_flow_nodes(data: Topology_Data, view: Topology_View, layout: Record, layout_revision: number, kinds: Map, status: Topology_Status_Config, status_expansion: MutableRefObject>): Flow_Node[] { + const arranged = topology_layout(data, layout) + const restore = view.initialized && view.layout_revision === layout_revision + const positions = new Map() + for(const node of data.nodes.filter(node => node.kind !== "client")) + positions.set(node.id, restore && view.node_positions[node.id] ? view.node_positions[node.id] : arranged.get(node.id)!) + const client_count = new Map() + for(const node of data.nodes.filter(node => node.kind === "client")) { + const edge = data.edges.find(item => item.kind === "runtime" && item.target === node.id)! + const source = positions.get(edge.source)! + const source_node = data.nodes.find(item => item.id === edge.source)! + const index = client_count.get(edge.source) ?? 0 + client_count.set(edge.source, index + 1) + positions.set(node.id, {x: source.x + index * 270, y: source.y + node_size(source_node)[1] + 92}) + } + return data.nodes.map(node => { + const [width] = node_size(node) + return {id: node.id, type: "topology", position: positions.get(node.id)!, draggable: node.kind !== "client", selectable: true, sourcePosition: Position.Bottom, targetPosition: Position.Top, style: {width}, data: {topology_node: node, kind: kinds.get(node.kind), status, status_expansion}} + }) +} +function create_flow_edges(data: Topology_Data, schema: Data_Topology_Schema, status_expansion: MutableRefObject>, on_context_menu: (event: React_Mouse_Event, edge_id: string) => void): Flow_Edge[] { + return data.edges.map(edge => { + const color = edge.kind === "runtime" ? "#fa8c16" : edge.active ? schema.graph.active_edge_color : schema.graph.inactive_edge_color + return {id: edge.id, source: edge.source, target: edge.target, type: "topology", animated: edge.kind === "runtime", selectable: true, interactionWidth: 24, style: {stroke: color, strokeWidth: edge.active ? 2.5 : 1.5, strokeDasharray: edge.active ? undefined : "6 5", opacity: edge.active ? 1 : 0.62}, markerEnd: {type: MarkerType.ArrowClosed, color}, data: {topology_edge: edge, status: schema.status, status_expansion, on_context_menu: event => on_context_menu(event, edge.id)}} + }) +} +function merge_flow_nodes(current: Flow_Node[], incoming: Flow_Node[], replace: boolean): Flow_Node[] { + if(replace) + return incoming + const current_by_id = new Map(current.map(node => [node.id, node])) + return incoming.map(node => { + const previous = current_by_id.get(node.id) + if(!previous) + return node + const position = node.data.topology_node.kind === "client" ? node.position : previous.position + if(JSON.stringify(previous.data.topology_node) === JSON.stringify(node.data.topology_node) && previous.position.x === position.x && previous.position.y === position.y) + return previous + return {...previous, ...node, position, selected: previous.selected, measured: previous.measured} + }) +} +function merge_flow_edges(current: Flow_Edge[], incoming: Flow_Edge[], replace: boolean): Flow_Edge[] { + if(replace) + return incoming + const current_by_id = new Map(current.map(edge => [edge.id, edge])) + return incoming.map(edge => { + const previous = current_by_id.get(edge.id) + if(!previous) + return edge + if(JSON.stringify(previous.data?.topology_edge) === JSON.stringify(edge.data?.topology_edge)) + return previous + return {...previous, ...edge, selected: previous.selected} + }) +} +function status_text(value: unknown): string { + if(value === null || value === undefined) + return "—" + return typeof value === "object" ? JSON.stringify(value) : String(value) +} +function use_polling_status(api: string | undefined, expanded: boolean, poll_interval_ms: number) { const [status, set_status] = useState | null>(null) + const [loading, set_loading] = useState(false) const [error, set_error] = useState("") - const [updated_at, set_updated_at] = useState(null) - + const [updated_at, set_updated_at] = useState("") useEffect(() => { - if (!expanded || !node.status_api) return - let active = true - let timer: number | undefined - let controller: AbortController | undefined - const poll = async () => { - controller = new AbortController() + if(!expanded || !api) + return + const controller = new AbortController() + let timer = 0 + const load = async () => { try { - const response = await axios.get>>(node.status_api, {signal: controller.signal}) - if (!active) return + const response = await axios.get>>(api, {signal: controller.signal, timeout: 5000}) + if(response.data.status !== 0) + throw new Error(response.data.msg) set_status(response.data.data) + set_updated_at(new Date().toLocaleTimeString()) set_error("") - set_updated_at(new Date()) - } catch (reason) { - if (!active || axios.isCancel(reason)) return - set_error(reason instanceof Error ? reason.message : String(reason)) + } catch(reason) { + if(!axios.isCancel(reason)) + set_error(reason instanceof Error ? reason.message : String(reason)) } finally { - if (active) timer = window.setTimeout(poll, Math.max(250, schema.poll_interval_ms)) + if(!controller.signal.aborted) { + set_loading(false) + timer = window.setTimeout(() => void load(), Math.max(500, poll_interval_ms)) + } } } - void poll() + set_loading(true) + void load() return () => { - active = false - controller?.abort() - if (timer !== undefined) window.clearTimeout(timer) + controller.abort() + window.clearTimeout(timer) } - }, [expanded, node.status_api, schema.poll_interval_ms]) - - if (!node.status_api) return 该节点没有后端运行状态接口 - return { - const open = keys.includes("status") - set_expanded(open) - if (!open) { - set_status(null) - set_error("") - set_updated_at(null) - } - }} - items={[{ - key: "status", - label: {schema.title}{expanded ? "轮询中" : schema.collapsed_hint}, - children: error - ? - : status - ? - ({ - key, - label: key, - children: {status_text(value)} - }))}/> - {updated_at && 更新于 {updated_at.toLocaleTimeString()}} - - : - }]}/> + }, [api, expanded, poll_interval_ms]) + return {status, loading, error, updated_at} +} +function use_status_expansion(key: string, expansion: MutableRefObject>) { + const [expanded, set_state] = useState(() => expansion.current.has(key)) + const set_expanded = useCallback((value: boolean) => { + if(value) + expansion.current.add(key) + else + expansion.current.delete(key) + set_state(value) + }, [expansion, key]) + return [expanded, set_expanded] as const +} +function Node_Status({node, config, expansion}: {node: Topology_Node, config: Topology_Status_Config, expansion: MutableRefObject>}) { + const [expanded, set_expanded] = use_status_expansion(`node:${node.id}`, expansion) + const {status, loading, error, updated_at} = use_polling_status(node.status_api, expanded, config.poll_interval_ms) + const rows = node.status_api ? Object.entries(status ?? {}) : node.details.map(detail => [detail.label, detail.value] as [string, unknown]) + return
event.stopPropagation()} onClick={event => event.stopPropagation()} style={{marginTop: 7, borderTop: "1px solid rgba(0,0,0,.08)", paddingTop: 5}}> + + {expanded &&
+ {loading &&
} + {error && {error}} + {!loading && !error && rows.map(([key, value]) =>
{key}{status_text(value)}
)} + {updated_at &&
更新于 {updated_at}
} +
} +
+} +function Topology_Flow_Edge({id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, style, interactionWidth, data}: EdgeProps) { + const [edge_path, label_x, label_y] = getSmoothStepPath({sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition}) + const edge = data!.topology_edge + const [expanded, set_expanded] = use_status_expansion(`edge:${edge.id}`, data!.status_expansion) + const {status, loading, error, updated_at} = use_polling_status(edge.status_api, expanded, data!.status.poll_interval_ms) + const rows = edge.status_api ? Object.entries(status ?? {}) : (edge.details ?? []).map(detail => [detail.label, detail.value] as [string, unknown]) + return <> + +
data!.on_context_menu(event)} style={{position: "absolute", transform: `translate(-50%, -50%) translate(${label_x}px, ${label_y}px)`, pointerEvents: "all", zIndex: expanded ? 8 : 2}}> + {!expanded &&
+ {loading &&
} + {error && {error}} + {!loading && !error && rows.map(([key, value]) =>
{key}{status_text(value)}
)} + {!loading && !error && rows.length === 0 && 暂无连接状态} + {updated_at &&
更新于 {updated_at}
} +
} + + +} +function Topology_Flow_Node({data}: NodeProps) { + const node = data.topology_node + const [width] = node_size(node) + return <> + {node.kind !== "data_source" && } +
+ }/> + +
+ {node.kind !== "client" && } + +} +const flow_node_types = {topology: Topology_Flow_Node} +const flow_edge_types = {topology: Topology_Flow_Edge} +function Editor_Overlay({state, editor, node, on_submit, on_close}: {state: Editor_State, editor: Topology_Editor, node?: Topology_Node, on_submit(config: Record): void, on_close(): void}) { + const body = useRef(null) + const schema = useMemo(() => draft_editor_schema(editor, state.mode), [editor, state.mode]) + const node_config = node?.config + const data = useMemo(() => clone_data(state.mode === "create" ? editor.create_data : node_config ?? {}), [editor.create_data, node_config, state.mode]) + const fetcher = useCallback(async (request: Amis_Request) => { + if(request.url !== draft_submit_api) + return amis_fetcher(request) + on_submit(request.data as Record) + return {status: 200, statusText: "OK", headers: {}, config: {}, data: {status: 0, msg: "已暂存,点击保存拓扑后下发后台", data: request.data}} + }, [on_submit]) + const title = state.mode === "create" ? `新建${editor.title}` : `编辑${editor.title}:${node?.label ?? ""}` + const submit = () => body.current?.querySelector("form")?.requestSubmit() + return
event.stopPropagation()} style={{position: "absolute", inset: 18, zIndex: 20, display: "flex", flexDirection: "column", overflow: "hidden", background: "rgba(255,255,255,.98)", border: "1px solid #d9d9d9", borderRadius: 12, boxShadow: "0 14px 42px rgba(0,0,0,.18)"}}> +
{title}
+
+
+
+} +function Connection_Overlay({initial_feed_id, feeds, sources, source_for_feed, on_submit, on_close}: {initial_feed_id?: string, feeds: Topology_Node[], sources: Topology_Node[], source_for_feed(feed_id: string): string, on_submit(feed: Topology_Node, source_id: string): void, on_close(): void}) { + const [feed_id, set_feed_id] = useState(initial_feed_id ?? "") + const [source_id, set_source_id] = useState(initial_feed_id ? source_for_feed(initial_feed_id) : "") + const feed = feeds.find(item => item.id === feed_id) + const current_source = feed_id ? source_for_feed(feed_id) : "" + const select_feed = (value: string) => { + set_feed_id(value) + set_source_id(source_for_feed(value)) + } + return
event.stopPropagation()} style={{position: "absolute", top: 24, left: "50%", zIndex: 21, width: 430, maxWidth: "calc(100% - 48px)", transform: "translateX(-50%)", background: "#fff", border: "1px solid #d9d9d9", borderRadius: 12, boxShadow: "0 14px 42px rgba(0,0,0,.18)", padding: 18}}> + {initial_feed_id ? `编辑连接:${feed?.label ?? ""}` : "新建连接"} + + ({label: source.label, value: source.id}))} onChange={set_source_id}/> + + +
} - export function DataTopology({schema}: {schema: Data_Topology_Schema}) { - const container = useRef(null) - const graph_ref = useRef(null) - const [data, set_data] = useState(null) - const [selected_id, set_selected_id] = useState("") - const [editor_kind, set_editor_kind] = useState("") + const shell = useRef(null) + const flow_ref = useRef | null>(null) + const working_view_ref = useRef(null) + const restoring_ref = useRef(true) + const replace_flow_ref = useRef(true) + const viewport_interaction_ref = useRef(false) + const dirty_ref = useRef(false) + const server_signature_ref = useRef("") + const runtime_signature_ref = useRef("") + const status_expansion_ref = useRef(new Set()) + const temporary_id = useRef(-1) + const [baseline, set_baseline] = useState(null) + const [draft, set_draft] = useState(null) + const [flow_nodes, set_flow_nodes] = useState([]) + const [flow_edges, set_flow_edges] = useState([]) + const [flow_ready, set_flow_ready] = useState(false) + const [view_dirty, set_view_dirty] = useState(false) + const [saving, set_saving] = useState(false) + const [editor_state, set_editor_state] = useState(null) + const [connection_state, set_connection_state] = useState(null) + const [context_menu, set_context_menu] = useState(null) const [error, set_error] = useState("") - const [view_error, set_view_error] = useState("") - - const load = useCallback(async () => { + const content_dirty = useMemo(() => draft_signature(draft) !== draft_signature(baseline), [baseline, draft]) + const dirty = content_dirty || view_dirty + dirty_ref.current = dirty + const finish_restoring = useCallback(() => { + window.requestAnimationFrame(() => window.requestAnimationFrame(() => { + restoring_ref.current = false + })) + }, []) + const restore_viewport = useCallback((view: Topology_View) => { + const instance = flow_ref.current + if(!instance) + return + restoring_ref.current = true + void instance.setViewport({x: view.pan_x, y: view.pan_y, zoom: view.zoom}, {duration: 0}).then(() => { + finish_restoring() + }) + }, [finish_restoring]) + const load = useCallback(async (discard: boolean) => { try { - const response = await axios.get>(schema.data_api) - if (response.data.data.protocol !== "ecap.data-topology" || response.data.data.protocol_version !== 1) + const response = await axios.get>(schema.data_api, {timeout: 8000}) + const next = response.data.data + if(next.protocol !== "ecap.data-topology" || next.protocol_version !== 1) throw new Error("数据拓扑协议或版本不受支持") - if (response.data.data.view.protocol !== "ecap.data-topology-view" || response.data.data.view.protocol_version !== 1) + if(next.view.protocol !== "ecap.data-topology-view" || next.view.protocol_version !== 1) throw new Error("数据拓扑视图协议或版本不受支持") - set_data(response.data.data) + const next_server_signature = draft_signature(next) + const next_runtime_signature = runtime_signature(next) + if(discard || !dirty_ref.current && next_server_signature !== server_signature_ref.current) { + server_signature_ref.current = next_server_signature + runtime_signature_ref.current = next_runtime_signature + working_view_ref.current = clone_data(next.view) + replace_flow_ref.current = true + set_baseline(clone_data(next)) + set_draft(clone_data(next)) + set_view_dirty(false) + if(discard) + restore_viewport(next.view) + } else if(next_runtime_signature !== runtime_signature_ref.current) { + runtime_signature_ref.current = next_runtime_signature + set_baseline(current => current ? merge_runtime(current, next) : clone_data(next)) + set_draft(current => current ? merge_runtime(current, next) : clone_data(next)) + } set_error("") - } catch (reason) { + } catch(reason) { set_error(reason instanceof Error ? reason.message : String(reason)) } - }, [schema.data_api]) - + }, [restore_viewport, schema.data_api]) useEffect(() => { - void load() - window.addEventListener(topology_changed_event, load) - return () => window.removeEventListener(topology_changed_event, load) - }, [load]) - + void load(true) + const timer = window.setInterval(() => void load(false), Math.max(500, schema.graph.poll_interval_ms)) + return () => window.clearInterval(timer) + }, [load, schema.graph.poll_interval_ms]) const kind_by_name = useMemo(() => new Map(schema.node_kinds.map(kind => [kind.kind, kind])), [schema.node_kinds]) - const selected = data?.nodes.find(node => node.id === selected_id) - const selected_kind = selected && kind_by_name.get(selected.kind) - const editor = schema.editors.find(item => item.kind === editor_kind) - + const editor_by_kind = useMemo(() => new Map(schema.editors.map(editor => [editor.kind, editor])), [schema.editors]) + const sources = useMemo(() => draft?.nodes.filter(node => node.kind === "data_source") ?? [], [draft]) + const feeds = useMemo(() => draft?.nodes.filter(node => node.kind === "data_feed") ?? [], [draft]) + const source_for_feed = useCallback((feed_id: string) => draft?.edges.find(edge => edge.kind === "configuration" && edge.target === feed_id)?.source ?? "", [draft]) + const update_draft = useCallback((update: (current: Topology_Data) => Topology_Data) => { + set_draft(current => { + if(!current) + return current + const view = flow_ref.current ? capture_flow_view(flow_ref.current, current, schema.graph.layout_revision) : working_view_ref.current ?? current.view + working_view_ref.current = view + return update({...current, view}) + }) + }, [schema.graph.layout_revision]) + const open_create = useCallback((kind: string) => { + set_context_menu(null) + set_connection_state(null) + set_editor_state({kind, mode: "create"}) + }, []) + const open_edit = useCallback((node: Topology_Node) => { + set_context_menu(null) + set_connection_state(null) + set_editor_state({kind: node.kind, mode: "edit", node_id: node.id}) + }, []) + const open_connection = useCallback((feed?: Topology_Node) => { + set_context_menu(null) + set_editor_state(null) + set_connection_state({feed_id: feed?.id}) + }, []) + const stage_editor = useCallback((config: Record) => { + if(!editor_state) + return + update_draft(current => { + if(editor_state.mode === "edit") { + const node = current.nodes.find(item => item.id === editor_state.node_id) + if(!node) + return current + const next_config = {...node.config, ...config} + return {...current, nodes: current.nodes.map(item => item.id === node.id ? {...item, key: String(next_config.key), label: String(next_config.key), type: String(next_config.type), enabled: node_enabled(next_config), config: next_config, details: node_details(item.kind, next_config)} : item)} + } + const key = String(config.key) + const id = `draft:${editor_state.kind}:${crypto.randomUUID()}` + const node: Topology_Node = {id, kind: editor_state.kind, entity_id: temporary_id.current--, key, label: key, type: String(config.type), enabled: node_enabled(config), config, details: node_details(editor_state.kind, config)} + return {...current, nodes: [...current.nodes.filter(item => item.kind !== "client"), node, ...current.nodes.filter(item => item.kind === "client")]} + }) + set_editor_state(null) + message.success("配置已暂存") + }, [editor_state, update_draft]) + const stage_connection = useCallback((feed: Topology_Node, source_id: string) => { + update_draft(current => { + const source = current.nodes.find(node => node.id === source_id) + const nodes = current.nodes.map(node => node.id === feed.id ? {...node, config: {...node.config, source_key: source?.key ?? ""}} : node) + const edges = current.edges.filter(edge => edge.kind !== "configuration" || edge.target !== feed.id) + if(source) + edges.push({id: `source-feed:${feed.id}`, kind: "configuration", source: source.id, target: feed.id, active: source.enabled && feed.enabled, details: [{label: "连接状态", value: "已暂存,保存后生效"}, {label: "数据源", value: source.label}, {label: "数据馈送", value: feed.label}]}) + return {...current, nodes, edges} + }) + set_connection_state(null) + set_context_menu(null) + message.success(source_id ? "连接已暂存" : "断开连接已暂存") + }, [update_draft]) + const connect_nodes = useCallback((connection: Connection) => { + const source = draft?.nodes.find(node => node.id === connection.source) + const feed = draft?.nodes.find(node => node.id === connection.target) + if(source?.kind !== "data_source" || feed?.kind !== "data_feed") + return + stage_connection(feed, source.id) + }, [draft, stage_connection]) + const remove_node = useCallback((node: Topology_Node) => { + set_context_menu(null) + Modal.confirm({title: `删除${node.kind === "data_source" ? "数据源" : "数据馈送"}“${node.label}”?`, content: "删除只会暂存,点击保存拓扑后才下发后台。", okText: "暂存删除", okButtonProps: {danger: true}, cancelText: "取消", onOk: () => { + update_draft(current => { + const nodes = current.nodes.filter(item => item.id !== node.id).map(item => node.kind === "data_source" && item.kind === "data_feed" && item.config?.source_key === node.key ? {...item, config: {...item.config, source_key: ""}} : item) + return {...current, nodes, edges: current.edges.filter(edge => edge.source !== node.id && edge.target !== node.id)} + }) + }}) + }, [update_draft]) + const sync_working_view = useCallback((mark_dirty: boolean) => { + if(!flow_ref.current || !draft || restoring_ref.current) + return + const view = capture_flow_view(flow_ref.current, draft, schema.graph.layout_revision) + if(mark_dirty && JSON.stringify(view) !== JSON.stringify(working_view_ref.current)) + set_view_dirty(true) + working_view_ref.current = view + }, [draft, schema.graph.layout_revision]) + const on_nodes_change = useCallback>(changes => { + set_flow_nodes(nodes => applyNodeChanges(changes, nodes)) + }, []) + const show_context_menu = useCallback((event: Context_Event, target: Context_Menu_State["target"], id?: string) => { + if(!shell.current) + return + set_editor_state(null) + set_connection_state(null) + set_context_menu({...context_position(event, shell.current), target, id}) + }, []) + const on_node_context_menu = useCallback>((event, node) => show_context_menu(event, "node", node.id), [show_context_menu]) + const on_edge_context_menu = useCallback>((event, edge) => show_context_menu(event, "edge", edge.id), [show_context_menu]) + const on_edge_label_context_menu = useCallback((event: React_Mouse_Event, edge_id: string) => show_context_menu(event, "edge", edge_id), [show_context_menu]) useEffect(() => { - if (!container.current || !data) return - let disposed = false - let restoring = true - let save_timer: number | undefined - const graph = new Graph({ - container: container.current, - autoFit: data.view.initialized ? undefined : "view", - animation: false, - data: { - nodes: data.nodes.map(node => ({id: node.id, data: node})), - edges: data.edges.map(edge => ({id: edge.id, source: edge.source, target: edge.target, data: edge})) - }, - layout: schema.graph.layout as never, - behaviors: schema.graph.behaviors, - node: { - type: "rect", - style: datum => { - const node = datum.data as unknown as Topology_Node - const kind = kind_by_name.get(node.kind) - return { - size: [190, 62], - radius: 10, - fill: kind?.fill ?? "#fafafa", - fillOpacity: node.enabled ? 1 : 0.48, - stroke: kind?.stroke ?? "#8c8c8c", - lineWidth: node.enabled ? 2 : 1, - lineDash: node.enabled ? undefined : [5, 4], - labelText: `${node.label}\n${node.type}`, - labelFill: "#1f1f1f", - labelFontSize: 13, - labelLineHeight: 18, - cursor: "pointer" - } - } - }, - edge: { - type: "polyline", - style: datum => { - const edge = datum.data as unknown as Topology_Edge - return { - stroke: edge.active ? schema.graph.active_edge_color : schema.graph.inactive_edge_color, - lineWidth: edge.active ? 2.5 : 1.5, - lineDash: edge.active ? undefined : [6, 5], - endArrow: true, - opacity: edge.active ? 1 : 0.62 - } - } - } - }) - graph.on(NodeEvent.CLICK, event => { - const target = (event as unknown as {target: {id: string}}).target - set_selected_id(target.id) - }) - graph.on(CanvasEvent.CLICK, () => set_selected_id("")) - const persist_view = async () => { - if (restoring || disposed) return - const node_positions: Topology_View["node_positions"] = {} - for (const node of data.nodes) { - const [x, y] = graph.getElementPosition(node.id) - node_positions[node.id] = {x, y} - } - const [pan_x, pan_y] = graph.getPosition() - const payload: Topology_View = { - protocol: "ecap.data-topology-view", - protocol_version: 1, - initialized: true, - zoom: graph.getZoom(), - pan_x, - pan_y, - node_positions - } - try { - await axios.put(schema.view_api, payload) - if (!disposed) set_view_error("") - } catch (reason) { - if (!disposed) set_view_error(reason instanceof Error ? reason.message : String(reason)) - } + if(!draft) + return + const view = working_view_ref.current ?? draft.view + const replace = replace_flow_ref.current + const next_nodes = create_flow_nodes(draft, view, schema.graph.layout, schema.graph.layout_revision, kind_by_name, schema.status, status_expansion_ref) + const next_edges = create_flow_edges(draft, schema, status_expansion_ref, on_edge_label_context_menu) + set_flow_nodes(current => merge_flow_nodes(current, next_nodes, replace)) + set_flow_edges(current => merge_flow_edges(current, next_edges, replace)) + replace_flow_ref.current = false + set_flow_ready(true) + }, [draft, kind_by_name, on_edge_label_context_menu, schema]) + const initialize_flow = useCallback(async (instance: ReactFlowInstance) => { + flow_ref.current = instance + restoring_ref.current = true + const view = working_view_ref.current ?? draft?.view + if(view?.initialized && view.layout_revision === schema.graph.layout_revision) + await instance.setViewport({x: view.pan_x, y: view.pan_y, zoom: view.zoom}, {duration: 0}) + else + await instance.fitView({padding: 0.12}) + if(draft) + working_view_ref.current = capture_flow_view(instance, draft, schema.graph.layout_revision) + finish_restoring() + }, [draft, finish_restoring, schema.graph.layout_revision]) + const save = useCallback(async () => { + if(!baseline || !draft || saving) + return + set_saving(true) + const request = async (method: "post" | "patch" | "delete" | "put", url: string, data?: unknown) => { + const response = await axios.request>({method, url, data, timeout: 15000}) + if(response.data.status !== 0) + throw new Error(response.data.msg) } - const schedule_view_save = () => { - if (restoring || disposed) return - if (save_timer !== undefined) window.clearTimeout(save_timer) - save_timer = window.setTimeout(() => void persist_view(), 400) - } - graph.on(NodeEvent.DRAG_END, schedule_view_save) - graph.on(GraphEvent.AFTER_TRANSFORM, schedule_view_save) - graph_ref.current = graph - const initialize = async () => { - await graph.render() - if (disposed) return - if (data.view.initialized) { - const positions: Record = {} - for (const [id, position] of Object.entries(data.view.node_positions)) { - if (data.nodes.some(node => node.id === id)) - positions[id] = [position.x, position.y] - } - if (Object.keys(positions).length > 0) - await graph.translateElementTo(positions, false) - await graph.zoomTo(data.view.zoom, false) - await graph.translateTo([data.view.pan_x, data.view.pan_y], false) + try { + const current_view = flow_ref.current ? capture_flow_view(flow_ref.current, draft, schema.graph.layout_revision) : working_view_ref.current ?? draft.view + working_view_ref.current = current_view + const baseline_sources = baseline.nodes.filter(node => node.kind === "data_source") + const baseline_feeds = baseline.nodes.filter(node => node.kind === "data_feed") + const draft_sources = draft.nodes.filter(node => node.kind === "data_source") + const draft_feeds = draft.nodes.filter(node => node.kind === "data_feed") + for(const node of draft_sources.filter(node => node.entity_id < 0)) + await request("post", data_sources_api, node.config) + for(const node of draft_feeds.filter(node => node.entity_id > 0)) { + const original = baseline_feeds.find(item => item.entity_id === node.entity_id) + if(original && JSON.stringify(original.config) !== JSON.stringify(node.config)) + await request("patch", `${data_feeds_api}/${node.entity_id}`, changed_config(original.config, node.config)) } - restoring = false + for(const node of baseline_feeds.filter(node => !draft_feeds.some(item => item.entity_id === node.entity_id)).sort((left, right) => right.entity_id - left.entity_id)) + await request("delete", `${data_feeds_api}/${node.entity_id}`) + for(const node of draft_sources.filter(node => node.entity_id > 0)) { + const original = baseline_sources.find(item => item.entity_id === node.entity_id) + if(original && JSON.stringify(original.config) !== JSON.stringify(node.config)) + await request("patch", `${data_sources_api}/${node.entity_id}`, changed_config(original.config, node.config)) + } + for(const node of baseline_sources.filter(node => !draft_sources.some(item => item.entity_id === node.entity_id)).sort((left, right) => right.entity_id - left.entity_id)) + await request("delete", `${data_sources_api}/${node.entity_id}`) + for(const node of draft_feeds.filter(node => node.entity_id < 0)) + await request("post", data_feeds_api, node.config) + await request("put", schema.view_api, canonical_view(current_view, draft.nodes)) + await load(true) + window.dispatchEvent(new Event(topology_changed_event)) + message.success("拓扑已保存并同步到后台") + } catch(reason) { + await load(true) + message.error(`保存中断,已同步后台实际状态:${reason instanceof Error ? reason.message : String(reason)}`) + } finally { + set_saving(false) } - void initialize() - const observer = new ResizeObserver(() => graph.resize()) - observer.observe(container.current) - return () => { - if (save_timer !== undefined) window.clearTimeout(save_timer) - if (!restoring) void persist_view() - disposed = true - observer.disconnect() - graph.destroy() - graph_ref.current = null + }, [baseline, draft, load, saving, schema.graph.layout_revision, schema.view_api]) + const undo = useCallback(() => { + if(!baseline) + return + replace_flow_ref.current = true + set_draft(clone_data(baseline)) + working_view_ref.current = clone_data(baseline.view) + restore_viewport(baseline.view) + set_view_dirty(false) + set_editor_state(null) + set_connection_state(null) + set_context_menu(null) + message.success("未保存修改已撤销") + }, [baseline, restore_viewport]) + const sync = useCallback(() => { + const execute = () => { + set_editor_state(null) + set_connection_state(null) + set_context_menu(null) + void load(true) } - }, [data, kind_by_name, schema.graph, schema.view_api]) - - return - - - {schema.editors.map(item => )} - }> + if(dirty) + Modal.confirm({title: "放弃未保存修改并同步后台?", content: "本地拓扑草稿将被后台当前配置替换。", okText: "同步后台", cancelText: "取消", onOk: execute}) + else + execute() + }, [dirty, load]) + const fit_view = useCallback(async () => { + if(!flow_ref.current) + return + await flow_ref.current.fitView({padding: 0.12}) + sync_working_view(true) + }, [sync_working_view]) + const layout = useCallback(() => { + if(!draft) + return + const positions = topology_layout(draft, schema.graph.layout) + set_flow_nodes(nodes => nodes.map(node => ({...node, position: positions.get(node.id)!}))) + const viewport = flow_ref.current?.getViewport() ?? {x: 0, y: 0, zoom: 1} + const node_positions: Topology_View["node_positions"] = {} + for(const node of draft.nodes) { + if(node.kind !== "client") + node_positions[node.id] = positions.get(node.id)! + } + working_view_ref.current = {protocol: "ecap.data-topology-view", protocol_version: 1, initialized: true, layout_revision: schema.graph.layout_revision, zoom: viewport.zoom, pan_x: viewport.x, pan_y: viewport.y, node_positions} + set_view_dirty(true) + window.requestAnimationFrame(() => void fit_view()) + }, [draft, fit_view, schema.graph.layout, schema.graph.layout_revision]) + const context_items = useMemo(() => { + if(!context_menu) + return [] + if(context_menu.target === "canvas") + return [{key: "create-source", icon: , label: "新建数据源", onClick: () => open_create("data_source")}, {key: "create-feed", icon: , label: "新建数据馈送", onClick: () => open_create("data_feed")}, {type: "divider"}, {key: "create-connection", icon: , label: "新建连接", onClick: () => open_connection()}] + if(context_menu.target === "edge") { + const edge = draft?.edges.find(item => item.id === context_menu.id) + const feed = feeds.find(item => item.id === edge?.target) + return edge?.kind === "configuration" && feed ? [{key: "edit-connection", icon: , label: "编辑连接", onClick: () => open_connection(feed)}, {key: "disconnect", icon: , danger: true, label: "断开连接", onClick: () => stage_connection(feed, "")}] : [{key: "runtime", disabled: true, label: "运行时客户端连接"}] + } + const node = draft?.nodes.find(item => item.id === context_menu.id) + if(!node) + return [] + if(node.kind === "client") + return [{key: "client", disabled: true, label: `${node.type} ${node.label}`}] + const items: NonNullable = [{key: "edit", icon: , label: "编辑配置", onClick: () => open_edit(node)}] + if(node.kind === "data_feed" && !draft?.edges.some(edge => edge.kind === "configuration" && edge.target === node.id)) + items.push({key: "connection", icon: , label: "添加连接", onClick: () => open_connection(node)}) + items.push({type: "divider"}, {key: "delete", icon: , danger: true, label: "删除节点", onClick: () => remove_node(node)}) + return items + }, [context_menu, draft, feeds, open_connection, open_create, open_edit, remove_node, stage_connection]) + const active_editor = editor_state ? editor_by_kind.get(editor_state.kind) : undefined + const editor_node = editor_state?.node_id ? draft?.nodes.find(node => node.id === editor_state.node_id) : undefined + const title = {schema.title}{dirty && 未保存} + return }> {error && } - {view_error && set_view_error("")} style={{marginBottom: 12}}/>} - {!data && !error ?
: data &&
-
- set_editor_kind(selected.kind)}>打开编辑器}> - {selected ? - {selected_kind?.title ?? selected.kind}{selected.enabled ? "已启用" : "未启用"} - - - : } - + {!draft || !flow_ready ?
:
+ + nodes={flow_nodes} edges={flow_edges} nodeTypes={flow_node_types} edgeTypes={flow_edge_types} onNodesChange={on_nodes_change} onConnect={connect_nodes} onInit={instance => void initialize_flow(instance)} onNodeDrag={() => sync_working_view(true)} onNodeDragStop={() => sync_working_view(true)} onMoveStart={event => { + viewport_interaction_ref.current = Boolean(event && (!(event.target instanceof Element) || !event.target.closest(".nodrag,.nopan"))) + }} onMoveEnd={() => { + if(viewport_interaction_ref.current) + sync_working_view(true) + viewport_interaction_ref.current = false + }} onNodeContextMenu={on_node_context_menu} onEdgeContextMenu={on_edge_context_menu} onPaneContextMenu={event => show_context_menu(event, "canvas")} onPaneClick={() => set_context_menu(null)} nodesConnectable edgesReconnectable={false} deleteKeyCode={null} zoomOnDoubleClick={false} minZoom={0.15} maxZoom={2.5} proOptions={{hideAttribution: true}}> + + set_view_dirty(true)} onZoomOut={() => set_view_dirty(true)} onFitView={() => set_view_dirty(true)}/> + + {context_menu &&
event.stopPropagation()} style={{position: "absolute", left: context_menu.x, top: context_menu.y, zIndex: 30, minWidth: 190, background: "#fff", border: "1px solid #e5e7eb", borderRadius: 8, boxShadow: "0 10px 28px rgba(0,0,0,.16)", overflow: "hidden"}}>
} + {editor_state && active_editor && set_editor_state(null)}/>} + {connection_state && set_connection_state(null)}/>}
} - { - set_editor_kind("") - void load() - }}> - {editor && } - + {draft &&
{schema.node_kinds.map(kind => {kind.title})}右键画布可新建节点或连接;右键连接线可编辑或断开;所有修改点击保存后才同步后台。
} } - export function DataTopologyPage() { const [schema, set_schema] = useState(null) const [error, set_error] = useState("") @@ -344,19 +794,22 @@ export function DataTopologyPage() { const response = await axios.get>("/api/adminive/config/data_topology/schema", {signal: controller.signal}) set_schema(response.data.data) set_error("") - } catch (reason) { - if (!axios.isCancel(reason)) set_error(reason instanceof Error ? reason.message : String(reason)) + } catch(reason) { + if(!axios.isCancel(reason)) + set_error(reason instanceof Error ? reason.message : String(reason)) } } - const handle_topology_changed = () => void load_schema() + const changed = () => void load_schema() void load_schema() - window.addEventListener(topology_changed_event, handle_topology_changed) + window.addEventListener(topology_changed_event, changed) return () => { controller.abort() - window.removeEventListener(topology_changed_event, handle_topology_changed) + window.removeEventListener(topology_changed_event, changed) } }, []) - if (error) return - if (!schema) return
+ if(error) + return + if(!schema) + return
return
} diff --git a/src/Adminive/amis_runtime.css b/src/Adminive/amis_runtime.css new file mode 100644 index 0000000..0a9f131 --- /dev/null +++ b/src/Adminive/amis_runtime.css @@ -0,0 +1,11 @@ +.ecap-amis-overlay-root { + position: fixed !important; + inset: 0 !important; + z-index: 2000 !important; + background: transparent !important; + overflow: visible !important; + pointer-events: none !important; +} +.ecap-amis-overlay-root > * { + pointer-events: auto !important; +} diff --git a/src/Adminive/amis_runtime.ts b/src/Adminive/amis_runtime.ts index 09b4629..4bf3a01 100644 --- a/src/Adminive/amis_runtime.ts +++ b/src/Adminive/amis_runtime.ts @@ -1,3 +1,4 @@ +import "./amis_runtime.css" type Amis_Request = { url: string method?: string @@ -6,7 +7,7 @@ type Amis_Request = { responseType?: XMLHttpRequest["responseType"] config?: Record } -type Amis_Fetcher = (request: Amis_Request) => Promise +export type Amis_Fetcher = (request: Amis_Request) => Promise interface Amis_Scoped { unmount(): void } @@ -19,10 +20,19 @@ declare global { } } export type {Amis_Request} -export function embed_amis(container: HTMLElement, schema: Record, fetcher: Amis_Fetcher): Amis_Scoped { - if (!window.amisRequire) { +let overlay_root: HTMLDivElement | null = null +function get_overlay_root(): HTMLDivElement { + if(overlay_root) + return overlay_root + overlay_root = document.createElement("div") + overlay_root.className = "amis-scope ecap-amis-overlay-root" + document.body.appendChild(overlay_root) + return overlay_root +} +export function embed_amis(container: HTMLElement, schema: Record, fetcher: Amis_Fetcher, data: Record = {}): Amis_Scoped { + if(!window.amisRequire) { throw new Error("AMIS SDK runtime is not loaded") } const module = window.amisRequire("amis/embed") as Amis_Embed_Module - return module.embed(container, schema, {}, {fetcher, theme: "cxd"}) + return module.embed(container, schema, {data}, {fetcher, getModalContainer: get_overlay_root, theme: "cxd"}) } diff --git a/vite.config.js b/vite.config.js index aaf140e..aefa26c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -139,10 +139,12 @@ const spaAliasPaths = new Set([ '/map3d', '/aircraftlist', '/settings', + '/topology', '/ui/map', '/ui/map3d', '/ui/aircraftlist', - '/ui/settings' + '/ui/settings', + '/ui/topology' ]) function spaAliasesPlugin() { return {