diff --git a/.gitignore b/.gitignore index a0420fec9..63420e072 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode *.sh !uptime.sh -*.idea \ No newline at end of file +*.idea +*.notes \ No newline at end of file diff --git a/Client/.env.production b/Client/.env.production index a5bce84e1..2ae6d13cd 100644 --- a/Client/.env.production +++ b/Client/.env.production @@ -1 +1,2 @@ VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL +VITE_STATUS_PAGE_SUBDOMAIN_PREFIX=UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX diff --git a/Client/package-lock.json b/Client/package-lock.json index f92a90496..51a0c8964 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -11,6 +11,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", + "@hello-pangea/dnd": "^17.0.0", "@mui/icons-material": "6.4.3", "@mui/lab": "6.0.0-beta.26", "@mui/material": "6.4.3", @@ -20,9 +21,15 @@ "@reduxjs/toolkit": "2.5.1", "axios": "^1.7.4", "dayjs": "1.11.13", + "flag-icons": "7.3.2", + "immutability-helper": "^3.1.1", "joi": "17.13.3", "jwt-decode": "^4.0.0", + "maplibre-gl": "5.1.0", + "mui-color-input": "^5.0.1", "react": "^18.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-redux": "9.2.0", "react-router": "^6.23.0", @@ -332,6 +339,14 @@ "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==", + "engines": { + "node": ">=14" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -984,6 +999,24 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@hello-pangea/dnd": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-17.0.0.tgz", + "integrity": "sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==", + "dependencies": { + "@babel/runtime": "^7.25.6", + "css-box-model": "^1.2.1", + "memoize-one": "^6.0.0", + "raf-schd": "^4.0.3", + "react-redux": "^9.1.2", + "redux": "^5.0.1", + "use-memo-one": "^1.1.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1070,6 +1103,83 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", + "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.1.0.tgz", + "integrity": "sha512-R6/ihEuC5KRexmKIYkWqUv84Gm+/QwsOUgHyt1yy2XqCdGdLvlBWVWIIeTZWN4NGdwmY6xDzdSGU2R9oBLNg2w==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.69", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.69.tgz", @@ -1618,6 +1728,21 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@react-spring/animated": { "version": "9.7.5", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", @@ -2359,12 +2484,50 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, "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/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "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", @@ -2400,6 +2563,15 @@ "@types/react": "*" } }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "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", @@ -2932,6 +3104,14 @@ "node": ">= 8" } }, + "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==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3215,6 +3395,24 @@ "node": ">=0.4.0" } }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, + "node_modules/dnd-core/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3263,6 +3461,12 @@ "node": ">= 0.4" } }, + "node_modules/earcut": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", + "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", + "license": "ISC" + }, "node_modules/electron-to-chromium": { "version": "1.5.65", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz", @@ -3785,7 +3989,6 @@ "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": { @@ -3857,6 +4060,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -3993,6 +4202,12 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, "node_modules/get-intrinsic": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", @@ -4018,6 +4233,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -4036,6 +4263,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4071,6 +4304,44 @@ "node": ">=10.13.0" } }, + "node_modules/global-prefix": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/global-prefix/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -4222,6 +4493,26 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4242,6 +4533,11 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutability-helper": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", + "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4287,6 +4583,15 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4789,6 +5094,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4826,6 +5137,12 @@ "node": ">=18" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4836,6 +5153,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4915,6 +5241,47 @@ "yallist": "^3.0.2" } }, + "node_modules/maplibre-gl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.1.0.tgz", + "integrity": "sha512-6lbf7qAnqAVm1T/vJBMmRtP+g8G/O/Z52IBtWX31SbFj7sEdlrk4YugxJen8IdV/pFjLFnDOw7HiHZl5nYdVjg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^23.1.0", + "@types/geojson": "^7946.0.16", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.1", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4925,6 +5292,11 @@ "node": ">= 0.4" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4959,12 +5331,48 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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": "5.0.1", + "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-5.0.1.tgz", + "integrity": "sha512-50Ws4vhg4UPQSZEZDCNc7vyUBSb9x1bK+bO1o0wxJvQYgeSyg2r7mYDlavpCh+ZvisgBL/98y0GVN6M9901JWg==", + "dependencies": { + "@ctrl/tinycolor": "^4.1.0" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": "^6.0.0", + "@types/react": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -5246,6 +5654,19 @@ "node": ">=8" } }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5302,6 +5723,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/potpack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", + "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5345,6 +5772,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "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", @@ -5382,6 +5815,17 @@ ], "license": "MIT" }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "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==" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -5394,6 +5838,43 @@ "node": ">=0.10.0" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -5658,6 +6139,15 @@ "node": ">=4" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5753,6 +6243,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -6100,6 +6596,15 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6144,6 +6649,12 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6313,6 +6824,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", @@ -6417,6 +6936,17 @@ "vite": ">=2.6.0" } }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/Client/package.json b/Client/package.json index 1b1ded008..6b006e24e 100644 --- a/Client/package.json +++ b/Client/package.json @@ -14,6 +14,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", + "@hello-pangea/dnd": "^17.0.0", "@mui/icons-material": "6.4.3", "@mui/lab": "6.0.0-beta.26", "@mui/material": "6.4.3", @@ -23,9 +24,15 @@ "@reduxjs/toolkit": "2.5.1", "axios": "^1.7.4", "dayjs": "1.11.13", + "flag-icons": "7.3.2", + "immutability-helper": "^3.1.1", "joi": "17.13.3", "jwt-decode": "^4.0.0", + "maplibre-gl": "5.1.0", + "mui-color-input": "^5.0.1", "react": "^18.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-redux": "9.2.0", "react-router": "^6.23.0", diff --git a/Client/src/Pages/Uptime/Monitors/Components/ActionsMenu/index.jsx b/Client/src/Components/ActionsMenu/index.jsx similarity index 94% rename from Client/src/Pages/Uptime/Monitors/Components/ActionsMenu/index.jsx rename to Client/src/Components/ActionsMenu/index.jsx index f0561d6e6..c39c130cb 100644 --- a/Client/src/Pages/Uptime/Monitors/Components/ActionsMenu/index.jsx +++ b/Client/src/Components/ActionsMenu/index.jsx @@ -2,23 +2,23 @@ import { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../../../Utils/toastUtils"; -import { logger } from "../../../../../Utils/Logger"; +import { createToast } from "../../Utils/toastUtils"; +import { logger } from "../../Utils/Logger"; import { IconButton, Menu, MenuItem } from "@mui/material"; import { deleteUptimeMonitor, pauseUptimeMonitor, -} from "../../../../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import Settings from "../../../../../assets/icons/settings-bold.svg?react"; +} from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import Settings from "../../assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; -import Dialog from "../../../../../Components/Dialog"; +import Dialog from "../../Components/Dialog"; const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback, - setIsLoading, + setIsLoading = () => {}, }) => { const [anchorEl, setAnchorEl] = useState(null); const [actions, setActions] = useState({}); diff --git a/Client/src/Components/Charts/ChartBox/index.jsx b/Client/src/Components/Charts/ChartBox/index.jsx index eed3efd0d..bd27d4775 100644 --- a/Client/src/Components/Charts/ChartBox/index.jsx +++ b/Client/src/Components/Charts/ChartBox/index.jsx @@ -11,6 +11,7 @@ const ChartBox = ({ justifyContent = "space-between", Legend, borderRadiusRight = 4, + sx, }) => { const theme = useTheme(); return ( @@ -70,8 +71,8 @@ const ChartBox = ({ alignItems="center" gap={theme.spacing(6)} > - {icon} - {header} + {icon && {icon}} + {header && {header}} {children} @@ -84,7 +85,7 @@ export default ChartBox; ChartBox.propTypes = { children: PropTypes.node, - icon: PropTypes.node.isRequired, - header: PropTypes.string.isRequired, + icon: PropTypes.node, + header: PropTypes.string, height: PropTypes.string, }; diff --git a/Client/src/Components/Charts/StatusPageBarChart/index.jsx b/Client/src/Components/Charts/StatusPageBarChart/index.jsx new file mode 100644 index 000000000..5acf4c3d6 --- /dev/null +++ b/Client/src/Components/Charts/StatusPageBarChart/index.jsx @@ -0,0 +1,175 @@ +import { useTheme } from "@emotion/react"; +import { Box, Stack, Tooltip, Typography } from "@mui/material"; +import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; + +/* TODO add prop validation and jsdocs */ +const StatusPageBarChart = ({ checks = [] }) => { + const theme = useTheme(); + const [animate, setAnimate] = useState(false); + const uiTimezone = useSelector((state) => state.ui.timezone); + + useEffect(() => { + setAnimate(true); + }, []); + + // set responseTime to average if there's only one check + if (checks.length === 1) { + checks[0] = { ...checks[0], responseTime: 50 }; + } + + if (checks.length !== 25) { + const placeholders = Array(25 - checks.length).fill("placeholder"); + checks = [...checks, ...placeholders]; + } + + return ( + event.stopPropagation()} + sx={{ + cursor: "default", + }} + > + {checks.map((check, index) => + check === "placeholder" ? ( + /* TODO what is the purpose of this box? */ + // CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars + // even if there are less than 25 checks + + ) : ( + + + {formatDateWithTz( + check.createdAt, + "ddd, MMMM D, YYYY, HH:mm A", + uiTimezone + )} + + + + + + Response Time + + + {check.originalResponseTime} + + {" "} + ms + + + + + + } + placement="top" + key={`check-${check?._id}`} + slotProps={{ + popper: { + className: "bar-tooltip", + modifiers: [ + { + name: "offset", + options: { + offset: [0, -10], + }, + }, + ], + sx: { + "& .MuiTooltip-tooltip": { + backgroundColor: theme.palette.secondary.main, + border: 1, + borderColor: theme.palette.primary.lowContrast, + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shape.boxShadow, + px: theme.spacing(4), + py: theme.spacing(3), + }, + "& .MuiTooltip-tooltip p": { + /* TODO Font size should point to theme */ + fontSize: 12, + color: theme.palette.secondary.contrastText, + fontWeight: 500, + }, + "& .MuiTooltip-tooltip span": { + /* TODO Font size should point to theme */ + fontSize: 11, + color: theme.palette.secondary.contrastText, + fontWeight: 600, + }, + }, + }, + }} + > + + + + + ) + )} + + ); +}; + +export default StatusPageBarChart; diff --git a/Client/src/Components/ConfigBox/index.jsx b/Client/src/Components/ConfigBox/index.jsx index 8f7eb7b24..9b64712fc 100644 --- a/Client/src/Components/ConfigBox/index.jsx +++ b/Client/src/Components/ConfigBox/index.jsx @@ -29,10 +29,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({ }, "& h1, & h2": { color: theme.palette.primary.contrastTextSecondary, - }, - "& p": { - color: theme.palette.primary.contrastTextTertiary, - }, + } })); export default ConfigBox; diff --git a/Client/src/Components/Dialog/index.jsx b/Client/src/Components/Dialog/index.jsx index 97d822e84..3cf7001ce 100644 --- a/Client/src/Components/Dialog/index.jsx +++ b/Client/src/Components/Dialog/index.jsx @@ -1,18 +1,19 @@ import PropTypes from "prop-types"; -import LoadingButton from "@mui/lab/LoadingButton"; import { Button, Stack } from "@mui/material"; import { GenericDialog } from "./genericDialog"; +import { useTheme } from "@emotion/react"; const Dialog = ({ title, description, open, - theme, onCancel, confirmationButtonLabel, onConfirm, isLoading, }) => { + const theme = useTheme(); + return ( Cancel - {confirmationButtonLabel} - + ); @@ -51,7 +52,6 @@ Dialog.propTypes = { title: PropTypes.string.isRequired, description: PropTypes.string, open: PropTypes.bool.isRequired, - theme: PropTypes.object.isRequired, onCancel: PropTypes.func.isRequired, confirmationButtonLabel: PropTypes.string.isRequired, onConfirm: PropTypes.func.isRequired, diff --git a/Client/src/Pages/Uptime/Monitors/Components/Host/index.jsx b/Client/src/Components/Host/index.jsx similarity index 96% rename from Client/src/Pages/Uptime/Monitors/Components/Host/index.jsx rename to Client/src/Components/Host/index.jsx index c2f082f3a..051e60331 100644 --- a/Client/src/Pages/Uptime/Monitors/Components/Host/index.jsx +++ b/Client/src/Components/Host/index.jsx @@ -1,7 +1,7 @@ import { Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import Dot from "../../../../../Components/Dot"; +import Dot from "../Dot"; /** * Host component. * This subcomponent receives a params object and displays the host details. diff --git a/Client/src/Components/Image/index.jsx b/Client/src/Components/Image/index.jsx new file mode 100644 index 000000000..9304a4385 --- /dev/null +++ b/Client/src/Components/Image/index.jsx @@ -0,0 +1,57 @@ +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", + maxWidth = "auto", + maxHeight = "auto", + base64, + 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}`; + } + + return ( + + ); +}; + +Image.propTypes = { + shouldRender: PropTypes.bool, + src: PropTypes.string, + alt: PropTypes.string.isRequired, + width: PropTypes.string, + height: PropTypes.string, + maxWidth: PropTypes.string, + maxHeight: PropTypes.string, + base64: PropTypes.string, + sx: PropTypes.object, +}; + +export default Image; diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx index b3a036d57..58b83c7c7 100644 --- a/Client/src/Components/Inputs/Checkbox/index.jsx +++ b/Client/src/Components/Inputs/Checkbox/index.jsx @@ -75,7 +75,6 @@ const Checkbox = ({ sx={{ "&:hover": { backgroundColor: "transparent" }, "& svg": { width: sizes[size], height: sizes[size] }, - alignSelf: "flex-start", }} /> } @@ -111,7 +110,7 @@ Checkbox.propTypes = { label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, size: PropTypes.oneOf(["small", "medium", "large"]), isChecked: PropTypes.bool.isRequired, - value: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), onChange: PropTypes.func, isDisabled: PropTypes.bool, }; diff --git a/Client/src/Components/Inputs/ColorPicker/index.jsx b/Client/src/Components/Inputs/ColorPicker/index.jsx new file mode 100644 index 000000000..e64c936fc --- /dev/null +++ b/Client/src/Components/Inputs/ColorPicker/index.jsx @@ -0,0 +1,63 @@ +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/Inputs/Image/index.jsx b/Client/src/Components/Inputs/Image/index.jsx index a955a8ad6..0a2a2ef91 100644 --- a/Client/src/Components/Inputs/Image/index.jsx +++ b/Client/src/Components/Inputs/Image/index.jsx @@ -11,11 +11,16 @@ import { checkImage } from "../../../Utils/fileUtils"; * @param {string} props.id - The unique identifier for the input field. * @param {string} props.src - The URL of the image to display. * @param {function} props.onChange - The function to handle file input change. + * @param {boolean} props.isRound - Whether the shape of the image to display is round. + * @param {string} props.maxSize - Custom message for the max uploaded file size * @returns {JSX.Element} The rendered component. */ -const ImageField = ({ id, src, loading, onChange }) => { +const ImageField = ({ id, src, loading, onChange, error, isRound = true, maxSize }) => { const theme = useTheme(); + const error_border_style = error ? { borderColor: theme.palette.error.main } : {}; + + const roundShape = isRound ? { borderRadius: "50%" } : {}; const [isDragging, setIsDragging] = useState(false); const handleDragEnter = () => { @@ -46,6 +51,7 @@ const ImageField = ({ id, src, loading, onChange }) => { borderColor: theme.palette.primary.main, backgroundColor: "hsl(215, 87%, 51%, 0.05)", }, + ...error_border_style, }} onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} @@ -62,6 +68,7 @@ const ImageField = ({ id, src, loading, onChange }) => { cursor: "pointer", maxWidth: "500px", minHeight: "175px", + zIndex: 1, }, "& fieldset": { padding: 0, @@ -78,7 +85,7 @@ const ImageField = ({ id, src, loading, onChange }) => { top: "50%", left: "50%", transform: "translate(-50%, -50%)", - zIndex: "-1", + zIndex: 0, width: "100%", }} > @@ -111,7 +118,7 @@ const ImageField = ({ id, src, loading, onChange }) => { color={theme.palette.primary.contrastTextTertiary} sx={{ opacity: 0.6 }} > - (maximum size: 3MB) + (maximum size: {maxSize ?? "3MB"}) @@ -122,6 +129,19 @@ const ImageField = ({ id, src, loading, onChange }) => { > Supported formats: JPG, PNG + {error && ( + + {error} + + )} ) : ( { sx={{ width: "250px", height: "250px", - borderRadius: "50%", overflow: "hidden", backgroundImage: `url(${src})`, backgroundSize: "cover", + ...roundShape, }} > @@ -148,6 +168,8 @@ ImageField.propTypes = { id: PropTypes.string.isRequired, src: PropTypes.string, onChange: PropTypes.func.isRequired, + isRound: PropTypes.bool, + maxSize: PropTypes.string, }; export default ImageField; diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx index 95930b0ec..662ade695 100644 --- a/Client/src/Components/Inputs/Search/index.jsx +++ b/Client/src/Components/Inputs/Search/index.jsx @@ -12,6 +12,7 @@ import SearchIcon from "../../../assets/icons/search.svg?react"; * @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 * @returns {JSX.Element} The rendered Search component */ @@ -54,10 +55,14 @@ const Search = ({ isAdorned = true, error, disabled, + startAdornment, + endAdornment, + onBlur, }) => { const theme = useTheme(); return ( { - handleChange && handleChange(newValue); + handleChange(newValue); }} fullWidth freeSolo @@ -74,6 +79,7 @@ const Search = ({ disableClearable options={options} getOptionLabel={(option) => option[filteredBy]} + isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier renderInput={(params) => ( }), + slotProps={{ + input: { + ...params.InputProps, + ...(isAdorned && { startAdornment: }), + ...(startAdornment && { startAdornment: startAdornment }), + ...(endAdornment && { endAdornment: endAdornment }), + }, }} sx={{ "& fieldset": { @@ -212,6 +222,9 @@ Search.propTypes = { sx: PropTypes.object, error: PropTypes.string, disabled: PropTypes.bool, + startAdornment: PropTypes.object, + endAdornment: PropTypes.object, + onBlur: PropTypes.func, }; export default Search; diff --git a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx index 2b3717085..d6af2a89e 100644 --- a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx +++ b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx @@ -1,9 +1,11 @@ import { Stack, Typography, InputAdornment, IconButton } from "@mui/material"; import { useTheme } from "@mui/material/styles"; -import { useState } from "react"; import PropTypes from "prop-types"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import Visibility from "@mui/icons-material/Visibility"; +import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded"; +import DeleteIcon from "../../../../assets/icons/trash-bin.svg?react"; + export const HttpAdornment = ({ https }) => { const theme = useTheme(); return ( @@ -23,7 +25,7 @@ export const HttpAdornment = ({ https }) => { color={theme.palette.primary.contrastTextSecondary} sx={{ lineHeight: 1, opacity: 0.8 }} > - {https ? "https" : "http"}:// + {https ? "https" : "http"} ); @@ -31,6 +33,7 @@ export const HttpAdornment = ({ https }) => { HttpAdornment.propTypes = { https: PropTypes.bool.isRequired, + prefix: PropTypes.string, }; export const PasswordEndAdornment = ({ fieldType, setFieldType }) => { diff --git a/Client/src/Components/ProgressBars/index.jsx b/Client/src/Components/ProgressBars/index.jsx index abb896844..4f5515d3a 100644 --- a/Client/src/Components/ProgressBars/index.jsx +++ b/Client/src/Components/ProgressBars/index.jsx @@ -93,6 +93,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) => {error ? error : label} @@ -168,7 +169,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) => ProgressUpload.propTypes = { icon: PropTypes.element, // JSX element for the icon (optional) - label: PropTypes.string.isRequired, // Label text for the progress item + label: PropTypes.string, // Label text for the progress item size: PropTypes.string.isRequired, // Size information for the progress item progress: PropTypes.number.isRequired, // Current progress value (0-100) onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button diff --git a/Client/src/Components/ProtectedDistributedUptimeRoute/index.jsx b/Client/src/Components/ProtectedDistributedUptimeRoute/index.jsx new file mode 100644 index 000000000..4a59859fe --- /dev/null +++ b/Client/src/Components/ProtectedDistributedUptimeRoute/index.jsx @@ -0,0 +1,30 @@ +import { Navigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import PropTypes from "prop-types"; + +/** + * @param {Object} props - The props passed to the ProtectedDistributedUptimeRoute component. + * @param {React.ReactNode} props.children - The children to render if the user is authenticated. + * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page. + */ + +const ProtectedDistributedUptimeRoute = ({ children }) => { + const distributedUptimeEnabled = useSelector( + (state) => state.ui.distributedUptimeEnabled + ); + + return distributedUptimeEnabled === true ? ( + children + ) : ( + + ); +}; + +ProtectedDistributedUptimeRoute.propTypes = { + children: PropTypes.node.isRequired, +}; + +export default ProtectedDistributedUptimeRoute; diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 3821306ad..27753dfcb 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -15,12 +15,6 @@ import { Tooltip, Typography, } from "@mui/material"; -import { useLocation, useNavigate } from "react-router"; -import { useTheme } from "@emotion/react"; -import { useDispatch, useSelector } from "react-redux"; -import { clearAuthState } from "../../Features/Auth/authSlice"; -import { toggleSidebar } from "../../Features/UI/uiSlice"; -import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; import ThemeSwitch from "../ThemeSwitch"; import Avatar from "../Avatar"; import LockSvg from "../../assets/icons/lock.svg?react"; @@ -43,16 +37,31 @@ import DotsVertical from "../../assets/icons/dots-vertical.svg?react"; import ChangeLog from "../../assets/icons/changeLog.svg?react"; import Docs from "../../assets/icons/docs.svg?react"; import Folder from "../../assets/icons/folder.svg?react"; +import StatusPages from "../../assets/icons/status-pages.svg?react"; import ChatBubbleOutlineRoundedIcon from "@mui/icons-material/ChatBubbleOutlineRounded"; - +import DistributedUptimeIcon from "../../assets/icons/distributed-uptime.svg?react"; import "./index.css"; +// Utils +import { useLocation, useNavigate } from "react-router"; +import { useTheme } from "@emotion/react"; +import { useDispatch, useSelector } from "react-redux"; +import { clearAuthState } from "../../Features/Auth/authSlice"; +import { toggleSidebar } from "../../Features/UI/uiSlice"; +import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; + const menu = [ { name: "Uptime", path: "uptime", icon: }, { name: "Pagespeed", path: "pagespeed", icon: }, { name: "Infrastructure", path: "infrastructure", icon: }, + { + name: "Distributed uptime", + path: "distributed-uptime", + icon: , + }, { name: "Incidents", path: "incidents", icon: }, - // { name: "Status pages", path: "status", icon: }, + + { name: "Status pages", path: "status", icon: }, { name: "Maintenance", path: "maintenance", icon: }, // { name: "Integrations", path: "integrations", icon: }, { @@ -97,6 +106,7 @@ const PATH_MAP = { monitors: "Dashboard", pagespeed: "Dashboard", infrastructure: "Dashboard", + ["distributed-uptime"]: "Dashboard", account: "Account", settings: "Settings", }; @@ -119,6 +129,9 @@ function Sidebar() { const [anchorEl, setAnchorEl] = useState(null); const [popup, setPopup] = useState(); const { user } = useSelector((state) => state.auth); + const distributedUptimeEnabled = useSelector( + (state) => state.ui.distributedUptimeEnabled + ); const accountMenuItem = menu.find((item) => item.name === "Account"); if (user.role?.includes("demo") && accountMenuItem) { @@ -313,8 +326,11 @@ function Sidebar() { /* overflow: "hidden", */ }} > - {menu.map((item) => - item.path ? ( + {menu.map((item) => { + if (item.path === "distributed-uptime" && distributedUptimeEnabled === false) { + return null; + } + return item.path ? ( /* If item has a path */ navigate(`/${item.path}`)} sx={{ - /* - TODO we do not need this height - minHeight: "37px", */ - p: theme.spacing(5), + height: "37px", gap: theme.spacing(4), borderRadius: theme.shape.borderRadius, + px: theme.spacing(4), }} > {item.icon} @@ -549,8 +563,8 @@ function Sidebar() { - ) - )} + ); + })} diff --git a/Client/src/Components/StatBox/index.jsx b/Client/src/Components/StatBox/index.jsx index 4b1fd2454..58fdb7ebf 100644 --- a/Client/src/Components/StatBox/index.jsx +++ b/Client/src/Components/StatBox/index.jsx @@ -1,4 +1,5 @@ -import { Box, Typography } from "@mui/material"; +import { Stack, Typography } from "@mui/material"; +import Image from "../Image"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; @@ -29,7 +30,15 @@ import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; * @returns {React.ReactElement} A styled box containing the statistic */ -const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) => { +const StatBox = ({ + img, + alt, + heading, + subHeading, + gradient = false, + status = "", + sx, +}) => { const theme = useTheme(); const { statusToTheme } = useUtils(); const themeColor = statusToTheme[status]; @@ -70,7 +79,8 @@ const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) => }; return ( - ...sx, }} > - {heading} - {subHeading} - + {img && ( + {alt} + )} + + {heading} + {subHeading} + + ); }; diff --git a/Client/src/Features/UI/uiSlice.js b/Client/src/Features/UI/uiSlice.js index 872691bbf..4565c6c5d 100644 --- a/Client/src/Features/UI/uiSlice.js +++ b/Client/src/Features/UI/uiSlice.js @@ -1,6 +1,8 @@ import { createSlice } from "@reduxjs/toolkit"; -const initialMode = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? "dark" : "light"; +const initialMode = window?.matchMedia?.("(prefers-color-scheme: dark)")?.matches + ? "dark" + : "light"; // Initial state for UI settings. // Add more settings as needed (e.g., theme preferences, user settings) @@ -20,12 +22,16 @@ const initialState = { mode: initialMode, greeting: { index: 0, lastUpdate: null }, timezone: "America/Toronto", + distributedUptimeEnabled: false, }; const uiSlice = createSlice({ name: "ui", initialState, reducers: { + setDistributedUptimeEnabled: (state, action) => { + state.distributedUptimeEnabled = action.payload; + }, setRowsPerPage: (state, action) => { const { table, value } = action.payload; if (state[table]) { @@ -49,5 +55,11 @@ const uiSlice = createSlice({ }); export default uiSlice.reducer; -export const { setRowsPerPage, toggleSidebar, setMode, setGreeting, setTimezone } = - uiSlice.actions; +export const { + setRowsPerPage, + toggleSidebar, + setMode, + setGreeting, + setTimezone, + setDistributedUptimeEnabled, +} = uiSlice.actions; diff --git a/Client/src/Hooks/useMonitorUtils.js b/Client/src/Hooks/useMonitorUtils.js new file mode 100644 index 000000000..1e235af68 --- /dev/null +++ b/Client/src/Hooks/useMonitorUtils.js @@ -0,0 +1,45 @@ +import { useCallback } from "react"; + +const useMonitorUtils = () => { + const getMonitorWithPercentage = useCallback((monitor, theme) => { + let uptimePercentage = ""; + let percentageColor = ""; + + if (monitor.uptimePercentage !== undefined) { + uptimePercentage = + monitor.uptimePercentage === 0 + ? "0" + : (monitor.uptimePercentage * 100).toFixed(2); + + percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.error.main + : monitor.uptimePercentage < 0.5 + ? theme.palette.warning.main + : monitor.uptimePercentage < 0.75 + ? theme.palette.success.main + : theme.palette.success.main; + } + + return { + id: monitor._id, + name: monitor.name, + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor, + monitor: monitor, + }; + }, []); + + const determineState = useCallback((monitor) => { + if (typeof monitor === "undefined") return "pending"; + if (monitor.isActive === false) return "paused"; + if (monitor?.status === undefined) return "pending"; + return monitor?.status == true ? "up" : "down"; + }, []); + + return { getMonitorWithPercentage, determineState }; +}; + +export { useMonitorUtils }; diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index 9fd38b5ef..00d805f34 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -73,11 +73,6 @@ const Account = ({ open = "profile" }) => { {tabList.map((label, index) => ( { onKeyDown={handleKeyDown} onFocus={() => handleFocus(label.toLowerCase())} tabIndex={index} - sx={{ - fontSize: 13, - color: theme.palette.tertiary.contrastText, - backgroundColor: theme.palette.tertiary.main, - textTransform: "none", - minWidth: "fit-content", - paddingY: theme.spacing(6), - fontWeight: 400, - borderBottom: "2px solid transparent", - borderRight: `1px solid ${theme.palette.primary.lowContrast}`, - "&:first-child": { borderTopLeftRadius: "8px" }, - "&:last-child": { borderTopRightRadius: "8px", borderRight: 0 }, - "&:focus-visible": { - color: theme.palette.primary.contrastText, - borderColor: theme.palette.tertiary.contrastText, - borderRightColor: theme.palette.primary.lowContrast, - }, - "&.Mui-selected": { - backgroundColor: theme.palette.secondary.main, - color: theme.palette.secondary.contrastText, - borderColor: theme.palette.secondary.contrastText, - borderRightColor: theme.palette.primary.lowContrast, - }, - "&:hover": { - borderColor: theme.palette.primary.lowContrast, - }, - }} /> ))} diff --git a/Client/src/Pages/DistributedUptime/Create/index.jsx b/Client/src/Pages/DistributedUptime/Create/index.jsx new file mode 100644 index 000000000..2df0a4482 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Create/index.jsx @@ -0,0 +1,307 @@ +// Components +import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; +import LoadingButton from "@mui/lab/LoadingButton"; + +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import ConfigBox from "../../../Components/ConfigBox"; +import TextInput from "../../../Components/Inputs/TextInput"; +import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; +import Radio from "../../../Components/Inputs/Radio"; +import Checkbox from "../../../Components/Inputs/Checkbox"; +import Select from "../../../Components/Inputs/Select"; +import { createToast } from "../../../Utils/toastUtils"; + +// Utility +import { useTheme } from "@emotion/react"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useSelector, useDispatch } from "react-redux"; +import { monitorValidation } from "../../../Validation/validation"; +import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; + +// Constants +const BREADCRUMBS = [ + { name: `distributed uptime`, path: "/distributed-uptime" }, + { name: "create", path: `/distributed-uptime/create` }, +]; +const MS_PER_MINUTE = 60000; +const SELECT_VALUES = [ + { _id: 1, name: "1 minute" }, + { _id: 2, name: "2 minutes" }, + { _id: 3, name: "3 minutes" }, + { _id: 4, name: "4 minutes" }, + { _id: 5, name: "5 minutes" }, +]; + +const CreateDistributedUptime = () => { + // Redux state + const { user, authToken } = useSelector((state) => state.auth); + const isLoading = useSelector((state) => state.uptimeMonitors.isLoading); + + // Local state + const [https, setHttps] = useState(true); + const [notifications, setNotifications] = useState([]); + const [monitor, setMonitor] = useState({ + type: "distributed_http", + name: "", + url: "", + interval: 1, + }); + const [errors, setErrors] = useState({}); + + //utils + const theme = useTheme(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + // Handlers + const handleCreateMonitor = async (event) => { + const monitorToSubmit = { ...monitor }; + + // Prepend protocol to url + monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url; + + const { error } = monitorValidation.validate(monitorToSubmit, { + abortEarly: false, + }); + + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + createToast({ body: "Please check the form for errors." }); + return; + } + + // Append needed fields + monitorToSubmit.description = monitor.name; + monitorToSubmit.interval = monitor.interval * MS_PER_MINUTE; + monitorToSubmit.teamId = user.teamId; + monitorToSubmit.userId = user._id; + monitorToSubmit.notifications = notifications; + + const action = await dispatch( + createUptimeMonitor({ authToken, monitor: monitorToSubmit }) + ); + if (action.meta.requestStatus === "fulfilled") { + createToast({ body: "Monitor created successfully!" }); + navigate("/distributed-uptime"); + } else { + createToast({ body: "Failed to create monitor." }); + } + }; + + const handleChange = (event) => { + const { name, value } = event.target; + setMonitor({ + ...monitor, + [name]: value, + }); + const { error } = monitorValidation.validate( + { [name]: value }, + { abortEarly: false } + ); + console.log(name); + setErrors((prev) => ({ + ...prev, + ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), + })); + }; + + const handleNotifications = (event, type) => { + const { value } = event.target; + let currentNotifications = [...notifications]; + const notificationAlreadyExists = notifications.some((notification) => { + if (notification.type === type && notification.address === value) { + return true; + } + return false; + }); + if (notificationAlreadyExists) { + currentNotifications = currentNotifications.filter((notification) => { + if (notification.type === type && notification.address === value) { + return false; + } + return true; + }); + } else { + currentNotifications.push({ type, address: value }); + } + setNotifications(currentNotifications); + }; + + return ( + + + console.log("submit")} + > + + + Create your{" "} + + + monitor + + + + + General settings + + Here you can select the URL of the host, together with the type of monitor. + + + + } + label="URL to monitor" + https={https} + placeholder={"www.google.com"} + value={monitor.url} + name="url" + onChange={handleChange} + error={errors["url"] ? true : false} + helperText={errors["url"]} + /> + + + + + + Checks to perform + + You can always add or remove checks after adding your site. + + + + + + {monitor.type === "http" || monitor.type === "distributed_http" ? ( + + + + + ) : ( + "" + )} + + + {errors["type"] ? ( + + + {errors["type"]} + + + ) : ( + "" + )} + + + + + Incident notifications + + When there is an incident, notify users. + + + + notification.type === "email" + )} + value={user?.email} + onChange={(event) => handleNotifications(event, "email")} + /> + + + + + Advanced settings + + + { @@ -229,6 +247,27 @@ const Settings = () => { > + {isAdmin && ( + + + Distributed uptime + + Enable/disable distributed uptime monitoring. + + + + { + dispatch(setDistributedUptimeEnabled(e.target.checked)); + }} + /> + {distributedUptimeEnabled === true ? "Enabled" : "Disabled"} + + + )} {isAdmin && ( diff --git a/Client/src/Pages/Status/index.css b/Client/src/Pages/Status/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/Client/src/Pages/Status/index.jsx b/Client/src/Pages/Status/index.jsx deleted file mode 100644 index 7ba2c0a8d..000000000 --- a/Client/src/Pages/Status/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Box } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import Fallback from "../../Components/Fallback"; - -const Status = () => { - const theme = useTheme(); - - return ( - [class*="fallback__"])': { - position: "relative", - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - borderStyle: "dashed", - backgroundColor: theme.palette.primary.main, - overflow: "hidden", - }, - }} - > - - - ); -}; - -export default Status; diff --git a/Client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/Client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx new file mode 100644 index 000000000..5484202d5 --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx @@ -0,0 +1,106 @@ +// Components +import { Stack, Typography } from "@mui/material"; +import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded"; +import DeleteIcon from "@mui/icons-material/Delete"; + +// 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 new file mode 100644 index 000000000..d85225551 --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/Progress/index.jsx @@ -0,0 +1,37 @@ +import { Button, Box } from "@mui/material"; +import ProgressUpload from "../../../../../Components/ProgressBars"; +import ImageIcon from "@mui/icons-material/Image"; + +import { formatBytes } from "../../../../../Utils/fileUtils"; +const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => { + 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 new file mode 100644 index 000000000..482c490fb --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx @@ -0,0 +1,14 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx new file mode 100644 index 000000000..8eda4fe0a --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx @@ -0,0 +1,104 @@ +// Components +import { Stack, Typography, Button } from "@mui/material"; +import { TabPanel } from "@mui/lab"; +import ConfigBox from "../../../../../Components/ConfigBox"; +import MonitorList from "../MonitorList"; +import Search from "../../../../../Components/Inputs/Search"; +import Checkbox from "../../../../../Components/Inputs/Checkbox"; +// Utils +import { useState } from "react"; +import { useTheme } from "@emotion/react"; +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(); + + return ( + + + + + Status page servers + + You can add any number of servers that you monitor to your status page. You + can also reorder them for the best viewing experience. + + + + + + + + {errors["monitors"]} + + + + {" "} + + + Features + Show more details on the status page + + + + + + + + + + ); +}; + +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 new file mode 100644 index 000000000..e4fd56ebb --- /dev/null +++ b/Client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -0,0 +1,140 @@ +// Components +import { Stack, Typography } from "@mui/material"; +import { TabPanel } from "@mui/lab"; +import ConfigBox from "../../../../../Components/ConfigBox"; +import Checkbox from "../../../../../Components/Inputs/Checkbox"; +import TextInput from "../../../../../Components/Inputs/TextInput"; +import Select from "../../../../../Components/Inputs/Select"; +import ImageField from "../../../../../Components/Inputs/Image"; +import ColorPicker from "../../../../../Components/Inputs/ColorPicker"; +import Progress from "../Progress"; + +// Utils +import { useTheme } from "@emotion/react"; +import timezones from "../../../../../Utils/timezones.json"; +import PropTypes from "prop-types"; + +const TabSettings = ({ + tabValue, + form, + handleFormChange, + handleImageChange, + progress, + removeLogo, + errors, +}) => { + // Utils + const theme = useTheme(); + + return ( + + + + + Access + + If your status page is ready, you can mark it as published. + + + + + + + + + Basic Information + + Define company name and the subdomain that your status page points to. + + + + + + + + + + Timezone + + Select the timezone that your status page will be displayed in. + + + +