feat: add customizable widget sizes with chart visualizations
Add three-tier size system (small/medium/large) for server stats widgets. Integrate recharts library for visualizing trends in large widgets with line charts (CPU), area charts (Memory), and radial bar charts (Disk). Fix layout overflow issues with proper flexbox patterns.
This commit is contained in:
355
package-lock.json
generated
355
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
24
src/components/ui/chart.tsx
Normal file
24
src/components/ui/chart.tsx
Normal file
@@ -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