diff --git a/.gitignore b/.gitignore index 74015c392..f411b5c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ .VSCodeCounter *.sh mongo +node_modules/ +docs/architecture +docs/reviews \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..dac57bbf1 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +echo "Running lint-staged..." +cd client && npx lint-staged && cd .. +cd server && npx lint-staged && cd .. \ No newline at end of file diff --git a/README.md b/README.md index 129953fb9..e0528d0cb 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,8 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb If you have any questions, suggestions or comments, you have several options: -- [Discord channel](https://discord.gg/NAb6H3UTjK) -- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) -- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/) +- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred) +- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time) Feel free to ask questions or share your ideas - we'd love to hear from you! @@ -148,7 +147,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec ## Contributing -We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers. +We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers. We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 7000+ stars and attracted 90+ contributors from around the globe. diff --git a/client/index.html b/client/index.html index 0b0f675b4..4f949edbb 100644 --- a/client/index.html +++ b/client/index.html @@ -18,7 +18,7 @@
diff --git a/client/package-lock.json b/client/package-lock.json index b4ad9f73c..038814543 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,10 +12,10 @@ "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^18.0.0", "@hookform/resolvers": "5.2.2", - "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", - "@mui/material": "6.4.11", - "@mui/x-charts": "^7.5.1", - "@mui/x-date-pickers": "7.29.0", + "@mui/lab": "7.0.0", + "@mui/material": "7.3.7", + "@mui/x-charts": "7.29.1", + "@mui/x-date-pickers": "7.29.4", "@reduxjs/toolkit": "2.7.0", "axios": "^1.7.4", "dayjs": "1.11.13", @@ -25,11 +25,12 @@ "i18next": "25.4.2", "joi": "17.13.3", "lucide-react": "^0.562.0", - "mui-color-input": "^6.0.0", + "mui-color-input": "^7.0.0", + "pretty-bytes": "^7.1.0", "pretty-ms": "^9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", - "react-hook-form": "7.63.0", + "react-hook-form": "^7.63.0", "react-i18next": "^15.4.0", "react-icons": "5.5.0", "react-redux": "9.2.0", @@ -40,7 +41,7 @@ "redux-persist": "6.0.0", "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", - "zod": "4.1.11" + "zod": "^4.1.11" }, "devDependencies": { "@types/node": "24.5.2", @@ -51,31 +52,17 @@ "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.6", + "lint-staged": "^16.2.7", "prettier": "^3.3.3", "typescript": "5.9.3", "vite": "6.3.6" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -84,31 +71,27 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "version": "7.29.0", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "version": "7.29.0", "license": "MIT", "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -125,20 +108,16 @@ }, "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.29.0", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -146,12 +125,10 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -161,28 +138,31 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "version": "7.28.6", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -192,9 +172,7 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", "dev": true, "license": "MIT", "engines": { @@ -203,17 +181,13 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -221,33 +195,27 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.28.6", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.29.0", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -258,8 +226,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { @@ -274,8 +240,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -289,63 +253,53 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "version": "7.28.6", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.29.0", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.29.0", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@ctrl/tinycolor": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", - "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==", + "version": "4.2.0", "license": "MIT", "engines": { "node": ">=14" @@ -353,8 +307,6 @@ }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.16.7", @@ -372,8 +324,6 @@ }, "node_modules/@emotion/cache": { "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", @@ -385,14 +335,10 @@ }, "node_modules/@emotion/hash": { "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "version": "1.4.0", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" @@ -400,14 +346,10 @@ }, "node_modules/@emotion/memoize": { "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", "license": "MIT" }, "node_modules/@emotion/react": { "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", "peer": true, "dependencies": { @@ -431,8 +373,6 @@ }, "node_modules/@emotion/serialize": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", @@ -444,14 +384,10 @@ }, "node_modules/@emotion/sheet": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", - "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "version": "11.14.1", "license": "MIT", "peer": true, "dependencies": { @@ -474,14 +410,10 @@ }, "node_modules/@emotion/unitless": { "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", "license": "MIT", "peerDependencies": { "react": ">=16.8.0" @@ -489,20 +421,16 @@ }, "node_modules/@emotion/utils": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -516,9 +444,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -532,9 +460,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -548,9 +476,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -564,9 +492,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -580,9 +508,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -596,9 +524,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -612,9 +540,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -628,9 +556,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -644,9 +572,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -660,9 +588,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -676,9 +604,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -692,9 +620,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -708,9 +636,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -724,9 +652,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -740,9 +668,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -756,9 +684,7 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.12", "cpu": [ "x64" ], @@ -772,9 +698,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -788,9 +714,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -804,9 +730,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -820,9 +746,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -835,10 +761,26 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -852,9 +794,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -868,9 +810,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -884,9 +826,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -900,9 +842,7 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", "dev": true, "license": "MIT", "dependencies": { @@ -919,9 +859,7 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", "dev": true, "license": "MIT", "engines": { @@ -930,8 +868,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -952,80 +888,20 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", - "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", - "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.0", - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "license": "MIT" - }, "node_modules/@hapi/hoek": { "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" @@ -1033,8 +909,6 @@ }, "node_modules/@hello-pangea/dnd": { "version": "18.0.1", - "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", - "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.26.7", @@ -1050,8 +924,6 @@ }, "node_modules/@hookform/resolvers": { "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", - "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" @@ -1062,9 +934,6 @@ }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1078,8 +947,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1092,97 +959,46 @@ }, "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mui/base": { - "version": "5.0.0-beta.42", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.42.tgz", - "integrity": "sha512-fWRiUJVCHCPF+mxd5drn08bY2qRw3jj5f1SSQdUXmaJ/yKpk23ys8MgLO2KGVTRtbks/+ctRfgffGPbXifj0Ug==", - "deprecated": "This package has been replaced by @base-ui-components/react", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.4", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.14", - "@mui/utils": "^6.0.0-alpha.1", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.11.tgz", - "integrity": "sha512-CzAQs9CTzlwbsF9ZYB4o4lLwBv1/qNE264NjuYao+ctAXsmlPtYa8RtER4UsUXSMxNN9Qi+aQdYcKl2sUpnmAw==", + "version": "7.3.7", "license": "MIT", "funding": { "type": "opencollective", @@ -1190,120 +1006,14 @@ } }, "node_modules/@mui/lab": { - "version": "6.0.0-dev.240424162023-9968b4889d", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz", - "integrity": "sha512-iKFAz7/EeWI4PaFsP4jK2FcYJmUYDBkn3XZwpQSAl5806yYq5J2U2mPQLuZBdhrH50gT2O98p95i3vwL4YBwAg==", + "version": "7.0.0", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.24.4", - "@mui/base": "5.0.0-beta.42", - "@mui/system": "^6.0.0-dev.240424162023-9968b4889d", - "@mui/types": "^7.2.14", - "@mui/utils": "^6.0.0-alpha.3", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material": "^6.0.0-dev.240424162023-9968b4889d", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/private-theming": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz", - "integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.9", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/styled-engine": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.11.tgz", - "integrity": "sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/system": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.11.tgz", - "integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.4.11", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", + "@babel/runtime": "^7.28.4", + "@mui/system": "^7.3.3", + "@mui/types": "^7.4.7", + "@mui/utils": "^7.3.3", "clsx": "^2.1.1", - "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { @@ -1316,66 +1026,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz", - "integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.4.11", - "@mui/system": "^6.4.11", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^19.0.0", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.4.11", + "@mui/material": "^7.3.3", + "@mui/material-pigment-css": "^7.3.3", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1395,81 +1047,23 @@ } } }, - "node_modules/@mui/material/node_modules/@mui/private-theming": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz", - "integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==", + "node_modules/@mui/material": { + "version": "7.3.7", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.9", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/styled-engine": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.11.tgz", - "integrity": "sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/system": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.11.tgz", - "integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.4.11", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", + "@babel/runtime": "^7.28.4", + "@mui/core-downloads-tracker": "^7.3.7", + "@mui/system": "^7.3.7", + "@mui/types": "^7.4.10", + "@mui/utils": "^7.3.7", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.3", + "react-transition-group": "^4.4.5" }, "engines": { "node": ">=14.0.0" @@ -1481,8 +1075,10 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.7", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -1491,33 +1087,20 @@ "@emotion/styled": { "optional": true }, - "@types/react": { + "@mui/material-pigment-css": { "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { + }, "@types/react": { "optional": true } } }, "node_modules/@mui/private-theming": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz", - "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==", + "version": "7.3.7", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/utils": "^7.1.0", + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.7", "prop-types": "^15.8.1" }, "engines": { @@ -1537,47 +1120,15 @@ } } }, - "node_modules/@mui/private-theming/node_modules/@mui/utils": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/types": "^7.4.2", - "@types/prop-types": "^15.7.14", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/styled-engine": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz", - "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==", + "version": "7.3.7", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.1", - "@emotion/cache": "^11.13.5", + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "engines": { @@ -1602,19 +1153,17 @@ } }, "node_modules/@mui/system": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz", - "integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==", + "version": "7.3.7", "license": "MIT", "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/private-theming": "^7.1.0", - "@mui/styled-engine": "^7.1.0", - "@mui/types": "^7.4.2", - "@mui/utils": "^7.1.0", + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.7", + "@mui/styled-engine": "^7.3.7", + "@mui/types": "^7.4.10", + "@mui/utils": "^7.3.7", "clsx": "^2.1.1", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "engines": { @@ -1642,43 +1191,11 @@ } } }, - "node_modules/@mui/system/node_modules/@mui/utils": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/types": "^7.4.2", - "@types/prop-types": "^15.7.14", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/types": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz", - "integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==", + "version": "7.4.10", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.1" + "@babel/runtime": "^7.28.4" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1690,17 +1207,15 @@ } }, "node_modules/@mui/utils": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz", - "integrity": "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==", + "version": "7.3.7", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/types": "~7.2.24", - "@types/prop-types": "^15.7.14", + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.10", + "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.0.0" + "react-is": "^19.2.3" }, "engines": { "node": ">=14.0.0" @@ -1719,24 +1234,8 @@ } } }, - "node_modules/@mui/utils/node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/x-charts": { "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.29.1.tgz", - "integrity": "sha512-5s9PX51HWhpMa+DCDa4RgjtODSaMe+PlTZUqoGIil2vaW/+4ouDLREXvyuVvIF93KfZwrPKAL2SJKSQS4YYB2w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1770,8 +1269,6 @@ }, "node_modules/@mui/x-charts-vendor": { "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz", - "integrity": "sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg==", "license": "MIT AND ISC", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1792,9 +1289,7 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.29.0.tgz", - "integrity": "sha512-6jpkbXB1/9/VLvlJ5wLs4M54fi9WeCpJSJ3I76MbZBKtZUDMFsuc3V5Mm4bsTd08xF6py1o+Qx2HapsE2gFhrg==", + "version": "7.29.4", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1859,8 +1354,6 @@ }, "node_modules/@mui/x-internals": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.29.0.tgz", - "integrity": "sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1879,8 +1372,6 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -1893,8 +1384,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -1903,8 +1392,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1917,8 +1404,6 @@ }, "node_modules/@popperjs/core": { "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", "funding": { "type": "opencollective", @@ -1927,8 +1412,6 @@ }, "node_modules/@react-spring/animated": { "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", - "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", "license": "MIT", "dependencies": { "@react-spring/shared": "~9.7.5", @@ -1940,8 +1423,6 @@ }, "node_modules/@react-spring/core": { "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", - "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", "license": "MIT", "dependencies": { "@react-spring/animated": "~9.7.5", @@ -1958,14 +1439,10 @@ }, "node_modules/@react-spring/rafz": { "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", - "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", "license": "MIT" }, "node_modules/@react-spring/shared": { "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", - "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", "license": "MIT", "dependencies": { "@react-spring/rafz": "~9.7.5", @@ -1977,14 +1454,10 @@ }, "node_modules/@react-spring/types": { "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", - "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", "license": "MIT" }, "node_modules/@react-spring/web": { "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", - "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", "license": "MIT", "dependencies": { "@react-spring/animated": "~9.7.5", @@ -1999,8 +1472,6 @@ }, "node_modules/@reduxjs/toolkit": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.7.0.tgz", - "integrity": "sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -2025,17 +1496,18 @@ }, "node_modules/@remix-run/router": { "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "version": "5.3.0", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -2055,9 +1527,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", - "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -2068,9 +1540,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", - "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -2081,9 +1553,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", - "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -2094,9 +1566,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", - "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -2107,9 +1579,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", - "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -2120,9 +1592,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", - "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -2133,9 +1605,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", - "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -2146,9 +1618,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", - "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -2159,9 +1631,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", - "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -2172,9 +1644,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", - "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -2184,10 +1656,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", - "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", "cpu": [ "loong64" ], @@ -2197,10 +1669,36 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", - "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -2211,9 +1709,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", - "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -2224,9 +1722,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", - "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -2237,9 +1735,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", - "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -2250,9 +1748,7 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", - "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "version": "4.57.1", "cpu": [ "x64" ], @@ -2263,9 +1759,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", - "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -2275,10 +1771,36 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", - "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -2289,9 +1811,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", - "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -2301,10 +1823,23 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", - "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -2316,8 +1851,6 @@ }, "node_modules/@sideway/address": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" @@ -2325,32 +1858,22 @@ }, "node_modules/@sideway/formula": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "license": "BSD-3-Clause" }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", "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/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", "license": "MIT", "engines": { "node": ">=14" @@ -2365,8 +1888,6 @@ }, "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", "license": "MIT", "engines": { "node": ">=14" @@ -2381,8 +1902,6 @@ }, "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", - "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", "license": "MIT", "engines": { "node": ">=14" @@ -2397,8 +1916,6 @@ }, "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", - "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", "license": "MIT", "engines": { "node": ">=14" @@ -2413,8 +1930,6 @@ }, "node_modules/@svgr/babel-plugin-svg-dynamic-title": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", - "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", "license": "MIT", "engines": { "node": ">=14" @@ -2429,8 +1944,6 @@ }, "node_modules/@svgr/babel-plugin-svg-em-dimensions": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", - "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", "license": "MIT", "engines": { "node": ">=14" @@ -2445,8 +1958,6 @@ }, "node_modules/@svgr/babel-plugin-transform-react-native-svg": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", - "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", "license": "MIT", "engines": { "node": ">=14" @@ -2461,8 +1972,6 @@ }, "node_modules/@svgr/babel-plugin-transform-svg-component": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", - "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", "license": "MIT", "engines": { "node": ">=12" @@ -2477,8 +1986,6 @@ }, "node_modules/@svgr/babel-preset": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", - "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", "license": "MIT", "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", @@ -2503,8 +2010,6 @@ }, "node_modules/@svgr/core": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", - "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", "dependencies": { "@babel/core": "^7.21.3", @@ -2523,8 +2028,6 @@ }, "node_modules/@svgr/core/node_modules/cosmiconfig": { "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "license": "MIT", "dependencies": { "import-fresh": "^3.3.0", @@ -2549,8 +2052,6 @@ }, "node_modules/@svgr/hast-util-to-babel-ast": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", - "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", "license": "MIT", "dependencies": { "@babel/types": "^7.21.3", @@ -2566,8 +2067,6 @@ }, "node_modules/@svgr/plugin-jsx": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", - "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", "license": "MIT", "dependencies": { "@babel/core": "^7.21.3", @@ -2588,8 +2087,6 @@ }, "node_modules/@types/babel__core": { "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", "dependencies": { @@ -2602,8 +2099,6 @@ }, "node_modules/@types/babel__generator": { "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", "dependencies": { @@ -2612,8 +2107,6 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", "dependencies": { @@ -2622,43 +2115,31 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "version": "3.2.2", "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-delaunay": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", "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": "*" @@ -2666,23 +2147,17 @@ }, "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==", + "version": "3.1.8", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -2690,26 +2165,18 @@ }, "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/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", "license": "MIT" }, "node_modules/@types/node": { "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "devOptional": true, "license": "MIT", "peer": true, @@ -2719,31 +2186,23 @@ }, "node_modules/@types/parse-json": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "version": "15.7.15", "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.22", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", - "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", + "version": "18.3.27", "license": "MIT", "peer": true, "dependencies": { "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2752,8 +2211,6 @@ }, "node_modules/@types/react-transition-group": { "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "license": "MIT", "peerDependencies": { "@types/react": "*" @@ -2761,27 +2218,22 @@ }, "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/@ungap/structured-clone": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "version": "4.7.0", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -2789,13 +2241,11 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", "dev": true, "license": "MIT", "peer": true, @@ -2808,8 +2258,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2818,8 +2266,6 @@ }, "node_modules/ajv": { "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -2833,10 +2279,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -2845,8 +2305,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -2861,14 +2319,10 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { @@ -2883,18 +2337,18 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2905,8 +2359,6 @@ }, "node_modules/array.prototype.findlast": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2926,8 +2378,6 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { @@ -2945,8 +2395,6 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -2964,8 +2412,6 @@ }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", "dependencies": { @@ -2981,8 +2427,6 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3003,8 +2447,6 @@ }, "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { @@ -3013,14 +2455,10 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3034,20 +2472,18 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "node_modules/babel-plugin-macros": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", @@ -3061,24 +2497,25 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/base64-arraybuffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "license": "MIT", "engines": { "node": ">= 0.6.0" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3086,10 +2523,21 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.28.1", "funding": [ { "type": "opencollective", @@ -3107,10 +2555,11 @@ "license": "MIT", "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3121,8 +2570,6 @@ }, "node_modules/call-bind": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { @@ -3140,8 +2587,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3153,8 +2598,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -3170,8 +2613,6 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "license": "MIT", "engines": { "node": ">=6" @@ -3179,8 +2620,6 @@ }, "node_modules/camelcase": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "license": "MIT", "engines": { "node": ">=10" @@ -3190,9 +2629,7 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001766", "funding": [ { "type": "opencollective", @@ -3209,10 +2646,56 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -3220,8 +2703,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3233,15 +2714,18 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -3250,23 +2734,27 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, "node_modules/cosmiconfig": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", @@ -3290,8 +2778,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -3305,8 +2791,6 @@ }, "node_modules/css-box-model": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", "license": "MIT", "dependencies": { "tiny-invariant": "^1.0.6" @@ -3314,23 +2798,17 @@ }, "node_modules/css-line-break": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", "dependencies": { "utrie": "^1.0.2" } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", "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" @@ -3341,8 +2819,6 @@ }, "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" @@ -3350,8 +2826,6 @@ }, "node_modules/d3-delaunay": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "license": "ISC", "dependencies": { "delaunator": "5" @@ -3362,17 +2836,13 @@ }, "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==", + "version": "3.1.2", "license": "ISC", "engines": { "node": ">=12" @@ -3380,8 +2850,6 @@ }, "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" @@ -3392,8 +2860,6 @@ }, "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" @@ -3401,8 +2867,6 @@ }, "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", @@ -3417,8 +2881,6 @@ }, "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" @@ -3429,8 +2891,6 @@ }, "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" @@ -3441,8 +2901,6 @@ }, "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" @@ -3453,8 +2911,6 @@ }, "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" @@ -3462,8 +2918,6 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3480,8 +2934,6 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3498,8 +2950,6 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3516,15 +2966,11 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "license": "MIT", "peer": true }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3540,21 +2986,15 @@ }, "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/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { @@ -3571,8 +3011,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -3589,8 +3027,6 @@ }, "node_modules/delaunator": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -3598,8 +3034,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3607,8 +3041,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" @@ -3616,8 +3048,6 @@ }, "node_modules/doctrine": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3629,8 +3059,6 @@ }, "node_modules/dom-helpers": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", @@ -3639,8 +3067,6 @@ }, "node_modules/dot-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -3649,8 +3075,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3662,15 +3086,18 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.283", "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -3679,19 +3106,28 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-abstract": { - "version": "1.23.10", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", - "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", + "version": "1.24.1", "dev": true, "license": "MIT", "dependencies": { @@ -3722,7 +3158,9 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", @@ -3737,6 +3175,7 @@ "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -3756,8 +3195,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -3765,35 +3202,31 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -3802,8 +3235,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3814,8 +3245,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3829,8 +3258,6 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { @@ -3842,8 +3269,6 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { @@ -3859,9 +3284,7 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.12", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3871,37 +3294,36 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -3909,8 +3331,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", "engines": { "node": ">=10" @@ -3921,9 +3341,6 @@ }, "node_modules/eslint": { "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "peer": true, @@ -3979,8 +3396,6 @@ }, "node_modules/eslint-plugin-react": { "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -4012,8 +3427,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -4024,9 +3437,7 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "version": "0.4.26", "dev": true, "license": "MIT", "peerDependencies": { @@ -4035,8 +3446,6 @@ }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4048,8 +3457,6 @@ }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", "dependencies": { @@ -4066,8 +3473,6 @@ }, "node_modules/eslint-scope": { "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4083,8 +3488,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4094,43 +3497,8 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4146,9 +3514,7 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4160,8 +3526,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4173,8 +3537,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4183,14 +3545,10 @@ }, "node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4199,21 +3557,15 @@ }, "node_modules/eventemitter3": { "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "version": "5.4.0", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -4221,22 +3573,16 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", "dev": true, "license": "ISC", "dependencies": { @@ -4244,10 +3590,11 @@ } }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -4259,8 +3606,6 @@ }, "node_modules/file-entry-cache": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "license": "MIT", "dependencies": { @@ -4270,16 +3615,25 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-root": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -4295,14 +3649,10 @@ }, "node_modules/flag-icons": { "version": "7.3.2", - "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.3.2.tgz", - "integrity": "sha512-QkaZ6Zvai8LIjx+UNAHUJ5Dhz9OLZpBDwCRWxF6YErxIcR16jTkIFm3bFu54EkvKQy4+wicW+Gm7/0631wVQyQ==", "license": "MIT" }, "node_modules/flat-cache": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "license": "MIT", "dependencies": { @@ -4316,15 +3666,11 @@ }, "node_modules/flatted": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", "funding": [ { "type": "individual", @@ -4343,8 +3689,6 @@ }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -4358,9 +3702,7 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4375,8 +3717,6 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, @@ -4396,8 +3736,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4405,8 +3743,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4426,27 +3762,42 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4469,8 +3820,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4482,8 +3831,6 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { @@ -4500,9 +3847,6 @@ }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -4522,8 +3866,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -4534,18 +3876,21 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "13.24.0", + "dev": true, "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4561,8 +3906,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4573,15 +3916,11 @@ }, "node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, "node_modules/has-bigints": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", "engines": { @@ -4593,8 +3932,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -4603,8 +3940,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { @@ -4616,8 +3951,6 @@ }, "node_modules/has-proto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4632,8 +3965,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4644,8 +3975,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4659,8 +3988,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4671,8 +3998,6 @@ }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" @@ -4680,14 +4005,10 @@ }, "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/html-parse-stringify": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", "license": "MIT", "dependencies": { "void-elements": "3.1.0" @@ -4695,8 +4016,6 @@ }, "node_modules/html2canvas": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "license": "MIT", "dependencies": { "css-line-break": "^2.1.0", @@ -4708,8 +4027,6 @@ }, "node_modules/human-interval": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz", - "integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==", "license": "MIT", "dependencies": { "numbered": "^1.1.0" @@ -4717,8 +4034,6 @@ }, "node_modules/i18next": { "version": "25.4.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz", - "integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==", "funding": [ { "type": "individual", @@ -4749,8 +4064,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -4758,9 +4071,7 @@ } }, "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "version": "10.2.0", "license": "MIT", "funding": { "type": "opencollective", @@ -4769,8 +4080,6 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -4785,8 +4094,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -4795,9 +4102,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -4807,15 +4111,11 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -4829,8 +4129,6 @@ }, "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" @@ -4838,8 +4136,6 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { @@ -4856,14 +4152,10 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4882,8 +4174,6 @@ }, "node_modules/is-bigint": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4898,8 +4188,6 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { @@ -4915,8 +4203,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -4928,8 +4214,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -4943,8 +4227,6 @@ }, "node_modules/is-data-view": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { @@ -4961,8 +4243,6 @@ }, "node_modules/is-date-object": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { @@ -4978,8 +4258,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -4988,8 +4266,6 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { @@ -5002,15 +4278,30 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -5023,8 +4314,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -5036,8 +4325,6 @@ }, "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { @@ -5047,10 +4334,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -5066,8 +4372,6 @@ }, "node_modules/is-path-inside": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "license": "MIT", "engines": { @@ -5076,8 +4380,6 @@ }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { @@ -5095,8 +4397,6 @@ }, "node_modules/is-set": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { @@ -5108,8 +4408,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { @@ -5124,8 +4422,6 @@ }, "node_modules/is-string": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { @@ -5141,8 +4437,6 @@ }, "node_modules/is-symbol": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5159,8 +4453,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5175,8 +4467,6 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", "engines": { @@ -5188,8 +4478,6 @@ }, "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -5204,8 +4492,6 @@ }, "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5221,22 +4507,16 @@ }, "node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { @@ -5253,8 +4533,6 @@ }, "node_modules/joi": { "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.3.0", @@ -5266,14 +4544,10 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5284,8 +4558,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -5296,35 +4568,25 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -5335,8 +4597,6 @@ }, "node_modules/jsx-ast-utils": { "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5351,8 +4611,6 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -5361,8 +4619,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5375,14 +4631,60 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.2", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -5396,22 +4698,65 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -5422,8 +4767,6 @@ }, "node_modules/lower-case": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "license": "MIT", "dependencies": { "tslib": "^2.0.3" @@ -5431,8 +4774,6 @@ }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -5440,8 +4781,6 @@ }, "node_modules/lucide-react": { "version": "0.562.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", - "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -5449,17 +4788,40 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -5467,8 +4829,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5477,10 +4837,21 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -5492,14 +4863,12 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/mui-color-input": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-6.0.0.tgz", - "integrity": "sha512-F9n/RqPLq7fnPWeF53/qssbIHvart7clpOfE+f+dsM+3ddfawwg98+PEhzv3cdAYlJLE9zfVViVTsp/ckUUd3g==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-7.0.0.tgz", + "integrity": "sha512-b9pHCCoRjD9CDTaw2b5S+UbBhJjAsB1NzdEqjF/9NzRFD0lD8zrlMxMwvI/NqsHkNxj2PteIFh/mzo2gBqnG9g==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.1.0" @@ -5507,7 +4876,7 @@ "peerDependencies": { "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", - "@mui/material": "^6.0.0", + "@mui/material": "^7.0.0", "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" @@ -5518,10 +4887,21 @@ } } }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -5538,15 +4918,11 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "license": "MIT", "dependencies": { "lower-case": "^2.0.2", @@ -5554,21 +4930,15 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", "license": "MIT" }, "node_modules/numbered": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz", - "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==", "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5576,8 +4946,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -5589,8 +4957,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "engines": { @@ -5599,8 +4965,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { @@ -5620,8 +4984,6 @@ }, "node_modules/object.entries": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -5636,8 +4998,6 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5655,8 +5015,6 @@ }, "node_modules/object.values": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -5674,18 +5032,30 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -5702,8 +5072,6 @@ }, "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { @@ -5720,8 +5088,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5736,8 +5102,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -5752,8 +5116,6 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -5764,8 +5126,6 @@ }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -5782,8 +5142,6 @@ }, "node_modules/parse-ms": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", "license": "MIT", "engines": { "node": ">=18" @@ -5794,8 +5152,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -5804,8 +5160,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -5814,8 +5168,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -5824,14 +5176,10 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "license": "MIT", "engines": { "node": ">=8" @@ -5839,14 +5187,10 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", "license": "MIT", "engines": { "node": ">=12" @@ -5855,10 +5199,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -5866,9 +5221,7 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", "funding": [ { "type": "opencollective", @@ -5885,7 +5238,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5895,8 +5248,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -5904,9 +5255,7 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.8.1", "dev": true, "license": "MIT", "bin": { @@ -5919,10 +5268,20 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-bytes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz", + "integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", "license": "MIT", "dependencies": { "parse-ms": "^4.0.0" @@ -5936,8 +5295,6 @@ }, "node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -5947,20 +5304,14 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/proxy-from-env": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -5969,8 +5320,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -5990,14 +5339,10 @@ }, "node_modules/raf-schd": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", "license": "MIT" }, "node_modules/react": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -6008,8 +5353,6 @@ }, "node_modules/react-dom": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "peer": true, "dependencies": { @@ -6021,9 +5364,7 @@ } }, "node_modules/react-hook-form": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", - "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", + "version": "7.71.1", "license": "MIT", "peer": true, "engines": { @@ -6038,16 +5379,14 @@ } }, "node_modules/react-i18next": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", - "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", + "version": "15.7.4", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.0", + "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "i18next": ">= 23.2.3", + "i18next": ">= 23.4.0", "react": ">= 16.8.0", "typescript": "^5" }, @@ -6065,23 +5404,17 @@ }, "node_modules/react-icons": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", "license": "MIT", "peerDependencies": { "react": "*" } }, "node_modules/react-is": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "version": "19.2.4", "license": "MIT" }, "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", @@ -6103,8 +5436,6 @@ }, "node_modules/react-refresh": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { @@ -6113,8 +5444,6 @@ }, "node_modules/react-router": { "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.2" @@ -6128,8 +5457,6 @@ }, "node_modules/react-router-dom": { "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.2", @@ -6145,8 +5472,6 @@ }, "node_modules/react-smooth": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", @@ -6160,8 +5485,6 @@ }, "node_modules/react-toastify": { "version": "10.0.6", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", - "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", "license": "MIT", "dependencies": { "clsx": "^2.1.0" @@ -6173,8 +5496,6 @@ }, "node_modules/react-transition-group": { "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", @@ -6189,8 +5510,6 @@ }, "node_modules/recharts": { "version": "2.15.2", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz", - "integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", @@ -6212,8 +5531,6 @@ }, "node_modules/recharts-scale": { "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", "license": "MIT", "dependencies": { "decimal.js-light": "^2.4.1" @@ -6221,21 +5538,15 @@ }, "node_modules/recharts/node_modules/react-is": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "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", "peer": true }, "node_modules/redux-persist": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", - "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", "license": "MIT", "peerDependencies": { "redux": ">4.0.0" @@ -6243,8 +5554,6 @@ }, "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" @@ -6252,8 +5561,6 @@ }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { @@ -6275,8 +5582,6 @@ }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { @@ -6296,17 +5601,13 @@ }, "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/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -6322,17 +5623,30 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -6340,11 +5654,15 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -6359,18 +5677,14 @@ }, "node_modules/robust-predicates": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", - "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", + "version": "4.57.1", "license": "MIT", "peer": true, "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -6380,33 +5694,36 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.0", - "@rollup/rollup-android-arm64": "4.41.0", - "@rollup/rollup-darwin-arm64": "4.41.0", - "@rollup/rollup-darwin-x64": "4.41.0", - "@rollup/rollup-freebsd-arm64": "4.41.0", - "@rollup/rollup-freebsd-x64": "4.41.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", - "@rollup/rollup-linux-arm-musleabihf": "4.41.0", - "@rollup/rollup-linux-arm64-gnu": "4.41.0", - "@rollup/rollup-linux-arm64-musl": "4.41.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-musl": "4.41.0", - "@rollup/rollup-linux-s390x-gnu": "4.41.0", - "@rollup/rollup-linux-x64-gnu": "4.41.0", - "@rollup/rollup-linux-x64-musl": "4.41.0", - "@rollup/rollup-win32-arm64-msvc": "4.41.0", - "@rollup/rollup-win32-ia32-msvc": "4.41.0", - "@rollup/rollup-win32-x64-msvc": "4.41.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -6429,8 +5746,6 @@ }, "node_modules/safe-array-concat": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6449,8 +5764,6 @@ }, "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { @@ -6466,8 +5779,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { @@ -6484,8 +5795,6 @@ }, "node_modules/scheduler": { "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -6493,8 +5802,6 @@ }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6502,8 +5809,6 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -6520,8 +5825,6 @@ }, "node_modules/set-function-name": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6536,8 +5839,6 @@ }, "node_modules/set-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -6551,8 +5852,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -6564,8 +5863,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -6574,8 +5871,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { @@ -6594,8 +5889,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", "dependencies": { @@ -6611,8 +5904,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -6630,8 +5921,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -6648,10 +5937,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/snake-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", "license": "MIT", "dependencies": { "dot-case": "^3.0.4", @@ -6660,8 +5990,6 @@ }, "node_modules/source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6669,17 +5997,81 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", "dependencies": { @@ -6706,8 +6098,6 @@ }, "node_modules/string.prototype.repeat": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6717,8 +6107,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { @@ -6739,8 +6127,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6758,8 +6144,6 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { @@ -6776,8 +6160,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -6789,8 +6171,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -6802,14 +6182,10 @@ }, "node_modules/stylis": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -6821,8 +6197,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6833,18 +6207,14 @@ }, "node_modules/svg-parser": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "license": "MIT" }, "node_modules/swr": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", - "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "version": "2.3.8", "license": "MIT", "dependencies": { "dequal": "^2.0.3", - "use-sync-external-store": "^1.4.0" + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -6852,8 +6222,6 @@ }, "node_modules/text-segmentation": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "license": "MIT", "dependencies": { "utrie": "^1.0.2" @@ -6861,25 +6229,19 @@ }, "node_modules/text-table": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "license": "MIT" }, "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/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -6888,16 +6250,25 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -6909,8 +6280,6 @@ }, "node_modules/type-fest": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -6922,8 +6291,6 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { @@ -6937,8 +6304,6 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { @@ -6957,8 +6322,6 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6979,8 +6342,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -7000,8 +6361,6 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "peer": true, @@ -7015,8 +6374,6 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { @@ -7034,15 +6391,11 @@ }, "node_modules/undici-types": { "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", "funding": [ { "type": "opencollective", @@ -7071,8 +6424,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7080,9 +6431,7 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -7090,8 +6439,6 @@ }, "node_modules/utrie": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "license": "MIT", "dependencies": { "base64-arraybuffer": "^1.0.2" @@ -7099,8 +6446,6 @@ }, "node_modules/victory-vendor": { "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -7121,8 +6466,6 @@ }, "node_modules/vite": { "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", "peer": true, "dependencies": { @@ -7195,12 +6538,10 @@ } }, "node_modules/vite-plugin-svgr": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", - "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", + "version": "4.5.0", "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.3", + "@rollup/pluginutils": "^5.2.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, @@ -7210,8 +6551,6 @@ }, "node_modules/void-elements": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7219,8 +6558,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -7235,8 +6572,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { @@ -7255,8 +6590,6 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7283,8 +6616,6 @@ }, "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", "dependencies": { @@ -7301,9 +6632,7 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", "dev": true, "license": "MIT", "dependencies": { @@ -7324,31 +6653,117 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "devOptional": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -7359,9 +6774,7 @@ } }, "node_modules/zod": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", - "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "version": "4.3.6", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/client/package.json b/client/package.json index a5c17e343..f0b34dbbc 100644 --- a/client/package.json +++ b/client/package.json @@ -12,15 +12,18 @@ "format": "prettier --write .", "format-check": "prettier --check ." }, + "lint-staged": { + "**/*": "npm run format" + }, "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^18.0.0", "@hookform/resolvers": "5.2.2", - "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", - "@mui/material": "6.4.11", - "@mui/x-charts": "^7.5.1", - "@mui/x-date-pickers": "7.29.0", + "@mui/lab": "7.0.0", + "@mui/material": "7.3.7", + "@mui/x-charts": "7.29.1", + "@mui/x-date-pickers": "7.29.4", "@reduxjs/toolkit": "2.7.0", "axios": "^1.7.4", "dayjs": "1.11.13", @@ -30,11 +33,12 @@ "i18next": "25.4.2", "joi": "17.13.3", "lucide-react": "^0.562.0", - "mui-color-input": "^6.0.0", + "mui-color-input": "^7.0.0", + "pretty-bytes": "^7.1.0", "pretty-ms": "^9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", - "react-hook-form": "7.63.0", + "react-hook-form": "^7.63.0", "react-i18next": "^15.4.0", "react-icons": "5.5.0", "react-redux": "9.2.0", @@ -45,7 +49,7 @@ "redux-persist": "6.0.0", "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", - "zod": "4.1.11" + "zod": "^4.1.11" }, "devDependencies": { "@types/node": "24.5.2", @@ -56,6 +60,7 @@ "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.6", + "lint-staged": "^16.2.7", "prettier": "^3.3.3", "typescript": "5.9.3", "vite": "6.3.6" diff --git a/client/src/App.jsx b/client/src/App.tsx similarity index 50% rename from client/src/App.jsx rename to client/src/App.tsx index da83f9061..127426c85 100644 --- a/client/src/App.jsx +++ b/client/src/App.tsx @@ -1,27 +1,16 @@ -import { useEffect } from "react"; +import { type CSSProperties } from "react"; import { useSelector } from "react-redux"; import "react-toastify/dist/ReactToastify.css"; import { ToastContainer } from "react-toastify"; import { ThemeProvider } from "@emotion/react"; -import lightTheme from "./Utils/Theme/lightTheme"; -import darkTheme from "./Utils/Theme/darkTheme"; import { CssBaseline, GlobalStyles } from "@mui/material"; -import { logger } from "./Utils/Logger"; // Import the logger -import { networkService } from "./main"; import { Routes } from "./Routes"; -import AppLayout from "@/Components/v1/Layouts/AppLayout"; +import AppLayout from "@/Components/layout/AppLayout"; +import type { RootState } from "@/Types/state"; +import { lightTheme, darkTheme } from "@/Utils/Theme/Theme"; function App() { - const mode = useSelector((state) => state.ui.mode); - - // Cleanup - useEffect(() => { - return () => { - logger.cleanup(); - networkService.cleanup(); - }; - }, []); - + const mode = useSelector((state: RootState) => state.ui.mode); const theme = mode === "light" ? lightTheme : darkTheme; return ( @@ -30,7 +19,7 @@ function App() { @@ -40,10 +29,12 @@ function App() { ); diff --git a/client/src/Components/WalletProvider/index.css b/client/src/Components/WalletProvider/index.css deleted file mode 100644 index f3da31efc..000000000 --- a/client/src/Components/WalletProvider/index.css +++ /dev/null @@ -1,53 +0,0 @@ -.wallet-adapter-dropdown { - width: 100%; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: var(--env-var-spacing-1); -} - -.wallet-adapter-button { - display: flex; - align-items: center; - justify-content: center; - width: auto; - height: var(--env-var-height-2) !important; - font-size: var(--env-var-font-size-medium-plus) !important; - font-weight: 500 !important; - margin: 0; - padding: calc( - (var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2 - ) - var(--env-var-spacing-1); - white-space: nowrap; -} - -.wallet-adapter-modal-title { - font-size: var(--font-size-h5) !important; -} - -/* Separator styling */ -.wallet-adapter-modal-divider { - background-color: var(--border-color) !important; - margin: var(--spacing-md) 0 !important; -} - -/* Responsive fixes */ -@media (max-width: 1200px) { - .wallet-adapter-button { - font-size: var(--env-var-font-size-medium) !important; - padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium) * 1.2) / 2) - var(--env-var-spacing-1-minus) !important; - } -} - -@media (max-width: 900px) { - .wallet-adapter-modal-wrapper { - flex-direction: column !important; - } - - .wallet-adapter-modal-divider { - margin: var(--spacing-sm) 0 !important; - } -} diff --git a/client/src/Components/WalletProvider/index.jsx b/client/src/Components/WalletProvider/index.jsx deleted file mode 100644 index 13cc83b07..000000000 --- a/client/src/Components/WalletProvider/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -// import { useMemo } from "react"; -// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react"; -// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; -// import { -// UnsafeBurnerWalletAdapter, -// PhantomWalletAdapter, -// } from "@solana/wallet-adapter-wallets"; - -// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; -// import { clusterApiUrl } from "@solana/web3.js"; -// import PropTypes from "prop-types"; -// import "./index.css"; - -// // Default styles that can be overridden by your app -// import "@solana/wallet-adapter-react-ui/styles.css"; - -// export const Wallet = ({ children }) => { -// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'. -// const network = WalletAdapterNetwork.Mainnet; - -// // You can also provide a custom RPC endpoint. -// const endpoint = useMemo(() => clusterApiUrl(network), [network]); - -// const wallets = useMemo( -// () => [new PhantomWalletAdapter()], -// // eslint-disable-next-line react-hooks/exhaustive-deps -// [network] -// ); - -// return ( -// -// -// {children} -// -// -// ); -// }; - -// Wallet.propTypes = { -// children: PropTypes.node, -// }; - -const Wallet = ({ children }) => { - return children; -}; - -export default Wallet; diff --git a/client/src/Components/v2/actions-menu/index.tsx b/client/src/Components/actions-menu/index.tsx similarity index 100% rename from client/src/Components/v2/actions-menu/index.tsx rename to client/src/Components/actions-menu/index.tsx diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx b/client/src/Components/common/charts/HeatmapResponseTime.tsx similarity index 97% rename from client/src/Components/v2/common/charts/HeatmapResponseTime.tsx rename to client/src/Components/common/charts/HeatmapResponseTime.tsx index 972291f57..8178ffb92 100644 --- a/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx +++ b/client/src/Components/common/charts/HeatmapResponseTime.tsx @@ -2,7 +2,7 @@ import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import type { CheckSnapshot } from "@/Types/Check"; import { getResponseColor } from "@/Utils/DataUtils"; -import { HeatmapResponseTimeTooltip } from "@/Components/v2/common/charts/HeatmapResponseTimeTooltip"; +import { HeatmapResponseTimeTooltip } from "@/Components/common/charts/HeatmapResponseTimeTooltip"; import type { SxProps } from "@mui/material/styles"; import type { ResponsiveStyleValue } from "@mui/system"; diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx b/client/src/Components/common/charts/HeatmapResponseTimeTooltip.tsx similarity index 92% rename from client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx rename to client/src/Components/common/charts/HeatmapResponseTimeTooltip.tsx index ec9ad4d26..c046e05b5 100644 --- a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx +++ b/client/src/Components/common/charts/HeatmapResponseTimeTooltip.tsx @@ -7,6 +7,7 @@ import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; import { useTranslation } from "react-i18next"; + type HeatmapCheck = | CheckSnapshot | { status: "placeholder"; responseTime: 0; createdAt: "" }; @@ -55,12 +56,9 @@ export const HeatmapResponseTimeTooltip = ({ {formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} - {check?.responseTime && ( - - {t("common.labels.responseTime")}: {check.responseTime.toFixed()} ms - - )} - + + {t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms + Status:{" "} diff --git a/client/src/Components/common/charts/HistogramResponseTime.tsx b/client/src/Components/common/charts/HistogramResponseTime.tsx new file mode 100644 index 000000000..1ef400daf --- /dev/null +++ b/client/src/Components/common/charts/HistogramResponseTime.tsx @@ -0,0 +1,155 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import type { ResponsiveStyleValue } from "@mui/system"; +import type { CheckSnapshot } from "@/Types/Check"; +import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; + +interface HistogramResponseTimeProps { + checks: CheckSnapshot[]; + height?: ResponsiveStyleValue; + gap?: ResponsiveStyleValue; + showStats?: boolean; + statsPosition?: "left" | "right"; +} + +interface ResponseTimeStats { + max: number | string; + avg: number | string; +} + +const DEFAULT_HEIGHT = 50; + +const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats => { + if (!Array.isArray(checks) || checks.length === 0) { + return { max: "-", avg: "-" }; + } + + const validChecks = checks.filter((check) => check.originalResponseTime != null); + if (validChecks.length === 0) { + return { max: "-", avg: "-" }; + } + + const responseTimes = validChecks.map((check) => check.originalResponseTime); + const max = Math.max(...responseTimes); + const avg = Math.round( + responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length + ); + + return { max, avg }; +}; + +export const HistogramResponseTime = ({ + checks, + height = DEFAULT_HEIGHT, + gap, + showStats = true, + statsPosition = "right", +}: HistogramResponseTimeProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const stats = useMemo(() => calculateResponseTimeStats(checks), [checks]); + + if (!Array.isArray(checks) || checks.length === 0) return null; + + const data = + checks.length !== 25 + ? [...checks, ...Array(25 - checks.length).fill({ status: "placeholder" })] + : checks; + + const chartHeight = typeof height === "number" ? `${height}px` : height; + const gridGap = gap ?? theme.spacing(0.5); + + const statsContent = showStats && + (typeof stats.max === "number" || typeof stats.avg === "number") && ( + + + {t("common.charts.histogram.avg", { value: stats.avg })} + + + {t("common.charts.histogram.max", { value: stats.max })} + + + ); + + return ( + + + + {data.map((check, index) => { + const isPlaceholder = (check as any).status === "placeholder"; + const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`; + const barColor = + check.status === true + ? theme.palette.success.light + : theme.palette.error.light; + const bar = ( + + + + ); + + return isPlaceholder ? ( + + {bar} + + ) : ( + + {bar} + + ); + })} + + + {statsContent} + + ); +}; diff --git a/client/src/Components/v2/common/controls/HeaderCreate.tsx b/client/src/Components/common/controls/HeaderCreate.tsx similarity index 100% rename from client/src/Components/v2/common/controls/HeaderCreate.tsx rename to client/src/Components/common/controls/HeaderCreate.tsx diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/common/controls/HeaderTimeRange.tsx similarity index 52% rename from client/src/Components/v2/common/controls/HeaderTimeRange.tsx rename to client/src/Components/common/controls/HeaderTimeRange.tsx index 57b61473a..d7c7102bc 100644 --- a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx +++ b/client/src/Components/common/controls/HeaderTimeRange.tsx @@ -1,7 +1,11 @@ import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs"; +import { ToggleButtonGroup, ToggleButton } from "@/Components/inputs"; import { useTheme } from "@mui/material/styles"; +import CircularProgress from "@mui/material/CircularProgress"; +import { useTranslation } from "react-i18next"; +import { useMediaQuery } from "@mui/material"; interface MonitorTimeFrameHeaderProps { isLoading?: boolean; @@ -17,7 +21,8 @@ export const HeaderTimeRange = ({ setDateRange, }: MonitorTimeFrameHeaderProps) => { const theme = useTheme(); - + const { t } = useTranslation(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down("md")); const handleChange = ( _event: React.MouseEvent, newValue: string | null @@ -32,34 +37,24 @@ export const HeaderTimeRange = ({ if (hasDateRange) { timeFramePicker = ( - - Recent + + {t("components.headerTimeRange.labels.recent")} - - Day + + {t("components.headerTimeRange.labels.day")} - - Week + + {t("components.headerTimeRange.labels.week")} - - Month + + {t("components.headerTimeRange.labels.month")} ); @@ -67,21 +62,19 @@ export const HeaderTimeRange = ({ return ( + + {isLoading && } + - Showing statistics for past{" "} - {dateRange === "recent" - ? "2 hours" - : dateRange === "day" - ? "24 hours" - : dateRange === "week" - ? "7 days" - : "30 days"} - . + {t(`components.headerTimeRange.description.${dateRange}`)} {timeFramePicker} diff --git a/client/src/Components/common/index.ts b/client/src/Components/common/index.ts new file mode 100644 index 000000000..4db9cac34 --- /dev/null +++ b/client/src/Components/common/index.ts @@ -0,0 +1,5 @@ +export * from "./charts/HistogramResponseTime"; +export * from "./charts/HeatmapResponseTime"; +export * from "./charts/HeatmapResponseTimeTooltip"; +export * from "./controls/HeaderCreate"; +export * from "./controls/HeaderTimeRange"; diff --git a/client/src/Components/design-elements/Avatar.tsx b/client/src/Components/design-elements/Avatar.tsx new file mode 100644 index 000000000..2dd1160ad --- /dev/null +++ b/client/src/Components/design-elements/Avatar.tsx @@ -0,0 +1,48 @@ +import { Avatar as MuiAvatar } from "@mui/material"; +import { useSelector } from "react-redux"; +import { useEffect, useState } from "react"; +import { useTheme } from "@mui/material"; +import type { RootState } from "@/Types/state"; + +interface AvatarProps { + src?: string; + small?: boolean; + sx?: object; + onClick?: (event: React.MouseEvent) => void; +} + +export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { + const { user } = useSelector((state: RootState) => state.auth); + const theme = useTheme(); + if (!user) return null; + + const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; + + const [image, setImage] = useState(); + useEffect(() => { + if (user.avatarImage) { + setImage(`data:image/png;base64,${user.avatarImage}`); + } + }, [user?.avatarImage]); + + return ( + + {user.firstName?.charAt(0)} + {user.lastName?.charAt(0) || ""} + + ); +}; diff --git a/client/src/Components/v2/design-elements/BaseBox.tsx b/client/src/Components/design-elements/BaseBox.tsx similarity index 100% rename from client/src/Components/v2/design-elements/BaseBox.tsx rename to client/src/Components/design-elements/BaseBox.tsx diff --git a/client/src/Components/v2/design-elements/BaseChart.tsx b/client/src/Components/design-elements/BaseChart.tsx similarity index 96% rename from client/src/Components/v2/design-elements/BaseChart.tsx rename to client/src/Components/design-elements/BaseChart.tsx index fd9251915..569652b20 100644 --- a/client/src/Components/v2/design-elements/BaseChart.tsx +++ b/client/src/Components/design-elements/BaseChart.tsx @@ -1,4 +1,4 @@ -import { BaseBox } from "../design-elements"; +import { BaseBox } from "."; import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/design-elements/BasePage.tsx similarity index 66% rename from client/src/Components/v2/design-elements/BasePage.tsx rename to client/src/Components/design-elements/BasePage.tsx index de20fc005..cf28da40f 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/design-elements/BasePage.tsx @@ -1,4 +1,6 @@ +import Logo from "@/assets/icons/checkmate-icon.svg?react"; import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; import Alert from "@mui/material/Alert"; import Link from "@mui/material/Link"; import { @@ -6,9 +8,10 @@ import { EmptyFallback, EmptyMonitorFallback, BaseFallback, -} from "@/Components/v2/design-elements/Fallback"; -import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb"; +} from "@/Components/design-elements/Fallback"; +import { Breadcrumb } from "@/Components/design-elements/Breadcrumb"; import CircularProgress from "@mui/material/CircularProgress"; +import { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls"; import type { StackProps } from "@mui/material/Stack"; import { useTheme } from "@mui/material/styles"; @@ -91,7 +94,7 @@ export const BasePage = ({ spacing={theme.spacing(10)} {...props} > - + {children} ); @@ -100,7 +103,7 @@ export const BasePage = ({ interface BasePageWithStatesProps extends StackProps { loading: boolean; error: any; - items: any[]; + totalCount: number; bullets: string[] | unknown; page: string; actionButtonText: string; @@ -111,7 +114,7 @@ interface BasePageWithStatesProps extends StackProps { export const BasePageWithStates = ({ loading, error, - items, + totalCount, page, bullets, actionButtonText, @@ -119,9 +122,9 @@ export const BasePageWithStates = ({ children, ...props }: BasePageWithStatesProps) => { - const showLoading = loading && (!items || items.length === 0); + const showLoading = loading && totalCount === 0; - if (isEmpty(items)) { + if (!loading && totalCount === 0) { return ( { - if (!items) return true; - if (Array.isArray(items) && items.length === 0) return true; - return false; -}; - export const MonitorBasePageWithStates = ({ loading, error, - items, + totalCount, page, actionLink, children, @@ -171,7 +168,7 @@ export const MonitorBasePageWithStates = ({ }: MonitorBasePageWithStatesProps) => { const { t } = useTranslation(); - const showLoading = loading && (!items || items.length === 0); + const showLoading = loading && totalCount === 0; if (priorityFallback) { return ( @@ -180,12 +177,12 @@ export const MonitorBasePageWithStates = ({ error={error} {...props} > - {priorityFallback} + {priorityFallback} ); } - if (!loading && isEmpty(items)) { + if (!loading && totalCount === 0) { return ( ); }; + +interface BaseAuthPageProps extends React.PropsWithChildren, StackProps { + title: string; + subtitle: string; +} + +export const BaseAuthPage = ({ + title, + subtitle, + children, + ...props +}: BaseAuthPageProps) => { + const theme = useTheme(); + return ( + + + + + + + + {title} + + {subtitle} + + + {children} + + + ); +}; diff --git a/client/src/Components/design-elements/Breadcrumb.tsx b/client/src/Components/design-elements/Breadcrumb.tsx new file mode 100644 index 000000000..ae3e768c2 --- /dev/null +++ b/client/src/Components/design-elements/Breadcrumb.tsx @@ -0,0 +1,140 @@ +import MuiBreadcrumbs from "@mui/material/Breadcrumbs"; +import Typography from "@mui/material/Typography"; +import { Link, useLocation } from "react-router-dom"; +import { useTheme } from "@mui/material/styles"; +import { ChevronRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import type { ReactNode } from "react"; + +const isId = (segment: string): boolean => { + return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment); +}; + +const actionSegments = ["create", "configure"]; + +const BreadcrumbWrapper = ({ children }: { children: ReactNode }) => { + const theme = useTheme(); + return ( + + } + sx={{ + fontSize: "14px", + marginBottom: theme.spacing(6), + "& .MuiBreadcrumbs-separator": { + color: theme.palette.text.secondary, + }, + }} + > + {children} + + ); +}; + +export const Breadcrumb = ({ + breadcrumbOverride, +}: { + breadcrumbOverride?: string[] | undefined; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const location = useLocation(); + + // If override is an empty array, hide entirely + if (breadcrumbOverride !== undefined && breadcrumbOverride.length === 0) { + return null; + } + + // If override has items, render them directly + if (breadcrumbOverride !== undefined && breadcrumbOverride.length > 0) { + return ( + + {breadcrumbOverride.map((item, index) => { + const isLast = index === breadcrumbOverride.length - 1; + return ( + + {item} + + ); + })} + + ); + } + + // Default behavior: use location pathname + const segments = location.pathname.split("/").filter((x) => x); + + if (segments.length === 0) { + return null; + } + + // Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create" + const basePage = segments[0] || t("common.breadcrumbs.home"); + + const secondSegment = segments[1]; + const isActionPage = secondSegment && actionSegments.includes(secondSegment); + const isDetailsPage = secondSegment && isId(secondSegment); + const hasSubPage = isActionPage || isDetailsPage; + + const getSubPageLabel = (): string => { + if (isActionPage) { + return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1); + } + return t("common.breadcrumbs.details"); + }; + + return ( + + {hasSubPage ? ( + + + {basePage.charAt(0).toUpperCase() + basePage.slice(1)} + + + ) : ( + + {basePage.charAt(0).toUpperCase() + basePage.slice(1)} + + )} + {hasSubPage && ( + + {getSubPageLabel()} + + )} + + ); +}; diff --git a/client/src/Components/v2/design-elements/BulletPointCheck.tsx b/client/src/Components/design-elements/BulletPointCheck.tsx similarity index 92% rename from client/src/Components/v2/design-elements/BulletPointCheck.tsx rename to client/src/Components/design-elements/BulletPointCheck.tsx index 2893ee4c2..4bd1ddf3c 100644 --- a/client/src/Components/v2/design-elements/BulletPointCheck.tsx +++ b/client/src/Components/design-elements/BulletPointCheck.tsx @@ -27,6 +27,7 @@ export const BulletPointCheck = ({ - - {title} - + {title} { + const theme = useTheme(); + const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE)); + + // Calculate the length of the stroke for the circle + const { circumference, totalSize, strokeLength } = useMemo( + () => ({ + circumference: 2 * Math.PI * radius, + totalSize: radius * 2 + strokeWidth * 2, + strokeLength: (progress / 100) * (2 * Math.PI * radius), + }), + [radius, strokeWidth, progress] + ); + + const [offset, setOffset] = useState(circumference); + useEffect(() => { + setOffset(circumference); + const timer = setTimeout(() => { + setOffset(circumference - strokeLength); + }, 100); + + return () => clearTimeout(timer); + }, [progress, circumference, strokeLength]); + + const fillColor = getInfraGaugeColor(progressWithinRange, theme); + + if (isLoading) { + return; + } + + return ( + + + + + + + + {`${progressWithinRange.toFixed(precision)}${unit}`} + + + ); +}; + +export const DetailGauge = ({ + title, + progress, + upperLabel, + upperValue, + lowerLabel, + lowerValue, +}: { + title: string; + progress: number; + upperLabel?: string; + upperValue?: string | number; + lowerLabel?: string; + lowerValue?: string | number; +}) => { + const theme = useTheme(); + return ( + + + + + + {upperLabel} + {upperValue} + + + {lowerLabel} + {lowerValue} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/design-elements/Icon.tsx similarity index 94% rename from client/src/Components/v2/design-elements/Icon.tsx rename to client/src/Components/design-elements/Icon.tsx index b32cd84b2..22f939750 100644 --- a/client/src/Components/v2/design-elements/Icon.tsx +++ b/client/src/Components/design-elements/Icon.tsx @@ -4,6 +4,7 @@ interface IconProps { icon: LucideIcon; size?: number; strokeWidth?: number; + stroke?: string; } const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => { diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/design-elements/MonitorStatus.tsx similarity index 80% rename from client/src/Components/v2/design-elements/MonitorStatus.tsx rename to client/src/Components/design-elements/MonitorStatus.tsx index 3d5e81845..04c62de47 100644 --- a/client/src/Components/v2/design-elements/MonitorStatus.tsx +++ b/client/src/Components/design-elements/MonitorStatus.tsx @@ -1,11 +1,11 @@ import type { Monitor } from "@/Types/Monitor"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import { PulseDot, Dot } from "@/Components/v2/design-elements"; +import { PulseDot, Dot } from "@/Components/design-elements"; import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils"; import { useTheme } from "@mui/material/styles"; import prettyMilliseconds from "pretty-ms"; -import { typographyLevels } from "@/Utils/Theme/v2Palette"; +import { typographyLevels } from "@/Utils/Theme/Palette"; import useMediaQuery from "@mui/material/useMediaQuery"; export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { const theme = useTheme(); @@ -19,11 +19,9 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { {monitor.name} @@ -40,7 +38,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { overflow={"hidden"} textOverflow={"ellipsis"} whiteSpace={"nowrap"} - maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"} > {formatUrl(monitor?.url)} diff --git a/client/src/Components/design-elements/OfflineBanner.tsx b/client/src/Components/design-elements/OfflineBanner.tsx new file mode 100644 index 000000000..062b1ca0d --- /dev/null +++ b/client/src/Components/design-elements/OfflineBanner.tsx @@ -0,0 +1,63 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { WifiOff } from "lucide-react"; +import { useState, useEffect } from "react"; + +interface OfflineBannerProps { + visible: boolean; +} + +export const OfflineBanner = ({ visible }: OfflineBannerProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [shouldRender, setShouldRender] = useState(visible); + const [isAnimating, setIsAnimating] = useState(false); + + useEffect(() => { + if (visible) { + setShouldRender(true); + requestAnimationFrame(() => setIsAnimating(true)); + } else { + setIsAnimating(false); + const timer = setTimeout(() => setShouldRender(false), 1000); + return () => clearTimeout(timer); + } + }, [visible]); + + if (!shouldRender) return null; + + return ( + + + + + {t("components.offlineBanner.serverUnreachable")} + + + + ); +}; diff --git a/client/src/Components/v2/design-elements/PulseDot.tsx b/client/src/Components/design-elements/PulseDot.tsx similarity index 73% rename from client/src/Components/v2/design-elements/PulseDot.tsx rename to client/src/Components/design-elements/PulseDot.tsx index ab792b628..1f5b3401e 100644 --- a/client/src/Components/v2/design-elements/PulseDot.tsx +++ b/client/src/Components/design-elements/PulseDot.tsx @@ -1,4 +1,17 @@ -import { Box, Stack, useTheme } from "@mui/material"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import { useTheme, keyframes } from "@mui/material"; + +const ripple = keyframes` + from { + opacity: 1; + transform: scale(0); + } + to { + opacity: 0; + transform: scale(2); + } +`; interface PulseDotProps { color: string; @@ -28,7 +41,7 @@ export const PulseDot = ({ color }: PulseDotProps) => { height: "100%", backgroundColor: "inherit", borderRadius: "50%", - animation: "ripple 1.8s ease-out infinite", + animation: `${ripple} 1.8s ease-out infinite`, }, "&::after": { content: `""`, diff --git a/client/src/Components/v2/design-elements/SkeletonCard.tsx b/client/src/Components/design-elements/SkeletonCard.tsx similarity index 100% rename from client/src/Components/v2/design-elements/SkeletonCard.tsx rename to client/src/Components/design-elements/SkeletonCard.tsx diff --git a/client/src/Components/design-elements/SplitBox.tsx b/client/src/Components/design-elements/SplitBox.tsx new file mode 100644 index 000000000..3a778dbcf --- /dev/null +++ b/client/src/Components/design-elements/SplitBox.tsx @@ -0,0 +1,78 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +export const SplitBox = ({ + left, + right, +}: { + left: React.ReactNode; + right: React.ReactNode; +}) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + return ( + + + {left} + + + {right} + + + ); +}; + +export const ConfigBox = ({ + title, + subtitle, + leftContent, + rightContent, +}: { + title: string; + subtitle: string; + leftContent?: React.ReactNode; + rightContent: React.ReactNode; +}) => { + const theme = useTheme(); + return ( + + + {title} + + {subtitle} + {leftContent} + + } + right={rightContent} + /> + ); +}; diff --git a/client/src/Components/v2/design-elements/StatBox.tsx b/client/src/Components/design-elements/StatBox.tsx similarity index 94% rename from client/src/Components/v2/design-elements/StatBox.tsx rename to client/src/Components/design-elements/StatBox.tsx index b78a003dc..01a8e9835 100644 --- a/client/src/Components/v2/design-elements/StatBox.tsx +++ b/client/src/Components/design-elements/StatBox.tsx @@ -4,8 +4,8 @@ import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import { lighten } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; -import type { PaletteKey } from "@/Utils/Theme/v2Theme"; -import { BaseBox, TooltipWithInfo } from "@/Components/v2/design-elements"; +import type { PaletteKey } from "@/Utils/Theme/Theme"; +import { BaseBox, TooltipWithInfo } from "@/Components/design-elements"; import type { SxProps } from "@mui/material"; type GradientBox = React.PropsWithChildren<{ diff --git a/client/src/Components/design-elements/StatusBox.tsx b/client/src/Components/design-elements/StatusBox.tsx new file mode 100644 index 000000000..96a9d9fbf --- /dev/null +++ b/client/src/Components/design-elements/StatusBox.tsx @@ -0,0 +1,186 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import { BaseBox } from "@/Components/design-elements"; +import { useTranslation } from "react-i18next"; +import type { SxProps } from "@mui/material"; + +import { useTheme } from "@mui/material/styles"; + +type StatusBoxProps = React.PropsWithChildren<{ + children: React.ReactNode; + sx?: SxProps; +}>; + +export const BGBox = ({ children, sx }: StatusBoxProps) => { + const theme = useTheme(); + return ( + + + {children} + + ); +}; + +const StatusBox = ({ + label, + n, + color, + sx, +}: { + label: string; + n: number; + color: string | undefined; + sx?: SxProps; +}) => { + const theme = useTheme(); + return ( + + + + {label} + + + {n} + + + + ); +}; + +export const UpStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; + +export const DownStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; + +export const PausedStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const MaintenanceStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const InitializingStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const BreachedStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; + +export const TotalChecksBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const DownChecksBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const UpChecksBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; diff --git a/client/src/Components/v2/design-elements/StatusLabel.tsx b/client/src/Components/design-elements/StatusLabel.tsx similarity index 73% rename from client/src/Components/v2/design-elements/StatusLabel.tsx rename to client/src/Components/design-elements/StatusLabel.tsx index c9991f0d4..d50058378 100644 --- a/client/src/Components/v2/design-elements/StatusLabel.tsx +++ b/client/src/Components/design-elements/StatusLabel.tsx @@ -1,5 +1,5 @@ import Box from "@mui/material/Box"; -import { BaseBox } from "@/Components/v2/design-elements"; +import { BaseBox } from "@/Components/design-elements"; import Typography from "@mui/material/Typography"; import type { MonitorStatus } from "@/Types/Monitor"; @@ -11,33 +11,26 @@ import { useTranslation } from "react-i18next"; export const ValueTypes = ["positive", "negative", "neutral"] as const; export type ValueType = (typeof ValueTypes)[number]; -export const StatusLabel = ({ - status, - isActive, - sx, -}: { - status: MonitorStatus; - isActive?: boolean; - sx?: SxProps; -}) => { +export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProps }) => { const { t } = useTranslation(); const theme = useTheme(); const palette = getStatusPalette(status); - const determineStatus = ( - isActive: boolean | undefined, - status: MonitorStatus - ): string => { - if (isActive === false) { + const determineStatus = (status: MonitorStatus): string => { + if (status === "up") { + return t("pages.common.monitors.status.up"); + } else if (status === "down") { + return t("pages.common.monitors.status.down"); + } else if (status === "breached") { + return t("pages.common.monitors.status.breached"); + } else if (status === "maintenance") { + return t("pages.common.monitors.status.maintenance"); + } else if (status === "paused") { return t("pages.common.monitors.status.paused"); + } else if (status === "initializing") { + return t("pages.common.monitors.status.initializing"); } - if (status === true) { - return t("pages.common.monitors.status.up"); - } - if (status === false) { - return t("pages.common.monitors.status.down"); - } return t("pages.common.monitors.status.initializing"); }; @@ -64,9 +57,7 @@ export const StatusLabel = ({ borderRadius="50%" marginRight="5px" /> - - {determineStatus(isActive, status)} - + {determineStatus(status)} ); }; @@ -98,7 +89,7 @@ export const ValueLabel = ({ value, text }: { value: ValueType; text: string }) borderRadius="50%" marginRight="5px" /> - {transformedText} + {transformedText} ); }; diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/design-elements/Table.tsx similarity index 95% rename from client/src/Components/v2/design-elements/Table.tsx rename to client/src/Components/design-elements/Table.tsx index 0516f1ddd..35cd4512f 100644 --- a/client/src/Components/v2/design-elements/Table.tsx +++ b/client/src/Components/design-elements/Table.tsx @@ -1,5 +1,5 @@ import Stack from "@mui/material/Stack"; -import Grid2 from "@mui/material/Grid2"; +import Grid2 from "@mui/material/Grid"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Paper from "@mui/material/Paper"; @@ -27,8 +27,9 @@ import TablePagination from "@mui/material/TablePagination"; import type { TablePaginationOwnProps } from "@mui/material/TablePagination"; import { useTranslation } from "react-i18next"; -import { useState, Fragment } from "react"; +import { useState, Fragment, useEffect } from "react"; import { useTheme } from "@mui/material/styles"; +import type { SxProps, Theme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; export type Header = { @@ -47,6 +48,7 @@ type DataTableProps = renderExpandedContent?: (row: T) => React.ReactNode; emptyViewText?: string; emptyViewPositive?: boolean; + getRowSx?: (row: T) => SxProps; }; export function DataTable< @@ -64,6 +66,7 @@ export function DataTable< renderExpandedContent, emptyViewText, emptyViewPositive, + getRowSx, }: DataTableProps) { const theme = useTheme(); const [expanded, setExpanded] = useState<(string | number) | null>(null); @@ -204,6 +207,7 @@ export function DataTable< { if (expandableRows) handleExpand(row); @@ -454,12 +458,26 @@ function HasMoreTablePaginationActions(props: HasMoreTablePaginationActionsProps interface PaginationProps extends TablePaginationOwnProps { component?: React.ElementType; hasMore?: boolean; + itemsOnPage?: number; } export const Pagination = ({ ...props }: PaginationProps) => { - const { hasMore, ...rest } = props; + const { hasMore, itemsOnPage, ...rest } = props; const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm")); const theme = useTheme(); + + useEffect(() => { + if ( + typeof itemsOnPage === "number" && + itemsOnPage === 0 && + rest.count > 0 && + rest.page > 0 && + rest.onPageChange + ) { + rest.onPageChange(null, rest.page - 1); + } + }, [itemsOnPage, rest.count, rest.page, rest.onPageChange]); + const labelDisplayedRows = ({ from, to, diff --git a/client/src/Components/design-elements/Tabs.tsx b/client/src/Components/design-elements/Tabs.tsx new file mode 100644 index 000000000..149c0bd93 --- /dev/null +++ b/client/src/Components/design-elements/Tabs.tsx @@ -0,0 +1,64 @@ +import MuiTabs from "@mui/material/Tabs"; +import type { TabsProps } from "@mui/material/Tabs"; +import { useTheme } from "@mui/material/styles"; +interface CustomTabsProps extends TabsProps {} + +export const Tabs = (props: CustomTabsProps) => { + const theme = useTheme(); + return ( + + {props.children} + + ); +}; + +import MuiTab from "@mui/material/Tab"; +import type { TabProps } from "@mui/material/Tab"; +interface CustomTabProps extends TabProps {} + +export const Tab = (props: CustomTabProps) => { + const theme = useTheme(); + return ( + + ); +}; diff --git a/client/src/Components/design-elements/TextLink.tsx b/client/src/Components/design-elements/TextLink.tsx new file mode 100644 index 000000000..2f79b1beb --- /dev/null +++ b/client/src/Components/design-elements/TextLink.tsx @@ -0,0 +1,43 @@ +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import type { StackProps } from "@mui/material/Stack"; +import Link from "@mui/material/Link"; +import { Link as RouterLink } from "react-router-dom"; + +import { useTheme } from "@mui/material/styles"; + +interface TextLinkProps extends StackProps { + text: string; + linkText: string; + href: string; + target?: "_self" | "_blank" | "_parent" | "_top"; +} + +export const TextLink = ({ + text, + linkText, + href, + target = "_self", + ...props +}: TextLinkProps) => { + const theme = useTheme(); + + return ( + + {text} + + {linkText} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/Tooltip.tsx b/client/src/Components/design-elements/Tooltip.tsx similarity index 100% rename from client/src/Components/v2/design-elements/Tooltip.tsx rename to client/src/Components/design-elements/Tooltip.tsx diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/design-elements/index.tsx similarity index 76% rename from client/src/Components/v2/design-elements/index.tsx rename to client/src/Components/design-elements/index.tsx index 6ebffc624..ed6a885f6 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/design-elements/index.tsx @@ -15,3 +15,9 @@ export { default as Icon } from "./Icon"; export * from "./Tooltip"; export * from "./StatBox"; export * from "./BaseChart"; +export * from "./Gauge"; +export * from "./Tabs"; +export * from "./SplitBox"; +export * from "./TextLink"; +export * from "./OfflineBanner"; +export * from "./Avatar"; diff --git a/client/src/Components/v1/I18nLoader/index.jsx b/client/src/Components/i18nLoader/index.tsx similarity index 63% rename from client/src/Components/v1/I18nLoader/index.jsx rename to client/src/Components/i18nLoader/index.tsx index 676e63d7b..f0555afed 100644 --- a/client/src/Components/v1/I18nLoader/index.jsx +++ b/client/src/Components/i18nLoader/index.tsx @@ -1,8 +1,9 @@ -import i18n from "../../../Utils/i18n.js"; +import i18n from "@/Utils/i18n.js"; import { useSelector } from "react-redux"; import { useEffect } from "react"; +import type { RootState } from "@/store"; const I18nLoader = () => { - const language = useSelector((state) => state.ui.language ?? "en"); + const language = useSelector((state: RootState) => state.ui.language ?? "en"); useEffect(() => { if (language && i18n.language !== language) { diff --git a/client/src/Components/v2/inputs/AutoComplete.tsx b/client/src/Components/inputs/AutoComplete.tsx similarity index 97% rename from client/src/Components/v2/inputs/AutoComplete.tsx rename to client/src/Components/inputs/AutoComplete.tsx index af0e8cbfd..c98e8ff32 100644 --- a/client/src/Components/v2/inputs/AutoComplete.tsx +++ b/client/src/Components/inputs/AutoComplete.tsx @@ -1,6 +1,6 @@ import Autocomplete from "@mui/material/Autocomplete"; import type { AutocompleteProps } from "@mui/material/Autocomplete"; -import { TextField, Checkbox } from "@/Components/v2/inputs"; +import { TextField, Checkbox } from "@/Components/inputs"; import ListItem from "@mui/material/ListItem"; import Stack from "@mui/material/Stack"; import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Components/v2/inputs/Button.tsx b/client/src/Components/inputs/Button.tsx similarity index 100% rename from client/src/Components/v2/inputs/Button.tsx rename to client/src/Components/inputs/Button.tsx diff --git a/client/src/Components/v2/inputs/Checkbox.tsx b/client/src/Components/inputs/Checkbox.tsx similarity index 100% rename from client/src/Components/v2/inputs/Checkbox.tsx rename to client/src/Components/inputs/Checkbox.tsx diff --git a/client/src/Components/inputs/ColorPicker.tsx b/client/src/Components/inputs/ColorPicker.tsx new file mode 100644 index 000000000..de736c85d --- /dev/null +++ b/client/src/Components/inputs/ColorPicker.tsx @@ -0,0 +1,48 @@ +import { MuiColorInput } from "mui-color-input"; +import type { MuiColorInputProps } from "mui-color-input"; +import { typographyLevels } from "@/Utils/Theme/Palette"; +import { useTheme } from "@mui/material"; +import Stack from "@mui/material/Stack"; +import { FieldLabel } from "@/Components/inputs/FieldLabel"; + +interface ColorPickerProps extends MuiColorInputProps { + fieldLabel?: string; + required?: boolean; +} + +export const ColorInput = ({ fieldLabel, required, ...props }: ColorPickerProps) => { + const theme = useTheme(); + const input = ( + + ); + if (fieldLabel) { + return ( + + {fieldLabel} + {input} + + ); + } + + return input; +}; diff --git a/client/src/Components/inputs/DatePicker.tsx b/client/src/Components/inputs/DatePicker.tsx new file mode 100644 index 000000000..c63c5ce08 --- /dev/null +++ b/client/src/Components/inputs/DatePicker.tsx @@ -0,0 +1,100 @@ +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import type { DatePickerProps } from "@mui/x-date-pickers/DatePicker"; +import type { Dayjs } from "dayjs"; +import { useTheme } from "@mui/material/styles"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { FieldLabel } from "./FieldLabel"; +import { Calendar } from "lucide-react"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers"; + +interface DatePickerComponentProps extends Omit, "label"> { + fieldLabel?: string; + required?: boolean; + error?: boolean; + helperText?: string; +} + +export const DatePickerComponent = ({ + fieldLabel, + required, + error, + helperText, + ...props +}: DatePickerComponentProps) => { + const theme = useTheme(); + + const picker = ( + + ( + + ), + ...props.slots, + }} + slotProps={{ + switchViewButton: { sx: { display: "none" } }, + nextIconButton: { sx: { ml: theme.spacing(2) } }, + day: { + sx: { + "&.MuiPickersDay-root.Mui-disabled": { + color: theme.palette.text.disabled, + }, + }, + }, + field: { + sx: { + width: "fit-content", + "& > .MuiOutlinedInput-root": { + flexDirection: "row-reverse", + }, + "& input": { + minHeight: 34, + p: 0, + pl: theme.spacing(3), + pr: theme.spacing(5), + }, + "& fieldset": { + borderColor: error ? theme.palette.error.main : theme.palette.divider, + borderRadius: theme.shape.borderRadius, + }, + "&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset": + { + borderColor: error ? theme.palette.error.main : theme.palette.divider, + }, + }, + }, + inputAdornment: { sx: { ml: 0 } }, + openPickerButton: { + sx: { + py: 0, + mr: 0, + }, + }, + ...props.slotProps, + }} + /> + + ); + + return ( + + {fieldLabel && {fieldLabel}} + {picker} + {helperText && ( + + {helperText} + + )} + + ); +}; diff --git a/client/src/Components/inputs/Dialog.tsx b/client/src/Components/inputs/Dialog.tsx new file mode 100644 index 000000000..b0c924f55 --- /dev/null +++ b/client/src/Components/inputs/Dialog.tsx @@ -0,0 +1,78 @@ +import Dialog from "@mui/material/Dialog"; +import type { DialogProps } from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { Button } from "@/Components/inputs"; +import { typographyLevels } from "@/Utils/Theme/Palette"; +import { useTranslation } from "react-i18next"; +import type { ReactNode } from "react"; + +export const DialogInput = ({ + open, + title, + content, + onConfirm, + onCancel, + confirmColor = "primary", + cancelColor = "error", + loading = false, + cancelText, + confirmText, + children, + maxWidth, + fullWidth = false, + additionalButtons, +}: { + open: boolean; + title?: string; + content?: string; + onConfirm?(item: any): any; + onCancel?(item: any): any; + confirmColor?: "error" | "primary"; + cancelColor?: "error" | "primary"; + loading?: boolean; + cancelText?: string; + confirmText?: string; + children?: ReactNode; + maxWidth?: DialogProps["maxWidth"]; + fullWidth?: boolean; + additionalButtons?: ReactNode; +}) => { + const { t } = useTranslation(); + return ( + + {title && {title}} + + {content && {content}} + {children} + + + + {additionalButtons} + {onConfirm && ( + + )} + + + ); +}; diff --git a/client/src/Components/v2/inputs/FieldLabel.tsx b/client/src/Components/inputs/FieldLabel.tsx similarity index 93% rename from client/src/Components/v2/inputs/FieldLabel.tsx rename to client/src/Components/inputs/FieldLabel.tsx index 552991538..448545a19 100644 --- a/client/src/Components/v2/inputs/FieldLabel.tsx +++ b/client/src/Components/inputs/FieldLabel.tsx @@ -9,7 +9,9 @@ interface FieldLabelProps { export const FieldLabel = ({ children, required = false, htmlFor }: FieldLabelProps) => { const theme = useTheme(); - + if (children === " ") { + children = "\u00A0"; + } return ( void; + maxSize?: number; + accept?: string[]; + error?: string; +} + +export const ImageUpload = ({ + src, + onChange, + maxSize = 3 * 1024 * 1024, + accept = ["jpg", "jpeg", "png"], + error, +}: ImageUploadProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const inputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [localError, setLocalError] = useState(null); + const [preview, setPreview] = useState(null); + + const handleFile = useCallback( + (file: File | undefined) => { + if (!file) return; + const isValidType = accept.some((type) => file.type.includes(type)); + const isValidSize = file.size <= maxSize; + if (!isValidType) { + setLocalError(t("components.imageUpload.errors.invalidFileFormat")); + return; + } + if (!isValidSize) { + setLocalError(t("components.imageUpload.errors.invalidFileSize")); + return; + } + setLocalError(null); + const fileObj: FileObject = { + src: URL.createObjectURL(file), + name: file.name, + file, + }; + setPreview(fileObj); + onChange?.(fileObj); + }, + [maxSize, accept, onChange, t] + ); + + const handleClear = () => { + setPreview(null); + setLocalError(null); + onChange?.(undefined); + if (inputRef.current) inputRef.current.value = ""; + }; + + const displaySrc = src || preview?.src; + const displayError = localError || error; + + return ( + + {displaySrc ? ( + + + + + + + ) : ( + { + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onDrop={(e) => { + e.preventDefault(); + setIsDragging(false); + handleFile(e.dataTransfer.files?.[0]); + }} + alignItems="center" + justifyContent="center" + gap={1} + sx={{ + position: "relative", + width: "100%", + minHeight: 150, + border: "2px dashed", + borderRadius: 1, + borderColor: isDragging ? theme.palette.primary.main : theme.palette.divider, + backgroundColor: isDragging ? theme.palette.action.hover : "transparent", + transition: "0.2s", + cursor: "pointer", + "&:hover": { + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.action.hover, + }, + }} + > + `.${ext}`).join(",")} + onChange={(e) => handleFile(e.target.files?.[0])} + style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }} + /> + + + + {t("components.imageUpload.clickToUpload")} + {" "} + {t("components.imageUpload.orDragAndDrop")} + + + {accept.join(", ").toUpperCase()} • {t("components.imageUpload.maxSize")}{" "} + {Math.round(maxSize / 1024 / 1024)}MB + + + )} + {displayError && ( + + {displayError} + + )} + + ); +}; diff --git a/client/src/Components/inputs/LanguageSelector.tsx b/client/src/Components/inputs/LanguageSelector.tsx new file mode 100644 index 000000000..a6d52a555 --- /dev/null +++ b/client/src/Components/inputs/LanguageSelector.tsx @@ -0,0 +1,100 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import MenuItem from "@mui/material/MenuItem"; +import { Select } from "@/Components/inputs"; +import "flag-icons/css/flag-icons.min.css"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material"; +import { useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; +import { setLanguage } from "@/Features/UI/uiSlice"; +import type { RootState } from "@/Types/state"; + +const langMap: Record = { + cs: "cz", + ja: "jp", + uk: "ua", + vi: "vn", +}; + +export const LanguageSelector = () => { + const { i18n } = useTranslation(); + const theme = useTheme(); + const { language = "en" } = useSelector((state: RootState) => state.ui); + const dispatch = useDispatch(); + const handleChange = (event: any) => { + const newLang = event.target.value; + dispatch(setLanguage(newLang)); + }; + + const languages = Object.keys(i18n.options.resources || {}); + + return ( + + ); +}; + +export default LanguageSelector; diff --git a/client/src/Components/inputs/Radio.tsx b/client/src/Components/inputs/Radio.tsx new file mode 100644 index 000000000..ad8dc3374 --- /dev/null +++ b/client/src/Components/inputs/Radio.tsx @@ -0,0 +1,88 @@ +import Radio from "@mui/material/Radio"; +import type { RadioProps } from "@mui/material/Radio"; +import { useTheme } from "@mui/material/styles"; +import { Circle, CircleDot } from "lucide-react"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Typography from "@mui/material/Typography"; + +interface RadioInputProps extends RadioProps {} + +export const RadioInput = ({ ...props }: RadioInputProps) => { + const theme = useTheme(); + return ( + + } + checkedIcon={ + + } + sx={{ + padding: 0, + mt: theme.spacing(0.5), + color: theme.palette.text.secondary, + "&.Mui-checked": { + color: theme.palette.primary.main, + "& svg circle": { + fill: theme.palette.primary.main, + }, + }, + "& .MuiSvgIcon-root": { + fontSize: 16, + }, + "& svg": { + stroke: "currentColor", + }, + "& svg path, & svg line, & svg polyline, & svg rect, & svg circle": { + stroke: "currentColor", + fill: "none", + }, + }} + /> + ); +}; + +export const RadioWithDescription = ({ + label, + description, + ...props +}: RadioInputProps & { label: string; description: string }) => { + const theme = useTheme(); + return ( + } + label={ + <> + {label} + + {description} + + + } + sx={{ + alignItems: "flex-start", + p: theme.spacing(2.5), + m: theme.spacing(-2.5), + borderRadius: theme.shape.borderRadius, + + "&:hover": { + backgroundColor: theme.palette.background.paper, + }, + "& .MuiButtonBase-root": { + p: 0, + mr: theme.spacing(6), + }, + }} + /> + ); +}; diff --git a/client/src/Components/v2/inputs/Select.tsx b/client/src/Components/inputs/Select.tsx similarity index 76% rename from client/src/Components/v2/inputs/Select.tsx rename to client/src/Components/inputs/Select.tsx index 057b0e72c..190307ec3 100644 --- a/client/src/Components/v2/inputs/Select.tsx +++ b/client/src/Components/inputs/Select.tsx @@ -1,7 +1,7 @@ import Select from "@mui/material/Select"; -import React from "react"; -import { useTheme } from "@mui/material/styles"; import type { SelectProps } from "@mui/material/Select"; +import React, { forwardRef } from "react"; +import { useTheme } from "@mui/material/styles"; import Stack from "@mui/material/Stack"; import { FieldLabel } from "./FieldLabel"; import { ChevronDown } from "lucide-react"; @@ -14,18 +14,15 @@ interface SelectInputProps extends Omit, "label"> { placeholderColor?: string; } -export const SelectInput = ({ - fieldLabel, - required, - placeholder, - placeholderColor, - ...props -}: SelectInputProps) => { +const SelectInputInner = ( + { fieldLabel, required, placeholder, placeholderColor, ...props }: SelectInputProps, + ref: React.ForwardedRef +) => { const theme = useTheme(); const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled; - const renderValue = (selected: any) => { - const isMultiple = Boolean((props as any).multiple); + const renderValue = (selected: unknown) => { + const isMultiple = Boolean((props as { multiple?: boolean }).multiple); const isEmpty = isMultiple ? !Array.isArray(selected) || selected.length === 0 : selected === undefined || selected === null || selected === ""; @@ -39,7 +36,7 @@ export const SelectInput = ({ const capitalized = items.map( (item) => item.charAt(0).toUpperCase() + item.slice(1) ); - return ({capitalized.join(" | ")}) as any; + return {capitalized.join(" | ")}; } const nodes = React.Children.toArray(props.children as React.ReactNode); @@ -59,6 +56,7 @@ export const SelectInput = ({ const select = ( {...props} + ref={ref} displayEmpty renderValue={renderValue} inputProps={{ @@ -74,6 +72,12 @@ export const SelectInput = ({ )} sx={{ height: "34px", + "& .MuiSelect-select": { + display: "flex", + alignItems: "center", + height: "100%", + boxSizing: "border-box", + }, "& .MuiSelect-icon": { right: theme.spacing(3), }, @@ -100,3 +104,7 @@ export const SelectInput = ({ return select; }; + +export const SelectInput = forwardRef(SelectInputInner) as ( + props: SelectInputProps & { ref?: React.ForwardedRef } +) => React.ReactElement; diff --git a/client/src/Components/inputs/Slider.tsx b/client/src/Components/inputs/Slider.tsx new file mode 100644 index 000000000..83037ab54 --- /dev/null +++ b/client/src/Components/inputs/Slider.tsx @@ -0,0 +1,92 @@ +import Typography from "@mui/material/Typography"; +import { forwardRef } from "react"; +import Slider from "@mui/material/Slider"; +import type { SliderProps } from "@mui/material/Slider"; +import { useTheme } from "@mui/material/styles"; +import Stack from "@mui/material/Stack"; +import { FieldLabel } from "./FieldLabel"; +import Box from "@mui/material/Box"; +import type { ResponsiveStyleValue } from "@mui/system"; + +interface SliderInputProps extends SliderProps { + sx?: SliderProps["sx"]; + showValue?: boolean; +} + +export const SliderInput = forwardRef( + ({ sx, showValue = false, ...props }, ref) => { + const theme = useTheme(); + const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + + return ( + + {showValue && {props.value}} + + + ); + } +); + +interface SliderWithLabelProps extends SliderProps { + fieldLabel?: string; + required?: boolean; + showValue?: boolean; + sliderMaxWidth?: ResponsiveStyleValue; +} + +export const SliderWithLabel = forwardRef( + ( + { fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props }, + ref + ) => { + const theme = useTheme(); + + const labelText = fieldLabel; + + return ( + + {fieldLabel && {labelText}} + + + + + ); + } +); diff --git a/client/src/Components/inputs/Switch.tsx b/client/src/Components/inputs/Switch.tsx new file mode 100644 index 000000000..05be73a24 --- /dev/null +++ b/client/src/Components/inputs/Switch.tsx @@ -0,0 +1,47 @@ +import { forwardRef } from "react"; +import Switch from "@mui/material/Switch"; +import type { SwitchProps } from "@mui/material/Switch"; +import { useTheme } from "@mui/material/styles"; + +interface SwitchComponentProps extends SwitchProps { + dualOption?: boolean; +} + +export const SwitchComponent = forwardRef( + function SwitchComponent({ sx, dualOption = false, ...props }, ref) { + const theme = useTheme(); + const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + + return ( + + ); + } +); diff --git a/client/src/Components/inputs/SwitchTheme.tsx b/client/src/Components/inputs/SwitchTheme.tsx new file mode 100644 index 000000000..e609f50fb --- /dev/null +++ b/client/src/Components/inputs/SwitchTheme.tsx @@ -0,0 +1,48 @@ +import IconButton from "@mui/material/IconButton"; + +import { Moon, Sun } from "lucide-react"; + +import { setMode } from "@/Features/UI/uiSlice.js"; +import { useTheme } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; + +export const SwitchTheme = () => { + const mode = useSelector((state: RootState) => state.ui.mode); + const dispatch = useDispatch(); + const theme = useTheme(); + + const handleChange = () => { + dispatch(setMode(mode === "light" ? "dark" : "light")); + }; + return ( + + {mode === "light" ? ( + + ) : ( + + )} + + ); +}; diff --git a/client/src/Components/v2/inputs/TextInput.tsx b/client/src/Components/inputs/TextInput.tsx similarity index 95% rename from client/src/Components/v2/inputs/TextInput.tsx rename to client/src/Components/inputs/TextInput.tsx index 9a16a43c7..718b60327 100644 --- a/client/src/Components/v2/inputs/TextInput.tsx +++ b/client/src/Components/inputs/TextInput.tsx @@ -1,7 +1,7 @@ import { forwardRef } from "react"; import TextField from "@mui/material/TextField"; import type { TextFieldProps } from "@mui/material"; -import { typographyLevels } from "@/Utils/Theme/v2Palette"; +import { typographyLevels } from "@/Utils/Theme/Palette"; import { useTheme } from "@mui/material/styles"; import Stack from "@mui/material/Stack"; import { FieldLabel } from "./FieldLabel"; diff --git a/client/src/Components/inputs/TimePicker.tsx b/client/src/Components/inputs/TimePicker.tsx new file mode 100644 index 000000000..251332fe1 --- /dev/null +++ b/client/src/Components/inputs/TimePicker.tsx @@ -0,0 +1,61 @@ +import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; +import type { MobileTimePickerProps } from "@mui/x-date-pickers/MobileTimePicker"; +import type { Dayjs } from "dayjs"; +import { useTheme } from "@mui/material/styles"; +import Stack from "@mui/material/Stack"; +import { FieldLabel } from "./FieldLabel"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers"; + +interface TimePickerComponentProps extends Omit, "label"> { + fieldLabel?: string; + required?: boolean; +} + +export const TimePickerComponent = ({ + fieldLabel, + required, + ...props +}: TimePickerComponentProps) => { + const theme = useTheme(); + + const picker = ( + + + + ); + + if (fieldLabel) { + return ( + + {fieldLabel} + {picker} + + ); + } + + return picker; +}; diff --git a/client/src/Components/v2/inputs/ToggleButton.tsx b/client/src/Components/inputs/ToggleButton.tsx similarity index 100% rename from client/src/Components/v2/inputs/ToggleButton.tsx rename to client/src/Components/inputs/ToggleButton.tsx diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/inputs/index.tsx similarity index 58% rename from client/src/Components/v2/inputs/index.tsx rename to client/src/Components/inputs/index.tsx index 70e740dbd..78f1a51cf 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/inputs/index.tsx @@ -9,3 +9,12 @@ export { ToggleButtonGroupInput as ToggleButtonGroup, } from "./ToggleButton"; export { DialogInput as Dialog } from "./Dialog"; +export * from "./Radio"; +export * from "./Switch"; +export * from "./Slider"; +export * from "./ImageUpload"; +export * from "./ColorPicker"; +export { DatePickerComponent as DatePicker } from "./DatePicker"; +export { TimePickerComponent as TimePicker } from "./TimePicker"; +export * from "./LanguageSelector"; +export * from "./SwitchTheme"; diff --git a/client/src/Components/layout/AppLayout.tsx b/client/src/Components/layout/AppLayout.tsx new file mode 100644 index 000000000..baadd85ca --- /dev/null +++ b/client/src/Components/layout/AppLayout.tsx @@ -0,0 +1,62 @@ +import Box from "@mui/material/Box"; +import { useState, useEffect, useRef } from "react"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import BackgroundSVG from "@/assets/Images/background.svg"; +import type { RootState } from "@/Types/state"; +import { OfflineBanner } from "@/Components/design-elements"; +import { setServerUnreachableCallback, get } from "@/Utils/ApiClient"; + +interface AppLayoutProps { + children: React.ReactNode; +} + +const AppLayout = ({ children }: AppLayoutProps) => { + const theme = useTheme(); + const mode = useSelector((state: RootState) => state.ui.mode); + const [serverUnreachable, setServerUnreachable] = useState(false); + const retryIntervalRef = useRef(null); + + useEffect(() => { + setServerUnreachableCallback(setServerUnreachable); + }, []); + + useEffect(() => { + if (serverUnreachable) { + retryIntervalRef.current = window.setInterval(async () => { + try { + await get("/health", { timeout: 5000 }); + } catch { + // NO_OP + } + }, 5000); + } else if (retryIntervalRef.current) { + clearInterval(retryIntervalRef.current); + retryIntervalRef.current = null; + } + + return () => { + if (retryIntervalRef.current) { + clearInterval(retryIntervalRef.current); + } + }; + }, [serverUnreachable]); + + return ( + + + {children} + + ); +}; + +export default AppLayout; diff --git a/client/src/Components/layout/RootLayout.tsx b/client/src/Components/layout/RootLayout.tsx new file mode 100644 index 000000000..c22bd7455 --- /dev/null +++ b/client/src/Components/layout/RootLayout.tsx @@ -0,0 +1,44 @@ +import { Sidebar } from "@/Components/sidebar"; +import { Outlet } from "react-router"; +import Stack from "@mui/material/Stack"; +import { useMediaQuery } from "@mui/material"; +import { useSidebar } from "@/Hooks/useSidebar"; + +import { useTheme } from "@mui/material"; + +const RootLayout = () => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const { collapsedWidth } = useSidebar(); + + return ( + + + + + + + + + ); +}; + +export default RootLayout; diff --git a/client/src/Components/v2/monitors/ControlsFilter.tsx b/client/src/Components/monitors/ControlsFilter.tsx similarity index 69% rename from client/src/Components/v2/monitors/ControlsFilter.tsx rename to client/src/Components/monitors/ControlsFilter.tsx index d7eecefb5..e37408819 100644 --- a/client/src/Components/v2/monitors/ControlsFilter.tsx +++ b/client/src/Components/monitors/ControlsFilter.tsx @@ -1,16 +1,17 @@ import Stack from "@mui/material/Stack"; -import { Select, Button } from "@/Components/v2/inputs"; +import { Select, Button } from "@/Components/inputs"; import MenuItem from "@mui/material/MenuItem"; import useMediaQuery from "@mui/material/useMediaQuery"; import type { MonitorType } from "@/Types/Monitor"; import { Typography, useTheme } from "@mui/material"; -const types = ["http", "ping", "port", "docker"]; +const types = ["http", "ping", "port", "docker", "game"]; const statuses = ["up", "down"]; const states = ["active", "paused"]; export const ControlsFilter = ({ + showTypes = true, selectedTypes, setSelectedTypes, selectedStatus, @@ -19,8 +20,9 @@ export const ControlsFilter = ({ setSelectedState, onClearFilters, }: { - selectedTypes: MonitorType[]; - setSelectedTypes: React.Dispatch>; + showTypes?: boolean; + selectedTypes?: MonitorType[]; + setSelectedTypes?: React.Dispatch>; selectedStatus: string; setSelectedStatus: React.Dispatch>; selectedState: string; @@ -30,27 +32,29 @@ export const ControlsFilter = ({ const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isFilterActive = - selectedTypes.length > 0 || selectedStatus !== "" || selectedState !== ""; + (selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== ""; return ( - + {showTypes && setSelectedTypes && ( + + )} ( - - )} - displayEmpty - value={controlledValue} - onChange={onChange} - renderValue={(selected) => { - if (!selected?.length) { - return header; - } - - return ( - header + - " | " + - selected - .map((value) => options.find((option) => option.value === value)?.label) - .filter(Boolean) - .join(", ") - ); - }} - MenuProps={{ - anchorOrigin: { - vertical: "bottom", - horizontal: "left", - }, - transformOrigin: { - vertical: "top", - horizontal: "left", - }, - }} - > - {options.map((option) => ( - - - - - ))} - - - ); -}; - -FilterHeader.propTypes = { - header: PropTypes.string.isRequired, - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - }) - ).isRequired, - value: PropTypes.arrayOf(PropTypes.string), - onChange: PropTypes.func.isRequired, - multiple: PropTypes.bool, -}; - -export default FilterHeader; diff --git a/client/src/Components/v1/GenericFallback/NetworkError.jsx b/client/src/Components/v1/GenericFallback/NetworkError.jsx deleted file mode 100644 index ddffa1c31..000000000 --- a/client/src/Components/v1/GenericFallback/NetworkError.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -const NetworkError = () => { - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - <> - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); -}; - -export default NetworkError; diff --git a/client/src/Components/v1/GenericFallback/index.jsx b/client/src/Components/v1/GenericFallback/index.jsx deleted file mode 100644 index 188c09f37..000000000 --- a/client/src/Components/v1/GenericFallback/index.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { Box, Stack } from "@mui/material"; -import OutputAnimation from "../../../assets/Animations/output.gif"; -import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import { useSelector } from "react-redux"; - -/** - * Fallback component to display a fallback UI for network errors - * - * @returns {JSX.Element} The rendered fallback UI. - */ - -const GenericFallback = ({ children }) => { - const theme = useTheme(); - const mode = useSelector((state) => state.ui.mode); - - return ( - - - - - - - - {children} - - - - ); -}; - -export default GenericFallback; diff --git a/client/src/Components/v1/HOC/withAdminCheck.jsx b/client/src/Components/v1/HOC/withAdminCheck.jsx deleted file mode 100644 index 8646c99b6..000000000 --- a/client/src/Components/v1/HOC/withAdminCheck.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { logger } from "../../../Utils/Logger.js"; -import { networkService } from "../../../main.jsx"; - -const withAdminCheck = (WrappedComponent) => { - const WithAdminCheck = (props) => { - const navigate = useNavigate(); - const [isChecking, setIsChecking] = useState(true); - const [superAdminExists, setSuperAdminExists] = useState(false); - - useEffect(() => { - networkService - .doesSuperAdminExist() - .then((response) => { - if (response?.data?.data === true) { - navigate("/login"); - } else { - setSuperAdminExists(false); - } - }) - .catch((error) => { - logger.error(error); - }) - .finally(() => { - setIsChecking(false); - }); - }, [navigate]); - - if (isChecking) { - return null; - } - - return ( - - ); - }; - const wrappedComponentName = - WrappedComponent.displayName || WrappedComponent.name || "Component"; - WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`; - - return WithAdminCheck; -}; - -export default withAdminCheck; diff --git a/client/src/Components/v1/Heading/index.jsx b/client/src/Components/v1/Heading/index.jsx deleted file mode 100644 index 1a5f1a53b..000000000 --- a/client/src/Components/v1/Heading/index.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Typography } from "@mui/material"; -import PropTypes from "prop-types"; - -/** - * A heading component that renders text with a specific heading level. - * - * @param {Object} props - The properties passed to the Heading component. - * @param {('h1'|'h2'|'h3')} props.component - The heading level for the component. - * @param {Object} props.style - Custom styles to apply to the heading. - * @param {string} props.children - The content to display inside the heading. - * @returns {JSX.Element} The Typography component with specified heading properties. - */ - -function Heading({ component, style, children }) { - return ( - - {children} - - ); -} - -Heading.propTypes = { - component: PropTypes.oneOf(["h1", "h2", "h3"]).isRequired, - style: PropTypes.object, - children: PropTypes.string.isRequired, -}; -export { Heading }; diff --git a/client/src/Components/v1/Host/index.jsx b/client/src/Components/v1/Host/index.jsx deleted file mode 100644 index 6670722c1..000000000 --- a/client/src/Components/v1/Host/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import Dot from "../Dot/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -/** - * Host component. - * This subcomponent receives a params object and displays the host details. - * - * @component - * @param {Object} params - An object containing the following properties: - * @param {string} params.url - The URL of the host. - * @param {string} params.title - The name of the host. - * @param {string} params.percentageColor - The color of the percentage text. - * @param {number} params.percentage - The percentage to display. - * @returns {React.ElementType} Returns a div element with the host details. - */ -const Host = ({ url, title, percentageColor, percentage, showURL, status }) => { - const theme = useTheme(); - return ( - - - - {title} - - - {percentageColor && percentage && ( - <> - - - {percentage}% - - {status && ( - - )} - - )} - - {showURL && {url}} - - ); -}; - -Host.propTypes = { - title: PropTypes.string, - percentageColor: PropTypes.string, - percentage: PropTypes.string, - url: PropTypes.string, - showURL: PropTypes.bool, - status: PropTypes.string, -}; - -export default Host; diff --git a/client/src/Components/v1/HttpStatusLabel/index.jsx b/client/src/Components/v1/HttpStatusLabel/index.jsx deleted file mode 100644 index 2253e323e..000000000 --- a/client/src/Components/v1/HttpStatusLabel/index.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; -import { BaseLabel } from "../Label/index.jsx"; - -/** - * @component - * @param {Object} props - * @param {number} props.status - The http status for the label - * @param {Styles} props.customStyles - CSS Styles passed from parent component - * @returns {JSX.Element} - * @example - * // Render a http status label - * - */ - -const DEFAULT_CODE = 9999; // Default code for unknown status - -const handleStatusCode = (status) => { - if (status) { - return status; - } - return DEFAULT_CODE; -}; - -const getRoundedStatusCode = (status) => { - return Math.floor(status / 100) * 100; -}; - -const HttpStatusLabel = ({ status, customStyles }) => { - const theme = useTheme(); - const colors = { - 400: { - color: theme.palette.warning.main, - borderColor: theme.palette.warning.lowContrast, - }, - 500: { - color: theme.palette.error.main, - borderColor: theme.palette.error.lowContrast, - }, - default: { - color: theme.palette.primary.contrastText, - borderColor: theme.palette.primary.contrastText, - }, - }; - - const statusCode = handleStatusCode(status); - - const { borderColor, color } = - colors[getRoundedStatusCode(statusCode)] || colors.default; - return ( - - ); -}; - -HttpStatusLabel.propTypes = { - status: PropTypes.number, - customStyles: PropTypes.object, -}; - -export { HttpStatusLabel }; diff --git a/client/src/Components/v1/Icon/iconMap.js b/client/src/Components/v1/Icon/iconMap.js deleted file mode 100644 index 58da7304b..000000000 --- a/client/src/Components/v1/Icon/iconMap.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Maps legacy icon names to Lucide icon names - * Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']] - */ -export const iconMap = { - // Navigation/Sidebar icons - monitors: "Globe", - incidents: "AlertTriangle", - integrations: "Link", - "page-speed": "Gauge", - settings: "Settings", - notifications: "Bell", - maintenance: "Wrench", - "status-pages": "Wifi", - docs: "FileText", - discussions: "MessageCircle", - support: "HelpCircle", - changeLog: "Code", - logs: "Database", - - // User icons - user: "User", - "user-two": "Users", - "user-edit": "UserPen", - groups: "Users", - lock: "Lock", - key: "Key", - logout: "LogOut", - - // Action icons - edit: "Pencil", - "trash-bin": "Trash2", - search: "Search", - "close-icon": "X", - check: "Check", - "check-icon": "CheckCircle", - "check-outlined": "CheckCircle", - - // Arrow icons - "left-arrow": "ChevronLeft", - "right-arrow": "ChevronRight", - "up-arrow": "ChevronUp", - "down-arrow": "ChevronDown", - "left-arrow-double": "ChevronsLeft", - "right-arrow-double": "ChevronsRight", - "left-arrow-long": "ArrowLeft", - "top-right-arrow": "ArrowUpRight", - "open-in-new-page": "ExternalLink", - "selector-vertical": "ChevronsUpDown", - - // Status/Alert icons - "alert-icon": "Bell", - "warning-icon": "AlertCircle", - "history-icon": "History", - - // Data/Chart icons - dashboard: "LayoutGrid", - "monitor-graph-line": "TrendingUp", - "response-time-icon": "TrendingUp", - "uptime-icon": "Activity", - "speedometer-icon": "Gauge", - "spedometer-icon": "Gauge", - "ruler-icon": "Ruler", - "performance-report": "Layers", - "average-response-icon": "BarChart3", - "distributed-uptime": "Network", - - // Misc icons - calendar: "Calendar", - "calendar-check": "CalendarCheck", - "clock-snooze": "Clock", - mail: "Mail", - email: "Mail", - folder: "Folder", - "dots-vertical": "MoreVertical", - "pause-icon": "PauseCircle", - "resume-icon": "PlayCircle", - "checkbox-filled": "CheckSquare", - "checkbox-outline": "Square", - "radio-checked": "Circle", - "cpu-chip": "Cpu", - certificate: "Award", - "interval-check": "RefreshCw", -}; - -/** - * MUI icon to Lucide mapping - */ -export const muiToLucideMap = { - AddCircleOutline: "PlusCircle", - ArrowDownwardRounded: "ArrowDown", - ArrowUpwardRounded: "ArrowUp", - ArrowDropDown: "ChevronDown", - ArrowOutward: "ExternalLink", - CheckCircle: "CheckCircle2", - Clear: "X", - Close: "X", - CloseRounded: "X", - CloudUpload: "Upload", - Delete: "Trash2", - DeleteOutlineRounded: "Trash2", - Email: "Mail", - ErrorOutline: "AlertTriangle", - ErrorOutlineOutlined: "AlertTriangle", - Image: "ImageIcon", - InfoOutlined: "Info", - KeyboardArrowDown: "ChevronDown", - Menu: "Menu", - PauseCircleOutline: "PauseCircle", - PauseOutlined: "Pause", - PlayArrowOutlined: "Play", - PlayCircleOutlineRounded: "PlayCircle", - RadioButtonChecked: "Circle", - ReorderRounded: "GripVertical", - SettingsOutlined: "Settings", - Share: "Share2", - Visibility: "Eye", - VisibilityOff: "EyeOff", - WarningAmberOutlined: "AlertTriangle", -}; diff --git a/client/src/Components/v1/Icon/index.jsx b/client/src/Components/v1/Icon/index.jsx deleted file mode 100644 index 9f7c61585..000000000 --- a/client/src/Components/v1/Icon/index.jsx +++ /dev/null @@ -1,198 +0,0 @@ -import { useTheme } from "@mui/material"; -import PropTypes from "prop-types"; -import { - Activity, - AlertCircle, - AlertTriangle, - ArrowDown, - ArrowLeft, - ArrowUp, - ArrowUpRight, - Bell, - Calendar, - Check, - CheckCircle, - CheckCircle2, - CheckSquare, - ChevronDown, - ChevronLeft, - ChevronRight, - ChevronsLeft, - ChevronsRight, - ChevronsUpDown, - Circle, - CircleDot, - Clock, - Code, - Cpu, - Database, - ExternalLink, - Eye, - EyeOff, - FileText, - Gauge, - Globe, - GripVertical, - HelpCircle, - History, - Image as ImageIcon, - Info, - Key, - Layers, - Link, - Lock, - LogOut, - Mail, - Menu, - MessageCircle, - MoreVertical, - Pause, - PauseCircle, - Play, - PlayCircle, - PlusCircle, - RefreshCw, - Ruler, - Search, - Settings, - Square, - Trash2, - TrendingUp, - Upload, - User, - Users, - Wifi, - Wrench, - X, -} from "lucide-react"; - -/** - * Map of icon names to Lucide icon components. - * Only icons explicitly imported here will be included in the bundle (tree-shaking). - * To add a new icon: import it above and add it to this object. - */ -const iconComponents = { - Activity, - AlertCircle, - AlertTriangle, - ArrowDown, - ArrowLeft, - ArrowUp, - ArrowUpRight, - Bell, - Calendar, - Check, - CheckCircle, - CheckCircle2, - CheckSquare, - ChevronDown, - ChevronLeft, - ChevronRight, - ChevronsLeft, - ChevronsRight, - ChevronsUpDown, - Circle, - CircleDot, - Clock, - Code, - Cpu, - Database, - ExternalLink, - Eye, - EyeOff, - FileText, - Gauge, - Globe, - GripVertical, - HelpCircle, - History, - Image: ImageIcon, - Info, - Key, - Layers, - Link, - Lock, - LogOut, - Mail, - Menu, - MessageCircle, - MoreVertical, - Pause, - PauseCircle, - Play, - PlayCircle, - PlusCircle, - RefreshCw, - Ruler, - Search, - Settings, - Square, - Trash2, - TrendingUp, - Upload, - User, - Users, - Wifi, - Wrench, - X, -}; - -/** - * Theme-aware icon component wrapping Lucide icons - * - * @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft") - * @param {number} size - Icon size in pixels (default: 20) - * @param {string} color - Direct color value OR theme path like "primary.contrastText" - * @param {number} strokeWidth - Stroke width (default: 1.5) - * @param {string} fill - Fill color (default: "none") - */ -const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => { - const theme = useTheme(); - - // Resolve color from theme path or use direct value - const resolveColor = (colorValue) => { - if (!colorValue) { - return theme.palette.primary.contrastTextTertiary; // Default icon color - } - - // If it's a theme path like "primary.contrastText" - if (typeof colorValue === "string" && colorValue.includes(".")) { - const parts = colorValue.split("."); - let resolved = theme.palette; - for (const part of parts) { - resolved = resolved?.[part]; - } - return resolved || colorValue; - } - - // Direct color value - return colorValue; - }; - - const LucideIcon = iconComponents[name]; - - if (!LucideIcon) { - console.warn(`Icon "${name}" not found in Icon component`); - return null; - } - - return ( - - ); -}; - -Icon.propTypes = { - name: PropTypes.string.isRequired, - size: PropTypes.number, - color: PropTypes.string, - strokeWidth: PropTypes.number, - fill: PropTypes.string, -}; - -export default Icon; diff --git a/client/src/Components/v1/IconBox/index.jsx b/client/src/Components/v1/IconBox/index.jsx deleted file mode 100644 index bb9770e6a..000000000 --- a/client/src/Components/v1/IconBox/index.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Box, styled } from "@mui/material"; -import PropTypes from "prop-types"; - -/** - * IconBox - A styled box component for rendering icons with consistent sizing and styling - * - * @component - * @param {Object} [props] - Configuration options for the IconBox - * @param {number} [props.height=34] - Height of the icon box - * @param {number} [props.width=34] - Width of the icon box - * @param {number} [props.minWidth=34] - Minimum width of the icon box - * @param {number} [props.borderRadius=4] - Border radius of the icon box - * @param {number} [props.svgWidth=20] - Width of the SVG icon - * @param {number} [props.svgHeight=20] - Height of the SVG icon - * - * @example - * // Basic usage - * - * - * - * - * @example - * // Customized usage - * - * - * - * - * @returns {React.ReactElement} A styled box containing an icon - */ -const IconBox = styled(Box)( - ({ - theme, - height = 34, - width = 34, - minWidth = 34, - borderRadius = 4, - svgWidth = 20, - svgHeight = 20, - }) => ({ - height: height, - minWidth: minWidth, - width: width, - position: "relative", - border: 1, - borderStyle: "solid", - borderColor: theme.palette.primary.lowContrast, - borderRadius: borderRadius, - backgroundColor: theme.palette.tertiary.main, - "& svg": { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: svgWidth, - height: svgHeight, - "& path": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }, - }) -); - -IconBox.propTypes = { - height: PropTypes.number, - width: PropTypes.number, - minWidth: PropTypes.number, - borderRadius: PropTypes.number, - svgWidth: PropTypes.number, - svgHeight: PropTypes.number, -}; - -export default IconBox; diff --git a/client/src/Components/v1/Image/index.jsx b/client/src/Components/v1/Image/index.jsx deleted file mode 100644 index 8f30f8a9e..000000000 --- a/client/src/Components/v1/Image/index.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Box } from "@mui/material"; -import PropTypes from "prop-types"; - -const isValidBase64Image = (data) => { - return /^[A-Za-z0-9+/=]+$/.test(data); -}; - -const Image = ({ - shouldRender = true, - src, - alt, - width = "auto", - height = "auto", - minWidth = "auto", - minHeight = "auto", - maxWidth = "auto", - maxHeight = "auto", - base64, - placeholder, - sx, -}) => { - if (shouldRender === false) { - return null; - } - - if (typeof src !== "undefined" && typeof base64 !== "undefined") { - console.warn("base64 takes precedence over src and overwrites it"); - } - - if (typeof base64 !== "undefined" && isValidBase64Image(base64)) { - src = `data:image/png;base64,${base64}`; - } - - if ( - typeof src === "undefined" && - typeof base64 === "undefined" && - typeof placeholder !== "undefined" - ) { - src = placeholder; - } - - return ( - - ); -}; - -Image.propTypes = { - shouldRender: PropTypes.bool, - src: PropTypes.string, - alt: PropTypes.string.isRequired, - width: PropTypes.string, - height: PropTypes.string, - minWidth: PropTypes.string, - minHeight: PropTypes.string, - maxWidth: PropTypes.string, - maxHeight: PropTypes.string, - base64: PropTypes.string, - sx: PropTypes.object, -}; - -export default Image; diff --git a/client/src/Components/v1/InfoBox/index.jsx b/client/src/Components/v1/InfoBox/index.jsx deleted file mode 100644 index 96e67975d..000000000 --- a/client/src/Components/v1/InfoBox/index.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import { RowContainer } from "../StandardContainer/index.jsx"; -import { Stack, Typography } from "@mui/material"; -import Image from "../Image/index.jsx"; - -import { useTheme } from "@mui/material/styles"; - -const InfoBox = ({ - img, - icon: Icon, - alt, - heading, - headingLevel = 2, - subHeading, - subHeadingLevel = "", - sx, -}) => { - const theme = useTheme(); - return ( - - {img && ( - {alt} - )} - {Icon && ( - - )} - - {heading} - - {subHeading} - - - - ); -}; - -export default InfoBox; diff --git a/client/src/Components/v1/Inputs/Checkbox/index.jsx b/client/src/Components/v1/Inputs/Checkbox/index.jsx deleted file mode 100644 index caa6830e7..000000000 --- a/client/src/Components/v1/Inputs/Checkbox/index.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import PropTypes from "prop-types"; -import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import Icon from "../../Icon"; - -/** - * Checkbox Component - * - * A customized checkbox component using Material-UI that supports custom sizing, - * disabled states, and custom icons. - * - * @component - * @param {Object} props - Component properties - * @param {string} props.id - Unique identifier for the checkbox input - * @param {string} [props.name] - Optional name attribute for the checkbox - * @param {(string|React.ReactNode)} props.label - Label text or node for the checkbox - * @param {('small'|'medium'|'large')} [props.size='medium'] - Size of the checkbox icon - * @param {boolean} props.isChecked - Current checked state of the checkbox - * @param {string} [props.value] - Optional value associated with the checkbox - * @param {Function} [props.onChange] - Callback function triggered when checkbox state changes - * @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled - * - * @returns {React.ReactElement} Rendered Checkbox component - * - * @example - * // Basic usage - * - * - * @example - * // With custom size and disabled state - * - */ -const Checkbox = ({ - id, - name, - label, - size = "medium", - isChecked, - value, - onChange, - isDisabled, -}) => { - /* TODO move sizes to theme */ - const sizes = { small: "14px", medium: "16px", large: "18px" }; - const theme = useTheme(); - return ( - } - checkedIcon={} - inputProps={{ - "aria-label": "controlled checkbox", - id: id, - }} - sx={{ - "&:hover": { backgroundColor: "transparent" }, - "& svg": { width: sizes[size], height: sizes[size] }, - }} - /> - } - label={label} - disabled={isDisabled} - sx={{ - borderRadius: theme.shape.borderRadius, - p: theme.spacing(2.5), - "& .MuiButtonBase-root": { - width: theme.spacing(10), - p: 0, - mr: theme.spacing(6), - }, - "&:not(:has(.Mui-disabled)):hover": { - backgroundColor: theme.palette.tertiary.main, - }, - "& span.MuiTypography-root": { - fontSize: 13, - color: theme.palette.primary.contrastTextTertiary, - }, - ".MuiFormControlLabel-label.Mui-disabled": { - color: theme.palette.primary.contrastTextTertiary, - opacity: 0.25, - }, - }} - /> - ); -}; - -Checkbox.propTypes = { - id: PropTypes.string, - name: PropTypes.string, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - size: PropTypes.oneOf(["small", "medium", "large"]), - isChecked: PropTypes.bool.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - onChange: PropTypes.func, - isDisabled: PropTypes.bool, -}; - -export default Checkbox; diff --git a/client/src/Components/v1/Inputs/ColorPicker/index.jsx b/client/src/Components/v1/Inputs/ColorPicker/index.jsx deleted file mode 100644 index e64c936fc..000000000 --- a/client/src/Components/v1/Inputs/ColorPicker/index.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { MuiColorInput } from "mui-color-input"; - -/** - * - * @param {*} id The ID of the component - * @param {*} value The color value of the component - * @param {*} error The error of the component - * @param {*} onChange The Change handler function - * @param {*} onBlur The Blur handler function - * @returns The ColorPicker component - * Example usage: - * - * - */ -const ColorPicker = ({ id, name, value, error, onChange, onBlur }) => { - const theme = useTheme(); - return ( - - onChange({ target: { name, value: color } })} - onBlur={onBlur} - /> - {error && ( - - {error} - - )} - - ); -}; - -ColorPicker.propTypes = { - id: PropTypes.string.isRequired, - value: PropTypes.string, - error: PropTypes.string, - onChange: PropTypes.func.isRequired, - onBlur: PropTypes.func, - name: PropTypes.string, -}; - -export default ColorPicker; diff --git a/client/src/Components/v1/Inputs/FieldWrapper/index.jsx b/client/src/Components/v1/Inputs/FieldWrapper/index.jsx deleted file mode 100644 index cc5061af8..000000000 --- a/client/src/Components/v1/Inputs/FieldWrapper/index.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; - -const DEFAULT_GAP = 6; -const FieldWrapper = ({ - label, - children, - gap, - labelMb, - labelFontWeight = 500, - labelVariant = "h3", - labelSx = {}, - sx = {}, -}) => { - const theme = useTheme(); - return ( - - {label && ( - - {label} - - )} - {children} - - ); -}; - -FieldWrapper.propTypes = { - label: PropTypes.node, - children: PropTypes.node.isRequired, - gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]), - labelMb: PropTypes.number, - labelFontWeight: PropTypes.number, - labelVariant: PropTypes.string, - labelSx: PropTypes.object, - sx: PropTypes.object, -}; - -export default FieldWrapper; diff --git a/client/src/Components/v1/Inputs/ImageUpload/index.jsx b/client/src/Components/v1/Inputs/ImageUpload/index.jsx deleted file mode 100644 index d224418e0..000000000 --- a/client/src/Components/v1/Inputs/ImageUpload/index.jsx +++ /dev/null @@ -1,249 +0,0 @@ -// Components -import { Box, Stack, Typography } from "@mui/material"; -import Icon from "../../Icon"; -import Image from "../../Image/index.jsx"; -import TextField from "@mui/material/TextField"; -import IconButton from "@mui/material/IconButton"; -import ProgressUpload from "../../ProgressBars/index.jsx"; - -// Utils -import PropTypes from "prop-types"; -import { useCallback, useState, useRef, useEffect } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -/** - * ImageUpload component allows users to upload images with drag-and-drop functionality. - * It supports file size and format validation. - * - * @component - * @param {Object} props - Component props - * @param {boolean} [props.previewIsRound=false] - Determines if the image preview should be round - * @param {string} [props.src] - Source URL of the image to display - * @param {function} props.onChange - Callback function to handle file change, takes a file as an argument - * @param {number} [props.maxSize=3145728] - Maximum file size allowed in bytes (default is 3MB) - * @param {Array} [props.accept=['jpg', 'jpeg', 'png']] - Array of accepted file formats - * @param {Object} [props.errors] - Object containing error messages - * @returns {JSX.Element} The rendered component - */ -const ImageUpload = ({ - previewIsRound = false, - src, - onChange, - maxSize = 3 * 1024 * 1024, - accept = ["jpg", "jpeg", "png"], - error, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - const [uploadComplete, setUploadComplete] = useState(false); - const [completedFile, setCompletedFile] = useState(null); - const [file, setFile] = useState(null); - const [progress, setProgress] = useState({ value: 0, isLoading: false }); - const intervalRef = useRef(null); - const [localError, setLocalError] = useState(null); - const [isDragging, setIsDragging] = useState(false); - - const roundStyle = previewIsRound ? { borderRadius: "50%" } : {}; - - const handleImageChange = useCallback( - (file) => { - if (!file) return; - - const isValidType = accept.some((type) => file.type.includes(type)); - const isValidSize = file.size <= maxSize; - - if (!isValidType) { - setLocalError(t("invalidFileFormat")); - return; - } - if (!isValidSize) { - setLocalError(t("invalidFileSize")); - return; - } - - setLocalError(null); - - const previewFile = { - src: URL.createObjectURL(file), - name: file.name, - file, - }; - - setFile(previewFile); - setProgress({ value: 0, isLoading: true }); - - intervalRef.current = setInterval(() => { - setProgress((prev) => { - const buffer = 12; - if (prev.value + buffer >= 100) { - clearInterval(intervalRef.current); - setUploadComplete(true); - setCompletedFile(previewFile); - return { value: 100, isLoading: false }; - } - return { value: prev.value + buffer, isLoading: true }; - }); - }, 120); - }, - [maxSize, accept] - ); - - useEffect(() => { - if (uploadComplete && completedFile) { - onChange?.(completedFile); - setUploadComplete(false); - setCompletedFile(null); - } - }, [uploadComplete, completedFile, onChange]); - - return ( - <> - {src ? ( - - Uploaded preview - - ) : ( - <> - setIsDragging(true)} - onDragLeave={() => setIsDragging(false)} - onDrop={() => setIsDragging(false)} - sx={{ - position: "relative", - height: "fit-content", - border: "dashed", - borderRadius: theme.shape.borderRadius, - borderColor: isDragging - ? theme.palette.primary.main - : theme.palette.primary.lowContrast, - backgroundColor: isDragging ? "hsl(215, 87%, 51%, 0.05)" : "transparent", - borderWidth: "2px", - transition: "0.2s", - "&:hover": { - borderColor: theme.palette.primary.main, - backgroundColor: "hsl(215, 87%, 51%, 0.05)", - }, - }} - > - handleImageChange(e?.target?.files?.[0])} - sx={{ - width: "100%", - "& .MuiInputBase-input[type='file']": { - opacity: 0, - cursor: "pointer", - maxWidth: "500px", - minHeight: "175px", - zIndex: 1, - }, - "& fieldset": { - padding: 0, - border: "none", - }, - }} - /> - - - - - - - {t("ClickUpload")} - {" "} - or {t("DragandDrop")} - - - ({t("MaxSize")}: {Math.round(maxSize / 1024 / 1024)}MB) - - - - {(localError || progress.isLoading || progress.value !== 0) && ( - - } - label={file?.name || "Upload failed"} - size={file?.size} - progress={progress.value} - onClick={() => { - clearInterval(intervalRef.current); - setFile(null); - setProgress({ value: 0, isLoading: false }); - setLocalError(null); - onChange(undefined); - }} - error={localError || error} - /> - )} - - {t("SupportedFormats")}: {accept.join(", ").toUpperCase()} - - - )} - - ); -}; - -ImageUpload.propTypes = { - previewIsRound: PropTypes.bool, - src: PropTypes.string, - onChange: PropTypes.func, - maxSize: PropTypes.number, - accept: PropTypes.array, - error: PropTypes.string, -}; - -export default ImageUpload; diff --git a/client/src/Components/v1/Inputs/Radio/index.css b/client/src/Components/v1/Inputs/Radio/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Components/v1/Inputs/Radio/index.jsx b/client/src/Components/v1/Inputs/Radio/index.jsx deleted file mode 100644 index b6bd4e69c..000000000 --- a/client/src/Components/v1/Inputs/Radio/index.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import PropTypes from "prop-types"; -import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import Icon from "../../Icon"; -import "./index.css"; - -/** - * Radio component. - * - * @component - * @example - * // Usage: - * - * - * @param {Object} props - The component - * @param {string} id - The id of the radio button. - * @param {string} title - The title of the radio button. - * @param {string} [desc] - The description of the radio button. - * @param {string} [size="small"] - The size of the radio button. - * @returns {JSX.Element} - The rendered Radio component. - */ - -const Radio = ({ - name, - checked, - value, - id, - size, - title, - desc, - onChange, - labelSpacing, -}) => { - const theme = useTheme(); - - return ( - - } - sx={{ - color: "transparent", - width: 16, - height: 16, - boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`, - "&:not(.Mui-checked)": { - boxShadow: `inset 0 0 0 1px ${theme.palette.primary.contrastText}70`, // Use theme text color for the outline - }, - mt: theme.spacing(0.5), - }} - /> - } - onChange={onChange} - label={ - <> - - {title} - - - {desc} - - - } - labelPlacement="end" - sx={{ - alignItems: "flex-start", - p: theme.spacing(2.5), - m: theme.spacing(-2.5), - borderRadius: theme.shape.borderRadius, - "&:hover": { - backgroundColor: theme.palette.tertiary.main, - }, - "& .MuiButtonBase-root": { - p: 0, - mr: theme.spacing(6), - }, - }} - /> - ); -}; - -Radio.propTypes = { - title: PropTypes.string, - desc: PropTypes.string, - size: PropTypes.string, - name: PropTypes.string, - checked: PropTypes.bool, - value: PropTypes.string, - id: PropTypes.string, - onChange: PropTypes.func, -}; - -export default Radio; diff --git a/client/src/Components/v1/Inputs/Search/index.jsx b/client/src/Components/v1/Inputs/Search/index.jsx deleted file mode 100644 index 4371ef971..000000000 --- a/client/src/Components/v1/Inputs/Search/index.jsx +++ /dev/null @@ -1,315 +0,0 @@ -import PropTypes from "prop-types"; -import { - Box, - ListItem, - Autocomplete, - TextField, - Typography, - Checkbox, -} from "@mui/material"; -import { useTheme } from "@emotion/react"; -import Icon from "../../Icon"; -import React, { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import FieldWrapper from "../FieldWrapper/index.jsx"; - -/** - * Search component using Material UI's Autocomplete. - * - * @param {Object} props - * @param {string} props.id - Unique identifier for the Autocomplete component - * @param {Array} props.options - Options to display in the Autocomplete dropdown - * @param {string} props.filteredBy - Key to access the option label from the options - * @param {string} props.value - Current input value for the Autocomplete - * @param {Function} props.handleChange - Function to call when the input changes - * @param {Function} Prop.onBlur - Function to call when the input is blured - * @param {Object} props.sx - Additional styles to apply to the component - * @param {string} props.unit - Label to identify type of options - * @returns {JSX.Element} The rendered Search component - */ - -const SearchAdornment = () => { - const theme = useTheme(); - return ( - - - - ); -}; - -//TODO keep search state inside of component. -const Search = ({ - label, - id, - options, - filteredBy, - secondaryLabel, - value, - inputValue, - handleInputChange, - handleChange, - sx, - multiple = false, - isAdorned = true, - error, - disabled, - startAdornment, - endAdornment, - onBlur, - //FieldWrapper's props - gap, - labelMb, - labelFontWeight, - labelVariant, - labelSx = {}, - unit = "option", - maxWidth = "100%", -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - const [selectAll, setSelectAll] = React.useState(false); - - const [open, setOpen] = React.useState(false); - const enhancedOptions = React.useMemo(() => { - return multiple && isAdorned - ? [ - { - [filteredBy]: t("selectAll"), - isSelectAll: true, - _id: "select_all", - id: "select_all", - }, - ...options, - ] - : options; - }, [multiple, isAdorned, options, filteredBy]); - const isOptionSelected = (option) => { - if (!multiple && !isAdorned) return false; - if (Array.isArray(value)) { - return value.some((item) => (item._id ?? item.id) === (option._id ?? option.id)); - } - return false; - }; - const handleSelectAll = (isSelectAll) => { - const newValue = isSelectAll ? [...options] : []; - handleChange(newValue); - setSelectAll(isSelectAll); - }; - useEffect(() => { - const allSelected = - Array.isArray(value) && Array.isArray(options) && value.length === options.length; - if (selectAll !== allSelected) setSelectAll(allSelected); - }, [value, options]); - return ( - setOpen(true)} - onClose={(event, reason) => { - if (reason === "blur" || reason === "escape") { - setOpen(false); - } - }} - inputValue={inputValue} - onInputChange={(_, newValue) => { - handleInputChange(newValue); - }} - onChange={(_, newValue) => { - if (multiple && isAdorned) { - const hasSelectAllSelected = - Array.isArray(newValue) && newValue.some((item) => item.isSelectAll); - if (hasSelectAllSelected) { - handleSelectAll(!selectAll); - } else { - handleChange(newValue); - setSelectAll(Array.isArray(newValue) && newValue.length === options.length); - } - } else { - handleChange(newValue); - setOpen(false); - } - }} - fullWidth - freeSolo - disabled={disabled} - disableClearable - options={enhancedOptions} - getOptionLabel={(option) => option[filteredBy]} - isOptionEqualToValue={(option, value) => - (option._id ?? option.id) === (value?._id ?? value?.id) - } // Compare by unique identifier - renderInput={(params) => ( - - }), - ...(startAdornment && { startAdornment: startAdornment }), - ...(endAdornment && { endAdornment: endAdornment }), - }, - }} - sx={{}} - /> - {error && ( - - {error} - - )} - - )} - filterOptions={(options, { inputValue }) => { - if (inputValue.trim() === "" && multiple && isAdorned) { - return enhancedOptions; - } - const filtered = options.filter((option) => - option[filteredBy].toLowerCase().includes(inputValue.toLowerCase()) - ); - - if (filtered.length === 0) { - return [ - { - [filteredBy]: t("general.noOptionsFound", { unit: unit }), - noOptions: true, - }, - ]; - } - return filtered; - }} - getOptionKey={(option) => { - return option._id ?? option.id; - }} - renderOption={(props, option) => { - const { key, ...optionProps } = props; - const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined; - const port = option["port"]; - const selected = isOptionSelected(option); - return ( - - {multiple && isAdorned && !option.noOptions && ( - - )} - {option[filteredBy] + - (hasSecondaryLabel - ? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})` - : "")} - - ); - }} - slotProps={{ - popper: { - keepMounted: true, - sx: { - "& ul": { p: 2, backgroundColor: theme.palette.primary.main }, - "& li.MuiAutocomplete-option": { - color: theme.palette.primary.contrastTextSecondary, - px: 4, - borderRadius: theme.shape.borderRadius, - }, - - "& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover": - { - backgroundColor: theme.palette.primary.lowContrast, - color: "red", - }, - "& li.MuiAutocomplete-option:hover:not([aria-selected='true'])": { - color: theme.palette.secondary.contrastText, - backgroundColor: theme.palette.secondary.main, - }, - "& .MuiAutocomplete-noOptions": { - px: theme.spacing(6), - py: theme.spacing(5), - }, - }, - }, - }} - sx={{ - /* height: 34,*/ - "&.MuiAutocomplete-root .MuiAutocomplete-input": { - padding: `0 ${theme.spacing(5)}`, - }, - ...sx, - }} - /> - ); -}; - -Search.propTypes = { - label: PropTypes.string, - id: PropTypes.string, - multiple: PropTypes.bool, - options: PropTypes.array.isRequired, - filteredBy: PropTypes.string.isRequired, - secondaryLabel: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), - inputValue: PropTypes.string.isRequired, - handleInputChange: PropTypes.func.isRequired, - handleChange: PropTypes.func, - isAdorned: PropTypes.bool, - sx: PropTypes.object, - error: PropTypes.string, - disabled: PropTypes.bool, - startAdornment: PropTypes.object, - endAdornment: PropTypes.object, - onBlur: PropTypes.func, - unit: PropTypes.string, -}; - -export default Search; diff --git a/client/src/Components/v1/Inputs/Select/index.css b/client/src/Components/v1/Inputs/Select/index.css deleted file mode 100644 index b40cf8564..000000000 --- a/client/src/Components/v1/Inputs/Select/index.css +++ /dev/null @@ -1,7 +0,0 @@ -.select-wrapper .select-component > .MuiSelect-select { - padding: 0 10px; - min-height: 34px; - display: flex; - align-items: center; - line-height: 1; -} diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx deleted file mode 100644 index 0f96a2ff6..000000000 --- a/client/src/Components/v1/Inputs/Select/index.jsx +++ /dev/null @@ -1,214 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material"; -import Icon from "../../Icon"; -import FieldWrapper from "../FieldWrapper/index.jsx"; - -import "./index.css"; - -/** - * @component - * @param {object} props - * @param {string} props.id - The ID attribute for the select element. - * @param {string} props.placeholder - The label of the select element. - * @param {string} props.placeholder - The placeholder text when no option is selected. - * @param {boolean} props.isHidden - Whether the placeholder should be hidden. - * @param {(string | number | boolean)} props.value - The currently selected value. - * @param {object[]} props.items - The array of items to populate in the select dropdown. - * @param {(string | number | boolean)} props.items._id - The unique identifier of each item. - * @param {string} props.items.name - The display name of each item. - * @param {function} props.onChange - The function to handle onChange event. - * @param {object} props.sx - The custom styles object for MUI Select component. - * @param {number} props.maxWidth - Maximum width in pixels for the select component. Enables responsive text truncation. - * @returns {JSX.Element} - * - * @example - * const frequencies = [ - * { _id: 1, name: "1 minute" }, - * { _id: 2, name: "2 minutes" }, - * { _id: 3, name: "3 minutes" }, - * ]; - * - * setRole([event.target.value])} - items={[ - { _id: "admin", name: t("roles.admin") }, - { _id: "user", name: t("roles.teamMember") }, - ]} - /> - - - } - sx={{ mb: theme.spacing(5) }} - /> - - } - sx={{ mb: theme.spacing(5) }} - /> - - - - - - - - ); -}; -AddTeamMember.propTypes = { - handleIsRegisterOpen: PropTypes.func.isRequired, - isRegisterOpen: PropTypes.bool.isRequired, - onMemberAdded: PropTypes.func.isRequired, -}; - -export default AddTeamMember; diff --git a/client/src/Pages/Account/components/AddTeamMemberDialog.tsx b/client/src/Pages/Account/components/AddTeamMemberDialog.tsx new file mode 100644 index 000000000..14765eb79 --- /dev/null +++ b/client/src/Pages/Account/components/AddTeamMemberDialog.tsx @@ -0,0 +1,186 @@ +import { Stack } from "@mui/material"; +import MenuItem from "@mui/material/MenuItem"; +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; +import { Dialog, TextField, Select } from "@/Components/inputs"; +import { useAddTeamMemberForm } from "@/Hooks/useAddTeamMemberForm"; +import type { AddTeamMemberFormData } from "@/Validation/addTeamMember"; +import type { UserRole, User } from "@/Types/User"; +import { usePost } from "@/Hooks/UseApi"; + +interface AddTeamMemberDialogProps { + open: boolean; + onClose: () => void; + onSuccess?: () => void; +} + +interface RegisterPayload { + firstName: string; + lastName: string; + email: string; + password: string; + role: string[]; +} + +export const AddTeamMemberDialog = ({ + open, + onClose, + onSuccess, +}: AddTeamMemberDialogProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { schema, defaults } = useAddTeamMemberForm(); + const { post, loading } = usePost(); + + const { control, handleSubmit, reset, setError } = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + values: defaults, + }); + + const roleOptions: { value: UserRole; label: string }[] = [ + { value: "admin", label: t("common.auth.roles.admin") }, + { value: "user", label: t("common.auth.roles.user") }, + ]; + + const handleClose = () => { + reset(); + onClose(); + }; + + const onSubmit = async (data: AddTeamMemberFormData) => { + if (loading) return; + + const { confirm, ...userData } = data; + const payload: RegisterPayload = userData; + + const result = await post("/auth/users", payload); + + if (result?.success) { + reset(); + onClose(); + onSuccess?.(); + } else if (result?.msg) { + if (result.msg.toLowerCase().includes("email")) { + setError("email", { message: result.msg }); + } + } + }; + + return ( + + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + ); +}; diff --git a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx deleted file mode 100644 index 725a9b00e..000000000 --- a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useState } from "react"; -import { Button, Stack } from "@mui/material"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog"; -import TextInput from "@/Components/v1/Inputs/TextInput"; -import PasswordTooltip from "@/Pages/Auth/components/PasswordTooltip"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { createToast } from "../../../../Utils/toastUtils"; -import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments"; -import usePasswordFeedback from "@/Pages/Auth/hooks/usePasswordFeedback"; -import PropTypes from "prop-types"; - -const ChangePasswordModal = ({ isSaving, isLoading, changePassword }) => { - const INITIAL_FORM_STATE = { - password: "", - confirm: "", - }; - const theme = useTheme(); - const { t } = useTranslation(); - const { feedback, handlePasswordFeedback } = usePasswordFeedback(); - const [form, setForm] = useState(INITIAL_FORM_STATE); - const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false); - const [errors, setErrors] = useState({}); - const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); - const closeChangePasswordModal = () => { - setIsChangePasswordOpen(false); - setForm(INITIAL_FORM_STATE); - }; - - const onChange = (e) => { - let { name, value } = e.target; - const updatedForm = { ...form, [name]: value }; - setForm(updatedForm); - - handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors); - }; - const isFormValid = - form.password.length > 1 && - form.confirm.length > 1 && - !errors.password && - !errors.confirm; - const onsubmitChangePassword = async (event) => { - event.preventDefault(); - if (!isFormValid) return; - const newPasswordForm = { - password: form.password, - }; - try { - setIsLoadingSubmit(true); - await changePassword(newPasswordForm); - closeChangePasswordModal(); - } catch (error) { - const errorMsg = error.response?.data?.msg || error.message || "unknownError"; - createToast({ - type: "error", - body: t(errorMsg), - }); - } finally { - setIsLoadingSubmit(false); - } - }; - - return ( - <> - - - - } - sx={{ mb: theme.spacing(5) }} - /> - - } - sx={{ mb: theme.spacing(5) }} - /> - - - - - - - - ); -}; - -ChangePasswordModal.propTypes = { - isSaving: PropTypes.bool.isRequired, - isLoading: PropTypes.bool.isRequired, - changePassword: PropTypes.func.isRequired, -}; - -export default ChangePasswordModal; diff --git a/client/src/Pages/Account/components/HeaderTeamControls.tsx b/client/src/Pages/Account/components/HeaderTeamControls.tsx new file mode 100644 index 000000000..51fea25b6 --- /dev/null +++ b/client/src/Pages/Account/components/HeaderTeamControls.tsx @@ -0,0 +1,80 @@ +import Stack from "@mui/material/Stack"; +import MenuItem from "@mui/material/MenuItem"; +import { Button, Select } from "@/Components/inputs"; +import { Icon } from "@/Components/design-elements"; +import { UserPlus, Mail } from "lucide-react"; + +import { UserRoles } from "@/Types/User"; +import { useTheme } from "@mui/material/styles"; +import type { UserRole } from "@/Types/User"; +import { useTranslation } from "react-i18next"; +import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; + +interface HeaderTeamControlsProps { + filter: UserRole | ""; + onFilterChange: (value: UserRole | "") => void; + onInviteClick?: () => void; + onAddMemberClick?: () => void; +} + +export const HeaderTeamControls = ({ + filter, + onFilterChange, + onInviteClick, + onAddMemberClick, +}: HeaderTeamControlsProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const isAdmin = useIsAdmin(); + const isSuperAdmin = useIsSuperAdmin(); + + const handleFilterChange = (event: { target: { value: unknown } }) => { + onFilterChange(event.target.value as UserRole | ""); + }; + + return ( + + + {isAdmin && ( + + )} + {isSuperAdmin && ( + + )} + + ); +}; diff --git a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx new file mode 100644 index 000000000..e7c42a1ac --- /dev/null +++ b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx @@ -0,0 +1,163 @@ +import { Stack } from "@mui/material"; +import MenuItem from "@mui/material/MenuItem"; +import { useTheme, FormHelperText, Typography } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useForm, Controller } from "react-hook-form"; +import { Dialog, TextField, Select, Button } from "@/Components/inputs"; +import type { UserRole } from "@/Types/User"; +import { useInviteForm } from "@/Hooks/useInviteForm"; +import type { InviteFormData } from "@/Validation/invite"; +import { usePost } from "@/Hooks/UseApi"; + +const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST; + +interface InviteResponse { + token: string; +} + +interface InviteTeamMemberDialogProps { + open: boolean; + onClose: () => void; +} + +export const InviteTeamMemberDialog = ({ + open, + onClose, +}: InviteTeamMemberDialogProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { resolver, defaults } = useInviteForm(); + + const { control, handleSubmit, reset } = useForm({ + resolver, + defaultValues: defaults, + values: defaults, + }); + + const { post: generateToken, loading: generateLoading } = usePost< + InviteFormData, + InviteResponse + >(); + const { post: sendInvite, loading: sendLoading } = usePost< + InviteFormData, + InviteResponse + >(); + const [inviteLink, setInviteLink] = useState(null); + + const roleOptions: { value: UserRole; label: string }[] = [ + { value: "admin", label: t("common.auth.roles.admin") }, + { value: "user", label: t("common.auth.roles.user") }, + ]; + + const handleGenerateToken = async (data: InviteFormData) => { + const result = await generateToken("/invite", data); + if (result?.data?.token) { + const token = result.data.token; + const link = CLIENT_HOST ? `${CLIENT_HOST}/register/${token}` : token; + setInviteLink(link); + } + }; + + const handleSendInvite = async (data: InviteFormData) => { + const result = await sendInvite("/invite/send", data); + if (result?.success) { + handleClose(); + } + }; + + const handleClose = () => { + reset(); + setInviteLink(null); + onClose(); + }; + + return ( + + {t("common.buttons.generateToken")} + + } + > + + ( + + )} + /> + ( + <> + + {fieldState.error && ( + {fieldState.error.message} + )} + + )} + /> + {inviteLink && ( + <> + + {t("pages.account.team.invite.linkLabel")} + + + + )} + + + ); +}; diff --git a/client/src/Pages/Account/components/PasswordPanel.jsx b/client/src/Pages/Account/components/PasswordPanel.jsx deleted file mode 100644 index e889db96c..000000000 --- a/client/src/Pages/Account/components/PasswordPanel.jsx +++ /dev/null @@ -1,281 +0,0 @@ -import TabPanel from "@mui/lab/TabPanel"; -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography, Button } from "@mui/material"; -import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { newOrChangedCredentials } from "../../../Validation/validation.js"; -import Alert from "@/Components/v1/Alert/index.jsx"; -import { update } from "../../../Features/Auth/authSlice.js"; -import { useDispatch, useSelector } from "react-redux"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { getTouchedFieldErrors } from "../../../Validation/error.js"; -import { useTranslation } from "react-i18next"; - -const defaultPasswordsState = { - password: "", - newPassword: "", - confirm: "", -}; - -/** - * PasswordPanel component manages the form for editing password. - * - * @returns {JSX.Element} - */ - -const PasswordPanel = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const SPACING_GAP = theme.spacing(12); - - //redux state - const { isLoading } = useSelector((state) => state.auth); - - const idToName = { - "edit-current-password": "password", - "edit-new-password": "newPassword", - "edit-confirm-password": "confirm", - }; - - const [localData, setLocalData] = useState(defaultPasswordsState); - const [errors, setErrors] = useState(defaultPasswordsState); - const [touchedFields, setTouchedFields] = useState({ - password: false, - newPassword: false, - confirm: false, - }); - - const handleChange = (event) => { - const { value, id } = event.target; - const name = idToName[id]; - - const updatedData = { - ...localData, - [name]: value, - }; - const updatedTouchedFields = { - ...touchedFields, - [name]: true, - }; - - const validation = newOrChangedCredentials.validate( - { ...updatedData }, - { abortEarly: false, context: { password: updatedData.newPassword } } - ); - - const updatedErrors = getTouchedFieldErrors(validation, updatedTouchedFields); - - if (!touchedFields[name]) { - setTouchedFields(updatedTouchedFields); - } - - setLocalData(updatedData); - setErrors(updatedErrors); - }; - - const handleSubmit = async (event) => { - event.preventDefault(); - - const { error } = newOrChangedCredentials.validate(localData, { - abortEarly: false, - context: { password: localData.newPassword }, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - } else { - const action = await dispatch(update({ localData })); - if (action.payload.success) { - createToast({ - body: t( - "passwordPanel.passwordChangedSuccess", - "Your password was changed successfully." - ), - }); - setLocalData({ - password: "", - newPassword: "", - confirm: "", - }); - } else { - // TODO: Check for other errors? - createToast({ - body: t( - "passwordPanel.passwordInputIncorrect", - "Your password input was incorrect." - ), - }); - setErrors({ password: "*" + action.payload.msg + "." }); - } - } - }; - - return ( - - - - - ); -}; - -PasswordPanel.propTypes = { - // No props are being passed to this component, hence no specific PropTypes are defined. -}; - -export default PasswordPanel; diff --git a/client/src/Pages/Account/components/ProfilePanel.jsx b/client/src/Pages/Account/components/ProfilePanel.jsx deleted file mode 100644 index dfe508879..000000000 --- a/client/src/Pages/Account/components/ProfilePanel.jsx +++ /dev/null @@ -1,437 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { useState } from "react"; -import TabPanel from "@mui/lab/TabPanel"; -import { Box, Button, Divider, Stack, Typography } from "@mui/material"; -import Avatar from "@/Components/v1/Avatar/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; -import { newOrChangedCredentials } from "../../../Validation/validation.js"; -import { useDispatch, useSelector } from "react-redux"; -import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { logger } from "../../../Utils/Logger.js"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import { useTranslation } from "react-i18next"; - -/** - * ProfilePanel component displays a form for editing user profile information - * and allows for actions like updating profile picture, credentials, - * and deleting account. - * - * @returns {JSX.Element} - */ - -const ProfilePanel = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const SPACING_GAP = theme.spacing(12); - - //redux state - const { user, isLoading } = useSelector((state) => state.auth); - - const idToName = { - "edit-first-name": "firstName", - "edit-last-name": "lastName", - // Disabled for now, will revisit in the future - // "edit-email": "email", - }; - - // Local state for form data, errors, and file handling - const [localData, setLocalData] = useState({ - firstName: user.firstName, - lastName: user.lastName, - // email: user.email, // Disabled for now - }); - const [errors, setErrors] = useState({}); - const [file, setFile] = useState(); - - // Handles input field changes and performs validation - const handleChange = (event) => { - errors["unchanged"] && clearError("unchanged"); - const { value, id } = event.target; - const name = idToName[id]; - setLocalData((prev) => ({ - ...prev, - [name]: value, - })); - - validateField({ [name]: value }, newOrChangedCredentials, name); - }; - - // Validates input against provided schema and updates error state - const validateField = (toValidate, schema, name = "picture") => { - const { error } = schema.validate(toValidate, { abortEarly: false }); - setErrors((prev) => { - const prevErrors = { ...prev }; - if (error) prevErrors[name] = error.details[0].message; - else delete prevErrors[name]; - return prevErrors; - }); - if (error) return true; - }; - - // Clears specific error from errors state - const clearError = (err) => { - setErrors((prev) => { - const updatedErrors = { ...prev }; - if (updatedErrors[err]) delete updatedErrors[err]; - return updatedErrors; - }); - }; - - // Resets picture-related states and clears interval - const removePicture = () => { - errors["picture"] && clearError("picture"); - setFile(undefined); - setLocalData((prev) => ({ - ...prev, - file: undefined, - deleteProfileImage: true, - })); - }; - - // Opens the picture update modal - const openPictureModal = () => { - setIsOpen("picture"); - setFile(undefined); - }; - - // Closes the picture update modal and resets related states - const closePictureModal = () => { - if (errors["picture"]) clearError("picture"); - setFile(undefined); - setIsOpen(""); - }; - - // Updates profile image displayed on UI - const handleUpdatePicture = () => { - setLocalData((prev) => ({ - ...prev, - file: file?.src, - deleteProfileImage: false, - })); - setIsOpen(""); - if (errors["unchanged"]) clearError("unchanged"); - }; - - // Handles form submission to update user profile - const handleSaveProfile = async (event) => { - event.preventDefault(); - const nameChanged = - localData.firstName !== user.firstName || localData.lastName !== user.lastName; - - const avatarChanged = - localData.deleteProfileImage === true || - (localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`); - - if (!nameChanged && !avatarChanged) { - createToast({ - body: "Unable to update profile — no changes detected.", - }); - setErrors({ unchanged: "unable to update profile" }); - return; - } - - const action = await dispatch(update({ localData })); - if (action.payload.success) { - createToast({ - body: "Your profile data was changed successfully.", - }); - } else { - createToast({ - body: "There was an error updating your profile data.", - }); - } - }; - - // Removes current profile image from UI - const handleDeletePicture = () => { - setLocalData((prev) => ({ - ...prev, - deleteProfileImage: true, - })); - setFile(undefined); - errors["unchanged"] && clearError("unchanged"); - }; - - // Initiates the account deletion process - const handleDeleteAccount = async () => { - const action = await dispatch(deleteUser()); - if (action.payload.success) { - dispatch(clearAuthState()); - } else { - if (action.payload) { - // dispatch errors - createToast({ - body: action.payload.msg, - }); - } else { - // unknown errors - createToast({ - body: "Unknown error.", - }); - } - } - }; - - // Modal state and control functions - const [isOpen, setIsOpen] = useState(""); - const isModalOpen = (name) => isOpen === name; - - return ( - - - - {/* This 0.9 is a bit magic numbering, refactor */} - - {t("FirstName")} - - - - - - {t("LastName")} - - - - - - {t("auth.common.inputs.email.label")} - - {t("EmailDescriptionText")} - - - logger.warn("disabled")} - disabled={true} - flex={1} - /> - - - - {t("YourPhoto")} - - {t("PhotoDescriptionText")} - - - - - - - - - - - ); -}; - -ProfilePanel.propTypes = { - // No props are being passed to this component, hence no specific PropTypes are defined. -}; - -export default ProfilePanel; diff --git a/client/src/Pages/Account/components/RoleTable/index.jsx b/client/src/Pages/Account/components/RoleTable/index.jsx deleted file mode 100644 index 90a30dcd4..000000000 --- a/client/src/Pages/Account/components/RoleTable/index.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import Typography from "@mui/material/Typography"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import Icon from "@/Components/v1/Icon"; - -import { useTranslation } from "react-i18next"; -import { ROLES } from "../../../../Utils/roleUtils.js"; - -const RoleTable = ({ roles, handleDeleteRole }) => { - const { t } = useTranslation(); - const HEADERS = [ - { - id: "name", - content: {t("editUserPage.table.roleHeader")}, - render: (row) => { - return row; - }, - }, - { - id: "delete", - content: {t("editUserPage.table.actionHeader")}, - render: (row) => { - if (row === ROLES.SUPERADMIN) return null; - return ( - { - handleDeleteRole(row); - }} - style={{ cursor: "pointer" }} - /> - ); - }, - }, - ]; - return ( - - ); -}; - -export default RoleTable; diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx deleted file mode 100644 index 6c658cc60..000000000 --- a/client/src/Pages/Account/components/TeamPanel.jsx +++ /dev/null @@ -1,360 +0,0 @@ -import { useTheme } from "@emotion/react"; -import TabPanel from "@mui/lab/TabPanel"; -import { Button, ButtonGroup, Stack, Typography } from "@mui/material"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { newOrChangedCredentials } from "../../../Validation/validation.js"; -import { networkService } from "../../../main.jsx"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import AddTeamMember from "./AddTeamMember/index.jsx"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import { useGetInviteToken } from "../../../Hooks/inviteHooks.js"; -import { useNavigate } from "react-router-dom"; -import { useIsSuperAdmin } from "@/Hooks/useIsAdmin.js"; -import AddMemberMenu from "./AddMemberMenu/index.jsx"; -/** - * TeamPanel component manages the organization and team members, - * providing functionalities like renaming the organization, managing team members, - * and inviting new members. - * - * @returns {JSX.Element} - */ - -const TeamPanel = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const SPACING_GAP = theme.spacing(12); - - const [toInvite, setToInvite] = useState({ - email: "", - role: ["user"], - }); - const [data, setData] = useState([]); - const [members, setMembers] = useState([]); - const [filter, setFilter] = useState("all"); - const [isDisabled, setIsDisabled] = useState(true); - const [errors, setErrors] = useState({}); - const [isSendingInvite, setIsSendingInvite] = useState(false); - const navigate = useNavigate(); - const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken(); - const isSuperAdmin = useIsSuperAdmin(); - - const headers = [ - { - id: "name", - content: t("teamPanel.table.name"), - render: (row) => { - return ( - - - {row.firstName + " " + row.lastName} - - - {t("teamPanel.table.created")}{" "} - {new Date(row.createdAt).toLocaleDateString()} - - - ); - }, - }, - { id: "email", content: t("teamPanel.table.email"), render: (row) => row.email }, - { - id: "role", - content: t("teamPanel.table.role"), - render: (row) => row.role, - }, - ]; - const [refreshTrigger, setRefreshTrigger] = useState(false); - const refreshTeamList = () => { - setRefreshTrigger((prev) => !prev); - }; - useEffect(() => { - const fetchTeam = async () => { - try { - const response = await networkService.getAllUsers(); - setMembers(response.data.data); - } catch (error) { - createToast({ - body: error.message || "Error fetching team members.", - }); - } - }; - - fetchTeam(); - }, [refreshTrigger]); - - useEffect(() => { - const ROLE_MAP = { - superadmin: t("roles.superAdmin"), - admin: t("roles.admin"), - user: t("roles.teamMember"), - demo: t("roles.demoUser"), - }; - let team = members; - if (filter !== "all") - team = members.filter((member) => { - if (filter === "admin") { - return member.role.includes("admin") || member.role.includes("superadmin"); - } - return member.role.includes(filter); - }); - - team = team.map((member) => ({ - ...member, - id: member.id, - role: member.role.map((role) => ROLE_MAP[role]).join(","), - })); - setData(team); - }, [members, filter, t]); - - useEffect(() => { - setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === ""); - }, [errors, toInvite.email]); - const [isOpen, setIsOpen] = useState(false); - const [isRegisterOpen, setIsRegisterOpen] = useState(false); - const handleIsRegisterOpen = (open) => { - setIsRegisterOpen(open); - }; - - const handleChange = (event) => { - const { value } = event.target; - const newEmail = value?.toLowerCase() || value; - setToInvite((prev) => ({ - ...prev, - email: newEmail, - })); - - const validation = newOrChangedCredentials.validate( - { email: newEmail }, - { abortEarly: false } - ); - - setErrors((prev) => { - const updatedErrors = { ...prev }; - - if (validation.error) { - updatedErrors.email = validation.error.details[0].message; - } else { - delete updatedErrors.email; - } - return updatedErrors; - }); - }; - - const handleGetToken = async () => { - await getInviteToken({ email: toInvite.email, role: toInvite.role }); - }; - const handleInviteMember = async () => { - if (!toInvite.email) { - setErrors((prev) => ({ ...prev, email: "Email is required." })); - return; - } - setIsSendingInvite(true); - if (!toInvite.role.includes("user") || !toInvite.role.includes("admin")) - setToInvite((prev) => ({ ...prev, role: ["user"] })); - - const { error } = newOrChangedCredentials.validate( - { email: toInvite.email }, - { - abortEarly: false, - } - ); - - if (error) { - setErrors((prev) => ({ ...prev, email: error.details[0].message })); - return; - } - - try { - await networkService.sendInvitationToken({ - email: toInvite.email, - role: toInvite.role, - }); - closeInviteModal(); - createToast({ - body: "Member invited. They will receive an email with details on how to create their account.", - }); - } catch (error) { - createToast({ - body: error?.response?.data?.msg || error.message || "Unknown error.", - }); - } finally { - setIsSendingInvite(false); - } - }; - - const closeInviteModal = () => { - setIsOpen(false); - clearToken(); - setToInvite({ email: "", role: ["0"] }); - setErrors({}); - }; - - return ( - - - {t("teamPanel.teamMembers")} - - - - - - - - - - - setIsOpen(true)} - handleIsRegisterOpen={handleIsRegisterOpen} - /> - - - { - if (isSuperAdmin) { - navigate(`/account/team/${row.id}`); - } - }, - }} - /> - - - - setSelectedMonitor(e.target.value)} - items={monitorNames} - sx={{ - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastTextSecondary, - }} - maxWidth={250} - /> - - - - {t("incidentsOptionsHeaderFilterBy")} - - { + setSelectedMonitor(e.target.value); + setPage(0); + }} + > + {t("pages.checks.selects.monitor.all")} + {monitorsResponse?.map((monitor) => ( + + {monitor.name} + + ))} + + + + + + + + + ); +}; + +export default Checks; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx new file mode 100644 index 000000000..5012c1154 --- /dev/null +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -0,0 +1,851 @@ +import { useMemo, useState } from "react"; +import { useEffect } from "react"; +import { useParams, useLocation, useNavigate } from "react-router"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTheme } from "@mui/material"; +import Stack from "@mui/material/Stack"; +import RadioGroup from "@mui/material/RadioGroup"; +import FormControl from "@mui/material/FormControl"; +import { Trans, useTranslation } from "react-i18next"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import Link from "@mui/material/Link"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import { Trash2 } from "lucide-react"; +import { HeaderDeleteControls } from "@/Components/monitors"; + +import { BasePage, ConfigBox } from "@/Components/design-elements"; +import { + RadioWithDescription, + Button, + TextField, + Select, + Autocomplete, + SwitchComponent as Switch, + SliderWithLabel, + Dialog, +} from "@/Components/inputs"; +import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; +import { useMonitorForm } from "@/Hooks/useMonitorForm"; +import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor"; +import type { Notification } from "@/Types/Notification"; +import type { MonitorFormData } from "@/Validation/monitor"; + +interface GeneralSettingsConfig { + urlLabel: string; + urlPlaceholder: string; + namePlaceholder: string; + showUrl: boolean; + showPort: boolean; + showGameSelect: boolean; + showSecret: boolean; +} + +const getGeneralSettingsConfig = ( + type: MonitorType, + t: (key: string) => string +): GeneralSettingsConfig => { + const configs: Record = { + http: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: false, + }, + ping: { + urlLabel: t("pages.createMonitor.form.general.option.host.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.host.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: false, + }, + docker: { + urlLabel: t("pages.createMonitor.form.general.option.container.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.container.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: false, + }, + port: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: true, + showGameSelect: false, + showSecret: false, + }, + game: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: true, + showGameSelect: true, + showSecret: false, + }, + pagespeed: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: false, + }, + hardware: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: true, + }, + }; + return configs[type] || configs.http; +}; + +const CreateMonitorPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const { monitorId } = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + const isEditMode = Boolean(monitorId); + + // Extract page type from URL path (e.g., /pagespeed/create -> pagespeed) + const pageType = useMemo(() => { + const pathSegments = location.pathname.split("/").filter(Boolean); + const firstSegment = pathSegments[0]; + if (firstSegment === "pagespeed") return "pagespeed"; + if (firstSegment === "infrastructure") return "hardware"; + return "uptime"; + }, [location.pathname]); + + const showTypeSelector = pageType === "uptime" && !isEditMode; + const defaultType: MonitorType = + pageType === "pagespeed" + ? "pagespeed" + : pageType === "hardware" + ? "hardware" + : "http"; + + const { data: existingMonitor, refetch: refetchMonitor } = useGet( + isEditMode ? `/monitors/${monitorId}` : null + ); + + const { data: notifications } = useGet("/notifications/team"); + const { data: games } = useGet("/monitors/games"); + + const { schema, defaults } = useMonitorForm({ + data: existingMonitor ?? null, + defaultType, + }); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + const { control, watch, handleSubmit, clearErrors } = form; + + useEffect(() => { + form.reset(defaults); + }, [defaults, form]); + + const watchedType = watch("type") as MonitorType; + + const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean; + + useEffect(() => { + clearErrors(); + }, [watchedType, clearErrors]); + + const generalSettingsConfig = useMemo( + () => getGeneralSettingsConfig(watchedType, t), + [watchedType, t] + ); + + const { post, loading: isCreating } = usePost(); + const { patch, loading: isUpdating } = usePatch(); + const isSubmitting = isCreating || isUpdating; + // Delete functionality + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const { deleteFn, loading: isDeleting } = useDelete(); + + const handleDeleteClick = () => { + setIsDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = async () => { + if (!monitorId) return; + await deleteFn(`/monitors/${monitorId}`); + setIsDeleteDialogOpen(false); + // Navigate based on page type + if (pageType === "pagespeed") { + navigate("/pagespeed"); + } else if (pageType === "hardware") { + navigate("/infrastructure"); + } else { + navigate("/uptime"); + } + }; + + const handleDeleteCancel = () => { + setIsDeleteDialogOpen(false); + }; + + const onSubmit = async (data: MonitorFormData) => { + let result; + if (isEditMode && monitorId) { + result = await patch(`/monitors/${monitorId}`, data); + } else { + result = await post("/monitors", data); + } + + if (result?.success) { + if (pageType === "pagespeed") { + navigate("/pagespeed"); + } else if (pageType === "hardware") { + navigate("/infrastructure"); + } else { + navigate("/uptime"); + } + } + }; + + const onError = (errors: unknown) => { + console.log(errors); + }; + + return ( + + + {/* Monitor Type Selection - only shown for uptime monitors */} + {showTypeSelector && ( + ( + + + + + + + + + + )} + /> + } + /> + )} + + + {/* URL/Host/Container field - not shown for hardware */} + {generalSettingsConfig.showUrl && ( + ( + + )} + /> + )} + + {/* Port field - only for port and game types */} + {generalSettingsConfig.showPort && ( + ( + { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} + type="number" + fieldLabel={t("pages.createMonitor.form.general.option.port.label")} + placeholder={t( + "pages.createMonitor.form.general.option.port.placeholder" + )} + fullWidth + error={!!fieldState.error} + helperText={fieldState.error?.message ?? ""} + /> + )} + /> + )} + + {/* Game select - only for game type */} + {generalSettingsConfig.showGameSelect && ( + ( + + )} + /> + )} + + {/* Secret field - only for hardware type */} + {generalSettingsConfig.showSecret && ( + ( + + )} + /> + )} + + ( + + )} + /> + + } + /> + + ( + + )} + /> + } + /> + + {/* Alert Thresholds - only for hardware type */} + {generalSettingsConfig.showSecret && ( + + ( + `${value}%`} + /> + )} + /> + ( + `${value}%`} + /> + )} + /> + ( + `${value}%`} + /> + )} + /> + ( + `${value}°C`} + /> + )} + /> + + } + /> + )} + + + ( + + )} + /> + ( + + )} + /> + + } + /> + + { + // Map notifications to have 'name' property for Autocomplete + const notificationOptions = (notifications ?? []).map((n) => ({ + ...n, + name: n.notificationName, + })); + const selectedNotifications = notificationOptions.filter((n) => + (field.value ?? []).includes(n.id) + ); + return ( + + option.name} + onChange={(_: unknown, newValue: typeof notificationOptions) => { + field.onChange(newValue.map((n) => n.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + /> + {selectedNotifications.length > 0 && ( + + {selectedNotifications.map((notification, index) => ( + + + {notification.notificationName} + + { + field.onChange( + (field.value ?? []).filter( + (id: string) => id !== notification.id + ) + ); + }} + aria-label="Remove notification" + > + + + {index < selectedNotifications.length - 1 && } + + ))} + + )} + + ); + }} + /> + } + /> + + {watchedType === "http" && ( + ( + + field.onChange(e.target.checked)} + /> + + {t("pages.createMonitor.form.ignoreTls.option.tls.label")} + + + )} + /> + } + /> + )} + + {watchedType === "http" && ( + + ( + + field.onChange(e.target.checked)} + /> + + {t( + "pages.createMonitor.form.advanced.option.advancedMatching.label" + )} + + + )} + /> + {watchedUseAdvancedMatching && ( + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + ), + }} + /> + + + )} + + } + /> + )} + + + + + + + ); +}; + +export default CreateMonitorPage; diff --git a/client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx b/client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx deleted file mode 100644 index fda257695..000000000 --- a/client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import PropTypes from "prop-types"; -import { Box, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx"; -import Icon from "@/Components/v1/Icon"; -import SummaryCard from "../SummaryCard/index.jsx"; - -/** - * ActiveIncidentsPanel Component - * - * Displays a quick overview of currently active incidents. - * Shows only essential information: monitor name, status badge, and duration. - * - * @param {Object} props - * @param {Array} props.incidents - Array of active incidents - * @param {number} props.totalCount - Total count of active incidents (for badge) - * @param {boolean} props.isLoading - Loading state - * @param {Object} props.error - Error object if any - * @param {Function} props.onIncidentClick - Callback when incident is clicked (optional) - */ -const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - if (isLoading) { - return ; - } - - if (error) { - return ( - - - {t("incidentsPage.incidentsActivePanelError")} - - - ); - } - - if (!totalCount || totalCount === 0) { - return ( - - - - - - - - {t("incidentsPage.allSystemsAreOperational")} - - - - ); - } - - return ( - - - - - - - - {totalCount} - - - - ); -}; - -ActiveIncidentsPanel.propTypes = { - totalCount: PropTypes.number, - isLoading: PropTypes.bool, - error: PropTypes.object, - onIncidentClick: PropTypes.func, -}; - -export default ActiveIncidentsPanel; diff --git a/client/src/Pages/Incidents/Components/CardDetails.tsx b/client/src/Pages/Incidents/Components/CardDetails.tsx new file mode 100644 index 000000000..70f3bc6e3 --- /dev/null +++ b/client/src/Pages/Incidents/Components/CardDetails.tsx @@ -0,0 +1,195 @@ +import Stack from "@mui/material/Stack"; +import Grid from "@mui/material/Grid"; +import Divider from "@mui/material/Divider"; +import Typography from "@mui/material/Typography"; +import { BaseBox, ValueLabel } from "@/Components/design-elements"; + +import { useTranslation } from "react-i18next"; +import type { Incident } from "@/Types/Incident"; +import type { Monitor } from "@/Types/Monitor"; +import { useTheme } from "@mui/material"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import { getIncidentsDuration } from "@/Pages/Incidents/utils"; + +interface CardDetailsProps { + incident: Incident | null; + monitor: Monitor | null; + sx?: object; +} + +export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + if (!incident) { + return null; + } + return ( + + + {t("pages.incidents.dialog.details.title")} + + + + + {t("pages.incidents.dialog.details.overview")} + + + + + {t("pages.incidents.dialog.details.status")} + + + + {monitor && ( + <> + {t("pages.incidents.dialog.details.monitor")} + + {monitor.name ?? "N/A"} + + + {t("pages.incidents.dialog.details.url")} + + + {monitor.url ?? "N/A"} + + + )} + + + + + + + {t("pages.incidents.dialog.details.analysis")} + + + + + {t("pages.incidents.dialog.details.timeline")} + + + {t("pages.incidents.dialog.details.detailsLabel")} + + + + + + + + + {t("pages.incidents.dialog.details.startedAt")} + + + + {formatDateWithTz(incident.startTime, "D MMM YYYY, h:mm A", uiTimezone)} + + + + {t("pages.incidents.dialog.details.statusCode")} + + + {incident.statusCode ?? "N/A"} + + + {t("pages.incidents.dialog.details.downtime")} + + + {getIncidentsDuration(incident)} + + + {t("pages.incidents.dialog.details.message")} + + + {incident.message ?? "N/A"} + + + + + {!incident.status && ( + + + + {t("pages.incidents.dialog.details.resolutionDetails")} + + + + + {t("pages.incidents.dialog.details.resolvedAt")} + + + + {incident.endTime + ? formatDateWithTz(incident.endTime, "D MMM YYYY, h:mm A", uiTimezone) + : "N/A"} + + + + + {t("pages.incidents.dialog.details.resolutionType")} + + + + + {incident.resolutionType + ? t( + `pages.incidents.dialog.details.resolutionTypes.${incident.resolutionType}` + ) + : "N/A"} + + + {incident.resolvedBy && ( + <> + + + {t("pages.incidents.dialog.details.resolvedBy")} + + + + + {incident.resolvedByEmail ?? incident.resolvedBy} + + + + )} + {incident.comment && ( + <> + + {t("pages.incidents.dialog.details.comment")} + + + {incident.comment} + + + )} + + + + )} + + ); +}; diff --git a/client/src/Pages/Incidents/Components/CardSummary.tsx b/client/src/Pages/Incidents/Components/CardSummary.tsx new file mode 100644 index 000000000..938455808 --- /dev/null +++ b/client/src/Pages/Incidents/Components/CardSummary.tsx @@ -0,0 +1,247 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import Grid from "@mui/material/Grid"; +import { BaseBox, ValueLabel } from "@/Components/design-elements"; +import { CircleCheck, TriangleAlert, Bell, Wrench, Globe } from "lucide-react"; +import Box from "@mui/material/Box"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material"; +import type { IncidentSummary, IncidentSummaryItem } from "@/Types/Incident"; +import { getIncidentsDuration } from "@/Pages/Incidents/utils"; + +interface SummaryItemProps { + icon: React.ReactNode; + label: string; + value: string | number; +} + +const SummaryItem = ({ icon, label, value }: SummaryItemProps) => { + const theme = useTheme(); + return ( + + + {icon} + {label} + + + {value} + + + ); +}; + +interface SummaryCardProps { + title: string; + sx?: React.CSSProperties; +} + +export const SummaryCard = ({ + title, + sx, + children, +}: React.PropsWithChildren) => { + const theme = useTheme(); + return ( + + + + {title} + + + {children} + + + ); +}; + +interface SummaryCardActiveIncidentsProps { + summary?: IncidentSummary | null; +} + +export const SummaryCardActiveIncidents = ({ + summary, +}: SummaryCardActiveIncidentsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + + if (!summary) return null; + + const activeCount = summary.totalActive; + const hasActive = activeCount > 0; + const color = hasActive ? theme.palette.error.main : theme.palette.success.main; + const icon = hasActive ? ( + + ) : ( + + ); + const msg = t("pages.incidents.summaryCard.activeIncidents.active", { + count: activeCount, + }); + + return ( + + + {icon} + {msg} + + + ); +}; + +const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const duration = getIncidentsDuration(incident); + return ( + + + + + {incident.monitorName ?? t("common.labels.na")} + + + + + + + + + {duration} + + + ); +}; + +interface SummaryCardLatestIncidentsProps { + summary?: IncidentSummary | null; +} + +export const SummaryCardLatestIncidents = ({ + summary, +}: SummaryCardLatestIncidentsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const latestIncidents = summary?.latestIncidents ?? []; + + return ( + + + {latestIncidents.slice(0, 3).map((incident, index) => ( + + + {index < latestIncidents.length - 1 && ( + + )} + + ))} + + + ); +}; + +interface SummaryCardStatsProps { + summary?: IncidentSummary | null; +} + +export const SummaryCardStats = ({ summary }: SummaryCardStatsProps) => { + const { t } = useTranslation(); + if (!summary) return null; + const mostAffected = + !summary.total || summary.total === 0 + ? t("common.labels.na") + : summary.topMonitor?.monitorName || t("common.labels.na"); + return ( + + } + label={t("pages.incidents.summaryCard.incidentStats.totalIncidents")} + value={summary?.total || 0} + /> + } + label={t("pages.incidents.summaryCard.incidentStats.mostAffectedMonitor")} + value={mostAffected} + /> + } + label={t("pages.incidents.summaryCard.incidentStats.avgResolutionTime")} + value={ + summary.total > 0 + ? `${summary.avgResolutionTimeHours || 0} hours` + : t("common.labels.na") + } + /> + + ); +}; diff --git a/client/src/Pages/Incidents/Components/ControlsIncidentFilter.tsx b/client/src/Pages/Incidents/Components/ControlsIncidentFilter.tsx new file mode 100644 index 000000000..a0f4eb15a --- /dev/null +++ b/client/src/Pages/Incidents/Components/ControlsIncidentFilter.tsx @@ -0,0 +1,83 @@ +import Stack from "@mui/material/Stack"; +import { Select, Button } from "@/Components/inputs"; +import MenuItem from "@mui/material/MenuItem"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { Typography, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import type { Monitor } from "@/Types/Monitor"; + +const resolutionTypes = ["all", "manual", "automatic"]; + +interface ControlsIncidentFilterProps { + monitors?: Monitor[]; + selectedMonitor: string; + setSelectedMonitor: React.Dispatch>; + selectedResolutionType: string; + setSelectedResolutionType: React.Dispatch>; + onClearFilters: () => void; +} + +export const ControlsIncidentFilter = ({ + monitors, + selectedMonitor, + setSelectedMonitor, + selectedResolutionType, + setSelectedResolutionType, + onClearFilters, +}: ControlsIncidentFilterProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const isFilterActive = + selectedMonitor !== "0" || + (selectedResolutionType !== "" && selectedResolutionType !== "all"); + + return ( + + + + {isFilterActive && ( + + )} + + ); +}; diff --git a/client/src/Pages/Incidents/Components/DialogIncidentDetails.tsx b/client/src/Pages/Incidents/Components/DialogIncidentDetails.tsx new file mode 100644 index 000000000..628f6cb29 --- /dev/null +++ b/client/src/Pages/Incidents/Components/DialogIncidentDetails.tsx @@ -0,0 +1,43 @@ +import { Dialog } from "@/Components/inputs"; +import { CardDetails } from "@/Pages/Incidents/Components/CardDetails"; + +import { useTranslation } from "react-i18next"; +import type { Incident } from "@/Types/Incident"; +import type { Monitor } from "@/Types/Monitor"; + +interface DialogIncidentDetailsProps { + open: boolean; + incident: Incident | null; + monitor: Monitor | null; + onClose: () => void; + onResolve: () => void; +} + +export const DialogIncidentDetails = ({ + open, + incident, + monitor, + onClose, + onResolve, +}: DialogIncidentDetailsProps) => { + const { t } = useTranslation(); + + const isActive = incident?.status === true; + + return ( + + + + ); +}; diff --git a/client/src/Pages/Incidents/Components/DialogResolution.tsx b/client/src/Pages/Incidents/Components/DialogResolution.tsx new file mode 100644 index 000000000..99dd898dd --- /dev/null +++ b/client/src/Pages/Incidents/Components/DialogResolution.tsx @@ -0,0 +1,71 @@ +import { useState, useEffect } from "react"; +import Box from "@mui/material/Box"; +import { Dialog, TextField } from "@/Components/inputs"; +import { usePut } from "@/Hooks/UseApi"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material"; + +interface DialogResolutionProps { + open: boolean; + incidentId: string | null; + onClose: () => void; + onResolved?: () => void; +} + +export const DialogResolution = ({ + open, + incidentId, + onClose, + onResolved, +}: DialogResolutionProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const [comment, setComment] = useState(""); + const { put: resolveIncident, loading: isResolving } = usePut(); + + useEffect(() => { + if (open) { + setComment(""); + } + }, [open]); + + const handleCancel = () => { + onClose(); + }; + + const handleConfirm = async () => { + if (!incidentId) return; + const body = comment.trim() ? { comment: comment.trim() } : {}; + const result = await resolveIncident(`/incidents/${incidentId}/resolve`, body); + if (result) { + onClose(); + onResolved?.(); + } + }; + + return ( + + + setComment(e.target.value)} + fullWidth + /> + + + ); +}; diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx deleted file mode 100644 index 34efd632e..000000000 --- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx +++ /dev/null @@ -1,413 +0,0 @@ -// Components -import { Stack, Typography, Grid, Box, Button, Divider } from "@mui/material"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import SummaryCard from "../SummaryCard/index.jsx"; -import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; - -// Utils -import { useEffect, useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -// Hooks -import useFetchIncidents from "../../hooks/useFetchIncidents.js"; -import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration.js"; - -const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const { fetchIncidentById, resolveIncident } = useFetchIncidents(); - const [incidentData, setIncidentData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false); - const uiTimezone = useSelector((state) => state.ui.timezone); - const isActive = incidentData?.incident?.status === true; - const duration = useGetIncidentsDuration(incidentData?.incident || null, isActive); - - const statusColor = isActive ? theme.palette.error.main : theme.palette.success.main; - const toCapitalLetter = (text) => - typeof text === "string" && text.length > 0 - ? text.charAt(0).toUpperCase() + text.slice(1) - : text; - - const KeyValueRow = ({ label, value, sx = {} }) => ( - - - - {label} - - - - - {value ?? "-"} - - - - ); - - useEffect(() => { - const loadIncident = async () => { - if (!incidentId || !open) { - setIncidentData(null); - setIsLoading(false); - return; - } - - setIsLoading(true); - try { - const incidentData = await fetchIncidentById(incidentId); - setIncidentData(incidentData); - } catch (error) { - console.error("Error fetching incident:", error); - setIncidentData(null); - } finally { - setIsLoading(false); - } - }; - - loadIncident(); - }, [incidentId, open, fetchIncidentById]); - - const openResolveDialog = () => { - setIsResolveDialogOpen(true); - }; - - const closeResolveDialog = () => { - setIsResolveDialogOpen(false); - }; - - const handleAfterResolve = async () => { - if (!incidentId) return; - const updatedIncident = await fetchIncidentById(incidentId); - if (updatedIncident) { - setIncidentData(updatedIncident); - } - }; - - const renderContent = () => { - if (isLoading) { - return ( - - ); - } - - if (!incidentData) { - return ( - - ); - } - - return ( - - - - - - - - {isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")} - - - - - - {t("incidentsPage.incidentItemMonitor")} - - - {incidentData?.monitor?.name || t("incidentsPage.unknownMonitor")} - - - - - {t("incidentsPage.URL")} - - - {incidentData?.monitor?.url || "-"} - - - - - - - - - - - - - - {t("incidentsPage.timeline")} - - - - - {!isActive && ( - - )} - - - - - - - - - - - {isActive - ? t("incidentsPage.errorDetails") - : t("incidentsPage.resolutionDetails")} - - - - - - - - - {!isActive && ( - <> - - {incidentData?.incident?.resolutionType === "manual" && ( - - )} - - )} - - - - - - - - ); - }; - - return ( - - - {renderContent()} - {!isActive && - incidentData?.incident?.resolutionType === "manual" && - incidentData?.user?.email && ( - - {t("incidentsPage.resolvedBy")}: {incidentData.user.email} - - )} - - {isActive && ( - - )} - - - - - - - ); -}; - -IncidentDetailsModal.propTypes = { - open: PropTypes.bool.isRequired, - incidentId: PropTypes.string, - onClose: PropTypes.func.isRequired, - onResolved: PropTypes.func, -}; - -export default IncidentDetailsModal; diff --git a/client/src/Pages/Incidents/Components/IncidentTable.tsx b/client/src/Pages/Incidents/Components/IncidentTable.tsx new file mode 100644 index 000000000..bb1759dcd --- /dev/null +++ b/client/src/Pages/Incidents/Components/IncidentTable.tsx @@ -0,0 +1,222 @@ +import { Table } from "@/Components/design-elements"; +import { Pagination } from "@/Components/design-elements/Table"; +import type { Header } from "@/Components/design-elements/Table"; +import { ValueLabel } from "@/Components/design-elements"; +import type { ValueType } from "@/Components/design-elements/StatusLabel"; +import { ActionsMenu } from "@/Components/actions-menu"; +import type { Incident } from "@/Types/Incident"; +import type { Monitor } from "@/Types/Monitor"; +import type { ActionMenuItem } from "@/Components/actions-menu"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import { getMonitorPath } from "@/Utils/MonitorUtils"; + +interface IncidentsTableProps { + title?: string; + incidents?: Incident[]; + monitors?: Monitor[]; + incidentsCount: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; + onOpenDetails?: (incidentId: string) => void; + onResolve?: (incidentId: string) => void; +} + +export const IncidentsTable = ({ + incidents, + monitors, + incidentsCount, + page, + setPage, + rowsPerPage, + setRowsPerPage, + onOpenDetails, + onResolve, +}: IncidentsTableProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + const getActions = (incident: Incident): ActionMenuItem[] => { + const monitor = monitors?.find((m) => m.id === incident.monitorId); + const isActive = incident.status === true; + + const actions: ActionMenuItem[] = [ + { + id: "details", + label: t("pages.incidents.table.actions.details"), + action: () => onOpenDetails?.(incident.id), + closeMenu: true, + }, + { + id: "goToMonitor", + label: t("pages.incidents.table.actions.goToMonitor"), + action: () => { + if (monitor) { + const path = getMonitorPath(monitor.type); + if (path && monitor.id) { + navigate(`/${path}/${monitor.id}`); + } + } + }, + closeMenu: true, + }, + ]; + + if (isActive) { + actions.push({ + id: "resolve", + label: t("pages.incidents.table.actions.resolveManually"), + action: () => onResolve?.(incident.id), + closeMenu: true, + }); + } + + return actions; + }; + + const getHeaders = (): Header[] => { + return [ + { + id: "monitorName", + content: t("common.table.headers.monitor"), + render: (row) => { + const monitor = monitors?.find((m) => m.id === row.monitorId); + return monitor?.name || "N/A"; + }, + }, + { + id: "status", + content: t("common.table.headers.status"), + render: (row) => { + const isActive = row.status === true; + return ( + + ); + }, + }, + { + id: "startTime", + content: t("pages.incidents.table.headers.startTime"), + render: (row) => { + return formatDateWithTz(row.createdAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone); + }, + }, + { + id: "endTime", + content: t("pages.incidents.table.headers.endTime"), + render: (row) => { + if (row.endTime) { + return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone); + } + return "-"; + }, + }, + { + id: "resolutionType", + content: t("pages.incidents.table.headers.resolutionType"), + render: (row) => { + if (row.resolutionType) { + return ( + + {row.resolutionType} + + ); + } + return "-"; + }, + }, + { + id: "statusCode", + content: t("pages.incidents.table.headers.statusCode"), + render: (row) => { + const code = row.statusCode; + if (!code) return "N/A"; + let value: ValueType = "neutral"; + if (code < 300) value = "positive"; + else if (code >= 400) value = "negative"; + return ( + + ); + }, + }, + { + id: "message", + content: t("common.table.headers.message"), + render: (row) => row.message || "-", + }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => , + }, + ]; + }; + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + if (!incidents || !monitors) { + return null; + } + + return ( + + onOpenDetails?.(row.id)} + emptyViewText={t("common.table.empty")} + /> + + + ); +}; diff --git a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx deleted file mode 100644 index d125a9fdb..000000000 --- a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx +++ /dev/null @@ -1,156 +0,0 @@ -// Components -import IconButton from "@mui/material/IconButton"; -import Menu from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import Icon from "@/Components/v1/Icon/index.jsx"; -import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { TypeToPathMap } from "@/Utils/monitorUtilsLegacy.js"; -import { useNavigate } from "react-router-dom"; -import PropTypes from "prop-types"; - -const IncidentActionsMenu = ({ incident, monitor, onResolve, onOpenDetails }) => { - const [anchorEl, setAnchorEl] = useState(null); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const openMenu = (event) => { - event.preventDefault(); - event.stopPropagation(); - setAnchorEl(event.currentTarget); - }; - - const closeMenu = (e) => { - if (e) { - e.stopPropagation(); - } - setAnchorEl(null); - }; - - const openResolveDialog = (e) => { - closeMenu(e); - setIsDialogOpen(true); - }; - - const closeDialog = (e) => { - if (e) { - e.stopPropagation(); - } - setIsDialogOpen(false); - }; - - const isActive = incident.status === true; - const handleGoToDetails = (e) => { - e.stopPropagation(); - closeMenu(e); - if (onOpenDetails) { - onOpenDetails(incident.id); - } - }; - - const handleGoToMonitor = async (e) => { - e.stopPropagation(); - closeMenu(e); - try { - if (monitor) { - const path = TypeToPathMap[monitor.type]; - if (path && monitor.id) { - navigate(`/${path}/${monitor.id}`); - } else { - console.error(`Monitor type not found`); - } - } else { - console.error("Monitor information not found in incident data"); - } - } catch (error) { - console.error("Error fetching incident details:", error); - } - }; - - return ( - <> - { - event.stopPropagation(); - openMenu(event); - }} - sx={{ - "&:focus": { - outline: "none", - }, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }} - > - - - - - - {t("incidentsPage.incidentsTableActionDetails", "Details")} - - - {t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")} - - {isActive && ( - { - e.stopPropagation(); - openResolveDialog(e); - }} - > - {t("incidentsPage.incidentsTableActionResolveManually")} - - )} - - - - ); -}; - -IncidentActionsMenu.propTypes = { - incident: PropTypes.shape({ - id: PropTypes.string.isRequired, - status: PropTypes.bool.isRequired, - }).isRequired, - monitor: PropTypes.shape({ - id: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, - }).isRequired, - onResolve: PropTypes.func.isRequired, - onOpenDetails: PropTypes.func, -}; - -export default IncidentActionsMenu; diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx deleted file mode 100644 index 2a03a8822..000000000 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ /dev/null @@ -1,193 +0,0 @@ -//Components -import DataTable from "@/Components/v1/Table/index.jsx"; -import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; -import IncidentActionsMenu from "./IncidentActionsMenu.jsx"; - -//Utils -import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; -import { Typography, useTheme } from "@mui/material"; - -const IncidentTable = ({ - monitors = [], - incidents = [], - incidentsCount = 0, - isLoading = false, - networkError = false, - page = 0, - rowsPerPage = 10, - handleChangePage, - handleChangeRowsPerPage, - resolveIncident, - handleUpdateTrigger, - onOpenDetails, -}) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - - const { t } = useTranslation(); - const theme = useTheme(); - - const handleResolveIncident = async (incidentId, options = {}) => { - try { - await resolveIncident(incidentId, options); - handleUpdateTrigger(); - } catch (error) { - console.error(t("incidentsPage.errorResolvingIncident"), error); - } - }; - - const headers = [ - { - id: "monitorName", - content: t("incidentsTableMonitorName"), - render: (row) => { - const monitor = monitors.find((monitor) => monitor.id === row.monitorId); - return monitor ? monitor.name : "N/A"; - }, - }, - { - id: "status", - content: t("incidentsTableStatus"), - render: (row) => { - const status = row.status === true ? "down" : "up"; - const statusText = - row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved"); - return ( - - ); - }, - }, - { - id: "startTime", - content: t("incidentsPage.startTime"), - render: (row) => { - const formattedDate = formatDateWithTz( - row.startTime || row.createdAt, - "YYYY-MM-DD HH:mm:ss A", - uiTimezone - ); - return formattedDate; - }, - }, - { - id: "endTime", - content: t("incidentsPage.endTime"), - render: (row) => { - if (row.endTime) { - return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone); - } - return "-"; - }, - }, - { - id: "resolutionType", - content: t("incidentsPage.resolutionType"), - render: (row) => { - if (row.resolutionType) { - return ( - - {row.resolutionType} - - ); - } - return "-"; - }, - }, - { - id: "statusCode", - content: t("incidentsTableStatusCode"), - render: (row) => , - }, - { - id: "message", - content: t("incidentsTableMessage"), - render: (row) => row.message || "-", - }, - { - id: "action", - content: t("actions"), - render: (row) => { - return ( - monitor.id === row.monitorId)} - onResolve={handleResolveIncident} - onOpenDetails={onOpenDetails} - /> - ); - }, - }, - ]; - - if (isLoading) return ; - - if (networkError) { - return ( - - - - ); - } - - if (!isLoading && !networkError && incidents?.length === 0) { - return ( - - {t("incidentsTableNoIncidents", "No incidents found")} - - ); - } - - const incidentsData = Array.isArray(incidents) ? incidents : []; - - return ( - <> - - - - ); -}; - -IncidentTable.propTypes = { - incidents: PropTypes.array.isRequired, // Array of incident objects - incidentsCount: PropTypes.number.isRequired, // Total count for pagination - isLoading: PropTypes.bool.isRequired, // Loading state - networkError: PropTypes.bool, // Network error object - page: PropTypes.number.isRequired, // Current page number - rowsPerPage: PropTypes.number.isRequired, // Number of rows per page - handleChangePage: PropTypes.func.isRequired, // Handler for page change - handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change - resolveIncident: PropTypes.func.isRequired, - handleUpdateTrigger: PropTypes.func.isRequired, - onOpenDetails: PropTypes.func, -}; - -export default IncidentTable; diff --git a/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/index.jsx b/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/index.jsx deleted file mode 100644 index 33ba21cd1..000000000 --- a/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/index.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useEffect, useState } from "react"; -// Hooks -import useFetchIncidents from "../../hooks/useFetchIncidents.js"; -import PropTypes from "prop-types"; -// Components -import { Grid } from "@mui/material"; -import ActiveIncidentsPanel from "../ActiveIncidentsPanel/index.jsx"; -import LatestIncidentsPanel from "../LatestIncidentsPanel/index.jsx"; -import StatisticsPanel from "../StatisticsPanel/index.jsx"; - -const IncidentsSummaryPanel = ({ updateTrigger }) => { - const { fetchIncidentSummary } = useFetchIncidents(); - const [summary, setSummary] = useState({ - totalActive: 0, - avgResolutionTimeHours: 0, - total: 0, - topMonitor: null, - totalManualResolutions: 0, - totalAutomaticResolutions: 0, - latestIncidents: [], - }); - - const [isLoadingSummary, setIsLoadingSummary] = useState(false); - const [summaryError, setSummaryError] = useState(null); - - useEffect(() => { - const fetchSummary = async () => { - setIsLoadingSummary(true); - setSummaryError(null); - - try { - const summaryData = await fetchIncidentSummary({ - limit: 3, - }); - - if (summaryData) { - setSummary(summaryData); - } else { - setSummaryError(new Error("Failed to fetch summary")); - } - } catch (error) { - console.error("Error fetching incident summary:", error); - setSummaryError(error); - } finally { - setIsLoadingSummary(false); - } - }; - - fetchSummary(); - }, [fetchIncidentSummary, updateTrigger]); - - return ( - <> - - - - - - - - - - - - - - ); -}; - -IncidentsSummaryPanel.propTypes = { - updateTrigger: PropTypes.bool.isRequired, -}; -export default IncidentsSummaryPanel; diff --git a/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/skeleton.jsx b/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/skeleton.jsx deleted file mode 100644 index 153cd7c25..000000000 --- a/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/skeleton.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Skeleton } from "@mui/material"; -import SummaryCard from "../SummaryCard/index.jsx"; - -/** - * Simple skeleton loader for summary panels - * Reusable for ActiveIncidentsPanel, LatestIncidentsPanel, and StatisticsPanel - */ -const PanelSkeleton = () => { - return ( - - - - ); -}; - -export default PanelSkeleton; diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx deleted file mode 100644 index ca5bb640f..000000000 --- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import PropTypes from "prop-types"; -import { Box, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; -import Icon from "@/Components/v1/Icon"; -import { useTranslation } from "react-i18next"; -import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration"; - -const IncidentItem = ({ incident }) => { - const { t } = useTranslation(); - const theme = useTheme(); - const isActive = incident.status === true; - const duration = useGetIncidentsDuration(incident, isActive); - const iconWrapperStyle = { - display: "flex", - justifyContent: "center", - mx: theme.spacing(2), - color: theme.palette.primary.contrastTextTertiary, - "& svg": { - width: 18, - height: 18, - }, - "& svg path": { - stroke: "currentColor", - strokeWidth: 1.5, - }, - }; - - return ( - - - - - - - {incident.monitorName || t("incidentsPage.unknownMonitor")} - - - - - - - - - {duration} - - - ); -}; - -IncidentItem.propTypes = { - incident: PropTypes.shape({ - _id: PropTypes.string, - id: PropTypes.string, - monitorId: PropTypes.string, - monitorName: PropTypes.string, - status: PropTypes.bool, - startTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), - endTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), - resolutionType: PropTypes.oneOf(["automatic", "manual"]), - }), - onClick: PropTypes.func, -}; - -export default IncidentItem; diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx deleted file mode 100644 index ff1ef9417..000000000 --- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx +++ /dev/null @@ -1,114 +0,0 @@ -import PropTypes from "prop-types"; -import { Box, Stack, Typography, Divider } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx"; -import IncidentItem from "./IncidentItem.jsx"; -import SummaryCard from "../SummaryCard/index.jsx"; -import Icon from "@/Components/v1/Icon"; - -/** - * LatestIncidentsPanel Component - * - * Displays a quick overview of recent incidents (both active and resolved). - * Shows only essential information: monitor name, status, duration, and resolution type. - * - * @param {Object} props - * @param {Array} props.incidents - Array of recent incidents - * @param {boolean} props.isLoading - Loading state - * @param {Object} props.error - Error object if any - */ -const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - if (isLoading) { - return ; - } - - if (error) { - return ( - - - - {t("incidentsPage.incidentsLatestPanelError")} - - - - ); - } - - return ( - - {!incidents || incidents.length === 0 ? ( - - - - - - - {t("incidentsPage.incidentsLatestPanelEmpty")} - - - ) : ( - - {incidents.map((incident, index) => ( - - - {index < incidents.length - 1 && } - - ))} - - )} - - ); -}; - -LatestIncidentsPanel.propTypes = { - incidents: PropTypes.arrayOf( - PropTypes.shape({ - _id: PropTypes.string, - monitorId: PropTypes.string, - monitorName: PropTypes.string, - status: PropTypes.bool, - duration: PropTypes.string, - resolutionType: PropTypes.oneOf(["automatic", "manual"]), - }) - ), - isLoading: PropTypes.bool, - error: PropTypes.object, - onIncidentClick: PropTypes.func, -}; - -export default LatestIncidentsPanel; diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx deleted file mode 100644 index 8d7439576..000000000 --- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx +++ /dev/null @@ -1,139 +0,0 @@ -// Components -import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import PropTypes from "prop-types"; - -//Utils -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -const OptionsHeader = ({ - shouldRender, - selectedMonitor = 0, - setSelectedMonitor, - monitors, - filter = "all", - setFilter, - dateRange = "all", - setDateRange, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; - - const filterOptions = [ - { _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") }, - { _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") }, - { _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") }, - { _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") }, - { _id: "automatic", name: t("incidentsPage.incidentsOptionsHeaderFilterAutomatic") }, - ]; - - const stackStyles = { - direction: "row", - alignItems: "center", - gap: theme.spacing(6), - }; - - if (!shouldRender) return; - - return ( - - - - {t("incidentsOptionsHeader")} - - setFilter(e.target.value)} - items={filterOptions} - sx={{ - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastTextSecondary, - }} - /> - - - - {t("incidentsOptionsHeaderShow")} - - - - - - - - - - ); -}; - -OptionsHeader.propTypes = { - shouldRender: PropTypes.bool, - selectedMonitor: PropTypes.string, - setSelectedMonitor: PropTypes.func, - monitors: PropTypes.object, - filter: PropTypes.string, - setFilter: PropTypes.func, - dateRange: PropTypes.string, - setDateRange: PropTypes.func, -}; - -export default OptionsHeader; diff --git a/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx b/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx deleted file mode 100644 index 3e868b2dc..000000000 --- a/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useEffect, useState } from "react"; -import PropTypes from "prop-types"; -import { Button, Stack } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; - -const ResolveIncidentDialog = ({ - open, - incidentId, - onClose, - onResolve, - onAfterResolve, - onResolved, - isLoading = false, -}) => { - const { t } = useTranslation(); - const theme = useTheme(); - const [comment, setComment] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - - useEffect(() => { - if (open) { - setComment(""); - } - }, [open]); - - const handleConfirm = async () => { - if (!incidentId) return; - const trimmed = comment.trim(); - try { - setIsSubmitting(true); - await onResolve(incidentId, trimmed ? { comment: trimmed } : {}); - if (onAfterResolve) { - await onAfterResolve(); - } - onResolved?.(); - onClose(); - } catch (error) { - console.error("Error resolving incident:", error); - } finally { - setIsSubmitting(false); - } - }; - - return ( - - - setComment(e.target.value)} - maxWidth="100%" - /> - - - - - - - ); -}; - -ResolveIncidentDialog.propTypes = { - open: PropTypes.bool.isRequired, - incidentId: PropTypes.string, - onClose: PropTypes.func.isRequired, - onResolve: PropTypes.func.isRequired, - onAfterResolve: PropTypes.func, - isLoading: PropTypes.bool, - onResolved: PropTypes.func, -}; - -export default ResolveIncidentDialog; diff --git a/client/src/Pages/Incidents/Components/StatisticsPanel/index.jsx b/client/src/Pages/Incidents/Components/StatisticsPanel/index.jsx deleted file mode 100644 index 7f07f688f..000000000 --- a/client/src/Pages/Incidents/Components/StatisticsPanel/index.jsx +++ /dev/null @@ -1,168 +0,0 @@ -import PropTypes from "prop-types"; -import { Box, Stack, Typography, Divider } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx"; -import { useTranslation } from "react-i18next"; -import Icon from "@/Components/v1/Icon"; -import SummaryCard from "../SummaryCard/index.jsx"; - -/** - * StatisticsPanel Component - * - * Displays key metrics and statistics about incidents. - * - * @param {Object} props - * @param {Object} props.statistics - Statistics data object - * @param {number} props.statistics.total - Total incidents count - * @param {number} props.statistics.totalTrend - Trend percentage for total (optional) - * @param {string} props.statistics.mttr - Mean Time To Resolve (formatted string, e.g., "2h 15m") - * @param {number} props.statistics.mttrTrend - Trend percentage for MTTR (optional) - * @param {Object} props.statistics.topMonitor - Top monitor with most incidents - * @param {string} props.statistics.topMonitor.name - Monitor name - * @param {number} props.statistics.topMonitor.count - Incident count - * @param {number} props.statistics.topMonitor.percentage - Percentage of total - * @param {string} props.statistics.uptimeAffected - Total downtime (formatted string, e.g., "12h 45m") - * @param {number} props.statistics.availability - Availability percentage - * @param {boolean} props.isLoading - Loading state - * @param {Object} props.error - Error object if any - */ -const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const totalResolutions = - (summary?.totalManualResolutions || 0) + (summary?.totalAutomaticResolutions || 0); - - const iconWrapperStyle = { - display: "flex", - justifyContent: "center", - mx: theme.spacing(2), - color: theme.palette.primary.contrastTextTertiary, - "& svg": { - width: 18, - height: 18, - }, - "& svg path": { - stroke: "currentColor", - strokeWidth: 1.5, - }, - }; - if (isLoading) { - return ; - } - - if (error) { - return ( - - - - {t("incidentsPage.incidentsStatisticsPanelError")} - - - - ); - } - - const getMostAffectedMonitor = () => { - if (!summary.total || summary.total === 0) { - return t("incidentsPage.none"); - } - return summary.topMonitor?.monitorName || t("incidentsPage.none"); - }; - - const rowStyle = { - py: theme.spacing(0.5), - minHeight: 32, - }; - - return ( - - - - - - - - - {t("incidentsPage.totalIncidents")}: {summary.total || 0} - - - - - - - - - - - {t("incidentsPage.mostAffectedMonitor")}: {getMostAffectedMonitor()} - - - - - - - - - - {t("incidentsPage.avgResolutionTime")}:{" "} - {summary.total > 0 - ? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}` - : "N/A"} - - - - - ); -}; - -StatisticsPanel.propTypes = { - summary: PropTypes.object, - isLoading: PropTypes.bool, - error: PropTypes.object, -}; - -export default StatisticsPanel; diff --git a/client/src/Pages/Incidents/Components/SummaryCard/index.jsx b/client/src/Pages/Incidents/Components/SummaryCard/index.jsx deleted file mode 100644 index 3a8589dc1..000000000 --- a/client/src/Pages/Incidents/Components/SummaryCard/index.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Paper } from "@mui/material"; -import { useTheme } from "@mui/material/styles"; -import PropTypes from "prop-types"; -import { Box, Stack, Typography, Divider } from "@mui/material"; -const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }) => { - const theme = useTheme(); - return ( - - {title && ( - - - - {title} - - - - - )} - - {children} - - - ); -}; -SummaryCard.propTypes = { - children: PropTypes.node, - isHighPriority: PropTypes.bool, - sx: PropTypes.object, - title: PropTypes.string, -}; -export default SummaryCard; diff --git a/client/src/Pages/Incidents/hooks/useFetchIncidents.js b/client/src/Pages/Incidents/hooks/useFetchIncidents.js deleted file mode 100644 index e308c9319..000000000 --- a/client/src/Pages/Incidents/hooks/useFetchIncidents.js +++ /dev/null @@ -1,215 +0,0 @@ -import { useState, useCallback } from "react"; -import { networkService } from "../../../main.jsx"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; -/** - * Hook to fetch and manage incidents - * - * Provides multiple functions to fetch incidents with different filters. - * - * @returns {Object} { - * incidents, - * incidentsCount, - * isLoading, - * networkError, - * fetchIncidents, - * fetchActiveIncidents, - * fetchResolvedIncidents, - * fetchIncidentsByTeam, - * fetchIncidentById, - * resolveIncident, - * fetchIncidentSummary - * } - */ -const useFetchIncidents = () => { - const [incidents, setIncidents] = useState(null); - const [incidentsCount, setIncidentsCount] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [networkError, setNetworkError] = useState(false); - const { t } = useTranslation(); - /** - * Generic function to fetch incidents by team with custom config - * - * @param {Object} config - Configuration object - * @param {string} [config.monitorId] - Filter by monitor ID - * @param {boolean} [config.status] - Filter by status (true=active, false=resolved) - * @param {string} [config.resolutionType] - Filter by resolution type (automatic/manual) - * @param {string} [config.sortOrder] - Sort order (asc/desc) - * @param {string} [config.dateRange] - Date range filter - * @param {number} [config.page] - Page number - * @param {number} [config.rowsPerPage] - Rows per page - */ - const fetchIncidentsByTeam = useCallback(async (config = {}) => { - try { - setIsLoading(true); - setNetworkError(false); - - const res = await networkService.getIncidentsByTeam(config); - setIncidents(res.data?.data?.incidents || []); - setIncidentsCount(res.data?.data?.count || 0); - } catch (error) { - setNetworkError(true); - console.error(t("incidentsPage.errorFetchingIncidents"), error); - createToast({ body: error.message || t("incidentsPage.errorFetchingIncidents") }); - } finally { - setIsLoading(false); - } - }, []); - - /** - * Fetch active (open) incidents - Default function - * - * @param {Object} config - Additional configuration - */ - const fetchActiveIncidents = useCallback( - async (config = {}) => { - await fetchIncidentsByTeam({ - status: true, - sortOrder: "desc", - ...config, - }); - }, - [fetchIncidentsByTeam] - ); - - const fetchIncidentsByResolutionType = useCallback( - async (resolutionType, config = {}) => { - await fetchIncidentsByTeam({ - resolutionType, - sortOrder: "desc", - ...config, - }); - }, - [fetchIncidentsByTeam] - ); - /** - * Fetch resolved incidents - * - * @param {Object} config - Additional configuration - */ - const fetchResolvedIncidents = useCallback( - async (config = {}) => { - await fetchIncidentsByTeam({ - status: false, - sortOrder: "desc", - ...config, - }); - }, - [fetchIncidentsByTeam] - ); - - /** - * Fetch incidents with custom filters (alias for fetchIncidentsByTeam) - * - * @param {Object} config - Configuration object - */ - const fetchIncidents = useCallback( - async (config = {}) => { - await fetchIncidentsByTeam(config); - }, - [fetchIncidentsByTeam] - ); - - /** - * Fetch a single incident by ID - * - * @param {string} incidentId - The ID of the incident to fetch - * @returns {Promise} The incident object or null if not found - */ - const fetchIncidentById = useCallback(async (incidentId) => { - if (!incidentId) { - console.error(t("incidentsPage.noIncidentIdProvided")); - return null; - } - - try { - setIsLoading(true); - setNetworkError(false); - - const res = await networkService.getIncidentById(incidentId); - return res.data?.data || null; - } catch (error) { - setNetworkError(true); - console.error(t("incidentsPage.errorFetchingIncident"), error); - createToast({ body: error.message || t("incidentsPage.errorFetchingIncident") }); - return null; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Resolve an incident manually - * - * @param {string} incidentId - The ID of the incident to resolve - * @param {Object} options - Resolution options - * @param {string} [options.comment] - Optional comment about the resolution - * @param {Function} [onSuccess] - Callback function to call on success - * @param {Function} [onError] - Callback function to call on error - */ - const resolveIncident = useCallback(async (incidentId, options = {}) => { - if (!incidentId) { - console.error(t("incidentsPage.noIncidentIdProvided")); - return; - } - - try { - setIsLoading(true); - setNetworkError(false); - - const res = await networkService.resolveIncidentManually(incidentId, options); - if (res.data?.success) { - createToast({ body: t("incidentsPage.incidentResolvedSuccessfully") }); - } else { - createToast({ body: t("incidentsPage.errorResolvingIncident") }); - } - } catch (error) { - setNetworkError(true); - console.error(t("incidentsPage.errorResolvingIncident"), error); - const errorMessage = - error.response?.data?.message || - error.message || - t("incidentsPage.errorResolvingIncident"); - createToast({ body: errorMessage }); - } finally { - setIsLoading(false); - } - }, []); - - /** - * Fetch incident summary - * - * @param {Object} config - Configuration object - * @param {number} [config.limit=10] - Number of latest incidents to return - * @returns {Promise} The summary object or null if error - */ - const fetchIncidentSummary = useCallback(async (config = {}) => { - try { - const res = await networkService.getIncidentSummary(config); - return res.data?.data || null; - } catch (error) { - console.error(t("incidentsPage.errorFetchingIncidentSummary"), error); - createToast({ - body: error.message || t("incidentsPage.errorFetchingIncidentSummary"), - }); - return null; - } - }, []); - - return { - incidents, - incidentsCount, - isLoading, - networkError, - fetchIncidents, - fetchActiveIncidents, - fetchResolvedIncidents, - fetchIncidentsByTeam, - fetchIncidentById, - resolveIncident, - fetchIncidentSummary, - fetchIncidentsByResolutionType, - }; -}; - -export default useFetchIncidents; diff --git a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js deleted file mode 100644 index a4f434a80..000000000 --- a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js +++ /dev/null @@ -1,25 +0,0 @@ -import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; -const useGetIncidentsDuration = (incident, isActive) => { - if (!incident?.startTime) { - return "-"; - } - const startTime = new Date(incident?.startTime); - const endTime = isActive - ? new Date() - : incident?.endTime - ? new Date(incident.endTime) - : null; - - if (!endTime) { - return "-"; - } - - const durationMs = endTime.getTime() - startTime.getTime(); - - if (durationMs < 0) { - return "-"; - } - - return getHumanReadableDuration(durationMs); -}; -export default useGetIncidentsDuration; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx deleted file mode 100644 index 0377405b7..000000000 --- a/client/src/Pages/Incidents/index.jsx +++ /dev/null @@ -1,178 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import IncidentTable from "./Components/IncidentTable/index.jsx"; -import OptionsHeader from "./Components/OptionsHeader/index.jsx"; -import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx"; -import IncidentDetailsModal from "./Components/IncidentDetailsModal/index.jsx"; -//Utils -import { useTheme } from "@emotion/react"; -import { useState, useEffect } from "react"; -import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; -import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; - -// Hooks -import useFetchIncidents from "./hooks/useFetchIncidents.js"; -import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js"; - -const Incidents2 = () => { - const { monitorId } = useParams(); - const { t } = useTranslation(); - - const BREADCRUMBS = [ - { name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }, - ]; - - const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); - const [filter, setFilter] = useState("all"); - const [dateRange, setDateRange] = useState("all"); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [monitorLookup, setMonitorLookup] = useState(undefined); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [isDetailsOpen, setIsDetailsOpen] = useState(false); - const [selectedIncidentId, setSelectedIncidentId] = useState(null); - const handleUpdateTrigger = () => { - setUpdateTrigger((prev) => !prev); - }; - - const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId( - {} - ); - - const { - incidents, - incidentsCount, - isLoading: isLoadingIncidents, - networkError: incidentsNetworkError, - fetchIncidents, - fetchActiveIncidents, - fetchResolvedIncidents, - resolveIncident, - fetchIncidentsByResolutionType, - } = useFetchIncidents(); - - const networkError = monitorsNetworkError || incidentsNetworkError; - - const theme = useTheme(); - - useEffect(() => { - setPage(0); - }, [selectedMonitor, filter, dateRange]); - - useEffect(() => { - const config = { - monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined, - sortOrder: "desc", - dateRange, - page, - rowsPerPage, - }; - - if (filter === "active") { - fetchActiveIncidents(config); - } else if (filter === "resolved") { - fetchResolvedIncidents(config); - } else if (filter === "manual") { - fetchIncidentsByResolutionType("manual", config); - } else if (filter === "automatic") { - fetchIncidentsByResolutionType("automatic", config); - } else { - fetchIncidents(config); - } - }, [ - selectedMonitor, - filter, - dateRange, - page, - rowsPerPage, - updateTrigger, - fetchActiveIncidents, - fetchResolvedIncidents, - fetchIncidents, - fetchIncidentsByResolutionType, - ]); - - useEffect(() => { - const lookup = monitors?.reduce((acc, monitor) => { - acc[monitor.id] = { - id: monitor.id, - name: monitor.name, - type: monitor.type, - }; - return acc; - }, {}); - setMonitorLookup(lookup); - }, [monitors]); - - if (networkError) { - return ( - - - - ); - } - - const handleChangePage = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - const handleOpenDetails = (id) => { - if (!id) return; - setSelectedIncidentId(id); - setIsDetailsOpen(true); - }; - - const handleCloseDetails = () => { - setIsDetailsOpen(false); - setSelectedIncidentId(null); - }; - - return ( - - - - - - - - - - - ); -}; - -export default Incidents2; diff --git a/client/src/Pages/Incidents/index.tsx b/client/src/Pages/Incidents/index.tsx new file mode 100644 index 000000000..9003f6956 --- /dev/null +++ b/client/src/Pages/Incidents/index.tsx @@ -0,0 +1,245 @@ +import { BasePage } from "@/Components/design-elements"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import { useTranslation } from "react-i18next"; +import { + SummaryCardActiveIncidents, + SummaryCardLatestIncidents, + SummaryCardStats, +} from "@/Pages/Incidents/Components/CardSummary"; +import { IncidentsTable } from "@/Pages/Incidents/Components/IncidentTable"; +import { DialogResolution } from "@/Pages/Incidents/Components/DialogResolution"; +import { DialogIncidentDetails } from "@/Pages/Incidents/Components/DialogIncidentDetails"; +import { HeaderTimeRange } from "@/Components/common"; +import { ControlsIncidentFilter } from "@/Pages/Incidents/Components/ControlsIncidentFilter"; + +import { useGet } from "@/Hooks/UseApi"; +import { useState, useEffect, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import type { Incident, IncidentsResponse, IncidentSummary } from "@/Types/Incident"; +import type { Monitor } from "@/Types/Monitor"; +import { useTheme } from "@mui/material"; + +const IncidentsPage = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const { monitorId } = useParams(); + + // Filter state + const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); + const [filter, setFilter] = useState("all"); + const [dateRange, setDateRange] = useState("recent"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [activeIncidentsPage, setActiveIncidentsPage] = useState(0); + const [activeIncidentsRowsPerPage, setActiveIncidentsRowsPerPage] = useState(5); + + // Resolve dialog state + const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false); + const [resolveIncidentId, setResolveIncidentId] = useState(null); + + // Details dialog state + const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false); + const [selectedIncident, setSelectedIncident] = useState(null); + const [selectedIncidentMonitor, setSelectedIncidentMonitor] = useState( + null + ); + + // Build incidents URL with query params + const incidentsUrl = useMemo(() => { + const params = new URLSearchParams(); + if (selectedMonitor !== "0") params.append("monitorId", selectedMonitor); + // Always show only resolved incidents (active ones are in the separate table above) + params.append("status", "false"); + if (filter === "manual" || filter === "automatic") + params.append("resolutionType", filter); + params.append("sortOrder", "desc"); + if (dateRange !== "all") params.append("dateRange", dateRange); + params.append("page", String(page)); + params.append("rowsPerPage", String(rowsPerPage)); + return `/incidents/team?${params.toString()}`; + }, [selectedMonitor, filter, dateRange, page, rowsPerPage]); + + // Fetch incidents + const { + data: incidentsData, + isLoading: isLoadingIncidents, + refetch: refetchIncidents, + } = useGet( + incidentsUrl, + {}, + { keepPreviousData: true, refreshInterval: 10000 } + ); + + // Fetch monitors for lookup + const { data: monitorsData } = useGet("/monitors/team"); + + // Build active incidents URL (always fetch all active, no date filter) + const activeIncidentsUrl = useMemo(() => { + const params = new URLSearchParams(); + if (selectedMonitor !== "0") params.append("monitorId", selectedMonitor); + params.append("status", "true"); + params.append("sortOrder", "desc"); + params.append("page", String(activeIncidentsPage)); + params.append("rowsPerPage", String(activeIncidentsRowsPerPage)); + return `/incidents/team?${params.toString()}`; + }, [selectedMonitor, activeIncidentsPage, activeIncidentsRowsPerPage]); + + // Fetch active incidents + const { data: activeIncidentsData, refetch: refetchActiveIncidents } = + useGet( + activeIncidentsUrl, + {}, + { keepPreviousData: true, refreshInterval: 10000 } + ); + + // Fetch incident summary + const { data: summaryData, refetch: refetchSummary } = useGet( + "/incidents/team/summary" + ); + + // Reset page when filters change + useEffect(() => { + setPage(0); + }, [selectedMonitor, filter, dateRange]); + + // Derived state + const incidents = incidentsData?.incidents ?? []; + const incidentsCount = incidentsData?.count ?? 0; + const activeIncidents = activeIncidentsData?.incidents ?? []; + const activeIncidentsCount = activeIncidentsData?.count ?? 0; + + const handleClearFilters = () => { + setSelectedMonitor("0"); + setFilter("all"); + }; + + const handleOpenDetails = (incidentId: string) => { + const incident = + incidents.find((i) => i.id === incidentId) ?? + activeIncidents.find((i) => i.id === incidentId) ?? + null; + const monitor = monitorsData?.find((m) => m.id === incident?.monitorId) ?? null; + setIsDetailsDialogOpen(true); + setSelectedIncident(incident); + setSelectedIncidentMonitor(monitor); + }; + + const handleDetailsClose = () => { + setIsDetailsDialogOpen(false); + }; + + const handleDetailsResolve = () => { + if (selectedIncident) { + handleDetailsClose(); + handleResolve(selectedIncident.id); + } + }; + + const handleResolve = (incidentId: string) => { + setResolveIncidentId(incidentId); + setIsResolveDialogOpen(true); + }; + + const handleResolveClose = () => { + setIsResolveDialogOpen(false); + setResolveIncidentId(null); + }; + + const handleResolved = () => { + refetchIncidents(); + refetchActiveIncidents(); + refetchSummary(); + }; + + return ( + + + + + + + + {activeIncidentsCount > 0 ? ( + <> + + {t("pages.incidents.table.activeIncidents")} + + + + + + ) : ( + + {t("pages.incidents.summaryCard.activeIncidents.active_zero")} + + )} + + + {t("pages.incidents.table.resolvedIncidents")} + + + + + + + + ); +}; + +export default IncidentsPage; diff --git a/client/src/Pages/Incidents/utils.ts b/client/src/Pages/Incidents/utils.ts new file mode 100644 index 000000000..6ff518a18 --- /dev/null +++ b/client/src/Pages/Incidents/utils.ts @@ -0,0 +1,31 @@ +import type { Incident, IncidentSummaryItem } from "@/Types/Incident"; +import prettyMilliseconds from "pretty-ms"; + +type IncidentLike = Pick; + +export const getIncidentsDuration = (incident: IncidentLike | IncidentSummaryItem) => { + if (!incident?.startTime) { + return "-"; + } + const startTime = new Date(incident?.startTime); + const endTime = incident.status + ? new Date() + : incident?.endTime + ? new Date(incident.endTime) + : null; + + if (!endTime) { + return "-"; + } + + const durationMs = endTime.getTime() - startTime.getTime(); + + if (durationMs < 0) { + return "-"; + } + + return prettyMilliseconds(durationMs, { + secondsDecimalDigits: 0, + millisecondsDecimalDigits: 0, + }); +}; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx deleted file mode 100644 index 64f82cec2..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import { Box, Stack, Typography } from "@mui/material"; -import { CustomThreshold } from "./CustomThreshold/index.jsx"; -import { capitalizeFirstLetter } from "../../../../Utils/stringUtils.js"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; -const CustomAlertsSection = ({ - errors, - onChange, - infrastructureMonitor, - handleCheckboxChange, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const METRICS = ["cpu", "memory", "disk", "temperature"]; - const METRIC_PREFIX = "usage_"; - const hasAlertError = (errors) => { - return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; - }; - const getAlertError = (errors) => { - const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)); - return errorKey ? errors[errorKey] : null; - }; - return ( - - - - {t("infrastructureCustomizeAlerts")} - - - {t("infrastructureAlertNotificationDescription")} - - - - {METRICS.map((metric) => { - return ( - - ); - })} - - {hasAlertError(errors) && ( - - {getAlertError(errors)} - - )} - - - ); -}; - -CustomAlertsSection.propTypes = { - errors: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, - handleCheckboxChange: PropTypes.func.isRequired, -}; - -export default CustomAlertsSection; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx deleted file mode 100644 index d783f7985..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Box, Stack, Typography } from "@mui/material"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; - -/** - * CustomThreshold Component - * - * A reusable component that renders a checkbox with an associated numeric input field - * and an optional unit label. The input field can be enabled/disabled based on checkbox state. - * - * @component - * @param {Object} props - Component properties - * @param {string} [props.checkboxId] - Optional unique identifier for the checkbox - * @param {string} [props.checkboxName] - Optional name attribute for the checkbox - * @param {string} props.checkboxLabel - Label text for the checkbox - * @param {boolean} props.isChecked - Current checked state of the checkbox - * @param {Function} props.onCheckboxChange - Callback function when checkbox is toggled - * @param {string} props.fieldId - Unique identifier for the input field - * @param {string} [props.fieldName] - Optional name attribute for the input field - * @param {string} props.fieldValue - Current value of the input field - * @param {Function} props.onFieldChange - Callback function when input field value changes - * @param {Function} props.onFieldBlur - Callback function when input field loses focus - * @param {string} props.alertUnit - Unit label displayed next to the input field - * @param {Object} props.errors - Object containing validation errors for the field - * @param {Object} props.infrastructureMonitor - Infrastructure monitor configuration object - * - * @returns {React.ReactElement} Rendered CustomThreshold component - * - * @example - * - */ -export const CustomThreshold = ({ - checkboxId, - checkboxName, - checkboxLabel, - onCheckboxChange, - isChecked, - fieldId, - fieldName, - fieldValue, - onFieldChange, - onFieldBlur, - alertUnit, - errors, -}) => { - const theme = useTheme(); - return ( - - - - - - - - - {alertUnit} - - - - ); -}; - -CustomThreshold.propTypes = { - checkboxId: PropTypes.string, - checkboxName: PropTypes.string, - checkboxLabel: PropTypes.string.isRequired, - isChecked: PropTypes.bool.isRequired, - onCheckboxChange: PropTypes.func.isRequired, - fieldId: PropTypes.string.isRequired, - fieldName: PropTypes.string, - fieldValue: PropTypes.string.isRequired, - onFieldChange: PropTypes.func.isRequired, - onFieldBlur: PropTypes.func, - alertUnit: PropTypes.string.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, -}; diff --git a/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx b/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx deleted file mode 100644 index 50c4ed980..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import { Box, Stack, Typography } from "@mui/material"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -const DiskSelection = ({ availableDisks, selectedDisks, onChange }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const handleDiskChange = (event, mountpoint) => { - const isChecked = event.target.checked; - let newSelectedDisks = []; - - if (isChecked) { - newSelectedDisks = [...selectedDisks, mountpoint]; - } else { - newSelectedDisks = selectedDisks.filter((disk) => disk !== mountpoint); - } - - onChange(newSelectedDisks); - }; - - return ( - - - - {t("v1.infrastructure.disk_selection_title")} - - - {t("v1.infrastructure.disk_selection_description")} - - - - - {!availableDisks || availableDisks.length === 0 ? ( - - {t("v1.infrastructure.disk_selection_info")} - - ) : ( - availableDisks.map((disk) => { - const identifier = disk.mountpoint || disk.device; - return ( - - - handleDiskChange(e, identifier)} - /> - - - ); - }) - )} - - - ); -}; - -DiskSelection.propTypes = { - availableDisks: PropTypes.arrayOf( - PropTypes.shape({ - mountpoint: PropTypes.string.isRequired, - }) - ), - selectedDisks: PropTypes.arrayOf(PropTypes.string).isRequired, - onChange: PropTypes.func.isRequired, -}; - -export default DiskSelection; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx deleted file mode 100644 index 2bde5f321..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useState } from "react"; -import { Box, Button } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import Icon from "@/Components/v1/Icon"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import PropTypes from "prop-types"; - -const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const [isOpen, setIsOpen] = useState(false); - - return ( - - - - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - /> - - ); -}; - -MonitorActionButtons.propTypes = { - monitor: PropTypes.object.isRequired, - isBusy: PropTypes.bool.isRequired, - handlePause: PropTypes.func.isRequired, - handleRemove: PropTypes.func.isRequired, -}; - -export default MonitorActionButtons; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx deleted file mode 100644 index 8fa6f4ff1..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; -import PropTypes from "prop-types"; -const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); - return ( - - - - - - - - {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - ); -}; - -MonitorStatusHeader.propTypes = { - monitor: PropTypes.object.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, -}; - -export default MonitorStatusHeader; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx deleted file mode 100644 index a38ac9fc6..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState, useCallback } from "react"; -const useInfrastructureMonitorForm = () => { - const [infrastructureMonitor, setInfrastructureMonitor] = useState({ - url: "", - name: "", - notifications: [], - notify_email: false, - interval: 0.25, - statusWindowSize: 5, - statusWindowThreshold: 60, - cpu: false, - usage_cpu: "", - memory: false, - usage_memory: "", - disk: false, - usage_disk: "", - temperature: false, - usage_temperature: "", - secret: "", - selectedDisks: [], - }); - - const onChangeForm = (name, value) => { - setInfrastructureMonitor({ - ...infrastructureMonitor, - [name]: value, - }); - }; - const handleCheckboxChange = (event) => { - setInfrastructureMonitor({ - ...infrastructureMonitor, - [event.target.name]: event.target.checked, - }); - }; - const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => { - const gt = globalSettings?.data?.settings?.globalThresholds || {}; - setInfrastructureMonitor((prev) => ({ - ...prev, - url: "", - name: "", - notifications: [], - interval: 0.25, - cpu: gt.cpu !== undefined, - usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "", - memory: gt.memory !== undefined, - usage_memory: gt.memory !== undefined ? gt.memory.toString() : "", - disk: gt.disk !== undefined, - usage_disk: gt.disk !== undefined ? gt.disk.toString() : "", - temperature: gt.temperature !== undefined, - usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", - secret: "", - selectedDisks: [], - })); - }, []); - - const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => { - const MS_PER_MINUTE = 60000; - const { thresholds = {} } = monitor; - setInfrastructureMonitor((prev) => ({ - ...prev, - url: monitor.url.replace(/^https?:\/\//, ""), - name: monitor.name || "", - notifications: monitor.notifications || [], - interval: monitor.interval / MS_PER_MINUTE, - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - cpu: thresholds.usage_cpu !== undefined, - usage_cpu: - thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "", - memory: thresholds.usage_memory !== undefined, - usage_memory: - thresholds.usage_memory !== undefined - ? (thresholds.usage_memory * 100).toString() - : "", - disk: thresholds.usage_disk !== undefined, - usage_disk: - thresholds.usage_disk !== undefined - ? (thresholds.usage_disk * 100).toString() - : "", - temperature: thresholds.usage_temperature !== undefined, - usage_temperature: - thresholds.usage_temperature !== undefined - ? (thresholds.usage_temperature * 100).toString() - : "", - secret: monitor.secret || "", - selectedDisks: monitor.selectedDisks || [], - })); - }, []); - return { - infrastructureMonitor, - setInfrastructureMonitor, - onChangeForm, - handleCheckboxChange, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - }; -}; - -export default useInfrastructureMonitorForm; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx deleted file mode 100644 index 04279cac4..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks.js"; -const useInfrastructureSubmit = () => { - const [createMonitor, isCreating] = useCreateMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - const buildForm = (infrastructureMonitor, https) => { - const MS_PER_MINUTE = 60000; - - let form = { - url: `http${https ? "s" : ""}://` + infrastructureMonitor.url, - name: - infrastructureMonitor.name === "" - ? infrastructureMonitor.url - : infrastructureMonitor.name, - interval: infrastructureMonitor.interval * MS_PER_MINUTE, - statusWindowSize: infrastructureMonitor.statusWindowSize, - statusWindowThreshold: infrastructureMonitor.statusWindowThreshold, - cpu: infrastructureMonitor.cpu, - ...(infrastructureMonitor.cpu - ? { usage_cpu: infrastructureMonitor.usage_cpu } - : {}), - memory: infrastructureMonitor.memory, - ...(infrastructureMonitor.memory - ? { usage_memory: infrastructureMonitor.usage_memory } - : {}), - disk: infrastructureMonitor.disk, - ...(infrastructureMonitor.disk - ? { usage_disk: infrastructureMonitor.usage_disk } - : {}), - ...(infrastructureMonitor.temperature - ? { usage_temperature: infrastructureMonitor.usage_temperature } - : {}), - secret: infrastructureMonitor.secret, - selectedDisks: infrastructureMonitor.selectedDisks, - }; - return form; - }; - const submitInfrastructureForm = async ( - infrastructureMonitor, - form, - isCreate, - monitorId - ) => { - const { - cpu, - usage_cpu, - memory, - usage_memory, - disk, - usage_disk, - temperature, - usage_temperature, - selectedDisks, - ...rest - } = form; - - const thresholds = { - ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}), - ...(memory ? { usage_memory: usage_memory / 100 } : {}), - ...(disk ? { usage_disk: usage_disk / 100 } : {}), - ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}), - }; - - const finalForm = { - ...(isCreate ? {} : { id: monitorId }), - ...rest, - description: form.name, - type: "hardware", - notifications: infrastructureMonitor.notifications, - selectedDisks, - thresholds, - }; - // Handle create or update - isCreate - ? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" }) - : await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" }); - }; - return { - buildForm, - submitInfrastructureForm, - isCreating, - isUpdating, - }; -}; -export default useInfrastructureSubmit; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx deleted file mode 100644 index f7665e65f..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useState } from "react"; -import { infrastructureMonitorValidation } from "../../../../Validation/validation.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -const useValidateInfrastructureForm = () => { - const [errors, setErrors] = useState({}); - - const validateField = (name, value) => { - const { error } = infrastructureMonitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - const validateForm = (form) => { - const { error } = infrastructureMonitorValidation.validate(form, { - abortEarly: false, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - console.log(newErrors); - setErrors(newErrors); - createToast({ body: "Please check the form for errors." }); - return error; - } - return null; - }; - return { errors, validateField, validateForm }; -}; -export default useValidateInfrastructureForm; diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx deleted file mode 100644 index 936d87ab0..000000000 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ /dev/null @@ -1,409 +0,0 @@ -//Components -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx"; -import Link from "@/Components/v1/Link/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx"; -import MonitorActionButtons from "./Components/MonitorActionButtons.jsx"; -import CustomAlertsSection from "./Components/CustomAlertsSection.jsx"; -import DiskSelection from "./Components/DiskSelection.jsx"; -// Utils -import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; -import { networkService } from "../../../Utils/NetworkService.js"; -import { useParams } from "react-router-dom"; -import { useState, useEffect } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { - useDeleteMonitor, - useFetchGlobalSettings, - useFetchMonitorById, - usePauseMonitor, -} from "@/Hooks/monitorHooks.js"; -import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx"; -import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx"; -import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; - -const CreateInfrastructureMonitor = () => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined"; - - const theme = useTheme(); - const { t } = useTranslation(); - - // State - const [monitor, setMonitor] = useState(null); - const [https, setHttps] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [availableDisks, setAvailableDisks] = useState([]); - - // Fetch monitor details if editing - const [isLoading] = useFetchMonitorById({ - monitorId, - setMonitor, - updateTrigger: true, - }); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); - const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId(); - const [pauseMonitor, isPausing] = usePauseMonitor(); - const { - infrastructureMonitor, - setInfrastructureMonitor, - onChangeForm, - handleCheckboxChange, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - } = useInfrastructureMonitorForm(); - const { errors, validateField, validateForm } = useValidateInfrastructureForm(); - const { buildForm, submitInfrastructureForm, isCreating, isUpdating } = - useInfrastructureSubmit(); - - const FREQUENCIES = [ - { _id: 0.25, name: t("time.fifteenSeconds") }, - { _id: 0.5, name: t("time.thirtySeconds") }, - { _id: 1, name: t("time.oneMinute") }, - { _id: 2, name: t("time.twoMinutes") }, - { _id: 5, name: t("time.fiveMinutes") }, - { _id: 10, name: t("time.tenMinutes") }, - ]; - const CRUMBS = [ - { name: "Infrastructure monitors", path: "/infrastructure" }, - ...(isCreate - ? [{ name: "Create", path: "/infrastructure/create" }] - : [ - { name: "Details", path: `/infrastructure/${monitorId}` }, - { name: "Configure", path: `/infrastructure/configure/${monitorId}` }, - ]), - ]; - // Populate form fields if editing - useEffect(() => { - if (isCreate) { - if (globalSettingsLoading) return; - setHttps(false); - initializeInfrastructureMonitorForCreate(globalSettings); - } else if (monitor) { - setHttps(monitor.url.startsWith("https")); - initializeInfrastructureMonitorForUpdate(monitor); - const fetchLastCheck = async () => { - try { - let disks = monitor?.recentChecks?.[0]?.disk || []; - - if (disks.length > 0) { - setAvailableDisks(disks); - return; - } - - const response = await networkService.getChecksByMonitor({ - monitorId, - dateRange: "all", - rowsPerPage: 1, - sortOrder: "desc", - }); - disks = response?.data?.data?.checks?.[0]?.disk || []; - setAvailableDisks(disks); - } catch (error) { - setAvailableDisks([]); - } - }; - - fetchLastCheck(); - } - }, [ - isCreate, - monitor, - globalSettings, - globalSettingsLoading, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - ]); - - // Handlers - const onSubmit = async (event) => { - event.preventDefault(); - const form = buildForm(infrastructureMonitor, https); - // When editing, exclude URL from validation since it's disabled and can't be changed - const formToValidate = isCreate ? form : { ...form, url: monitor.url }; - const error = validateForm(formToValidate); - if (error) { - return; - } - submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId); - }; - - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const onChange = (event) => { - const { value, name } = event.target; - onChangeForm(name, value); - validateField(name, value); - }; - - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/infrastructure" }); - }; - - const isBusy = - isLoading || - isUpdating || - isCreating || - isDeleting || - isPausing || - notificationsAreLoading; - - return ( - - - - - - - - {!isCreate ? infrastructureMonitor.name : t("createYour") + " "} - - {isCreate ? ( - - {t("monitor")} - - ) : ( - <> - )} - - {!isCreate && monitor && ( - - )} - - {!isCreate && monitor && ( - - )} - - - - - {t("settingsGeneralSettings")} - - - {t("infrastructureCreateGeneralSettingsDescription")} - - - {t("infrastructureServerRequirement")}{" "} - - - - - } - placeholder={"localhost:59232/api/v1/metrics"} - label={t("infrastructureServerUrlLabel")} - https={https} - value={infrastructureMonitor.url} - onChange={onChange} - error={errors["url"] ? true : false} - helperText={errors["url"]} - disabled={!isCreate} - /> - {isCreate && ( - - - - - - - )} - - - - - - - - {t("notificationConfig.title")} - - {t("notificationConfig.description")} - - - - - - - {t("createMonitorPage.incidentConfigTitle")} - - - {t("createMonitorPage.incidentConfigDescription")} - - - - - - - - - - {monitorId && ( - - onChangeForm("selectedDisks", newSelectedDisks) - } - /> - )} - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - setSelectedInterface(e.target.value)} - items={availableInterfaces.map((interfaceName) => ({ - _id: interfaceName, - name: interfaceName, - }))} - sx={{ minWidth: 200 }} - /> - )} - - - - - ); -}; - -Network.propTypes = { - net: PropTypes.array, - checks: PropTypes.array, - isLoading: PropTypes.bool.isRequired, - dateRange: PropTypes.string.isRequired, - setDateRange: PropTypes.func.isRequired, -}; - -export default Network; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx deleted file mode 100644 index 52fcfad87..000000000 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { - Card, - CardContent, - Skeleton, - Table, - TableHead, - TableRow, - TableCell, - TableBody, -} from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - -
- - - Name - Bytes Sent - Bytes Received - Packets Sent - Packets Received - Errors In - Errors Out - Drops In - Drops Out - - - - {Array.from({ length: 5 }).map((_, idx) => ( - - {Array.from({ length: 9 }).map((__, colIdx) => ( - - - - ))} - - ))} - -
- - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx new file mode 100644 index 000000000..ebfdad569 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx @@ -0,0 +1,68 @@ +import Stack from "@mui/material/Stack"; +import { StatBox } from "@/Components/design-elements"; + +import prettyBytes from "pretty-bytes"; +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import type { Monitor } from "@/Types/Monitor"; +import { getAvgTemp, getCores, getFrequency, getOsAndPlatform } from "@/Utils/InfraUtils"; + +export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const latestCheck = monitor?.recentChecks?.[0]; + // Get data from latest check + const physicalCores = getCores(latestCheck?.cpu?.physical_core); + const logicalCores = getCores(latestCheck?.cpu?.logical_core); + const cpuFrequency = getFrequency(latestCheck?.cpu?.frequency); + const cpuTemps = latestCheck?.cpu?.temperature ?? []; + const cpuTemperature = getAvgTemp(cpuTemps); + const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; + const diskTotalBytes = + latestCheck?.disk?.reduce((acc, disk) => acc + (disk.total_bytes || 0), 0) || 0; + const os = getOsAndPlatform(latestCheck?.host); + + const platform = latestCheck?.host?.platform ?? undefined; + const osPlatform = + typeof os === "undefined" && typeof platform === "undefined" + ? undefined + : `${os} ${platform}`; + + return ( + + + + + + + + + + ); +}; diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx deleted file mode 100644 index d411886c7..000000000 --- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx +++ /dev/null @@ -1,115 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; - -//Utils -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; -import { useTranslation } from "react-i18next"; - -const InfraStatBoxes = ({ shouldRender, monitor }) => { - // Utils - const { formatBytes } = useHardwareUtils(); - const { determineState } = useMonitorUtils(); - const { t } = useTranslation(); - - const latestCheck = monitor?.recentChecks?.[0]; - - // Get data from latest check - const physicalCores = latestCheck?.cpu?.physical_core ?? 0; - const logicalCores = latestCheck?.cpu?.logical_core ?? 0; - const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; - const cpuTemperature = - latestCheck?.cpu?.temperature?.length > 0 - ? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) / - latestCheck.cpu.temperature.length - : 0; - const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; - const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0; - const os = latestCheck?.host?.os ?? undefined; - const platform = latestCheck?.host?.platform ?? undefined; - const osPlatform = - typeof os === "undefined" && typeof platform === "undefined" - ? undefined - : `${os} ${platform}`; - - return ( - - - - {physicalCores} - - {physicalCores === 1 ? "core" : "cores"} - - - } - /> - - {logicalCores} - - {logicalCores === 1 ? "core" : "cores"} - - - } - /> - - {(cpuFrequency / 1000).toFixed(2)} - Ghz - - } - /> - - {cpuTemperature.toFixed(2)} - °C - - } - /> - - - {/* - {(uptimePercentage * 100).toFixed(2)} - % - - } - /> */} - - - ); -}; - -export default InfraStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx b/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx new file mode 100644 index 000000000..612040cb1 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx @@ -0,0 +1,29 @@ +import Stack from "@mui/material/Stack"; +import { InfraNetworkCharts } from "./ChartsNetwork"; + +import { useTheme } from "@mui/material"; +import type { HardwareStats } from "@/Types/Monitor"; + +export const TabNetwork = ({ + stats, + dateRange, +}: { + stats: HardwareStats | undefined; + dateRange: string; +}) => { + const theme = useTheme(); + + if (!stats) { + return null; + } + + const checks = stats?.checks || []; + return ( + + + + ); +}; diff --git a/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx b/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx new file mode 100644 index 000000000..447a2cce6 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx @@ -0,0 +1,34 @@ +import Stack from "@mui/material/Stack"; +import { InfraDetailsGauges } from "@/Pages/Infrastructure/Details/Components/Gauges"; +import { StatusBoxes } from "@/Pages/Infrastructure/Details/Components/StatusBoxes"; +import { InfraDetailsCharts } from "@/Pages/Infrastructure/Details/Components/Charts"; + +import { useTheme } from "@mui/material"; +import type { HardwareStats, Monitor } from "@/Types/Monitor"; + +export const TabOverview = ({ + monitor, + stats, + dateRange, +}: { + monitor: Monitor | undefined; + stats: HardwareStats | undefined; + dateRange: string; +}) => { + const theme = useTheme(); + if (!monitor) { + return null; + } + + const checks = stats?.checks || []; + return ( + + + + + + ); +}; diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx deleted file mode 100644 index 8938847a4..000000000 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx +++ /dev/null @@ -1,270 +0,0 @@ -import { Typography, Tooltip } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -// Constants -const BASE_BOX_PADDING_VERTICAL = 4; -const BASE_BOX_PADDING_HORIZONTAL = 8; -const TYPOGRAPHY_PADDING = 8; -const CHART_CONTAINER_HEIGHT = 300; - -const useHardwareUtils = () => { - const theme = useTheme(); - const { t } = useTranslation(); - - const getDimensions = () => { - const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2; - const totalChartContainerPadding = - parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2; - return { - baseBoxPaddingVertical: BASE_BOX_PADDING_VERTICAL, - baseBoxPaddingHorizontal: BASE_BOX_PADDING_HORIZONTAL, - totalContainerPadding: parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2, - areaChartHeight: - CHART_CONTAINER_HEIGHT - totalChartContainerPadding - totalTypographyPadding, - }; - }; - - const formatBytes = (bytes, space = false) => { - if (bytes === undefined || bytes === null) - return ( - <> - {0} - {space ? " " : ""} - {t("gb")} - - ); - if (typeof bytes !== "number") - return ( - <> - {0} - {space ? " " : ""} - {t("gb")} - - ); - if (bytes === 0) - return ( - <> - {0} - {space ? " " : ""} - {t("gb")} - - ); - - const GB = bytes / (1024 * 1024 * 1024); - const MB = bytes / (1024 * 1024); - - if (GB >= 1) { - return ( - <> - {Number(GB.toFixed(2))} - {space ? " " : ""} - {t("gb")} - - ); - } else { - return ( - <> - {Number(MB.toFixed(2))} - {space ? " " : ""} - {t("mb")} - - ); - } - }; - - const formatBytesPerSecondString = (bytesPerSec, space = false) => { - if ( - bytesPerSec === undefined || - bytesPerSec === null || - typeof bytesPerSec !== "number" || - bytesPerSec === 0 - ) { - return `0${space ? " " : ""}B/s`; - } - - const GB = bytesPerSec / (1024 * 1024 * 1024); - const MB = bytesPerSec / (1024 * 1024); - const KB = bytesPerSec / 1024; - - if (GB >= 1) { - return `${Number(GB.toFixed(1))}${space ? " " : ""}GB/s`; - } else if (MB >= 1) { - return `${Number(MB.toFixed(1))}${space ? " " : ""}MB/s`; - } else if (KB >= 1) { - return `${Number(KB.toFixed(1))}${space ? " " : ""}KB/s`; - } else { - return `${Number(bytesPerSec.toFixed(1))}${space ? " " : ""}B/s`; - } - }; - - const formatPacketsPerSecondString = (packetsPerSec, space = false) => { - if ( - packetsPerSec === undefined || - packetsPerSec === null || - typeof packetsPerSec !== "number" || - packetsPerSec === 0 - ) { - return `0${space ? " " : ""}pps`; - } - - const M = packetsPerSec / (1000 * 1000); - const K = packetsPerSec / 1000; - - if (M >= 1) { - return `${Number(M.toFixed(1))}${space ? " " : ""}Mpps`; - } else if (K >= 1) { - return `${Number(K.toFixed(1))}${space ? " " : ""}Kpps`; - } else { - return `${Math.round(packetsPerSec)}${space ? " " : ""}pps`; - } - }; - - const formatDeviceName = (device) => { - const deviceStr = String(device || ""); - - // Show full device path - return ( - - - {deviceStr} - - - ); - }; - - const formatMountpoint = (mountpoint) => { - const mountpointStr = String(mountpoint || ""); - - if (!mountpointStr) { - return ( - - - N/A - - - ); - } - - // Show full mountpoint path - return ( - - - {mountpointStr} - - - ); - }; - - /** - * Converts a decimal value to a percentage - * - * @function decimalToPercentage - * @param {number} value - Decimal value to convert - * @returns {number} Percentage representation - * - * @example - * decimalToPercentage(0.75) // Returns 75 - * decimalToPercentage(null) // Returns 0 - */ - const decimalToPercentage = (value) => { - if (value === null || value === undefined) return 0; - return value * 100; - }; - - const buildTemps = (checks) => { - let numCores = 1; - if (checks === null) return { temps: [], tempKeys: [] }; - - for (const check of checks) { - if (check?.avgTemperature?.length > numCores) { - numCores = check.avgTemperature.length; - break; - } - } - const temps = checks.map((check) => { - // If there's no data, set the temperature to 0 - if ( - check?.avgTemperature?.length === 0 || - check?.avgTemperature === undefined || - check?.avgTemperature === null - ) { - check.avgTemperature = Array(numCores).fill(0); - } - const res = check?.avgTemperature?.reduce( - (acc, cur, idx) => { - acc[`core${idx + 1}`] = cur; - return acc; - }, - { - _id: check._id, - } - ); - return res; - }); - if (temps.length === 0 || !temps[0]) { - return { temps: [], tempKeys: [] }; - } - - return { - tempKeys: Object.keys(temps[0] || {}).filter((key) => key !== "_id"), - temps, - }; - }; - - return { - formatBytes, - formatDeviceName, - formatMountpoint, - decimalToPercentage, - buildTemps, - getDimensions, - formatBytesPerSecondString, - formatPacketsPerSecondString, - }; -}; - -export { useHardwareUtils }; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx deleted file mode 100644 index 6ae630822..000000000 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ /dev/null @@ -1,142 +0,0 @@ -// Components -import { Stack, Typography, Tab } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; -import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; -import StatusBoxes from "./Components/StatusBoxes/index.jsx"; -import GaugeBoxes from "./Components/GaugeBoxes/index.jsx"; -import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import NetworkStats from "./Components/NetworkStats/index.jsx"; -import CustomTabList from "@/Components/v1/Tab/index.jsx"; -import TabContext from "@mui/lab/TabContext"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks.js"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; - -// Constants -const BREADCRUMBS = [ - { name: "infrastructure monitors", path: "/infrastructure" }, - { name: "details", path: "" }, -]; -const InfrastructureDetails = () => { - // Local state - const [dateRange, setDateRange] = useState("recent"); - const [trigger, setTrigger] = useState(false); - const [tab, setTab] = useState("details"); - - // Utils - const theme = useTheme(); - const { monitorId } = useParams(); - const { t } = useTranslation(); - const isAdmin = useIsAdmin(); - - const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({ - monitorId, - dateRange, - updateTrigger: trigger, - }); - - const triggerUpdate = () => { - setTrigger(!trigger); - }; - - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - if (!isLoading && monitor?.stats?.checks?.length === 0) { - return ( - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - setTab(v)} - > - - - - {tab === "details" && ( - <> - - - - - - )} - {tab === "network" && ( - - )} - - - ); -}; - -export default InfrastructureDetails; diff --git a/client/src/Pages/Infrastructure/Details/index.tsx b/client/src/Pages/Infrastructure/Details/index.tsx new file mode 100644 index 000000000..202887ad5 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/index.tsx @@ -0,0 +1,90 @@ +import { BasePage, Tab, Tabs } from "@/Components/design-elements"; +import { HeaderTimeRange } from "@/Components/common"; +import { MonitorStatBoxes, HeaderMonitorControls } from "@/Components/monitors"; +import { TabNetwork } from "@/Pages/Infrastructure/Details/Components/TabNetwork"; +import { TabOverview } from "@/Pages/Infrastructure/Details/Components/TabOverview"; + +import { useMemo, useState } from "react"; +import { useParams } from "react-router-dom"; +import { useGet } from "@/Hooks/UseApi"; +import type { HardwareDetailsResponse } from "@/Types/Monitor"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import { useTranslation } from "react-i18next"; + +const InfrastructureDetails = () => { + const { t } = useTranslation(); + const isAdmin = useIsAdmin(); + + const { monitorId } = useParams<{ monitorId: string }>(); + + const [dateRange, setDateRange] = useState("recent"); + const [selectedTab, setSelectedTab] = useState(0); + + const monitorDetailsUrl = useMemo(() => { + if (!monitorId) { + return null; + } + const params = new URLSearchParams(); + params.append("dateRange", dateRange); + return `/monitors/hardware/details/${monitorId}?${params.toString()}`; + }, [monitorId, dateRange]); + + const { + data: monitorDetailsData, + isLoading: monitorIsLoading, + refetch: refetchMonitor, + } = useGet( + monitorDetailsUrl, + {}, + { refreshInterval: 10000, keepPreviousData: true } + ); + + const monitor = monitorDetailsData?.monitor; + const monitorStats = monitorDetailsData?.monitorStats ?? null; + const stats = monitorDetailsData?.stats; + + return ( + + + + + { + setSelectedTab(value); + }} + > + + + + {selectedTab === 0 && ( + + )} + {selectedTab === 1 && ( + + )} + + ); +}; + +export default InfrastructureDetails; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx deleted file mode 100644 index 01e56c68a..000000000 --- a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; -import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; -import { useMemo } from "react"; -import { Box, Button } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; -import { useTranslation } from "react-i18next"; - -/** - * Filter Component - * - * A high-level component that provides filtering options for status in Infrastructure Page. - * It allows users to select multiple options for each filter and reset the filters. - * - * @component - * @param {Object} props - The component props. - * @param {string[]} props.selectedStatus - An array of selected status values. - * @param {function} props.setSelectedStatus - A function to set the selected status values. - * @param {function} props.setToFilterStatus - A function to set the filter status based on selected status values. - * @param {function} props.handleReset - A function to reset all filters. - * - * @returns {JSX.Element} The rendered Filter component. - */ - -const statusOptions = [ - { value: "Up", label: "Up" }, - { value: "Down", label: "Down" }, -]; - -const Filter = ({ - selectedStatus, - setSelectedStatus, - setToFilterStatus, - handleReset, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const handleStatusChange = (event) => { - const selectedValues = event.target.value; - setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined); - - if (selectedValues.length === 0 || selectedValues.length === 2) { - setToFilterStatus(undefined); - } else { - setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false"); - } - }; - - const isFilterActive = useMemo(() => { - return (selectedStatus?.length ?? 0) > 0; - }, [selectedStatus]); - - return ( - - - - - ); -}; - -Filter.propTypes = { - selectedStatus: PropTypes.arrayOf(PropTypes.string), - setSelectedStatus: PropTypes.func.isRequired, - setToFilterStatus: PropTypes.func.isRequired, - handleReset: PropTypes.func.isRequired, -}; - -export default Filter; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx new file mode 100644 index 000000000..994ddcea1 --- /dev/null +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx @@ -0,0 +1,271 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Table, Pagination, StatusLabel, Gauge } from "@/Components/design-elements"; +import type { Header } from "@/Components/design-elements/Table"; +import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu"; +import { ArrowUp, ArrowDown } from "lucide-react"; + +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { usePost } from "@/Hooks/UseApi"; + +import type { Monitor } from "@/Types/Monitor"; + +export const InfraMonitorsTable = ({ + monitors, + refetch, + setSelectedMonitor, + sortField, + setSortField, + sortOrder, + setSortOrder, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: Monitor[]; + refetch: Function; + setSelectedMonitor: Function; + sortField: string; + setSortField: (field: string) => void; + sortOrder: "asc" | "desc"; + setSortOrder: (order: "asc" | "desc") => void; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const navigate = useNavigate(); + const { + post, + // loading: isPatching, + // error: postError, + } = usePost(); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + const handleSort = (e: any, field: string) => { + e.preventDefault(); + e.stopPropagation(); + if (sortField === field) { + const newOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newOrder); + } else { + setSortField(field); + setSortOrder("asc"); + } + refetch(); + }; + + const getActions = (monitor: Monitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.openSite"), + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: t("pages.common.monitors.actions.details"), + action: () => { + navigate(`${monitor.id}`); + }, + }, + { + id: 3, + label: t("pages.common.monitors.actions.incidents"), + action: () => { + navigate(`/incidents?monitorId=${monitor.id}`); + }, + }, + { + id: 4, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/infrastructure/configure/${monitor.id}`); + }, + }, + // { + // id: 5, + // label: "Clone", + // action: () => { + + // }, + // }, + { + id: 6, + label: + monitor.isActive === false + ? t("common.buttons.resume") + : t("common.buttons.pause"), + action: async () => { + await post(`/monitors/pause/${monitor.id}`, {}); + refetch(); + }, + closeMenu: true, + }, + { + id: 7, + label: ( + + {t("common.buttons.delete")} + + ), + action: () => { + setSelectedMonitor(monitor); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const renderSortIcon = (isActive: boolean) => ( + + {isActive ? ( + sortOrder === "asc" ? ( + + ) : ( + + ) + ) : null} + + ); + const headers: Header[] = [ + { + id: "name", + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.name")} + {renderSortIcon(sortField === "name")} + + ), + render: (row) => { + return row.name; + }, + }, + { + id: "status", + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.status")} + {renderSortIcon(sortField === "status")} + + ), + render: (row) => { + return ; + }, + }, + { + id: "cpu", + content: t("pages.infrastructure.table.headers.cpu"), + render: (row) => { + const check = row.recentChecks?.[0]; + const cpuUsage = (check?.cpu?.usage_percent || 0) * 100; + return ; + }, + }, + { + id: "memory", + content: t("pages.infrastructure.table.headers.memory"), + render: (row) => { + const check = row.recentChecks?.[0]; + const memoryUsage = (check?.memory?.usage_percent || 0) * 100; + return ; + }, + }, + { + id: "disk", + content: t("pages.infrastructure.table.headers.disk"), + render: (row) => { + const check = row.recentChecks?.[0]; + + const totalDiskUsage = check?.disk?.reduce( + (acc, disk) => acc + (disk?.usage_percent || 0), + 0 + ); + const diskCount = check?.disk?.length || 1; + const diskUsage = ((totalDiskUsage || 0) / diskCount) * 100; + return ; + }, + }, + + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + let headers = getHeaders(); + + if (isSmall) { + headers = headers.filter((h) => h.id !== "histogram"); + } + return ( + + { + navigate(`/infrastructure/${row.id}`); + }} + /> + + + ); +}; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx deleted file mode 100644 index d12ef1d24..000000000 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx +++ /dev/null @@ -1,162 +0,0 @@ -// Components -import { Box } from "@mui/material"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import Host from "@/Components/v1/Host/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { Stack } from "@mui/material"; -import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx"; -import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx"; -// Assets -import Icon from "@/Components/v1/Icon"; -import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; -import { useNavigate } from "react-router-dom"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -const MonitorsTable = ({ - isLoading, - monitors, - isAdmin, - handleActionMenuDelete, - isSearching, -}) => { - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - const { determineState } = useMonitorUtils(); - const navigate = useNavigate(); - - // Handlers - const openDetails = (id) => { - navigate(`/infrastructure/${id}`); - }; - const headers = [ - { - id: "host", - content: t("host"), - render: (row) => ( - - ), - }, - { - id: "status", - content: t("incidentsTableStatus"), - render: (row) => ( - - ), - }, - { - id: "frequency", - content: t("frequency"), - render: (row) => ( - - - {row.processor} - - ), - }, - { id: "cpu", content: t("cpu"), render: (row) => }, - { - id: "memory", - content: t("memory"), - render: (row) => , - }, - { - id: "disk", - content: t("disk"), - render: (row) => , - }, - { - id: "actions", - content: t("actions"), - render: (row) => ( - - ), - }, - ]; - - const data = monitors?.map((monitor) => { - const processor = - ((monitor.recentChecks?.[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz"; - const cpu = (monitor?.recentChecks?.[0]?.cpu?.usage_percent ?? 0) * 100; - const mem = (monitor?.recentChecks?.[0]?.memory?.usage_percent ?? 0) * 100; - const disk = (monitor?.recentChecks?.[0]?.disk?.[0]?.usage_percent ?? 0) * 100; - const status = determineState(monitor); - const percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.error.main - : monitor.uptimePercentage < 0.5 - ? theme.palette.warning.main - : theme.palette.success.main; - - return { - id: monitor.id, - name: monitor.name, - url: monitor.url, - processor, - cpu, - mem, - disk, - status, - percentageColor, - }; - }); - - return ( - - - openDetails(row.id), - emptyView: "No monitors found", - }} - /> - - ); -}; - -MonitorsTable.propTypes = { - isLoading: PropTypes.bool, - monitors: PropTypes.array, - isAdmin: PropTypes.bool, - handleActionMenuDelete: PropTypes.func, - isSearching: PropTypes.bool, -}; - -export default MonitorsTable; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx deleted file mode 100644 index 50af0aaa2..000000000 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx +++ /dev/null @@ -1,196 +0,0 @@ -/* TODO I basically copied and pasted this component from the actionsMenu. Check how we can make it reusable */ - -import { useRef, useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { createToast } from "@/Utils/toastUtils.jsx"; -import { IconButton, Menu, MenuItem } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; -import PropTypes from "prop-types"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import { networkService } from "@/Utils/NetworkService.js"; -import { usePauseMonitor } from "@/Hooks/monitorHooks.js"; -import { useTranslation } from "react-i18next"; - -/** - * InfrastructureMenu Component - * Provides a dropdown menu for managing infrastructure monitors. - * - * @param {Object} props - The component props. - * @param {Object} props.monitor - The monitor object containing details about the infrastructure monitor. - * @param {string} props.monitor.id - Unique ID of the monitor. - * @param {string} [props.monitor.url] - URL associated with the monitor. - * @param {string} props.monitor.type - Type of monitor (e.g., uptime, infrastructure). - * @param {boolean} props.monitor.isActive - Indicates if the monitor is currently active (true) or paused (false). - * @param {boolean} props.isAdmin - Whether the user has admin privileges. - * @param {Function} props.updateCallback - Callback to trigger when the monitor data is updated. - * @returns {JSX.Element} The rendered component. - */ -const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => { - const anchor = useRef(null); - const [isOpen, setIsOpen] = useState(false); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const theme = useTheme(); - const [pauseMonitor] = usePauseMonitor(); - const { t } = useTranslation(); - - const openMenu = (e) => { - e.stopPropagation(); - setIsOpen(true); - }; - - const closeMenu = (e) => { - e.stopPropagation(); - setIsOpen(false); - }; - - const openRemove = (e) => { - closeMenu(e); - setIsDialogOpen(true); - }; - const cancelRemove = () => { - setIsDialogOpen(false); - }; - - const navigate = useNavigate(); - - function openDetails(id) { - navigate(`/infrastructure/${id}`); - } - - const openConfigure = (id) => { - navigate(`/infrastructure/configure/${id}`); - }; - - const handlePause = async () => { - // Pass updateCallback as triggerUpdate to the hook - await pauseMonitor({ monitorId: monitor.id, triggerUpdate: updateCallback }); - // Toast is already displayed in the hook, no need to display it again - }; - - const handleRemove = async () => { - try { - await networkService.deleteMonitorById({ - monitorId: monitor.id, - }); - createToast({ - body: t("monitorActions.deleteSuccess"), - }); - } catch (error) { - createToast({ body: t("monitorActions.deleteFailed") }); - } finally { - setIsDialogOpen(false); - updateCallback(); - } - }; - - return ( - <> - - - - - - { - e.stopPropagation(); - openDetails(monitor.id); - closeMenu(e); - }} - > - {t("monitorActions.details")} - - {isAdmin && ( - { - e.stopPropagation(); - openConfigure(monitor.id); - closeMenu(e); - }} - > - {t("configure")} - - )} - {isAdmin && ( - { - e.stopPropagation(); - await handlePause(); - closeMenu(e); - }} - > - {!monitor.isActive ? t("resume") : t("pause")} - - )} - {isAdmin && ( - - {t("remove")} - - )} - - - - ); -}; - -InfrastructureMenu.propTypes = { - monitor: PropTypes.shape({ - id: PropTypes.string.isRequired, - url: PropTypes.string, - // Note: type must remain optional. Making it required (type: PropTypes.string.isRequired) - // causes runtime errors as some monitors don't have a defined type property - type: PropTypes.string, - isActive: PropTypes.bool, // Determines whether the monitor is paused (false) or active (true) - status: PropTypes.string, // Represents the monitor's operational status (e.g., 'up', 'down', etc.) - }).isRequired, - isAdmin: PropTypes.bool.isRequired, - updateCallback: PropTypes.func.isRequired, -}; - -export { InfrastructureMenu }; diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx deleted file mode 100644 index e4add415c..000000000 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ /dev/null @@ -1,139 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; -import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; -import MonitorsTable from "./Components/MonitorsTable/index.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; -import Filter from "./Components/Filters/index.jsx"; -import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/index.jsx"; -// Utils -import { useTheme } from "@emotion/react"; -import { useEffect, useState } from "react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useTranslation } from "react-i18next"; -import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; - -import { useDispatch, useSelector } from "react-redux"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; -// Constants -const TYPES = ["hardware"]; -const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }]; - -const InfrastructureMonitors = () => { - // Redux state - const rowsPerPage = useSelector((state) => state.ui?.infrastructure?.rowsPerPage ?? 5); - const dispatch = useDispatch(); - - // Local state - const [page, setPage] = useState(0); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [selectedStatus, setSelectedStatus] = useState(undefined); - const [toFilterStatus, setToFilterStatus] = useState(undefined); - const [search, setSearch] = useState(undefined); - const [isSearching, setIsSearching] = useState(false); - - // Utils - const theme = useTheme(); - const isAdmin = useIsAdmin(); - const { t } = useTranslation(); - - // Handlers - const handleActionMenuDelete = () => { - setUpdateTrigger(!updateTrigger); - }; - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: "infrastructure", - }) - ); - setPage(0); - }; - - useEffect(() => { - if (isSearching) { - setPage(0); - } - }, [isSearching]); - - const handleReset = () => { - setSelectedStatus(undefined); - setToFilterStatus(undefined); - }; - - const field = toFilterStatus !== undefined ? "status" : undefined; - - const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ - types: TYPES, - limit: 1, - page: page, - field: field, - filter: toFilterStatus ?? search, - rowsPerPage: rowsPerPage, - monitorUpdateTrigger: updateTrigger, - }); - - return ( - <> - - - - - - - - - - - - - - - - ); -}; - -export default InfrastructureMonitors; diff --git a/client/src/Pages/Infrastructure/Monitors/index.tsx b/client/src/Pages/Infrastructure/Monitors/index.tsx new file mode 100644 index 000000000..bdf477613 --- /dev/null +++ b/client/src/Pages/Infrastructure/Monitors/index.tsx @@ -0,0 +1,195 @@ +import Stack from "@mui/material/Stack"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { MonitorBasePageWithStates } from "@/Components/design-elements"; +import { HeaderCreate } from "@/Components/common"; +import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors"; +import { TextField, Dialog } from "@/Components/inputs"; + +import { useGet, useDelete } from "@/Hooks/UseApi"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import type { Monitor, MonitorsWithChecksResponse } from "@/Types/Monitor"; +import { useTheme } from "@mui/material"; +import { useState, useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector, useDispatch } from "react-redux"; +import { setRowsPerPage } from "@/Features/UI/uiSlice.js"; +import type { RootState } from "@/Types/state"; +import { InfraMonitorsTable } from "./Components/MonitorsTable"; +import useDebounce from "@/Hooks/useDebounce"; + +const InfrastructureMonitors = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const isAdmin = useIsAdmin(); + const dispatch = useDispatch(); + + // Redux state + const rowsPerPage = useSelector( + (state: RootState) => state.ui?.infrastructure?.rowsPerPage ?? 5 + ); + + // Local state + const [selectedStatus, setSelectedStatus] = useState(""); + const [selectedState, setSelectedState] = useState(""); + const [search, setSearch] = useState(""); + const [page, setPage] = useState(0); + const [sortField, setSortField] = useState(""); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); + + const debouncedSearch = useDebounce(search, 300); + + const handleClearFilters = useCallback(() => { + setSelectedStatus(""); + setSelectedState(""); + setSearch(""); + }, []); + + // Convert filter selections to API filter values + const toFilterStatus = useMemo(() => { + if (selectedStatus === "up") return "true"; + if (selectedStatus === "down") return "false"; + return undefined; + }, [selectedStatus]); + + const toFilterActive = useMemo(() => { + if (selectedState === "active") return "true"; + if (selectedState === "paused") return "false"; + return undefined; + }, [selectedState]); + + // Determine field and filter for the API request + // Priority: status > isActive > search + const filterLookup = new Map([ + [toFilterStatus, "status"], + [toFilterActive, "isActive"], + ]); + const activeFilter = [...filterLookup].find(([key]) => key !== undefined); + const field = activeFilter?.[1] || (debouncedSearch ? "name" : sortField || undefined); + const filter = activeFilter?.[0] || debouncedSearch || undefined; + + // Build URL for monitors with checks + const monitorsWithChecksUrl = useMemo(() => { + const params = new URLSearchParams(); + params.append("type", "hardware"); + params.append("limit", "1"); + if (page !== undefined) params.append("page", String(page)); + if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage)); + if (filter) params.append("filter", filter); + if (field) params.append("field", field); + if (sortOrder) params.append("order", sortOrder); + return `/monitors/team/with-checks?${params.toString()}`; + }, [page, rowsPerPage, filter, field, sortOrder]); + + const { + data: monitorsWithChecksData, + isLoading: monitorsWithChecksLoading, + error: monitorsWithChecksError, + refetch, + } = useGet( + monitorsWithChecksUrl, + {}, + { refreshInterval: 5000, keepPreviousData: true } + ); + + const { summary, count } = monitorsWithChecksData ?? { summary: null, count: 0 }; + const isLoading = monitorsWithChecksLoading; + + // Check if any filters are active + const hasActiveFilters = Boolean(selectedStatus || selectedState || search); + + // Show empty state only when there are truly no monitors (not just filtered out) + const effectiveTotalCount = + hasActiveFilters && (summary?.totalMonitors ?? 0) === 0 + ? 1 + : (summary?.totalMonitors ?? 0); + + // Delete hook + const { deleteFn, loading: isDeleting } = useDelete(); + + const handleConfirm = async () => { + if (!selectedMonitor) return; + await deleteFn(`/monitors/${selectedMonitor.id}`); + setSelectedMonitor(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + + return ( + + + + + + { + setSearch(event.target.value); + }} + /> + + { + dispatch( + setRowsPerPage({ + value, + table: "infrastructure", + }) + ); + setPage(0); + }} + /> + + + ); +}; + +export default InfrastructureMonitors; diff --git a/client/src/Pages/Integrations/index.css b/client/src/Pages/Integrations/index.css deleted file mode 100644 index 29969577a..000000000 --- a/client/src/Pages/Integrations/index.css +++ /dev/null @@ -1,10 +0,0 @@ -.integrations h1.MuiTypography-root { - font-size: var(--env-var-font-size-large); - font-weight: 600; -} -.integrations p.MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} -.integrations button { - height: var(--env-var-height-2); -} diff --git a/client/src/Pages/Integrations/index.jsx b/client/src/Pages/Integrations/index.jsx deleted file mode 100644 index dde6b52ee..000000000 --- a/client/src/Pages/Integrations/index.jsx +++ /dev/null @@ -1,156 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack, Typography, Grid, Button } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import Discord from "../../../assets/icons/discord-icon.svg?react"; -import Slack from "../../../assets/icons/slack-icon.svg?react"; -import Zapier from "../../../assets/icons/zapier-icon.svg?react"; -import { useTranslation } from "react-i18next"; - -import "./index.css"; - -/** - * Integrations component - * @param {Object} props - Props for the IntegrationsComponent. - * @param {string} props.icon - The icon for the integration image. - * @param {string} props.header - The header for the integration. - * @param {string} props.info - Information about the integration. - * @param {Function} props.onClick - The onClick handler for the integration button. - * @returns {JSX.Element} The JSX representation of the IntegrationsComponent. - */ -const IntegrationsComponent = ({ icon, header, info, onClick }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - {icon} - - {header} - - {info} - - - - - - ); -}; - -// PropTypes for IntegrationsComponent -IntegrationsComponent.propTypes = { - icon: PropTypes.object.isRequired, - header: PropTypes.string.isRequired, - info: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, -}; - -/** - * Integrations Page Component - * @returns {JSX.Element} The JSX representation of the Integrations page. - */ - -const Integrations = () => { - const theme = useTheme(); - const { t } = useTranslation(); - - const integrations = [ - { - icon: ( - - ), - header: t("integrationsSlack"), - info: t("integrationsSlackInfo"), - onClick: () => {}, - }, - { - icon: ( - - ), - header: t("integrationsDiscord"), - info: t("integrationsDiscordInfo"), - onClick: () => {}, - }, - { - icon: ( - - ), - header: t("integrationsZapier"), - info: t("integrationsZapierInfo"), - onClick: () => {}, - }, - // Add more integrations as needed - ]; - - return ( - - {t("integrations")} - {t("integrationsPrism")} - - {integrations.map((integration, index) => ( - - ))} - - - ); -}; - -export default Integrations; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx deleted file mode 100644 index be6dc99aa..000000000 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import Stack from "@mui/material/Stack"; -import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx"; -import Typography from "@mui/material/Typography"; - -// Utils -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; -import { getPercentage, formatBytes } from "../../utils/utils.js"; -import { useTranslation } from "react-i18next"; -import { Box } from "@mui/material"; - -const BaseContainer = ({children}) => { - const theme = useTheme() - return( - - {children} - - ); -}; - -const InfrastructureStyleGauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { - const theme = useTheme(); - - const MetricRow = ({ label, value }) => ( - - {label} - - {value} - - - ); - - return( - - - - - - {heading} - - - - - {metricTwo && valueTwo && ( - - )} - - - - ); -}; - -const Gauges = ({ diagnostics, isLoading }) => { - const heapTotalSize = getPercentage( - diagnostics?.v8HeapStats?.totalHeapSizeBytes, - diagnostics?.v8HeapStats?.heapSizeLimitBytes - ); - - const heapUsedSize = getPercentage( - diagnostics?.v8HeapStats?.usedHeapSizeBytes, - diagnostics?.v8HeapStats?.heapSizeLimitBytes - ); - - const actualHeapUsed = getPercentage( - diagnostics?.v8HeapStats?.usedHeapSizeBytes, - diagnostics?.v8HeapStats?.totalHeapSizeBytes - ); - - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - - - - - ); -}; - -Gauges.propTypes = { - diagnostics: PropTypes.object, - isLoading: PropTypes.bool, -}; - -InfrastructureStyleGauge.propTypes = { - value: PropTypes.number, - heading: PropTypes.string, - metricOne: PropTypes.string, - valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - metricTwo: PropTypes.string, - valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), -}; - -BaseContainer.propTypes = { - children: PropTypes.node.isRequired, -}; - -export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx deleted file mode 100644 index 13ed63618..000000000 --- a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; -import CircularProgress from "@mui/material/CircularProgress"; - -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; - -import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; -import { formatBytes } from "../../utils/utils.js"; -import { useTranslation } from "react-i18next"; - -const StatsCard = ({ title, value, unit = "", isLoading }) => { - const theme = useTheme(); - return ( - - {isLoading ? ( - - - - ) : ( - - - {title} - - - {value} {unit} - - - )} - - ); -}; - -StatsCard.propTypes = { - title: PropTypes.string, - value: PropTypes.string, - unit: PropTypes.string, - isLoading: PropTypes.bool, -}; - -const Stats = ({ diagnostics, isLoading }) => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( - - - - - - - - - - - ); -}; - -Stats.propTypes = { - diagnostics: PropTypes.object, - isLoading: PropTypes.bool, -}; - -export default Stats; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx deleted file mode 100644 index 194e13821..000000000 --- a/client/src/Pages/Logs/Diagnostics/index.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import Gauges from "./components/gauges/index.jsx"; -import Button from "@mui/material/Button"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { useFetchDiagnostics } from "../../../Hooks/logHooks.js"; -import { getHumanReadableDuration } from "../../../Utils/timeUtilsLegacy.js"; -import { formatBytes, getPercentage } from "./utils/utils.js"; - -const Diagnostics = () => { - // Local state - - // Hooks - const theme = useTheme(); - const { t } = useTranslation(); - const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics(); - // Setup - return ( - - - - - - - - - - - - - - - ); -}; - -export default Diagnostics; diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/Logs/Diagnostics/utils/utils.js deleted file mode 100644 index a3b75631f..000000000 --- a/client/src/Pages/Logs/Diagnostics/utils/utils.js +++ /dev/null @@ -1,16 +0,0 @@ -export const getPercentage = (value, total) => { - if (!value || !total) return 0; - return (value / total) * 100; -}; - -export const formatBytes = (bytes) => { - if (!bytes) return "N/A"; - - if (bytes === 0) return "0 Bytes"; - - if (bytes >= 1024 * 1024 * 1024) { - return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; - } - - return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; -}; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx deleted file mode 100644 index d0319a387..000000000 --- a/client/src/Pages/Logs/Logs/index.jsx +++ /dev/null @@ -1,176 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import Typography from "@mui/material/Typography"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; - -import { useFetchLogs } from "../../../Hooks/logHooks.js"; -import { useTheme } from "@emotion/react"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; - -const LevelBadge = ({ level }) => { - const theme = useTheme(); - - const levelColors = { - info: theme.palette.success.main, - warn: theme.palette.warning.main, - error: theme.palette.error.main, - debug: theme.palette.accent.main, - }; - - const color = levelColors[level] || theme.palette.primary.contrastText; - - return ( - - {level} - - ); -}; - -const formatTimestamp = (timestamp) => { - if (!timestamp) return "-"; - const date = new Date(timestamp); - return date.toLocaleString(); -}; - -const Logs = () => { - const [logLevel, setLogLevel] = useState("all"); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(15); - - const theme = useTheme(); - const { t } = useTranslation(); - const [logs, isLoading, error] = useFetchLogs(); - - const LOG_LEVELS = [ - { _id: "all", name: t("logsPage.logLevelSelect.values.all") }, - { _id: "info", name: t("logsPage.logLevelSelect.values.info") }, - { _id: "warn", name: t("logsPage.logLevelSelect.values.warn") }, - { _id: "error", name: t("logsPage.logLevelSelect.values.error") }, - { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") }, - ]; - - const headers = [ - { - id: "timestamp", - content: t("logsPage.table.timestamp"), - render: (row) => ( - - {formatTimestamp(row.timestamp)} - - ), - }, - { - id: "level", - content: t("logsPage.table.level"), - render: (row) => , - }, - { - id: "service", - content: t("logsPage.table.service"), - render: (row) => ( - {row.service || "-"} - ), - }, - { - id: "method", - content: t("logsPage.table.method"), - render: (row) => ( - {row.method || "-"} - ), - }, - { - id: "message", - content: t("logsPage.table.message"), - render: (row) => ( - - {row.message || "-"} - - ), - }, - ]; - - const filteredLogs = logs - ?.filter((log) => { - if (logLevel === "all") return true; - return log.level === logLevel; - }) - .reverse() - .map((log, idx) => ({ ...log, id: idx })); - - const paginatedLogs = filteredLogs?.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ); - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - const handleLogLevelChange = (e) => { - setLogLevel(e.target.value); - setPage(0); - }; - - return ( - - - {t("logsPage.logLevelSelect.title")} - setSelectedLogLevel(e.target.value)} + > + {LOG_LEVEL_OPTIONS.map((level) => { + return ( + + {level} + + ); + })} + + + + ); +}; diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx new file mode 100644 index 000000000..06ce8ed0c --- /dev/null +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -0,0 +1,60 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { TableJobs, TableFailedJobs } from "./components/TableJobs"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useGet, usePost } from "@/Hooks/UseApi"; +import type { QueueData } from "@/Types/Queue"; +import { Metrics } from "@/Pages/Logs/components/Metrics"; +import { Button } from "@/Components/inputs"; + +export const TabQueue = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const { + data: queueData, + // isLoading, + // error, + refetch, + } = useGet("/queue/all-metrics", {}, { refreshInterval: 5000 }); + + const { post, loading: isFlushing } = usePost(); + + const jobs = queueData?.jobs ?? []; + const metrics = queueData?.metrics ?? null; + + const handleFlushQueue = async () => { + await post("/queue/flush", {}); + refetch(); + }; + + return ( + + + + {t("pages.logs.jobQueue")} + + + + {t("pages.logs.failedJobs")} + + + + + + + ); +}; diff --git a/client/src/Pages/Logs/components/Metrics.tsx b/client/src/Pages/Logs/components/Metrics.tsx new file mode 100644 index 000000000..9705b9ecd --- /dev/null +++ b/client/src/Pages/Logs/components/Metrics.tsx @@ -0,0 +1,50 @@ +import Stack from "@mui/material/Stack"; +import { StatBox } from "@/Components/design-elements"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import type { QueueMetrics } from "@/Types/Queue"; + +interface MetricsProps { + metrics: QueueMetrics | null; +} + +export const Metrics = ({ metrics }: MetricsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + + if (!metrics) { + return null; + } + + return ( + + + 0 ? "success" : undefined} + title={t("pages.logs.metrics.activeJobs")} + subtitle={String(metrics.activeJobs)} + /> + 0 ? "error" : undefined} + title={t("pages.logs.metrics.failingJobs")} + subtitle={String(metrics.failingJobs)} + /> + + 0 ? "error" : undefined} + title={t("pages.logs.metrics.totalFailures")} + subtitle={String(metrics.totalFailures)} + /> + + ); +}; diff --git a/client/src/Pages/Logs/components/StatGauges.tsx b/client/src/Pages/Logs/components/StatGauges.tsx new file mode 100644 index 000000000..65c443831 --- /dev/null +++ b/client/src/Pages/Logs/components/StatGauges.tsx @@ -0,0 +1,72 @@ +import Stack from "@mui/material/Stack"; +import { DetailGauge } from "@/Components/design-elements"; + +import { getPercentage, formatPercentageFromWhole } from "@/Utils/FormatUtils"; +import prettyBytes from "pretty-bytes"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material"; +import type { Diagnostics } from "@/Types/Diagnostics"; + +interface StatGaugesProps { + diagnostics: Diagnostics | null; +} + +export const StatGauges = ({ diagnostics }: StatGaugesProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + if (!diagnostics) { + return null; + } + + const heapTotalSize = getPercentage( + diagnostics?.v8HeapStats?.totalHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes + ); + + const heapUsedSize = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes + ); + + const actualHeapUsed = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.totalHeapSizeBytes + ); + + return ( + + + + + + + ); +}; diff --git a/client/src/Pages/Logs/components/Stats.tsx b/client/src/Pages/Logs/components/Stats.tsx new file mode 100644 index 000000000..7bf343de3 --- /dev/null +++ b/client/src/Pages/Logs/components/Stats.tsx @@ -0,0 +1,51 @@ +import Stack from "@mui/material/Stack"; +import { StatBox } from "@/Components/design-elements"; + +import prettyBytes from "pretty-bytes"; +import prettyMilliseconds from "pretty-ms"; +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import type { Diagnostics } from "@/Types/Diagnostics"; + +interface StatsProps { + diagnostics: Diagnostics | null; +} + +export const Stats = ({ diagnostics }: StatsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + + if (!diagnostics) { + return null; + } + + return ( + + + + + + + + ); +}; diff --git a/client/src/Pages/Logs/components/TableJobs.tsx b/client/src/Pages/Logs/components/TableJobs.tsx new file mode 100644 index 000000000..660a5df4f --- /dev/null +++ b/client/src/Pages/Logs/components/TableJobs.tsx @@ -0,0 +1,164 @@ +import { Table } from "@/Components/design-elements"; +import { Typography, useTheme } from "@mui/material"; +import prettyMilliseconds from "pretty-ms"; +import { formatTimestamp } from "@/Utils/TimeUtils"; + +import { useTranslation } from "react-i18next"; +import type { QueueJobFailure, QueueJobSummary, QueueMetrics } from "@/Types/Queue"; +import type { Header } from "@/Components/design-elements"; + +type QueueJobWithId = QueueJobSummary & { id: string | number }; + +interface TableJobsProps { + jobs: QueueJobSummary[]; +} + +export const TableJobs = ({ jobs }: TableJobsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const jobsWithId: QueueJobWithId[] = jobs.map((job) => ({ + ...job, + id: job.monitorId, + })); + + const headers: Header[] = [ + { + id: "id", + content: t("common.table.headers.monitorId"), + render: (row) => {row.monitorId}, + }, + { + id: "url", + content: t("common.table.headers.url"), + render: (row) => {row.monitorUrl}, + }, + { + id: "interval", + content: t("common.table.headers.interval"), + render: (row) => ( + {prettyMilliseconds(row.monitorInterval ?? 0)} + ), + }, + { + id: "type", + content: t("common.table.headers.type"), + render: (row) => {row.monitorType}, + }, + { + id: "active", + content: t("common.table.headers.active"), + render: (row) => {row.active.toString()}, + }, + { + id: "runCount", + content: t("pages.logs.table.headers.runCount"), + render: (row) => {row.runCount}, + }, + { + id: "failCount", + content: t("pages.logs.table.headers.failCount"), + render: (row) => {row.failCount}, + }, + { + id: "lastRun", + content: t("pages.logs.table.headers.lastRunAt"), + render: (row) => {formatTimestamp(row.lastRunAt)}, + }, + { + id: "lockedAt", + content: t("pages.logs.table.headers.lockedAt"), + render: (row) => {formatTimestamp(row.lockedAt)}, + }, + + { + id: "lastFinish", + content: t("pages.logs.table.headers.lastFinishedAt"), + render: (row) => {formatTimestamp(row.lastFinishedAt)}, + }, + { + id: "lastRunTook", + content: t("pages.logs.table.headers.lastRunTook"), + render: (row) => { + const value = row.lastRunTook ? prettyMilliseconds(row.lastRunTook) : "-"; + return {value}; + }, + }, + ]; + + return ( +
({ + ...(row.lockedAt && { + "& td": { backgroundColor: theme.palette.success.light }, + }), + })} + /> + ); +}; + +type QueueJobFailureWithId = QueueJobFailure & { id: string | number }; + +interface TableFailedJobsProps { + metrics: QueueMetrics | null; +} +export const TableFailedJobs = ({ metrics }: TableFailedJobsProps) => { + const { t } = useTranslation(); + if (!metrics) { + return null; + } + + const jobFailuresWithId: QueueJobFailureWithId[] = metrics.jobsWithFailures.map( + (job) => ({ + ...job, + id: job.monitorId, + }) + ); + + const headers: Header[] = [ + { + id: "monitorId", + content: t("common.table.headers.monitorId"), + render: (row) => { + return {row.monitorId}; + }, + }, + { + id: "monitorUrl", + content: t("common.table.headers.url"), + render: (row) => { + return {row.monitorUrl}; + }, + }, + { + id: "failCount", + content: t("pages.logs.table.headers.failCount"), + render: (row) => { + return {row.failCount}; + }, + }, + { + id: "failedAt", + content: t("pages.logs.table.headers.lastFailedAt"), + render: (row) => { + return {formatTimestamp(row.failedAt)}; + }, + }, + { + id: "failReason", + content: t("pages.logs.table.headers.failReason"), + render: (row) => { + return {row.failReason}; + }, + }, + ]; + + return ( +
+ ); +}; diff --git a/client/src/Pages/Logs/components/TableLogs.tsx b/client/src/Pages/Logs/components/TableLogs.tsx new file mode 100644 index 000000000..144d6056c --- /dev/null +++ b/client/src/Pages/Logs/components/TableLogs.tsx @@ -0,0 +1,128 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Table } from "@/Components/design-elements"; +import { Pagination } from "@/Components/design-elements/Table"; +import { useTheme } from "@mui/material"; +import type { Header } from "@/Components/design-elements/Table"; +import type { Log, LogLevel } from "@/Types/Log"; +import { useTranslation } from "react-i18next"; +import { useSelector, useDispatch } from "react-redux"; +import type { RootState } from "@/Types/state"; +import { setRowsPerPage } from "@/Features/UI/uiSlice"; +import { formatTimestamp } from "@/Utils/TimeUtils"; + +type LogWithId = Log & { id: number }; + +interface TableLogsProps { + logs: LogWithId[]; + logCount: number; + page: number; + setPage: (page: number) => void; +} + +const LevelBadge = ({ level }: { level: LogLevel }) => { + const theme = useTheme(); + + const levelColors: Record = { + info: theme.palette.success.main, + warn: theme.palette.warning.main, + error: theme.palette.error.main, + debug: theme.palette.info.main, + }; + + const color = levelColors[level] || theme.palette.text.primary; + + return ( + + {level} + + ); +}; + +export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const rowsPerPage = useSelector( + (state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15 + ); + + const headers: Header[] = [ + { + id: "timestamp", + content: t("pages.logs.table.headers.timestamp"), + render: (row) => ( + + {formatTimestamp(row.timestamp)} + + ), + }, + { + id: "level", + content: t("pages.logs.table.headers.level"), + render: (row) => , + }, + { + id: "service", + content: t("pages.logs.table.headers.service"), + render: (row) => {row.service || "-"}, + }, + { + id: "method", + content: t("pages.logs.table.headers.method"), + render: (row) => ( + {row.method || "-"} + ), + }, + { + id: "message", + content: t("common.table.headers.message"), + render: (row) => ( + + {row.message || "-"} + + ), + }, + ]; + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + dispatch(setRowsPerPage({ value: Number(e.target.value), table: "logs" })); + setPage(0); + }; + + return ( + +
+ + + ); +}; diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/Logs/index.jsx deleted file mode 100644 index e5bdf623d..000000000 --- a/client/src/Pages/Logs/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Tabs from "@mui/material/Tabs"; -import Tab from "@mui/material/Tab"; -import Queue from "./Queue/index.jsx"; -import LogsComponent from "./Logs/index.jsx"; -import Diagnostics from "./Diagnostics/index.jsx"; - -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { useState } from "react"; - -const Logs = () => { - const { t } = useTranslation(); - const theme = useTheme(); - - // Local state - const [value, setValue] = useState(0); - - // Handlers - const handleChange = (event, newValue) => { - setValue(newValue); - }; - - const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }]; - return ( - - - - - - - - {value === 0 && } - {value === 1 && } - {value === 2 && } - - ); -}; - -export default Logs; diff --git a/client/src/Pages/Logs/index.tsx b/client/src/Pages/Logs/index.tsx new file mode 100644 index 000000000..101ac11af --- /dev/null +++ b/client/src/Pages/Logs/index.tsx @@ -0,0 +1,29 @@ +import { BasePage, Tabs, Tab } from "@/Components/design-elements"; +import { TabLogs } from "@/Pages/Logs/TabLogs"; +import { TabQueue } from "@/Pages/Logs/TabQueue"; +import { TabDiagnostics } from "@/Pages/Logs/TabDiagnostics"; + +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +const LogsPage = () => { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState(2); + return ( + + setActiveTab(newValue)} + > + + + + + {activeTab === 0 && } + {activeTab === 1 && } + {activeTab === 2 && } + + ); +}; + +export default LogsPage; diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx deleted file mode 100644 index 8d4c0ae53..000000000 --- a/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import PropTypes from "prop-types"; -const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => { - const getValueById = (config, id) => { - const item = config.find((config) => config._id === id); - return item ? (item.value ? item.value : item.name) : null; - }; - - const getIdByValue = (config, name) => { - const item = config.find((config) => { - if (config.value) { - return config.value === name; - } else { - return config.name === name; - } - }); - return item ? item._id : null; - }; - - return ( -
navigate(`/maintenance/create/${row.id}`)} + emptyViewText={t("common.table.empty")} + /> + + { + setDeleteDialogOpen(false); + setSelectedWindow(null); + }} + onConfirm={handleDelete} + loading={deleteLoading} + /> + + ); +}; diff --git a/client/src/Pages/Maintenance/create/index.tsx b/client/src/Pages/Maintenance/create/index.tsx new file mode 100644 index 000000000..b6666d4e2 --- /dev/null +++ b/client/src/Pages/Maintenance/create/index.tsx @@ -0,0 +1,368 @@ +import Stack from "@mui/material/Stack"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import IconButton from "@mui/material/IconButton"; +import { BasePage, ConfigBox } from "@/Components/design-elements"; +import { + TextField, + Select, + DatePicker, + TimePicker, + Button, + Autocomplete, +} from "@/Components/inputs"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import dayjs from "dayjs"; +import type { MaintenanceWindow } from "@/Types/MaintenanceWindow"; +import type { MaintenanceWindowFormData } from "@/Validation/maintenanceWindow"; +import { repeatOptions, durationUnitOptions } from "@/Validation/maintenanceWindow"; +import { useMaintenanceWindowForm } from "@/Hooks/useMaintenanceWindowForm"; +import { useGet, usePost, usePatch } from "@/Hooks/UseApi"; +import { useParams, useNavigate } from "react-router-dom"; +import type { Monitor } from "@/Types/Monitor"; +import { useForm, Controller } from "react-hook-form"; +import { useEffect } from "react"; +import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; +import { Trash2 } from "lucide-react"; + +const CreateMaintenanceWindowPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const { maintenanceWindowId } = useParams<{ maintenanceWindowId: string }>(); + const isEditMode = Boolean(maintenanceWindowId); + + const { data: existingMaintenanceWindow } = useGet( + isEditMode ? `/maintenance-window/${maintenanceWindowId}` : null, + {}, + { keepPreviousData: false } + ); + + const { data: monitors } = useGet("/monitors/team"); + + const { post, loading: isPosting } = usePost(); + const { patch, loading: isPatching } = usePatch(); + + const { schema, defaults } = useMaintenanceWindowForm({ + data: existingMaintenanceWindow, + }); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + + const { control, handleSubmit, trigger } = form; + useEffect(() => { + if (existingMaintenanceWindow) { + form.reset(defaults); + } + }, [existingMaintenanceWindow, defaults, form]); + + const buildPayload = (data: MaintenanceWindowFormData) => { + const startDateTime = dayjs(data.startDate) + .set("hour", parseInt(data.startTime.split(":")[0], 10)) + .set("minute", parseInt(data.startTime.split(":")[1], 10)); + + const durationUnit = durationUnitOptions.find((o) => o.id === data.durationUnit); + const durationInMs = data.duration * (durationUnit?.multiplier ?? 1000); + const endDateTime = startDateTime.add(durationInMs, "milliseconds"); + + const repeatOption = repeatOptions.find((o) => o.id === data.repeat); + const repeat = repeatOption?.value ?? 0; + + const payload: Record = { + name: data.name, + duration: data.duration, + durationUnit: data.durationUnit, + monitors: data.monitors, + start: startDateTime.toISOString(), + end: endDateTime.toISOString(), + repeat, + }; + + if (repeat === 0) { + payload.expiry = endDateTime.toISOString(); + } + + return payload; + }; + + const onSubmit = async (data: MaintenanceWindowFormData) => { + const payload = buildPayload(data); + + let result; + if (isEditMode && maintenanceWindowId) { + result = await patch(`/maintenance-window/${maintenanceWindowId}`, payload); + } else { + result = await post("/maintenance-window", payload); + } + + if (result?.success) { + navigate("/maintenance"); + } + }; + + const isLoading = isPosting || isPatching; + + const onError = (errors: any) => { + console.error("Form submission errors:", errors); + }; + + return ( + + + ( + + )} + /> + ( + + )} + /> + + } + /> + + ( + { + field.onChange(date ? date.format("YYYY-MM-DD") : ""); + trigger("startDate"); + }} + error={!!fieldState.error} + helperText={fieldState.error?.message} + /> + )} + /> + + } + /> + + ( + { + field.onChange(time ? time.format("HH:mm") : ""); + trigger("startDate"); + }} + /> + )} + /> + + ( + { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} + error={!!fieldState.error} + helperText={fieldState.error?.message ?? ""} + sx={{ width: 120 }} + /> + )} + /> + ( + + )} + /> + + + } + /> + { + const monitorsList = monitors ?? []; + const selectedMonitors = field.value + .map((id: string) => monitorsList.find((m) => m.id === id)) + .filter((m): m is Monitor => m !== undefined); + + return ( + + option.name} + value={selectedMonitors} + onChange={(_, newValue) => { + field.onChange(newValue.map((m: Monitor) => m.id)); + }} + fieldLabel={t( + "pages.maintenanceWindow.form.startTime.monitors.option.addMonitors.label" + )} + renderInput={(params) => ( + + )} + /> + {selectedMonitors.length > 0 && ( + + {selectedMonitors.map((monitor) => ( + + {monitor.name} + { + field.onChange( + field.value.filter((id: string) => id !== monitor.id) + ); + }} + aria-label="Remove monitor" + > + + + + ))} + + )} + + ); + }} + /> + } + /> + + + + + ); +}; + +export default CreateMaintenanceWindowPage; diff --git a/client/src/Pages/Maintenance/index.css b/client/src/Pages/Maintenance/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/Maintenance/index.jsx b/client/src/Pages/Maintenance/index.jsx deleted file mode 100644 index b49cd9f6e..000000000 --- a/client/src/Pages/Maintenance/index.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Stack, Button } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useState, useEffect } from "react"; -import "./index.css"; -import MaintenanceTable from "./MaintenanceTable/index.jsx"; -import { useSelector } from "react-redux"; -import { networkService } from "../../main.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; - -const Maintenance = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5); - const [maintenanceWindows, setMaintenanceWindows] = useState(undefined); - const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0); - const [page, setPage] = useState(0); - const [sort, setSort] = useState({}); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [networkError, setNetworkError] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - const handleActionMenuDelete = () => { - setUpdateTrigger((prev) => !prev); - }; - - useEffect(() => { - const fetchMaintenanceWindows = async () => { - try { - setNetworkError(false); - setIsLoading(true); - const response = await networkService.getMaintenanceWindowsByTeamId({ - page: page, - rowsPerPage: rowsPerPage, - }); - const { maintenanceWindows, maintenanceWindowCount } = response.data.data; - setMaintenanceWindows(maintenanceWindows ?? []); - setMaintenanceWindowCount(maintenanceWindowCount ?? 0); - } catch (error) { - setNetworkError(true); - } finally { - setIsLoading(false); - } - }; - fetchMaintenanceWindows(); - }, [page, rowsPerPage, updateTrigger]); - - return ( - <> - - - - - - - - - - - ); -}; - -export default Maintenance; diff --git a/client/src/Pages/Maintenance/index.tsx b/client/src/Pages/Maintenance/index.tsx new file mode 100644 index 000000000..6a9c6f12b --- /dev/null +++ b/client/src/Pages/Maintenance/index.tsx @@ -0,0 +1,61 @@ +import { BasePageWithStates } from "@/Components/design-elements"; +import { HeaderCreate } from "@/Components/common"; +import { useTranslation } from "react-i18next"; +import { useGet } from "@/Hooks/UseApi"; +import { MaintenanceWindowTable } from "./MaintenanceWindowTable"; +import { useState } from "react"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import type { MaintenanceWindow } from "@/Types/MaintenanceWindow"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; + +interface MaintenanceWindowsResponse { + maintenanceWindows: MaintenanceWindow[]; + maintenanceWindowCount: number; +} + +const MaintenanceWindowPage = () => { + const { t } = useTranslation(); + const isAdmin = useIsAdmin(); + const [page, setPage] = useState(0); + const rowsPerPage = useSelector( + (state: RootState) => state?.ui?.maintenance?.rowsPerPage ?? 5 + ); + + const { data, isLoading, isValidating, error, refetch } = + useGet( + `/maintenance-window/team?page=${page}&rowsPerPage=${rowsPerPage}` + ); + + const maintenanceWindows = data?.maintenanceWindows ?? []; + const maintenanceWindowCount = data?.maintenanceWindowCount ?? 0; + + return ( + + + + + ); +}; + +export default MaintenanceWindowPage; diff --git a/client/src/Pages/NotFound/index.jsx b/client/src/Pages/NotFound/index.jsx deleted file mode 100644 index 27e3173b0..000000000 --- a/client/src/Pages/NotFound/index.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from "prop-types"; -import NotFoundSvg from "@/assets/Images/sushi_404.svg"; -import { Button, Stack, Typography } from "@mui/material"; -import { useNavigate } from "react-router-dom"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -/** - * Support for defaultProps will be removed from function components in a future major release - * So That why we're using JavaScript default parameters instead. - */ -const DefaultValue = { - title: "Oh no! You dropped your sushi!", - desc: "Either the URL doesn’t exist, or you don’t have access to it.", -}; - -/** - * Renders an error page component with a customizable title and description. - * - * @component - * @example - * // Usage: - * - * - * @param {Object} props - The component props. - * @param {string} [props.title="Oh no! You dropped your sushi!"] - The title of the error page. - * @param {string} [props.desc="Either the URL doesn’t exist, or you don’t have access to it."] - The description of the error page. - * @returns {JSX.Element} The rendered error page component. - */ -const NotFound = ({ title = DefaultValue.title, desc = DefaultValue.desc }) => { - const navigate = useNavigate(); - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - 404 - - {title} - - {desc} - - - - ); -}; - -NotFound.propTypes = { - title: PropTypes.string, - desc: PropTypes.string, -}; - -export default NotFound; diff --git a/client/src/Pages/NotFound/index.tsx b/client/src/Pages/NotFound/index.tsx new file mode 100644 index 000000000..ce974a2e1 --- /dev/null +++ b/client/src/Pages/NotFound/index.tsx @@ -0,0 +1,64 @@ +import NotFoundSvg from "@/assets/Images/sushi_404.svg"; +import { Button } from "@/Components/inputs"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; + +import { useNavigate } from "react-router-dom"; +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; + +interface NotFoundProps { + title?: string; + desc?: string; +} + +const NotFoundPage = ({ title, desc }: NotFoundProps) => { + const navigate = useNavigate(); + const theme = useTheme(); + const { t } = useTranslation(); + + if (!title || title === "") { + title = t("pages.notFound.title"); + } + + if (!desc || desc === "") { + desc = t("pages.notFound.subtitle"); + } + + return ( + + + + + {title} + + {desc} + + + + ); +}; + +export default NotFoundPage; diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx deleted file mode 100644 index a3ccc4bbb..000000000 --- a/client/src/Pages/Notifications/components/ActionMenu.jsx +++ /dev/null @@ -1,83 +0,0 @@ -// Components -import Menu from "@mui/material/Menu"; -import IconButton from "@mui/material/IconButton"; -import Icon from "@/Components/v1/Icon"; -import MenuItem from "@mui/material/MenuItem"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -const ActionMenu = ({ notification, onDelete }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - const { t } = useTranslation(); - // Handlers - const handleClick = (event) => { - event.stopPropagation(); - setAnchorEl(event.currentTarget); - }; - - const handleClose = (event) => { - if (event) { - event.stopPropagation(); - } - setAnchorEl(null); - }; - - const handleRemove = (e) => { - e.stopPropagation(); - onDelete(notification.id); - handleClose(); - }; - - const handleConfigure = (e) => { - e.stopPropagation(); - navigate(`/notifications/${notification.id}`); - handleClose(); - }; - - return ( - <> - e.stopPropagation()} - > - - - - e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - > - {t("configure")} - - {t("delete")} - - - - ); -}; - -ActionMenu.propTypes = { - notification: PropTypes.object, - onDelete: PropTypes.func, -}; - -export default ActionMenu; diff --git a/client/src/Pages/Notifications/components/NotificationsTable.tsx b/client/src/Pages/Notifications/components/NotificationsTable.tsx new file mode 100644 index 000000000..d103458c3 --- /dev/null +++ b/client/src/Pages/Notifications/components/NotificationsTable.tsx @@ -0,0 +1,96 @@ +import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu"; +import Typography from "@mui/material/Typography"; +import type { Header } from "@/Components/design-elements/Table"; +import { Table } from "@/Components/design-elements"; + +import type { Notification } from "@/Types/Notification"; +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material"; + +interface NotificationsTableProps { + notifications: Notification[]; + setSelectedChannel: Function; +} + +export const NotificationsTable = ({ + notifications, + setSelectedChannel, +}: NotificationsTableProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + const theme = useTheme(); + + const getActions = (channel: Notification): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/notifications/configure/${channel.id}`); + }, + closeMenu: true, + }, + + { + id: 7, + label: ( + + {t("pages.common.monitors.actions.delete")} + + ), + action: async () => { + setSelectedChannel(channel); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: t("common.table.headers.name"), + render: (row) => { + return {row?.notificationName}; + }, + }, + + { + id: "type", + content: t("common.table.headers.type"), + render: (row) => { + return {row?.type}; + }, + }, + { + id: "destination", + content: t("pages.notifications.table.headers.destination"), + render: (row) => { + return {row?.address}; + }, + }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( +
{ + navigate(`/notifications/configure/${row.id}`); + }} + /> + ); +}; diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx deleted file mode 100644 index 14b72ab61..000000000 --- a/client/src/Pages/Notifications/create/index.jsx +++ /dev/null @@ -1,320 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Button from "@mui/material/Button"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Box from "@mui/material/Box"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { - useCreateNotification, - useGetNotificationById, - useEditNotification, - useTestNotification, - useDeleteNotification, -} from "../../../Hooks/useNotifications.js"; -import { notificationValidation } from "../../../Validation/validation.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; -import { useParams, useNavigate } from "react-router-dom"; -import { - NOTIFICATION_TYPES, - TITLE_MAP, - DESCRIPTION_MAP, - LABEL_MAP, - PLACEHOLDER_MAP, -} from "../utils.js"; - -// Setup - -const CreateNotifications = () => { - const { notificationId } = useParams(); - const navigate = useNavigate(); - const theme = useTheme(); - const [createNotification, isCreating] = useCreateNotification(); - const [editNotification, isEditing] = useEditNotification(); - const [testNotification, isTesting] = useTestNotification(); - const [deleteNotification, isDeleting] = useDeleteNotification(); - - const BREADCRUMBS = [ - { name: "notifications", path: "/notifications" }, - { - name: notificationId ? "edit" : "create", - path: notificationId ? `/notifications/${notificationId}` : "/notifications/create", - }, - ]; - - // Redux state - - // local state - const [notification, setNotification] = useState({ - notificationName: "", - address: "", - type: NOTIFICATION_TYPES[0].id, - }); - const [errors, setErrors] = useState({}); - const { t } = useTranslation(); - - const [notificationIsLoading] = useGetNotificationById(notificationId, setNotification); - - const [isOpen, setIsOpen] = useState(false); - - const getNotificationTypeValue = (typeId) => { - return NOTIFICATION_TYPES.find((type) => type.id === typeId)?.value || "email"; - }; - - const extractError = (error, field) => - error?.details.find((d) => d.path.includes(field))?.message; - - // handlers - const onSubmit = (e) => { - e.preventDefault(); - const form = { - ...notification, - type: getNotificationTypeValue(notification.type), - }; - - let error = null; - - error = notificationValidation.validate(form, { abortEarly: false }).error; - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - createToast({ body: Object.values(newErrors)[0] }); - setErrors(newErrors); - return; - } - - if (notificationId) { - editNotification(notificationId, form); - } else { - createNotification(form); - } - }; - - const onChange = (e) => { - const { name, value } = e.target; - let rawNotification = { ...notification, [name]: value }; - let newNotification = { - ...rawNotification, - type: getNotificationTypeValue(rawNotification.type), - }; - - const { error } = notificationValidation.validate(newNotification, { - abortEarly: false, - }); - let validationError = { ...errors }; - - if (name === "type") { - validationError["type"] = extractError(error, "type"); - validationError["address"] = extractError(error, "address"); - } else { - validationError[name] = extractError(error, name); - } - - setNotification(rawNotification); - setErrors(validationError); - }; - - const onTestNotification = () => { - const form = { - ...notification, - type: getNotificationTypeValue(notification.type), - }; - - let error = null; - - error = notificationValidation.validate(form, { abortEarly: false }).error; - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - createToast({ body: Object.values(newErrors)[0] }); - setErrors(newErrors); - return; - } - - testNotification(form); - }; - - const onDelete = () => { - if (notificationId) { - deleteNotification(notificationId, () => navigate("/notifications")); - } - }; - - const type = getNotificationTypeValue(notification.type); - return ( - - - {t("createNotifications.title")} - - - - - {t("createNotifications.nameSettings.title")} - - - {t("createNotifications.nameSettings.description")} - - - - - - - - - - {t("createNotifications.typeSettings.title")} - - - {t("createNotifications.typeSettings.description")} - - - - + {NotificationChannels.map((type: string) => ( + + {type} + + ))} + + )} + /> + } + /> + {watchedType !== "matrix" && ( + ( + + )} + /> + } + /> + )} + {watchedType === "matrix" && ( + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + } + /> + )} + + + + + + ); +}; + +export default NotificationsCreatePage; diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx deleted file mode 100644 index 70ed3ef96..000000000 --- a/client/src/Pages/Notifications/index.jsx +++ /dev/null @@ -1,121 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Button from "@mui/material/Button"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import ActionMenu from "./components/ActionMenu.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { - useGetNotificationsByTeamId, - useDeleteNotification, -} from "../../Hooks/useNotifications.js"; -import { useTranslation } from "react-i18next"; - -const Notifications = () => { - const navigate = useNavigate(); - const theme = useTheme(); - const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }]; - const [updateTrigger, setUpdateTrigger] = useState(false); - const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger); - const [deleteNotification, isDeleting, deleteError] = useDeleteNotification(); - const { t } = useTranslation(); - // Handlers - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const onDelete = (id) => { - deleteNotification(id, triggerUpdate); - }; - - const headers = [ - { - id: "name", - content: "Name", - render: (row) => { - return row.notificationName; - }, - }, - { - id: "target", - content: "Target", - render: (row) => { - return row.address; - }, - }, - { - id: "platform", - content: "Platform", - render: (row) => { - return row?.config?.platform || row.type; - }, - }, - - { - id: "actions", - content: "Actions", - onClick: (e) => { - e.stopPropagation(); - }, - render: (row) => { - return ( - - ); - }, - }, - ]; - - return ( - <> - - - - - - - {t("notifications.createTitle")} - navigate(`/notifications/${row.id}`), - rowSX: { - cursor: "pointer", - "&:hover td": { - backgroundColor: theme.palette.tertiary.main, - transition: "background-color .3s ease", - }, - }, - }} - headers={headers} - data={notifications} - /> - - - - ); -}; - -export default Notifications; diff --git a/client/src/Pages/Notifications/index.tsx b/client/src/Pages/Notifications/index.tsx new file mode 100644 index 000000000..1e4091461 --- /dev/null +++ b/client/src/Pages/Notifications/index.tsx @@ -0,0 +1,73 @@ +import { BasePageWithStates } from "@/Components/design-elements"; +import { NotificationsTable } from "@/Pages/Notifications/components/NotificationsTable"; +import { Dialog } from "@/Components/inputs"; +import { HeaderCreate } from "@/Components/common"; + +import { useState } from "react"; +import { useGet, useDelete } from "@/Hooks/UseApi"; +import { useTranslation } from "react-i18next"; +import type { Notification } from "@/Types/Notification"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; + +const NotificationsPage = () => { + const { t } = useTranslation(); + const isAdmin = useIsAdmin(); + + const [selectedChannel, setSelectedChannel] = useState(null); + const isDialogOpen = Boolean(selectedChannel); + + const { + data: notifications, + isLoading, + isValidating, + error, + refetch, + } = useGet("/notifications/team", {}, { keepPreviousData: true }); + + const { deleteFn, loading: isDeleting } = useDelete(); + + const handleConfirm = async () => { + if (!selectedChannel) return; + await deleteFn(`/notifications/${selectedChannel.id}`); + setSelectedChannel(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedChannel(null); + }; + + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js deleted file mode 100644 index 9ad422101..000000000 --- a/client/src/Pages/Notifications/utils.js +++ /dev/null @@ -1,44 +0,0 @@ -export const NOTIFICATION_TYPES = [ - { id: 1, name: "E-mail", value: "email" }, - { id: 2, name: "Slack", value: "slack" }, - { id: 3, name: "PagerDuty", value: "pager_duty" }, - { id: 4, name: "Webhook", value: "webhook" }, - { id: 5, name: "Discord", value: "discord" }, - { id: 6, name: "Matrix", value: "matrix" }, -]; - -export const TITLE_MAP = { - email: "createNotifications.emailSettings.title", - slack: "createNotifications.slackSettings.title", - pager_duty: "createNotifications.pagerdutySettings.title", - webhook: "createNotifications.webhookSettings.title", - discord: "createNotifications.discordSettings.title", - matrix: "createNotifications.matrixSettings.title", -}; - -export const DESCRIPTION_MAP = { - email: "createNotifications.emailSettings.description", - slack: "createNotifications.slackSettings.description", - pager_duty: "createNotifications.pagerdutySettings.description", - webhook: "createNotifications.webhookSettings.description", - discord: "createNotifications.discordSettings.description", - matrix: "createNotifications.matrixSettings.description", -}; - -export const LABEL_MAP = { - email: "createNotifications.emailSettings.emailLabel", - slack: "createNotifications.slackSettings.webhookLabel", - pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel", - webhook: "createNotifications.webhookSettings.webhookLabel", - discord: "createNotifications.discordSettings.webhookLabel", - matrix: "createNotifications.matrixSettings.homeserverLabel", -}; - -export const PLACEHOLDER_MAP = { - email: "createNotifications.emailSettings.emailPlaceholder", - slack: "createNotifications.slackSettings.webhookPlaceholder", - pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder", - webhook: "createNotifications.webhookSettings.webhookPlaceholder", - discord: "createNotifications.discordSettings.webhookPlaceholder", - matrix: "createNotifications.matrixSettings.homeserverPlaceholder", -}; diff --git a/client/src/Pages/PageSpeed/Create/index.css b/client/src/Pages/PageSpeed/Create/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx deleted file mode 100644 index 6a786eb00..000000000 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ /dev/null @@ -1,514 +0,0 @@ -// Components -import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; -import SkeletonLayout from "./skeleton.jsx"; -import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; - -// Utils -import { useState, useEffect } from "react"; -import { useSelector } from "react-redux"; -import { monitorValidation } from "../../../Validation/validation.js"; -import { parseDomainName } from "../../../Utils/monitorUtilsLegacy.js"; -import { useTranslation } from "react-i18next"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; -import { useTheme } from "@emotion/react"; -import { createToast } from "../../../Utils/toastUtils.jsx"; - -import { useParams } from "react-router"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; -import { - useCreateMonitor, - useFetchMonitorById, - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, -} from "../../../Hooks/monitorHooks.js"; - -const PageSpeedSetup = () => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined"; - const CRUMBS = [ - { name: "pagespeed", path: "/pagespeed" }, - ...(isCreate - ? [{ name: "create", path: `/pagespeed/create` }] - : [ - { name: "details", path: `/pagespeed/${monitorId}` }, - { name: "configure", path: `/pagespeed/configure/${monitorId}` }, - ]), - ]; - - // States - const [monitor, setMonitor] = useState( - isCreate - ? { - url: "", - name: "", - type: "pagespeed", - notifications: [], - interval: 180000, - } - : {} - ); - const [https, setHttps] = useState(true); - const [errors, setErrors] = useState({}); - const [isOpen, setIsOpen] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - - // Setup - const { t } = useTranslation(); - const theme = useTheme(); - // Constants - const MS_PER_MINUTE = 60000; - const FREQUENCIES = [ - { _id: 3, name: t("time.threeMinutes") }, - { _id: 5, name: t("time.fiveMinutes") }, - { _id: 10, name: t("time.tenMinutes") }, - { _id: 20, name: t("time.twentyMinutes") }, - { _id: 60, name: t("time.oneHour") }, - { _id: 1440, name: t("time.oneDay") }, - { _id: 10080, name: t("time.oneWeek") }, - ]; - - const { user } = useSelector((state) => state.auth); - const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); - const [notifications, notificationsAreLoading, notificationsError] = - useGetNotificationsByTeamId(); - - // Hooks for API actions - const [isLoading] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger }); - const [createMonitor, isCreating] = useCreateMonitor(); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - const [pauseMonitor, isPausing] = usePauseMonitor(); - - // Handlers - const onSubmit = async (event) => { - event.preventDefault(); - if (isCreate) { - let form = { - url: `http${https ? "s" : ""}://` + monitor.url, - name: monitor.name === "" ? monitor.url : monitor.name, - type: monitor.type, - interval: monitor.interval, - }; - - const { error } = monitorValidation.validate(form, { abortEarly: false }); - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: t("checkFormError") }); - return; - } - - form = { - ...form, - description: form.name, - notifications: monitor.notifications, - }; - await createMonitor({ monitor: form, redirect: "/pagespeed" }); - } else { - const monitorParams = { - url: monitor.url, - name: monitor.name === "" ? monitor.url : monitor.name, - type: monitor.type, - interval: monitor.interval, - }; - const { error } = monitorValidation.validate(monitorParams, { abortEarly: false }); - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: t("checkFormError") }); - return; - } - await updateMonitor({ monitor, redirect: "/pagespeed" }); - } - }; - - const handleChange = (event) => { - let { value, name } = event.target; - - if (name === "interval") { - value = value * MS_PER_MINUTE; - } - - setMonitor({ - ...monitor, - [name]: value, - }); - - const { error } = monitorValidation.validate( - { [name]: value, type: monitor.type }, - { abortEarly: false } - ); - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - const handleBlur = (event) => { - const { name, value } = event.target; - if (name === "url" && monitor.name === "") { - setMonitor((prev) => ({ ...prev, name: parseDomainName(value) })); - } - }; - - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - await deleteMonitor({ monitor: { id: monitorId }, redirect: "/pagespeed" }); - }; - - const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; - - if (Object.keys(monitor).length === 0) { - return ; - } - - return ( - - - - - - - - {!isCreate ? monitor.name : t("createYour") + " "} - - {isCreate ? ( - - {t("pageSpeedMonitor")} - - ) : ( - <> - )} - - {!isCreate && ( - - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - )} - - {!isCreate && ( - - - - - )} - - - - - {t("settingsGeneralSettings")} - - - {t("pageSpeedConfigureSettingsDescription")} - - - .Mui-disabled)": { - backgroundColor: theme.palette.tertiary.main, - }, - }} - > - : undefined} - placeholder="random.website.com" - value={monitor.url || ""} - onChange={handleChange} - onBlur={isCreate ? handleBlur : undefined} - error={!!errors["url"]} - helperText={errors["url"]} - disabled={!isCreate} - /> - - - - {isCreate && ( - - - - {t("distributedUptimeCreateChecks")} - - - {t("distributedUptimeCreateChecksDescription")} - - - - - - - - - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - - )} - - - - {t("notificationConfig.title")} - - {t("notificationConfig.description")} - - - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - - - - - - - - - ); -}; - -SettingsUI.propTypes = { - HEADING_SX: PropTypes.object, - handleChange: PropTypes.func, - mode: PropTypes.string, - language: PropTypes.string, - chartType: PropTypes.string, -}; - -export default SettingsUI; diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/Settings/SettingsURL.jsx deleted file mode 100644 index c52dcf317..000000000 --- a/client/src/Pages/Settings/SettingsURL.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import Box from "@mui/material/Box"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import { PropTypes } from "prop-types"; -import { useTranslation } from "react-i18next"; - -const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => { - const { t } = useTranslation(); - const theme = useTheme(); - return ( - - - - {t("settingsPage.urlSettings.title")} - - - {t("settingsPage.urlSettings.description")} - - - - - - - ); -}; - -SettingsURL.propTypes = { - HEADING_SX: PropTypes.object, - handleChange: PropTypes.func, - showURL: PropTypes.bool, -}; - -export default SettingsURL; diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx deleted file mode 100644 index bd3513a2c..000000000 --- a/client/src/Pages/Settings/index.jsx +++ /dev/null @@ -1,324 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import SettingsTimeZone from "./SettingsTimeZone.jsx"; -import SettingsUI from "./SettingsUI.jsx"; -import SettingsURL from "./SettingsURL.jsx"; -import SettingsPagespeed from "./SettingsPagespeed.jsx"; -import SettingsDemoMonitors from "./SettingsDemoMonitors.jsx"; -import SettingsAbout from "./SettingsAbout.jsx"; -import SettingsEmail from "./SettingsEmail.jsx"; -import SettingsGlobalThresholds from "./SettingsGlobalThresholds.jsx"; -import SettingsExport from "./SettingsExport.jsx"; -import Button from "@mui/material/Button"; -// Utils -import { settingsValidation } from "@/Validation/validation.js"; -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { useSelector, useDispatch } from "react-redux"; -import { - setTimezone, - setMode, - setLanguage, - setShowURL, - setChartType, -} from "@/Features/UI/uiSlice.js"; -import SettingsStats from "./SettingsStats.jsx"; - -import { useFetchSettings, useSaveSettings } from "@/Hooks/settingsHooks.js"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { - useAddDemoMonitors, - useDeleteAllMonitors, - useDeleteMonitorStats, - useFetchJson, -} from "@/Hooks/monitorHooks.js"; -// Constants -const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; - -const Settings = () => { - // Redux state - const { - mode, - language = "en", - timezone, - showURL, - chartType = "histogram", - } = useSelector((state) => state.ui); - - // Local state - const [settingsData, setSettingsData] = useState({}); - const [errors, setErrors] = useState({}); - const [isApiKeySet, setIsApiKeySet] = useState(settingsData?.pagespeedKeySet ?? false); - const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false); - const [isEmailPasswordSet, setIsEmailPasswordSet] = useState( - settingsData?.emailPasswordSet ?? false - ); - const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false); - - // Network - const [isSettingsLoading, settingsError] = useFetchSettings({ - setSettingsData, - setIsApiKeySet, - setIsEmailPasswordSet, - }); - - const [addDemoMonitors, isAddingDemoMonitors] = useAddDemoMonitors(); - - const [isSaving, saveError, saveSettings] = useSaveSettings({ - setSettingsData, - setIsApiKeySet, - setApiKeyHasBeenReset, - setIsEmailPasswordSet, - setEmailPasswordHasBeenReset, - }); - const [deleteAllMonitors, isDeletingMonitors] = useDeleteAllMonitors(); - const [deleteMonitorStats, isDeletingMonitorStats] = useDeleteMonitorStats(); - const [fetchJson, isFetchingJson] = useFetchJson(); - - // Setup - const isAdmin = useIsAdmin(); - const theme = useTheme(); - const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) }; - const { t } = useTranslation(); - const dispatch = useDispatch(); - // Handlers - const handleChange = async (e) => { - const { name, value, checked } = e.target; - - let newValue; - if ( - name === "systemEmailIgnoreTLS" || - name === "systemEmailRequireTLS" || - name === "systemEmailRejectUnauthorized" || - name === "systemEmailSecure" || - name === "systemEmailPool" - ) { - newValue = checked; - } - - // Ensure showURL is a proper boolean - if (name === "showURL") { - newValue = value === true || value === "true"; - } - - // Build next state early - const newSettingsData = { - ...settingsData, - settings: { ...settingsData.settings, [name]: newValue ?? value }, - }; - - if (name === "timezone") { - dispatch(setTimezone({ timezone: value })); - return; - } - - if (name === "mode") { - dispatch(setMode(value)); - return; - } - - if (name === "language") { - dispatch(setLanguage(value)); - return; - } - - if (name === "chartType") { - dispatch(setChartType(value)); - return; - } - - if (name === "deleteStats") { - await deleteMonitorStats(); - return; - } - - if (name === "demo") { - await addDemoMonitors(); - return; - } - - if (name === "deleteMonitors") { - await deleteAllMonitors(); - return; - } - - if (name === "export") { - const json = await fetchJson(); - if (!json || json.length === 0) { - return; - } - - const blob = new Blob([JSON.stringify(json, null, 2)], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = "monitors.json"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - return; - } - - setSettingsData(newSettingsData); - - // Update Redux immediately for UI feedback - if (name === "showURL") { - dispatch(setShowURL(newValue)); - } - }; - - const handleSave = () => { - // Validate] - - const toSubmit = { - checkTTL: settingsData.settings.checkTTL, - pagespeedApiKey: settingsData.settings.pagespeedApiKey, - language: settingsData.settings.language, - timezone: settingsData.settings.timezone, - systemEmailHost: settingsData.settings.systemEmailHost, - systemEmailPort: settingsData.settings.systemEmailPort, - systemEmailSecure: settingsData.settings.systemEmailSecure, - systemEmailPool: settingsData.settings.systemEmailPool, - systemEmailAddress: settingsData.settings.systemEmailAddress, - systemEmailPassword: settingsData.settings.systemEmailPassword, - systemEmailUser: settingsData.settings.systemEmailUser, - systemEmailConnectionHost: settingsData.settings.systemEmailConnectionHost, - systemEmailTLSServername: settingsData.settings.systemEmailTLSServername, - systemEmailIgnoreTLS: settingsData.settings.systemEmailIgnoreTLS, - systemEmailRequireTLS: settingsData.settings.systemEmailRequireTLS, - systemEmailRejectUnauthorized: settingsData.settings.systemEmailRejectUnauthorized, - showURL: settingsData.settings.showURL, - globalThresholds: settingsData.settings.globalThresholds, - }; - - const { error } = settingsValidation.validate(toSubmit, { - abortEarly: false, - }); - if (!error || error.details.length === 0) { - setErrors({}); - saveSettings(toSubmit); - } else { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - } - }; - - return ( - - - {t("settingsPage.title")} - - - - - - - - - - - - - - - - - ); -}; - -export default Settings; diff --git a/client/src/Pages/Settings/index.tsx b/client/src/Pages/Settings/index.tsx new file mode 100644 index 000000000..b9d3e1f3c --- /dev/null +++ b/client/src/Pages/Settings/index.tsx @@ -0,0 +1,963 @@ +import { BasePage, ConfigBox, TextLink } from "@/Components/design-elements"; +import { Autocomplete, Select, Dialog, SwitchComponent } from "@/Components/inputs"; +import { Stack, useTheme, MenuItem, Link, type SelectChangeEvent } from "@mui/material"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslation } from "react-i18next"; +import { useSelector, useDispatch } from "react-redux"; +import DummyChart from "@/Pages/Settings/DummyChart"; +import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi"; +import { useSettingsForm } from "@/Hooks/useSettingsForm"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; +import type { SettingsFormData } from "@/Validation/settings"; +import { useState } from "react"; +import { Controller } from "react-hook-form"; +import { TextField, Button, FieldLabel } from "@/Components/inputs"; +import { Box, Typography } from "@mui/material"; +import { useDelete } from "@/Hooks/UseApi"; + +import { + setTimezone, + setMode, + setLanguage, + setChartType, + type ThemeMode, + type ChartType, +} from "@/Features/UI/uiSlice.js"; +import timezones from "@/Utils/timezones.json"; +import type { RootState } from "@/Types/state"; + +interface Timezone { + id: string; + name: string; +} + +interface SettingsResponse { + settings: any; + pagespeedKeySet: boolean; + emailPasswordSet: boolean; +} + +export const SettingsPage = () => { + const theme = useTheme(); + const { t, i18n } = useTranslation(); + const dispatch = useDispatch(); + const isAdmin = useIsAdmin(); + // Local state for demo monitors dialog + const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false); + const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost(); + const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete(); + + // Fetch settings data from API + const { data: fetchedSettings } = useGet("/settings"); + // Form submission + const { patch, loading: isSaving } = usePatch(); + + // Local state for API key reset + const [isApiKeySet, setIsApiKeySet] = useState( + fetchedSettings?.pagespeedKeySet ?? false + ); + const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false); + // Local state for email password reset + const [isEmailPasswordSet, setIsEmailPasswordSet] = useState( + fetchedSettings?.emailPasswordSet ?? false + ); + const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false); + // Test email functionality + const { post: sendTestEmail, loading: isSendingTestEmail } = usePost(); + // Local state for clear stats dialog + const [isStatsDialogOpen, setIsStatsDialogOpen] = useState(false); + const { deleteFn: deleteStats, loading: isDeletingStats } = useDelete(); + // Export monitors functionality + const { get: fetchMonitorsJson } = useLazyGet(); + + // Initialize form with schema and defaults + const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings }); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + mode: "onChange", + }); + + // Reset form when defaults change + useEffect(() => { + form.reset(defaults); + }, [defaults, form]); + + // Update isApiKeySet when fetchedSettings changes + useEffect(() => { + if (fetchedSettings) { + setIsApiKeySet(fetchedSettings.pagespeedKeySet); + setIsEmailPasswordSet(fetchedSettings.emailPasswordSet); + } + }, [fetchedSettings]); + + const { + timezone: selectedTimezoneId, + mode, + language = "en", + chartType = "histogram", + } = useSelector((state: RootState) => state.ui); + + const user = useSelector((state: RootState) => state.auth.user); + + // Convert timezones to match AutoComplete format (id instead of _id) + const timezoneOptions: Timezone[] = timezones.map((tz) => ({ + id: tz._id, + name: tz.name, + })); + + const selectedTimezone = + timezoneOptions.find((tz) => tz.id === selectedTimezoneId) ?? null; + + const handleTimezoneChange = (newValue: Timezone | null) => { + const newId = newValue?.id ?? ""; + dispatch(setTimezone({ timezone: newId })); + }; + + const handleModeChange = (e: SelectChangeEvent) => { + dispatch(setMode(e.target.value)); + }; + + const handleLanguageChange = (e: SelectChangeEvent) => { + dispatch(setLanguage(e.target.value)); + }; + + const handleChartTypeChange = (e: SelectChangeEvent) => { + dispatch(setChartType(e.target.value)); + }; + + const handleResetApiKey = () => { + form.setValue("pagespeedApiKey", ""); + setApiKeyHasBeenReset(true); + }; + + const handleResetEmailPassword = () => { + form.setValue("systemEmailPassword", ""); + setEmailPasswordHasBeenReset(true); + }; + + const handleSendTestEmail = async () => { + const formValues = form.getValues(); + if (!user) { + alert("User not authenticated"); + return; + } + if ( + !formValues.systemEmailHost || + !formValues.systemEmailPort || + !formValues.systemEmailAddress || + !formValues.systemEmailPassword + ) { + alert("Please fill in all required email fields before testing."); + return; + } + + await sendTestEmail("/settings/test-email", { + to: user.email, + systemEmailHost: formValues.systemEmailHost, + systemEmailPort: formValues.systemEmailPort, + systemEmailAddress: formValues.systemEmailAddress, + systemEmailPassword: formValues.systemEmailPassword, + systemEmailSecure: formValues.systemEmailSecure, + systemEmailPool: formValues.systemEmailPool, + systemEmailIgnoreTLS: formValues.systemEmailIgnoreTLS, + systemEmailRequireTLS: formValues.systemEmailRequireTLS, + systemEmailRejectUnauthorized: formValues.systemEmailRejectUnauthorized, + ...(formValues.systemEmailUser && { systemEmailUser: formValues.systemEmailUser }), + ...(formValues.systemEmailTLSServername && { + systemEmailTLSServername: formValues.systemEmailTLSServername, + }), + ...(formValues.systemEmailConnectionHost && { + systemEmailConnectionHost: formValues.systemEmailConnectionHost, + }), + }); + }; + + const handleClearStats = async () => { + await deleteStats("/checks/team"); + setIsStatsDialogOpen(false); + }; + + const handleExportMonitors = async () => { + const res = await fetchMonitorsJson("/monitors/export/json"); + const json = res?.data ?? []; + if (!json || json.length === 0) { + return; + } + + const blob = new Blob([JSON.stringify(json, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = "monitors.json"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + const onSubmit = async (data: SettingsFormData) => { + // Don't send pagespeedApiKey if it's already set and user hasn't clicked reset + const dataToSend = { ...data }; + if (isApiKeySet && !apiKeyHasBeenReset) { + delete (dataToSend as any).pagespeedApiKey; + } + if (isEmailPasswordSet && !emailPasswordHasBeenReset) { + delete (dataToSend as any).systemEmailPassword; + } + + // Convert undefined to empty string for backend unsetting + const processedData = Object.entries(dataToSend).reduce((acc, [key, value]) => { + const typedKey = key as keyof SettingsFormData; + if (value === undefined) { + (acc as any)[typedKey] = ""; + } else if (typeof value === "object" && value !== null) { + // Handle nested objects like globalThresholds + // Convert 0 to "" for unsetting thresholds + (acc as any)[typedKey] = Object.entries(value).reduce( + (nested, [nestedKey, nestedValue]) => ({ + ...nested, + [nestedKey]: + nestedValue === undefined || nestedValue === 0 ? "" : nestedValue, + }), + {} + ); + } else { + (acc as any)[typedKey] = value; + } + return acc; + }, {} as Partial); + + const result = await patch("/settings", processedData as SettingsFormData); + + if (result?.success) { + // Update API key state from response + if (result.data) { + setIsApiKeySet(result.data.pagespeedKeySet); + setApiKeyHasBeenReset(false); + setIsEmailPasswordSet(result.data.emailPasswordSet); + setEmailPasswordHasBeenReset(false); + } + } + }; + + const onError = (errors: unknown) => { + console.log("Form validation errors:", errors); + }; + + const languages = Object.keys(i18n.options.resources || {}); + + return ( + + + option.name} + isOptionEqualToValue={(option: Timezone, value: Timezone) => + option.id === value.id + } + onChange={(_, newValue) => { + handleTimezoneChange(newValue as Timezone | null); + }} + fieldLabel={t("pages.settings.form.timezone.option.timezone.label")} + /> + } + /> + + + + + + + } + /> + {isAdmin && ( + + {(isApiKeySet === false || apiKeyHasBeenReset === true) && ( + ( + + )} + /> + )} + + {isApiKeySet === true && apiKeyHasBeenReset === false && ( + + + {t("pages.settings.form.pagespeed.option.apiKey.labelSet")} + + + + )} + + } + /> + )} + + {/* URL Settings */} + ( + + )} + /> + } + /> + + {/* Clear All Stats */} + {isAdmin && ( + setIsStatsDialogOpen(true)} + > + {t("common.buttons.clear")} + + } + /> + )} + + {/* Global Thresholds */} + {isAdmin && ( + + ( + { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} + fieldLabel={t("pages.settings.form.thresholds.option.cpu.label")} + type="number" + inputProps={{ min: 0 }} + placeholder={t( + "pages.settings.form.thresholds.option.cpu.placeholder" + )} + error={!!fieldState.error} + helperText={fieldState.error?.message} + /> + )} + /> + ( + { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} + fieldLabel={t("pages.settings.form.thresholds.option.memory.label")} + type="number" + inputProps={{ min: 0 }} + placeholder={t( + "pages.settings.form.thresholds.option.memory.placeholder" + )} + error={!!fieldState.error} + helperText={fieldState.error?.message} + /> + )} + /> + ( + { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} + fieldLabel={t("pages.settings.form.thresholds.option.disk.label")} + type="number" + inputProps={{ min: 0 }} + placeholder={t( + "pages.settings.form.thresholds.option.disk.placeholder" + )} + error={!!fieldState.error} + helperText={fieldState.error?.message} + /> + )} + /> + ( + { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} + fieldLabel={t( + "pages.settings.form.thresholds.option.temperature.label" + )} + type="number" + inputProps={{ min: 0 }} + placeholder={t( + "pages.settings.form.thresholds.option.temperature.placeholder" + )} + error={!!fieldState.error} + helperText={fieldState.error?.message} + /> + )} + /> + + } + /> + )} + + + {/* Email Settings - Admin Only */} + {isAdmin && ( + + + + + {JSON.stringify( + { + host: form.watch("systemEmailHost") || "", + port: form.watch("systemEmailPort") || "", + secure: form.watch("systemEmailSecure") ?? false, + auth: { + user: + form.watch("systemEmailUser") || + form.watch("systemEmailAddress") || + "", + pass: "", + }, + name: form.watch("systemEmailConnectionHost") || "localhost", + pool: form.watch("systemEmailPool") ?? false, + tls: { + rejectUnauthorized: + form.watch("systemEmailRejectUnauthorized") ?? true, + ignoreTLS: form.watch("systemEmailIgnoreTLS") ?? false, + requireTLS: form.watch("systemEmailRequireTLS") ?? false, + servername: form.watch("systemEmailTLSServername") || "", + }, + }, + null, + 2 + )} + + + + } + rightContent={ + + {/* Email Host */} + ( + + )} + /> + + {/* Email Port */} + ( + { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} + fieldLabel={t("pages.settings.form.email.option.port.label")} + type="number" + inputProps={{ min: 0 }} + placeholder={t("pages.settings.form.email.option.port.placeholder")} + error={!!fieldState.error} + helperText={fieldState.error?.message} + /> + )} + /> + + {/* Email Address */} + ( + + )} + /> + + {/* Email User (Optional) */} + ( + + )} + /> + + {/* Email Password with Reset Pattern */} + {isEmailPasswordSet && !emailPasswordHasBeenReset ? ( + + + {t("pages.settings.form.email.option.password.labelSet")} + + + + + + ) : ( + ( + + )} + /> + )} + + {/* TLS Servername (Optional) */} + ( + + )} + /> + + {/* Connection Host (Optional) */} + ( + + )} + /> + + {/* Boolean Switches */} + + {[ + { + name: "systemEmailSecure", + label: t("pages.settings.form.email.option.secure.label"), + }, + { + name: "systemEmailPool", + label: t("pages.settings.form.email.option.pool.label"), + }, + { + name: "systemEmailIgnoreTLS", + label: t("pages.settings.form.email.option.ignoreTLS.label"), + }, + { + name: "systemEmailRequireTLS", + label: t("pages.settings.form.email.option.requireTLS.label"), + }, + { + name: "systemEmailRejectUnauthorized", + label: t("pages.settings.form.email.option.rejectUnauthorized.label"), + }, + ].map(({ name, label }) => ( + ( + + {label} + ) => + field.onChange(e.target.checked) + } + /> + + )} + /> + ))} + + + {/* Test Email Button */} + + + + + } + /> + )} + + {/* Demo Monitors - Admin Only */} + {isAdmin && ( + + + + } + /> + )} + + {/* Remove All Monitors - Admin Only */} + {isAdmin && ( + + + + } + /> + )} + + {/* Export Monitors - Admin Only */} + {isAdmin && ( + + + + } + /> + )} + + {/* About */} + + + {t("common.appName")} {__APP_VERSION__} + + + {t("pages.settings.form.about.developedBy")} + + + https://github.com/bluewave-labs/checkmate + + + } + /> + + {/* Clear Stats Confirmation Dialog */} + setIsStatsDialogOpen(false)} + onConfirm={handleClearStats} + loading={isDeletingStats} + /> + + {/* Delete All Monitors Confirmation Dialog */} + setIsDemoMonitorsDialogOpen(false)} + onConfirm={async () => { + await deleteAllMonitors("/monitors/"); + setIsDemoMonitorsDialogOpen(false); + }} + loading={isDeletingAllMonitors} + /> + + {/* Sticky Save Button */} + + + + + ); +}; + +export default SettingsPage; diff --git a/client/src/Pages/StatusPage/Create/Components/HeaderConfigStatusControls.tsx b/client/src/Pages/StatusPage/Create/Components/HeaderConfigStatusControls.tsx new file mode 100644 index 000000000..407306f1b --- /dev/null +++ b/client/src/Pages/StatusPage/Create/Components/HeaderConfigStatusControls.tsx @@ -0,0 +1,35 @@ +import Stack from "@mui/material/Stack"; +import { Icon } from "@/Components/design-elements"; +import { Button } from "@/Components/inputs"; +import { Trash } from "lucide-react"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; + +interface HeaderConfigStatusControlsProps { + onDelete: () => void; +} + +export const HeaderConfigStatusControls = ({ + onDelete, +}: React.PropsWithChildren) => { + const theme = useTheme(); + const translate = useTranslation(); + return ( + + + + ); +}; diff --git a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx deleted file mode 100644 index 349571fe8..000000000 --- a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx +++ /dev/null @@ -1,110 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; - -// Utils -import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; -import { useTheme } from "@emotion/react"; -const MonitorListItem = ({ - monitor, - innerRef, - draggableProps, - dragHandleProps, - onDelete, -}) => { - const theme = useTheme(); - return ( - - - {monitor.name} - { - onDelete(monitor); - }} - /> - - ); -}; - -const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { - const onDelete = (monitorToDelete) => { - const newMonitors = selectedMonitors.filter( - (monitor) => monitor.id !== monitorToDelete.id - ); - setSelectedMonitors(newMonitors); - }; - const reorder = (list, startIndex, endIndex) => { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - - return result; - }; - - const onDragEnd = (result) => { - // dropped outside the list - if (!result.destination) { - return; - } - - const reorderedMonitors = reorder( - selectedMonitors, - result.source.index, - result.destination.index - ); - - setSelectedMonitors(reorderedMonitors); - }; - - return ( - - - {(provided, snapshot) => ( - - {selectedMonitors?.map((monitor, index) => ( - - {(provided, snapshot) => ( - - )} - - ))} - {provided.placeholder} - - )} - - - ); -}; - -export default MonitorList; diff --git a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx b/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx deleted file mode 100644 index 594fd9709..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Button, Box } from "@mui/material"; -import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import { useTranslation } from "react-i18next"; -import { formatBytes } from "../../../../../Utils/fileUtils.js"; -const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => { - const { t } = useTranslation(); - if (isLoading) { - return ( - - } - label={logo?.name} - size={formatBytes(logo?.size)} - progress={progressValue} - onClick={removeLogo} - /> - ); - } - - if (logo && logoType) { - return ( - - - - ); - } -}; - -export default Progress; diff --git a/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx b/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx deleted file mode 100644 index 482c490fb..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx deleted file mode 100644 index f31c456ef..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; - -// This can be used to add any extra/additional section/stacks on top of existing sections on the tab -const ConfigStack = ({ title, description, children }) => { - const theme = useTheme(); - return ( - - - - {title} - - {description} - - {children} - - ); -}; - -ConfigStack.propTypes = { - title: PropTypes.string.isRequired, // Title must be a string and is required - description: PropTypes.string.isRequired, // Description must be a string and is required - children: PropTypes.node.isRequired, -}; - -export default ConfigStack; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx deleted file mode 100644 index e84de16dc..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx +++ /dev/null @@ -1,107 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import { TabPanel } from "@mui/lab"; -import MonitorList from "../MonitorList/index.jsx"; -import Search from "@/Components/v1/Inputs/Search/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import ConfigStack from "./ConfigStack.jsx"; -const Content = ({ - tabValue, - form, - monitors, - handleFormChange, - errors, - selectedMonitors, - setSelectedMonitors, -}) => { - // Local state - const [search, setSearch] = useState(""); - - // Handlers - const handleMonitorsChange = (selectedMonitors) => { - handleFormChange({ - target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor.id) }, - }); - setSelectedMonitors(selectedMonitors); - }; - - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - - - - - - - {errors["monitors"]} - - - - - - - - - - - - - - ); -}; - -export default Content; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx deleted file mode 100644 index d4c583d3a..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ /dev/null @@ -1,187 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import { TabPanel } from "@mui/lab"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Search from "@/Components/v1/Inputs/Search/index.jsx"; -import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; -import ColorPicker from "@/Components/v1/Inputs/ColorPicker/index.jsx"; -import Progress from "../Progress/index.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import timezones from "../../../../../Utils/timezones.json"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; -import { useMemo, useState, useCallback } from "react"; - -const TabSettings = ({ - isCreate, - tabValue, - form, - handleFormChange, - handleImageChange, - progress, - removeLogo, - errors, -}) => { - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - const [rawInput, setRawInput] = useState(""); - - const selectedTimezone = useMemo( - () => timezones.find((tz) => tz._id === form.timezone) ?? null, - [form.timezone] - ); - - const handleTimezoneChange = useCallback( - (newValue) => { - setRawInput(""); - handleFormChange({ - target: { - name: "timezone", - value: newValue?._id ?? "", - }, - }); - }, - [handleFormChange] - ); - - return ( - - - - - - {t("access")} - - {t("statusPageCreateSettings")} - - - - - - - - - {t("basicInformation")} - - - {t("statusPageCreateBasicInfoDescription")} - - - - - - - - - - - {t("timezone")} - - - {t("statusPageCreateSelectTimeZoneDescription")} - - - - setRawInput(newVal)} - handleChange={handleTimezoneChange} - isAdorned={true} - unit="timezone" - /> - - - - - - {t("statusPageCreateAppearanceTitle")} - - - {t("statusPageCreateAppearanceDescription")} - - - - - - - - - - - ); -}; - -TabSettings.propTypes = { - isCreate: PropTypes.bool, - tabValue: PropTypes.string, - form: PropTypes.object, - handleFormChange: PropTypes.func, - handleImageChange: PropTypes.func, - progress: PropTypes.object, - removeLogo: PropTypes.func, - errors: PropTypes.object, -}; - -export default TabSettings; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx deleted file mode 100644 index e8669c474..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx +++ /dev/null @@ -1,99 +0,0 @@ -// Components -import { TabContext } from "@mui/lab"; -import { Tab } from "@mui/material"; -import Settings from "./Settings.jsx"; -import Content from "./Content.jsx"; - -// Utils -import PropTypes from "prop-types"; -import CustomTabList from "@/Components/v1/Tab/index.jsx"; - -const Tabs = ({ - isCreate, - form, - errors, - monitors, - selectedMonitors, - setSelectedMonitors, - handleFormChange, - handleImageChange, - progress, - removeLogo, - tab, - setTab, - TAB_LIST, - handleDelete, - isDeleteOpen, - setIsDeleteOpen, - isDeleting, - isLoading, -}) => { - return ( - - { - setTab(TAB_LIST.indexOf(selected)); - }} - aria-label="status page tabs" - > - {TAB_LIST.map((tabLabel) => ( - - ))} - - {tab === 0 ? ( - - ) : ( - - )} - - ); -}; - -Tabs.propTypes = { - isCreate: PropTypes.bool, - form: PropTypes.object, - errors: PropTypes.object, - monitors: PropTypes.array, - selectedMonitors: PropTypes.array, - setSelectedMonitors: PropTypes.func, - handleFormChange: PropTypes.func, - handleImageChange: PropTypes.func, - progress: PropTypes.object, - removeLogo: PropTypes.func, - tab: PropTypes.number, - setTab: PropTypes.func, - TAB_LIST: PropTypes.array, - handleDelete: PropTypes.func, - isDeleteOpen: PropTypes.bool, - setIsDeleteOpen: PropTypes.func, - isDeleting: PropTypes.bool, - isLoading: PropTypes.bool, -}; - -export default Tabs; diff --git a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx deleted file mode 100644 index c0bcf5ab4..000000000 --- a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../../../../main.jsx"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -const useCreateStatusPage = (isCreate) => { - const [isLoading, setIsLoading] = useState(false); - const [networkError, setNetworkError] = useState(false); - const createStatusPage = async ({ form, id }) => { - setIsLoading(true); - try { - await networkService.createStatusPage({ form, isCreate, id }); - return true; - } catch (error) { - setNetworkError(true); - createToast({ body: error?.response?.data?.msg ?? error.message }); - return false; - } finally { - setIsLoading(false); - } - }; - - return [createStatusPage, isLoading, networkError]; -}; - -export { useCreateStatusPage }; diff --git a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx deleted file mode 100644 index 03ebb07bf..000000000 --- a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useState } from "react"; -import { networkService } from "../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -const useMonitorsFetch = () => { - const { user } = useSelector((state) => state.auth); - - const [monitors, setMonitors] = useState(undefined); - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - useEffect(() => { - const fetchMonitors = async () => { - try { - const response = await networkService.getMonitorsByTeamId({ - limit: null, // donot return any checks for the monitors - types: ["http", "ping", "port", "game"], // include game servers in status page monitor selection - }); - setMonitors(response.data.data); - } catch (error) { - setNetworkError(true); - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [user]); - - return [monitors, isLoading, networkError]; -}; - -export { useMonitorsFetch }; diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/index.jsx deleted file mode 100644 index 4eb171232..000000000 --- a/client/src/Pages/StatusPage/Create/index.jsx +++ /dev/null @@ -1,312 +0,0 @@ -// Components -import { Stack, Button, Typography } from "@mui/material"; -import Tabs from "./Components/Tabs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import SkeletonLayout from "./Components/Skeleton/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -//Utils -import { useTheme } from "@emotion/react"; -import { useState, useEffect, useRef, useCallback } from "react"; -import { statusPageValidation } from "../../../Validation/validation.js"; -import { buildErrors } from "../../../Validation/error.js"; -import { useMonitorsFetch } from "./Hooks/useMonitorsFetch.jsx"; -import { useCreateStatusPage } from "./Hooks/useCreateStatusPage.jsx"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { useNavigate } from "react-router-dom"; -import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx"; -import { useParams } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete.jsx"; -//Constants -const ERROR_TAB_MAPPING = [ - ["companyName", "url", "timezone", "color", "isPublished", "logo"], - ["monitors", "showUptimePercentage", "showCharts", "showAdminLoginLink"], -]; - -const CreateStatusPage = () => { - const { url } = useParams(); - //Local state - const [tab, setTab] = useState(0); - const [progress, setProgress] = useState({ value: 0, isLoading: false }); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); - const [form, setForm] = useState({ - isPublished: false, - companyName: "", - url: url ?? Math.floor(Math.random() * 1000000).toFixed(0), - logo: undefined, - timezone: "America/Toronto", - color: "#4169E1", - type: "uptime", - monitors: [], - showCharts: true, - showUptimePercentage: true, - showAdminLoginLink: false, - }); - const [errors, setErrors] = useState({}); - const [selectedMonitors, setSelectedMonitors] = useState([]); - // Refs - const intervalRef = useRef(null); - - // Setup - const isCreate = typeof url === "undefined"; - - //Utils - const theme = useTheme(); - const [monitors, isLoading, networkError] = useMonitorsFetch(); - const navigate = useNavigate(); - const { t } = useTranslation(); - - const [createStatusPage] = useCreateStatusPage(isCreate); - const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] = - useStatusPageFetch(isCreate, url); - const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url); - - // Handlers - const handleFormChange = (e) => { - let { type, name, value, checked } = e.target; - // Handle errors - const { error } = statusPageValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => { - return buildErrors(prev, name, error); - }); - - //Handle checkbox - if (type === "checkbox") { - setForm((prev) => ({ - ...prev, - [name]: checked, - })); - return; - } - - // Handle other inputs - setForm((prev) => ({ - ...prev, - [name]: value, - })); - }; - - const handleImageChange = useCallback((fileObj) => { - if (!fileObj || !fileObj.file) return; - - setForm((prev) => ({ - ...prev, - logo: { - src: fileObj.src, - name: fileObj.name, - type: fileObj.file.type, - size: fileObj.file.size, - }, - })); - - intervalRef.current = setInterval(() => { - const buffer = 12; - setProgress((prev) => { - if (prev.value + buffer >= 100) { - clearInterval(intervalRef.current); - return { value: 100, isLoading: false }; - } - return { ...prev, value: prev.value + buffer }; - }); - }, 120); - }, []); - - const removeLogo = () => { - setForm((prev) => ({ - ...prev, - logo: undefined, - })); - // interrupt interval if image upload is canceled prior to completing the process - clearInterval(intervalRef.current); - setProgress({ value: 0, isLoading: false }); - }; - - /** - * Handle status page deletion with optimistic UI update - * Immediately navigates away without waiting for the deletion to complete - * to prevent unnecessary network requests for the deleted page - */ - const handleDelete = async () => { - setIsDeleteOpen(false); - // Start deletion process but don't wait for it - deleteStatusPage(); - // Immediately navigate away to prevent additional fetches for the deleted page - navigate("/status"); - }; - - const handleSubmit = async () => { - let toSubmit = { - ...form, - logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null }, - }; - - const { error } = statusPageValidation.validate(toSubmit, { - abortEarly: false, - }); - - if (typeof error === "undefined") { - const success = await createStatusPage({ form, id: statusPage?.id }); - if (success) { - createToast({ - body: isCreate ? t("statusPage.createSuccess") : t("statusPage.updateSuccess"), - }); - navigate(`/status/uptime/${form.url}`); - } - return; - } - - const newErrors = {}; - error?.details?.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors((prev) => ({ ...prev, ...newErrors })); - const errorTabs = Object.keys(newErrors).map((err) => { - return ERROR_TAB_MAPPING.findIndex((tab) => tab.includes(err)); - }); - - // If there's an error in the current tab, don't change the tab - if (errorTabs.some((errorTab) => errorTab === tab)) { - return; - } - - // If we get -1, there's an unknown error - if (errorTabs[0] === -1) { - createToast({ body: t("common.toasts.unknownError") }); - return; - } - - // Otherwise go to tab with error - setTab(errorTabs[0]); - }; - - // If we are configuring, populate fields - useEffect(() => { - if (isCreate) return; - if (typeof statusPage === "undefined") { - return; - } - - let newLogo = undefined; - if (statusPage.logo && Object.keys(statusPage.logo).length > 0) { - newLogo = { - src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`, - name: "logo", - type: statusPage.logo.contentType, - size: null, - }; - } - setForm((prev) => { - return { - ...prev, - companyName: statusPage?.companyName, - isPublished: statusPage?.isPublished, - timezone: statusPage?.timezone, - monitors: statusPageMonitors.map((monitor) => monitor.id), - color: statusPage?.color, - logo: newLogo, - showCharts: statusPage?.showCharts ?? true, - showUptimePercentage: statusPage?.showUptimePercentage ?? true, - showAdminLoginLink: statusPage?.showAdminLoginLink ?? false, - }; - }); - setSelectedMonitors(statusPageMonitors); - }, [isCreate, statusPage, statusPageMonitors]); - - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - if (isLoading) return ; - - // Load fields - return ( - - - {!isCreate && ( - - - setIsDeleteOpen(false)} - open={isDeleteOpen} - confirmationButtonLabel={t("deleteStatusPageConfirm")} - description={t("deleteStatusPageDescription")} - isLoading={isDeleting || statusPageIsLoading} - /> - - )} - - - - - - ); -}; - -export default CreateStatusPage; diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx new file mode 100644 index 000000000..b1b43d8bb --- /dev/null +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -0,0 +1,488 @@ +import { BasePage, ConfigBox } from "@/Components/design-elements"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import IconButton from "@mui/material/IconButton"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { Trash2, GripVertical } from "lucide-react"; +import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; +import type { DropResult } from "@hello-pangea/dnd"; +import { + ImageUpload, + SwitchComponent, + Button, + TextField, + Autocomplete, + Checkbox, + Dialog, + ColorInput, +} from "@/Components/inputs"; + +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { useEffect, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useStatusPageForm } from "@/Hooks/useStatusPageForm"; +import type { StatusPageFormData } from "@/Validation/statusPage"; +import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi"; +import type { Monitor } from "@/Types/Monitor"; +import type { StatusPageResponse } from "@/Types/StatusPage"; +import timezones from "@/Utils/timezones.json"; +import { useNavigate, useParams } from "react-router-dom"; +import axios from "axios"; +import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls"; + +interface TimezoneOption { + _id: string; + name: string; +} + +const CreateStatusPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const { url } = useParams<{ url: string }>(); + + const isCreate = typeof url === "undefined"; + + // Fetch existing status page data when configuring + const { data: statusPageData, isLoading: isLoadingStatusPage } = + useGet(isCreate ? null : `/status-page/${url}?type=uptime`); + + const { data: monitorsResponse } = useGet( + "/monitors/team?type=http&type=ping&type=port&type=docker" + ); + const monitors = monitorsResponse ?? []; + + const { post, loading: isSubmittingPost } = usePost(); + const { put, loading: isSubmittingPut } = usePut(); + const { deleteFn, loading: isDeleting } = useDelete(); + + // Delete dialog state + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const isSubmitting = isSubmittingPost || isSubmittingPut; + + const { schema, defaults } = useStatusPageForm({ + data: statusPageData?.statusPage ?? null, + monitors: statusPageData?.monitors ?? null, + }); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + + const { control, reset, handleSubmit } = form; + + // Reset form when defaults change (from fetched data) + useEffect(() => { + reset(defaults); + }, [defaults, reset]); + + const onError = (errors: any) => { + console.log(errors); + }; + + const handleDeleteClick = () => { + setIsDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = async () => { + const result = await deleteFn(`/status-page/${statusPageData?.statusPage?.id}`); + if (result) { + navigate("/status"); + } + setIsDeleteDialogOpen(false); + }; + + const handleDeleteCancel = () => { + setIsDeleteDialogOpen(false); + }; + + const onSubmit = async (data: StatusPageFormData) => { + const fd = new FormData(); + fd.append("type", "uptime"); + fd.append("isPublished", String(data.isPublished)); + if (data.companyName) fd.append("companyName", data.companyName); + if (data.url) fd.append("url", data.url); + if (data.timezone) fd.append("timezone", data.timezone); + if (data.color) fd.append("color", data.color); + fd.append("showCharts", String(data.showCharts)); + fd.append("showUptimePercentage", String(data.showUptimePercentage)); + fd.append("showAdminLoginLink", String(data.showAdminLoginLink)); + + data.monitors.forEach((monitorId) => { + fd.append("monitors[]", monitorId); + }); + + // Handle logo upload + if (data.logo === null) { + // Signal to remove the logo + fd.append("removeLogo", "true"); + } else if (data.logo?.data && data.logo.data !== "") { + if (data.logo.data.startsWith("blob:")) { + try { + const imageResult = await axios.get(data.logo.data, { + responseType: "blob", + }); + fd.append("logo", imageResult.data); + URL.revokeObjectURL(data.logo.data); + } catch (e) { + console.error("Error fetching logo blob:", e); + } + } + } + + let result; + if (isCreate) { + result = await post("/status-page", fd, { + headers: { "Content-Type": "multipart/form-data" }, + }); + } else { + result = await put(`/status-page/${statusPageData?.statusPage.id}`, fd, { + headers: { "Content-Type": "multipart/form-data" }, + }); + } + + if (result) { + navigate(`/status/${data.url}`); + } + }; + + if (!isCreate && isLoadingStatusPage) { + return Loading...; + } + return ( + + {!isCreate && } + + ( + field.onChange(e.target.checked)} + /> + )} + /> + + {t("pages.statusPages.form.access.option.published.name")} + + + } + /> + + ( + + )} + /> + ( + + )} + /> + + } + /> + { + const selectedMonitors = field.value + .map((id: string) => monitors.find((m) => m.id === id)) + .filter((m): m is Monitor => m !== undefined); + + const handleDragEnd = (result: DropResult) => { + if (!result.destination) return; + const reordered = Array.from(field.value); + const [removed] = reordered.splice(result.source.index, 1); + reordered.splice(result.destination.index, 0, removed); + field.onChange(reordered); + }; + + return ( + + option.name} + value={selectedMonitors} + onChange={(_, newValue) => { + field.onChange(newValue.map((m: Monitor) => m.id)); + }} + fieldLabel={t( + "pages.statusPages.form.monitors.option.monitors.label" + )} + renderInput={(params) => ( + + )} + /> + {selectedMonitors.length > 0 && ( + + + {(provided) => ( + + {selectedMonitors.map((monitor, index) => ( + + {(provided) => ( + + + {monitor.name} + { + field.onChange( + field.value.filter( + (id: string) => id !== monitor.id + ) + ); + }} + aria-label="Remove monitor" + > + + + + )} + + ))} + {provided.placeholder} + + )} + + + )} + + ); + }} + /> + } + /> + ( + option.name} + value={ + timezones.find((tz: TimezoneOption) => tz._id === field.value) ?? null + } + onChange={(_, newValue: TimezoneOption | null) => { + field.onChange(newValue?._id ?? ""); + }} + fieldLabel={t("pages.statusPages.form.timezone.option.timezone.label")} + renderInput={(params) => ( + + )} + /> + )} + /> + } + /> + + + ( + { + if (file) { + field.onChange({ + data: file.src, + contentType: file.file.type, + }); + } else { + field.onChange(null); + } + }} + /> + )} + /> + + ( + + )} + /> + + } + /> + + ( + + } + label={t("pages.statusPages.form.features.option.showCharts.label")} + /> + )} + /> + {/* ( + + } + label={t( + "pages.statusPages.form.features.option.showUptimePercentage.label" + )} + /> + )} + /> + ( + + } + label={t( + "pages.statusPages.form.features.option.showAdminLoginLink.label" + )} + /> + )} + /> */} + + } + /> + + + + + + ); +}; + +export default CreateStatusPage; diff --git a/client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx b/client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx deleted file mode 100644 index 816fbd375..000000000 --- a/client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx +++ /dev/null @@ -1,36 +0,0 @@ -// Components -import { Box, Typography } from "@mui/material"; - -// Utils -import { useTheme } from "@mui/material/styles"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -const AdminLink = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - - return ( - - - {t("administrator")} - - navigate("/login")} - > - {t("loginHere")} - - - ); -}; - -export default AdminLink; diff --git a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx b/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx deleted file mode 100644 index fe1c4c852..000000000 --- a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx +++ /dev/null @@ -1,141 +0,0 @@ -// Components -import { Box, Stack, Typography, Button } from "@mui/material"; -import Image from "@/Components/v1/Image/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; -//Utils -import { useTheme } from "@mui/material/styles"; -import { useNavigate } from "react-router-dom"; -import { useLocation } from "react-router-dom"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -const Controls = ({ url, type }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const location = useLocation(); - const currentPath = location.pathname; - const navigate = useNavigate(); - - if (currentPath.startsWith("/status/uptime/public")) { - return null; - } - - return ( - - - - - - ); -}; - -Controls.propTypes = { - type: PropTypes.string, - url: PropTypes.string, -}; - -const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const publicUrl = `/status/uptime/public/${url}`; - - return ( - - - - - {statusPage?.companyName} - - {statusPage?.isPublished && !isPublic && ( - { - window.open(publicUrl, "_blank", "noopener,noreferrer"); - }} - sx={{ - display: "inline-flex", - ":hover": { - cursor: "pointer", - borderBottom: 1, - }, - }} - > - {t("publicLink")} - - - )} - - - {isPublic && } - - ); -}; - -ControlsHeader.propTypes = { - url: PropTypes.string, - statusPage: PropTypes.object, - isPublic: PropTypes.bool, - type: PropTypes.string, -}; - -export default ControlsHeader; diff --git a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx new file mode 100644 index 000000000..2cc2395bb --- /dev/null +++ b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx @@ -0,0 +1,87 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Icon } from "@/Components/design-elements"; +import { Button } from "@/Components/inputs"; +import { Settings, ExternalLink } from "lucide-react"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import type { StatusPage } from "@/Types/StatusPage"; + +interface HeaderStatusPageControlsProps { + isAdmin: boolean; + statusPage: StatusPage; + isPublic?: boolean; +} +export const HeaderStatusPageControls = ({ + isAdmin, + statusPage, + isPublic = false, +}: HeaderStatusPageControlsProps) => { + const theme = useTheme(); + const navigate = useNavigate(); + const { t } = useTranslation(); + return ( + + + + {statusPage?.companyName} + + {statusPage?.isPublished && !isPublic && ( + <> + { + window.open( + `/status/public/${statusPage.url}`, + "_blank", + "noopener,noreferrer" + ); + }} + sx={{ + borderBottom: 1, + borderColor: "transparent", + ":hover": { + cursor: "pointer", + borderBottom: 1, + }, + }} + > + {t("components.headerStatusPageControls.publicLink")} + + + + + + )} + + {isAdmin && !isPublic && ( + + )} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx new file mode 100644 index 000000000..b94b20a80 --- /dev/null +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -0,0 +1,110 @@ +import Stack from "@mui/material/Stack"; +import { useTranslation } from "react-i18next"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common"; +import { StatusLabel, BaseBox } from "@/Components/design-elements"; +import { SwitchComponent } from "@/Components/inputs"; + +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import { useState } from "react"; +import type { Monitor } from "@/Types/Monitor"; +import type { StatusPage } from "@/Types/StatusPage"; +import type { RootState } from "@/Types/state"; + +interface StatusPageMonitor extends Monitor { + checks?: Monitor["recentChecks"]; +} + +interface MonitorsListProps { + statusPage: StatusPage; + monitors: StatusPageMonitor[]; +} + +export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const showURL = useSelector((state: RootState) => state.ui?.showURL); + + const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram"); + + return ( + + {statusPage.showCharts !== false && ( + + {t("pages.statusPages.monitorsList.chartTypeHeatmap")} + { + setChartType(e.target.checked ? "histogram" : "heatmap"); + }} + /> + + {t("pages.statusPages.monitorsList.chartTypeHistogram")} + + + )} + + {monitors?.map((monitor) => { + return ( + + + + + {monitor.name} + + {showURL && ( + + {monitor.url} + + )} + + + + {statusPage.showCharts !== false && ( + + {chartType === "histogram" ? ( + + ) : ( + + )} + + )} + + ); + })} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx deleted file mode 100644 index 33f771233..000000000 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// Components -import { Stack, Box } from "@mui/material"; -import Host from "@/Components/v1/Host/index.jsx"; -import StatusPageBarChart from "@/Components/v1/Charts/StatusPageBarChart/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; - -//Utils -import { useTheme } from "@mui/material/styles"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; -import PropTypes from "prop-types"; - -import { useSelector } from "react-redux"; - -const MonitorsList = ({ - isLoading = false, - shouldRender = true, - monitors = [], - statusPage = {}, -}) => { - const theme = useTheme(); - const { determineState } = useMonitorUtils(); - - const { showURL } = useSelector((state) => state.ui); - - return ( - <> - {monitors?.map((monitor) => { - const status = determineState(monitor); - return ( - - - - {statusPage.showCharts !== false && ( - - - - )} - - - ); - })} - - ); -}; - -MonitorsList.propTypes = { - monitors: PropTypes.array.isRequired, - statusPage: PropTypes.object, -}; - -export default MonitorsList; diff --git a/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx b/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx deleted file mode 100644 index 482c490fb..000000000 --- a/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx new file mode 100644 index 000000000..b5c52b32b --- /dev/null +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx @@ -0,0 +1,58 @@ +import { AlertTriangle, CircleCheck } from "lucide-react"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; + +import { useTranslation } from "react-i18next"; +import type { Theme } from "@mui/material"; +import { useTheme } from "@mui/material"; +import type { Monitor } from "@/Types/Monitor"; + +const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => { + const monitorsStatus: Record = { + icon: , + }; + + if (monitors.every((monitor) => monitor.status === "up")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.allUp"); + monitorsStatus.color = theme.palette.success.main; + monitorsStatus.icon = ; + return monitorsStatus; + } else if (monitors.every((monitor) => monitor.status === "down")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.allDown"); + monitorsStatus.color = theme.palette.error.main; + return monitorsStatus; + } else if (monitors.some((monitor) => monitor.status === "down")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.degraded"); + monitorsStatus.color = theme.palette.warning.main; + return monitorsStatus; + } else { + monitorsStatus.msg = t("pages.statusPages.statusBar.unknown"); + monitorsStatus.color = theme.palette.warning.main; + return monitorsStatus; + } +}; + +interface StatusBarProps { + monitors: Monitor[]; +} + +export const StatusBar = ({ monitors }: StatusBarProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const monitorsStatus = getMonitorStatus(monitors, theme, t); + + return ( + + {monitorsStatus.icon} + {monitorsStatus.msg} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx deleted file mode 100644 index 7b9460068..000000000 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx +++ /dev/null @@ -1,77 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; - -// Utils -import { useTheme } from "@mui/material/styles"; -import PropTypes from "prop-types"; - -const getMonitorStatus = (monitors, theme) => { - const monitorsStatus = { - icon: ( - - ), - }; - if (monitors.every((monitor) => monitor.status === true)) { - monitorsStatus.msg = "All systems operational"; - monitorsStatus.color = theme.palette.success.lowContrast; - monitorsStatus.icon = ( - - ); - } - - if (monitors.every((monitor) => monitor.status === false)) { - monitorsStatus.msg = "All systems down"; - monitorsStatus.color = theme.palette.error.lowContrast; - } - - if (monitors.some((monitor) => monitor.status === false)) { - monitorsStatus.msg = "Degraded performance"; - monitorsStatus.color = theme.palette.warning.lowContrast; - } - - // Paused or unknown - if (monitors.some((monitor) => typeof monitor.status === "undefined")) { - monitorsStatus.msg = "Unknown status"; - monitorsStatus.color = theme.palette.warning.lowContrast; - } - return monitorsStatus; -}; - -const StatusBar = ({ monitors }) => { - const theme = useTheme(); - - if (typeof monitors === "undefined") return; - - const monitorsStatus = getMonitorStatus(monitors, theme); - return ( - - {monitorsStatus.icon} - {/* CAIO_REVIEW */} - {monitorsStatus.msg} - - ); -}; - -export default StatusBar; - -StatusBar.propTypes = { - status: PropTypes.object, -}; diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx deleted file mode 100644 index 73b9a8a5f..000000000 --- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../../../../Utils/NetworkService.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; - -/** - * Hook for deleting a status page with optimistic UI update - * @param {Function} fetchStatusPage - Function to fetch status page data - * @param {string} url - URL of the status page - * @returns {Array} - [deleteStatusPage function, isLoading state] - */ -const useStatusPageDelete = (fetchStatusPage, url) => { - const [isLoading, setIsLoading] = useState(false); - const { t } = useTranslation(); - - /** - * Delete a status page with optimistic UI update - * @returns {Promise} - Success status - */ - const deleteStatusPage = async () => { - // We don't need to call fetchStatusPage after deletion - // This prevents the 404 error when trying to fetch a deleted status page - try { - setIsLoading(true); - await networkService.deleteStatusPage({ url }); - createToast({ - body: t("statusPage.deleteSuccess", "Status page deleted successfully"), - }); - return true; - } catch (error) { - createToast({ - body: t("statusPage.deleteFailed", "Failed to delete status page"), - }); - return false; - } finally { - setIsLoading(false); - } - }; - - return [deleteStatusPage, isLoading]; -}; - -export { useStatusPageDelete }; diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx deleted file mode 100644 index 7db6b6c5e..000000000 --- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; - -const useStatusPageFetch = (isCreate = false, url) => { - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [statusPage, setStatusPage] = useState(undefined); - const [monitors, setMonitors] = useState(undefined); - const theme = useTheme(); - const { getMonitorWithPercentage } = useMonitorUtils(); - const fetchStatusPage = useCallback(async () => { - try { - const response = await networkService.getStatusPageByUrl({ - url, - type: "uptime", - }); - if (!response?.data?.data) return; - const { statusPage, monitors } = response.data.data; - setStatusPage(statusPage); - - const monitorsWithPercentage = monitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - - setMonitors(monitorsWithPercentage); - } catch (error) { - // If there is a 404, status page is not found - if (error?.response?.status === 404) { - setStatusPage(undefined); - return; - } - createToast({ body: error.message }); - setNetworkError(true); - } finally { - setIsLoading(false); - } - }, [theme, getMonitorWithPercentage, url]); - - useEffect(() => { - if (isCreate === true) { - return; - } - fetchStatusPage(); - }, [isCreate, fetchStatusPage]); - - return [statusPage, monitors, isLoading, networkError, fetchStatusPage]; -}; - -export { useStatusPageFetch }; diff --git a/client/src/Pages/StatusPage/Status/index.jsx b/client/src/Pages/StatusPage/Status/index.jsx deleted file mode 100644 index adcd9c2c7..000000000 --- a/client/src/Pages/StatusPage/Status/index.jsx +++ /dev/null @@ -1,166 +0,0 @@ -// Components -import { Typography, Stack } from "@mui/material"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import AdminLink from "./Components/AdminLink/index.jsx"; -import ControlsHeader from "./Components/ControlsHeader/index.jsx"; -import SkeletonLayout from "./Components/Skeleton/index.jsx"; -import StatusBar from "./Components/StatusBar/index.jsx"; -import MonitorsList from "./Components/MonitorsList/index.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import TextLink from "@/Components/v1/TextLink/index.jsx"; - -// Utils -import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx"; -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { useLocation } from "react-router-dom"; -import { useParams } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -const PublicStatus = () => { - const { url } = useParams(); - - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - const location = useLocation(); - const isAdmin = useIsAdmin(); - - const [statusPage, monitors, isLoading, networkError] = useStatusPageFetch(false, url); - - // Breadcrumbs - const crumbs = [ - { name: t("statusBreadCrumbsStatusPages"), path: "/status" }, - { name: t("statusBreadCrumbsDetails"), path: `/status/uptime/${statusPage?.url}` }, - ]; - - // Setup - let sx = { paddingLeft: theme.spacing(20), paddingRight: theme.spacing(20) }; - let link = undefined; - const isPublic = location.pathname.startsWith("/status/uptime/public"); - // Public status page - if (isPublic && statusPage && statusPage.showAdminLoginLink === true) { - sx = { - paddingTop: theme.spacing(20), - paddingLeft: "20vw", - paddingRight: "20vw", - }; - link = ; - } - - // Loading - if (isLoading) { - return ; - } - - if (monitors?.length === 0) { - return ( - - - {"Theres nothing here yet"} - - {isAdmin && ( - - )} - - ); - } - - // Error fetching data - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Public status page fallback - if (!isLoading && typeof statusPage === "undefined" && isPublic) { - return ( - - - - {t("statusPageStatus")} - - {t("statusPageStatusContactAdmin")} - - - ); - } - - // Finished loading, but status page is not public - if (!isLoading && isPublic && statusPage.isPublished === false) { - return ( - - - - {t("statusPageStatusNotPublic")} - - {t("statusPageStatusContactAdmin")} - - - ); - } - - // Status page doesn't exist - if (!isLoading && typeof statusPage === "undefined") { - return ( - - - {t("statusPageStatusNoPage")} - - {t("statusPageStatusContactAdmin")} - - ); - } - - return ( - - {!isPublic && } - - {t("statusPageStatusServiceStatus")} - - - {link} - - ); -}; - -export default PublicStatus; diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx new file mode 100644 index 000000000..0ca629581 --- /dev/null +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -0,0 +1,108 @@ +import { BasePage, BaseFallback } from "@/Components/design-elements"; +import { StatusBar } from "@/Pages/StatusPage/Status/Components/StatusBar"; +import { MonitorsList } from "@/Pages/StatusPage/Status/Components/MonitorsList"; +import Typography from "@mui/material/Typography"; +import { Link } from "react-router-dom"; +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; + +import { useMediaQuery, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import { useLocation, useParams } from "react-router-dom"; +import { useGet } from "@/Hooks/UseApi"; +import type { StatusPageResponse } from "@/Types/StatusPage"; +import { HeaderStatusPageControls } from "./Components/HeaderStatusPageControls"; + +const StatusPageView = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const { url } = useParams(); + const isAdmin = useIsAdmin(); + const location = useLocation(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const isPublic = location.pathname.startsWith("/status/public"); + + const apiUrl = url ? `/status-page/${url}?type=uptime` : null; + + const { data, isLoading, error } = useGet(apiUrl); + + const statusPage = data?.statusPage; + const monitors = data?.monitors ?? []; + + if (!statusPage) return null; + + if (monitors?.length === 0) { + return ( + + + + + {t("pages.statusPages.details.empty.title")} + + {isAdmin && ( + + {t("pages.statusPages.details.empty.addMonitor")} + + )} + + + + ); + } + + let sx: React.CSSProperties = {}; + if (isPublic) { + sx.paddingTop = theme.spacing(20); + sx.paddingLeft = isSmall ? "5vw" : "20vw"; + sx.paddingRight = isSmall ? "5vw" : "20vw"; + } + + const logoSrc = statusPage.logo?.data + ? `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}` + : null; + + return ( + + + {logoSrc && ( + + )} + + + + ); +}; + +export default StatusPageView; diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx new file mode 100644 index 000000000..978466bb4 --- /dev/null +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx @@ -0,0 +1,144 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Table, type Header, ValueLabel } from "@/Components/design-elements"; +import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu"; +import { ExternalLink } from "lucide-react"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; + +import type { StatusPage } from "@/Types/StatusPage"; + +interface StatusPagesTableProps { + data: StatusPage[]; + setSelectedStatusPage: (statusPage: StatusPage | null) => void; +} + +export const StatusPagesTable = ({ + data, + setSelectedStatusPage, +}: StatusPagesTableProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + + const getActions = (row: StatusPage): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("common.buttons.configure"), + action: () => { + navigate(`/status/configure/${row.url}`); + }, + closeMenu: true, + }, + { + id: 2, + label: ( + + {t("common.buttons.delete")} + + ), + action: () => setSelectedStatusPage(row), + closeMenu: true, + }, + ]; + }; + + const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => { + if (row.isPublished) { + e.stopPropagation(); + const url = `/status/public/${row.url}`; + window.open(url, "_blank", "noopener,noreferrer"); + } + }; + + const getHeaders = (): Header[] => { + return [ + { + id: "name", + content: t("pages.statusPages.table.headers.name"), + render: (row) => row.companyName, + }, + { + id: "url", + content: t("pages.statusPages.table.headers.url"), + render: (row) => { + const content = row.isPublished + ? `/${row.url}` + : t("pages.statusPages.table.unpublished"); + return ( + handleUrlClick(e, row)} + sx={{ + ...(row.isPublished && { + display: "inline-flex", + ":hover": { + cursor: "pointer", + borderBottom: 1, + }, + }), + }} + > + {content} + {row.isPublished && } + + ); + }, + }, + { + id: "type", + content: t("common.table.headers.type"), + render: (row) => row.type, + }, + { + id: "status", + content: t("common.table.headers.status"), + render: (row) => { + return ( + + ); + }, + }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + }; + + const handleRowClick = (statusPage: StatusPage) => { + navigate(`/status/${statusPage.url}`); + }; + + return ( + +
+ + ); +}; + +export default StatusPagesTable; diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx deleted file mode 100644 index 9882239bd..000000000 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import DataTable from "@/Components/v1/Table/index.jsx"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import { Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -const StatusPagesTable = ({ data }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const headers = [ - { - id: "name", - content: t("statusPageName"), - render: (row) => { - return row.companyName; - }, - }, - { - id: "url", - content: t("publicURL"), - onClick: (e, row) => { - if (row.isPublished) { - e.stopPropagation(); - const url = `/status/uptime/public/${row.url}`; - window.open(url, "_blank", "noopener,noreferrer"); - } - }, - render: (row) => { - const content = row.isPublished ? `/${row.url}` : "Unpublished"; - return ( - - {content} - {row.isPublished && ( - - )} - - ); - }, - }, - { - id: "type", - content: t("type"), - render: (row) => { - return row.type; - }, - }, - { - id: "status", - content: t("status"), - render: (row) => { - const status = row.isPublished ? "published" : "unpublished"; - return ( - - ); - }, - }, - ]; - - const handleRowClick = (statusPage) => { - navigate(`/status/uptime/${statusPage.url}`); - }; - - return ( - { - handleRowClick(row); - }, - }} - headers={headers} - data={data} - /> - ); -}; - -export default StatusPagesTable; diff --git a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx deleted file mode 100644 index 9336e5d31..000000000 --- a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -const useStatusPagesFetch = () => { - const { user } = useSelector((state) => state.auth); - - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [statusPages, setStatusPages] = useState(undefined); - - useEffect(() => { - const fetchStatusPages = async () => { - try { - const res = await networkService.getStatusPagesByTeamId(); - setStatusPages(res?.data?.data || []); - } catch (error) { - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - fetchStatusPages(); - }, [user]); - return [isLoading, networkError, statusPages]; -}; - -export { useStatusPagesFetch }; diff --git a/client/src/Pages/StatusPage/StatusPages/index.jsx b/client/src/Pages/StatusPage/StatusPages/index.jsx deleted file mode 100644 index 7137d01ad..000000000 --- a/client/src/Pages/StatusPage/StatusPages/index.jsx +++ /dev/null @@ -1,43 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; -import StatusPagesTable from "./Components/StatusPagesTable/index.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; -// Utils -import { useTheme } from "@emotion/react"; -import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx"; -import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -const BREADCRUMBS = [{ name: `Status Pages`, path: "" }]; - -const StatusPages = () => { - // Utils - const theme = useTheme(); - const isAdmin = useIsAdmin(); - const [isLoading, networkError, statusPages] = useStatusPagesFetch(); - - return ( - <> - - - - - - - - - ); -}; - -export default StatusPages; diff --git a/client/src/Pages/StatusPage/StatusPages/index.tsx b/client/src/Pages/StatusPage/StatusPages/index.tsx new file mode 100644 index 000000000..9468b7e12 --- /dev/null +++ b/client/src/Pages/StatusPage/StatusPages/index.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { BasePageWithStates } from "@/Components/design-elements"; +import { Dialog } from "@/Components/inputs"; +import { StatusPagesTable } from "./Components/StatusPagesTable"; +import { useGet, useDelete } from "@/Hooks/UseApi"; +import type { StatusPage } from "@/Types/StatusPage"; +import { useTranslation } from "react-i18next"; +import { HeaderCreate } from "@/Components/common"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; + +const StatusPages = () => { + const { t } = useTranslation(); + + const { + data: statusPages, + isLoading, + error, + refetch, + } = useGet("/status-page/team"); + + const { deleteFn, loading: isDeleting } = useDelete(); + const [selectedStatusPage, setSelectedStatusPage] = useState(null); + const isDialogOpen = Boolean(selectedStatusPage); + + const isAdmin = useIsAdmin(); + + const handleConfirm = async () => { + if (!selectedStatusPage) return; + await deleteFn(`/status-page/${selectedStatusPage.id}`); + setSelectedStatusPage(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedStatusPage(null); + }; + + return ( + + + + + + ); +}; + +export default StatusPages; diff --git a/client/src/Pages/Uptime/BulkImport/Upload.jsx b/client/src/Pages/Uptime/BulkImport/Upload.jsx deleted file mode 100644 index 24bc0ec38..000000000 --- a/client/src/Pages/Uptime/BulkImport/Upload.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { useState, useRef } from "react"; -import { Button, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -const UploadFile = ({ onFileSelect }) => { - // Changed prop to onFileSelect - const theme = useTheme(); - const [file, setFile] = useState(); - const [error, setError] = useState(""); - const inputRef = useRef(); - const { t } = useTranslation(); - - const handleSelectFile = () => { - inputRef.current.click(); - }; - - const handleFileChange = (e) => { - setError(""); - const selectedFile = e.target.files[0]; - - // Basic file validation - if (!selectedFile) return; - - if (!selectedFile.name.toLowerCase().endsWith(".csv")) { - setError(t("bulkImport.invalidFileType")); - return; - } - - setFile(selectedFile); - onFileSelect(selectedFile); // Pass the file directly to parent - }; - - return ( -
- - - {file?.name || t("bulkImport.noFileSelected")} - - - {error} - - -
- ); -}; - -UploadFile.prototype = { - onFileSelect: PropTypes.func.isRequired, -}; - -export default UploadFile; diff --git a/client/src/Pages/Uptime/BulkImport/index.jsx b/client/src/Pages/Uptime/BulkImport/index.jsx deleted file mode 100644 index 6f2f7bcb5..000000000 --- a/client/src/Pages/Uptime/BulkImport/index.jsx +++ /dev/null @@ -1,117 +0,0 @@ -// React, Redux, Router -import { useTheme } from "@emotion/react"; -import { useState } from "react"; -// MUI -import { Box, Stack, Typography, Button, Link } from "@mui/material"; - -//Components -import { createToast } from "../../../Utils/toastUtils.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import UploadFile from "./Upload.jsx"; -import { useSelector } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import { Trans, useTranslation } from "react-i18next"; -import { useCreateBulkMonitors } from "../../../Hooks/monitorHooks.js"; - -const BulkImport = () => { - const theme = useTheme(); - const { user } = useSelector((state) => state.auth); - const navigate = useNavigate(); - const { t } = useTranslation(); - const [selectedFile, setSelectedFile] = useState(null); - - const crumbs = [ - { name: t("uptime"), path: "/uptime" }, - { name: t("bulkImport.title"), path: `/uptime/bulk-import` }, - ]; - - const [createBulkMonitors, hookLoading] = useCreateBulkMonitors(); - - const handleSubmit = async () => { - if (!selectedFile) { - createToast({ body: t("bulkImport.noFileSelected") }); - return; - } - - const [success, data, error] = await createBulkMonitors(selectedFile, user); - - if (success) { - // You can use `data` here if needed - createToast({ body: t("bulkImport.uploadSuccess") }); - navigate("/uptime"); - } else { - createToast({ body: error || t("bulkImport.uploadFailed") }); - } - }; - - return ( - - - - - {t("bulkImport.title")} - - - - - {t("bulkImport.selectFileTips")} - - - - ), - sample: ( - - ), - }} - /> - - - - - setSelectedFile(file)} /> - - - - - - - - - ); -}; - -export default BulkImport; diff --git a/client/src/Pages/Uptime/Create/index.css b/client/src/Pages/Uptime/Create/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx deleted file mode 100644 index 01fc9eb7a..000000000 --- a/client/src/Pages/Uptime/Create/index.jsx +++ /dev/null @@ -1,826 +0,0 @@ -//Components -import { - Box, - Button, - ButtonGroup, - FormControlLabel, - Stack, - Switch, - Tooltip, - Typography, -} from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; -import SkeletonLayout from "./skeleton.jsx"; - -// Utils -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { monitorValidation } from "../../../Validation/validation.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import Icon from "@/Components/v1/Icon"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; -import { useParams } from "react-router-dom"; -import { - useCreateMonitor, - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, - useFetchMonitorById, - useFetchMonitorGames, -} from "../../../Hooks/monitorHooks.js"; - -/** - * Create page renders monitor creation or configuration views. - * @component - */ -const UptimeCreate = ({ isClone = false }) => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined" || isClone; - - // States - const [monitor, setMonitor] = useState({ - type: "http", - statusWindowSize: 5, - statusWindowThreshold: 60, - matchMethod: "equal", - expectedValue: "", - jsonPath: "", - notifications: [], - interval: 60000, - ignoreTlsErrors: false, - ...(isCreate ? { url: "", name: "" } : { port: undefined }), - }); - const [errors, setErrors] = useState({}); - const [https, setHttps] = useState(true); - const [isOpen, setIsOpen] = useState(false); - const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [games, setGames] = useState({}); - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - // Hooks - const [notifications, notificationsAreLoading, notificationsError] = - useGetNotificationsByTeamId(); - const { determineState, statusColor } = useMonitorUtils(); - // Fetch monitor details - const [isFetchingMonitor] = useFetchMonitorById({ - monitorId, - setMonitor, - updateTrigger, - }); - - // Fetch games - const [isFetchingGames] = useFetchMonitorGames({ - setGames, - updateTrigger, - }); - - // Combine the loading states - const isLoading = isFetchingMonitor || isFetchingGames; - - const [createMonitor, isCreating] = useCreateMonitor(); - const [pauseMonitor, isPausing] = usePauseMonitor({}); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - - // Setup - const theme = useTheme(); - const { t } = useTranslation(); - - // Constants - const MS_PER_MINUTE = 60000; - const FREQUENCIES = [ - { _id: 0.25, name: t("time.fifteenSeconds") }, - { _id: 0.5, name: t("time.thirtySeconds") }, - { _id: 1, name: t("time.oneMinute") }, - { _id: 2, name: t("time.twoMinutes") }, - { _id: 3, name: t("time.threeMinutes") }, - { _id: 4, name: t("time.fourMinutes") }, - { _id: 5, name: t("time.fiveMinutes") }, - { _id: 10, name: t("time.tenMinutes") }, - { _id: 15, name: t("time.fifteenMinutes") }, - { _id: 30, name: t("time.thirtyMinutes") }, - ]; - - const GAMELIST = Object.entries(games).map(([key, value]) => ({ - _id: key, - name: value.name, - })); - - const CRUMBS = [ - { name: "uptime", path: "/uptime" }, - ...(isCreate - ? [{ name: "create", path: `/uptime/create` }] - : [ - { name: "details", path: `/uptime/${monitorId}` }, - { name: "configure", path: `/uptime/configure/${monitorId}` }, - ]), - ]; - const matchMethodOptions = [ - { _id: "equal", name: t("matchMethodOptions.equal") }, - { _id: "include", name: t("matchMethodOptions.include") }, - { _id: "regex", name: t("matchMethodOptions.regex") }, - ]; - const expectedValuePlaceholders = { - regex: t("matchMethodOptions.regexPlaceholder"), - equal: t("matchMethodOptions.equalPlaceholder"), - include: t("matchMethodOptions.includePlaceholder"), - }; - const monitorTypeMaps = { - http: { - label: t("monitorType.http.label"), - placeholder: t("monitorType.http.placeholder"), - namePlaceholder: t("monitorType.http.namePlaceholder"), - }, - ping: { - label: t("monitorType.ping.label"), - placeholder: t("monitorType.ping.placeholder"), - namePlaceholder: t("monitorType.ping.namePlaceholder"), - }, - docker: { - label: t("monitorType.docker.label"), - placeholder: t("monitorType.docker.placeholder"), - namePlaceholder: t("monitorType.docker.namePlaceholder"), - }, - port: { - label: t("monitorType.port.label"), - placeholder: t("monitorType.port.placeholder"), - namePlaceholder: t("monitorType.port.namePlaceholder"), - }, - game: { - label: t("monitorType.game.label"), - placeholder: t("monitorType.game.placeholder"), - namePlaceholder: t("monitorType.game.namePlaceholder"), - }, - }; - - // Handlers - const onSubmit = async (event) => { - event.preventDefault(); - const { notifications, ...rest } = monitor; - - let form = {}; - if (isCreate) { - form = { - url: - monitor.type === "http" && !isClone - ? `http${https ? "s" : ""}://` + monitor.url - : monitor.url, - name: monitor.name || monitor.url.substring(0, 50), - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - type: monitor.type, - - port: - monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, - interval: monitor.interval, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - jsonPath: monitor.jsonPath, - ignoreTlsErrors: monitor.ignoreTlsErrors, - gameId: monitor.gameId || undefined, - }; - } else { - form = { - id: monitor.id, - url: monitor.url, - name: monitor.name || monitor.url.substring(0, 50), - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - type: monitor.type, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - jsonPath: monitor.jsonPath, - interval: monitor.interval, - teamId: monitor.teamId, - userId: monitor.userId, - port: - monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, - ignoreTlsErrors: monitor.ignoreTlsErrors, - gameId: monitor.gameId || undefined, - }; - } - if (!useAdvancedMatching) { - form.matchMethod = isCreate ? undefined : ""; - form.expectedValue = isCreate ? undefined : ""; - form.jsonPath = isCreate ? undefined : ""; - } - - const { error } = monitorValidation.validate(form, { - abortEarly: false, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: t("checkFormError") }); - return; - } - - form = { - ...form, - description: monitor.name || monitor.url, - notifications: monitor.notifications, - }; - - if (isCreate) { - await createMonitor({ monitor: form, redirect: "/uptime" }); - } else { - await updateMonitor({ monitor: form, redirect: "/uptime" }); - } - }; - - const onChange = (event) => { - let { name, value, checked } = event.target; - - if (name === "ignoreTlsErrors") { - value = checked; - } - - if (name === "useAdvancedMatching") { - setUseAdvancedMatching(checked); - return; - } - - if (name === "interval") { - value = value * MS_PER_MINUTE; - } - - setMonitor((prev) => ({ ...prev, [name]: value })); - - if (name === "type") { - setErrors({}); - return; - } - - const { error } = monitorValidation.validate( - { type: monitor.type, [name]: value }, - { abortEarly: false } - ); - - setErrors((prev) => ({ - ...prev, - ...(error && error.details[0].path[0] === name - ? { [name]: error.details[0].message } - : { [name]: undefined }), - })); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - const TEMP_MONITOR = { id: monitor.id }; - await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" }); - }; - - const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; - const displayInterval = monitor?.interval / MS_PER_MINUTE || 1; - const parsedUrl = monitor?.url; - const protocol = parsedUrl?.protocol?.replace(":", "") || ""; - - useEffect(() => { - if (!isCreate || isClone) { - if (monitor.matchMethod) { - setUseAdvancedMatching(true); - } else { - setUseAdvancedMatching(false); - } - } - }, [monitor, isCreate]); - - if (Object.keys(monitor).length === 0) { - return ; - } - - return ( - - - - - - - - - {!isCreate ? monitor.name : t("createYour") + " "} - - {isCreate && ( - - {t("monitor")} - - )} - - {!isCreate && ( - - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - )} - - {!isCreate && ( - - - - - )} - - {isCreate && ( - - - - {t("distributedUptimeCreateChecks")} - - - {t("distributedUptimeCreateChecksDescription")} - - - - - - {monitor.type === "http" ? ( - - - - - ) : ( - "" - )} - - - - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - - )} - - - - {t("settingsGeneralSettings")} - - - {isCreate - ? t(`uptimeGeneralInstructions.${monitor.type}`) - : t("distributedUptimeCreateSelectURL")} - - - - - ) - } - helperText={errors["url"]} - onChange={onChange} - disabled={!isCreate} - /> -