diff --git a/package-lock.json b/package-lock.json index 363bc91..eeb7c2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,9 @@ "next": "16.1.1", "react": "19.2.3", "react-dom": "19.2.3", - "tailwind-merge": "^3.4.0" + "recharts": "^3.6.0", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -864,6 +866,54 @@ } } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "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/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.0.tgz", + "integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1144,6 +1194,69 @@ "tailwindcss": "4.1.18" } }, + "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/docker-modem": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", @@ -1218,6 +1331,12 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "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/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1439,6 +1558,127 @@ "devOptional": true, "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/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1456,6 +1696,12 @@ } } }, + "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/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1528,6 +1774,16 @@ "node": ">=10.13.0" } }, + "node_modules/es-toolkit": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", + "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1537,6 +1793,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -1579,12 +1841,31 @@ ], "license": "BSD-3-Clause" }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "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/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2113,6 +2394,36 @@ "react": "^19.2.3" } }, + "node_modules/react-is": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", + "license": "MIT", + "peer": true + }, + "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/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -2127,6 +2438,51 @@ "node": ">= 6" } }, + "node_modules/recharts": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", + "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "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/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2136,6 +2492,12 @@ "node": ">=0.10.0" } }, + "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/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2330,9 +2692,17 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, "license": "MIT" }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -2375,6 +2745,12 @@ "node": ">=6" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2417,6 +2793,15 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "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/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2436,6 +2821,28 @@ "uuid": "dist/bin/uuid" } }, + "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/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 36cb898..a13920d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "next": "16.1.1", "react": "19.2.3", "react-dom": "19.2.3", - "tailwind-merge": "^3.4.0" + "recharts": "^3.6.0", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx new file mode 100644 index 0000000..108c2cb --- /dev/null +++ b/src/app/applications/page.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { StatusIndicator } from "@/components/dashboard/StatusIndicator"; +import { MetricBar } from "@/components/dashboard/MetricBar"; +import { Play, Square, RotateCcw, MoreVertical, Plus } from "lucide-react"; + +const applications = [ + { + id: "app-001", + name: "api-gateway", + image: "dockship/api-gateway:v2.4.1", + containers: 3, + replicas: "3/3", + cpu: 45, + memory: 62, + network: "12.4 MB/s", + status: "running" as const, + ports: ["80:8080", "443:8443"], + }, + { + id: "app-002", + name: "auth-service", + image: "dockship/auth:v1.8.0", + containers: 2, + replicas: "2/2", + cpu: 28, + memory: 45, + network: "3.2 MB/s", + status: "running" as const, + ports: ["9000:9000"], + }, + { + id: "app-003", + name: "postgres-db", + image: "postgres:15-alpine", + containers: 1, + replicas: "1/1", + cpu: 65, + memory: 78, + network: "8.1 MB/s", + status: "running" as const, + ports: ["5432:5432"], + }, + { + id: "app-004", + name: "redis-cache", + image: "redis:7-alpine", + containers: 1, + replicas: "1/1", + cpu: 12, + memory: 34, + network: "1.8 MB/s", + status: "running" as const, + ports: ["6379:6379"], + }, + { + id: "app-005", + name: "worker-queue", + image: "dockship/worker:v3.1.2", + containers: 4, + replicas: "3/4", + cpu: 55, + memory: 48, + network: "5.6 MB/s", + status: "pending" as const, + ports: [], + }, + { + id: "app-006", + name: "metrics-collector", + image: "dockship/metrics:v1.2.0", + containers: 2, + replicas: "0/2", + cpu: 0, + memory: 0, + network: "0 MB/s", + status: "error" as const, + ports: ["9090:9090"], + }, +]; + +export default function ApplicationsPage() { + return ( + +
+
+ {applications.length} apps + + {applications.filter((a) => a.status === "running").length} running + +
+ +
+ +
+ {applications.map((app) => ( + + +
+ {/* Status & Name */} +
+ +
+
+ {app.name} +
+
+ {app.image} +
+
+
+ + {/* Replicas */} +
+
Replicas
+
{app.replicas}
+
+ + {/* CPU */} +
+
CPU
+
+ + {app.cpu}% +
+
+ + {/* Memory */} +
+
+ Memory +
+
+ + {app.memory}% +
+
+ + {/* Network */} +
+
Network
+
{app.network}
+
+ + {/* Ports */} +
+
+ Ports +
+
+ {app.ports.length > 0 ? ( + app.ports.map((port) => ( + + {port} + + )) + ) : ( + — + )} +
+
+ + {/* Actions */} +
+ {app.status === "running" ? ( + + ) : ( + + )} + + +
+
+
+
+ ))} +
+
+ ); +} diff --git a/src/app/containers/page.tsx b/src/app/containers/page.tsx new file mode 100644 index 0000000..8839456 --- /dev/null +++ b/src/app/containers/page.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { StatusIndicator } from "@/components/dashboard/StatusIndicator"; +import { Terminal, MoreVertical, Play, Square, Trash2 } from "lucide-react"; + +const containers = [ + { + id: "c-a1b2c3d4", + name: "api-gateway-1", + image: "dockship/api-gateway:v2.4.1", + node: "node-01", + status: "running" as const, + uptime: "3d 12h 45m", + cpu: "12%", + memory: "256MB / 512MB", + }, + { + id: "c-e5f6g7h8", + name: "api-gateway-2", + image: "dockship/api-gateway:v2.4.1", + node: "node-02", + status: "running" as const, + uptime: "3d 12h 45m", + cpu: "18%", + memory: "312MB / 512MB", + }, + { + id: "c-i9j0k1l2", + name: "api-gateway-3", + image: "dockship/api-gateway:v2.4.1", + node: "node-03", + status: "running" as const, + uptime: "3d 12h 45m", + cpu: "15%", + memory: "284MB / 512MB", + }, + { + id: "c-m3n4o5p6", + name: "auth-service-1", + image: "dockship/auth:v1.8.0", + node: "node-01", + status: "running" as const, + uptime: "5d 8h 22m", + cpu: "8%", + memory: "128MB / 256MB", + }, + { + id: "c-q7r8s9t0", + name: "auth-service-2", + image: "dockship/auth:v1.8.0", + node: "node-02", + status: "running" as const, + uptime: "5d 8h 22m", + cpu: "6%", + memory: "142MB / 256MB", + }, + { + id: "c-u1v2w3x4", + name: "postgres-db-1", + image: "postgres:15-alpine", + node: "node-01", + status: "running" as const, + uptime: "12d 4h 15m", + cpu: "22%", + memory: "1.2GB / 2GB", + }, + { + id: "c-y5z6a7b8", + name: "redis-cache-1", + image: "redis:7-alpine", + node: "node-02", + status: "running" as const, + uptime: "8d 16h 33m", + cpu: "4%", + memory: "64MB / 128MB", + }, + { + id: "c-c9d0e1f2", + name: "worker-queue-1", + image: "dockship/worker:v3.1.2", + node: "node-03", + status: "running" as const, + uptime: "1d 2h 18m", + cpu: "34%", + memory: "512MB / 1GB", + }, + { + id: "c-g3h4i5j6", + name: "worker-queue-2", + image: "dockship/worker:v3.1.2", + node: "node-03", + status: "pending" as const, + uptime: "—", + cpu: "—", + memory: "—", + }, + { + id: "c-k7l8m9n0", + name: "metrics-collector-1", + image: "dockship/metrics:v1.2.0", + node: "node-01", + status: "error" as const, + uptime: "—", + cpu: "—", + memory: "—", + }, +]; + +export default function ContainersPage() { + return ( + +
+ {containers.length} containers + + {containers.filter((c) => c.status === "running").length} running + + + {containers.filter((c) => c.status === "pending").length} pending + + + {containers.filter((c) => c.status === "error").length} error + +
+ + + + + + + + + + + + + + + + + + {containers.map((container) => ( + + + + + + + + + + + ))} + +
+ Container + + Image + + Node + + Status + + Uptime + + CPU + + Memory + + Actions +
+
+ +
+
+ {container.name} +
+
+ {container.id} +
+
+
+
+ + {container.image} + + + {container.node} + + + + + {container.uptime} + + + {container.cpu} + + + {container.memory} + + +
+ {container.status === "running" ? ( + + ) : ( + + )} + + +
+
+
+
+
+ ); +} diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx new file mode 100644 index 0000000..703262a --- /dev/null +++ b/src/app/deployments/page.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { StatusIndicator } from "@/components/dashboard/StatusIndicator"; +import { + Rocket, + RotateCcw, + Eye, + MoreVertical, + Plus, + GitBranch, +} from "lucide-react"; + +const deployments = [ + { + id: "dep-001", + app: "api-gateway", + version: "v2.4.1", + previousVersion: "v2.4.0", + status: "running" as const, + deployedAt: "2024-01-15 14:32:18", + deployedBy: "ci/github-actions", + duration: "45s", + replicas: "3/3", + strategy: "rolling", + }, + { + id: "dep-002", + app: "auth-service", + version: "v1.8.0", + previousVersion: "v1.7.9", + status: "running" as const, + deployedAt: "2024-01-15 14:18:42", + deployedBy: "admin@dockship.io", + duration: "32s", + replicas: "2/2", + strategy: "rolling", + }, + { + id: "dep-003", + app: "worker-queue", + version: "v3.1.2", + previousVersion: "v3.1.1", + status: "pending" as const, + deployedAt: "2024-01-15 13:58:05", + deployedBy: "ci/github-actions", + duration: "—", + replicas: "3/4", + strategy: "rolling", + }, + { + id: "dep-004", + app: "metrics-collector", + version: "v1.2.0", + previousVersion: "v1.1.8", + status: "error" as const, + deployedAt: "2024-01-15 13:42:11", + deployedBy: "ci/github-actions", + duration: "—", + replicas: "0/2", + strategy: "rolling", + }, + { + id: "dep-005", + app: "postgres-db", + version: "15-alpine", + previousVersion: "14-alpine", + status: "running" as const, + deployedAt: "2024-01-10 09:15:33", + deployedBy: "admin@dockship.io", + duration: "2m 18s", + replicas: "1/1", + strategy: "recreate", + }, + { + id: "dep-006", + app: "redis-cache", + version: "7-alpine", + previousVersion: "7-alpine", + status: "running" as const, + deployedAt: "2024-01-08 16:45:22", + deployedBy: "admin@dockship.io", + duration: "18s", + replicas: "1/1", + strategy: "recreate", + }, +]; + +export default function DeploymentsPage() { + return ( + +
+
+ {deployments.length} deployments +
+ +
+ + + + + + + + + + + + + + + + + + {deployments.map((dep) => ( + + + + + + + + + + + ))} + +
+ Application + + Version + + Status + + Replicas + + Deployed + + Duration + + Deployed By + + Actions +
+
+ + {dep.app} +
+
+
+ + {dep.version} + + ← {dep.previousVersion} + +
+
+ + + {dep.replicas} + + + {dep.deployedAt} + + + {dep.duration} + + + {dep.deployedBy} + + +
+ + + +
+
+
+
+
+ ); +} diff --git a/src/app/environment/page.tsx b/src/app/environment/page.tsx new file mode 100644 index 0000000..162df2e --- /dev/null +++ b/src/app/environment/page.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useState } from "react"; +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Plus, Trash2, Eye, EyeOff, Copy, Save, Lock } from "lucide-react"; + +const envGroups = [ + { + name: "api-gateway", + variables: [ + { key: "PORT", value: "8080", secret: false }, + { key: "NODE_ENV", value: "production", secret: false }, + { key: "API_KEY", value: "sk-xxxx-xxxx-xxxx", secret: true }, + { key: "DATABASE_URL", value: "postgresql://...", secret: true }, + { key: "REDIS_URL", value: "redis://localhost:6379", secret: false }, + { key: "LOG_LEVEL", value: "info", secret: false }, + ], + }, + { + name: "auth-service", + variables: [ + { key: "PORT", value: "9000", secret: false }, + { key: "JWT_SECRET", value: "xxxxxxxxxxxx", secret: true }, + { key: "JWT_EXPIRY", value: "7d", secret: false }, + { key: "OAUTH_CLIENT_ID", value: "client-id-xxxx", secret: true }, + { key: "OAUTH_CLIENT_SECRET", value: "client-secret-xxxx", secret: true }, + ], + }, + { + name: "worker-queue", + variables: [ + { key: "QUEUE_URL", value: "amqp://localhost:5672", secret: false }, + { key: "CONCURRENCY", value: "10", secret: false }, + { key: "RETRY_LIMIT", value: "3", secret: false }, + { key: "DEAD_LETTER_QUEUE", value: "dlq.worker", secret: false }, + ], + }, +]; + +export default function EnvironmentPage() { + const [selectedApp, setSelectedApp] = useState("api-gateway"); + const [showSecrets, setShowSecrets] = useState>({}); + + const currentGroup = envGroups.find((g) => g.name === selectedApp); + + const toggleSecret = (key: string) => { + setShowSecrets((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + const maskValue = (value: string) => { + return "•".repeat(Math.min(value.length, 24)); + }; + + return ( + +
+ {/* Sidebar */} +
+ + + + Applications + + + + {envGroups.map((group) => ( + + ))} + + +
+ + {/* Main Content */} +
+ + +
+ {selectedApp} +

+ {currentGroup?.variables.length} variables •{" "} + {currentGroup?.variables.filter((v) => v.secret).length}{" "} + secrets +

+
+
+ + +
+
+ +
+
+
Key
+
Value
+
Actions
+
+ {currentGroup?.variables.map((variable) => ( +
+
+ {variable.secret && ( + + )} + {variable.key} +
+
+
+ +
+
+
+ {variable.secret && ( + + )} + + +
+
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..11234a9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,7 +25,7 @@ export default function RootLayout({ return ( {children} diff --git a/src/app/logs/page.tsx b/src/app/logs/page.tsx new file mode 100644 index 0000000..595a3ad --- /dev/null +++ b/src/app/logs/page.tsx @@ -0,0 +1,267 @@ +"use client"; + +import { useState } from "react"; +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Pause, Play, Download, Filter, RefreshCw } from "lucide-react"; + +const logEntries = [ + { + timestamp: "2024-01-15 14:32:45.123", + level: "info", + source: "api-gateway-1", + message: "Request received: GET /api/v1/users", + }, + { + timestamp: "2024-01-15 14:32:45.145", + level: "info", + source: "api-gateway-1", + message: "Response sent: 200 OK (22ms)", + }, + { + timestamp: "2024-01-15 14:32:46.012", + level: "info", + source: "auth-service-1", + message: "Token validation successful for user_id=12345", + }, + { + timestamp: "2024-01-15 14:32:46.234", + level: "warn", + source: "worker-queue-2", + message: "Job queue depth exceeded threshold: 150 pending jobs", + }, + { + timestamp: "2024-01-15 14:32:47.001", + level: "info", + source: "postgres-db-1", + message: "Query executed: SELECT * FROM users WHERE id = $1 (2ms)", + }, + { + timestamp: "2024-01-15 14:32:47.456", + level: "info", + source: "api-gateway-2", + message: "Request received: POST /api/v1/orders", + }, + { + timestamp: "2024-01-15 14:32:47.789", + level: "error", + source: "metrics-collector-1", + message: "Failed to connect to metrics endpoint: connection refused", + }, + { + timestamp: "2024-01-15 14:32:48.012", + level: "error", + source: "metrics-collector-1", + message: "Retry attempt 1/3 failed", + }, + { + timestamp: "2024-01-15 14:32:48.234", + level: "info", + source: "redis-cache-1", + message: "Cache hit: session:user:12345", + }, + { + timestamp: "2024-01-15 14:32:49.001", + level: "info", + source: "worker-queue-1", + message: "Job completed: send_email_notification (job_id=abc123)", + }, + { + timestamp: "2024-01-15 14:32:49.345", + level: "warn", + source: "api-gateway-3", + message: "High latency detected: upstream response took 850ms", + }, + { + timestamp: "2024-01-15 14:32:50.012", + level: "info", + source: "auth-service-2", + message: "New session created for user_id=67890", + }, + { + timestamp: "2024-01-15 14:32:50.456", + level: "error", + source: "metrics-collector-1", + message: "Retry attempt 2/3 failed", + }, + { + timestamp: "2024-01-15 14:32:51.001", + level: "info", + source: "api-gateway-1", + message: "Request received: GET /api/v1/health", + }, + { + timestamp: "2024-01-15 14:32:51.023", + level: "info", + source: "api-gateway-1", + message: "Response sent: 200 OK (2ms)", + }, + { + timestamp: "2024-01-15 14:32:52.012", + level: "info", + source: "worker-queue-3", + message: "Processing job: generate_report (job_id=def456)", + }, + { + timestamp: "2024-01-15 14:32:52.234", + level: "error", + source: "metrics-collector-1", + message: "Retry attempt 3/3 failed. Giving up.", + }, + { + timestamp: "2024-01-15 14:32:52.456", + level: "error", + source: "metrics-collector-1", + message: "Container entering unhealthy state", + }, + { + timestamp: "2024-01-15 14:32:53.012", + level: "info", + source: "postgres-db-1", + message: "Connection pool: 12/50 active connections", + }, + { + timestamp: "2024-01-15 14:32:54.001", + level: "info", + source: "api-gateway-2", + message: "Request received: PUT /api/v1/users/12345", + }, +]; + +export default function LogsPage() { + const [isPaused, setIsPaused] = useState(false); + const [filter, setFilter] = useState<"all" | "info" | "warn" | "error">( + "all" + ); + + const filteredLogs = logEntries.filter((log) => { + if (filter === "all") return true; + return log.level === filter; + }); + + const getLevelClass = (level: string) => { + switch (level) { + case "info": + return "log-info"; + case "warn": + return "log-warn"; + case "error": + return "log-error"; + default: + return ""; + } + }; + + return ( + +
+
+ + + + +
+
+ + +
+ +
+
+ + + + + Log Stream + +
+ {filteredLogs.length} entries + {!isPaused && ( +
+ + Live +
+ )} +
+
+ +
+ {filteredLogs.map((log, index) => ( +
+ + {log.timestamp} + + + [{log.level}] + + + {log.source} + + {log.message} +
+ ))} +
+
+
+ + ); +} diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx new file mode 100644 index 0000000..95c3a18 --- /dev/null +++ b/src/app/monitoring/page.tsx @@ -0,0 +1,361 @@ +"use client"; + +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { MetricBar } from "@/components/dashboard/MetricBar"; +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + AreaChart, + Area, +} from "recharts"; + +const cpuData = [ + { time: "00:00", value: 45 }, + { time: "01:00", value: 52 }, + { time: "02:00", value: 38 }, + { time: "03:00", value: 42 }, + { time: "04:00", value: 35 }, + { time: "05:00", value: 48 }, + { time: "06:00", value: 55 }, + { time: "07:00", value: 62 }, + { time: "08:00", value: 78 }, + { time: "09:00", value: 85 }, + { time: "10:00", value: 72 }, + { time: "11:00", value: 68 }, + { time: "12:00", value: 75 }, +]; + +const memoryData = [ + { time: "00:00", value: 62 }, + { time: "01:00", value: 64 }, + { time: "02:00", value: 58 }, + { time: "03:00", value: 61 }, + { time: "04:00", value: 59 }, + { time: "05:00", value: 63 }, + { time: "06:00", value: 67 }, + { time: "07:00", value: 72 }, + { time: "08:00", value: 78 }, + { time: "09:00", value: 82 }, + { time: "10:00", value: 76 }, + { time: "11:00", value: 74 }, + { time: "12:00", value: 71 }, +]; + +const networkData = [ + { time: "00:00", in: 12, out: 8 }, + { time: "01:00", in: 15, out: 10 }, + { time: "02:00", in: 8, out: 5 }, + { time: "03:00", in: 10, out: 7 }, + { time: "04:00", in: 6, out: 4 }, + { time: "05:00", in: 14, out: 9 }, + { time: "06:00", in: 22, out: 15 }, + { time: "07:00", in: 35, out: 28 }, + { time: "08:00", in: 48, out: 38 }, + { time: "09:00", in: 52, out: 42 }, + { time: "10:00", in: 45, out: 35 }, + { time: "11:00", in: 42, out: 32 }, + { time: "12:00", in: 38, out: 28 }, +]; + +const containerMetrics = [ + { name: "api-gateway-1", cpu: 45, memory: 62, network: "12.4 MB/s" }, + { name: "api-gateway-2", cpu: 52, memory: 58, network: "10.2 MB/s" }, + { name: "api-gateway-3", cpu: 38, memory: 55, network: "8.8 MB/s" }, + { name: "auth-service-1", cpu: 28, memory: 45, network: "3.2 MB/s" }, + { name: "auth-service-2", cpu: 32, memory: 48, network: "2.8 MB/s" }, + { name: "postgres-db-1", cpu: 65, memory: 78, network: "8.1 MB/s" }, + { name: "redis-cache-1", cpu: 12, memory: 34, network: "1.8 MB/s" }, + { name: "worker-queue-1", cpu: 55, memory: 48, network: "5.6 MB/s" }, +]; + +export default function MonitoringPage() { + return ( + + {/* Summary Stats */} +
+ + +
+ Cluster CPU +
+
68%
+ +
+
+ + +
+ Cluster Memory +
+
71%
+ +
+
+ + +
+ Network In +
+
38 MB/s
+
↑ 12% from avg
+
+
+ + +
+ Network Out +
+
28 MB/s
+
+ ↓ 5% from avg +
+
+
+
+ + {/* Charts */} +
+ + + + CPU Usage + Last 12 hours + + + +
+ + + + + + + + + + + + + + +
+
+
+ + + + + Memory Usage + Last 12 hours + + + +
+ + + + + + + + + + + + + + +
+
+
+
+ + {/* Network Chart */} + + + + Network I/O + Last 12 hours + + + +
+ + + + + + + + + +
+
+
+
+ Inbound +
+
+
+ Outbound +
+
+ + + + {/* Container Metrics */} + + + Container Metrics + + +
+ {containerMetrics.map((container) => ( +
+
+ {container.name} +
+
+
+
+ CPU + {container.cpu}% +
+ +
+
+
+ Memory + {container.memory}% +
+ +
+
+ Network + {container.network} +
+
+
+ ))} +
+
+
+ + ); +} diff --git a/src/app/nodes/page.tsx b/src/app/nodes/page.tsx new file mode 100644 index 0000000..565c9d2 --- /dev/null +++ b/src/app/nodes/page.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { StatusIndicator } from "@/components/dashboard/StatusIndicator"; +import { MetricBar } from "@/components/dashboard/MetricBar"; +import { Server, Plus, Settings, Trash2 } from "lucide-react"; + +const nodes = [ + { + id: "node-01", + name: "node-01", + hostname: "dockship-node-01.local", + ip: "10.0.1.10", + os: "Ubuntu 22.04 LTS", + docker: "24.0.7", + cpu: { cores: 8, usage: 72 }, + memory: { total: "32 GB", used: "21.8 GB", percentage: 68 }, + disk: { total: "500 GB", used: "245 GB", percentage: 49 }, + containers: 8, + status: "running" as const, + labels: ["production", "primary"], + }, + { + id: "node-02", + name: "node-02", + hostname: "dockship-node-02.local", + ip: "10.0.1.11", + os: "Ubuntu 22.04 LTS", + docker: "24.0.7", + cpu: { cores: 8, usage: 45 }, + memory: { total: "32 GB", used: "16.6 GB", percentage: 52 }, + disk: { total: "500 GB", used: "178 GB", percentage: 36 }, + containers: 6, + status: "running" as const, + labels: ["production"], + }, + { + id: "node-03", + name: "node-03", + hostname: "dockship-node-03.local", + ip: "10.0.1.12", + os: "Ubuntu 22.04 LTS", + docker: "24.0.7", + cpu: { cores: 16, usage: 89 }, + memory: { total: "64 GB", used: "54.4 GB", percentage: 85 }, + disk: { total: "1 TB", used: "612 GB", percentage: 61 }, + containers: 12, + status: "running" as const, + labels: ["production", "high-memory"], + }, +]; + +export default function NodesPage() { + return ( + +
+
+ {nodes.length} nodes + + {nodes.filter((n) => n.status === "running").length} online + +
+ +
+ +
+ {nodes.map((node) => ( + + +
+
+ +
+
+
+ + {node.name} +
+
+ {node.hostname} • {node.ip} +
+
+
+
+ {node.labels.map((label) => ( + + {label} + + ))} + + +
+
+ +
+ {/* System Info */} +
+
+ System +
+
+
+ OS + {node.os} +
+
+ Docker + {node.docker} +
+
+ Containers + {node.containers} +
+
+
+ + {/* CPU */} +
+
+ CPU ({node.cpu.cores} cores) +
+
{node.cpu.usage}%
+ +
+ + {/* Memory */} +
+
+ Memory +
+
+ {node.memory.percentage}% +
+ +
+ {node.memory.used} / {node.memory.total} +
+
+ + {/* Disk */} +
+
Disk
+
{node.disk.percentage}%
+ +
+ {node.disk.used} / {node.disk.total} +
+
+ + {/* Quick Actions */} +
+
+ Quick Actions +
+
+ + +
+
+
+
+
+ ))} +
+
+ ); +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..044b110 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect } from "react"; + +const NotFound = () => { + useEffect(() => { + console.error( + "404 Error: User attempted to access non-existent route:", + location.pathname + ); + }, [location.pathname]); + + return ( +
+
+

404

+

+ Oops! Page not found +

+ + Return to Home + +
+
+ ); +}; + +export default NotFound; diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx new file mode 100644 index 0000000..1083036 --- /dev/null +++ b/src/app/overview/page.tsx @@ -0,0 +1,278 @@ +"use client"; + +import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; +import { StatCard } from "@/components/dashboard/StatCard"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { StatusIndicator } from "@/components/dashboard/StatusIndicator"; +import { MetricBar } from "@/components/dashboard/MetricBar"; +import { + Box, + Server, + Container, + Activity, + ArrowRight, + Clock, +} from "lucide-react"; + +const recentDeployments = [ + { + id: "dep-001", + app: "api-gateway", + version: "v2.4.1", + status: "running" as const, + time: "2m ago", + }, + { + id: "dep-002", + app: "auth-service", + version: "v1.8.0", + status: "running" as const, + time: "15m ago", + }, + { + id: "dep-003", + app: "worker-queue", + version: "v3.1.2", + status: "pending" as const, + time: "32m ago", + }, + { + id: "dep-004", + app: "metrics-collector", + version: "v1.2.0", + status: "error" as const, + time: "1h ago", + }, +]; + +const applications = [ + { + name: "api-gateway", + containers: 3, + cpu: 45, + memory: 62, + status: "running" as const, + }, + { + name: "auth-service", + containers: 2, + cpu: 28, + memory: 45, + status: "running" as const, + }, + { + name: "postgres-db", + containers: 1, + cpu: 65, + memory: 78, + status: "running" as const, + }, + { + name: "redis-cache", + containers: 1, + cpu: 12, + memory: 34, + status: "running" as const, + }, + { + name: "worker-queue", + containers: 4, + cpu: 55, + memory: 48, + status: "pending" as const, + }, +]; + +const nodes = [ + { + name: "node-01", + ip: "10.0.1.10", + cpu: 72, + memory: 68, + containers: 8, + status: "running" as const, + }, + { + name: "node-02", + ip: "10.0.1.11", + cpu: 45, + memory: 52, + containers: 6, + status: "running" as const, + }, + { + name: "node-03", + ip: "10.0.1.12", + cpu: 89, + memory: 85, + containers: 12, + status: "running" as const, + }, +]; + +export default function OverviewPage() { + return ( + + {/* Stats Grid */} +
+ } + /> + } + /> + } + /> + } + /> +
+ +
+ {/* Applications Table */} + + + Applications + + + +
+
+
Name
+
Containers
+
CPU
+
Memory
+
Status
+
+ {applications.map((app) => ( +
+
{app.name}
+
+ {app.containers} +
+
+
+ + + {app.cpu}% + +
+
+
+
+ + + {app.memory}% + +
+
+
+ +
+
+ ))} +
+
+
+ + {/* Recent Deployments */} + + + Recent Deployments + + + +
+ {recentDeployments.map((dep) => ( +
+
+ +
+
{dep.app}
+
+ {dep.version} +
+
+
+
+ + {dep.time} +
+
+ ))} +
+
+
+
+ + {/* Nodes */} + + + Cluster Nodes + {nodes.length} nodes + + +
+ {nodes.map((node) => ( +
+
+
+ + {node.name} +
+ + {node.ip} + +
+
+
+ CPU + {node.cpu}% +
+ +
+ Memory + {node.memory}% +
+ +
+ Containers + {node.containers} +
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/components/dashboard/DashboardHeader.tsx b/src/components/dashboard/DashboardHeader.tsx new file mode 100644 index 0000000..1997bf5 --- /dev/null +++ b/src/components/dashboard/DashboardHeader.tsx @@ -0,0 +1,33 @@ +import { Bell, Search, User } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface DashboardHeaderProps { + title: string; + subtitle?: string; +} + +export function DashboardHeader({ title, subtitle }: DashboardHeaderProps) { + return ( +
+
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+ +
+ + +
+ +
+
+ ); +} diff --git a/src/components/dashboard/DashboardLayout.tsx b/src/components/dashboard/DashboardLayout.tsx new file mode 100644 index 0000000..9202589 --- /dev/null +++ b/src/components/dashboard/DashboardLayout.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from "react"; +import { DashboardSidebar } from "./DashboardSidebar"; +import { DashboardHeader } from "./DashboardHeader"; + +interface DashboardLayoutProps { + children: ReactNode; + title: string; + subtitle?: string; +} + +export function DashboardLayout({ children, title, subtitle }: DashboardLayoutProps) { + return ( +
+ +
+ +
+ {children} +
+
+
+ ); +} diff --git a/src/components/dashboard/DashboardSidebar.tsx b/src/components/dashboard/DashboardSidebar.tsx new file mode 100644 index 0000000..d3cbf27 --- /dev/null +++ b/src/components/dashboard/DashboardSidebar.tsx @@ -0,0 +1,97 @@ +import { useState } from "react"; +import { + LayoutGrid, + Box, + Server, + Rocket, + FileText, + Settings, + Activity, + ChevronLeft, + ChevronRight, + Terminal, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { usePathname } from "next/navigation"; +import Link from "next/link"; + +const navigation = [ + { name: "Overview", href: "/", icon: LayoutGrid }, + { name: "Applications", href: "/applications", icon: Box }, + { name: "Containers", href: "/containers", icon: Terminal }, + { name: "Nodes", href: "/nodes", icon: Server }, + { name: "Deployments", href: "/deployments", icon: Rocket }, + { name: "Logs", href: "/logs", icon: FileText }, + { name: "Environment", href: "/environment", icon: Settings }, + { name: "Monitoring", href: "/monitoring", icon: Activity }, +]; + +export function DashboardSidebar() { + const [collapsed, setCollapsed] = useState(false); + const location = usePathname(); + + return ( + + ); +} diff --git a/src/components/dashboard/MetricBar.tsx b/src/components/dashboard/MetricBar.tsx new file mode 100644 index 0000000..f3c1c3c --- /dev/null +++ b/src/components/dashboard/MetricBar.tsx @@ -0,0 +1,32 @@ +interface MetricBarProps { + value: number; + max: number; + variant?: "default" | "success" | "warning" | "danger"; +} + +export function MetricBar({ value, max, variant = "default" }: MetricBarProps) { + const percentage = Math.min((value / max) * 100, 100); + + const getColor = () => { + if (variant !== "default") { + switch (variant) { + case "success": return "bg-success"; + case "warning": return "bg-warning"; + case "danger": return "bg-destructive"; + } + } + + if (percentage > 80) return "bg-destructive"; + if (percentage > 60) return "bg-warning"; + return "bg-primary"; + }; + + return ( +
+
+
+ ); +} diff --git a/src/components/dashboard/StatCard.tsx b/src/components/dashboard/StatCard.tsx new file mode 100644 index 0000000..835429d --- /dev/null +++ b/src/components/dashboard/StatCard.tsx @@ -0,0 +1,46 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +interface StatCardProps { + title: string; + value: string | number; + subtitle?: string; + icon?: React.ReactNode; + trend?: { + value: number; + isPositive: boolean; + }; +} + +export function StatCard({ title, value, subtitle, icon, trend }: StatCardProps) { + return ( + + + + {title} + + {icon &&
{icon}
} +
+ +
{value}
+ {(subtitle || trend) && ( +
+ {trend && ( + + {trend.isPositive ? "+" : ""}{trend.value}% + + )} + {subtitle && ( + {subtitle} + )} +
+ )} +
+
+ ); +} diff --git a/src/components/dashboard/StatusIndicator.tsx b/src/components/dashboard/StatusIndicator.tsx new file mode 100644 index 0000000..6f3b752 --- /dev/null +++ b/src/components/dashboard/StatusIndicator.tsx @@ -0,0 +1,33 @@ +import { cn } from "@/lib/utils"; + +interface StatusIndicatorProps { + status: "running" | "stopped" | "error" | "pending"; + showLabel?: boolean; + size?: "sm" | "md"; +} + +const statusConfig = { + running: { label: "Running", className: "status-running" }, + stopped: { label: "Stopped", className: "status-stopped" }, + error: { label: "Error", className: "status-error" }, + pending: { label: "Pending", className: "status-pending" }, +}; + +export function StatusIndicator({ status, showLabel = false, size = "md" }: StatusIndicatorProps) { + const config = statusConfig[status]; + + return ( +
+
+ {showLabel && ( + {config.label} + )} +
+ ); +} diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..1e7878c --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..6dfbfb4 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,104 @@ +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..2efc3c8 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c9e6f4b --- /dev/null +++ b/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +const AspectRatio = AspectRatioPrimitive.Root; + +export { AspectRatio }; diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..68d21bb --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index fd3a406..f7bdefa 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,46 +1,34 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + "inline-flex items-center border px-2 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + default: "border-transparent bg-primary text-primary-foreground shadow", + secondary: "border-transparent bg-secondary text-secondary-foreground", + destructive: "border-transparent bg-destructive/20 text-destructive", + success: "border-transparent bg-success/20 text-success", + warning: "border-transparent bg-warning/20 text-warning", + outline: "text-foreground border-border", + terminal: "border-border bg-transparent text-muted-foreground font-mono", }, }, defaultVariants: { variant: "default", }, } -) +); -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} - return ( - - ) +function Badge({ className, variant, ...props }: BadgeProps) { + return
; } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..ca91ff5 --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,90 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>