diff --git a/package-lock.json b/package-lock.json index dd61081b..d618e92e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "react-simple-keyboard": "^3.8.120", "react-syntax-highlighter": "^15.6.6", "react-xtermjs": "^1.0.10", + "recharts": "^3.2.1", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "speakeasy": "^2.0.0", @@ -3356,6 +3357,32 @@ "version": "1.1.1", "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": { "version": "6.0.1", "license": "MIT", @@ -3656,6 +3683,69 @@ "@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": { "version": "4.1.12", "license": "MIT", @@ -3876,6 +3966,12 @@ "version": "3.0.3", "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": { "version": "8.18.1", "dev": true, @@ -5752,6 +5848,127 @@ "version": "1.4.5", "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": { "version": "8.1.0", "dev": true, @@ -5809,6 +6026,12 @@ "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": { "version": "1.2.0", "license": "MIT", @@ -6579,6 +6802,16 @@ "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": { "version": "4.1.1", "dev": true, @@ -6854,7 +7087,6 @@ }, "node_modules/eventemitter3": { "version": "5.0.1", - "dev": true, "license": "MIT" }, "node_modules/expand-template": { @@ -8108,6 +8340,16 @@ "version": "3.0.6", "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": { "version": "3.3.1", "dev": true, @@ -8185,6 +8427,15 @@ "version": "0.2.4", "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": { "version": "10.0.1", "dev": true, @@ -11511,6 +11762,29 @@ "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": { "version": "2.7.1", "license": "MIT", @@ -11674,6 +11948,48 @@ "version": "5.1.2", "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": { "version": "3.6.0", "license": "MIT", @@ -11863,6 +12179,12 @@ "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": { "version": "1.5.1", "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": { "version": "1.0.5", "dev": true, @@ -13389,6 +13720,28 @@ "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": { "version": "1.6.0", "license": "MIT", diff --git a/package.json b/package.json index 9175d02c..bdc1ce32 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "react-simple-keyboard": "^3.8.120", "react-syntax-highlighter": "^15.6.6", "react-xtermjs": "^1.0.10", + "recharts": "^3.2.1", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "speakeasy": "^2.0.0", diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx new file mode 100644 index 00000000..c0479b5a --- /dev/null +++ b/src/components/ui/chart.tsx @@ -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 +>(({ className, ...props }, ref) => { + return ( +
+ ); +}); +ChartContainer.displayName = "ChartContainer"; + +export { ChartContainer, RechartsPrimitive }; diff --git a/src/types/stats-widgets.ts b/src/types/stats-widgets.ts index 632dfd98..818c01d5 100644 --- a/src/types/stats-widgets.ts +++ b/src/types/stats-widgets.ts @@ -7,9 +7,12 @@ export type WidgetType = // | 'processes' // 进程数 // | 'uptime'; // 运行时间 +export type WidgetSize = "small" | "medium" | "large"; + export interface Widget { id: string; // 唯一 ID:"cpu-1", "memory-2" type: WidgetType; // 卡片类型 + size: WidgetSize; // 尺寸:small/medium/large x: number; // 网格X坐标 (0-11) y: number; // 网格Y坐标 w: number; // 宽度(网格单位 1-12) @@ -22,9 +25,9 @@ export interface StatsConfig { export const DEFAULT_STATS_CONFIG: StatsConfig = { widgets: [ - { id: "cpu-1", type: "cpu", x: 0, y: 0, w: 4, h: 2 }, - { id: "memory-1", type: "memory", x: 4, y: 0, w: 4, h: 2 }, - { id: "disk-1", type: "disk", x: 8, 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", size: "medium", x: 4, y: 0, w: 4, h: 2 }, + { id: "disk-1", type: "disk", size: "medium", x: 8, y: 0, w: 4, h: 2 }, ], }; diff --git a/src/ui/Desktop/Apps/Server/Server.tsx b/src/ui/Desktop/Apps/Server/Server.tsx index 36062298..60207a51 100644 --- a/src/ui/Desktop/Apps/Server/Server.tsx +++ b/src/ui/Desktop/Apps/Server/Server.tsx @@ -25,6 +25,7 @@ import { DiskWidget, generateWidgetId, getWidgetConfig, + getWidgetSize, } from "./widgets"; import { AddWidgetDialog } from "./widgets/AddWidgetDialog"; import "react-grid-layout/css/styles.css"; @@ -54,6 +55,9 @@ export function Server({ "offline", ); const [metrics, setMetrics] = React.useState(null); + const [metricsHistory, setMetricsHistory] = React.useState( + [], + ); const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); const [isLoadingMetrics, setIsLoadingMetrics] = React.useState(false); const [isRefreshing, setIsRefreshing] = React.useState(false); @@ -120,10 +124,36 @@ export function Server({ setHasUnsavedChanges(true); }; - const handleAddWidget = (widgetType: string) => { + const handleChangeWidgetSize = ( + widgetId: string, + newSize: any, + e: React.MouseEvent, + ) => { + 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 newId = generateWidgetId(widgetType as any, existingIds); const config = getWidgetConfig(widgetType as any); + const sizeConfig = getWidgetSize(widgetType as any, size); // Find the next available position const maxY = widgets.reduce((max, w) => Math.max(max, w.y + w.h), 0); @@ -131,10 +161,11 @@ export function Server({ const newWidget: Widget = { id: newId, type: widgetType as any, + size: size, x: 0, y: maxY, - w: config.defaultSize.w, - h: config.defaultSize.h, + w: sizeConfig.w, + h: sizeConfig.h, }; setWidgets((prev) => [...prev, newWidget]); @@ -173,9 +204,12 @@ export function Server({ return ( ); @@ -183,9 +217,12 @@ export function Server({ return ( ); @@ -193,9 +230,12 @@ export function Server({ return ( ); @@ -275,6 +315,11 @@ export function Server({ const data = await getServerMetricsById(currentHostConfig.id); if (!cancelled) { setMetrics(data); + setMetricsHistory((prev) => { + const newHistory = [...prev, data]; + // Keep last 20 data points for chart + return newHistory.slice(-20); + }); setShowStatsUI(true); } } catch (error: any) { diff --git a/src/ui/Desktop/Apps/Server/widgets/AddWidgetDialog.tsx b/src/ui/Desktop/Apps/Server/widgets/AddWidgetDialog.tsx index 1001fec8..dc6cbe1b 100644 --- a/src/ui/Desktop/Apps/Server/widgets/AddWidgetDialog.tsx +++ b/src/ui/Desktop/Apps/Server/widgets/AddWidgetDialog.tsx @@ -1,11 +1,12 @@ import React from "react"; import { getAvailableWidgets, type WidgetRegistryItem } from "./registry"; import { Plus, X } from "lucide-react"; +import type { WidgetSize } from "@/types/stats-widgets"; interface AddWidgetDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onAddWidget: (widgetType: string) => void; + onAddWidget: (widgetType: string, size: WidgetSize) => void; existingWidgetTypes: string[]; } @@ -16,6 +17,13 @@ export function AddWidgetDialog({ existingWidgetTypes, }: AddWidgetDialogProps) { const availableWidgets = getAvailableWidgets(); + const [selectedSize, setSelectedSize] = React.useState("medium"); + + const sizeLabels: Record = { + small: "Small", + medium: "Medium", + large: "Large", + }; if (!open) return null; @@ -36,8 +44,26 @@ export function AddWidgetDialog({

- Choose a widget to add to your dashboard + Choose a widget and size to add to your dashboard

+ + {/* Size selector */} +
+ {(["small", "medium", "large"] as WidgetSize[]).map((size) => ( + + ))} +
+
{availableWidgets.map((widget: WidgetRegistryItem) => { const Icon = widget.icon; @@ -45,7 +71,7 @@ export function AddWidgetDialog({ + <> + + + )}

CPU Usage

-
-
- - {(() => { - const pct = metrics?.cpu?.percent; - const cores = metrics?.cpu?.cores; - const pctText = typeof pct === "number" ? `${pct}%` : "N/A"; - const coresText = - typeof cores === "number" - ? t("serverStats.cpuCores", { count: cores }) - : t("serverStats.naCpus"); - return `${pctText} ${t("serverStats.of")} ${coresText}`; - })()} - + + {widgetSize === "small" && ( +
+
+ {typeof metrics?.cpu?.percent === "number" + ? `${metrics.cpu.percent}%` + : "N/A"} +
+
+ {typeof metrics?.cpu?.cores === "number" + ? t("serverStats.cpuCores", { count: metrics.cpu.cores }) + : t("serverStats.naCpus")} +
-
- + )} + + {widgetSize === "medium" && ( +
+
+ + {(() => { + const pct = metrics?.cpu?.percent; + const cores = metrics?.cpu?.cores; + const pctText = typeof pct === "number" ? `${pct}%` : "N/A"; + const coresText = + typeof cores === "number" + ? t("serverStats.cpuCores", { count: cores }) + : t("serverStats.naCpus"); + return `${pctText} ${t("serverStats.of")} ${coresText}`; + })()} + +
+
+ +
+
+ {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"} +
-
- {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"} + )} + + {widgetSize === "large" && ( +
+
+
+ {typeof metrics?.cpu?.percent === "number" + ? `${metrics.cpu.percent}%` + : "N/A"} +
+
+ {typeof metrics?.cpu?.cores === "number" + ? t("serverStats.cpuCores", { count: metrics.cpu.cores }) + : t("serverStats.naCpus")} +
+
+
+ {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"} +
+
+ + + + + + [`${value.toFixed(1)}%`, "CPU"]} + /> + + + +
-
+ )}
); } diff --git a/src/ui/Desktop/Apps/Server/widgets/DiskWidget.tsx b/src/ui/Desktop/Apps/Server/widgets/DiskWidget.tsx index 6c360762..ddb43aa1 100644 --- a/src/ui/Desktop/Apps/Server/widgets/DiskWidget.tsx +++ b/src/ui/Desktop/Apps/Server/widgets/DiskWidget.tsx @@ -1,74 +1,200 @@ 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 { useTranslation } from "react-i18next"; 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 { metrics: ServerMetrics | null; + metricsHistory: ServerMetrics[]; isEditMode: boolean; widgetId: string; - onDelete: (widgetId: string, e: React.MouseEvent) => void; + widgetSize: WidgetSize; + onDelete: (widgetId: string, e: React.MouseEvent) => void; + onChangeSize: ( + widgetId: string, + newSize: WidgetSize, + e: React.MouseEvent, + ) => void; } export function DiskWidget({ metrics, + metricsHistory, isEditMode, widgetId, + widgetSize, onDelete, + onChangeSize, }: DiskWidgetProps) { 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 ( -
+
{isEditMode && ( - + <> + + + )}

Disk Usage

-
-
- + + {widgetSize === "small" && ( +
+
+ {typeof metrics?.disk?.percent === "number" + ? `${metrics.disk.percent}%` + : "N/A"} +
+
{(() => { - const pct = metrics?.disk?.percent; const used = metrics?.disk?.usedHuman; const total = metrics?.disk?.totalHuman; - const pctText = typeof pct === "number" ? `${pct}%` : "N/A"; - const usedText = used ?? "N/A"; - const totalText = total ?? "N/A"; - return `${pctText} (${usedText} ${t("serverStats.of")} ${totalText})`; + if (used && total) { + return `${used} / ${total}`; + } + return "N/A"; })()} - +
-
- + )} + + {widgetSize === "medium" && ( +
+
+ + {(() => { + const pct = metrics?.disk?.percent; + const used = metrics?.disk?.usedHuman; + const total = metrics?.disk?.totalHuman; + const pctText = typeof pct === "number" ? `${pct}%` : "N/A"; + const usedText = used ?? "N/A"; + const totalText = total ?? "N/A"; + return `${pctText} (${usedText} ${t("serverStats.of")} ${totalText})`; + })()} + +
+
+ +
+
+ {(() => { + const available = metrics?.disk?.availableHuman; + return available ? `Available: ${available}` : "Available: N/A"; + })()} +
-
- {(() => { - const available = metrics?.disk?.availableHuman; - return available ? `Available: ${available}` : "Available: N/A"; - })()} + )} + + {widgetSize === "large" && ( +
+
+ + + + + + {typeof metrics?.disk?.percent === "number" + ? `${metrics.disk.percent}%` + : "N/A"} + + + +
+
+
+ {(() => { + const used = metrics?.disk?.usedHuman; + const total = metrics?.disk?.totalHuman; + if (used && total) { + return `${used} / ${total}`; + } + return "N/A"; + })()} +
+
+ {(() => { + const available = metrics?.disk?.availableHuman; + return available ? `Available: ${available}` : "Available: N/A"; + })()} +
+
-
+ )}
); } diff --git a/src/ui/Desktop/Apps/Server/widgets/MemoryWidget.tsx b/src/ui/Desktop/Apps/Server/widgets/MemoryWidget.tsx index b00e98de..bd56f322 100644 --- a/src/ui/Desktop/Apps/Server/widgets/MemoryWidget.tsx +++ b/src/ui/Desktop/Apps/Server/widgets/MemoryWidget.tsx @@ -1,81 +1,233 @@ 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 { useTranslation } from "react-i18next"; 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 { metrics: ServerMetrics | null; + metricsHistory: ServerMetrics[]; isEditMode: boolean; widgetId: string; + widgetSize: WidgetSize; onDelete: (widgetId: string, e: React.MouseEvent) => void; + onChangeSize: ( + widgetId: string, + newSize: WidgetSize, + e: React.MouseEvent, + ) => void; } export function MemoryWidget({ metrics, + metricsHistory, isEditMode, widgetId, + widgetSize, onDelete, + onChangeSize, }: MemoryWidgetProps) { 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 ( -
+
{isEditMode && ( - + <> + + + )}

Memory Usage

-
-
- + + {widgetSize === "small" && ( +
+
+ {typeof metrics?.memory?.percent === "number" + ? `${metrics.memory.percent}%` + : "N/A"} +
+
{(() => { - const pct = metrics?.memory?.percent; const used = metrics?.memory?.usedGiB; const total = metrics?.memory?.totalGiB; - const pctText = typeof pct === "number" ? `${pct}%` : "N/A"; - const usedText = - typeof used === "number" ? `${used.toFixed(1)} GiB` : "N/A"; - const totalText = - typeof total === "number" ? `${total.toFixed(1)} GiB` : "N/A"; - return `${pctText} (${usedText} ${t("serverStats.of")} ${totalText})`; + if (typeof used === "number" && typeof total === "number") { + return `${used.toFixed(1)} / ${total.toFixed(1)} GiB`; + } + return "N/A"; })()} - +
-
- + )} + + {widgetSize === "medium" && ( +
+
+ + {(() => { + const pct = metrics?.memory?.percent; + const used = metrics?.memory?.usedGiB; + const total = metrics?.memory?.totalGiB; + const pctText = typeof pct === "number" ? `${pct}%` : "N/A"; + const usedText = + typeof used === "number" ? `${used.toFixed(1)} GiB` : "N/A"; + const totalText = + typeof total === "number" ? `${total.toFixed(1)} GiB` : "N/A"; + return `${pctText} (${usedText} ${t("serverStats.of")} ${totalText})`; + })()} + +
+
+ +
+
+ {(() => { + 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`; + })()} +
-
- {(() => { - 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`; - })()} + )} + + {widgetSize === "large" && ( +
+
+
+ {typeof metrics?.memory?.percent === "number" + ? `${metrics.memory.percent}%` + : "N/A"} +
+
+ {(() => { + 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"; + })()} +
+
+
+ {(() => { + 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`; + })()} +
+
+ + + + + + + + + + + + [ + `${value.toFixed(1)}%`, + "Memory", + ]} + /> + + + +
-
+ )}
); } diff --git a/src/ui/Desktop/Apps/Server/widgets/index.ts b/src/ui/Desktop/Apps/Server/widgets/index.ts index 55e897ff..f4ef1263 100644 --- a/src/ui/Desktop/Apps/Server/widgets/index.ts +++ b/src/ui/Desktop/Apps/Server/widgets/index.ts @@ -5,6 +5,7 @@ export { WIDGET_REGISTRY, getAvailableWidgets, getWidgetConfig, + getWidgetSize, generateWidgetId, type WidgetRegistryItem, } from "./registry"; diff --git a/src/ui/Desktop/Apps/Server/widgets/registry.ts b/src/ui/Desktop/Apps/Server/widgets/registry.ts index 3f41e990..807a5cf3 100644 --- a/src/ui/Desktop/Apps/Server/widgets/registry.ts +++ b/src/ui/Desktop/Apps/Server/widgets/registry.ts @@ -1,5 +1,10 @@ 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 { type: WidgetType; @@ -7,7 +12,7 @@ export interface WidgetRegistryItem { description: string; icon: LucideIcon; iconColor: string; - defaultSize: { w: number; h: number }; + sizes: Record; minSize: { w: number; h: number }; maxSize: { w: number; h: number }; } @@ -19,7 +24,11 @@ export const WIDGET_REGISTRY: Record = { description: "Monitor CPU utilization and load average", icon: Cpu, 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 }, maxSize: { w: 12, h: 4 }, }, @@ -29,7 +38,11 @@ export const WIDGET_REGISTRY: Record = { description: "Track RAM usage and availability", icon: MemoryStick, 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 }, maxSize: { w: 12, h: 4 }, }, @@ -39,7 +52,11 @@ export const WIDGET_REGISTRY: Record = { description: "View disk space consumption", icon: HardDrive, 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 }, maxSize: { w: 12, h: 4 }, }, @@ -59,6 +76,16 @@ export function getWidgetConfig(type: WidgetType): WidgetRegistryItem { 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 */