feat: enhance server stats widgets and fix TypeScript/ESLint errors #394
Generated
+354
-1
@@ -82,6 +82,7 @@
|
|||||||
"react-simple-keyboard": "^3.8.120",
|
"react-simple-keyboard": "^3.8.120",
|
||||||
"react-syntax-highlighter": "^15.6.6",
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
|
"recharts": "^3.2.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
@@ -3356,6 +3357,32 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^10.0.3",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@replit/codemirror-lang-nix": {
|
"node_modules/@replit/codemirror-lang-nix": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3656,6 +3683,69 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3876,6 +3966,12 @@
|
|||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5752,6 +5848,127 @@
|
|||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dargs": {
|
"node_modules/dargs": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5809,6 +6026,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/decode-named-character-reference": {
|
"node_modules/decode-named-character-reference": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -6579,6 +6802,16 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-toolkit": {
|
||||||
|
"version": "1.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz",
|
||||||
|
"integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"docs",
|
||||||
|
"benchmarks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/es6-error": {
|
"node_modules/es6-error": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -6854,7 +7087,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/expand-template": {
|
"node_modules/expand-template": {
|
||||||
@@ -8108,6 +8340,16 @@
|
|||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||||
|
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8185,6 +8427,15 @@
|
|||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
"version": "10.0.1",
|
"version": "10.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -11511,6 +11762,29 @@
|
|||||||
"react-dom": "^17.0.2 || ^18 || ^19"
|
"react-dom": "^17.0.2 || ^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -11674,6 +11948,48 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
|
"reselect": "5.1.1",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"victory-vendor": "^37.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/refractor": {
|
"node_modules/refractor": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -11863,6 +12179,12 @@
|
|||||||
"url": "https://github.com/sponsors/jet2jet"
|
"url": "https://github.com/sponsors/jet2jet"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resize-observer-polyfill": {
|
"node_modules/resize-observer-polyfill": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
@@ -13349,6 +13671,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/utf8-byte-length": {
|
"node_modules/utf8-byte-length": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -13389,6 +13720,28 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "37.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vimeo-video-element": {
|
"node_modules/vimeo-video-element": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -103,6 +103,7 @@
|
|||||||
"react-simple-keyboard": "^3.8.120",
|
"react-simple-keyboard": "^3.8.120",
|
||||||
"react-syntax-highlighter": "^15.6.6",
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
|
"recharts": "^3.2.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as RechartsPrimitive from "recharts";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// Chart Container
|
||||||
|
const ChartContainer = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ChartContainer.displayName = "ChartContainer";
|
||||||
|
|
||||||
|
export { ChartContainer, RechartsPrimitive };
|
||||||
@@ -7,9 +7,12 @@ export type WidgetType =
|
|||||||
// | 'processes' // 进程数
|
// | 'processes' // 进程数
|
||||||
// | 'uptime'; // 运行时间
|
// | 'uptime'; // 运行时间
|
||||||
|
|
||||||
|
export type WidgetSize = "small" | "medium" | "large";
|
||||||
|
|
||||||
export interface Widget {
|
export interface Widget {
|
||||||
id: string; // 唯一 ID:"cpu-1", "memory-2"
|
id: string; // 唯一 ID:"cpu-1", "memory-2"
|
||||||
type: WidgetType; // 卡片类型
|
type: WidgetType; // 卡片类型
|
||||||
|
size: WidgetSize; // 尺寸:small/medium/large
|
||||||
x: number; // 网格X坐标 (0-11)
|
x: number; // 网格X坐标 (0-11)
|
||||||
y: number; // 网格Y坐标
|
y: number; // 网格Y坐标
|
||||||
w: number; // 宽度(网格单位 1-12)
|
w: number; // 宽度(网格单位 1-12)
|
||||||
@@ -22,9 +25,9 @@ export interface StatsConfig {
|
|||||||
|
|
||||||
export const DEFAULT_STATS_CONFIG: StatsConfig = {
|
export const DEFAULT_STATS_CONFIG: StatsConfig = {
|
||||||
widgets: [
|
widgets: [
|
||||||
{ id: "cpu-1", type: "cpu", x: 0, y: 0, w: 4, h: 2 },
|
{ id: "cpu-1", type: "cpu", size: "medium", x: 0, y: 0, w: 4, h: 2 },
|
||||||
{ id: "memory-1", type: "memory", x: 4, y: 0, w: 4, h: 2 },
|
{ id: "memory-1", type: "memory", size: "medium", x: 4, y: 0, w: 4, h: 2 },
|
||||||
{ id: "disk-1", type: "disk", x: 8, y: 0, w: 4, h: 2 },
|
{ id: "disk-1", type: "disk", size: "medium", x: 8, y: 0, w: 4, h: 2 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
DiskWidget,
|
DiskWidget,
|
||||||
generateWidgetId,
|
generateWidgetId,
|
||||||
getWidgetConfig,
|
getWidgetConfig,
|
||||||
|
getWidgetSize,
|
||||||
} from "./widgets";
|
} from "./widgets";
|
||||||
import { AddWidgetDialog } from "./widgets/AddWidgetDialog";
|
import { AddWidgetDialog } from "./widgets/AddWidgetDialog";
|
||||||
import "react-grid-layout/css/styles.css";
|
import "react-grid-layout/css/styles.css";
|
||||||
@@ -54,6 +55,9 @@ export function Server({
|
|||||||
"offline",
|
"offline",
|
||||||
);
|
);
|
||||||
const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null);
|
const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null);
|
||||||
|
const [metricsHistory, setMetricsHistory] = React.useState<ServerMetrics[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
|
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
|
||||||
const [isLoadingMetrics, setIsLoadingMetrics] = React.useState(false);
|
const [isLoadingMetrics, setIsLoadingMetrics] = React.useState(false);
|
||||||
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
||||||
@@ -120,10 +124,36 @@ export function Server({
|
|||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddWidget = (widgetType: string) => {
|
const handleChangeWidgetSize = (
|
||||||
|
widgetId: string,
|
||||||
|
newSize: any,
|
||||||
|
e: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setWidgets((prev) =>
|
||||||
|
prev.map((widget) => {
|
||||||
|
if (widget.id === widgetId) {
|
||||||
|
const sizeConfig = getWidgetSize(widget.type, newSize);
|
||||||
|
return {
|
||||||
|
...widget,
|
||||||
|
size: newSize,
|
||||||
|
w: sizeConfig.w,
|
||||||
|
h: sizeConfig.h,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return widget;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddWidget = (widgetType: string, size: any) => {
|
||||||
const existingIds = widgets.map((w) => w.id);
|
const existingIds = widgets.map((w) => w.id);
|
||||||
const newId = generateWidgetId(widgetType as any, existingIds);
|
const newId = generateWidgetId(widgetType as any, existingIds);
|
||||||
const config = getWidgetConfig(widgetType as any);
|
const config = getWidgetConfig(widgetType as any);
|
||||||
|
const sizeConfig = getWidgetSize(widgetType as any, size);
|
||||||
|
|
||||||
// Find the next available position
|
// Find the next available position
|
||||||
const maxY = widgets.reduce((max, w) => Math.max(max, w.y + w.h), 0);
|
const maxY = widgets.reduce((max, w) => Math.max(max, w.y + w.h), 0);
|
||||||
@@ -131,10 +161,11 @@ export function Server({
|
|||||||
const newWidget: Widget = {
|
const newWidget: Widget = {
|
||||||
id: newId,
|
id: newId,
|
||||||
type: widgetType as any,
|
type: widgetType as any,
|
||||||
|
size: size,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: maxY,
|
y: maxY,
|
||||||
w: config.defaultSize.w,
|
w: sizeConfig.w,
|
||||||
h: config.defaultSize.h,
|
h: sizeConfig.h,
|
||||||
};
|
};
|
||||||
|
|
||||||
setWidgets((prev) => [...prev, newWidget]);
|
setWidgets((prev) => [...prev, newWidget]);
|
||||||
@@ -173,9 +204,12 @@ export function Server({
|
|||||||
return (
|
return (
|
||||||
<CpuWidget
|
<CpuWidget
|
||||||
metrics={metrics}
|
metrics={metrics}
|
||||||
|
metricsHistory={metricsHistory}
|
||||||
isEditMode={isEditMode}
|
isEditMode={isEditMode}
|
||||||
widgetId={widget.id}
|
widgetId={widget.id}
|
||||||
|
widgetSize={widget.size}
|
||||||
onDelete={handleDeleteWidget}
|
onDelete={handleDeleteWidget}
|
||||||
|
onChangeSize={handleChangeWidgetSize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -183,9 +217,12 @@ export function Server({
|
|||||||
return (
|
return (
|
||||||
<MemoryWidget
|
<MemoryWidget
|
||||||
metrics={metrics}
|
metrics={metrics}
|
||||||
|
metricsHistory={metricsHistory}
|
||||||
isEditMode={isEditMode}
|
isEditMode={isEditMode}
|
||||||
widgetId={widget.id}
|
widgetId={widget.id}
|
||||||
|
widgetSize={widget.size}
|
||||||
onDelete={handleDeleteWidget}
|
onDelete={handleDeleteWidget}
|
||||||
|
onChangeSize={handleChangeWidgetSize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -193,9 +230,12 @@ export function Server({
|
|||||||
return (
|
return (
|
||||||
<DiskWidget
|
<DiskWidget
|
||||||
metrics={metrics}
|
metrics={metrics}
|
||||||
|
metricsHistory={metricsHistory}
|
||||||
isEditMode={isEditMode}
|
isEditMode={isEditMode}
|
||||||
widgetId={widget.id}
|
widgetId={widget.id}
|
||||||
|
widgetSize={widget.size}
|
||||||
onDelete={handleDeleteWidget}
|
onDelete={handleDeleteWidget}
|
||||||
|
onChangeSize={handleChangeWidgetSize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -275,6 +315,11 @@ export function Server({
|
|||||||
const data = await getServerMetricsById(currentHostConfig.id);
|
const data = await getServerMetricsById(currentHostConfig.id);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setMetrics(data);
|
setMetrics(data);
|
||||||
|
setMetricsHistory((prev) => {
|
||||||
|
const newHistory = [...prev, data];
|
||||||
|
// Keep last 20 data points for chart
|
||||||
|
return newHistory.slice(-20);
|
||||||
|
});
|
||||||
setShowStatsUI(true);
|
setShowStatsUI(true);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { getAvailableWidgets, type WidgetRegistryItem } from "./registry";
|
import { getAvailableWidgets, type WidgetRegistryItem } from "./registry";
|
||||||
import { Plus, X } from "lucide-react";
|
import { Plus, X } from "lucide-react";
|
||||||
|
import type { WidgetSize } from "@/types/stats-widgets";
|
||||||
|
|
||||||
interface AddWidgetDialogProps {
|
interface AddWidgetDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onAddWidget: (widgetType: string) => void;
|
onAddWidget: (widgetType: string, size: WidgetSize) => void;
|
||||||
existingWidgetTypes: string[];
|
existingWidgetTypes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +17,13 @@ export function AddWidgetDialog({
|
|||||||
existingWidgetTypes,
|
existingWidgetTypes,
|
||||||
}: AddWidgetDialogProps) {
|
}: AddWidgetDialogProps) {
|
||||||
const availableWidgets = getAvailableWidgets();
|
const availableWidgets = getAvailableWidgets();
|
||||||
|
const [selectedSize, setSelectedSize] = React.useState<WidgetSize>("medium");
|
||||||
|
|
||||||
|
const sizeLabels: Record<WidgetSize, string> = {
|
||||||
|
small: "Small",
|
||||||
|
medium: "Medium",
|
||||||
|
large: "Large",
|
||||||
|
};
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
@@ -36,8 +44,26 @@ export function AddWidgetDialog({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 text-sm mb-4">
|
<p className="text-gray-400 text-sm mb-4">
|
||||||
Choose a widget to add to your dashboard
|
Choose a widget and size to add to your dashboard
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Size selector */}
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
{(["small", "medium", "large"] as WidgetSize[]).map((size) => (
|
||||||
|
<button
|
||||||
|
key={size}
|
||||||
|
onClick={() => setSelectedSize(size)}
|
||||||
|
className={`flex-1 py-2 px-4 rounded-lg border transition-all ${
|
||||||
|
selectedSize === size
|
||||||
|
? "bg-blue-500 border-blue-500 text-white"
|
||||||
|
: "bg-dark-bg-darker border-dark-border text-gray-400 hover:border-blue-500/50 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{sizeLabels[size]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 max-h-[400px] overflow-y-auto">
|
<div className="grid gap-3 max-h-[400px] overflow-y-auto">
|
||||||
{availableWidgets.map((widget: WidgetRegistryItem) => {
|
{availableWidgets.map((widget: WidgetRegistryItem) => {
|
||||||
const Icon = widget.icon;
|
const Icon = widget.icon;
|
||||||
@@ -45,7 +71,7 @@ export function AddWidgetDialog({
|
|||||||
<button
|
<button
|
||||||
key={widget.type}
|
key={widget.type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onAddWidget(widget.type);
|
onAddWidget(widget.type, selectedSize);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}}
|
}}
|
||||||
className="flex items-start gap-4 p-4 rounded-lg border border-dark-border bg-dark-bg/50 hover:bg-dark-bg hover:border-blue-500/50 transition-all duration-200 text-left group"
|
className="flex items-start gap-4 p-4 rounded-lg border border-dark-border bg-dark-bg/50 hover:bg-dark-bg hover:border-blue-500/50 transition-all duration-200 text-left group"
|
||||||
|
|||||||
@@ -1,27 +1,72 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cpu, X } from "lucide-react";
|
import { Cpu, X, Maximize2 } from "lucide-react";
|
||||||
import { Progress } from "@/components/ui/progress.tsx";
|
import { Progress } from "@/components/ui/progress.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { ServerMetrics } from "@/ui/main-axios.ts";
|
import type { ServerMetrics } from "@/ui/main-axios.ts";
|
||||||
|
import type { WidgetSize } from "@/types/stats-widgets";
|
||||||
|
import { ChartContainer, RechartsPrimitive } from "@/components/ui/chart.tsx";
|
||||||
|
|
||||||
|
const {
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} = RechartsPrimitive;
|
||||||
|
|
||||||
interface CpuWidgetProps {
|
interface CpuWidgetProps {
|
||||||
metrics: ServerMetrics | null;
|
metrics: ServerMetrics | null;
|
||||||
|
metricsHistory: ServerMetrics[];
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
widgetId: string;
|
widgetId: string;
|
||||||
|
widgetSize: WidgetSize;
|
||||||
onDelete: (widgetId: string, e: React.MouseEvent<HTMLButtonElement>) => void;
|
onDelete: (widgetId: string, e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onChangeSize: (
|
||||||
|
widgetId: string,
|
||||||
|
newSize: WidgetSize,
|
||||||
|
e: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CpuWidget({
|
export function CpuWidget({
|
||||||
metrics,
|
metrics,
|
||||||
|
metricsHistory,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
widgetId,
|
widgetId,
|
||||||
|
widgetSize,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onChangeSize,
|
||||||
}: CpuWidgetProps) {
|
}: CpuWidgetProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const sizeOrder: WidgetSize[] = ["small", "medium", "large"];
|
||||||
|
const nextSize =
|
||||||
|
sizeOrder[(sizeOrder.indexOf(widgetSize) + 1) % sizeOrder.length];
|
||||||
|
|
||||||
|
// Prepare chart data
|
||||||
|
const chartData = React.useMemo(() => {
|
||||||
|
return metricsHistory.map((m, index) => ({
|
||||||
|
index,
|
||||||
|
cpu: m.cpu?.percent || 0,
|
||||||
|
}));
|
||||||
|
}, [metricsHistory]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={(e) => onChangeSize(widgetId, nextSize, e)}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="absolute top-2 right-11 z-[9999] w-7 h-7 bg-blue-500/90 hover:bg-blue-600 text-white rounded-full flex items-center justify-center cursor-pointer shadow-lg"
|
||||||
|
type="button"
|
||||||
|
title={`Change to ${nextSize}`}
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => onDelete(widgetId, e)}
|
onClick={(e) => onDelete(widgetId, e)}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
@@ -31,13 +76,31 @@ export function CpuWidget({
|
|||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 mb-3 ${isEditMode ? "drag-handle cursor-move" : ""}`}
|
className={`flex items-center gap-2 flex-shrink-0 mb-3 ${isEditMode ? "drag-handle cursor-move" : ""}`}
|
||||||
>
|
>
|
||||||
<Cpu className="h-5 w-5 text-blue-400" />
|
<Cpu className="h-5 w-5 text-blue-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">CPU Usage</h3>
|
<h3 className="font-semibold text-lg text-white">CPU Usage</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{widgetSize === "small" && (
|
||||||
|
<div className="flex flex-col items-center justify-center flex-1">
|
||||||
|
<div className="text-4xl font-bold text-blue-400">
|
||||||
|
{typeof metrics?.cpu?.percent === "number"
|
||||||
|
? `${metrics.cpu.percent}%`
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400 mt-2">
|
||||||
|
{typeof metrics?.cpu?.cores === "number"
|
||||||
|
? t("serverStats.cpuCores", { count: metrics.cpu.cores })
|
||||||
|
: t("serverStats.naCpus")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{widgetSize === "medium" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-300">
|
<span className="text-sm text-gray-300">
|
||||||
@@ -69,6 +132,64 @@ export function CpuWidget({
|
|||||||
: "Load: N/A"}
|
: "Load: N/A"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{widgetSize === "large" && (
|
||||||
|
<div className="flex flex-col flex-1 min-h-0 gap-2">
|
||||||
|
<div className="flex items-baseline gap-3 flex-shrink-0">
|
||||||
|
<div className="text-2xl font-bold text-blue-400">
|
||||||
|
{typeof metrics?.cpu?.percent === "number"
|
||||||
|
? `${metrics.cpu.percent}%`
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{typeof metrics?.cpu?.cores === "number"
|
||||||
|
? t("serverStats.cpuCores", { count: metrics.cpu.cores })
|
||||||
|
: t("serverStats.naCpus")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 flex-shrink-0">
|
||||||
|
{metrics?.cpu?.load
|
||||||
|
? `Load: ${metrics.cpu.load[0].toFixed(2)} / ${metrics.cpu.load[1].toFixed(2)} / ${metrics.cpu.load[2].toFixed(2)}`
|
||||||
|
: "Load: N/A"}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<LineChart data={chartData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="index"
|
||||||
|
stroke="#9ca3af"
|
||||||
|
tick={{ fill: "#9ca3af" }}
|
||||||
|
hide
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
domain={[0, 100]}
|
||||||
|
stroke="#9ca3af"
|
||||||
|
tick={{ fill: "#9ca3af" }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: "#1f2937",
|
||||||
|
border: "1px solid #374151",
|
||||||
|
borderRadius: "6px",
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [`${value.toFixed(1)}%`, "CPU"]}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="cpu"
|
||||||
|
stroke="#60a5fa"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={false}
|
||||||
|
animationDuration={300}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,69 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { HardDrive, X } from "lucide-react";
|
import { HardDrive, X, Maximize2 } from "lucide-react";
|
||||||
import { Progress } from "@/components/ui/progress.tsx";
|
import { Progress } from "@/components/ui/progress.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { ServerMetrics } from "@/ui/main-axios.ts";
|
import type { ServerMetrics } from "@/ui/main-axios.ts";
|
||||||
|
import type { WidgetSize } from "@/types/stats-widgets";
|
||||||
|
import { ChartContainer, RechartsPrimitive } from "@/components/ui/chart.tsx";
|
||||||
|
|
||||||
|
const { RadialBarChart, RadialBar, PolarAngleAxis, ResponsiveContainer } =
|
||||||
|
RechartsPrimitive;
|
||||||
|
|
||||||
interface DiskWidgetProps {
|
interface DiskWidgetProps {
|
||||||
metrics: ServerMetrics | null;
|
metrics: ServerMetrics | null;
|
||||||
|
metricsHistory: ServerMetrics[];
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
widgetId: string;
|
widgetId: string;
|
||||||
onDelete: (widgetId: string, e: React.MouseEvent<HTMLButtonElement>) => void;
|
widgetSize: WidgetSize;
|
||||||
|
onDelete: (widgetId: string, e: React.MouseEvent<HTMLButtonButton>) => void;
|
||||||
|
onChangeSize: (
|
||||||
|
widgetId: string,
|
||||||
|
newSize: WidgetSize,
|
||||||
|
e: React.MouseEvent<HTMLButtonButton>,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DiskWidget({
|
export function DiskWidget({
|
||||||
metrics,
|
metrics,
|
||||||
|
metricsHistory,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
widgetId,
|
widgetId,
|
||||||
|
widgetSize,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onChangeSize,
|
||||||
}: DiskWidgetProps) {
|
}: DiskWidgetProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const sizeOrder: WidgetSize[] = ["small", "medium", "large"];
|
||||||
|
const nextSize =
|
||||||
|
sizeOrder[(sizeOrder.indexOf(widgetSize) + 1) % sizeOrder.length];
|
||||||
|
|
||||||
|
// Prepare radial chart data
|
||||||
|
const radialData = React.useMemo(() => {
|
||||||
|
const percent = metrics?.disk?.percent || 0;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Disk",
|
||||||
|
value: percent,
|
||||||
|
fill: "#fb923c",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [metrics]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={(e) => onChangeSize(widgetId, nextSize, e)}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="absolute top-2 right-11 z-[9999] w-7 h-7 bg-blue-500/90 hover:bg-blue-600 text-white rounded-full flex items-center justify-center cursor-pointer shadow-lg"
|
||||||
|
type="button"
|
||||||
|
title={`Change to ${nextSize}`}
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => onDelete(widgetId, e)}
|
onClick={(e) => onDelete(widgetId, e)}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
@@ -31,13 +73,36 @@ export function DiskWidget({
|
|||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 mb-3 ${isEditMode ? "drag-handle cursor-move" : ""}`}
|
className={`flex items-center gap-2 flex-shrink-0 mb-3 ${isEditMode ? "drag-handle cursor-move" : ""}`}
|
||||||
>
|
>
|
||||||
<HardDrive className="h-5 w-5 text-orange-400" />
|
<HardDrive className="h-5 w-5 text-orange-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">Disk Usage</h3>
|
<h3 className="font-semibold text-lg text-white">Disk Usage</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{widgetSize === "small" && (
|
||||||
|
<div className="flex flex-col items-center justify-center flex-1">
|
||||||
|
<div className="text-4xl font-bold text-orange-400">
|
||||||
|
{typeof metrics?.disk?.percent === "number"
|
||||||
|
? `${metrics.disk.percent}%`
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400 mt-2">
|
||||||
|
{(() => {
|
||||||
|
const used = metrics?.disk?.usedHuman;
|
||||||
|
const total = metrics?.disk?.totalHuman;
|
||||||
|
if (used && total) {
|
||||||
|
return `${used} / ${total}`;
|
||||||
|
}
|
||||||
|
return "N/A";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{widgetSize === "medium" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-300">
|
<span className="text-sm text-gray-300">
|
||||||
@@ -69,6 +134,67 @@ export function DiskWidget({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{widgetSize === "large" && (
|
||||||
|
<div className="flex flex-col flex-1 min-h-0">
|
||||||
|
<div className="flex-1 min-h-0 flex items-center justify-center">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<RadialBarChart
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius="60%"
|
||||||
|
outerRadius="90%"
|
||||||
|
data={radialData}
|
||||||
|
startAngle={90}
|
||||||
|
endAngle={-270}
|
||||||
|
>
|
||||||
|
<PolarAngleAxis
|
||||||
|
type="number"
|
||||||
|
domain={[0, 100]}
|
||||||
|
angleAxisId={0}
|
||||||
|
tick={false}
|
||||||
|
/>
|
||||||
|
<RadialBar
|
||||||
|
background
|
||||||
|
dataKey="value"
|
||||||
|
cornerRadius={10}
|
||||||
|
fill="#fb923c"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="50%"
|
||||||
|
y="50%"
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
className="text-2xl font-bold fill-orange-400"
|
||||||
|
>
|
||||||
|
{typeof metrics?.disk?.percent === "number"
|
||||||
|
? `${metrics.disk.percent}%`
|
||||||
|
: "N/A"}
|
||||||
|
</text>
|
||||||
|
</RadialBarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 space-y-1 text-center pb-2">
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{(() => {
|
||||||
|
const used = metrics?.disk?.usedHuman;
|
||||||
|
const total = metrics?.disk?.totalHuman;
|
||||||
|
if (used && total) {
|
||||||
|
return `${used} / ${total}`;
|
||||||
|
}
|
||||||
|
return "N/A";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{(() => {
|
||||||
|
const available = metrics?.disk?.availableHuman;
|
||||||
|
return available ? `Available: ${available}` : "Available: N/A";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,72 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { MemoryStick, X } from "lucide-react";
|
import { MemoryStick, X, Maximize2 } from "lucide-react";
|
||||||
import { Progress } from "@/components/ui/progress.tsx";
|
import { Progress } from "@/components/ui/progress.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { ServerMetrics } from "@/ui/main-axios.ts";
|
import type { ServerMetrics } from "@/ui/main-axios.ts";
|
||||||
|
import type { WidgetSize } from "@/types/stats-widgets";
|
||||||
|
import { ChartContainer, RechartsPrimitive } from "@/components/ui/chart.tsx";
|
||||||
|
|
||||||
|
const {
|
||||||
|
AreaChart,
|
||||||
|
Area,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} = RechartsPrimitive;
|
||||||
|
|
||||||
interface MemoryWidgetProps {
|
interface MemoryWidgetProps {
|
||||||
metrics: ServerMetrics | null;
|
metrics: ServerMetrics | null;
|
||||||
|
metricsHistory: ServerMetrics[];
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
widgetId: string;
|
widgetId: string;
|
||||||
|
widgetSize: WidgetSize;
|
||||||
onDelete: (widgetId: string, e: React.MouseEvent<HTMLButtonElement>) => void;
|
onDelete: (widgetId: string, e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onChangeSize: (
|
||||||
|
widgetId: string,
|
||||||
|
newSize: WidgetSize,
|
||||||
|
e: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MemoryWidget({
|
export function MemoryWidget({
|
||||||
metrics,
|
metrics,
|
||||||
|
metricsHistory,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
widgetId,
|
widgetId,
|
||||||
|
widgetSize,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onChangeSize,
|
||||||
}: MemoryWidgetProps) {
|
}: MemoryWidgetProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const sizeOrder: WidgetSize[] = ["small", "medium", "large"];
|
||||||
|
const nextSize =
|
||||||
|
sizeOrder[(sizeOrder.indexOf(widgetSize) + 1) % sizeOrder.length];
|
||||||
|
|
||||||
|
// Prepare chart data
|
||||||
|
const chartData = React.useMemo(() => {
|
||||||
|
return metricsHistory.map((m, index) => ({
|
||||||
|
index,
|
||||||
|
memory: m.memory?.percent || 0,
|
||||||
|
}));
|
||||||
|
}, [metricsHistory]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={(e) => onChangeSize(widgetId, nextSize, e)}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="absolute top-2 right-11 z-[9999] w-7 h-7 bg-blue-500/90 hover:bg-blue-600 text-white rounded-full flex items-center justify-center cursor-pointer shadow-lg"
|
||||||
|
type="button"
|
||||||
|
title={`Change to ${nextSize}`}
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => onDelete(widgetId, e)}
|
onClick={(e) => onDelete(widgetId, e)}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
@@ -31,13 +76,36 @@ export function MemoryWidget({
|
|||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 mb-3 ${isEditMode ? "drag-handle cursor-move" : ""}`}
|
className={`flex items-center gap-2 flex-shrink-0 mb-3 ${isEditMode ? "drag-handle cursor-move" : ""}`}
|
||||||
>
|
>
|
||||||
<MemoryStick className="h-5 w-5 text-green-400" />
|
<MemoryStick className="h-5 w-5 text-green-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">Memory Usage</h3>
|
<h3 className="font-semibold text-lg text-white">Memory Usage</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{widgetSize === "small" && (
|
||||||
|
<div className="flex flex-col items-center justify-center flex-1">
|
||||||
|
<div className="text-4xl font-bold text-green-400">
|
||||||
|
{typeof metrics?.memory?.percent === "number"
|
||||||
|
? `${metrics.memory.percent}%`
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400 mt-2">
|
||||||
|
{(() => {
|
||||||
|
const used = metrics?.memory?.usedGiB;
|
||||||
|
const total = metrics?.memory?.totalGiB;
|
||||||
|
if (typeof used === "number" && typeof total === "number") {
|
||||||
|
return `${used.toFixed(1)} / ${total.toFixed(1)} GiB`;
|
||||||
|
}
|
||||||
|
return "N/A";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{widgetSize === "medium" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-300">
|
<span className="text-sm text-gray-300">
|
||||||
@@ -76,6 +144,90 @@ export function MemoryWidget({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{widgetSize === "large" && (
|
||||||
|
<div className="flex flex-col flex-1 min-h-0 gap-2">
|
||||||
|
<div className="flex items-baseline gap-3 flex-shrink-0">
|
||||||
|
<div className="text-2xl font-bold text-green-400">
|
||||||
|
{typeof metrics?.memory?.percent === "number"
|
||||||
|
? `${metrics.memory.percent}%`
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{(() => {
|
||||||
|
const used = metrics?.memory?.usedGiB;
|
||||||
|
const total = metrics?.memory?.totalGiB;
|
||||||
|
if (typeof used === "number" && typeof total === "number") {
|
||||||
|
return `${used.toFixed(1)} / ${total.toFixed(1)} GiB`;
|
||||||
|
}
|
||||||
|
return "N/A";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 flex-shrink-0">
|
||||||
|
{(() => {
|
||||||
|
const used = metrics?.memory?.usedGiB;
|
||||||
|
const total = metrics?.memory?.totalGiB;
|
||||||
|
const free =
|
||||||
|
typeof used === "number" && typeof total === "number"
|
||||||
|
? (total - used).toFixed(1)
|
||||||
|
: "N/A";
|
||||||
|
return `Free: ${free} GiB`;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<AreaChart data={chartData}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="memoryGradient"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2="1"
|
||||||
|
>
|
||||||
|
<stop offset="5%" stopColor="#34d399" stopOpacity={0.8} />
|
||||||
|
<stop offset="95%" stopColor="#34d399" stopOpacity={0.1} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="index"
|
||||||
|
stroke="#9ca3af"
|
||||||
|
tick={{ fill: "#9ca3af" }}
|
||||||
|
hide
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
domain={[0, 100]}
|
||||||
|
stroke="#9ca3af"
|
||||||
|
tick={{ fill: "#9ca3af" }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: "#1f2937",
|
||||||
|
border: "1px solid #374151",
|
||||||
|
borderRadius: "6px",
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [
|
||||||
|
`${value.toFixed(1)}%`,
|
||||||
|
"Memory",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="memory"
|
||||||
|
stroke="#34d399"
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="url(#memoryGradient)"
|
||||||
|
animationDuration={300}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export {
|
|||||||
WIDGET_REGISTRY,
|
WIDGET_REGISTRY,
|
||||||
getAvailableWidgets,
|
getAvailableWidgets,
|
||||||
getWidgetConfig,
|
getWidgetConfig,
|
||||||
|
getWidgetSize,
|
||||||
generateWidgetId,
|
generateWidgetId,
|
||||||
type WidgetRegistryItem,
|
type WidgetRegistryItem,
|
||||||
} from "./registry";
|
} from "./registry";
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { Cpu, MemoryStick, HardDrive, type LucideIcon } from "lucide-react";
|
import { Cpu, MemoryStick, HardDrive, type LucideIcon } from "lucide-react";
|
||||||
import type { WidgetType } from "@/types/stats-widgets";
|
import type { WidgetType, WidgetSize } from "@/types/stats-widgets";
|
||||||
|
|
||||||
|
export interface WidgetSizeConfig {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WidgetRegistryItem {
|
export interface WidgetRegistryItem {
|
||||||
type: WidgetType;
|
type: WidgetType;
|
||||||
@@ -7,7 +12,7 @@ export interface WidgetRegistryItem {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
iconColor: string;
|
iconColor: string;
|
||||||
defaultSize: { w: number; h: number };
|
sizes: Record<WidgetSize, WidgetSizeConfig>;
|
||||||
minSize: { w: number; h: number };
|
minSize: { w: number; h: number };
|
||||||
maxSize: { w: number; h: number };
|
maxSize: { w: number; h: number };
|
||||||
}
|
}
|
||||||
@@ -19,7 +24,11 @@ export const WIDGET_REGISTRY: Record<WidgetType, WidgetRegistryItem> = {
|
|||||||
description: "Monitor CPU utilization and load average",
|
description: "Monitor CPU utilization and load average",
|
||||||
icon: Cpu,
|
icon: Cpu,
|
||||||
iconColor: "text-blue-400",
|
iconColor: "text-blue-400",
|
||||||
defaultSize: { w: 4, h: 2 },
|
sizes: {
|
||||||
|
small: { w: 3, h: 2 }, // 紧凑:大号百分比+核心数
|
||||||
|
medium: { w: 4, h: 2 }, // 标准:进度条+load average
|
||||||
|
large: { w: 7, h: 3 }, // 图表:折线图需要宽度展示趋势
|
||||||
|
},
|
||||||
minSize: { w: 3, h: 2 },
|
minSize: { w: 3, h: 2 },
|
||||||
maxSize: { w: 12, h: 4 },
|
maxSize: { w: 12, h: 4 },
|
||||||
},
|
},
|
||||||
@@ -29,7 +38,11 @@ export const WIDGET_REGISTRY: Record<WidgetType, WidgetRegistryItem> = {
|
|||||||
description: "Track RAM usage and availability",
|
description: "Track RAM usage and availability",
|
||||||
icon: MemoryStick,
|
icon: MemoryStick,
|
||||||
iconColor: "text-green-400",
|
iconColor: "text-green-400",
|
||||||
defaultSize: { w: 4, h: 2 },
|
sizes: {
|
||||||
|
small: { w: 3, h: 2 }, // 紧凑:百分比+用量
|
||||||
|
medium: { w: 4, h: 2 }, // 标准:进度条+详细信息
|
||||||
|
large: { w: 6, h: 3 }, // 图表:面积图展示
|
||||||
|
},
|
||||||
minSize: { w: 3, h: 2 },
|
minSize: { w: 3, h: 2 },
|
||||||
maxSize: { w: 12, h: 4 },
|
maxSize: { w: 12, h: 4 },
|
||||||
},
|
},
|
||||||
@@ -39,7 +52,11 @@ export const WIDGET_REGISTRY: Record<WidgetType, WidgetRegistryItem> = {
|
|||||||
description: "View disk space consumption",
|
description: "View disk space consumption",
|
||||||
icon: HardDrive,
|
icon: HardDrive,
|
||||||
iconColor: "text-orange-400",
|
iconColor: "text-orange-400",
|
||||||
defaultSize: { w: 4, h: 2 },
|
sizes: {
|
||||||
|
small: { w: 3, h: 2 }, // 紧凑:百分比+用量
|
||||||
|
medium: { w: 4, h: 2 }, // 标准:进度条+可用空间
|
||||||
|
large: { w: 4, h: 4 }, // 图表:径向图(方形,不需要太宽)
|
||||||
|
},
|
||||||
minSize: { w: 3, h: 2 },
|
minSize: { w: 3, h: 2 },
|
||||||
maxSize: { w: 12, h: 4 },
|
maxSize: { w: 12, h: 4 },
|
||||||
},
|
},
|
||||||
@@ -59,6 +76,16 @@ export function getWidgetConfig(type: WidgetType): WidgetRegistryItem {
|
|||||||
return WIDGET_REGISTRY[type];
|
return WIDGET_REGISTRY[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get widget size configuration
|
||||||
|
*/
|
||||||
|
export function getWidgetSize(
|
||||||
|
type: WidgetType,
|
||||||
|
size: WidgetSize,
|
||||||
|
): WidgetSizeConfig {
|
||||||
|
return WIDGET_REGISTRY[type].sizes[size];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate unique widget ID
|
* Generate unique widget ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user