diff --git a/Client/package-lock.json b/Client/package-lock.json
index 4cc59f162..51a0c8964 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -12,7 +12,7 @@
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
"@hello-pangea/dnd": "^17.0.0",
- "@mui/icons-material": "6.4.3",
+ "@mui/icons-material": "6.4.3",
"@mui/lab": "6.0.0-beta.26",
"@mui/material": "6.4.3",
"@mui/x-charts": "^7.5.1",
@@ -21,9 +21,11 @@
"@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",
@@ -1101,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",
@@ -2405,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",
@@ -2446,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",
@@ -3335,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",
@@ -3928,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",
@@ -4064,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",
@@ -4089,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",
@@ -4107,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",
@@ -4142,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",
@@ -4293,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",
@@ -4363,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",
@@ -4865,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",
@@ -4902,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",
@@ -4912,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",
@@ -4991,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",
@@ -5040,6 +5331,15 @@
"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",
@@ -5067,6 +5367,12 @@
}
}
},
+ "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",
@@ -5348,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",
@@ -5404,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",
@@ -5447,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",
@@ -5484,6 +5815,12 @@
],
"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",
@@ -5802,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",
@@ -5897,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",
@@ -6244,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",
@@ -6288,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",
@@ -6569,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 b37c1a723..6b006e24e 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -18,16 +18,17 @@
"@mui/icons-material": "6.4.3",
"@mui/lab": "6.0.0-beta.26",
"@mui/material": "6.4.3",
-
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.25.0",
"@mui/x-date-pickers": "7.25.0",
"@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",
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/Image/index.jsx b/Client/src/Components/Image/index.jsx
index 778675f92..9304a4385 100644
--- a/Client/src/Components/Image/index.jsx
+++ b/Client/src/Components/Image/index.jsx
@@ -14,6 +14,7 @@ const Image = ({
maxWidth = "auto",
maxHeight = "auto",
base64,
+ sx,
}) => {
if (shouldRender === false) {
return null;
@@ -36,6 +37,7 @@ const Image = ({
maxHeight={maxHeight}
width={width}
height={height}
+ sx={{ ...sx }}
/>
);
};
@@ -49,6 +51,7 @@ Image.propTypes = {
maxWidth: PropTypes.string,
maxHeight: PropTypes.string,
base64: PropTypes.string,
+ sx: PropTypes.object,
};
export default Image;
diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx
index c4c574c8f..03c6b54ba 100644
--- a/Client/src/Components/Sidebar/index.jsx
+++ b/Client/src/Components/Sidebar/index.jsx
@@ -45,6 +45,7 @@ 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 Groups from "../../assets/icons/groups.svg?react";
import "./index.css";
@@ -52,6 +53,7 @@ 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: },
@@ -99,6 +101,7 @@ const PATH_MAP = {
monitors: "Dashboard",
pagespeed: "Dashboard",
infrastructure: "Dashboard",
+ ["distributed-uptime"]: "Dashboard",
account: "Account",
settings: "Settings",
};
@@ -337,15 +340,13 @@ function Sidebar() {
disableInteractive
>
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}
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 && (
+
+ )}
+
+ {heading}
+ {subHeading}
+
+
);
};
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
+
+
+
+
+
+
+ handleCreateMonitor()}
+ disabled={!Object.values(errors).every((value) => value === undefined)}
+ loading={isLoading}
+ >
+ Create monitor
+
+
+
+
+ );
+};
+
+export default CreateDistributedUptime;
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
new file mode 100644
index 000000000..22ef5596d
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
@@ -0,0 +1,71 @@
+import { Stack, Typography, List, ListItem } from "@mui/material";
+import { useTheme } from "@emotion/react";
+import PulseDot from "../../../../../Components/Animated/PulseDot";
+import "/node_modules/flag-icons/css/flag-icons.min.css";
+
+const BASE_BOX_PADDING_VERTICAL = 16;
+const BASE_BOX_PADDING_HORIZONTAL = 8;
+
+const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
+ const theme = useTheme();
+ const statusColor = {
+ up: theme.palette.success.main,
+ down: theme.palette.error.main,
+ };
+
+ return (
+
+
+
+
+
+ {connectionStatus === "up" ? "Connected" : "No connection"}
+
+
+
+ {data.slice(Math.max(data.length - 5, 0)).map((dataPoint) => {
+ const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
+ const flag = countryCode ? `fi fi-${countryCode}` : null;
+ return (
+
+
+
+ {flag && }
+ {dataPoint?.city || "Unknown"}
+
+ {`Response time: ${Math.floor(dataPoint?.responseTime ?? 0)} ms`}
+ {`UPT burned: ${dataPoint.uptBurnt}`}
+ {`${dataPoint?.device?.manufacturer} ${dataPoint?.device?.model}`}
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default DeviceTicker;
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json
new file mode 100644
index 000000000..d8ee81d5b
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json
@@ -0,0 +1,169 @@
+{
+ "id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7",
+ "name": "MapLibre",
+ "zoom": 0.861983335785597,
+ "pitch": 0,
+ "center": [17.6543171043124, 32.9541203267468],
+ "glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
+ "layers": [
+ {
+ "id": "background",
+ "type": "background",
+ "paint": {
+ "background-color": "#121217"
+ },
+ "filter": ["all"],
+ "layout": {
+ "visibility": "visible"
+ },
+ "maxzoom": 24
+ },
+ {
+ "id": "coastline",
+ "type": "line",
+ "paint": {
+ "line-blur": 0.5,
+ "line-color": "#000000",
+ "line-width": {
+ "stops": [
+ [0, 2],
+ [6, 6],
+ [14, 9],
+ [22, 18]
+ ]
+ }
+ },
+ "filter": ["all"],
+ "layout": {
+ "line-cap": "round",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "source": "maplibre",
+ "maxzoom": 24,
+ "minzoom": 0,
+ "source-layer": "countries"
+ },
+ {
+ "id": "countries-fill",
+ "type": "fill",
+ "paint": {
+ "fill-color": "#292929"
+ },
+ "filter": ["all"],
+ "layout": {
+ "visibility": "visible"
+ },
+ "source": "maplibre",
+ "maxzoom": 24,
+ "source-layer": "countries"
+ },
+ {
+ "id": "countries-boundary",
+ "type": "line",
+ "paint": {
+ "line-color": "#484848",
+ "line-width": {
+ "stops": [
+ [1, 1],
+ [6, 2],
+ [14, 6],
+ [22, 12]
+ ]
+ },
+ "line-opacity": {
+ "stops": [
+ [3, 0.5],
+ [6, 1]
+ ]
+ }
+ },
+ "layout": {
+ "line-cap": "round",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "source": "maplibre",
+ "maxzoom": 24,
+ "source-layer": "countries"
+ },
+ {
+ "id": "countries-label",
+ "type": "symbol",
+ "paint": {
+ "text-color": "rgba(8, 37, 77, 1)",
+ "text-halo-blur": {
+ "stops": [
+ [2, 0.2],
+ [6, 0]
+ ]
+ },
+ "text-halo-color": "rgba(255, 255, 255, 1)",
+ "text-halo-width": {
+ "stops": [
+ [2, 1],
+ [6, 1.6]
+ ]
+ }
+ },
+ "filter": ["all"],
+ "layout": {
+ "text-font": ["Open Sans Semibold"],
+ "text-size": {
+ "stops": [
+ [2, 10],
+ [4, 12],
+ [6, 16]
+ ]
+ },
+ "text-field": {
+ "stops": [
+ [2, "{ABBREV}"],
+ [4, "{NAME}"]
+ ]
+ },
+ "visibility": "visible",
+ "text-max-width": 10,
+ "text-transform": {
+ "stops": [
+ [0, "uppercase"],
+ [2, "none"]
+ ]
+ }
+ },
+ "source": "maplibre",
+ "maxzoom": 24,
+ "minzoom": 2,
+ "source-layer": "centroids"
+ },
+ {
+ "id": "data-dots",
+ "type": "circle",
+ "source": "data-dots",
+ "paint": {
+ "circle-radius": 3,
+ "circle-color": ["get", "color"],
+ "circle-opacity": 0.5
+ }
+ }
+ ],
+ "bearing": 0,
+ "sources": {
+ "maplibre": {
+ "url": "https://demotiles.maplibre.org/tiles/tiles.json",
+ "type": "vector"
+ },
+ "data-dots": {
+ "type": "geojson",
+ "data": {
+ "type": "FeatureCollection",
+ "features": []
+ }
+ }
+ },
+ "version": 8,
+ "metadata": {
+ "maptiler:copyright": "This style was generated on MapTiler Cloud. Usage is governed by the license terms in https://github.com/maplibre/demotiles/blob/gh-pages/LICENSE",
+ "openmaptiles:version": "3.x"
+ }
+}
\ No newline at end of file
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
new file mode 100644
index 000000000..e7c0c961a
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
@@ -0,0 +1,88 @@
+import "maplibre-gl/dist/maplibre-gl.css";
+import PropTypes from "prop-types";
+import { useRef, useState, useEffect } from "react";
+import { useTheme } from "@mui/material/styles";
+import style from "./DistributedUptimeMapStyle.json";
+import maplibregl from "maplibre-gl";
+const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => {
+ const mapContainer = useRef(null);
+ const map = useRef(null);
+ const theme = useTheme();
+ const [mapLoaded, setMapLoaded] = useState(false);
+
+ const colorLookup = (avgResponseTime) => {
+ if (avgResponseTime <= 150) {
+ return "#00FF00"; // Green
+ } else if (avgResponseTime <= 250) {
+ return "#FFFF00"; // Yellow
+ } else {
+ return "#FF0000"; // Red
+ }
+ };
+
+ useEffect(() => {
+ if (mapContainer.current && !map.current) {
+ map.current = new maplibregl.Map({
+ container: mapContainer.current,
+ style,
+ center: [0, 20],
+ zoom: 0.8,
+ });
+ }
+ map.current.on("load", () => {
+ setMapLoaded(true);
+ });
+
+ return () => {
+ if (map.current) {
+ map.current.remove();
+ map.current = null;
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (map.current && checks?.length > 0) {
+ // Convert dots to GeoJSON
+ const geojson = {
+ type: "FeatureCollection",
+ features: checks.map((check) => {
+ return {
+ type: "Feature",
+ geometry: {
+ type: "Point",
+ coordinates: [check.lng, check.lat],
+ },
+ properties: {
+ color: theme.palette.accent.main,
+ // color: colorLookup(check.avgResponseTime) || "blue", // Default to blue if no color specified
+ },
+ };
+ }),
+ };
+
+ // Update the source with new dots
+ const source = map.current.getSource("data-dots");
+ if (source) {
+ source.setData(geojson);
+ }
+ }
+ }, [checks, theme, mapLoaded]);
+ return (
+
+ );
+};
+
+DistributedUptimeMap.propTypes = {
+ checks: PropTypes.array,
+ width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+};
+
+export default DistributedUptimeMap;
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx
new file mode 100644
index 000000000..56d2f9618
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx
@@ -0,0 +1,214 @@
+import PropTypes from "prop-types";
+import {
+ AreaChart,
+ Area,
+ XAxis,
+ Tooltip,
+ CartesianGrid,
+ ResponsiveContainer,
+ Text,
+} from "recharts";
+import { Box, Stack, Typography } from "@mui/material";
+import { useTheme } from "@emotion/react";
+import { useState } from "react";
+import { useSelector } from "react-redux";
+import { formatDateWithTz } from "../../../../../Utils/timeUtils";
+
+const CustomToolTip = ({ active, payload, label }) => {
+ const uiTimezone = useSelector((state) => state.ui.timezone);
+ const theme = useTheme();
+ if (active && payload && payload.length) {
+ const responseTime = payload[0]?.payload?.originalAvgResponseTime
+ ? payload[0]?.payload?.originalAvgResponseTime
+ : (payload[0]?.payload?.avgResponseTime ?? 0);
+ return (
+
+
+ {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
+
+
+
+
+
+ Response Time
+ {" "}
+
+ {Math.floor(responseTime)}
+
+ {" "}
+ ms
+
+
+
+
+ {/* Display original value */}
+
+ );
+ }
+ return null;
+};
+
+CustomToolTip.propTypes = {
+ active: PropTypes.bool,
+ payload: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.number,
+ payload: PropTypes.shape({
+ _id: PropTypes.string,
+ avgResponseTime: PropTypes.number,
+ originalAvgResponseTime: PropTypes.number,
+ }),
+ })
+ ),
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+};
+const CustomTick = ({ x, y, payload, index }) => {
+ const theme = useTheme();
+
+ const uiTimezone = useSelector((state) => state.ui.timezone);
+ // Render nothing for the first tick
+ if (index === 0) return null;
+ return (
+
+ {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
+
+ );
+};
+
+CustomTick.propTypes = {
+ x: PropTypes.number,
+ y: PropTypes.number,
+ payload: PropTypes.object,
+ index: PropTypes.number,
+};
+
+const DistributedUptimeResponseChart = ({ checks }) => {
+ const theme = useTheme();
+ const [isHovered, setIsHovered] = useState(false);
+
+ if (checks.length === 0) return null;
+ return (
+
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
+
+
+
+
+ }
+ minTickGap={0}
+ axisLine={false}
+ tickLine={false}
+ height={20}
+ />
+ }
+ wrapperStyle={{ pointerEvents: "none" }}
+ />
+
+
+
+ );
+};
+
+DistributedUptimeResponseChart.propTypes = {
+ checks: PropTypes.array,
+};
+
+export default DistributedUptimeResponseChart;
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx
new file mode 100644
index 000000000..8032b6527
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx
@@ -0,0 +1,31 @@
+import { Stack, Typography, Box } from "@mui/material";
+import SolanaLogo from "../../../../../assets/icons/solana_logo.svg?react";
+import { useTheme } from "@mui/material/styles";
+const Footer = () => {
+ const theme = useTheme();
+ return (
+
+ Made with ❤️ by UpRock & Bluewave Labs
+
+ Built on
+
+ Solana
+
+
+ );
+};
+
+export default Footer;
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx
new file mode 100644
index 000000000..a095b5feb
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx
@@ -0,0 +1,15 @@
+import { useState, useEffect } from "react";
+const LastUpdate = ({ suffix, lastUpdateTime, trigger }) => {
+ const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
+
+ useEffect(() => {
+ setElapsedMs(lastUpdateTime);
+ const timer = setInterval(() => {
+ setElapsedMs((prev) => prev + 1000);
+ }, 1000);
+ return () => clearInterval(timer);
+ }, [lastUpdateTime, trigger]);
+
+ return `${Math.floor(elapsedMs / 1000)} ${suffix}`;
+};
+export default LastUpdate;
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx
new file mode 100644
index 000000000..03949a9f7
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx
@@ -0,0 +1,30 @@
+import { LinearProgress } from "@mui/material";
+import { useState, useEffect } from "react";
+
+const NextExpectedCheck = ({ lastUpdateTime, interval, trigger }) => {
+ const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
+
+ useEffect(() => {
+ setElapsedMs(lastUpdateTime);
+ const timer = setInterval(() => {
+ setElapsedMs((prev) => {
+ const newElapsedMs = prev + 100;
+ return newElapsedMs;
+ });
+ }, 100);
+ return () => clearInterval(timer);
+ }, [interval, trigger]);
+
+ return (
+
+ );
+};
+
+export default NextExpectedCheck;
diff --git a/Client/src/Pages/DistributedUptime/Details/index.jsx b/Client/src/Pages/DistributedUptime/Details/index.jsx
new file mode 100644
index 000000000..6c921063f
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/index.jsx
@@ -0,0 +1,325 @@
+//Components
+import DistributedUptimeMap from "./Components/DistributedUptimeMap";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import { Stack, Typography, Box, Button, ButtonGroup } from "@mui/material";
+import ChartBox from "../../../Components/Charts/ChartBox";
+import StatBox from "../../../Components/StatBox";
+import ResponseTimeIcon from "../../../assets/icons/response-time-icon.svg?react";
+import DeviceTicker from "./Components/DeviceTicker";
+import DistributedUptimeResponseChart from "./Components/DistributedUptimeResponseChart";
+import UptLogo from "../../../assets/icons/upt_logo.png";
+import LastUpdate from "./Components/LastUpdate";
+import NextExpectedCheck from "./Components/NextExpectedCheck";
+import Footer from "./Components/Footer";
+//Utils
+import { networkService } from "../../../main";
+import { useSelector } from "react-redux";
+import { useTheme } from "@mui/material/styles";
+import { useEffect, useState, useCallback, useRef } from "react";
+import { useParams } from "react-router-dom";
+
+//Constants
+const BASE_BOX_PADDING_VERTICAL = 8;
+const BASE_BOX_PADDING_HORIZONTAL = 8;
+const MAX_RETRIES = 10;
+const RETRY_DELAY = 5000;
+
+function getRandomDevice() {
+ const manufacturers = {
+ Apple: ["iPhone 15 Pro Max", "iPhone 15", "iPhone 14 Pro", "iPhone 14", "iPhone 13"],
+ Samsung: [
+ "Galaxy S23 Ultra",
+ "Galaxy S23+",
+ "Galaxy S23",
+ "Galaxy Z Fold5",
+ "Galaxy Z Flip5",
+ ],
+ Google: ["Pixel 8 Pro", "Pixel 8", "Pixel 7a", "Pixel 7", "Pixel 6a"],
+ OnePlus: [
+ "OnePlus 11",
+ "OnePlus 10T",
+ "OnePlus Nord 3",
+ "OnePlus 10 Pro",
+ "OnePlus Nord 2T",
+ ],
+ Xiaomi: ["13 Pro", "13", "Redmi Note 12", "POCO F5", "Redmi 12"],
+ Huawei: ["P60 Pro", "Mate X3", "Nova 11", "P50 Pro", "Mate 50"],
+ Sony: ["Xperia 1 V", "Xperia 5 V", "Xperia 10 V", "Xperia Pro-I", "Xperia 1 IV"],
+ Motorola: ["Edge 40 Pro", "Edge 40", "G84", "G54", "Razr 40 Ultra"],
+ ASUS: [
+ "ROG Phone 7",
+ "Zenfone 10",
+ "ROG Phone 6",
+ "Zenfone 9",
+ "ROG Phone 7 Ultimate",
+ ],
+ };
+
+ const manufacturerNames = Object.keys(manufacturers);
+ const randomManufacturer =
+ manufacturerNames[Math.floor(Math.random() * manufacturerNames.length)];
+
+ const models = manufacturers[randomManufacturer];
+ const randomModel = models[Math.floor(Math.random() * models.length)];
+
+ return {
+ manufacturer: randomManufacturer,
+ model: randomModel,
+ };
+}
+
+// export const StatBox = ({ heading, value, img, altTxt }) => {
+// const theme = useTheme();
+// return (
+//
+// {img && (
+//
+// )}
+//
+// {heading}
+// {value}
+//
+//
+// );
+// };
+
+const DistributedUptimeDetails = () => {
+ // Redux State
+ const { authToken } = useSelector((state) => state.auth);
+ const { mode } = useSelector((state) => state.ui);
+
+ // Local State
+ // const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
+ // const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
+ const [retryCount, setRetryCount] = useState(0);
+ const [connectionStatus, setConnectionStatus] = useState("down");
+ const [lastUpdateTrigger, setLastUpdateTrigger] = useState(Date.now());
+ const [dateRange, setDateRange] = useState("day");
+ const [monitor, setMonitor] = useState(null);
+ const [devices, setDevices] = useState([]);
+
+ // Refs
+ const prevDateRangeRef = useRef(dateRange);
+
+ // Utils
+ const theme = useTheme();
+ const { monitorId } = useParams();
+
+ // Constants
+ const BREADCRUMBS = [
+ { name: "Distributed Uptime", path: "/distributed-uptime" },
+ { name: "Details", path: `/distributed-uptime/${monitorId}` },
+ ];
+
+ useEffect(() => {
+ const hasDateRangeChanged = prevDateRangeRef.current !== dateRange;
+ prevDateRangeRef.current = dateRange; // Update the ref to the current dateRange
+
+ if (!hasDateRangeChanged) {
+ setDevices(Array.from({ length: 5 }, getRandomDevice));
+ }
+ }, [dateRange]);
+
+ const connectToService = useCallback(() => {
+ return networkService.subscribeToDistributedUptimeDetails({
+ authToken,
+ monitorId,
+ dateRange: dateRange,
+ normalize: true,
+ onUpdate: (data) => {
+ setLastUpdateTrigger(Date.now());
+ const latestChecksWithDevice = data?.monitor?.latestChecks.map((check, idx) => {
+ check.device = devices[idx];
+ return check;
+ });
+ const monitorWithDevice = {
+ ...data.monitor,
+ latestChecks: latestChecksWithDevice,
+ };
+ setMonitor(monitorWithDevice);
+ },
+ onOpen: () => {
+ setConnectionStatus("up");
+ setRetryCount(0); // Reset retry count on successful connection
+ },
+ onError: () => {
+ setConnectionStatus("down");
+ console.log("Error, attempting reconnect...");
+
+ if (retryCount < MAX_RETRIES) {
+ setTimeout(() => {
+ setRetryCount((prev) => prev + 1);
+ connectToService();
+ }, RETRY_DELAY);
+ } else {
+ console.log("Max retries reached");
+ }
+ },
+ });
+ }, [authToken, monitorId, dateRange, retryCount, devices]);
+
+ useEffect(() => {
+ const devices = Array.from({ length: 5 }, getRandomDevice);
+ const cleanup = connectToService(devices);
+ return cleanup;
+ }, [connectToService]);
+
+ return (
+ monitor && (
+
+
+ {monitor?.url !== "https://jup.ag/" &&
+ monitor?.url !== "https://explorer.solana.com/" && (
+
+
+ {monitor.name}
+
+
+ )}
+
+
+ Distributed Uptime Monitoring powered by DePIN
+
+
+
+
+
+
+ }
+ />
+
+ }
+ />
+
+
+
+
+
+
+
+
+ Showing statistics for past{" "}
+ {dateRange === "day"
+ ? "24 hours"
+ : dateRange === "week"
+ ? "7 days"
+ : "30 days"}
+ .
+
+
+
+
+
+
+
+ }
+ header="Response Times"
+ sx={{ padding: 0 }}
+ >
+
+
+
+
+
+
+
+
+ )
+ );
+};
+
+export default DistributedUptimeDetails;
diff --git a/Client/src/Pages/DistributedUptime/Monitors/index.jsx b/Client/src/Pages/DistributedUptime/Monitors/index.jsx
new file mode 100644
index 000000000..0d1288aa9
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Monitors/index.jsx
@@ -0,0 +1,187 @@
+// Components
+import { Stack, Box, Button } from "@mui/material";
+import DataTable from "../../../Components/Table";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import Host from "../../Uptime/Monitors/Components/Host";
+import BarChart from "../../../Components/Charts/BarChart";
+import ActionsMenu from "../../Uptime/Monitors/Components/ActionsMenu";
+import { StatusLabel } from "../../../Components/Label";
+// Utils
+import { networkService } from "../../../main";
+import { useTheme } from "@mui/material/styles";
+import { useNavigate } from "react-router-dom";
+import useUtils from "../../Uptime/Monitors/Hooks/useUtils";
+import { useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+
+// Constants
+const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" }];
+const TYPE_MAP = {
+ distributed_http: "Distributed HTTP",
+};
+
+const DistributedUptimeMonitors = () => {
+ // Redux state
+ const { authToken, user } = useSelector((state) => state.auth);
+ // Local state
+ const [monitors, setMonitors] = useState([]);
+ const [filteredMonitors, setFilteredMonitors] = useState([]);
+ const [monitorsSummary, setMonitorsSummary] = useState({});
+ // Utils
+
+ const { determineState } = useUtils();
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const headers = [
+ {
+ id: "name",
+ content: Host,
+ render: (row) => (
+
+ ),
+ },
+ {
+ id: "status",
+ content: Status,
+ render: (row) => {
+ const status = determineState(row?.monitor);
+ return (
+
+ );
+ },
+ },
+ {
+ id: "responseTime",
+ content: "Response Time",
+ render: (row) => ,
+ },
+ {
+ id: "type",
+ content: "Type",
+ render: (row) => {TYPE_MAP[row.monitor.type]},
+ },
+ {
+ id: "actions",
+ content: "Actions",
+ render: (row) => (
+
+ ),
+ },
+ ];
+
+ const getMonitorWithPercentage = (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,
+ };
+ };
+
+ useEffect(() => {
+ const cleanup = networkService.subscribeToDistributedUptimeMonitors({
+ authToken: authToken,
+ teamId: user.teamId,
+ limit: 25,
+ types: ["distributed_http"],
+ page: 0,
+ rowsPerPage: 10,
+ filter: null,
+ field: null,
+ order: null,
+ onUpdate: (data) => {
+ const res = data.monitors;
+ const { monitors, filteredMonitors, summary } = res;
+ const mappedMonitors = filteredMonitors.map((monitor) =>
+ getMonitorWithPercentage(monitor, theme)
+ );
+ setMonitors(monitors);
+ setFilteredMonitors(mappedMonitors);
+ setMonitorsSummary(summary);
+ },
+ });
+
+ return cleanup;
+ }, [user.teamId, authToken, theme]);
+
+ return (
+
+
+
+
+
+ {monitors.length > 0 && (
+ {
+ navigate(`/distributed-uptime/${row.id}`);
+ },
+ }}
+ />
+ )}
+
+ );
+};
+
+export default DistributedUptimeMonitors;
diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx
index 12fad43ae..8883a3212 100644
--- a/Client/src/Routes/index.jsx
+++ b/Client/src/Routes/index.jsx
@@ -27,9 +27,15 @@ import Infrastructure from "../Pages/Infrastructure/Monitors";
import InfrastructureCreate from "../Pages/Infrastructure/Create";
import InfrastructureDetails from "../Pages/Infrastructure/Details";
+// Distributed Uptime
+import DistributedUptimeMonitors from "../Pages/DistributedUptime/Monitors";
+import CreateDistributedUptime from "../Pages/DistributedUptime/Create";
+import DistributedUptimeDetails from "../Pages/DistributedUptime/Details";
+
+// Incidents
import Incidents from "../Pages/Incidents";
-//Status pages
+// Status pages
import CreateStatus from "../Pages/StatusPage/Create";
import Status from "../Pages/StatusPage/Status";
@@ -78,6 +84,18 @@ const Routes = () => {
path="/uptime/configure/:monitorId/"
element={}
/>
+ }
+ />
+ }
+ />
+ }
+ />
}
diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js
index 85e82554e..254138ff3 100644
--- a/Client/src/Utils/NetworkService.js
+++ b/Client/src/Utils/NetworkService.js
@@ -862,6 +862,110 @@ class NetworkService {
);
}
+ subscribeToDistributedUptimeMonitors(config) {
+ const {
+ authToken,
+ teamId,
+ onUpdate,
+ onError,
+ onOpen,
+ limit,
+ types,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ } = config;
+
+ const params = new URLSearchParams();
+
+ if (limit) params.append("limit", limit);
+ if (types) {
+ types.forEach((type) => {
+ params.append("type", type);
+ });
+ }
+ if (page) params.append("page", page);
+ if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
+ if (filter) params.append("filter", filter);
+ if (field) params.append("field", field);
+ if (order) params.append("order", order);
+
+ if (this.eventSource) {
+ this.eventSource.close();
+ }
+
+ const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}?${params.toString()}`;
+ this.eventSource = new EventSource(url, {
+ headers: { Authorization: `Bearer ${authToken}` },
+ });
+
+ this.eventSource.onopen = () => {
+ onOpen?.();
+ };
+
+ this.eventSource.addEventListener("open", (e) => {
+ console.log("getDistributedUptimeMonitors connection opened:");
+ });
+
+ this.eventSource.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ onUpdate(data);
+ };
+
+ this.eventSource.onerror = (error) => {
+ console.error("Monitor stream error:", error);
+ onError?.();
+ this.eventSource.close();
+ };
+
+ // Returns a cleanup function
+ return () => {
+ if (this.eventSource) {
+ this.eventSource.close();
+ this.eventSource = null;
+ }
+ return () => {
+ console.log("Nothing to cleanup");
+ };
+ };
+ }
+
+ subscribeToDistributedUptimeDetails(config) {
+ const params = new URLSearchParams();
+ const { authToken, monitorId, onUpdate, onOpen, onError, dateRange, normalize } =
+ config;
+ if (dateRange) params.append("dateRange", dateRange);
+ if (normalize) params.append("normalize", normalize);
+
+ const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}?${params.toString()}`;
+ this.eventSource = new EventSource(url, {
+ headers: { Authorization: `Bearer ${authToken}` },
+ });
+
+ this.eventSource.onopen = (e) => {
+ onOpen?.();
+ };
+
+ this.eventSource.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ onUpdate(data);
+ };
+
+ this.eventSource.onerror = (error) => {
+ console.error("Monitor stream error:", error);
+ onError?.();
+ this.eventSource.close();
+ };
+ return () => {
+ if (this.eventSource) {
+ this.eventSource.close();
+ this.eventSource = null;
+ }
+ };
+ }
+
async getStatusPage(config) {
const { authToken } = config;
return this.axiosInstance.get(`/status-page`, {
diff --git a/Client/src/assets/icons/groups.svg b/Client/src/assets/icons/groups.svg
new file mode 100644
index 000000000..fa2cae03c
--- /dev/null
+++ b/Client/src/assets/icons/groups.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Client/src/assets/icons/solana_logo.svg b/Client/src/assets/icons/solana_logo.svg
new file mode 100644
index 000000000..bd349af26
--- /dev/null
+++ b/Client/src/assets/icons/solana_logo.svg
@@ -0,0 +1,28 @@
+
+
+
diff --git a/Client/src/assets/icons/upt_logo.png b/Client/src/assets/icons/upt_logo.png
new file mode 100644
index 000000000..d443d0897
Binary files /dev/null and b/Client/src/assets/icons/upt_logo.png differ
diff --git a/Server/controllers/distributedUptimeController.js b/Server/controllers/distributedUptimeController.js
index ee8f5398e..09c1497cf 100644
--- a/Server/controllers/distributedUptimeController.js
+++ b/Server/controllers/distributedUptimeController.js
@@ -138,6 +138,7 @@ class DistributedUptimeController {
clearInterval(keepAlive);
});
} catch (error) {
+ console.log(error);
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitors"));
}
}
diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js
index e62828a41..8f035c2dc 100644
--- a/Server/db/mongo/MongoDB.js
+++ b/Server/db/mongo/MongoDB.js
@@ -43,6 +43,11 @@ import * as hardwareCheckModule from "./modules/hardwareCheckModule.js";
import * as checkModule from "./modules/checkModule.js";
+//****************************************
+// Distributed Checks
+//****************************************
+import * as distributedCheckModule from "./modules/distributedCheckModule.js";
+
//****************************************
// Maintenance Window
//****************************************
@@ -74,6 +79,7 @@ class MongoDB {
Object.assign(this, pageSpeedCheckModule);
Object.assign(this, hardwareCheckModule);
Object.assign(this, checkModule);
+ Object.assign(this, distributedCheckModule);
Object.assign(this, maintenanceWindowModule);
Object.assign(this, notificationModule);
Object.assign(this, settingsModule);
diff --git a/Server/db/mongo/modules/distributedCheckModule.js b/Server/db/mongo/modules/distributedCheckModule.js
new file mode 100644
index 000000000..5c7fb09a8
--- /dev/null
+++ b/Server/db/mongo/modules/distributedCheckModule.js
@@ -0,0 +1,15 @@
+import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
+const SERVICE_NAME = "distributedCheckModule";
+
+const createDistributedCheck = async (checkData) => {
+ try {
+ const check = await new DistributedUptimeCheck({ ...checkData }).save();
+ return check;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "createCheck";
+ throw error;
+ }
+};
+
+export { createDistributedCheck };
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index c3d1a38be..9a8e98f4e 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -405,7 +405,7 @@ const getDistributedUptimeDetailsById = async (req) => {
return monitorStats;
} catch (error) {
error.service = SERVICE_NAME;
- error.method = "getUptimeDetailsById";
+ error.method = "getDistributedUptimeDetailsById";
throw error;
}
};
@@ -674,6 +674,26 @@ const getMonitorsByTeamId = async (req) => {
},
]
: []),
+ ...(limit
+ ? [
+ {
+ $lookup: {
+ from: "distributeduptimechecks",
+ let: { monitorId: "$_id" },
+ pipeline: [
+ {
+ $match: {
+ $expr: { $eq: ["$monitorId", "$$monitorId"] },
+ },
+ },
+ { $sort: { createdAt: -1 } },
+ ...(limit ? [{ $limit: limit }] : []),
+ ],
+ as: "distributeduptimechecks",
+ },
+ },
+ ]
+ : []),
{
$addFields: {
@@ -692,6 +712,10 @@ const getMonitorsByTeamId = async (req) => {
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
+ {
+ case: { $eq: ["$type", "distributed_http"] },
+ then: "$distributeduptimechecks",
+ },
],
default: [],
},
diff --git a/Server/index.js b/Server/index.js
index 7a4129bc9..23d16856a 100644
--- a/Server/index.js
+++ b/Server/index.js
@@ -244,7 +244,11 @@ const startApp = async () => {
ServiceRegistry.get(MongoDB.SERVICE_NAME)
);
- const distributedUptimeController = new DistributedUptimeController();
+ const distributedUptimeController = new DistributedUptimeController(
+ ServiceRegistry.get(MongoDB.SERVICE_NAME),
+ http,
+ ServiceRegistry.get(StatusService.SERVICE_NAME)
+ );
//Create routes
const authRoutes = new AuthRoutes(authController);