diff --git a/app/dashboard/package-lock.json b/app/dashboard/package-lock.json
@@ -2588,9 +2588,9 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz",
- "integrity": "sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==",
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz",
+ "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==",
"engines": {
"node": ">=14.0.0"
}
@@ -2848,9 +2848,9 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.25.0",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.25.0.tgz",
- "integrity": "sha512-vlobHP64HTuSE68lWF1mEhwSRC5Q7gaT+a/m9S+ItuN+ruSOxe1rFnR9j0ACWQ314BPhBEVKfBQ6mHL0OWfdbQ==",
+ "version": "5.26.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.26.3.tgz",
+ "integrity": "sha512-kKv7VHIleC7cwpMS3M63mWUFFbdiRo7JZMVUyZ4z/79RZoAi7/vGTVkCrnNqyKl+zwBRkumYL+eiYtyShGZd0A==",
"peer": true,
"funding": {
"type": "github",
@@ -2867,12 +2867,12 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.25.0",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.25.0.tgz",
- "integrity": "sha512-u+n5R7mLO7RmeiIonpaCRVXNRWtZEef/aVZ/XGWRPa7trBIvGtzlfo0Ah7ZtnTYfrKEVwnZ/tzRCBcoiqJ/tFw==",
+ "version": "5.26.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.26.3.tgz",
+ "integrity": "sha512-fP/kEu8aVLb6ln0ySazaeyivVkSQ9EnFfA5kQ7yIQNklx5JCW7otHVY6GVjZtd/3wK6QU99yvdSH0rN0LV2vmA==",
"peer": true,
"dependencies": {
- "@tanstack/query-core": "5.25.0"
+ "@tanstack/query-core": "5.26.3"
},
"funding": {
"type": "github",
@@ -2883,9 +2883,9 @@
}
},
"node_modules/@tanstack/react-query-devtools": {
- "version": "5.25.0",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.25.0.tgz",
- "integrity": "sha512-bqtr1Bwvo/jspJXb2l4R1DSZ848TvIzGBk4V0b6YGS5EQ3015dhm3mPqyTgh0DquK5ZR0h1yP/4DpzhhvTnFHA==",
+ "version": "5.26.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.26.3.tgz",
+ "integrity": "sha512-byhbST7CDC8cH1tPLPYG/lNMr/QcYMMqxG9+jayiz7H//QScp9+B7cpSItp9QPaN30fCj7pIAlbCifWDcLC4/A==",
"dependencies": {
"@tanstack/query-devtools": "5.24.0"
},
@@ -2894,7 +2894,7 @@
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
- "@tanstack/react-query": "^5.25.0",
+ "@tanstack/react-query": "^5.26.3",
"react": "^18.0.0"
}
},
@@ -2987,9 +2987,9 @@
"devOptional": true
},
"node_modules/@types/react": {
- "version": "18.2.63",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.63.tgz",
- "integrity": "sha512-ppaqODhs15PYL2nGUOaOu2RSCCB4Difu4UFrP4I3NHLloXC/ESQzQMi9nvjfT1+rudd0d2L3fQPJxRSey+rGlQ==",
+ "version": "18.2.64",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz",
+ "integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
@@ -3019,9 +3019,9 @@
}
},
"node_modules/@types/react-dom": {
- "version": "18.2.20",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.20.tgz",
- "integrity": "sha512-HXN/biJY8nv20Cn9ZbCFq3liERd4CozVZmKbaiZ9KiKTrWqsP7eoGDO6OOGvJQwoVFuiXaiJ7nBBjiFFbRmQMQ==",
+ "version": "18.2.21",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz",
+ "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==",
"dev": true,
"dependencies": {
"@types/react": "*"
@@ -3411,9 +3411,9 @@
}
},
"node_modules/apexcharts": {
- "version": "3.46.0",
- "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.46.0.tgz",
- "integrity": "sha512-ELAY6vj8JQD7QLktKasTzwm9Wt0qxqfQSo+3QWS7G7I774iK8HCkG1toGsqJH0mkK6PtYBtnSIe66uUcwoCw1w==",
+ "version": "3.47.0",
+ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.47.0.tgz",
+ "integrity": "sha512-s/fgNCA69b8lJdhI3R7Z+/Df47RPplLyHwuvttecR+aaZ3/Pm6wHYPiAGjqDNbVsMGXhuA9mcOpIYU5ZWeSdeg==",
"dependencies": {
"@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2",
@@ -3938,9 +3938,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001594",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001594.tgz",
- "integrity": "sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g==",
+ "version": "1.0.30001597",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
+ "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
"dev": true,
"funding": [
{
@@ -4521,9 +4521,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.693",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.693.tgz",
- "integrity": "sha512-/if4Ueg0GUQlhCrW2ZlXwDAm40ipuKo+OgeHInlL8sbjt+hzISxZK949fZeJaVsheamrzANXvw1zQTvbxTvSHw==",
+ "version": "1.4.700",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.700.tgz",
+ "integrity": "sha512-40dqKQ3F7C8fbBEmjSeJ+qEHCKzPyrP9SkeIBZ3wSCUH9nhWStrDz030XlDzlhNhlul1Z0fz7TpDFnsIzo4Jtg==",
"dev": true
},
"node_modules/emoji-regex": {
@@ -6238,9 +6238,9 @@
"dev": true
},
"node_modules/hasown": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
- "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -6672,9 +6672,12 @@
"dev": true
},
"node_modules/is-map": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
- "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -6746,9 +6749,12 @@
}
},
"node_modules/is-set": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
- "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -6810,9 +6816,12 @@
}
},
"node_modules/is-weakmap": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
- "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -6829,12 +6838,15 @@
}
},
"node_modules/is-weakset": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz",
- "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
+ "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.1"
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -8619,11 +8631,11 @@
}
},
"node_modules/react-router": {
- "version": "6.22.2",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.2.tgz",
- "integrity": "sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==",
+ "version": "6.22.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
+ "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==",
"dependencies": {
- "@remix-run/router": "1.15.2"
+ "@remix-run/router": "1.15.3"
},
"engines": {
"node": ">=14.0.0"
@@ -8633,12 +8645,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.22.2",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.2.tgz",
- "integrity": "sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==",
+ "version": "6.22.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz",
+ "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==",
"dependencies": {
- "@remix-run/router": "1.15.2",
- "react-router": "6.22.2"
+ "@remix-run/router": "1.15.3",
+ "react-router": "6.22.3"
},
"engines": {
"node": ">=14.0.0"
@@ -9058,12 +9070,12 @@
}
},
"node_modules/safe-array-concat": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz",
- "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
+ "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
"dependencies": {
- "call-bind": "^1.0.5",
- "get-intrinsic": "^1.2.2",
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4",
"has-symbols": "^1.0.3",
"isarray": "^2.0.5"
},
@@ -9218,16 +9230,16 @@
"dev": true
},
"node_modules/set-function-length": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
- "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dependencies": {
- "define-data-property": "^1.1.2",
+ "define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.3",
+ "get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.1"
+ "has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -9814,9 +9826,9 @@
"dev": true
},
"node_modules/ts-api-utils": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz",
- "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+ "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
"dev": true,
"engines": {
"node": ">=16"
@@ -10238,9 +10250,9 @@
}
},
"node_modules/vite-tsconfig-paths/node_modules/tsconfck": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.2.tgz",
- "integrity": "sha512-6lWtFjwuhS3XI4HsX4Zg0izOI3FU/AI9EGVlPEUMDIhvLPMD4wkiof0WCoDgW7qY+Dy198g4d9miAqUHWHFH6Q==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz",
+ "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==",
"bin": {
"tsconfck": "bin/tsconfck.js"
},
@@ -10257,9 +10269,9 @@
}
},
"node_modules/vite-tsconfig-paths/node_modules/typescript": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
- "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
+ "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
"optional": true,
"peer": true,
"bin": {
@@ -10424,29 +10436,32 @@
}
},
"node_modules/which-collection": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
- "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
"dependencies": {
- "is-map": "^2.0.1",
- "is-set": "^2.0.1",
- "is-weakmap": "^2.0.1",
- "is-weakset": "^2.0.1"
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-typed-array": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz",
- "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==",
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
+ "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
"dependencies": {
- "available-typed-arrays": "^1.0.6",
- "call-bind": "^1.0.5",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
- "has-tostringtag": "^1.0.1"
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
diff --git a/app/dashboard/src/pages/nodes/index.tsx b/app/dashboard/src/pages/nodes/index.tsx
@@ -1,6 +1,6 @@
import { FC } from 'react';
-import { CoreSettingsModal } from './modals/core-settings-modal';
+import { CoreSettingsModal } from './modals/core-settings';
import { NodesDialog } from './dialog';
import { NodesUsage } from './usage';
import { NodesTable } from './table';
diff --git a/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx b/app/dashboard/src/pages/nodes/modals/core-settings/content.tsx
rename from app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx
rename to app/dashboard/src/pages/nodes/modals/core-settings/content.tsx
@@ -1,115 +1,38 @@
import {
- Badge,
Box,
Button,
- chakra,
CircularProgress,
FormControl,
FormLabel,
HStack,
IconButton,
- Modal,
ModalBody,
- ModalCloseButton,
- ModalContent,
ModalFooter,
- ModalHeader,
- ModalOverlay,
Text,
- Tooltip,
useToast,
} from '@chakra-ui/react';
-import {
- ArrowPathIcon,
- ArrowsPointingInIcon,
- ArrowsPointingOutIcon,
- Cog6ToothIcon,
-} from '@heroicons/react/24/outline';
-import { joinPaths } from '@remix-run/router';
-import classNames from 'classnames';
-import { useNodes, useCoreSettings } from 'stores';
+import { useCoreSettings } from 'stores';
import debounce from 'lodash.debounce';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
-import { useMutation } from '@tanstack/react-query';
-import { ReadyState } from 'react-use-websocket';
import { useWebSocket } from 'react-use-websocket/dist/lib/use-websocket';
-import { getAuthToken } from 'service/auth-storage';
-import { Icon } from 'components/icon';
import { JsonEditor } from 'components/json-editor';
import 'components/json-editor/themes.js';
-
-export const MAX_NUMBER_OF_LOGS = 500;
-
-const UsageIcon = chakra(Cog6ToothIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
-export const ReloadIcon = chakra(ArrowPathIcon, {
- baseStyle: {
- w: 4,
- h: 4,
- },
-});
-
-export const FullScreenIcon = chakra(ArrowsPointingOutIcon, {
- baseStyle: {
- w: 4,
- h: 4,
- },
-});
-export const ExitFullScreenIcon = chakra(ArrowsPointingInIcon, {
- baseStyle: {
- w: 3,
- h: 3,
- },
-});
-
-const getStatus = (status: string) => {
- return {
- [ReadyState.CONNECTING]: 'connecting',
- [ReadyState.OPEN]: 'connected',
- [ReadyState.CLOSING]: 'closed',
- [ReadyState.CLOSED]: 'closed',
- [ReadyState.UNINSTANTIATED]: 'closed',
- }[status];
-};
-
-const getWebsocketUrl = () => {
- try {
- let baseURL = new URL(
- import.meta.env.VITE_BASE_API.startsWith('/')
- ? window.location.origin + import.meta.env.VITE_BASE_API
- : import.meta.env.VITE_BASE_API
- );
-
- return (
- (baseURL.protocol === 'https:' ? 'wss://' : 'ws://') +
- joinPaths([baseURL.host + baseURL.pathname, '/core/logs']) +
- '?interval=1&token=' +
- getAuthToken()
- );
- } catch (e) {
- console.error('Unable to generate websocket url');
- console.error(e);
- return null;
- }
-};
+import { ExitFullScreenIcon, FullScreenIcon } from './icon';
+import { getStatus, getWebsocketUrl } from './helper';
+import { MAX_NUMBER_OF_LOGS } from '.';
let logsTmp: string[] = [];
-const CoreSettingModalContent: FC = () => {
- const { isEditingCore } = useNodes();
+
+export const CoreSettingModalContent: FC = () => {
const {
fetchCoreSettings,
updateConfig,
isLoading,
config,
isPostLoading,
- version,
- restartCore,
+ selectedNode,
} = useCoreSettings();
const logsDiv = useRef<HTMLDivElement | null>(null);
const [logs, setLogs] = useState<string[]>([]);
@@ -124,8 +47,8 @@ const CoreSettingModalContent: FC = () => {
}, [config]);
useEffect(() => {
- if (isEditingCore) fetchCoreSettings();
- }, [isEditingCore]);
+ if (selectedNode) fetchCoreSettings();
+ }, [selectedNode]);
''.startsWith;
const scrollShouldStayOnEnd = useRef(true);
const updateLogs = useCallback(
@@ -144,7 +67,7 @@ const CoreSettingModalContent: FC = () => {
[]
);
- const { readyState } = useWebSocket(getWebsocketUrl(), {
+ const { readyState } = useWebSocket(selectedNode?.id ? getWebsocketUrl(selectedNode.id) : '', {
onMessage: (e: any) => {
logsTmp.push(e.data);
if (logsTmp.length > MAX_NUMBER_OF_LOGS)
@@ -169,10 +92,10 @@ const CoreSettingModalContent: FC = () => {
const status = getStatus(readyState.toString());
- const {
- mutate: handleRestartCore,
- isPending: isRestarting
- } = useMutation({ mutationFn: restartCore });
+ // const {
+ // mutate: handleRestartCore,
+ // isPending: isRestarting
+ // } = useMutation({ mutationFn: restartCore });
const handleOnSave = ({ config }: any) => {
updateConfig(config)
@@ -202,6 +125,7 @@ const CoreSettingModalContent: FC = () => {
});
});
};
+
const editorRef = useRef<HTMLDivElement>(null);
const [isFullScreen, setFullScreen] = useState(false);
const handleFullScreen = () => {
@@ -222,13 +146,6 @@ const CoreSettingModalContent: FC = () => {
{t('core.configuration')}{' '}
{isLoading && <CircularProgress isIndeterminate size="15px" />}
</FormLabel>
- <HStack gap={0}>
- <Tooltip label="Xray Version" placement="top">
- <Badge height="100%" textTransform="lowercase">
- {version && `v${version}`}
- </Badge>
- </Tooltip>
- </HStack>
</HStack>
<Box position="relative" ref={editorRef} minHeight="300px">
<Controller
@@ -282,19 +199,19 @@ const CoreSettingModalContent: FC = () => {
<ModalFooter>
<HStack w="full" justifyContent="space-between">
<Box>
- <Button
- size="sm"
- leftIcon={
- <ReloadIcon
- className={classNames({
- 'animate-spin': isRestarting,
- })}
- />
- }
- onClick={() => handleRestartCore()}
- >
- {t(isRestarting ? 'core.restarting' : 'core.restartCore')}
- </Button>
+ {/* <Button */}
+ {/* size="sm" */}
+ {/* leftIcon={ */}
+ {/* <ReloadIcon */}
+ {/* className={classNames({ */}
+ {/* 'animate-spin': isRestarting, */}
+ {/* })} */}
+ {/* /> */}
+ {/* } */}
+ {/* onClick={() => handleRestartCore()} */}
+ {/* > */}
+ {/* {t(isRestarting ? 'core.restarting' : 'core.restartCore')} */}
+ {/* </Button> */}
</Box>
<HStack>
<Button
@@ -314,28 +231,3 @@ const CoreSettingModalContent: FC = () => {
</form>
);
};
-export const CoreSettingsModal: FC = () => {
- const { isEditingCore } = useNodes();
- const onClose = useNodes.setState.bind(null, { isEditingCore: false });
- const { t } = useTranslation();
-
- return (
- <Modal isOpen={isEditingCore} onClose={onClose} size="3xl">
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <ModalContent mx="3" w="full">
- <ModalHeader pt={6}>
- <HStack gap={2}>
- <Icon color="primary">
- <UsageIcon color="white" />
- </Icon>
- <Text fontWeight="semibold" fontSize="lg">
- {t('core.title')}
- </Text>
- </HStack>
- </ModalHeader>
- <ModalCloseButton mt={3} />
- <CoreSettingModalContent />
- </ModalContent>
- </Modal>
- );
-};
diff --git a/app/dashboard/src/pages/nodes/modals/core-settings/helper.ts b/app/dashboard/src/pages/nodes/modals/core-settings/helper.ts
@@ -0,0 +1,34 @@
+import { ReadyState } from 'react-use-websocket';
+import { joinPaths } from '@remix-run/router';
+import { getAuthToken } from 'service/auth-storage';
+
+export const getStatus = (status: string) => {
+ return {
+ [ReadyState.CONNECTING]: 'connecting',
+ [ReadyState.OPEN]: 'connected',
+ [ReadyState.CLOSING]: 'closed',
+ [ReadyState.CLOSED]: 'closed',
+ [ReadyState.UNINSTANTIATED]: 'closed',
+ }[status];
+};
+
+export const getWebsocketUrl = (node_id: number) => {
+ try {
+ let baseURL = new URL(
+ import.meta.env.VITE_BASE_API.startsWith('/')
+ ? window.location.origin + import.meta.env.VITE_BASE_API
+ : import.meta.env.VITE_BASE_API
+ );
+
+ return (
+ (baseURL.protocol === 'https:' ? 'wss://' : 'ws://') +
+ joinPaths([baseURL.host + baseURL.pathname, `/nodes/${node_id}/logs`]) +
+ '?interval=1&token=' +
+ getAuthToken()
+ );
+ } catch (e) {
+ console.error('Unable to generate websocket url');
+ console.error(e);
+ return null;
+ }
+};
diff --git a/app/dashboard/src/pages/nodes/modals/core-settings/icon.tsx b/app/dashboard/src/pages/nodes/modals/core-settings/icon.tsx
@@ -0,0 +1,34 @@
+import { chakra } from '@chakra-ui/react';
+import {
+ ArrowPathIcon,
+ ArrowsPointingInIcon,
+ ArrowsPointingOutIcon,
+ Cog6ToothIcon,
+} from '@heroicons/react/24/outline';
+
+export const UsageIcon = chakra(Cog6ToothIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export const ReloadIcon = chakra(ArrowPathIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+
+export const FullScreenIcon = chakra(ArrowsPointingOutIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+export const ExitFullScreenIcon = chakra(ArrowsPointingInIcon, {
+ baseStyle: {
+ w: 3,
+ h: 3,
+ },
+});
diff --git a/app/dashboard/src/pages/nodes/modals/core-settings/index.tsx b/app/dashboard/src/pages/nodes/modals/core-settings/index.tsx
@@ -0,0 +1,44 @@
+import {
+ HStack,
+ Modal,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ Text
+} from '@chakra-ui/react';
+import { Icon } from 'components/icon';
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useCoreSettings } from 'stores';
+import { CoreSettingModalContent } from './content';
+import { UsageIcon } from './icon';
+
+export const MAX_NUMBER_OF_LOGS = 500;
+
+
+export const CoreSettingsModal: FC = () => {
+ const { selectedNode } = useCoreSettings();
+ const onClose = useCoreSettings.setState.bind(null, { selectedNode: null });
+ const { t } = useTranslation();
+
+ return (
+ <Modal isOpen={selectedNode !== null} onClose={onClose} size="3xl">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3" w="full">
+ <ModalHeader pt={6}>
+ <HStack gap={2}>
+ <Icon color="primary">
+ <UsageIcon color="white" />
+ </Icon>
+ <Text fontWeight="semibold" fontSize="lg">
+ {t('core.title')}
+ </Text>
+ </HStack>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <CoreSettingModalContent />
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/pages/nodes/table/action-buttons.tsx b/app/dashboard/src/pages/nodes/table/action-buttons.tsx
@@ -2,6 +2,7 @@
import { t } from 'i18next';
import { Cog6ToothIcon } from '@heroicons/react/24/outline';
import { chakra, HStack, IconButton, Tooltip } from '@chakra-ui/react';
+import { NodeType, useCoreSettings } from 'stores';
import { FC } from 'react';
const iconProps = {
@@ -11,9 +12,12 @@ const iconProps = {
},
};
const CoreSettingsIcon = chakra(Cog6ToothIcon, iconProps);
-type ActionButtonsProps = {};
-export const ActionButtons: FC<ActionButtonsProps> = () => {
+type ActionButtonsProps = {
+ node: NodeType,
+};
+
+export const ActionButtons: FC<ActionButtonsProps> = ({ node }) => {
return (
<HStack
justifyContent="flex-end"
@@ -41,6 +45,9 @@ export const ActionButtons: FC<ActionButtonsProps> = () => {
base: 'sm',
md: 'md',
}}
+ onClick={() => {
+ useCoreSettings.setState({ selectedNode: node });
+ }}
>
<CoreSettingsIcon />
</IconButton>
@@ -48,4 +55,3 @@ export const ActionButtons: FC<ActionButtonsProps> = () => {
</HStack >
);
};
-
diff --git a/app/dashboard/src/pages/nodes/table/index.tsx b/app/dashboard/src/pages/nodes/table/index.tsx
@@ -232,7 +232,7 @@ export const NodesTable: FC<NodesTableProps> = (props) => {
</VStack>
<HStack w="full" justifyContent="space-between">
<HStack>
- <ActionButtons />
+ <ActionButtons node={node} />
<Tooltip
label={t('nodesDialog.editNode')}
placement="top"
@@ -369,7 +369,7 @@ export const NodesTable: FC<NodesTableProps> = (props) => {
{node.address}:{node.port}
</Td>
<Td width="200px" minW="180px">
- <ActionButtons />
+ <ActionButtons node={node} />
</Td>
</Tr>
);
diff --git a/app/dashboard/src/stores/core-settings.store.ts b/app/dashboard/src/stores/core-settings.store.ts
@@ -1,5 +1,6 @@
import { fetch } from 'service/http';
import { create } from 'zustand';
+import { NodeType } from './nodes.store';
type CoreSettingsStore = {
isLoading: boolean;
@@ -7,31 +8,26 @@ type CoreSettingsStore = {
fetchCoreSettings: () => void;
updateConfig: (json: string) => Promise<void>;
restartCore: () => Promise<void>;
- version: string | null;
- started: boolean | null;
- logs_websocket: string | null;
config: string;
+ selectedNode: NodeType | null;
+ isEditingCore: boolean;
};
-export const useCoreSettings = create<CoreSettingsStore>((set) => ({
+export const useCoreSettings = create<CoreSettingsStore>((set, get) => ({
isLoading: true,
isPostLoading: false,
- version: null,
- started: false,
- logs_websocket: null,
+ isEditingCore: false,
config: '',
+ selectedNode: null,
fetchCoreSettings: () => {
set({ isLoading: true });
Promise.all([
- fetch('/core').then(({ version, started, logs_websocket }) =>
- set({ version, started, logs_websocket })
- ),
- fetch('/core/config').then((config) => set({ config })),
+ fetch(`/nodes/${get().selectedNode?.id}/xray_config`).then((config) => set({ config })),
]).finally(() => set({ isLoading: false }));
},
- updateConfig: (body) => {
+ updateConfig: async (body) => {
set({ isPostLoading: true });
- return fetch('/core/config', { method: 'PUT', body }).finally(() => {
+ return fetch(`/nodes/${get().selectedNode?.id}/xray_config`, { method: 'PUT', body }).finally(() => {
set({ isPostLoading: false });
});
},
diff --git a/app/dashboard/src/stores/nodes.store.ts b/app/dashboard/src/stores/nodes.store.ts
@@ -63,7 +63,7 @@ export const getNodeCreateDefaultValues = (): NodeCreate => ({
export const FetchNodesQueryKey = 'fetch-nodes-query-key';
-export function fetchNodes(): Promise<NodeType[]> {
+export async function fetchNodes(): Promise<NodeType[]> {
useDashboard.setState({ loading: true })
return fetch('/nodes').then((nodes) => {
return nodes;
@@ -79,7 +79,6 @@ export type NodeStore = {
isAddingNode: boolean;
isShowingNodesUsage: boolean;
isEditingNode: boolean;
- isEditingCore: boolean;
nodesFilters: NodesFilterType;
refetchNodes: () => void;
refetchCertificate: () => void;
diff --git a/app/db/crud.py b/app/db/crud.py
@@ -4,7 +4,7 @@ from datetime import datetime
from enum import Enum
from typing import List, Optional, Tuple, Union
-from sqlalchemy import and_, delete
+from sqlalchemy import and_, delete, update, select
from sqlalchemy.orm import Session
from app.db.models import (JWT, TLS, Admin, Node, NodeUsage, NodeUserUsage,
@@ -33,21 +33,31 @@ def add_default_hosts(db: Session, inbounds: List[Inbound]):
def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int):
- existing_inbounds = set(
- (i.protocol, i.tag, i.config) for i in db.query(Inbound).filter(Inbound.node_id == node_id).all())
- inbounds_set = set((json.loads(i.config)["protocol"], i.tag, i.config) for i in list(inbounds))
- new_inbounds = inbounds_set - existing_inbounds
- deleted_inbounds = existing_inbounds - inbounds_set
- # difference = [ i for i in existing_inbounds if (i.protocol, i.tag) not in existing_inbounds ]
- if not new_inbounds and not deleted_inbounds:
- return
- """stmt = (
- delete(Inbound)
- .where(Inbound.node_id == node_id)
- .execution_options(synchronize_session="fetch")
+ current_tags = [i[0] for i in db.execute(select(Inbound.tag).filter(Inbound.node_id == node_id)).all()]
+ updated_tags = set(i.tag for i in list(inbounds))
+ inbound_additions, tag_deletions = list(), set()
+ for tag in current_tags:
+ if tag not in updated_tags:
+ tag_deletions.add(tag)
+ db_inbounds = db.query(Inbound).where(and_(Inbound.node_id == node_id, Inbound.tag.in_(tag_deletions)))
+ for i in db_inbounds:
+ db.delete(i)
+
+ for inb in inbounds:
+ if inb.tag in current_tags:
+ stmt = update(Inbound).where(
+ and_(Inbound.node_id == node_id,
+ Inbound.tag == inb.tag)
+ ).values(
+ protocol=json.loads(inb.config)["protocol"], config=inb.config
)
- db.execute(stmt)"""
- new_inbounds = [Inbound(tag=i[1], protocol=i[0], config=i[2], node_id=node_id) for i in new_inbounds]
+ db.execute(stmt)
+ else:
+ inbound_additions.append(inb)
+ new_inbounds = [Inbound(tag=inb.tag,
+ protocol=json.loads(inb.config)["protocol"],
+ config=inb.config,
+ node_id=node_id) for inb in inbound_additions]
db.add_all(new_inbounds)
db.flush()
for i in new_inbounds:
diff --git a/app/marznode/grpcio.py b/app/marznode/grpcio.py
@@ -9,7 +9,7 @@ from grpc.aio import insecure_channel
from .base import MarzNodeBase
from .database import MarzNodeDB
from .marznode_pb2_grpc import MarzServiceStub
-from .marznode_pb2 import UserData, UsersData, Empty, User, Inbound
+from .marznode_pb2 import UserData, UsersData, Empty, User, Inbound, XrayLogsRequest, XrayConfig
from ..models.node import NodeStatus
logger = logging.getLogger(__name__)
@@ -102,3 +102,17 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB):
users = self.list_users()
await self._repopulate_users(users)
self.synced = True
+
+ async def get_logs(self, include_buffer=True):
+ async for response in self._stub.StreamXrayLogs(
+ XrayLogsRequest(include_buffer=include_buffer)
+ ):
+ yield response.line
+
+ async def restart_xray(self, config: str):
+ await self._stub.RestartXray(XrayConfig(configuration=config))
+ await self._sync()
+
+ async def get_xray_config(self):
+ response = await self._stub.FetchXrayConfig(Empty())
+ return response.configuration
diff --git a/app/marznode/grpclib.py b/app/marznode/grpclib.py
@@ -11,7 +11,7 @@ from grpclib.exceptions import StreamTerminatedError
from .base import MarzNodeBase
from .database import MarzNodeDB
from .marznode_grpc import MarzServiceStub
-from .marznode_pb2 import UserData, UsersData, Empty, User, Inbound
+from .marznode_pb2 import UserData, UsersData, Empty, User, Inbound, XrayLogsRequest, XrayConfig
from ..models.node import NodeStatus
logger = logging.getLogger(__name__)
@@ -54,24 +54,18 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB):
try:
await asyncio.wait_for(self._channel.__connect__(), timeout=2)
except Exception:
- logger.info("timeout for node, id: %i", self.id)
+ logger.debug("timeout for node, id: %i", self.id)
self.set_status(NodeStatus.unhealthy, "timeout")
self.synced = False
if self._streaming_task:
self._streaming_task.cancel()
- await asyncio.sleep(10)
- continue
else:
if not self.synced:
- try:
- await self._sync()
- self._streaming_task = asyncio.create_task(self._stream_user_updates())
- self.set_status(NodeStatus.healthy)
- logger.info("Connected to node %i", self.id)
- except:
- await asyncio.sleep(10)
- continue
- await asyncio.sleep(30)
+ await self._sync()
+ self._streaming_task = asyncio.create_task(self._stream_user_updates())
+ self.set_status(NodeStatus.healthy)
+ logger.info("Connected to node %i", self.id)
+ await asyncio.sleep(10)
async def _stream_user_updates(self):
try:
@@ -114,3 +108,18 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB):
users = self.list_users()
await self._repopulate_users(users)
self.synced = True
+
+ async def get_logs(self, include_buffer=True):
+ async with self._stub.StreamXrayLogs.open() as stm:
+ await stm.send_message(XrayLogsRequest(include_buffer=include_buffer))
+ while True:
+ response = await stm.recv_message()
+ yield response.line
+
+ async def restart_xray(self, config: str):
+ await self._stub.RestartXray(XrayConfig(configuration=config))
+ await self._sync()
+
+ async def get_xray_config(self):
+ response = await self._stub.FetchXrayConfig(Empty())
+ return response.configuration
diff --git a/app/marznode/marznode.proto b/app/marznode/marznode.proto
@@ -36,9 +36,24 @@ message UsersStats {
repeated UserStats users_stats = 1;
}
+message LogLine {
+ string line = 1;
+}
+
+message XrayConfig {
+ string configuration = 1;
+}
+
+message XrayLogsRequest {
+ bool include_buffer = 1;
+}
+
service MarzService {
rpc SyncUsers(stream UserData) returns (Empty);
rpc RepopulateUsers(UsersData) returns (Empty);
rpc FetchInbounds(Empty) returns (InboundsResponse);
rpc FetchUsersStats(Empty) returns (UsersStats);
+ rpc FetchXrayConfig(Empty) returns (XrayConfig);
+ rpc RestartXray(XrayConfig) returns (InboundsResponse);
+ rpc StreamXrayLogs(XrayLogsRequest) returns(stream LogLine);
}
diff --git a/app/marznode/marznode_grpc.py b/app/marznode/marznode_grpc.py
@@ -30,6 +30,18 @@ class MarzServiceBase(abc.ABC):
async def FetchUsersStats(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.UsersStats]') -> None:
pass
+ @abc.abstractmethod
+ async def FetchXrayConfig(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.XrayConfig]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def RestartXray(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.XrayConfig, app.marznode.marznode_pb2.InboundsResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def StreamXrayLogs(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.XrayLogsRequest, app.marznode.marznode_pb2.LogLine]') -> None:
+ pass
+
def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
return {
'/marznode.MarzService/SyncUsers': grpclib.const.Handler(
@@ -56,6 +68,24 @@ class MarzServiceBase(abc.ABC):
app.marznode.marznode_pb2.Empty,
app.marznode.marznode_pb2.UsersStats,
),
+ '/marznode.MarzService/FetchXrayConfig': grpclib.const.Handler(
+ self.FetchXrayConfig,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ app.marznode.marznode_pb2.Empty,
+ app.marznode.marznode_pb2.XrayConfig,
+ ),
+ '/marznode.MarzService/RestartXray': grpclib.const.Handler(
+ self.RestartXray,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ app.marznode.marznode_pb2.XrayConfig,
+ app.marznode.marznode_pb2.InboundsResponse,
+ ),
+ '/marznode.MarzService/StreamXrayLogs': grpclib.const.Handler(
+ self.StreamXrayLogs,
+ grpclib.const.Cardinality.UNARY_STREAM,
+ app.marznode.marznode_pb2.XrayLogsRequest,
+ app.marznode.marznode_pb2.LogLine,
+ ),
}
@@ -86,3 +116,21 @@ class MarzServiceStub:
app.marznode.marznode_pb2.Empty,
app.marznode.marznode_pb2.UsersStats,
)
+ self.FetchXrayConfig = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/marznode.MarzService/FetchXrayConfig',
+ app.marznode.marznode_pb2.Empty,
+ app.marznode.marznode_pb2.XrayConfig,
+ )
+ self.RestartXray = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/marznode.MarzService/RestartXray',
+ app.marznode.marznode_pb2.XrayConfig,
+ app.marznode.marznode_pb2.InboundsResponse,
+ )
+ self.StreamXrayLogs = grpclib.client.UnaryStreamMethod(
+ channel,
+ '/marznode.MarzService/StreamXrayLogs',
+ app.marznode.marznode_pb2.XrayLogsRequest,
+ app.marznode.marznode_pb2.LogLine,
+ )
diff --git a/app/marznode/marznode_pb2.py b/app/marznode/marznode_pb2.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: app/marznode/marznode.proto
-# Protobuf Python Version: 4.25.0
+# Protobuf Python Version: 4.25.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
@@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61pp/marznode/marznode.proto\x12\x08marznode\"\x07\n\x05\x45mpty\"6\n\x07Inbound\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x13\n\x06\x63onfig\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_config\"7\n\x10InboundsResponse\x12#\n\x08inbounds\x18\x01 \x03(\x0b\x32\x11.marznode.Inbound\"1\n\x04User\x12\n\n\x02id\x18\x01 \x01(\r\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\"M\n\x08UserData\x12\x1c\n\x04user\x18\x01 \x01(\x0b\x32\x0e.marznode.User\x12#\n\x08inbounds\x18\x02 \x03(\x0b\x32\x11.marznode.Inbound\"3\n\tUsersData\x12&\n\nusers_data\x18\x01 \x03(\x0b\x32\x12.marznode.UserData\"j\n\nUsersStats\x12\x33\n\x0busers_stats\x18\x01 \x03(\x0b\x32\x1e.marznode.UsersStats.UserStats\x1a\'\n\tUserStats\x12\x0b\n\x03uid\x18\x01 \x01(\r\x12\r\n\x05usage\x18\x02 \x01(\x04\x32\xf2\x01\n\x0bMarzService\x12\x32\n\tSyncUsers\x12\x12.marznode.UserData\x1a\x0f.marznode.Empty(\x01\x12\x37\n\x0fRepopulateUsers\x12\x13.marznode.UsersData\x1a\x0f.marznode.Empty\x12<\n\rFetchInbounds\x12\x0f.marznode.Empty\x1a\x1a.marznode.InboundsResponse\x12\x38\n\x0f\x46\x65tchUsersStats\x12\x0f.marznode.Empty\x1a\x14.marznode.UsersStatsb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61pp/marznode/marznode.proto\x12\x08marznode\"\x07\n\x05\x45mpty\"6\n\x07Inbound\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x13\n\x06\x63onfig\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_config\"7\n\x10InboundsResponse\x12#\n\x08inbounds\x18\x01 \x03(\x0b\x32\x11.marznode.Inbound\"1\n\x04User\x12\n\n\x02id\x18\x01 \x01(\r\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\"M\n\x08UserData\x12\x1c\n\x04user\x18\x01 \x01(\x0b\x32\x0e.marznode.User\x12#\n\x08inbounds\x18\x02 \x03(\x0b\x32\x11.marznode.Inbound\"3\n\tUsersData\x12&\n\nusers_data\x18\x01 \x03(\x0b\x32\x12.marznode.UserData\"j\n\nUsersStats\x12\x33\n\x0busers_stats\x18\x01 \x03(\x0b\x32\x1e.marznode.UsersStats.UserStats\x1a\'\n\tUserStats\x12\x0b\n\x03uid\x18\x01 \x01(\r\x12\r\n\x05usage\x18\x02 \x01(\x04\"\x17\n\x07LogLine\x12\x0c\n\x04line\x18\x01 \x01(\t\"#\n\nXrayConfig\x12\x15\n\rconfiguration\x18\x01 \x01(\t\")\n\x0fXrayLogsRequest\x12\x16\n\x0einclude_buffer\x18\x01 \x01(\x08\x32\xaf\x03\n\x0bMarzService\x12\x32\n\tSyncUsers\x12\x12.marznode.UserData\x1a\x0f.marznode.Empty(\x01\x12\x37\n\x0fRepopulateUsers\x12\x13.marznode.UsersData\x1a\x0f.marznode.Empty\x12<\n\rFetchInbounds\x12\x0f.marznode.Empty\x1a\x1a.marznode.InboundsResponse\x12\x38\n\x0f\x46\x65tchUsersStats\x12\x0f.marznode.Empty\x1a\x14.marznode.UsersStats\x12\x38\n\x0f\x46\x65tchXrayConfig\x12\x0f.marznode.Empty\x1a\x14.marznode.XrayConfig\x12?\n\x0bRestartXray\x12\x14.marznode.XrayConfig\x1a\x1a.marznode.InboundsResponse\x12@\n\x0eStreamXrayLogs\x12\x19.marznode.XrayLogsRequest\x1a\x11.marznode.LogLine0\x01\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -37,6 +37,12 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_globals['_USERSSTATS']._serialized_end=452
_globals['_USERSSTATS_USERSTATS']._serialized_start=413
_globals['_USERSSTATS_USERSTATS']._serialized_end=452
- _globals['_MARZSERVICE']._serialized_start=455
- _globals['_MARZSERVICE']._serialized_end=697
+ _globals['_LOGLINE']._serialized_start=454
+ _globals['_LOGLINE']._serialized_end=477
+ _globals['_XRAYCONFIG']._serialized_start=479
+ _globals['_XRAYCONFIG']._serialized_end=514
+ _globals['_XRAYLOGSREQUEST']._serialized_start=516
+ _globals['_XRAYLOGSREQUEST']._serialized_end=557
+ _globals['_MARZSERVICE']._serialized_start=560
+ _globals['_MARZSERVICE']._serialized_end=991
# @@protoc_insertion_point(module_scope)
diff --git a/app/marznode/marznode_pb2.pyi b/app/marznode/marznode_pb2.pyi
@@ -59,3 +59,21 @@ class UsersStats(_message.Message):
USERS_STATS_FIELD_NUMBER: _ClassVar[int]
users_stats: _containers.RepeatedCompositeFieldContainer[UsersStats.UserStats]
def __init__(self, users_stats: _Optional[_Iterable[_Union[UsersStats.UserStats, _Mapping]]] = ...) -> None: ...
+
+class LogLine(_message.Message):
+ __slots__ = ("line",)
+ LINE_FIELD_NUMBER: _ClassVar[int]
+ line: str
+ def __init__(self, line: _Optional[str] = ...) -> None: ...
+
+class XrayConfig(_message.Message):
+ __slots__ = ("configuration",)
+ CONFIGURATION_FIELD_NUMBER: _ClassVar[int]
+ configuration: str
+ def __init__(self, configuration: _Optional[str] = ...) -> None: ...
+
+class XrayLogsRequest(_message.Message):
+ __slots__ = ("include_buffer",)
+ INCLUDE_BUFFER_FIELD_NUMBER: _ClassVar[int]
+ include_buffer: bool
+ def __init__(self, include_buffer: bool = ...) -> None: ...
diff --git a/app/marznode/marznode_pb2_grpc.py b/app/marznode/marznode_pb2_grpc.py
@@ -34,6 +34,21 @@ class MarzServiceStub(object):
request_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
response_deserializer=app_dot_marznode_dot_marznode__pb2.UsersStats.FromString,
)
+ self.FetchXrayConfig = channel.unary_unary(
+ '/marznode.MarzService/FetchXrayConfig',
+ request_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
+ response_deserializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString,
+ )
+ self.RestartXray = channel.unary_unary(
+ '/marznode.MarzService/RestartXray',
+ request_serializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString,
+ response_deserializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString,
+ )
+ self.StreamXrayLogs = channel.unary_stream(
+ '/marznode.MarzService/StreamXrayLogs',
+ request_serializer=app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.SerializeToString,
+ response_deserializer=app_dot_marznode_dot_marznode__pb2.LogLine.FromString,
+ )
class MarzServiceServicer(object):
@@ -63,6 +78,24 @@ class MarzServiceServicer(object):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
+ def FetchXrayConfig(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def RestartXray(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StreamXrayLogs(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
def add_MarzServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
@@ -86,6 +119,21 @@ def add_MarzServiceServicer_to_server(servicer, server):
request_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString,
response_serializer=app_dot_marznode_dot_marznode__pb2.UsersStats.SerializeToString,
),
+ 'FetchXrayConfig': grpc.unary_unary_rpc_method_handler(
+ servicer.FetchXrayConfig,
+ request_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString,
+ response_serializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString,
+ ),
+ 'RestartXray': grpc.unary_unary_rpc_method_handler(
+ servicer.RestartXray,
+ request_deserializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString,
+ response_serializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.SerializeToString,
+ ),
+ 'StreamXrayLogs': grpc.unary_stream_rpc_method_handler(
+ servicer.StreamXrayLogs,
+ request_deserializer=app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.FromString,
+ response_serializer=app_dot_marznode_dot_marznode__pb2.LogLine.SerializeToString,
+ ),
}
generic_handler = grpc.method_handlers_generic_handler(
'marznode.MarzService', rpc_method_handlers)
@@ -163,3 +211,54 @@ class MarzService(object):
app_dot_marznode_dot_marznode__pb2.UsersStats.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+ @staticmethod
+ def FetchXrayConfig(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/FetchXrayConfig',
+ app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
+ app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString,
+ options, channel_credentials,
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+ @staticmethod
+ def RestartXray(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/RestartXray',
+ app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString,
+ app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString,
+ options, channel_credentials,
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+ @staticmethod
+ def StreamXrayLogs(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_stream(request, target, '/marznode.MarzService/StreamXrayLogs',
+ app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.SerializeToString,
+ app_dot_marznode_dot_marznode__pb2.LogLine.FromString,
+ options, channel_credentials,
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
diff --git a/app/routes/node.py b/app/routes/node.py
@@ -1,21 +1,24 @@
+import json
import logging
from datetime import datetime
-from typing import List
+from typing import List, Annotated
import sqlalchemy
-from fastapi import APIRouter, Depends
+from fastapi import APIRouter, Depends, Body
from fastapi import HTTPException, WebSocket
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse
from app import marznode
from app.db import crud, get_tls_certificate
-from app.dependencies import DBDep, SudoAdminDep, sudo_admin, EndDateDep, StartDateDep
+from app.dependencies import DBDep, SudoAdminDep, sudo_admin, EndDateDep, StartDateDep, get_admin
from app.marznode import MarzNodeGRPCIO
from app.models.admin import Admin
from app.models.node import (NodeCreate, NodeModify, NodeResponse,
NodeSettings, NodeStatus, NodesUsageResponse)
logger = logging.getLogger(__name__)
-router = APIRouter(prefix="/nodes", tags=['Node'], dependencies=[Depends(sudo_admin)])
+router = APIRouter(prefix="/nodes", tags=['Node'])
@router.get("/settings", response_model=NodeSettings)
@@ -28,6 +31,19 @@ def get_node_settings(db: DBDep,
)
+@router.get("/usage", response_model=NodesUsageResponse)
+def get_usage(db: DBDep,
+ admin: SudoAdminDep,
+ start_date: StartDateDep,
+ end_date: EndDateDep):
+ """
+ Get nodes usage
+ """
+ usages = crud.get_nodes_usage(db, start_date, end_date)
+
+ return {"usages": usages}
+
+
@router.post("", response_model=NodeResponse)
async def add_node(new_node: NodeCreate,
db: DBDep,
@@ -56,40 +72,30 @@ def get_node(node_id: int,
return db_node
-@router.websocket("/{node_id}/logs", dependencies=None)
+@router.websocket("/{node_id}/logs")
async def node_logs(node_id: int,
websocket: WebSocket,
- db: DBDep):
+ db: DBDep,
+ include_buffer: bool = True):
token = (
websocket.query_params.get('token')
or
websocket.headers.get('Authorization', '').removeprefix("Bearer ")
)
- admin = Admin.get_admin(token, db)
- if not admin:
- return await websocket.close(reason="Unauthorized", code=4401)
+ admin = get_admin(db, token)
- if not admin.is_sudo:
+ if not admin or not admin.is_sudo:
return await websocket.close(reason="You're not allowed", code=4403)
if not marznode.nodes.get(node_id):
return await websocket.close(reason="Node not found", code=4404)
- if not await xray.nodes[node_id].is_healthy():
- return await websocket.close(reason="Node is not connected", code=4400)
-
- interval = websocket.query_params.get('interval')
- if interval:
- try:
- interval = float(interval)
- except ValueError:
- return await websocket.close(reason="Invalid interval value", code=4400)
- if interval > 10:
- return await websocket.close(reason="Interval must be more than 0 and at most 10 seconds", code=4400)
+ # if not await marznode.nodes[node_id].is_healthy:
+ # return await websocket.close(reason="Node is not connected", code=4400)
await websocket.accept()
- async for l in xray.nodes[node_id].get_logs():
- await websocket.send_text(l)
+ async for line in marznode.nodes[node_id].get_logs(include_buffer=include_buffer):
+ await websocket.send_text(line)
@router.get("", response_model=List[NodeResponse])
@@ -129,6 +135,27 @@ async def reconnect_node(node_id: int,
return {}
+@router.get("/{node_id}/xray_config")
+async def get_node_xray_config(node_id: int,
+ db: DBDep,
+ admin: SudoAdminDep):
+ if not (node := marznode.nodes.get(node_id)):
+ raise HTTPException(status_code=404, detail="Node not found")
+ return json.loads(await node.get_xray_config())
+
+
+@router.put("/{node_id}/xray_config")
+async def alter_node_xray_config(node_id: int,
+ db: DBDep,
+ admin: SudoAdminDep,
+ body: Annotated[dict, Body()]):
+ if not (node := marznode.nodes.get(node_id)):
+ raise HTTPException(status_code=404, detail="Node not found")
+ xray_config = json.dumps(body)
+ await node.restart_xray(xray_config)
+ return {}
+
+
@router.delete("/{node_id}")
async def remove_node(node_id: int,
db: DBDep,
@@ -142,16 +169,3 @@ async def remove_node(node_id: int,
logger.info(f"Node `%s` deleted", db_node.name)
return {}
-
-
-@router.get("/usage", response_model=NodesUsageResponse)
-def get_usage(db: DBDep,
- admin: SudoAdminDep,
- start_date: StartDateDep,
- end_date: EndDateDep):
- """
- Get nodes usage
- """
- usages = crud.get_nodes_usage(db, start_date, end_date)
-
- return {"usages": usages}