Merge pull request #1696 from bluewave-labs/feat/fe/distributed-uptime

feat: fe/distributed uptime
This commit is contained in:
Alexander Holliday
2025-02-05 15:19:47 -08:00
committed by GitHub
26 changed files with 2063 additions and 20 deletions

380
Client/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
{icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>}
</Stack>
{children}
</Stack>
@@ -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,
};

View File

@@ -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;

View File

@@ -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: <Monitors /> },
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
{ name: "Infrastructure", path: "infrastructure", icon: <Integrations /> },
{ name: "Distributed Uptime", path: "distributed-uptime", icon: <Groups /> },
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
{ name: "Status pages", path: "status", icon: <StatusPages /> },
@@ -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
>
<ListItemButton
className={location.pathname.includes(item.path) ? "selected-path" : ""}
className={location.pathname === `/${item.path}` ? "selected-path" : ""}
onClick={() => 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),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>

View File

@@ -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 (
<Box
<Stack
direction="row"
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
/* TODO why are we using width and min width here? */
@@ -95,9 +105,20 @@ const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) =>
...sx,
}}
>
<Typography component="h2">{heading}</Typography>
<Typography>{subHeading}</Typography>
</Box>
{img && (
<Image
src={img}
height={"30px"}
width={"30px"}
alt={alt}
sx={{ marginRight: theme.spacing(8) }}
/>
)}
<Stack>
<Typography component="h2">{heading}</Typography>
<Typography>{subHeading}</Typography>
</Stack>
</Stack>
);
};

View File

@@ -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 (
<Box>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
component="form"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
onSubmit={() => console.log("submit")}
>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
>
Create your{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
monitor
</Typography>
</Typography>
<ConfigBox>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the type of monitor.
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
type={"url"}
id="monitor-url"
startAdornment={<HttpAdornment https={https} />}
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"]}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={"Google"}
value={monitor.name}
name="name"
onChange={handleChange}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Checks to perform</Typography>
<Typography component="p">
You can always add or remove checks after adding your site.
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
title="Website monitoring"
desc="Use HTTP(s) to monitor your website or API endpoint."
size="small"
value="http"
name="type"
checked={true}
onChange={handleChange}
/>
{monitor.type === "http" || monitor.type === "distributed_http" ? (
<ButtonGroup sx={{ ml: theme.spacing(16) }}>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
HTTPS
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
HTTP
</Button>
</ButtonGroup>
) : (
""
)}
</Stack>
{errors["type"] ? (
<Box className="error-container">
<Typography
component="p"
className="input-error"
color={theme.palette.error.contrastText}
>
{errors["type"]}
</Typography>
</Box>
) : (
""
)}
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Incident notifications</Typography>
<Typography component="p">
When there is an incident, notify users.
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
<Checkbox
id="notify-email-default"
label={`Notify via email (to ${user.email})`}
isChecked={notifications.some(
(notification) => notification.type === "email"
)}
value={user?.email}
onChange={(event) => handleNotifications(event, "email")}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Advanced settings</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Select
id="monitor-interval"
label="Check frequency"
name="interval"
value={monitor.interval || 1}
onChange={handleChange}
items={SELECT_VALUES}
/>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<LoadingButton
variant="contained"
color="primary"
onClick={() => handleCreateMonitor()}
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
Create monitor
</LoadingButton>
</Stack>
</Stack>
</Box>
);
};
export default CreateDistributedUptime;

View File

@@ -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 (
<Stack
direction="column"
gap={theme.spacing(2)}
width={width}
sx={{
padding: `${theme.spacing(BASE_BOX_PADDING_VERTICAL)} ${theme.spacing(BASE_BOX_PADDING_HORIZONTAL)}`,
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
}}
>
<Stack
direction="row"
justifyContent={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[connectionStatus]} />
<Typography
variant="h1"
mb={theme.spacing(8)}
sx={{ alignSelf: "center" }}
>
{connectionStatus === "up" ? "Connected" : "No connection"}
</Typography>
</Stack>
<List>
{data.slice(Math.max(data.length - 5, 0)).map((dataPoint) => {
const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
const flag = countryCode ? `fi fi-${countryCode}` : null;
return (
<ListItem key={Math.random()}>
<Stack direction="column">
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
{flag && <span className={flag} />}
<Typography variant="h2">{dataPoint?.city || "Unknown"}</Typography>
</Stack>
<Typography variant="p">{`Response time: ${Math.floor(dataPoint?.responseTime ?? 0)} ms`}</Typography>
<Typography variant="p">{`UPT burned: ${dataPoint.uptBurnt}`}</Typography>
<Typography variant="p">{`${dataPoint?.device?.manufacturer} ${dataPoint?.device?.model}`}</Typography>
</Stack>
</ListItem>
);
})}
</List>
</Stack>
);
};
export default DeviceTicker;

View File

@@ -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"
}
}

View File

@@ -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 (
<div
ref={mapContainer}
style={{
width: width,
height: height,
}}
/>
);
};
DistributedUptimeMap.propTypes = {
checks: PropTypes.array,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default DistributedUptimeMap;

View File

@@ -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 (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
</Typography>
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={theme.palette.primary.main}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response Time
</Typography>{" "}
<Typography component="span">
{Math.floor(responseTime)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
{/* Display original value */}
</Box>
);
}
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 (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.tertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
</Text>
);
};
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 (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
>
<AreaChart
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.accent}
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
/>
</AreaChart>
</ResponsiveContainer>
);
};
DistributedUptimeResponseChart.propTypes = {
checks: PropTypes.array,
};
export default DistributedUptimeResponseChart;

View File

@@ -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 (
<Stack
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Typography variant="h2">Made with by UpRock & Bluewave Labs</Typography>
<Stack
width="100%"
direction="row"
gap={theme.spacing(2)}
justifyContent="center"
alignItems="center"
>
<Typography variant="h2">Built on</Typography>
<SolanaLogo
width={15}
height={15}
/>
<Typography variant="h2">Solana</Typography>
</Stack>
</Stack>
);
};
export default Footer;

View File

@@ -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;

View File

@@ -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 (
<LinearProgress
variant="determinate"
color="accent"
value={Math.min((elapsedMs / interval) * 100, 100)}
sx={{
transition: "width 1s linear", // Smooth transition over 1 second
}}
/>
);
};
export default NextExpectedCheck;

View File

@@ -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 (
// <Stack
// direction="row"
// width={"25%"}
// justifyContent="center"
// sx={{
// padding: `${theme.spacing(BASE_BOX_PADDING_VERTICAL)} ${theme.spacing(BASE_BOX_PADDING_HORIZONTAL)}`,
// backgroundColor: theme.palette.background.main,
// border: 1,
// borderStyle: "solid",
// borderColor: theme.palette.primary.lowContrast,
// }}
// >
// {img && (
// <img
// style={{ marginRight: theme.spacing(8) }}
// height={30}
// width={30}
// src={img}
// alt={altTxt}
// />
// )}
// <Stack direction="column">
// <Typography variant="h2">{heading}</Typography>
// <Typography>{value}</Typography>
// </Stack>
// </Stack>
// );
// };
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 && (
<Stack
direction="column"
gap={theme.spacing(8)}
>
<Breadcrumbs list={BREADCRUMBS} />
{monitor?.url !== "https://jup.ag/" &&
monitor?.url !== "https://explorer.solana.com/" && (
<Box>
<Typography
component="h1"
variant="h1"
>
{monitor.name}
</Typography>
</Box>
)}
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(8)}
>
<Typography variant="h2">
Distributed Uptime Monitoring powered by DePIN
</Typography>
</Stack>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
heading="Avg Response Time"
subHeading={`${Math.floor(monitor?.avgResponseTime ?? 0)} ms`}
/>
<StatBox
heading="Checking every"
subHeading={`${(monitor?.interval ?? 0) / 1000} seconds`}
/>
<StatBox
heading={"Last check"}
subHeading={
<LastUpdate
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
suffix={"seconds ago"}
/>
}
/>
<StatBox
heading="Last server push"
subHeading={
<LastUpdate
suffix={"seconds ago"}
lastUpdateTime={0}
trigger={lastUpdateTrigger}
/>
}
/>
<StatBox
heading="UPT Burned"
subHeading={monitor?.totalUptBurnt ?? 0}
img={UptLogo}
alt="Upt Logo"
/>
</Stack>
<Box sx={{ width: "100%" }}>
<NextExpectedCheck
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
interval={monitor?.interval ?? 0}
trigger={lastUpdateTrigger}
/>
</Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "day"
? "24 hours"
: dateRange === "week"
? "7 days"
: "30 days"}
.
</Typography>
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
</Stack>
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
sx={{ padding: 0 }}
>
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
</ChartBox>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<DistributedUptimeMap
checks={monitor?.groupedMapChecks ?? []}
height={"100%"}
width={"100%"}
/>
<DeviceTicker
width={"25vw"}
data={monitor?.latestChecks ?? []}
connectionStatus={connectionStatus}
/>
</Stack>
<Footer />
</Stack>
)
);
};
export default DistributedUptimeDetails;

View File

@@ -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: <Box>Host</Box>,
render: (row) => (
<Host
key={row._id}
url={row.url}
title={row.title}
percentageColor={row.percentageColor}
percentage={row.percentage}
/>
),
},
{
id: "status",
content: <Box width="max-content"> Status</Box>,
render: (row) => {
const status = determineState(row?.monitor);
return (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
id: "responseTime",
content: "Response Time",
render: (row) => <BarChart checks={row?.monitor?.checks.slice().reverse()} />,
},
{
id: "type",
content: "Type",
render: (row) => <span>{TYPE_MAP[row.monitor.type]}</span>,
},
{
id: "actions",
content: "Actions",
render: (row) => (
<ActionsMenu
monitor={row.monitor}
isAdmin={true}
/>
),
},
];
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 (
<Stack
direction="column"
gap={theme.spacing(8)}
>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
gap={theme.spacing(6)}
>
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/distributed-uptime/create");
}}
sx={{ fontWeight: 500, whiteSpace: "nowrap" }}
>
Create new
</Button>
</Stack>
{monitors.length > 0 && (
<DataTable
headers={headers}
data={filteredMonitors}
config={{
rowSX: {
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
},
onRowClick: (row) => {
navigate(`/distributed-uptime/${row.id}`);
},
}}
/>
)}
</Stack>
);
};
export default DistributedUptimeMonitors;

View File

@@ -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={<UptimeConfigure />}
/>
<Route
path="/distributed-uptime"
element={<DistributedUptimeMonitors />}
/>
<Route
path="/distributed-uptime/create"
element={<CreateDistributedUptime />}
/>
<Route
path="/distributed-uptime/:monitorId"
element={<DistributedUptimeDetails />}
/>
<Route
path="pagespeed"
element={<PageSpeed />}

View File

@@ -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`, {

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#5f6368"><g><rect fill="none" height="24" width="24"/></g><g><g/><g><g><path d="M16.67,13.13C18.04,14.06,19,15.32,19,17v3h4v-3 C23,14.82,19.43,13.53,16.67,13.13z" fill-rule="evenodd"/></g><g><circle cx="9" cy="8" fill-rule="evenodd" r="4"/></g><g><path d="M15,12c2.21,0,4-1.79,4-4c0-2.21-1.79-4-4-4c-0.47,0-0.91,0.1-1.33,0.24 C14.5,5.27,15,6.58,15,8s-0.5,2.73-1.33,3.76C14.09,11.9,14.53,12,15,12z" fill-rule="evenodd"/></g><g><path d="M9,13c-2.67,0-8,1.34-8,4v3h16v-3C17,14.34,11.67,13,9,13z" fill-rule="evenodd"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 659 B

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 397.7 311.7" style="enable-background:new 0 0 397.7 311.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="360.8791" y1="351.4553" x2="141.213" y2="-69.2936" gradientTransform="matrix(1 0 0 -1 0 314)">
<stop offset="0" style="stop-color:#00FFA3"/>
<stop offset="1" style="stop-color:#DC1FFF"/>
</linearGradient>
<path class="st0" d="M64.6,237.9c2.4-2.4,5.7-3.8,9.2-3.8h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5
c-5.8,0-8.7-7-4.6-11.1L64.6,237.9z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="264.8291" y1="401.6014" x2="45.163" y2="-19.1475" gradientTransform="matrix(1 0 0 -1 0 314)">
<stop offset="0" style="stop-color:#00FFA3"/>
<stop offset="1" style="stop-color:#DC1FFF"/>
</linearGradient>
<path class="st1" d="M64.6,3.8C67.1,1.4,70.4,0,73.8,0h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5
c-5.8,0-8.7-7-4.6-11.1L64.6,3.8z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="312.5484" y1="376.688" x2="92.8822" y2="-44.061" gradientTransform="matrix(1 0 0 -1 0 314)">
<stop offset="0" style="stop-color:#00FFA3"/>
<stop offset="1" style="stop-color:#DC1FFF"/>
</linearGradient>
<path class="st2" d="M333.1,120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8,0-8.7,7-4.6,11.1l62.7,62.7c2.4,2.4,5.7,3.8,9.2,3.8h317.4
c5.8,0,8.7-7,4.6-11.1L333.1,120.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

View File

@@ -138,6 +138,7 @@ class DistributedUptimeController {
clearInterval(keepAlive);
});
} catch (error) {
console.log(error);
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitors"));
}
}

View File

@@ -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);

View File

@@ -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 };

View File

@@ -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: [],
},

View File

@@ -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);