+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
serverExternalPackages: ["ssh2", "docker-modem", "dockerode"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+300
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
@@ -23,9 +24,11 @@
|
|||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
|
"os-name": "^6.1.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
|
"systeminformation": "^5.28.2",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1246,6 +1249,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-label": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||||
@@ -2483,6 +2509,20 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-spawn": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^3.1.0",
|
||||||
|
"shebang-command": "^2.0.0",
|
||||||
|
"which": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -2804,6 +2844,29 @@
|
|||||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/execa": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
|
"get-stream": "^8.0.1",
|
||||||
|
"human-signals": "^5.0.0",
|
||||||
|
"is-stream": "^3.0.0",
|
||||||
|
"merge-stream": "^2.0.0",
|
||||||
|
"npm-run-path": "^5.1.0",
|
||||||
|
"onetime": "^6.0.0",
|
||||||
|
"signal-exit": "^4.1.0",
|
||||||
|
"strip-final-newline": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-equals": {
|
"node_modules/fast-equals": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||||
@@ -2919,6 +2982,18 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -2983,6 +3058,15 @@
|
|||||||
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
|
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/human-signals": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
@@ -3027,6 +3111,24 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-stream": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/isexe": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||||
@@ -3352,6 +3454,18 @@
|
|||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/macos-release": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -3371,6 +3485,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/merge-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
@@ -3392,6 +3512,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-fn": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mkdirp-classic": {
|
"node_modules/mkdirp-classic": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
@@ -3510,6 +3642,33 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/npm-run-path": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/npm-run-path/node_modules/path-key": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -3528,6 +3687,46 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/onetime": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-fn": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/os-name": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/os-name/-/os-name-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-zBd1G8HkewNd2A8oQ8c6BN/f/c9EId7rSUueOLGu28govmUctXmM+3765GwsByv9nYUdrLqHphXlYIc86saYsg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"macos-release": "^3.3.0",
|
||||||
|
"windows-release": "^6.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-key": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -3892,6 +4091,39 @@
|
|||||||
"@img/sharp-win32-x64": "0.34.5"
|
"@img/sharp-win32-x64": "0.34.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shebang-command": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"shebang-regex": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shebang-regex": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -3959,6 +4191,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-final-newline": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/styled-jsx": {
|
"node_modules/styled-jsx": {
|
||||||
"version": "5.1.6",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||||
@@ -3982,6 +4226,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/systeminformation": {
|
||||||
|
"version": "5.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.28.2.tgz",
|
||||||
|
"integrity": "sha512-AWrPnmbIcfjLX7wiQMd7r6bl3PDaGoqvrfn8pCLJbk9CiB+vQXTk8RVxNs2vQRTezYlhb9skxEIyAPTm1If5bQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"os": [
|
||||||
|
"darwin",
|
||||||
|
"linux",
|
||||||
|
"win32",
|
||||||
|
"freebsd",
|
||||||
|
"openbsd",
|
||||||
|
"netbsd",
|
||||||
|
"sunos",
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"systeminformation": "lib/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "Buy me a coffee",
|
||||||
|
"url": "https://www.buymeacoffee.com/systeminfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||||
@@ -4173,6 +4443,36 @@
|
|||||||
"d3-timer": "^3.0.1"
|
"d3-timer": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"isexe": "^2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-which": "bin/node-which"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/windows-release": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"execa": "^8.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
@@ -23,9 +24,11 @@
|
|||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
|
"os-name": "^6.1.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
|
"systeminformation": "^5.28.2",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { version } from "@/../package.json";
|
import { version } from "@/../package.json";
|
||||||
import testAuth from "../auth";
|
import testAuth from "../auth";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import { read } from "@/app/api/file";
|
||||||
|
import { NodeJsonTemplate } from "../types";
|
||||||
|
import osName from "os-name";
|
||||||
|
|
||||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
const auth = testAuth(req);
|
const auth = await testAuth(req);
|
||||||
|
|
||||||
if (auth) {
|
if (auth) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -13,9 +16,13 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { node_id } = await read(NodeJsonTemplate, "data", "node.json");
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: "ok",
|
message: "ok",
|
||||||
app: "dockship",
|
app: "dockship",
|
||||||
version,
|
version,
|
||||||
|
node_id,
|
||||||
|
os: osName(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -13,9 +13,21 @@ export async function read<T extends object>(
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
} catch {
|
} catch {
|
||||||
await fs.mkdir(dirPath, { recursive: true });
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
await fs.writeFile(filePath, JSON.stringify(template, null, 2), "utf-8");
|
await fs.writeFile(filePath, JSON.stringify(template(), null, 2), "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await fs.readFile(filePath, "utf-8");
|
const content = await fs.readFile(filePath, "utf-8");
|
||||||
return JSON.parse(content);
|
return JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function write<T extends object>(
|
||||||
|
val: T,
|
||||||
|
fp: string,
|
||||||
|
...filePaths: string[]
|
||||||
|
) {
|
||||||
|
const filePath = path.join(process.cwd(), fp, ...filePaths);
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(val, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import testAuth from "../auth";
|
import testAuth from "../auth";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import { getDocker } from "@/core/docker";
|
||||||
|
import { Docker } from "@/lib/types";
|
||||||
|
|
||||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
const auth = await testAuth(req);
|
const auth = await testAuth(req);
|
||||||
@@ -12,8 +14,11 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const docker = await getDocker();
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: "ok",
|
message: "ok",
|
||||||
containers: [],
|
version: docker.version.Version,
|
||||||
});
|
containers: docker.containers,
|
||||||
|
} as Docker);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import testAuth from "../auth";
|
||||||
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import si from "systeminformation";
|
||||||
|
import { average } from "@/lib/format";
|
||||||
|
|
||||||
|
type Usage = {
|
||||||
|
cpu: number;
|
||||||
|
memory: number;
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const usageSecs: Usage[] = [];
|
||||||
|
|
||||||
|
const usageMins: (Usage & { time: string })[] = [];
|
||||||
|
|
||||||
|
const usageHours: (Usage & { time: string })[] = new Array(12).fill({
|
||||||
|
time: "00:00",
|
||||||
|
cpu: 0,
|
||||||
|
memory: 0,
|
||||||
|
in: 0,
|
||||||
|
out: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
var hr = 0;
|
||||||
|
|
||||||
|
function getTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const hours = String(now.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
const [mem, load, network] = await Promise.all([
|
||||||
|
si.mem(),
|
||||||
|
si.currentLoad(),
|
||||||
|
si.networkStats(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sec = {
|
||||||
|
cpu: load.currentLoad,
|
||||||
|
memory: ((mem.total - mem.available) / mem.total) * 100,
|
||||||
|
in: average(network, (n) => n.rx_bytes),
|
||||||
|
out: average(network, (n) => n.tx_bytes),
|
||||||
|
};
|
||||||
|
usageSecs.push(sec);
|
||||||
|
|
||||||
|
if (usageSecs.length === 10) {
|
||||||
|
usageHours[hr] = { time: getTime(), ...sec };
|
||||||
|
} else if (usageSecs.length >= 60) {
|
||||||
|
usageMins.push({
|
||||||
|
time: getTime(),
|
||||||
|
cpu: average(usageSecs, (s) => s.cpu),
|
||||||
|
memory: average(usageSecs, (s) => s.memory),
|
||||||
|
in: average(usageSecs, (s) => s.in),
|
||||||
|
out: average(usageSecs, (s) => s.out),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (usageMins.length >= 60) {
|
||||||
|
const time = getTime();
|
||||||
|
if (hr >= 12) {
|
||||||
|
usageHours.shift();
|
||||||
|
usageHours.push({
|
||||||
|
time,
|
||||||
|
cpu: average(usageMins, (s) => s.cpu),
|
||||||
|
memory: average(usageMins, (s) => s.memory),
|
||||||
|
in: average(usageMins, (s) => s.in),
|
||||||
|
out: average(usageMins, (s) => s.out),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
usageHours[hr] = {
|
||||||
|
time,
|
||||||
|
cpu: average(usageMins, (s) => s.cpu),
|
||||||
|
memory: average(usageMins, (s) => s.memory),
|
||||||
|
in: average(usageMins, (s) => s.in),
|
||||||
|
out: average(usageMins, (s) => s.out),
|
||||||
|
};
|
||||||
|
hr++;
|
||||||
|
}
|
||||||
|
|
||||||
|
usageMins.splice(0, usageSecs.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
usageSecs.splice(0, usageSecs.length);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
const auth = await testAuth(req);
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: auth },
|
||||||
|
{ status: StatusCodes.UNAUTHORIZED }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "ok",
|
||||||
|
metrics: usageHours,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { version } from "@/../package.json";
|
import { version } from "@/../package.json";
|
||||||
import { read } from "@/app/api/file";
|
|
||||||
import { NodeJsonTemplate } from "@/app/api/types";
|
|
||||||
|
|
||||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
const { node_id } = await read(NodeJsonTemplate, "data", "node.json");
|
return NextResponse.json({ app: "dockship", version });
|
||||||
|
|
||||||
return NextResponse.json({ app: "dockship", version, node_id });
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import testAuth from "../auth";
|
||||||
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import si from "systeminformation";
|
||||||
|
import { NodeLiveData } from "@/lib/types";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
const auth = await testAuth(req);
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: auth },
|
||||||
|
{ status: StatusCodes.UNAUTHORIZED }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [cpu, mem, load, disk, network] = await Promise.all([
|
||||||
|
si.cpu(),
|
||||||
|
si.mem(),
|
||||||
|
si.currentLoad(),
|
||||||
|
si.fsSize(),
|
||||||
|
si.networkStats(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const disks = await si.fsSize();
|
||||||
|
|
||||||
|
const root = disks.find((d) => d.mount === "/" || d.fs.includes("disk3"));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "ok",
|
||||||
|
liveData: {
|
||||||
|
cpu: {
|
||||||
|
manufacturer: cpu.manufacturer,
|
||||||
|
brand: cpu.brand,
|
||||||
|
cores: cpu.cores,
|
||||||
|
usage: load.currentLoad,
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
total: mem.total,
|
||||||
|
used: mem.used,
|
||||||
|
free: mem.free,
|
||||||
|
usage: ((mem.total - mem.available) / mem.total) * 100,
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
used: (root?.size || 0) - (root?.available || 0),
|
||||||
|
size: root?.size || 0,
|
||||||
|
available: root?.available || 0,
|
||||||
|
},
|
||||||
|
disks: disk.map((d) => ({
|
||||||
|
fs: d.fs,
|
||||||
|
size: d.size,
|
||||||
|
used: d.used,
|
||||||
|
usage: d.use,
|
||||||
|
})),
|
||||||
|
network: network.map((n) => ({
|
||||||
|
iface: n.iface,
|
||||||
|
rx: n.rx_bytes,
|
||||||
|
tx: n.tx_bytes,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
} as NodeLiveData);
|
||||||
|
}
|
||||||
+154
-86
@@ -45,96 +45,164 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(0.97 0.02 225.66);
|
--background: oklch(0.9842 0.0010 264.5478);
|
||||||
--foreground: oklch(0.15 0.02 269.18);
|
--foreground: oklch(0.2115 0.0072 264.4099);
|
||||||
--card: oklch(0.98 0.01 228.79);
|
--card: oklch(1.0000 0 0);
|
||||||
--card-foreground: oklch(0.15 0.02 269.18);
|
--card-foreground: oklch(0.2115 0.0072 264.4099);
|
||||||
--popover: oklch(0.98 0.01 238.45);
|
--popover: oklch(1.0000 0 0);
|
||||||
--popover-foreground: oklch(0.15 0.02 269.18);
|
--popover-foreground: oklch(0.2115 0.0072 264.4099);
|
||||||
--primary: oklch(0.71 0.15 239.15);
|
--primary: oklch(0.5096 0.1483 253.4619);
|
||||||
--primary-foreground: oklch(0.94 0.03 232.39);
|
--primary-foreground: oklch(1.0000 0 0);
|
||||||
--secondary: oklch(0.91 0.03 229.20);
|
--secondary: oklch(0.9210 0.0088 247.9219);
|
||||||
--secondary-foreground: oklch(0.15 0.02 269.18);
|
--secondary-foreground: oklch(0.4114 0.0323 248.6180);
|
||||||
--muted: oklch(0.89 0.02 225.69);
|
--muted: oklch(0.9607 0.0044 247.8691);
|
||||||
--muted-foreground: oklch(0.36 0.03 231.55);
|
--muted-foreground: oklch(0.5921 0.0247 248.2437);
|
||||||
--accent: oklch(0.88 0.02 235.72);
|
--accent: oklch(0.9607 0.0044 247.8691);
|
||||||
--accent-foreground: oklch(0.34 0.05 229.20);
|
--accent-foreground: oklch(0.4144 0.0162 248.2179);
|
||||||
--destructive: oklch(0.61 0.24 20.96);
|
--destructive: oklch(0.5875 0.2303 28.2622);
|
||||||
--border: oklch(0.82 0.02 240.77);
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
--input: oklch(0.82 0.02 240.77);
|
--border: oklch(0.9050 0.0107 247.9443);
|
||||||
--ring: oklch(0.55 0.10 235.72);
|
--input: oklch(0.9210 0.0088 247.9219);
|
||||||
--chart-1: oklch(0.57 0.11 228.92);
|
--ring: oklch(0.5096 0.1483 253.4619);
|
||||||
--chart-2: oklch(0.45 0.10 270.08);
|
--chart-1: oklch(0.5096 0.1483 253.4619);
|
||||||
--chart-3: oklch(0.65 0.15 159.03);
|
--chart-2: oklch(0.6610 0.1560 154.8128);
|
||||||
--chart-4: oklch(0.75 0.10 100.01);
|
--chart-3: oklch(0.7139 0.1737 55.1486);
|
||||||
--chart-5: oklch(0.55 0.15 299.88);
|
--chart-4: oklch(0.5103 0.2201 301.6038);
|
||||||
--sidebar: oklch(0.93 0.01 238.46);
|
--chart-5: oklch(0.5875 0.2303 28.2622);
|
||||||
--sidebar-foreground: oklch(0.15 0.02 269.18);
|
--sidebar: oklch(0.9605 0.0024 264.5432);
|
||||||
--sidebar-primary: oklch(0.57 0.11 228.92);
|
--sidebar-foreground: oklch(0.2639 0.0102 264.3844);
|
||||||
--sidebar-primary-foreground: oklch(0.99 0.01 203.97);
|
--sidebar-primary: oklch(0.5546 0.1629 253.5932);
|
||||||
--sidebar-accent: oklch(0.88 0.02 235.72);
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.15 0.02 269.18);
|
--sidebar-accent: oklch(0.9210 0.0088 247.9219);
|
||||||
--sidebar-border: oklch(0.82 0.02 240.77);
|
--sidebar-accent-foreground: oklch(0.4144 0.0162 248.2179);
|
||||||
--sidebar-ring: oklch(0.57 0.11 228.92);
|
--sidebar-border: oklch(0.8809 0.0134 247.9797);
|
||||||
|
--sidebar-ring: oklch(0.5096 0.1483 253.4619);
|
||||||
--font-sans: 'Source Code Pro', 'Geist', 'Geist Fallback', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-sans: 'Inter', sans-serif;
|
||||||
--font-serif: 'Source Serif 4', 'Geist', 'Geist Fallback', ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
--font-serif: 'Lora', serif;
|
||||||
--font-mono: 'Source Code Pro', 'Geist Mono', 'Geist Mono Fallback', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
--font-mono: 'Fira Code', monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
--radius: 0rem;
|
--shadow-x: 0rem;
|
||||||
|
--shadow-y: 0.25rem;
|
||||||
--shadow-2xs: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.03);
|
--shadow-blur: 0.5rem;
|
||||||
--shadow-xs: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.03);
|
--shadow-spread: 0rem;
|
||||||
--shadow-sm: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 1px 2px -1px oklch(0.49 0.09 235.00 / 0.06);
|
--shadow-opacity: 0.1;
|
||||||
--shadow: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 1px 2px -1px oklch(0.49 0.09 235.00 / 0.06);
|
--shadow-color: 0 0 0;
|
||||||
--shadow-md: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 2px 4px -1px oklch(0.49 0.09 235.00 / 0.06);
|
--shadow-2xs: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.05);
|
||||||
--shadow-lg: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 4px 6px -1px oklch(0.49 0.09 235.00 / 0.06);
|
--shadow-xs: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.05);
|
||||||
--shadow-xl: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 8px 10px -1px oklch(0.49 0.09 235.00 / 0.06);
|
--shadow-sm: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 1px 2px -1px hsl(0 0 0 / 0.10);
|
||||||
--shadow-2xl: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.15);
|
--shadow: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 1px 2px -1px hsl(0 0 0 / 0.10);
|
||||||
|
--shadow-md: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 2px 4px -1px hsl(0 0 0 / 0.10);
|
||||||
|
--shadow-lg: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 4px 6px -1px hsl(0 0 0 / 0.10);
|
||||||
|
--shadow-xl: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 8px 10px -1px hsl(0 0 0 / 0.10);
|
||||||
|
--shadow-2xl: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.25);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.18 0.02 271.27);
|
--background: oklch(0.1984 0.0154 256.8228);
|
||||||
--foreground: oklch(0.90 0.01 238.47);
|
--foreground: oklch(0.8809 0.0134 247.9797);
|
||||||
--card: oklch(0.22 0.02 271.67);
|
--card: oklch(0.2292 0.0222 256.8398);
|
||||||
--card-foreground: oklch(0.90 0.01 238.47);
|
--card-foreground: oklch(0.8809 0.0134 247.9797);
|
||||||
--popover: oklch(0.22 0.02 271.67);
|
--popover: oklch(0.2292 0.0222 256.8398);
|
||||||
--popover-foreground: oklch(0.90 0.01 238.47);
|
--popover-foreground: oklch(0.8809 0.0134 247.9797);
|
||||||
--primary: oklch(52.5% 0.105 236);
|
--primary: oklch(0.5096 0.1483 253.4619);
|
||||||
--primary-foreground: oklch(95.1% 0.00932 232.789);
|
--primary-foreground: oklch(0.9843 0.0017 247.8393);
|
||||||
--secondary: oklch(0.28 0.03 270.91);
|
--secondary: oklch(0.3095 0.0306 256.8416);
|
||||||
--secondary-foreground: oklch(0.90 0.01 238.47);
|
--secondary-foreground: oklch(0.8809 0.0134 247.9797);
|
||||||
--muted: oklch(0.28 0.03 270.91);
|
--muted: oklch(0.2605 0.0240 256.8358);
|
||||||
--muted-foreground: oklch(0.60 0.03 269.46);
|
--muted-foreground: oklch(0.6780 0.0191 248.1098);
|
||||||
--accent: oklch(0.28 0.03 270.91);
|
--accent: oklch(0.2605 0.0240 256.8358);
|
||||||
--accent-foreground: oklch(0.90 0.01 238.47);
|
--accent-foreground: oklch(0.8809 0.0134 247.9797);
|
||||||
--destructive: oklch(0.64 0.25 19.69);
|
--destructive: oklch(0.5875 0.2303 28.2622);
|
||||||
--border: oklch(0.90 0.01 238.47 / 15%);
|
--destructive-foreground: oklch(0.9843 0.0017 247.8393);
|
||||||
--input: oklch(0.90 0.01 238.47 / 20%);
|
--border: oklch(0.3567 0.0369 256.8455);
|
||||||
--ring: oklch(0.66 0.13 227.70);
|
--input: oklch(0.3095 0.0306 256.8416);
|
||||||
--chart-1: oklch(0.66 0.13 227.70);
|
--ring: oklch(0.5096 0.1483 253.4619);
|
||||||
--chart-2: oklch(0.60 0.10 269.83);
|
--chart-1: oklch(0.5096 0.1483 253.4619);
|
||||||
--chart-3: oklch(0.70 0.15 159.83);
|
--chart-2: oklch(0.6610 0.1560 154.8128);
|
||||||
--chart-4: oklch(0.80 0.10 100.65);
|
--chart-3: oklch(0.7139 0.1737 55.1486);
|
||||||
--chart-5: oklch(0.60 0.15 300.14);
|
--chart-4: oklch(0.5103 0.2201 301.6038);
|
||||||
--sidebar: oklch(0.22 0.02 271.67);
|
--chart-5: oklch(0.5875 0.2303 28.2622);
|
||||||
--sidebar-foreground: oklch(0.90 0.01 238.47);
|
--sidebar: oklch(0.2292 0.0222 256.8398);
|
||||||
--sidebar-primary: oklch(0.66 0.13 227.70);
|
--sidebar-foreground: oklch(0.9210 0.0088 247.9219);
|
||||||
--sidebar-primary-foreground: oklch(0.18 0.02 271.27);
|
--sidebar-primary: oklch(0.5096 0.1483 253.4619);
|
||||||
--sidebar-accent: oklch(0.28 0.03 270.91);
|
--sidebar-primary-foreground: oklch(0.9843 0.0017 247.8393);
|
||||||
--sidebar-accent-foreground: oklch(0.90 0.01 238.47);
|
--sidebar-accent: oklch(0.2605 0.0240 256.8358);
|
||||||
--sidebar-border: oklch(0.90 0.01 238.47 / 15%);
|
--sidebar-accent-foreground: oklch(0.8809 0.0134 247.9797);
|
||||||
--sidebar-ring: oklch(0.66 0.13 227.70);
|
--sidebar-border: oklch(0.3567 0.0369 256.8455);
|
||||||
|
--sidebar-ring: oklch(0.5096 0.1483 253.4619);
|
||||||
|
--font-sans: 'Roboto Mono', monospace;
|
||||||
|
--font-serif: 'Roboto Slab', serif;
|
||||||
|
--font-mono: 'Fira Code', monospace;
|
||||||
|
--radius: 0.25rem;
|
||||||
|
--shadow-x: 0rem;
|
||||||
|
--shadow-y: 0.5rem;
|
||||||
|
--shadow-blur: 0.75rem;
|
||||||
|
--shadow-spread: 0rem;
|
||||||
|
--shadow-opacity: 0.3;
|
||||||
|
--shadow-color: 0 0 0;
|
||||||
|
--shadow-2xs: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.15);
|
||||||
|
--shadow-xs: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.15);
|
||||||
|
--shadow-sm: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 1px 2px -1px hsl(0 0 0 / 0.30);
|
||||||
|
--shadow: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 1px 2px -1px hsl(0 0 0 / 0.30);
|
||||||
|
--shadow-md: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 2px 4px -1px hsl(0 0 0 / 0.30);
|
||||||
|
--shadow-lg: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 4px 6px -1px hsl(0 0 0 / 0.30);
|
||||||
|
--shadow-xl: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 8px 10px -1px hsl(0 0 0 / 0.30);
|
||||||
|
--shadow-2xl: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
--shadow-2xs: 0px 1px 2px 0px oklch(0 0 0 / 0.01);
|
@theme inline {
|
||||||
--shadow-xs: 0px 1px 2px 0px oklch(0 0 0 / 0.01);
|
--color-background: var(--background);
|
||||||
--shadow-sm: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 1px 2px -1px oklch(0 0 0 / 0.01);
|
--color-foreground: var(--foreground);
|
||||||
--shadow: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 1px 2px -1px oklch(0 0 0 / 0.01);
|
--color-card: var(--card);
|
||||||
--shadow-md: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 2px 4px -1px oklch(0 0 0 / 0.01);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--shadow-lg: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 4px 6px -1px oklch(0 0 0 / 0.01);
|
--color-popover: var(--popover);
|
||||||
--shadow-xl: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 8px 10px -1px oklch(0 0 0 / 0.01);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--shadow-2xl: 0px 1px 2px 0px oklch(0 0 0 / 0.03);
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|||||||
+138
-288
@@ -1,69 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
LineChart,
|
|
||||||
Line,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
Tooltip,
|
|
||||||
ResponsiveContainer,
|
|
||||||
AreaChart,
|
|
||||||
Area,
|
|
||||||
} from "recharts";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { type ChartConfig } from "@/components/ui/chart";
|
||||||
|
import GradientAreaChart from "@/components/GradientAreaChart";
|
||||||
|
import { getMetrics } from "@/core/client/metrics";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import RadialChart from "@/components/RadialChart";
|
import RadialChart from "@/components/RadialChart";
|
||||||
import { ArrowDownLeft, ArrowUpRight, Cpu, MemoryStick } from "lucide-react";
|
import { Node, NodeLiveData } from "@/lib/types";
|
||||||
|
import { getLiveNodes } from "@/core/client/node";
|
||||||
const cpuData = [
|
import { average } from "@/lib/format";
|
||||||
{ time: "00:00", value: 45 },
|
|
||||||
{ time: "01:00", value: 52 },
|
|
||||||
{ time: "02:00", value: 38 },
|
|
||||||
{ time: "03:00", value: 42 },
|
|
||||||
{ time: "04:00", value: 35 },
|
|
||||||
{ time: "05:00", value: 48 },
|
|
||||||
{ time: "06:00", value: 55 },
|
|
||||||
{ time: "07:00", value: 62 },
|
|
||||||
{ time: "08:00", value: 78 },
|
|
||||||
{ time: "09:00", value: 85 },
|
|
||||||
{ time: "10:00", value: 72 },
|
|
||||||
{ time: "11:00", value: 68 },
|
|
||||||
{ time: "12:00", value: 75 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const memoryData = [
|
|
||||||
{ time: "00:00", value: 62 },
|
|
||||||
{ time: "01:00", value: 64 },
|
|
||||||
{ time: "02:00", value: 58 },
|
|
||||||
{ time: "03:00", value: 61 },
|
|
||||||
{ time: "04:00", value: 59 },
|
|
||||||
{ time: "05:00", value: 63 },
|
|
||||||
{ time: "06:00", value: 67 },
|
|
||||||
{ time: "07:00", value: 72 },
|
|
||||||
{ time: "08:00", value: 78 },
|
|
||||||
{ time: "09:00", value: 82 },
|
|
||||||
{ time: "10:00", value: 76 },
|
|
||||||
{ time: "11:00", value: 74 },
|
|
||||||
{ time: "12:00", value: 71 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const networkData = [
|
|
||||||
{ time: "00:00", in: 12, out: 8 },
|
|
||||||
{ time: "01:00", in: 15, out: 10 },
|
|
||||||
{ time: "02:00", in: 8, out: 5 },
|
|
||||||
{ time: "03:00", in: 10, out: 7 },
|
|
||||||
{ time: "04:00", in: 6, out: 4 },
|
|
||||||
{ time: "05:00", in: 14, out: 9 },
|
|
||||||
{ time: "06:00", in: 22, out: 15 },
|
|
||||||
{ time: "07:00", in: 35, out: 28 },
|
|
||||||
{ time: "08:00", in: 48, out: 38 },
|
|
||||||
{ time: "09:00", in: 52, out: 42 },
|
|
||||||
{ time: "10:00", in: 45, out: 35 },
|
|
||||||
{ time: "11:00", in: 42, out: 32 },
|
|
||||||
{ time: "12:00", in: 38, out: 28 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const containerMetrics = [
|
const containerMetrics = [
|
||||||
{ name: "api-gateway-1", cpu: 45, memory: 62, network: "12.4 MB/s" },
|
{ name: "api-gateway-1", cpu: 45, memory: 62, network: "12.4 MB/s" },
|
||||||
@@ -76,244 +24,146 @@ const containerMetrics = [
|
|||||||
{ name: "worker-queue-1", cpu: 55, memory: 48, network: "5.6 MB/s" },
|
{ name: "worker-queue-1", cpu: 55, memory: 48, network: "5.6 MB/s" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const cpuChartConfig = {
|
||||||
|
cpu: { label: "CPU Usage", color: "var(--chart-1)" },
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
const memoryChartConfig = {
|
||||||
|
memory: { label: "RAM Usage", color: "var(--chart-3)" },
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
const networkChartConfig = {
|
||||||
|
in: { label: "Inbound", color: "var(--chart-2)" },
|
||||||
|
out: { label: "Outbound", color: "var(--chart-4)" },
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export default function MonitoringPage() {
|
export default function MonitoringPage() {
|
||||||
|
const [data, setData] = useState<
|
||||||
|
{
|
||||||
|
time: string;
|
||||||
|
cpu: number;
|
||||||
|
memory: number;
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [liveData, setLiveData] = useState<{
|
||||||
|
cpuUsage: number;
|
||||||
|
ramUsage: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchNodes = async () => {
|
||||||
|
const nodes = await getLiveNodes();
|
||||||
|
setLiveData({
|
||||||
|
cpuUsage: average(nodes, (n) => n.liveData.cpu.usage),
|
||||||
|
ramUsage: average(nodes, (n) => n.liveData.memory.usage),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fetchNodes();
|
||||||
|
const interval = setInterval(fetchNodes, 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchMetrics() {
|
||||||
|
setData(await getMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMetrics();
|
||||||
|
|
||||||
|
const i = setInterval(fetchMetrics, 30000);
|
||||||
|
|
||||||
|
return () => clearInterval(i);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout
|
<DashboardLayout
|
||||||
title="Monitoring"
|
title="Monitoring"
|
||||||
subtitle="Resource utilization and metrics"
|
subtitle="Resource utilization and metrics"
|
||||||
>
|
>
|
||||||
{/* Summary Stats */}
|
{/* Summary Stats */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
{isMobile ? (
|
||||||
<Card>
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
<CardContent className="p-4">
|
<Card>
|
||||||
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
<CardContent className="px-4">
|
||||||
Cluster CPU
|
<RadialChart value={liveData?.cpuUsage || 0}>CPU</RadialChart>
|
||||||
</div>
|
</CardContent>
|
||||||
<div className="stat-value">68%</div>
|
</Card>
|
||||||
<Progress value={68} className="w-full" />
|
<Card>
|
||||||
</CardContent>
|
<CardContent className="px-4">
|
||||||
</Card>
|
<RadialChart value={liveData?.ramUsage || 0}>RAM</RadialChart>
|
||||||
<Card>
|
</CardContent>
|
||||||
<CardContent className="p-4">
|
</Card>
|
||||||
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
</div>
|
||||||
Cluster Memory
|
) : (
|
||||||
</div>
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
<div className="stat-value">71%</div>
|
<Card>
|
||||||
<Progress value={71} className="w-full" />
|
<CardContent className="p-4">
|
||||||
</CardContent>
|
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
</Card>
|
Cluster CPU
|
||||||
<Card>
|
</div>
|
||||||
<CardContent className="p-4">
|
<div className="stat-value">68%</div>
|
||||||
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
<Progress value={68} className="w-full" />
|
||||||
Network In
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
<div className="stat-value">38 MB/s</div>
|
<Card>
|
||||||
<div className="text-xs text-success mt-1">↑ 12% from avg</div>
|
<CardContent className="p-4">
|
||||||
</CardContent>
|
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
</Card>
|
Cluster Memory
|
||||||
<Card>
|
</div>
|
||||||
<CardContent className="p-4">
|
<div className="stat-value">71%</div>
|
||||||
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
<Progress value={71} className="w-full" />
|
||||||
Network Out
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
<div className="stat-value">28 MB/s</div>
|
<Card>
|
||||||
<div className="text-xs text-muted-foreground mt-1">
|
<CardContent className="p-4">
|
||||||
↓ 5% from avg
|
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
</div>
|
Network In
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
<div className="stat-value">38 MB/s</div>
|
||||||
</div>
|
<div className="text-xs text-success mt-1">↑ 12% from avg</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
|
Network Out
|
||||||
|
</div>
|
||||||
|
<div className="stat-value">28 MB/s</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
↓ 5% from avg
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Charts */}
|
{/* CPU & Memory Charts */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
||||||
<Card>
|
<GradientAreaChart
|
||||||
<CardHeader className="pb-2">
|
title="CPU Usage"
|
||||||
<CardTitle className="flex items-center justify-between flex-wrap gap-2">
|
data={data}
|
||||||
CPU Usage <Badge variant="outline">Last 12 hours</Badge>
|
config={cpuChartConfig}
|
||||||
</CardTitle>
|
max={100}
|
||||||
</CardHeader>
|
/>
|
||||||
<CardContent>
|
<GradientAreaChart
|
||||||
<div className="h-48 w-full">
|
title="Memory Usage"
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
data={data}
|
||||||
<AreaChart data={cpuData}>
|
config={memoryChartConfig}
|
||||||
<defs>
|
max={100}
|
||||||
<linearGradient
|
/>
|
||||||
id="cpuGradient"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="0"
|
|
||||||
y2="1"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="oklch(0.66 0.13 227.70)"
|
|
||||||
stopOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="oklch(0.66 0.13 227.70)"
|
|
||||||
stopOpacity={0}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<XAxis
|
|
||||||
dataKey="time"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
|
||||||
domain={[0, 100]}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: "hsl(0,0%,7%)",
|
|
||||||
border: "1px solid hsl(0,0%,18%)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
fontSize: "12px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="value"
|
|
||||||
stroke="oklch(0.66 0.13 227.70)"
|
|
||||||
strokeWidth={2}
|
|
||||||
fill="url(#cpuGradient)"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="flex items-center justify-between flex-wrap gap-2">
|
|
||||||
Memory Usage <Badge variant="outline">Last 12 hours</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="h-48 w-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<AreaChart data={memoryData}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id="memoryGradient"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="0"
|
|
||||||
y2="1"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="oklch(0.70 0.15 159.83)"
|
|
||||||
stopOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="oklch(0.70 0.15 159.83)"
|
|
||||||
stopOpacity={0}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<XAxis
|
|
||||||
dataKey="time"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
|
||||||
domain={[0, 100]}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: "hsl(0,0%,7%)",
|
|
||||||
border: "1px solid hsl(0,0%,18%)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
fontSize: "12px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="value"
|
|
||||||
stroke="oklch(0.70 0.15 159.83)"
|
|
||||||
strokeWidth={2}
|
|
||||||
fill="url(#memoryGradient)"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Network Chart */}
|
{/* Network Metrics */}
|
||||||
<Card className="mb-6">
|
<div className="mb-6">
|
||||||
<CardHeader className="pb-2">
|
<GradientAreaChart
|
||||||
<CardTitle className="flex items-center justify-between flex-wrap gap-2">
|
title="Network I/O"
|
||||||
Network I/O <Badge variant="outline">Last 12 hours</Badge>
|
data={data}
|
||||||
</CardTitle>
|
config={networkChartConfig}
|
||||||
</CardHeader>
|
/>
|
||||||
<CardContent>
|
</div>
|
||||||
<div className="h-48 w-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<LineChart data={networkData}>
|
|
||||||
<XAxis
|
|
||||||
dataKey="time"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: "hsl(0,0%,7%)",
|
|
||||||
border: "1px solid hsl(0,0%,18%)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
fontSize: "12px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="in"
|
|
||||||
stroke="oklch(0.80 0.10 100.65)"
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={false}
|
|
||||||
name="Inbound"
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="out"
|
|
||||||
stroke="oklch(0.60 0.15 300.14)"
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={false}
|
|
||||||
name="Outbound"
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-6 mt-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-0.5 bg-primary" />
|
|
||||||
<span className="text-xs text-muted-foreground">Inbound</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-0.5 bg-warning" />
|
|
||||||
<span className="text-xs text-muted-foreground">Outbound</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Container Metrics */}
|
{/* Container Metrics */}
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
+94
-73
@@ -4,71 +4,70 @@ import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
|
||||||
import { Server, Plus, Settings, Trash2 } from "lucide-react";
|
import { Server, Plus, Settings, Trash2 } from "lucide-react";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getDockerNode, getLiveNodes } from "@/core/client/node";
|
||||||
|
import { formatBytes } from "@/lib/format";
|
||||||
|
import { setNodes as updateNodes } from "@/core/client/node";
|
||||||
|
import AddNode from "@/components/dialogs/AddNode";
|
||||||
|
|
||||||
const nodes = [
|
function DiskUsage({ liveData }: NodeLiveData) {
|
||||||
{
|
const usage = (liveData.disk.used / liveData.disk.size) * 100;
|
||||||
id: "node-01",
|
|
||||||
name: "node-01",
|
return (
|
||||||
hostname: "dockship-node-01.local",
|
<div>
|
||||||
ip: "10.0.1.10",
|
<div className="text-xs text-muted-foreground mb-2">Disk</div>
|
||||||
os: "Ubuntu 22.04 LTS",
|
<div className="stat-value mb-2">{usage.toFixed(0)}%</div>
|
||||||
docker: "24.0.7",
|
<Progress value={usage} className="w-full" />
|
||||||
cpu: { cores: 8, usage: 72 },
|
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
||||||
memory: { total: "32 GB", used: "21.8 GB", percentage: 68 },
|
{formatBytes(liveData.disk.used)} / {formatBytes(liveData.disk.size)}
|
||||||
disk: { total: "500 GB", used: "245 GB", percentage: 49 },
|
</div>
|
||||||
containers: 8,
|
</div>
|
||||||
status: "running" as const,
|
);
|
||||||
labels: ["production", "primary"],
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "node-02",
|
|
||||||
name: "node-02",
|
|
||||||
hostname: "dockship-node-02.local",
|
|
||||||
ip: "10.0.1.11",
|
|
||||||
os: "Ubuntu 22.04 LTS",
|
|
||||||
docker: "24.0.7",
|
|
||||||
cpu: { cores: 8, usage: 45 },
|
|
||||||
memory: { total: "32 GB", used: "16.6 GB", percentage: 52 },
|
|
||||||
disk: { total: "500 GB", used: "178 GB", percentage: 36 },
|
|
||||||
containers: 6,
|
|
||||||
status: "running" as const,
|
|
||||||
labels: ["production"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "node-03",
|
|
||||||
name: "node-03",
|
|
||||||
hostname: "dockship-node-03.local",
|
|
||||||
ip: "10.0.1.12",
|
|
||||||
os: "Ubuntu 22.04 LTS",
|
|
||||||
docker: "24.0.7",
|
|
||||||
cpu: { cores: 16, usage: 89 },
|
|
||||||
memory: { total: "64 GB", used: "54.4 GB", percentage: 85 },
|
|
||||||
disk: { total: "1 TB", used: "612 GB", percentage: 61 },
|
|
||||||
containers: 12,
|
|
||||||
status: "running" as const,
|
|
||||||
labels: ["production", "high-memory"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function NodesPage() {
|
export default function NodesPage() {
|
||||||
|
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]);
|
||||||
|
const [dockerNodes, setDockerNodes] = useState<Docker[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchNodes = async () => {
|
||||||
|
const nodes = await getLiveNodes();
|
||||||
|
setNodes(nodes);
|
||||||
|
};
|
||||||
|
fetchNodes();
|
||||||
|
const interval = setInterval(fetchNodes, 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDocker = async () => {
|
||||||
|
setDockerNodes(await Promise.all(nodes.map(getDockerNode)));
|
||||||
|
};
|
||||||
|
fetchDocker();
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="Nodes" subtitle="Cluster node management">
|
<DashboardLayout title="Nodes" subtitle="Cluster node management">
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-2">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-2">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Badge>{nodes.length} nodes</Badge>
|
<Badge>{nodes.length} nodes</Badge>
|
||||||
<Badge>{nodes.filter((n) => n.status === "running").length} online</Badge>
|
<Badge>
|
||||||
|
{/* {nodes.filter((n) => n.status === "running").length} online */}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" className="gap-2">
|
<AddNode>
|
||||||
<Plus className="w-4 h-4" /> Add Node
|
<Button size="sm" className="gap-2">
|
||||||
</Button>
|
<Plus className="w-4 h-4" /> Add Node
|
||||||
|
</Button>
|
||||||
|
</AddNode>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{nodes.map((node) => (
|
{nodes.map((node, i) => (
|
||||||
<Card key={node.id}>
|
<Card key={i}>
|
||||||
<CardHeader className="flex flex-col sm:flex-row sm:items-start sm:justify-between pb-2 gap-2 sm:gap-0">
|
<CardHeader className="flex flex-col sm:flex-row sm:items-start sm:justify-between pb-2 gap-2 sm:gap-0">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-secondary flex items-center justify-center">
|
<div className="w-10 h-10 bg-secondary flex items-center justify-center">
|
||||||
@@ -76,11 +75,11 @@ export default function NodesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<StatusIndicator status={node.status} />
|
{/* <StatusIndicator status={node.status} /> */}
|
||||||
<CardTitle className="font-mono">{node.name}</CardTitle>
|
<CardTitle className="font-mono">{node.name}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground font-mono">
|
<div className="text-xs text-muted-foreground font-mono">
|
||||||
{node.hostname} • {node.ip}
|
{node.ip} • {node.ip}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +92,15 @@ export default function NodesPage() {
|
|||||||
<Button variant="ghost" size="icon-sm">
|
<Button variant="ghost" size="icon-sm">
|
||||||
<Settings className="w-4 h-4" />
|
<Settings className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon-sm">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={async () => {
|
||||||
|
const newNodes = nodes.filter((_, index) => index !== i);
|
||||||
|
setNodes(newNodes);
|
||||||
|
await updateNodes(newNodes);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +109,9 @@ export default function NodesPage() {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-6">
|
||||||
{/* System Info */}
|
{/* System Info */}
|
||||||
<div className="border-b sm:border-b-0 sm:border-r border-border pb-2 sm:pb-0 pr-0 sm:pr-6">
|
<div className="border-b sm:border-b-0 sm:border-r border-border pb-2 sm:pb-0 pr-0 sm:pr-6">
|
||||||
<div className="text-xs text-muted-foreground mb-2">System</div>
|
<div className="text-xs text-muted-foreground mb-2">
|
||||||
|
System
|
||||||
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
<span className="text-muted-foreground">OS</span>
|
<span className="text-muted-foreground">OS</span>
|
||||||
@@ -110,11 +119,15 @@ export default function NodesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
<span className="text-muted-foreground">Docker</span>
|
<span className="text-muted-foreground">Docker</span>
|
||||||
<span className="font-mono">{node.docker}</span>
|
<span className="font-mono">
|
||||||
|
{dockerNodes[i]?.version}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
<span className="text-muted-foreground">Containers</span>
|
<span className="text-muted-foreground">Containers</span>
|
||||||
<span className="font-mono">{node.containers}</span>
|
<span className="font-mono">
|
||||||
|
{dockerNodes[i]?.containers.length}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,35 +135,43 @@ export default function NodesPage() {
|
|||||||
{/* CPU */}
|
{/* CPU */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground mb-2">
|
<div className="text-xs text-muted-foreground mb-2">
|
||||||
CPU ({node.cpu.cores} cores)
|
CPU ({node.liveData.cpu.cores} cores)
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-value mb-2">{node.cpu.usage}%</div>
|
<div className="stat-value mb-2">
|
||||||
<Progress value={node.cpu.usage} className="w-full" />
|
{node.liveData.cpu.usage.toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={node.liveData.cpu.usage}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Memory */}
|
{/* Memory */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground mb-2">Memory</div>
|
<div className="text-xs text-muted-foreground mb-2">
|
||||||
<div className="stat-value mb-2">{node.memory.percentage}%</div>
|
Memory
|
||||||
<Progress value={node.memory.percentage} className="w-full" />
|
</div>
|
||||||
|
<div className="stat-value mb-2">
|
||||||
|
{node.liveData.memory.usage.toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={node.liveData.memory.usage}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
||||||
{node.memory.used} / {node.memory.total}
|
{formatBytes(node.liveData.memory.used)} /{" "}
|
||||||
|
{formatBytes(node.liveData.memory.total)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Disk */}
|
{/* Disk */}
|
||||||
<div>
|
<DiskUsage liveData={node.liveData} />
|
||||||
<div className="text-xs text-muted-foreground mb-2">Disk</div>
|
|
||||||
<div className="stat-value mb-2">{node.disk.percentage}%</div>
|
|
||||||
<Progress value={node.disk.percentage} className="w-full" />
|
|
||||||
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
|
||||||
{node.disk.used} / {node.disk.total}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground mb-2">Quick Actions</div>
|
<div className="text-xs text-muted-foreground mb-2">
|
||||||
|
Quick Actions
|
||||||
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Button size="sm" className="w-full justify-start">
|
<Button size="sm" className="w-full justify-start">
|
||||||
SSH Connect
|
SSH Connect
|
||||||
|
|||||||
+34
-42
@@ -16,6 +16,9 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { getLiveNodes } from "@/core/client/node";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Node, NodeLiveData } from "@/lib/types";
|
||||||
|
|
||||||
const recentDeployments = [
|
const recentDeployments = [
|
||||||
{
|
{
|
||||||
@@ -84,36 +87,21 @@ const applications = [
|
|||||||
memory: 48,
|
memory: 48,
|
||||||
status: "pending" as const,
|
status: "pending" as const,
|
||||||
},
|
},
|
||||||
];
|
].slice(-5);
|
||||||
|
|
||||||
const nodes = [
|
|
||||||
{
|
|
||||||
name: "node-01",
|
|
||||||
ip: "10.0.1.10",
|
|
||||||
cpu: 72,
|
|
||||||
memory: 68,
|
|
||||||
containers: 8,
|
|
||||||
status: "running" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node-02",
|
|
||||||
ip: "10.0.1.11",
|
|
||||||
cpu: 45,
|
|
||||||
memory: 52,
|
|
||||||
containers: 6,
|
|
||||||
status: "running" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node-03",
|
|
||||||
ip: "10.0.1.12",
|
|
||||||
cpu: 89,
|
|
||||||
memory: 85,
|
|
||||||
containers: 12,
|
|
||||||
status: "running" as const,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function OverviewPage() {
|
export default function OverviewPage() {
|
||||||
|
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchNodes = async () => {
|
||||||
|
const nodes = await getLiveNodes();
|
||||||
|
setNodes(nodes);
|
||||||
|
};
|
||||||
|
fetchNodes();
|
||||||
|
const interval = setInterval(fetchNodes, 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="Overview" subtitle="System status and metrics">
|
<DashboardLayout title="Overview" subtitle="System status and metrics">
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
@@ -239,14 +227,11 @@ export default function OverviewPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="overflow-x-auto">
|
<CardContent className="overflow-x-auto">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 min-w-75">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 min-w-75">
|
||||||
{nodes.map((node) => (
|
{nodes.map((node, i) => (
|
||||||
<div
|
<div key={i} className="border border-border p-4 bg-secondary/30">
|
||||||
key={node.name}
|
|
||||||
className="border border-border p-4 bg-secondary/30"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-3 gap-2">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-3 gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StatusIndicator status={node.status} />
|
<StatusIndicator status={"running"} />
|
||||||
<span className="font-mono text-sm">{node.name}</span>
|
<span className="font-mono text-sm">{node.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground font-mono">
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
@@ -256,20 +241,27 @@ export default function OverviewPage() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-muted-foreground">CPU</span>
|
<span className="text-muted-foreground">CPU</span>
|
||||||
<span className="font-mono">{node.cpu}%</span>
|
<span className="font-mono">
|
||||||
</div>
|
{node.liveData.cpu.usage.toFixed(0)}%
|
||||||
<Progress value={node.cpu} className="[&>div]:bg-chart-1" />
|
</span>
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="text-muted-foreground">Memory</span>
|
|
||||||
<span className="font-mono">{node.memory}%</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={node.memory}
|
value={node.liveData.cpu.usage}
|
||||||
|
className="[&>div]:bg-chart-1"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">Memory</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{node.liveData.memory.usage.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={node.liveData.memory.usage}
|
||||||
className="w-full [&>div]:bg-chart-3"
|
className="w-full [&>div]:bg-chart-3"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between text-xs pt-2 border-t border-border mt-2">
|
<div className="flex items-center justify-between text-xs pt-2 border-t border-border mt-2">
|
||||||
<span className="text-muted-foreground">Containers</span>
|
<span className="text-muted-foreground">Containers</span>
|
||||||
<span className="font-mono">{node.containers}</span>
|
<span className="font-mono">{6}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-13
@@ -1,17 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { getDocker } from "@/core/client/docker";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return "ok";
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
console.log(await getDocker("localhost", "oD7tMrnkxIvuFrR+b6QZh66E4PU="));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Get docker
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { AreaChart, Area, XAxis, CartesianGrid, YAxis } from "recharts";
|
||||||
|
import {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
type ChartConfig,
|
||||||
|
} from "@/components/ui/chart";
|
||||||
|
|
||||||
|
export default function GradientAreaChart({
|
||||||
|
title,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
max,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
data: any[];
|
||||||
|
config: ChartConfig;
|
||||||
|
max?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
<CardDescription>Last 12 hours</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer config={config} className="h-60 w-full">
|
||||||
|
<AreaChart data={data} margin={{ left: 12, right: 12 }}>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
/>
|
||||||
|
{max && (
|
||||||
|
<YAxis
|
||||||
|
domain={[0, max]}
|
||||||
|
width={16}
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||||
|
<defs>
|
||||||
|
{Object.entries(config).map(([key, config]) => (
|
||||||
|
<linearGradient
|
||||||
|
key={key}
|
||||||
|
id={`fill-${key}`}
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2="1"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="5%"
|
||||||
|
stopColor={config.color}
|
||||||
|
stopOpacity={0.7}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="95%"
|
||||||
|
stopColor={config.color}
|
||||||
|
stopOpacity={0.1}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
))}
|
||||||
|
</defs>
|
||||||
|
{Object.entries(config).map(([key, config]) => (
|
||||||
|
<Area
|
||||||
|
key={key}
|
||||||
|
type="natural"
|
||||||
|
dataKey={key}
|
||||||
|
stroke={config.color}
|
||||||
|
fill={`url(#fill-${key})`}
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stackId="a"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,19 +25,19 @@ export default function RadialChart({
|
|||||||
const chartData = [{ value, fill: "var(--color-chart-1)" }];
|
const chartData = [{ value, fill: "var(--color-chart-1)" }];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContainer config={chartConfig} className="size-30">
|
<ChartContainer config={chartConfig} className="w-full aspect-square">
|
||||||
<RadialBarChart
|
<RadialBarChart
|
||||||
data={chartData}
|
data={chartData}
|
||||||
startAngle={-50}
|
startAngle={230 - 360 * (value / 100)}
|
||||||
endAngle={(value / 100) * 230}
|
endAngle={230}
|
||||||
innerRadius="65%"
|
innerRadius={80}
|
||||||
outerRadius="100%"
|
outerRadius={110}
|
||||||
>
|
>
|
||||||
<PolarGrid
|
<PolarGrid
|
||||||
gridType="circle"
|
gridType="circle"
|
||||||
radialLines={false}
|
radialLines={false}
|
||||||
stroke="none"
|
stroke="none"
|
||||||
className=""
|
className="first:fill-muted last:fill-background"
|
||||||
polarRadius={[86, 74]}
|
polarRadius={[86, 74]}
|
||||||
/>
|
/>
|
||||||
<RadialBar dataKey="value" background cornerRadius={10} />
|
<RadialBar dataKey="value" background cornerRadius={10} />
|
||||||
@@ -57,7 +57,7 @@ export default function RadialChart({
|
|||||||
y={viewBox.cy}
|
y={viewBox.cy}
|
||||||
className="fill-foreground text-xl font-bold"
|
className="fill-foreground text-xl font-bold"
|
||||||
>
|
>
|
||||||
{chartData[0].value.toLocaleString()}
|
{chartData[0].value.toFixed(1)}%
|
||||||
</tspan>
|
</tspan>
|
||||||
<tspan
|
<tspan
|
||||||
x={viewBox.cx}
|
x={viewBox.cx}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, ReactNode } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import {
|
||||||
|
authNode,
|
||||||
|
getNodes,
|
||||||
|
setNodes as updateNodes,
|
||||||
|
} from "@/core/client/node";
|
||||||
|
|
||||||
|
export default function AddNode({ children }: { children: ReactNode }) {
|
||||||
|
const [step, setStep] = useState<1 | 2>(1);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [ip, setIp] = useState("");
|
||||||
|
const [key, setKey] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [auth, setAuth] = useState<{ node_id: string; os: string }>();
|
||||||
|
|
||||||
|
const authenticateNode = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setAuth(await authNode(ip.trim(), key.trim()));
|
||||||
|
setStep(2);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNode = async () => {
|
||||||
|
if (!auth) return;
|
||||||
|
const nodes = await getNodes();
|
||||||
|
|
||||||
|
await updateNodes([
|
||||||
|
...nodes,
|
||||||
|
{
|
||||||
|
node_id: auth.node_id,
|
||||||
|
key,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
ip,
|
||||||
|
os: auth.os,
|
||||||
|
labels: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setStep(1);
|
||||||
|
setAuth(undefined);
|
||||||
|
setIp("");
|
||||||
|
setKey("");
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="sm:max-w-106.25">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{step === 1 && "Add Node"}
|
||||||
|
{step === 2 && "Node Details"}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* STEP 1: IP & Node Key */}
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="ip">
|
||||||
|
IP address <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="ip"
|
||||||
|
value={ip}
|
||||||
|
onChange={(e) => setIp(e.target.value)}
|
||||||
|
placeholder="10.7.1.21"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="key">
|
||||||
|
Node Key <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="key"
|
||||||
|
type="password"
|
||||||
|
value={key}
|
||||||
|
onChange={(e) => setKey(e.target.value)}
|
||||||
|
placeholder="xxxxxxxxxxxxxxxx"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 3: Node Name & Description */}
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="name">
|
||||||
|
Node Name <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Captain's node"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="description">
|
||||||
|
Node Description{" "}
|
||||||
|
<span className="text-muted-foreground/60">(optional)</span>
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="A node for the Captain's main operations"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{step === 1 && (
|
||||||
|
<>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button onClick={authenticateNode} disabled={loading}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === 2 && (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => setStep(1)}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button onClick={addNode}>Add Node</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
import { Node } from "@/lib/types";
|
||||||
|
|
||||||
|
export async function getDocker(node: Node): Promise<{ version: string }> {
|
||||||
|
return await (
|
||||||
|
await axios.get(`http://${node.ip}:3000/api/get-docker`, {
|
||||||
|
headers: { Authorization: `ApiKey ${node.key}` },
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export async function getDocker(
|
|
||||||
node_ip: string,
|
|
||||||
key: string
|
|
||||||
): Promise<{ version: string }> {
|
|
||||||
return await (
|
|
||||||
await axios.get(`http://${node_ip}:3000/api/get-docker`, {
|
|
||||||
headers: { Authorization: `ApiKey ${key}` },
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { Node } from "@/lib/types";
|
||||||
|
import axios from "axios";
|
||||||
|
import { getNodes } from "./node";
|
||||||
|
|
||||||
|
export async function getMetrics(): Promise<
|
||||||
|
{
|
||||||
|
time: string;
|
||||||
|
cpu: number;
|
||||||
|
memory: number;
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
}[]
|
||||||
|
> {
|
||||||
|
const nodes = await getNodes();
|
||||||
|
const nodeMetrics = await Promise.all(nodes.map(getNodeMetrics));
|
||||||
|
|
||||||
|
if (nodeMetrics.length === 0) return [];
|
||||||
|
|
||||||
|
const numNodes = nodeMetrics.length;
|
||||||
|
const numPoints = nodeMetrics[0].length;
|
||||||
|
|
||||||
|
const averaged: (typeof nodeMetrics)[0] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < numPoints; i++) {
|
||||||
|
const sum = nodeMetrics.reduce(
|
||||||
|
(acc, metrics) => {
|
||||||
|
const m = metrics[i];
|
||||||
|
acc.cpu += m.cpu;
|
||||||
|
acc.memory += m.memory;
|
||||||
|
acc.in += m.in;
|
||||||
|
acc.out += m.out;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ cpu: 0, memory: 0, in: 0, out: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
averaged.push({
|
||||||
|
time: nodeMetrics[0][i].time,
|
||||||
|
cpu: sum.cpu / numNodes,
|
||||||
|
memory: sum.memory / numNodes,
|
||||||
|
in: sum.in / numNodes,
|
||||||
|
out: sum.out / numNodes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return averaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNodeMetrics(node: Node): Promise<
|
||||||
|
{
|
||||||
|
time: string;
|
||||||
|
cpu: number;
|
||||||
|
memory: number;
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
}[]
|
||||||
|
> {
|
||||||
|
const data = await (
|
||||||
|
await axios.get(`http://${node.ip}:3000/api/metrics`, {
|
||||||
|
headers: { Authorization: `ApiKey ${node.key}` },
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return data.metrics;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { read, write } from "@/app/api/file";
|
||||||
|
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export async function getNodes(): Promise<Node[]> {
|
||||||
|
return await read(() => [], "data", "nodes.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setNodes(nodes: Node[]) {
|
||||||
|
await write(nodes, "data", "nodes.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNode(node_id: string): Promise<Node | undefined> {
|
||||||
|
const nodes = await read<Node[]>(() => [], "data", "nodes.json");
|
||||||
|
return nodes.find((v) => v.node_id === node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLiveNode(node: Node): Promise<NodeLiveData & Node> {
|
||||||
|
const data = await (
|
||||||
|
await axios.get(`http://${node.ip}:3000/api/status`, {
|
||||||
|
headers: { Authorization: `ApiKey ${node.key}` },
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
return { ...node, liveData: data.liveData };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLiveNodes(): Promise<(NodeLiveData & Node)[]> {
|
||||||
|
const nodes = await read(() => [], "data", "nodes.json");
|
||||||
|
return await Promise.all(nodes.map(getLiveNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDockerNode(node: Node): Promise<Docker> {
|
||||||
|
const data = await (
|
||||||
|
await axios.get(`http://${node.ip}:3000/api/get-docker`, {
|
||||||
|
headers: { Authorization: `ApiKey ${node.key}` },
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
return { ...node, containers: data.containers, version: data.version };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authNode(ip: string, key: string) {
|
||||||
|
const data = await (
|
||||||
|
await axios.get(`http://${ip}:3000/api/auth`, {
|
||||||
|
headers: { Authorization: `ApiKey ${key}` },
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return { version: data.version, node_id: data.node_id, os: data.os };
|
||||||
|
}
|
||||||
@@ -6,6 +6,6 @@ const docker = new Docker();
|
|||||||
|
|
||||||
export async function getDocker() {
|
export async function getDocker() {
|
||||||
const version = await docker.version();
|
const version = await docker.version();
|
||||||
console.log("Docker version:", version);
|
const containers = await docker.listContainers();
|
||||||
return version;
|
return { version, containers };
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export function formatBytes(bytes: number, decimals = 2) {
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${
|
||||||
|
sizes[i]
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function average<T>(values: Array<T>, p: (v: T) => number) {
|
||||||
|
return values.length > 0
|
||||||
|
? values.reduce((sum, value) => sum + p(value), 0) / values.length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { ContainerInfo } from "dockerode";
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
node_id: string;
|
||||||
|
key: string;
|
||||||
|
ip: string;
|
||||||
|
os: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
labels: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeLiveData {
|
||||||
|
liveData: {
|
||||||
|
cpu: {
|
||||||
|
manufacturer: string;
|
||||||
|
brand: string;
|
||||||
|
cores: number;
|
||||||
|
usage: number;
|
||||||
|
};
|
||||||
|
memory: {
|
||||||
|
total: number;
|
||||||
|
used: number;
|
||||||
|
free: number;
|
||||||
|
usage: number;
|
||||||
|
};
|
||||||
|
disk: {
|
||||||
|
size: number;
|
||||||
|
available: number;
|
||||||
|
used: number;
|
||||||
|
};
|
||||||
|
disks: {
|
||||||
|
fs: string;
|
||||||
|
size: number;
|
||||||
|
used: number;
|
||||||
|
usage: number;
|
||||||
|
}[];
|
||||||
|
network: {
|
||||||
|
iface: string;
|
||||||
|
rx: number;
|
||||||
|
tx: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Docker {
|
||||||
|
version: string;
|
||||||
|
containers: ContainerInfo[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user