Resolved merge conflict in Server/index.js
This commit is contained in:
commit
e9bcf66b29
99 files changed
+5354
-358
No files matched your search
+2
-1
@@ -2,4 +2,5 @@
|
||||
.vscode
|
||||
*.sh
|
||||
!uptime.sh
|
||||
*.idea
|
||||
*.idea
|
||||
*.notes
|
||||
@@ -1 +1,2 @@
|
||||
VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL
|
||||
VITE_STATUS_PAGE_SUBDOMAIN_PREFIX=UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX
|
||||
Generated
+531
-1
@@ -11,6 +11,7 @@
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@hello-pangea/dnd": "^17.0.0",
|
||||
"@mui/icons-material": "6.4.3",
|
||||
"@mui/lab": "6.0.0-beta.26",
|
||||
"@mui/material": "6.4.3",
|
||||
@@ -20,9 +21,15 @@
|
||||
"@reduxjs/toolkit": "2.5.1",
|
||||
"axios": "^1.7.4",
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"joi": "17.13.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"maplibre-gl": "5.1.0",
|
||||
"mui-color-input": "^5.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "9.2.0",
|
||||
"react-router": "^6.23.0",
|
||||
@@ -332,6 +339,14 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz",
|
||||
"integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||
@@ -984,6 +999,24 @@
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hello-pangea/dnd": {
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-17.0.0.tgz",
|
||||
"integrity": "sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"css-box-model": "^1.2.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react-redux": "^9.1.2",
|
||||
"redux": "^5.0.1",
|
||||
"use-memo-one": "^1.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
@@ -1070,6 +1103,83 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/geojson-rewind": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"get-stream": "^6.0.1",
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"geojson-rewind": "geojson-rewind"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
|
||||
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
|
||||
"integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/unitbezier": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
|
||||
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/whoots-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "23.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.1.0.tgz",
|
||||
"integrity": "sha512-R6/ihEuC5KRexmKIYkWqUv84Gm+/QwsOUgHyt1yy2XqCdGdLvlBWVWIIeTZWN4NGdwmY6xDzdSGU2R9oBLNg2w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"quickselect": "^3.0.0",
|
||||
"rw": "^1.3.3",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gl-style-format": "dist/gl-style-format.mjs",
|
||||
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/base": {
|
||||
"version": "5.0.0-beta.69",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.69.tgz",
|
||||
@@ -1618,6 +1728,21 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-dnd/asap": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
|
||||
"integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
|
||||
},
|
||||
"node_modules/@react-dnd/invariant": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
|
||||
"integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
|
||||
},
|
||||
"node_modules/@react-dnd/shallowequal": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
|
||||
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
|
||||
},
|
||||
"node_modules/@react-spring/animated": {
|
||||
"version": "9.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
|
||||
@@ -2359,12 +2484,50 @@
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson-vt": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
|
||||
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mapbox__point-geometry": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
||||
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mapbox__vector-tile": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
|
||||
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*",
|
||||
"@types/mapbox__point-geometry": "*",
|
||||
"@types/pbf": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/pbf": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
@@ -2400,6 +2563,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
@@ -2932,6 +3104,14 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-box-model": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||
"dependencies": {
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -3215,6 +3395,24 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dnd-core": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
|
||||
"integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
|
||||
"dependencies": {
|
||||
"@react-dnd/asap": "^5.0.1",
|
||||
"@react-dnd/invariant": "^4.0.1",
|
||||
"redux": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dnd-core/node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
@@ -3263,6 +3461,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz",
|
||||
"integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.65",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz",
|
||||
@@ -3785,7 +3989,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
@@ -3857,6 +4060,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/flag-icons": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.3.2.tgz",
|
||||
"integrity": "sha512-QkaZ6Zvai8LIjx+UNAHUJ5Dhz9OLZpBDwCRWxF6YErxIcR16jTkIFm3bFu54EkvKQy4+wicW+Gm7/0631wVQyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||
@@ -3993,6 +4202,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/geojson-vt": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
|
||||
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
|
||||
@@ -4018,6 +4233,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-symbol-description": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
|
||||
@@ -4036,6 +4263,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@@ -4071,6 +4304,44 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
|
||||
"integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ini": "^4.1.3",
|
||||
"kind-of": "^6.0.3",
|
||||
"which": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix/node_modules/isexe": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
|
||||
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix/node_modules/which": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
|
||||
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/which.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
@@ -4222,6 +4493,26 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4242,6 +4533,11 @@
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/immutability-helper": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz",
|
||||
"integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ=="
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -4287,6 +4583,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
|
||||
"integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@@ -4789,6 +5094,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-pretty-compact": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -4826,6 +5137,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -4836,6 +5153,15 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -4915,6 +5241,47 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.1.0.tgz",
|
||||
"integrity": "sha512-6lbf7qAnqAVm1T/vJBMmRtP+g8G/O/Z52IBtWX31SbFj7sEdlrk4YugxJen8IdV/pFjLFnDOw7HiHZl5nYdVjg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/point-geometry": "^0.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.0.6",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "^23.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/geojson-vt": "3.2.5",
|
||||
"@types/mapbox__point-geometry": "^0.1.4",
|
||||
"@types/mapbox__vector-tile": "^1.3.4",
|
||||
"@types/pbf": "^3.0.5",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"earcut": "^3.0.1",
|
||||
"geojson-vt": "^4.0.2",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"global-prefix": "^4.0.0",
|
||||
"kdbush": "^4.0.2",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^3.3.0",
|
||||
"potpack": "^2.0.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"supercluster": "^8.0.1",
|
||||
"tinyqueue": "^3.0.0",
|
||||
"vt-pbf": "^3.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0",
|
||||
"npm": ">=8.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -4925,6 +5292,11 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -4959,12 +5331,48 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mui-color-input": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-5.0.1.tgz",
|
||||
"integrity": "sha512-50Ws4vhg4UPQSZEZDCNc7vyUBSb9x1bK+bO1o0wxJvQYgeSyg2r7mYDlavpCh+ZvisgBL/98y0GVN6M9901JWg==",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material": "^6.0.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
@@ -5246,6 +5654,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
|
||||
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"ieee754": "^1.1.12",
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -5302,6 +5723,12 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
|
||||
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -5345,6 +5772,12 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -5382,6 +5815,17 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/raf-schd": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
@@ -5394,6 +5838,43 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dnd": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
|
||||
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
|
||||
"dependencies": {
|
||||
"@react-dnd/invariant": "^4.0.1",
|
||||
"@react-dnd/shallowequal": "^4.0.1",
|
||||
"dnd-core": "^16.0.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/hoist-non-react-statics": ">= 3.3.1",
|
||||
"@types/node": ">= 12",
|
||||
"@types/react": ">= 16",
|
||||
"react": ">= 16.14"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/hoist-non-react-statics": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-dnd-html5-backend": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
|
||||
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
|
||||
"dependencies": {
|
||||
"dnd-core": "^16.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
@@ -5658,6 +6139,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
@@ -5753,6 +6243,12 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/safe-array-concat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||
@@ -6100,6 +6596,15 @@
|
||||
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supercluster": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -6144,6 +6649,12 @@
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -6313,6 +6824,14 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-memo-one": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
|
||||
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||
@@ -6417,6 +6936,17 @@
|
||||
"vite": ">=2.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vt-pbf": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
||||
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "0.1.0",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"pbf": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@hello-pangea/dnd": "^17.0.0",
|
||||
"@mui/icons-material": "6.4.3",
|
||||
"@mui/lab": "6.0.0-beta.26",
|
||||
"@mui/material": "6.4.3",
|
||||
@@ -23,9 +24,15 @@
|
||||
"@reduxjs/toolkit": "2.5.1",
|
||||
"axios": "^1.7.4",
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"joi": "17.13.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"maplibre-gl": "5.1.0",
|
||||
"mui-color-input": "^5.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "9.2.0",
|
||||
"react-router": "^6.23.0",
|
||||
|
||||
+6
-6
@@ -2,23 +2,23 @@ import { useState } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../../../../Utils/toastUtils";
|
||||
import { logger } from "../../../../../Utils/Logger";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { logger } from "../../Utils/Logger";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import {
|
||||
deleteUptimeMonitor,
|
||||
pauseUptimeMonitor,
|
||||
} from "../../../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Settings from "../../../../../assets/icons/settings-bold.svg?react";
|
||||
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Settings from "../../assets/icons/settings-bold.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
import Dialog from "../../../../../Components/Dialog";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
|
||||
const ActionsMenu = ({
|
||||
monitor,
|
||||
isAdmin,
|
||||
updateRowCallback,
|
||||
pauseCallback,
|
||||
setIsLoading,
|
||||
setIsLoading = () => {},
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [actions, setActions] = useState({});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
/* TODO add prop validation and jsdocs */
|
||||
const StatusPageBarChart = ({ checks = [] }) => {
|
||||
const theme = useTheme();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
useEffect(() => {
|
||||
setAnimate(true);
|
||||
}, []);
|
||||
|
||||
// set responseTime to average if there's only one check
|
||||
if (checks.length === 1) {
|
||||
checks[0] = { ...checks[0], responseTime: 50 };
|
||||
}
|
||||
|
||||
if (checks.length !== 25) {
|
||||
const placeholders = Array(25 - checks.length).fill("placeholder");
|
||||
checks = [...checks, ...placeholders];
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
flexWrap="nowrap"
|
||||
height="50px"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
sx={{
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{checks.map((check, index) =>
|
||||
check === "placeholder" ? (
|
||||
/* TODO what is the purpose of this box? */
|
||||
// CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars
|
||||
// even if there are less than 25 checks
|
||||
<Box
|
||||
key={`${check}-${index}`}
|
||||
position="relative"
|
||||
width="calc(30vw / 25)"
|
||||
height="100%"
|
||||
backgroundColor={theme.palette.primary.lowContrast}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
<Typography>
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(2)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={
|
||||
check.status
|
||||
? theme.palette.success.lowContrast
|
||||
: theme.palette.error.lowContrast
|
||||
}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(2)}
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
Response Time
|
||||
</Typography>
|
||||
<Typography component="span">
|
||||
{check.originalResponseTime}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
placement="top"
|
||||
key={`check-${check?._id}`}
|
||||
slotProps={{
|
||||
popper: {
|
||||
className: "bar-tooltip",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -10],
|
||||
},
|
||||
},
|
||||
],
|
||||
sx: {
|
||||
"& .MuiTooltip-tooltip": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
px: theme.spacing(4),
|
||||
py: theme.spacing(3),
|
||||
},
|
||||
"& .MuiTooltip-tooltip p": {
|
||||
/* TODO Font size should point to theme */
|
||||
fontSize: 12,
|
||||
color: theme.palette.secondary.contrastText,
|
||||
fontWeight: 500,
|
||||
},
|
||||
"& .MuiTooltip-tooltip span": {
|
||||
/* TODO Font size should point to theme */
|
||||
fontSize: 11,
|
||||
color: theme.palette.secondary.contrastText,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
width="calc(30vw / 25)"
|
||||
height="100%"
|
||||
backgroundColor={theme.palette.primary.lowContrast} // CAIO_REVIEW
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
width="100%"
|
||||
height={`${animate ? check.responseTime : 0}%`}
|
||||
backgroundColor={
|
||||
check.status
|
||||
? theme.palette.success.lowContrast
|
||||
: theme.palette.error.lowContrast
|
||||
}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusPageBarChart;
|
||||
@@ -29,10 +29,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
},
|
||||
"& h1, & h2": {
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
},
|
||||
"& p": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
||||
export default ConfigBox;
|
||||
@@ -1,18 +1,19 @@
|
||||
import PropTypes from "prop-types";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { Button, Stack } from "@mui/material";
|
||||
import { GenericDialog } from "./genericDialog";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const Dialog = ({
|
||||
title,
|
||||
description,
|
||||
open,
|
||||
theme,
|
||||
onCancel,
|
||||
confirmationButtonLabel,
|
||||
onConfirm,
|
||||
isLoading,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<GenericDialog
|
||||
title={title}
|
||||
@@ -34,14 +35,14 @@ const Dialog = ({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading={isLoading}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmationButtonLabel}
|
||||
</LoadingButton>
|
||||
</Button>
|
||||
</Stack>
|
||||
</GenericDialog>
|
||||
);
|
||||
@@ -51,7 +52,6 @@ Dialog.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
open: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
confirmationButtonLabel: PropTypes.string.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Dot from "../../../../../Components/Dot";
|
||||
import Dot from "../Dot";
|
||||
/**
|
||||
* Host component.
|
||||
* This subcomponent receives a params object and displays the host details.
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Box } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const isValidBase64Image = (data) => {
|
||||
return /^[A-Za-z0-9+/=]+$/.test(data);
|
||||
};
|
||||
|
||||
const Image = ({
|
||||
shouldRender = true,
|
||||
src,
|
||||
alt,
|
||||
width = "auto",
|
||||
height = "auto",
|
||||
maxWidth = "auto",
|
||||
maxHeight = "auto",
|
||||
base64,
|
||||
sx,
|
||||
}) => {
|
||||
if (shouldRender === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof src !== "undefined" && typeof base64 !== "undefined") {
|
||||
console.warn("base64 takes precedence over src and overwrites it");
|
||||
}
|
||||
|
||||
if (typeof base64 !== "undefined" && isValidBase64Image(base64)) {
|
||||
src = `data:image/png;base64,${base64}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt={alt}
|
||||
maxWidth={maxWidth}
|
||||
maxHeight={maxHeight}
|
||||
width={width}
|
||||
height={height}
|
||||
sx={{ ...sx }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Image.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
src: PropTypes.string,
|
||||
alt: PropTypes.string.isRequired,
|
||||
width: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
maxWidth: PropTypes.string,
|
||||
maxHeight: PropTypes.string,
|
||||
base64: PropTypes.string,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Image;
|
||||
@@ -75,7 +75,6 @@ const Checkbox = ({
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"& svg": { width: sizes[size], height: sizes[size] },
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -111,7 +110,7 @@ Checkbox.propTypes = {
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||
isChecked: PropTypes.bool.isRequired,
|
||||
value: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
onChange: PropTypes.func,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { MuiColorInput } from "mui-color-input";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} id The ID of the component
|
||||
* @param {*} value The color value of the component
|
||||
* @param {*} error The error of the component
|
||||
* @param {*} onChange The Change handler function
|
||||
* @param {*} onBlur The Blur handler function
|
||||
* @returns The ColorPicker component
|
||||
* Example usage:
|
||||
* <ColorPicker
|
||||
* id="color"
|
||||
* value={form.color}
|
||||
* error={errors["color"]}
|
||||
* onChange={handleColorChange}
|
||||
* onBlur={handleBlur}
|
||||
* >
|
||||
* </ColorPicker>
|
||||
*/
|
||||
const ColorPicker = ({ id, name, value, error, onChange, onBlur }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<MuiColorInput
|
||||
format="hex"
|
||||
name={name}
|
||||
type="color-picker"
|
||||
value={value}
|
||||
id={id}
|
||||
onChange={(color) => onChange({ target: { name, value: color } })}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
ColorPicker.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func,
|
||||
name: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ColorPicker;
|
||||
@@ -11,11 +11,16 @@ import { checkImage } from "../../../Utils/fileUtils";
|
||||
* @param {string} props.id - The unique identifier for the input field.
|
||||
* @param {string} props.src - The URL of the image to display.
|
||||
* @param {function} props.onChange - The function to handle file input change.
|
||||
* @param {boolean} props.isRound - Whether the shape of the image to display is round.
|
||||
* @param {string} props.maxSize - Custom message for the max uploaded file size
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
|
||||
const ImageField = ({ id, src, loading, onChange }) => {
|
||||
const ImageField = ({ id, src, loading, onChange, error, isRound = true, maxSize }) => {
|
||||
const theme = useTheme();
|
||||
const error_border_style = error ? { borderColor: theme.palette.error.main } : {};
|
||||
|
||||
const roundShape = isRound ? { borderRadius: "50%" } : {};
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handleDragEnter = () => {
|
||||
@@ -46,6 +51,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
...error_border_style,
|
||||
}}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
@@ -62,6 +68,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
cursor: "pointer",
|
||||
maxWidth: "500px",
|
||||
minHeight: "175px",
|
||||
zIndex: 1,
|
||||
},
|
||||
"& fieldset": {
|
||||
padding: 0,
|
||||
@@ -78,7 +85,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: "-1",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
@@ -111,7 +118,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
(maximum size: 3MB)
|
||||
(maximum size: {maxSize ?? "3MB"})
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -122,6 +129,19 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
>
|
||||
Supported formats: JPG, PNG
|
||||
</Typography>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Stack
|
||||
@@ -132,10 +152,10 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
sx={{
|
||||
width: "250px",
|
||||
height: "250px",
|
||||
borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
backgroundImage: `url(${src})`,
|
||||
backgroundSize: "cover",
|
||||
...roundShape,
|
||||
}}
|
||||
></Box>
|
||||
</Stack>
|
||||
@@ -148,6 +168,8 @@ ImageField.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
src: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isRound: PropTypes.bool,
|
||||
maxSize: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageField;
|
||||
@@ -12,6 +12,7 @@ import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
* @param {string} props.filteredBy - Key to access the option label from the options
|
||||
* @param {string} props.value - Current input value for the Autocomplete
|
||||
* @param {Function} props.handleChange - Function to call when the input changes
|
||||
* @param {Function} Prop.onBlur - Function to call when the input is blured
|
||||
* @param {Object} props.sx - Additional styles to apply to the component
|
||||
* @returns {JSX.Element} The rendered Search component
|
||||
*/
|
||||
@@ -54,10 +55,14 @@ const Search = ({
|
||||
isAdorned = true,
|
||||
error,
|
||||
disabled,
|
||||
startAdornment,
|
||||
endAdornment,
|
||||
onBlur,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Autocomplete
|
||||
onBlur={onBlur}
|
||||
multiple={multiple}
|
||||
id={id}
|
||||
value={value}
|
||||
@@ -66,7 +71,7 @@ const Search = ({
|
||||
handleInputChange(newValue);
|
||||
}}
|
||||
onChange={(_, newValue) => {
|
||||
handleChange && handleChange(newValue);
|
||||
handleChange(newValue);
|
||||
}}
|
||||
fullWidth
|
||||
freeSolo
|
||||
@@ -74,6 +79,7 @@ const Search = ({
|
||||
disableClearable
|
||||
options={options}
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
||||
renderInput={(params) => (
|
||||
<Stack>
|
||||
<Typography
|
||||
@@ -88,9 +94,13 @@ const Search = ({
|
||||
{...params}
|
||||
error={Boolean(error)}
|
||||
placeholder="Type to search"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
...(isAdorned && { startAdornment: <SearchAdornment /> }),
|
||||
slotProps={{
|
||||
input: {
|
||||
...params.InputProps,
|
||||
...(isAdorned && { startAdornment: <SearchAdornment /> }),
|
||||
...(startAdornment && { startAdornment: startAdornment }),
|
||||
...(endAdornment && { endAdornment: endAdornment }),
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
@@ -212,6 +222,9 @@ Search.propTypes = {
|
||||
sx: PropTypes.object,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
startAdornment: PropTypes.object,
|
||||
endAdornment: PropTypes.object,
|
||||
onBlur: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
|
||||
import DeleteIcon from "../../../../assets/icons/trash-bin.svg?react";
|
||||
|
||||
export const HttpAdornment = ({ https }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
@@ -23,7 +25,7 @@ export const HttpAdornment = ({ https }) => {
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
sx={{ lineHeight: 1, opacity: 0.8 }}
|
||||
>
|
||||
{https ? "https" : "http"}://
|
||||
{https ? "https" : "http"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -31,6 +33,7 @@ export const HttpAdornment = ({ https }) => {
|
||||
|
||||
HttpAdornment.propTypes = {
|
||||
https: PropTypes.bool.isRequired,
|
||||
prefix: PropTypes.string,
|
||||
};
|
||||
|
||||
export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
|
||||
|
||||
@@ -93,6 +93,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
<Typography
|
||||
component="h2"
|
||||
mb={theme.spacing(1.5)}
|
||||
sx={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{error ? error : label}
|
||||
</Typography>
|
||||
@@ -168,7 +169,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
|
||||
ProgressUpload.propTypes = {
|
||||
icon: PropTypes.element, // JSX element for the icon (optional)
|
||||
label: PropTypes.string.isRequired, // Label text for the progress item
|
||||
label: PropTypes.string, // Label text for the progress item
|
||||
size: PropTypes.string.isRequired, // Size information for the progress item
|
||||
progress: PropTypes.number.isRequired, // Current progress value (0-100)
|
||||
onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* @param {Object} props - The props passed to the ProtectedDistributedUptimeRoute component.
|
||||
* @param {React.ReactNode} props.children - The children to render if the user is authenticated.
|
||||
* @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
|
||||
*/
|
||||
|
||||
const ProtectedDistributedUptimeRoute = ({ children }) => {
|
||||
const distributedUptimeEnabled = useSelector(
|
||||
(state) => state.ui.distributedUptimeEnabled
|
||||
);
|
||||
|
||||
return distributedUptimeEnabled === true ? (
|
||||
children
|
||||
) : (
|
||||
<Navigate
|
||||
to="/uptime"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProtectedDistributedUptimeRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default ProtectedDistributedUptimeRoute;
|
||||
@@ -15,12 +15,6 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { toggleSidebar } from "../../Features/UI/uiSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import ThemeSwitch from "../ThemeSwitch";
|
||||
import Avatar from "../Avatar";
|
||||
import LockSvg from "../../assets/icons/lock.svg?react";
|
||||
@@ -43,16 +37,31 @@ import DotsVertical from "../../assets/icons/dots-vertical.svg?react";
|
||||
import ChangeLog from "../../assets/icons/changeLog.svg?react";
|
||||
import Docs from "../../assets/icons/docs.svg?react";
|
||||
import Folder from "../../assets/icons/folder.svg?react";
|
||||
import StatusPages from "../../assets/icons/status-pages.svg?react";
|
||||
import ChatBubbleOutlineRoundedIcon from "@mui/icons-material/ChatBubbleOutlineRounded";
|
||||
|
||||
import DistributedUptimeIcon from "../../assets/icons/distributed-uptime.svg?react";
|
||||
import "./index.css";
|
||||
|
||||
// Utils
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { toggleSidebar } from "../../Features/UI/uiSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
|
||||
const menu = [
|
||||
{ name: "Uptime", path: "uptime", icon: <Monitors /> },
|
||||
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
|
||||
{ name: "Infrastructure", path: "infrastructure", icon: <Integrations /> },
|
||||
{
|
||||
name: "Distributed uptime",
|
||||
path: "distributed-uptime",
|
||||
icon: <DistributedUptimeIcon />,
|
||||
},
|
||||
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
|
||||
// { name: "Status pages", path: "status", icon: <StatusPages /> },
|
||||
|
||||
{ name: "Status pages", path: "status", icon: <StatusPages /> },
|
||||
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
|
||||
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
|
||||
{
|
||||
@@ -97,6 +106,7 @@ const PATH_MAP = {
|
||||
monitors: "Dashboard",
|
||||
pagespeed: "Dashboard",
|
||||
infrastructure: "Dashboard",
|
||||
["distributed-uptime"]: "Dashboard",
|
||||
account: "Account",
|
||||
settings: "Settings",
|
||||
};
|
||||
@@ -119,6 +129,9 @@ function Sidebar() {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [popup, setPopup] = useState();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const distributedUptimeEnabled = useSelector(
|
||||
(state) => state.ui.distributedUptimeEnabled
|
||||
);
|
||||
|
||||
const accountMenuItem = menu.find((item) => item.name === "Account");
|
||||
if (user.role?.includes("demo") && accountMenuItem) {
|
||||
@@ -313,8 +326,11 @@ function Sidebar() {
|
||||
/* overflow: "hidden", */
|
||||
}}
|
||||
>
|
||||
{menu.map((item) =>
|
||||
item.path ? (
|
||||
{menu.map((item) => {
|
||||
if (item.path === "distributed-uptime" && distributedUptimeEnabled === false) {
|
||||
return null;
|
||||
}
|
||||
return item.path ? (
|
||||
/* If item has a path */
|
||||
<Tooltip
|
||||
key={item.path}
|
||||
@@ -335,15 +351,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>
|
||||
@@ -549,8 +563,8 @@ function Sidebar() {
|
||||
</List>
|
||||
</Collapse>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider sx={{ mt: "auto" }} />
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialMode = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? "dark" : "light";
|
||||
const initialMode = window?.matchMedia?.("(prefers-color-scheme: dark)")?.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
// Initial state for UI settings.
|
||||
// Add more settings as needed (e.g., theme preferences, user settings)
|
||||
@@ -20,12 +22,16 @@ const initialState = {
|
||||
mode: initialMode,
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
timezone: "America/Toronto",
|
||||
distributedUptimeEnabled: false,
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
name: "ui",
|
||||
initialState,
|
||||
reducers: {
|
||||
setDistributedUptimeEnabled: (state, action) => {
|
||||
state.distributedUptimeEnabled = action.payload;
|
||||
},
|
||||
setRowsPerPage: (state, action) => {
|
||||
const { table, value } = action.payload;
|
||||
if (state[table]) {
|
||||
@@ -49,5 +55,11 @@ const uiSlice = createSlice({
|
||||
});
|
||||
|
||||
export default uiSlice.reducer;
|
||||
export const { setRowsPerPage, toggleSidebar, setMode, setGreeting, setTimezone } =
|
||||
uiSlice.actions;
|
||||
export const {
|
||||
setRowsPerPage,
|
||||
toggleSidebar,
|
||||
setMode,
|
||||
setGreeting,
|
||||
setTimezone,
|
||||
setDistributedUptimeEnabled,
|
||||
} = uiSlice.actions;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
const useMonitorUtils = () => {
|
||||
const getMonitorWithPercentage = useCallback((monitor, theme) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = "";
|
||||
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.error.main
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.warning.main
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.success.main
|
||||
: theme.palette.success.main;
|
||||
}
|
||||
|
||||
return {
|
||||
id: monitor._id,
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
monitor: monitor,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const determineState = useCallback((monitor) => {
|
||||
if (typeof monitor === "undefined") return "pending";
|
||||
if (monitor.isActive === false) return "paused";
|
||||
if (monitor?.status === undefined) return "pending";
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
}, []);
|
||||
|
||||
return { getMonitorWithPercentage, determineState };
|
||||
};
|
||||
|
||||
export { useMonitorUtils };
|
||||
@@ -73,11 +73,6 @@ const Account = ({ open = "profile" }) => {
|
||||
<TabList
|
||||
onChange={handleTabChange}
|
||||
aria-label="account tabs"
|
||||
sx={{
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: theme.palette.tertiary.contrastText,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{tabList.map((label, index) => (
|
||||
<Tab
|
||||
@@ -87,33 +82,6 @@ const Account = ({ open = "profile" }) => {
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => handleFocus(label.toLowerCase())}
|
||||
tabIndex={index}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.tertiary.contrastText,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
textTransform: "none",
|
||||
minWidth: "fit-content",
|
||||
paddingY: theme.spacing(6),
|
||||
fontWeight: 400,
|
||||
borderBottom: "2px solid transparent",
|
||||
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
"&:first-child": { borderTopLeftRadius: "8px" },
|
||||
"&:last-child": { borderTopRightRadius: "8px", borderRight: 0 },
|
||||
"&:focus-visible": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
borderColor: theme.palette.tertiary.contrastText,
|
||||
borderRightColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: theme.palette.secondary.contrastText,
|
||||
borderColor: theme.palette.secondary.contrastText,
|
||||
borderRightColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+169
@@ -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"
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import baseStyle from "./DistributedUptimeMapStyle.json";
|
||||
|
||||
const buildStyle = (theme, mode) => {
|
||||
const style = JSON.parse(JSON.stringify(baseStyle));
|
||||
|
||||
if (mode === "dark") {
|
||||
return baseStyle;
|
||||
}
|
||||
|
||||
if (style.layers) {
|
||||
const newLayers = style.layers.map((layer) => {
|
||||
if (layer.id === "background") {
|
||||
layer.paint["background-color"] = theme.palette.map.main;
|
||||
}
|
||||
|
||||
if (layer.id === "countries-fill") {
|
||||
layer.paint["fill-color"] = theme.palette.map.lowContrast;
|
||||
}
|
||||
|
||||
if (layer.id === "coastline") {
|
||||
layer.paint["line-color"] = theme.palette.map.highContrast;
|
||||
}
|
||||
if (layer.id === "countries-boundary") {
|
||||
layer.paint["line-color"] = theme.palette.map.highContrast;
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
style.layers = newLayers;
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
export default buildStyle;
|
||||
@@ -0,0 +1,102 @@
|
||||
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 maplibregl from "maplibre-gl";
|
||||
import { useSelector } from "react-redux";
|
||||
import buildStyle from "./buildStyle";
|
||||
|
||||
const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => {
|
||||
const mapContainer = useRef(null);
|
||||
const map = useRef(null);
|
||||
const theme = useTheme();
|
||||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const initialTheme = useRef(theme);
|
||||
const initialMode = useRef(mode);
|
||||
|
||||
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) {
|
||||
const initialStyle = buildStyle(initialTheme.current, initialMode.current);
|
||||
map.current = new maplibregl.Map({
|
||||
container: mapContainer.current,
|
||||
style: initialStyle,
|
||||
center: [0, 20],
|
||||
zoom: 0.8,
|
||||
attributionControl: false,
|
||||
});
|
||||
}
|
||||
map.current.on("load", () => {
|
||||
setMapLoaded(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (map.current) {
|
||||
map.current.remove();
|
||||
map.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const style = buildStyle(theme, mode);
|
||||
if (map.current && mapLoaded) {
|
||||
map.current.setStyle(style);
|
||||
}
|
||||
}, [theme, mode, mapLoaded]);
|
||||
|
||||
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;
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
} from "recharts";
|
||||
import ChartBox from "../../../../../Components/Charts/ChartBox";
|
||||
import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
|
||||
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 (
|
||||
<ChartBox
|
||||
icon={<ResponseTimeIcon />}
|
||||
header="Response Times"
|
||||
sx={{ padding: 0 }}
|
||||
>
|
||||
<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>
|
||||
</ChartBox>
|
||||
);
|
||||
}
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorHeader = ({ monitor }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack direction="row">
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography variant="h1">{monitor.name}</Typography>
|
||||
<Typography variant="h2">
|
||||
Distributed Uptime Monitoring powered by DePIN
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorHeader.propTypes = {
|
||||
monitor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MonitorHeader;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,62 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import StatBox from "../../../../../Components/StatBox";
|
||||
import LastUpdate from "../LastUpdate";
|
||||
import UptLogo from "../../../../../assets/icons/upt_logo.png";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const StatBoxes = ({ monitor, lastUpdateTrigger }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
StatBoxes.propTypes = {
|
||||
monitor: PropTypes.object,
|
||||
lastUpdateTrigger: PropTypes.number,
|
||||
};
|
||||
|
||||
export default StatBoxes;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const 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,
|
||||
};
|
||||
};
|
||||
const useSubscribeToDetails = ({ monitorId, dateRange }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [connectionStatus, setConnectionStatus] = useState(undefined);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
const [lastUpdateTrigger, setLastUpdateTrigger] = useState(0);
|
||||
const [devices, setDevices] = useState(Array.from({ length: 5 }, getRandomDevice));
|
||||
const authToken = useSelector((state) => state.auth.token);
|
||||
|
||||
const prevDateRangeRef = useRef(dateRange);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const cleanup = networkService.subscribeToDistributedUptimeDetails({
|
||||
authToken,
|
||||
monitorId,
|
||||
dateRange: dateRange,
|
||||
normalize: true,
|
||||
onUpdate: (data) => {
|
||||
if (networkError === true) {
|
||||
setNetworkError(false);
|
||||
}
|
||||
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: () => {
|
||||
setNetworkError(true);
|
||||
setConnectionStatus("down");
|
||||
},
|
||||
});
|
||||
return cleanup;
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [
|
||||
authToken,
|
||||
dateRange,
|
||||
monitorId,
|
||||
retryCount,
|
||||
setConnectionStatus,
|
||||
networkError,
|
||||
devices,
|
||||
]);
|
||||
|
||||
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]);
|
||||
|
||||
return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger];
|
||||
};
|
||||
|
||||
export default useSubscribeToDetails;
|
||||
@@ -0,0 +1,103 @@
|
||||
//Components
|
||||
import DistributedUptimeMap from "./Components/DistributedUptimeMap";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import DeviceTicker from "./Components/DeviceTicker";
|
||||
import DistributedUptimeResponseChart from "./Components/DistributedUptimeResponseChart";
|
||||
import NextExpectedCheck from "./Components/NextExpectedCheck";
|
||||
import Footer from "./Components/Footer";
|
||||
import StatBoxes from "./Components/StatBoxes";
|
||||
import MonitorHeader from "./Components/MonitorHeader";
|
||||
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useSubscribeToDetails from "./Hooks/useSubscribeToDetails";
|
||||
|
||||
const DistributedUptimeDetails = () => {
|
||||
const { monitorId } = useParams();
|
||||
|
||||
// Local State
|
||||
const [dateRange, setDateRange] = useState("day");
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
|
||||
useSubscribeToDetails({ monitorId, dateRange });
|
||||
// Constants
|
||||
const BREADCRUMBS = [
|
||||
{ name: "Distributed Uptime", path: "/distributed-uptime" },
|
||||
{ name: "Details", path: `/distributed-uptime/${monitorId}` },
|
||||
];
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
Network error
|
||||
</Typography>
|
||||
<Typography>Please check your connection</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof monitor === "undefined" || monitor.totalChecks === 0) {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<GenericFallback>
|
||||
<Typography>There is no check history for this monitor yet.</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorHeader monitor={monitor} />
|
||||
<StatBoxes
|
||||
monitor={monitor}
|
||||
lastUpdateTrigger={lastUpdateTrigger}
|
||||
/>
|
||||
<NextExpectedCheck
|
||||
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
|
||||
interval={monitor?.interval ?? 0}
|
||||
trigger={lastUpdateTrigger}
|
||||
/>
|
||||
<MonitorTimeFrameHeader
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
|
||||
<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;
|
||||
@@ -0,0 +1,94 @@
|
||||
// Components
|
||||
import DataTable from "../../../../../Components/Table";
|
||||
import BarChart from "../../../../../Components/Charts/BarChart";
|
||||
import { Box } from "@mui/material";
|
||||
import { StatusLabel } from "../../../../../Components/Label";
|
||||
import Host from "../../../../../Components/Host";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
// Constants
|
||||
const TYPE_MAP = {
|
||||
distributed_http: "Distributed HTTP",
|
||||
};
|
||||
|
||||
const MonitorTable = ({ isLoading, monitors }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { determineState } = useMonitorUtils();
|
||||
|
||||
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) => <span>{"TODO"}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
shouldRender={!isLoading}
|
||||
headers={headers}
|
||||
data={monitors}
|
||||
config={{
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/distributed-uptime/${row.id}`);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorTable.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitors: PropTypes.array,
|
||||
};
|
||||
|
||||
export default MonitorTable;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
|
||||
const useSubscribeToMonitors = () => {
|
||||
// Redux
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
|
||||
// Local state
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
|
||||
const [filteredMonitors, setFilteredMonitors] = useState(undefined);
|
||||
|
||||
const theme = useTheme();
|
||||
const { getMonitorWithPercentage } = useMonitorUtils();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
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);
|
||||
setMonitorsSummary(summary);
|
||||
setFilteredMonitors(mappedMonitors);
|
||||
},
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authToken, user, getMonitorWithPercentage, theme]);
|
||||
return [isLoading, networkError, monitors, monitorsSummary, filteredMonitors];
|
||||
};
|
||||
export { useSubscribeToMonitors };
|
||||
@@ -0,0 +1,71 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
|
||||
import MonitorTable from "./Components/MonitorTable";
|
||||
import Fallback from "../../../Components/Fallback";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useSubscribeToMonitors } from "./Hooks/useSubscribeToMonitors";
|
||||
// Constants
|
||||
const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" }];
|
||||
|
||||
const DistributedUptimeMonitors = () => {
|
||||
// Local state
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const [isLoading, networkError, monitors, monitorsSummary, filteredMonitors] =
|
||||
useSubscribeToMonitors();
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
Network error
|
||||
</Typography>
|
||||
<Typography>Please check your connection</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof monitorsSummary !== "undefined" && monitorsSummary.totalMonitors === 0) {
|
||||
return (
|
||||
<Fallback
|
||||
vowelStart={false}
|
||||
title="distributed uptime monitor"
|
||||
checks={[
|
||||
"Check if a server is online from multiple locations",
|
||||
"Detect outages and performance issues in real time",
|
||||
"Reduce false alarms by verifying downtime from different networks",
|
||||
"Provide insights on regional availability and latency",
|
||||
]}
|
||||
link="/distributed-uptime/create"
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<CreateMonitorHeader
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!isLoading}
|
||||
path="/distributed-uptime/create"
|
||||
/>
|
||||
<MonitorTable
|
||||
isLoading={isLoading}
|
||||
monitors={filteredMonitors}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistributedUptimeMonitors;
|
||||
@@ -1,6 +1,6 @@
|
||||
// Components
|
||||
import DataTable from "../../../../../Components/Table";
|
||||
import Host from "../../../../Uptime/Monitors/Components/Host";
|
||||
import Host from "../../../../../Components/Host";
|
||||
import { StatusLabel } from "../../../../../Components/Label";
|
||||
import { Stack } from "@mui/material";
|
||||
import { InfrastructureMenu } from "../MonitorsTableMenu";
|
||||
@@ -12,6 +12,7 @@ import CustomGauge from "../../../../../Components/Charts/CustomGauge";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete }) => {
|
||||
// Utils
|
||||
@@ -131,4 +132,11 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
|
||||
);
|
||||
};
|
||||
|
||||
MonitorsTable.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
monitors: PropTypes.array,
|
||||
isAdmin: PropTypes.bool,
|
||||
handleActionMenuDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MonitorsTable;
|
||||
@@ -181,7 +181,7 @@ const CreateMaintenance = () => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleSelectMonitors = (monitors) => {
|
||||
const handleSelectMonitors = (_, monitors) => {
|
||||
setForm({ ...form, monitors });
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ monitors },
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
.settings a.MuiTypography-root,
|
||||
.settings h2.MuiTypography-root,
|
||||
.settings span.MuiTypography-root,
|
||||
.settings button,
|
||||
.settings p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.settings h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
.settings h1.MuiTypography-root,
|
||||
.settings h2.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.settings button {
|
||||
min-height: 34px;
|
||||
}
|
||||
.settings span.MuiTypography-root {
|
||||
opacity: 0.6;
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
// Components
|
||||
import { Box, Stack, Typography, Button, Switch } from "@mui/material";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Link from "../../Components/Link";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { logger } from "../../Utils/Logger";
|
||||
import "./index.css";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import {
|
||||
@@ -14,15 +20,17 @@ import {
|
||||
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { update } from "../../Features/Auth/authSlice";
|
||||
import PropTypes from "prop-types";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { setTimezone, setMode } from "../../Features/UI/uiSlice";
|
||||
import {
|
||||
setTimezone,
|
||||
setMode,
|
||||
setDistributedUptimeEnabled,
|
||||
} from "../../Features/UI/uiSlice";
|
||||
import timezones from "../../Utils/timezones.json";
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../../main";
|
||||
import { settingsValidation } from "../../Validation/validation";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
|
||||
// Constants
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
|
||||
const Settings = () => {
|
||||
@@ -32,10 +40,11 @@ const Settings = () => {
|
||||
const { checkTTL } = user;
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
|
||||
const { timezone } = useSelector((state) => state.ui);
|
||||
const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui);
|
||||
const { mode } = useSelector((state) => state.ui);
|
||||
const [checksIsLoading, setChecksIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
enableDistributedUptime: distributedUptimeEnabled,
|
||||
ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
|
||||
});
|
||||
const [version, setVersion] = useState("unknown");
|
||||
@@ -64,7 +73,16 @@ const Settings = () => {
|
||||
}, []);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const { type, checked, value, id } = event.target;
|
||||
|
||||
if (type === "checkbox") {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[id]: checked,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = settingsValidation.validate(
|
||||
{ [id]: value },
|
||||
{
|
||||
@@ -79,7 +97,6 @@ const Settings = () => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
console.log(newErrors);
|
||||
logger.error("Validation errors:", error.details);
|
||||
}
|
||||
let inputValue = value;
|
||||
@@ -100,6 +117,7 @@ const Settings = () => {
|
||||
});
|
||||
const updatedUser = { ...user, checkTTL: form.ttl };
|
||||
const action = await dispatch(update({ authToken, localData: updatedUser }));
|
||||
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: "Settings saved successfully",
|
||||
@@ -197,7 +215,7 @@ const Settings = () => {
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
id="display-timezone"
|
||||
id="display-timezones"
|
||||
label="Display timezone"
|
||||
value={timezone}
|
||||
onChange={(e) => {
|
||||
@@ -229,6 +247,27 @@ const Settings = () => {
|
||||
></Select>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Distributed uptime</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
|
||||
Enable/disable distributed uptime monitoring.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Switch
|
||||
id="enableDistributedUptime"
|
||||
color="accent"
|
||||
checked={distributedUptimeEnabled}
|
||||
onChange={(e) => {
|
||||
dispatch(setDistributedUptimeEnabled(e.target.checked));
|
||||
}}
|
||||
/>
|
||||
{distributedUptimeEnabled === true ? "Enabled" : "Disabled"}
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
|
||||
Whitespace-only changes.
@@ -1,35 +0,0 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
|
||||
const Status = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="status"
|
||||
sx={{
|
||||
':has(> [class*="fallback__"])': {
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderStyle: "dashed",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
overflow: "hidden",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Fallback
|
||||
title="status page"
|
||||
checks={[
|
||||
"Share your uptime publicly",
|
||||
"Keep your users informed about incidents",
|
||||
"Build trust with your customers",
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Status;
|
||||
@@ -0,0 +1,106 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
|
||||
// Utils
|
||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||
import { useTheme } from "@emotion/react";
|
||||
const MonitorListItem = ({
|
||||
monitor,
|
||||
innerRef,
|
||||
draggableProps,
|
||||
dragHandleProps,
|
||||
onDelete,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
ref={innerRef}
|
||||
gap={theme.spacing(4)}
|
||||
margin={theme.spacing(4)}
|
||||
padding={theme.spacing(4)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
alignItems={"center"}
|
||||
justifyContent={"start"}
|
||||
border={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
>
|
||||
<ReorderRoundedIcon />
|
||||
<Typography>{monitor.name}</Typography>
|
||||
<DeleteIcon
|
||||
sx={{ marginLeft: "auto" }}
|
||||
onClick={() => {
|
||||
onDelete(monitor);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
|
||||
const onDelete = (monitorToDelete) => {
|
||||
const newMonitors = selectedMonitors.filter(
|
||||
(monitor) => monitor._id !== monitorToDelete._id
|
||||
);
|
||||
setSelectedMonitors(newMonitors);
|
||||
};
|
||||
const reorder = (list, startIndex, endIndex) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const onDragEnd = (result) => {
|
||||
// dropped outside the list
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedMonitors = reorder(
|
||||
selectedMonitors,
|
||||
result.source.index,
|
||||
result.destination.index
|
||||
);
|
||||
|
||||
setSelectedMonitors(reorderedMonitors);
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided, snapshot) => (
|
||||
<Stack
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{selectedMonitors?.map((monitor, index) => (
|
||||
<Draggable
|
||||
key={monitor._id}
|
||||
draggableId={monitor._id}
|
||||
index={index}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<MonitorListItem
|
||||
monitor={monitor}
|
||||
innerRef={provided.innerRef}
|
||||
draggableProps={provided.draggableProps}
|
||||
dragHandleProps={provided.dragHandleProps}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</Stack>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorList;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Button, Box } from "@mui/material";
|
||||
import ProgressUpload from "../../../../../Components/ProgressBars";
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
|
||||
import { formatBytes } from "../../../../../Utils/fileUtils";
|
||||
const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
label={logo?.name}
|
||||
size={formatBytes(logo?.size)}
|
||||
progress={progressValue}
|
||||
onClick={removeLogo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (logo && logoType) {
|
||||
return (
|
||||
<Box
|
||||
width="fit-content"
|
||||
alignSelf="center"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={removeLogo}
|
||||
>
|
||||
Remove Logo
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Progress;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,104 @@
|
||||
// Components
|
||||
import { Stack, Typography, Button } from "@mui/material";
|
||||
import { TabPanel } from "@mui/lab";
|
||||
import ConfigBox from "../../../../../Components/ConfigBox";
|
||||
import MonitorList from "../MonitorList";
|
||||
import Search from "../../../../../Components/Inputs/Search";
|
||||
import Checkbox from "../../../../../Components/Inputs/Checkbox";
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
const Content = ({
|
||||
tabValue,
|
||||
form,
|
||||
monitors,
|
||||
handleFormChange,
|
||||
errors,
|
||||
selectedMonitors,
|
||||
setSelectedMonitors,
|
||||
}) => {
|
||||
// Local state
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Handlers
|
||||
const handleMonitorsChange = (selectedMonitors) => {
|
||||
handleFormChange({
|
||||
target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor._id) },
|
||||
});
|
||||
setSelectedMonitors(selectedMonitors);
|
||||
};
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<TabPanel value={tabValue}>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Status page servers</Typography>
|
||||
<Typography component="p">
|
||||
You can add any number of servers that you monitor to your status page. You
|
||||
can also reorder them for the best viewing experience.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Search
|
||||
options={monitors}
|
||||
multiple={true}
|
||||
filteredBy="name"
|
||||
value={selectedMonitors}
|
||||
inputValue={search}
|
||||
handleInputChange={setSearch}
|
||||
handleChange={handleMonitorsChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{errors["monitors"]}
|
||||
</Typography>
|
||||
<MonitorList
|
||||
selectedMonitors={selectedMonitors}
|
||||
setSelectedMonitors={handleMonitorsChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>{" "}
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Features</Typography>
|
||||
<Typography component="p">Show more details on the status page</Typography>
|
||||
</Stack>
|
||||
<Stack sx={{ margin: theme.spacing(6) }}>
|
||||
<Checkbox
|
||||
id="showCharts"
|
||||
name="showCharts"
|
||||
label={`Show charts`}
|
||||
isChecked={form.showCharts}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
<Checkbox
|
||||
id="showUptimePercentage"
|
||||
name="showUptimePercentage"
|
||||
label={`Show uptime percentage`}
|
||||
isChecked={form.showUptimePercentage}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(6)}></Stack>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export default Content;
|
||||
@@ -0,0 +1,140 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { TabPanel } from "@mui/lab";
|
||||
import ConfigBox from "../../../../../Components/ConfigBox";
|
||||
import Checkbox from "../../../../../Components/Inputs/Checkbox";
|
||||
import TextInput from "../../../../../Components/Inputs/TextInput";
|
||||
import Select from "../../../../../Components/Inputs/Select";
|
||||
import ImageField from "../../../../../Components/Inputs/Image";
|
||||
import ColorPicker from "../../../../../Components/Inputs/ColorPicker";
|
||||
import Progress from "../Progress";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import timezones from "../../../../../Utils/timezones.json";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const TabSettings = ({
|
||||
tabValue,
|
||||
form,
|
||||
handleFormChange,
|
||||
handleImageChange,
|
||||
progress,
|
||||
removeLogo,
|
||||
errors,
|
||||
}) => {
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<TabPanel value={tabValue}>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<ConfigBox>
|
||||
<Stack>
|
||||
<Typography component="h2">Access</Typography>
|
||||
<Typography component="p">
|
||||
If your status page is ready, you can mark it as published.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(18)}>
|
||||
<Checkbox
|
||||
id="publish"
|
||||
name="isPublished"
|
||||
label={`Published and visible to the public`}
|
||||
isChecked={form.isPublished}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Basic Information</Typography>
|
||||
<Typography component="p">
|
||||
Define company name and the subdomain that your status page points to.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(18)}>
|
||||
<TextInput
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
type="text"
|
||||
label="Company name"
|
||||
value={form.companyName}
|
||||
onChange={handleFormChange}
|
||||
helperText={errors["companyName"]}
|
||||
error={errors["companyName"] ? true : false}
|
||||
/>
|
||||
<TextInput
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
label="Your status page address"
|
||||
disabled
|
||||
value={form.url}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Timezone</Typography>
|
||||
<Typography component="p">
|
||||
Select the timezone that your status page will be displayed in.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Select
|
||||
id="timezone"
|
||||
name="timezone"
|
||||
label="Display timezone"
|
||||
items={timezones}
|
||||
value={form.timezone}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Appearance</Typography>
|
||||
<Typography component="p">
|
||||
Define the default look and feel of your public status page.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<ImageField
|
||||
id="logo"
|
||||
src={form?.logo?.src}
|
||||
isRound={false}
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
<Progress
|
||||
isLoading={progress.isLoading}
|
||||
progressValue={progress.value}
|
||||
logo={form.logo}
|
||||
logoType={form.logo?.type}
|
||||
removeLogo={removeLogo}
|
||||
/>
|
||||
<ColorPicker
|
||||
id="color"
|
||||
name="color"
|
||||
value={form.color}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
TabSettings.propTypes = {
|
||||
tabValue: PropTypes.string,
|
||||
form: PropTypes.object,
|
||||
handleFormChange: PropTypes.func,
|
||||
handleImageChange: PropTypes.func,
|
||||
progress: PropTypes.object,
|
||||
removeLogo: PropTypes.func,
|
||||
errors: PropTypes.object,
|
||||
};
|
||||
|
||||
export default TabSettings;
|
||||
@@ -0,0 +1,81 @@
|
||||
// Components
|
||||
import { TabContext, TabList } from "@mui/lab";
|
||||
import { Tab } from "@mui/material";
|
||||
import Settings from "./Settings";
|
||||
import Content from "./Content";
|
||||
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Tabs = ({
|
||||
form,
|
||||
errors,
|
||||
monitors,
|
||||
selectedMonitors,
|
||||
setSelectedMonitors,
|
||||
handleFormChange,
|
||||
handleImageChange,
|
||||
progress,
|
||||
removeLogo,
|
||||
tab,
|
||||
setTab,
|
||||
TAB_LIST,
|
||||
}) => {
|
||||
return (
|
||||
<TabContext value={TAB_LIST[tab]}>
|
||||
<TabList
|
||||
onChange={(_, tab) => {
|
||||
setTab(TAB_LIST.indexOf(tab));
|
||||
}}
|
||||
>
|
||||
{TAB_LIST.map((tab, idx) => {
|
||||
return (
|
||||
<Tab
|
||||
key={tab}
|
||||
label={TAB_LIST[idx]}
|
||||
value={TAB_LIST[idx]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
{tab === 0 ? (
|
||||
<Settings
|
||||
tabValue={TAB_LIST[0]}
|
||||
form={form}
|
||||
handleFormChange={handleFormChange}
|
||||
handleImageChange={handleImageChange}
|
||||
progress={progress}
|
||||
removeLogo={removeLogo}
|
||||
errors={errors}
|
||||
/>
|
||||
) : (
|
||||
<Content
|
||||
tabValue={TAB_LIST[1]}
|
||||
form={form}
|
||||
monitors={monitors}
|
||||
handleFormChange={handleFormChange}
|
||||
errors={errors}
|
||||
selectedMonitors={selectedMonitors}
|
||||
setSelectedMonitors={setSelectedMonitors}
|
||||
/>
|
||||
)}
|
||||
</TabContext>
|
||||
);
|
||||
};
|
||||
|
||||
Tabs.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
monitors: PropTypes.array,
|
||||
selectedMonitors: PropTypes.array,
|
||||
setSelectedMonitors: PropTypes.func,
|
||||
handleFormChange: PropTypes.func,
|
||||
handleImageChange: PropTypes.func,
|
||||
progress: PropTypes.object,
|
||||
removeLogo: PropTypes.func,
|
||||
tab: PropTypes.number,
|
||||
setTab: PropTypes.func,
|
||||
TAB_LIST: PropTypes.array,
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
|
||||
const useCreateStatusPage = (isCreate) => {
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const createStatusPage = async ({ form }) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await networkService.createStatusPage({ authToken, user, form, isCreate });
|
||||
return true;
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error?.response?.data?.msg ?? error.message });
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [createStatusPage, isLoading, networkError];
|
||||
};
|
||||
|
||||
export { useCreateStatusPage };
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
|
||||
const useMonitorsFetch = () => {
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
const response = await networkService.getMonitorsByTeamId({
|
||||
authToken: authToken,
|
||||
teamId: user.teamId,
|
||||
limit: null, // donot return any checks for the monitors
|
||||
types: ["http"], // status page is available only for the uptime type
|
||||
});
|
||||
setMonitors(response.data.data.monitors);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [authToken, user]);
|
||||
|
||||
return [monitors, isLoading, networkError];
|
||||
};
|
||||
|
||||
export { useMonitorsFetch };
|
||||
@@ -0,0 +1,238 @@
|
||||
// Components
|
||||
import { Stack, Button, Typography } from "@mui/material";
|
||||
import Tabs from "./Components/Tabs";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
import SkeletonLayout from "./Components/Skeleton";
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { statusPageValidation } from "../../../Validation/validation";
|
||||
import { buildErrors } from "../../../Validation/error";
|
||||
import { useMonitorsFetch } from "./Hooks/useMonitorsFetch";
|
||||
import { useCreateStatusPage } from "./Hooks/useCreateStatusPage";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch";
|
||||
|
||||
//Constants
|
||||
const TAB_LIST = ["General settings", "Contents"];
|
||||
|
||||
const ERROR_TAB_MAPPING = [
|
||||
["companyName", "url", "timezone", "color", "isPublished", "logo"],
|
||||
["monitors", "showUptimePercentage", "showCharts"],
|
||||
];
|
||||
|
||||
const CreateStatusPage = () => {
|
||||
//Local state
|
||||
const [tab, setTab] = useState(0);
|
||||
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
||||
const [form, setForm] = useState({
|
||||
isPublished: false,
|
||||
companyName: "",
|
||||
url: "/status/public",
|
||||
logo: undefined,
|
||||
timezone: "America/Toronto",
|
||||
color: "#4169E1",
|
||||
monitors: [],
|
||||
showCharts: true,
|
||||
showUptimePercentage: true,
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [selectedMonitors, setSelectedMonitors] = useState([]);
|
||||
// Refs
|
||||
const intervalRef = useRef(null);
|
||||
|
||||
// Setup
|
||||
const location = useLocation();
|
||||
const isCreate = location.pathname === "/status/create";
|
||||
|
||||
//Utils
|
||||
const theme = useTheme();
|
||||
const [monitors, isLoading, networkError] = useMonitorsFetch();
|
||||
const [createStatusPage, createStatusIsLoading, createStatusPageNetworkError] =
|
||||
useCreateStatusPage(isCreate);
|
||||
const navigate = useNavigate();
|
||||
const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] =
|
||||
useStatusPageFetch(isCreate);
|
||||
|
||||
// Handlers
|
||||
const handleFormChange = (e) => {
|
||||
let { type, name, value, checked } = e.target;
|
||||
// Handle errors
|
||||
const { error } = statusPageValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, name, error);
|
||||
});
|
||||
|
||||
//Handle checkbox
|
||||
if (type === "checkbox") {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: checked,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle other inputs
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleImageChange = useCallback((event) => {
|
||||
const img = event.target?.files?.[0];
|
||||
const newLogo = {
|
||||
src: URL.createObjectURL(img),
|
||||
name: img.name,
|
||||
type: img.type,
|
||||
size: img.size,
|
||||
};
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
logo: newLogo,
|
||||
}));
|
||||
intervalRef.current = setInterval(() => {
|
||||
const buffer = 12;
|
||||
setProgress((prev) => {
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { ...prev, value: prev.value + buffer };
|
||||
});
|
||||
}, 120);
|
||||
}, []);
|
||||
|
||||
const removeLogo = () => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
logo: undefined,
|
||||
}));
|
||||
// interrupt interval if image upload is canceled prior to completing the process
|
||||
clearInterval(intervalRef.current);
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
let toSubmit = {
|
||||
...form,
|
||||
logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null },
|
||||
};
|
||||
const { error } = statusPageValidation.validate(toSubmit, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (typeof error === "undefined") {
|
||||
const success = await createStatusPage({ form });
|
||||
if (success) {
|
||||
createToast({ body: "Status page created successfully" });
|
||||
navigate("/status");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const newErrors = {};
|
||||
error?.details?.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors((prev) => ({ ...prev, ...newErrors }));
|
||||
|
||||
const errorTabs = Object.keys(newErrors).map((err) => {
|
||||
return ERROR_TAB_MAPPING.findIndex((tab) => tab.includes(err));
|
||||
});
|
||||
|
||||
// If there's an error in the current tab, don't change the tab
|
||||
if (errorTabs.some((errorTab) => errorTab === tab)) {
|
||||
return;
|
||||
}
|
||||
// Otherwise go to tab with error
|
||||
setTab(errorTabs[0]);
|
||||
};
|
||||
|
||||
// If we are configuring, populate fields
|
||||
useEffect(() => {
|
||||
if (isCreate) return;
|
||||
if (typeof statusPage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
let newLogo = undefined;
|
||||
if (statusPage.logo) {
|
||||
newLogo = {
|
||||
src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`,
|
||||
name: "logo",
|
||||
type: statusPage.logo.contentType,
|
||||
size: null,
|
||||
};
|
||||
}
|
||||
|
||||
setForm((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
companyName: statusPage?.companyName,
|
||||
isPublished: statusPage?.isPublished,
|
||||
timezone: statusPage?.timezone,
|
||||
monitors: statusPageMonitors.map((monitor) => monitor._id),
|
||||
color: statusPage?.color,
|
||||
logo: newLogo,
|
||||
};
|
||||
});
|
||||
setSelectedMonitors(statusPageMonitors);
|
||||
}, [isCreate, statusPage, statusPageMonitors]);
|
||||
|
||||
if (networkError === true) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
Network error
|
||||
</Typography>
|
||||
<Typography>Please check your connection</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) return <SkeletonLayout />;
|
||||
|
||||
// Load fields
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Tabs
|
||||
form={form}
|
||||
errors={errors}
|
||||
monitors={monitors}
|
||||
selectedMonitors={selectedMonitors}
|
||||
setSelectedMonitors={setSelectedMonitors}
|
||||
handleFormChange={handleFormChange}
|
||||
handleImageChange={handleImageChange}
|
||||
progress={progress}
|
||||
removeLogo={removeLogo}
|
||||
tab={tab}
|
||||
setTab={setTab}
|
||||
TAB_LIST={TAB_LIST}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStatusPage;
|
||||
@@ -0,0 +1,34 @@
|
||||
// Components
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const AdminLink = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
Administrator?
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.accent.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => navigate("/login")}
|
||||
>
|
||||
Login here
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLink;
|
||||
@@ -0,0 +1,104 @@
|
||||
// Components
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import Image from "../../../../../Components/Image";
|
||||
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting }) => {
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
const currentPath = location.pathname;
|
||||
const navigate = useNavigate();
|
||||
if (currentPath === "/status/public") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsDeleteOpen(!isDeleteOpen)}
|
||||
loading={isDeleting}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/status/configure`)}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg": {
|
||||
mr: theme.spacing(3),
|
||||
"& path": {
|
||||
stroke: theme.palette.secondary.contrastText,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SettingsIcon /> Configure
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Controls.propTypes = {
|
||||
isDeleting: PropTypes.bool,
|
||||
isDeleteOpen: PropTypes.bool.isRequired,
|
||||
setIsDeleteOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ControlsHeader = ({ statusPage, isDeleting, isDeleteOpen, setIsDeleteOpen }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
alignSelf="flex-start"
|
||||
direction="row"
|
||||
width="100%"
|
||||
gap={theme.spacing(2)}
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Image
|
||||
shouldRender={statusPage?.logo?.data ? true : false}
|
||||
alt={"Company logo"}
|
||||
maxWidth={"100px"}
|
||||
base64={statusPage?.logo?.data}
|
||||
/>
|
||||
<Typography variant="h2">{statusPage?.companyName}</Typography>
|
||||
</Stack>
|
||||
<Controls
|
||||
isDeleting={isDeleting}
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
ControlsHeader.propTypes = {
|
||||
statusPage: PropTypes.object,
|
||||
isDeleting: PropTypes.bool,
|
||||
isDeleteOpen: PropTypes.bool.isRequired,
|
||||
setIsDeleteOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ControlsHeader;
|
||||
@@ -0,0 +1,58 @@
|
||||
// Components
|
||||
import { Stack, Box } from "@mui/material";
|
||||
import Host from "../../../../../Components/Host";
|
||||
import StatusPageBarChart from "../../../../../Components/Charts/StatusPageBarChart";
|
||||
import { StatusLabel } from "../../../../../Components/Label";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
|
||||
import PropTypes from "prop-types";
|
||||
const MonitorsList = ({ monitors = [] }) => {
|
||||
const theme = useTheme();
|
||||
const { determineState } = useUtils();
|
||||
return (
|
||||
<>
|
||||
{monitors?.map((monitor) => {
|
||||
const status = determineState(monitor);
|
||||
return (
|
||||
<Stack
|
||||
key={monitor._id}
|
||||
width="100%"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Host
|
||||
key={monitor._id}
|
||||
url={monitor.url}
|
||||
title={monitor.title}
|
||||
percentageColor={monitor.percentageColor}
|
||||
percentage={monitor.percentage}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
<Box flex={9}>
|
||||
<StatusPageBarChart checks={monitor.checks.slice().reverse()} />
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorsList.propTypes = {
|
||||
monitors: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default MonitorsList;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,74 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const getMonitorStatus = (monitors, theme) => {
|
||||
const monitorsStatus = {
|
||||
icon: (
|
||||
<ErrorOutlineIcon
|
||||
sx={{ color: theme.palette.primary.contrastTextSecondaryDarkBg }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
if (monitors.every((monitor) => monitor.status === true)) {
|
||||
monitorsStatus.msg = "All systems operational";
|
||||
monitorsStatus.color = theme.palette.success.lowContrast;
|
||||
monitorsStatus.icon = (
|
||||
<CheckCircleIcon
|
||||
sx={{ color: theme.palette.primary.contrastTextSecondaryDarkBg }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (monitors.every((monitor) => monitor.status === false)) {
|
||||
monitorsStatus.msg = "All systems down";
|
||||
monitorsStatus.color = theme.palette.error.lowContrast;
|
||||
}
|
||||
|
||||
if (monitors.some((monitor) => monitor.status === false)) {
|
||||
monitorsStatus.msg = "Degraded performance";
|
||||
monitorsStatus.color = theme.palette.warning.lowContrast;
|
||||
}
|
||||
|
||||
// Paused or unknown
|
||||
if (monitors.some((monitor) => typeof monitor.status === "undefined")) {
|
||||
monitorsStatus.msg = "Unknown status";
|
||||
monitorsStatus.color = theme.palette.warning.lowContrast;
|
||||
}
|
||||
return monitorsStatus;
|
||||
};
|
||||
|
||||
const StatusBar = ({ monitors }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (typeof monitors === "undefined") return;
|
||||
|
||||
const monitorsStatus = getMonitorStatus(monitors, theme);
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={theme.spacing(2)}
|
||||
height={theme.spacing(30)}
|
||||
width={"100%"}
|
||||
backgroundColor={monitorsStatus.color}
|
||||
borderRadius={theme.spacing(2)}
|
||||
>
|
||||
{monitorsStatus.icon}
|
||||
{/* CAIO_REVIEW */}
|
||||
<Typography variant="h2DarkBg">{monitorsStatus.msg}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusBar;
|
||||
|
||||
StatusBar.propTypes = {
|
||||
status: PropTypes.object,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const useStatusPageDelete = (fetchStatusPage) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
|
||||
const deleteStatusPage = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.deleteStatusPage({ authToken });
|
||||
fetchStatusPage?.();
|
||||
return true;
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [deleteStatusPage, isLoading];
|
||||
};
|
||||
|
||||
export { useStatusPageDelete };
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
|
||||
|
||||
const useStatusPageFetch = (isCreate = false) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [statusPage, setStatusPage] = useState(undefined);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const theme = useTheme();
|
||||
const { getMonitorWithPercentage } = useMonitorUtils();
|
||||
const fetchStatusPage = useCallback(async () => {
|
||||
try {
|
||||
const response = await networkService.getStatusPage({ authToken });
|
||||
if (!response?.data?.data) return;
|
||||
const { statusPage, monitors } = response.data.data;
|
||||
setStatusPage(statusPage);
|
||||
|
||||
const monitorsWithPercentage = monitors.map((monitor) =>
|
||||
getMonitorWithPercentage(monitor, theme)
|
||||
);
|
||||
setMonitors(monitorsWithPercentage);
|
||||
} catch (error) {
|
||||
// If there is a 404, status page is not found
|
||||
if (error?.response?.status === 404) {
|
||||
setStatusPage(undefined);
|
||||
return;
|
||||
}
|
||||
createToast({ body: error.message });
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authToken, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreate === true) {
|
||||
return;
|
||||
}
|
||||
fetchStatusPage();
|
||||
}, [isCreate, fetchStatusPage]);
|
||||
|
||||
return [statusPage, monitors, isLoading, networkError, fetchStatusPage];
|
||||
};
|
||||
|
||||
export { useStatusPageFetch };
|
||||
@@ -0,0 +1,160 @@
|
||||
// Components
|
||||
import { Typography, Stack } from "@mui/material";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
import Fallback from "../../../Components/Fallback";
|
||||
import AdminLink from "./Components/AdminLink";
|
||||
import ControlsHeader from "./Components/ControlsHeader";
|
||||
import SkeletonLayout from "./Components/Skeleton";
|
||||
import StatusBar from "./Components/StatusBar";
|
||||
import MonitorsList from "./Components/MonitorsList";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
|
||||
// Utils
|
||||
import { useStatusPageFetch } from "./Hooks/useStatusPageFetch";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useStatusPageDelete } from "./Hooks/useStatusPageDelete";
|
||||
import { useState } from "react";
|
||||
|
||||
const PublicStatus = () => {
|
||||
// Local state
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const [statusPage, monitors, isLoading, networkError, fetchStatusPage] =
|
||||
useStatusPageFetch();
|
||||
const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage);
|
||||
const location = useLocation();
|
||||
|
||||
// Setup
|
||||
const currentPath = location.pathname;
|
||||
let sx = { paddingLeft: theme.spacing(20), paddingRight: theme.spacing(20) };
|
||||
let link = undefined;
|
||||
// Public status page
|
||||
if (currentPath === "/status/public") {
|
||||
sx = {
|
||||
paddingTop: theme.spacing(20),
|
||||
paddingLeft: "20vw",
|
||||
paddingRight: "20vw",
|
||||
};
|
||||
link = <AdminLink />;
|
||||
}
|
||||
|
||||
// Loading
|
||||
if (isLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
// Error fetching data
|
||||
if (networkError === true) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
Network error
|
||||
</Typography>
|
||||
<Typography>Please check your connection</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
// Public status page fallback
|
||||
if (
|
||||
!isLoading &&
|
||||
typeof statusPage === "undefined" &&
|
||||
currentPath === "/status/public"
|
||||
) {
|
||||
return (
|
||||
<Stack sx={sx}>
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
A public status page is not set up.
|
||||
</Typography>
|
||||
<Typography>Please contact to your administrator</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Finished loading, but status page is not public
|
||||
if (
|
||||
!isLoading &&
|
||||
currentPath === "/status/public" &&
|
||||
statusPage.isPublished === false
|
||||
) {
|
||||
return (
|
||||
<Stack sx={sx}>
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
This status page is not public.
|
||||
</Typography>
|
||||
<Typography>Please contact to your administrator</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Status page doesn't exist
|
||||
if (!isLoading && typeof statusPage === "undefined") {
|
||||
return (
|
||||
<Fallback
|
||||
title="status page"
|
||||
checks={[
|
||||
"Display a list of monitors to track",
|
||||
"Share your monitors with the public",
|
||||
]}
|
||||
link="/status/create"
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
alignItems="center"
|
||||
sx={{ ...sx, position: "relative" }}
|
||||
>
|
||||
<ControlsHeader
|
||||
statusPage={statusPage}
|
||||
isDeleting={isDeleting}
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
/>
|
||||
<Typography variant="h2">Service status</Typography>
|
||||
<StatusBar monitors={monitors} />
|
||||
<MonitorsList monitors={monitors} />
|
||||
{link}
|
||||
<Dialog
|
||||
// open={isOpen.deleteStats}
|
||||
title="Do you want to delete this status page?"
|
||||
onConfirm={() => {
|
||||
deleteStatusPage();
|
||||
setIsDeleteOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteOpen(false);
|
||||
}}
|
||||
open={isDeleteOpen}
|
||||
confirmationButtonLabel="Yes, delete status page"
|
||||
description="Once deleted, your status page cannot be retrieved."
|
||||
isLoading={isDeleting || isLoading}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicStatus;
|
||||
@@ -3,10 +3,10 @@ import { Box, Stack } from "@mui/material";
|
||||
import DataTable from "../../../../../Components/Table";
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
|
||||
import Host from "../Host";
|
||||
import Host from "../../../../../Components/Host";
|
||||
import { StatusLabel } from "../../../../../Components/Label";
|
||||
import BarChart from "../../../../../Components/Charts/BarChart";
|
||||
import ActionsMenu from "../ActionsMenu";
|
||||
import ActionsMenu from "../../../../../Components/ActionsMenu";
|
||||
|
||||
import LoadingSpinner from "../LoadingSpinner";
|
||||
import TableSkeleton from "../../../../../Components/Table/skeleton";
|
||||
@@ -49,7 +49,6 @@ import PropTypes from "prop-types";
|
||||
const UptimeDataTable = ({
|
||||
isAdmin,
|
||||
isSearching,
|
||||
setIsLoading,
|
||||
filteredMonitors,
|
||||
sort,
|
||||
setSort,
|
||||
@@ -167,7 +166,6 @@ const UptimeDataTable = ({
|
||||
monitor={row.monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateRowCallback={triggerUpdate}
|
||||
setIsLoading={setIsLoading}
|
||||
pauseCallback={triggerUpdate}
|
||||
/>
|
||||
),
|
||||
@@ -206,12 +204,10 @@ UptimeDataTable.propTypes = {
|
||||
isSearching: PropTypes.bool,
|
||||
setSort: PropTypes.func,
|
||||
setSearch: PropTypes.func,
|
||||
setIsLoading: PropTypes.func,
|
||||
triggerUpdate: PropTypes.func,
|
||||
debouncedSearch: PropTypes.string,
|
||||
onSearchChange: PropTypes.func,
|
||||
isAdmin: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
monitors: PropTypes.array,
|
||||
filteredMonitors: PropTypes.array,
|
||||
monitorCount: PropTypes.number,
|
||||
|
||||
@@ -2,34 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
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,
|
||||
};
|
||||
};
|
||||
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
|
||||
|
||||
export const useMonitorFetch = ({
|
||||
authToken,
|
||||
@@ -49,7 +22,7 @@ export const useMonitorFetch = ({
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const { getMonitorWithPercentage } = useMonitorUtils();
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
|
||||
@@ -62,7 +62,6 @@ const UptimeMonitors = () => {
|
||||
const [page, setPage] = useState(undefined);
|
||||
const [sort, setSort] = useState(undefined);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
|
||||
|
||||
// Utils
|
||||
@@ -107,8 +106,7 @@ const UptimeMonitors = () => {
|
||||
order: sort?.order,
|
||||
triggerUpdate: monitorUpdateTrigger,
|
||||
});
|
||||
const totalMonitors = monitorsSummary?.totalMonitors ?? 0;
|
||||
|
||||
const totalMonitors = monitorsSummary?.totalMonitors;
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
@@ -123,18 +121,26 @@ const UptimeMonitors = () => {
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
if (!isLoading && !monitorsAreLoading && totalMonitors === 0) {
|
||||
if (
|
||||
!monitorsAreLoading &&
|
||||
(totalMonitors === 0 || typeof totalMonitors === "undefined")
|
||||
) {
|
||||
|
||||
return (
|
||||
<Fallback
|
||||
vowelStart={true}
|
||||
title="uptime monitor"
|
||||
checks={["Check if a website or service is up and running"]}
|
||||
checks={[
|
||||
"Check if websites or servers are online & responsive",
|
||||
"Alert teams about downtime or performance issues",
|
||||
"Monitor HTTP endpoints, pings, containers & ports",
|
||||
"Track historical uptime and reliability trends",
|
||||
]}
|
||||
link="/uptime/create"
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="monitors"
|
||||
@@ -143,7 +149,7 @@ const UptimeMonitors = () => {
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<CreateMonitorHeader
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!isLoading}
|
||||
shouldRender={!monitorsAreLoading}
|
||||
path="/uptime/create"
|
||||
/>
|
||||
<Greeting type="uptime" />
|
||||
@@ -166,8 +172,7 @@ const UptimeMonitors = () => {
|
||||
</Stack>
|
||||
<UptimeDataTable
|
||||
isAdmin={isAdmin}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
isLoading={monitorsAreLoading}
|
||||
filteredMonitors={filteredMonitors}
|
||||
monitors={monitors}
|
||||
monitorCount={totalMonitors}
|
||||
|
||||
@@ -27,9 +27,18 @@ 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";
|
||||
|
||||
import Status from "../Pages/Status";
|
||||
// Status pages
|
||||
import CreateStatus from "../Pages/StatusPage/Create";
|
||||
import Status from "../Pages/StatusPage/Status";
|
||||
|
||||
import Integrations from "../Pages/Integrations";
|
||||
|
||||
// Settings
|
||||
@@ -39,6 +48,7 @@ import Settings from "../Pages/Settings";
|
||||
import Maintenance from "../Pages/Maintenance";
|
||||
|
||||
import ProtectedRoute from "../Components/ProtectedRoute";
|
||||
import ProtectedDistributedUptimeRoute from "../Components/ProtectedDistributedUptimeRoute";
|
||||
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
|
||||
import withAdminCheck from "../Components/HOC/withAdminCheck";
|
||||
|
||||
@@ -75,6 +85,31 @@ const Routes = () => {
|
||||
path="/uptime/configure/:monitorId/"
|
||||
element={<UptimeConfigure />}
|
||||
/>
|
||||
<Route
|
||||
path="/distributed-uptime"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeMonitors />{" "}
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/distributed-uptime/create"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<CreateDistributedUptime />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/distributed-uptime/:monitorId"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeDetails />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed"
|
||||
element={<PageSpeed />}
|
||||
@@ -112,6 +147,16 @@ const Routes = () => {
|
||||
path="status"
|
||||
element={<Status />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="status/create"
|
||||
element={<CreateStatus />}
|
||||
/>
|
||||
<Route
|
||||
path="status/configure"
|
||||
element={<CreateStatus />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="integrations"
|
||||
element={<Integrations />}
|
||||
@@ -174,6 +219,10 @@ const Routes = () => {
|
||||
path="/new-password-confirmed"
|
||||
element={<AuthNewPasswordConfirmed />}
|
||||
/>
|
||||
<Route
|
||||
path="/status/public"
|
||||
element={<Status />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="*"
|
||||
|
||||
@@ -861,6 +861,165 @@ class NetworkService {
|
||||
"https://api.github.com/repos/bluewave-labs/bluewave-uptime/releases/latest"
|
||||
);
|
||||
}
|
||||
|
||||
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) => {});
|
||||
|
||||
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`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createStatusPage(config) {
|
||||
const { authToken, user, form, isCreate } = config;
|
||||
const fd = new FormData();
|
||||
fd.append("isPublished", form.isPublished);
|
||||
fd.append("companyName", form.companyName);
|
||||
fd.append("url", form.url);
|
||||
fd.append("timezone", form.timezone);
|
||||
fd.append("color", form.color);
|
||||
fd.append("showCharts", form.showCharts);
|
||||
fd.append("showUptimePercentage", form.showUptimePercentage);
|
||||
form.monitors.forEach((monitorId) => {
|
||||
fd.append("monitors[]", monitorId);
|
||||
});
|
||||
if (form?.logo?.src && form?.logo?.src !== "") {
|
||||
const imageResult = await axios.get(form.logo.src, {
|
||||
responseType: "blob",
|
||||
});
|
||||
fd.append("logo", imageResult.data);
|
||||
// Cleanup blob
|
||||
if (form.logo.src.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(form.logo.src);
|
||||
}
|
||||
}
|
||||
if (isCreate) {
|
||||
return this.axiosInstance.post(`/status-page`, fd, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return this.axiosInstance.put(`/status-page`, fd, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteStatusPage(config) {
|
||||
const { authToken } = config;
|
||||
return this.axiosInstance.delete(`/status-page`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
|
||||
@@ -152,6 +152,11 @@ const newSemanticColors = {
|
||||
light: newColors.blueGray600,
|
||||
dark: newColors.gray200,
|
||||
},
|
||||
// CAIO_REVIEW, need a brighter color for dark bg
|
||||
contrastTextSecondaryDarkBg: {
|
||||
light: newColors.gray200,
|
||||
dark: newColors.gray200,
|
||||
},
|
||||
contrastTextTertiary: {
|
||||
light: newColors.blueGray500,
|
||||
dark: newColors.gray500,
|
||||
@@ -246,6 +251,21 @@ const newSemanticColors = {
|
||||
dark: paletteColors.gray850,
|
||||
},
|
||||
},
|
||||
|
||||
map: {
|
||||
main: {
|
||||
light: newColors.offWhite,
|
||||
dark: undefined,
|
||||
},
|
||||
lowContrast: {
|
||||
light: newColors.gray200,
|
||||
dark: undefined,
|
||||
},
|
||||
highContrast: {
|
||||
light: newColors.gray500,
|
||||
dark: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export { typographyLevels, semanticColors as colors, newSemanticColors };
|
||||
@@ -21,6 +21,12 @@ const baseTheme = (palette) => ({
|
||||
color: palette.primary.contrastTextSecondary,
|
||||
fontWeight: 400,
|
||||
},
|
||||
// CAIO_REVIEW, need a brighter color for dark bg
|
||||
h2DarkBg: {
|
||||
fontSize: typographyLevels.l,
|
||||
color: palette.primary.contrastTextSecondaryDarkBg,
|
||||
fontWeight: 400,
|
||||
},
|
||||
body1: {
|
||||
fontSize: typographyLevels.m,
|
||||
color: palette.primary.contrastTextTertiary,
|
||||
@@ -53,6 +59,18 @@ const baseTheme = (palette) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) => props.color === "error",
|
||||
style: {
|
||||
"&.Mui-disabled": {
|
||||
backgroundColor: theme.palette.error.lowContrast,
|
||||
},
|
||||
"& .MuiButton-loadingIndicator": {
|
||||
// styles for error variant loading indicator
|
||||
color: theme.palette.error.contrastText,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) => props.variant === "group",
|
||||
style: {
|
||||
@@ -302,6 +320,62 @@ const baseTheme = (palette) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.tertiary.contrastText,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
textTransform: "none",
|
||||
minWidth: "fit-content",
|
||||
paddingY: theme.spacing(6),
|
||||
fontWeight: 400,
|
||||
borderBottom: "2px solid transparent",
|
||||
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
"&:first-of-type": { borderTopLeftRadius: "8px" },
|
||||
"&:last-child": { borderTopRightRadius: "8px", borderRight: 0 },
|
||||
"&:focus-visible": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
borderColor: theme.palette.tertiary.contrastText,
|
||||
borderRightColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: theme.palette.secondary.contrastText,
|
||||
borderColor: theme.palette.secondary.contrastText,
|
||||
borderRightColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiSvgIcon: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: theme.palette.tertiary.contrastText,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiSwitch: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
"& .MuiSwitch-track": {
|
||||
backgroundColor: theme.palette.primary.contrastText,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
|
||||
@@ -15,3 +15,22 @@ export const capitalizeFirstLetter = (str) => {
|
||||
}
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get first letter as a lower case string
|
||||
* @param {string} str String whose first letter is to be lower cased
|
||||
* @returns A string with first letter lower cased
|
||||
*/
|
||||
|
||||
export const toLowerCaseFirstLetter = (str) => {
|
||||
if (str === null || str === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Input must be a string");
|
||||
}
|
||||
if (str.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return str.charAt(0).toLowerCase() + str.slice(1);
|
||||
};
|
||||
@@ -1,6 +1,16 @@
|
||||
/**
|
||||
* Update errors if passed id matches the error.details[0].path, otherwise remove
|
||||
* the error for the id
|
||||
* @param {*} prev Previous errors *
|
||||
* @param {*} id ID of the field whose error is to be either updated or removed
|
||||
* @param {*} error the error object
|
||||
* @returns the Update Errors with the specific field with id being either removed or updated
|
||||
*/
|
||||
|
||||
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
if (error && id == error.details[0].path) {
|
||||
updatedErrors[id] = error.details[0].message ?? "Validation error";
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
@@ -32,7 +42,15 @@ const getTouchedFieldErrors = (validation, touchedErrors) => {
|
||||
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} form The form object of the submitted form data
|
||||
* @param {*} validation The Joi validation rules
|
||||
* @param {*} setErrors The function used to set the local errors
|
||||
* @returns true if there is no error or false if there is error after validating the form
|
||||
* the error will be reset to {} if returns false; otherwise the errors object will be set with
|
||||
* the new value
|
||||
*/
|
||||
const hasValidationErrors = (form, validation, setErrors) => {
|
||||
const { error } = validation.validate(form, {
|
||||
abortEarly: false,
|
||||
@@ -48,6 +66,10 @@ const hasValidationErrors = (form, validation, setErrors) => {
|
||||
"refreshTokenTTL",
|
||||
"jwtTTL",
|
||||
"notify-email-list",
|
||||
"_id",
|
||||
"__v",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
].includes(err.path[0])
|
||||
) {
|
||||
newErrors[err.path[0]] = err.message ?? "Validation error";
|
||||
@@ -66,8 +88,6 @@ const hasValidationErrors = (form, validation, setErrors) => {
|
||||
newErrors["usage_temperature"] = null;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("newErrors", newErrors);
|
||||
if (Object.values(newErrors).some((v) => v)) {
|
||||
setErrors(newErrors);
|
||||
return true;
|
||||
@@ -76,6 +96,7 @@ const hasValidationErrors = (form, validation, setErrors) => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setErrors({});
|
||||
return false;
|
||||
};
|
||||
export { buildErrors, hasValidationErrors, getTouchedFieldErrors };
|
||||
@@ -91,58 +91,61 @@ const credentials = joi.object({
|
||||
});
|
||||
|
||||
const monitorValidation = joi.object({
|
||||
url: joi.when('type', {
|
||||
is: 'docker',
|
||||
then: joi.string()
|
||||
.trim().regex(/^[a-z0-9]{64}$/),
|
||||
otherwise: joi
|
||||
.string()
|
||||
.trim()
|
||||
.custom((value, helpers) => {
|
||||
// Regex from https://gist.github.com/dperini/729294
|
||||
var urlRegex = new RegExp(
|
||||
"^" +
|
||||
// protocol identifier (optional)
|
||||
// short syntax // still required
|
||||
"(?:(?:https?|ftp):\\/\\/)?" +
|
||||
// user:pass BasicAuth (optional)
|
||||
"(?:" +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broadcast addresses
|
||||
// (first & last IP address of each class)
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// host & domain names, may end with dot
|
||||
// can be replaced by a shortest alternative
|
||||
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
|
||||
"(?:" +
|
||||
"(?:" +
|
||||
"[a-z0-9\\u00a1-\\uffff]" +
|
||||
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
")?" +
|
||||
"[a-z0-9\\u00a1-\\uffff]\\." +
|
||||
")+" +
|
||||
// TLD identifier name, may end with dot
|
||||
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
|
||||
")" +
|
||||
// port number (optional)
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path (optional)
|
||||
"(?:[/?#]\\S*)?" +
|
||||
"$",
|
||||
"i"
|
||||
);
|
||||
if (!urlRegex.test(value)) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
url: joi
|
||||
.when("type", {
|
||||
is: "docker",
|
||||
then: joi
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[a-z0-9]{64}$/),
|
||||
otherwise: joi
|
||||
.string()
|
||||
.trim()
|
||||
.custom((value, helpers) => {
|
||||
// Regex from https://gist.github.com/dperini/729294
|
||||
var urlRegex = new RegExp(
|
||||
"^" +
|
||||
// protocol identifier (optional)
|
||||
// short syntax // still required
|
||||
"(?:(?:https?|ftp):\\/\\/)?" +
|
||||
// user:pass BasicAuth (optional)
|
||||
"(?:" +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broadcast addresses
|
||||
// (first & last IP address of each class)
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// host & domain names, may end with dot
|
||||
// can be replaced by a shortest alternative
|
||||
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
|
||||
"(?:" +
|
||||
"(?:" +
|
||||
"[a-z0-9\\u00a1-\\uffff]" +
|
||||
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
")?" +
|
||||
"[a-z0-9\\u00a1-\\uffff]\\." +
|
||||
")+" +
|
||||
// TLD identifier name, may end with dot
|
||||
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
|
||||
")" +
|
||||
// port number (optional)
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path (optional)
|
||||
"(?:[/?#]\\S*)?" +
|
||||
"$",
|
||||
"i"
|
||||
);
|
||||
if (!urlRegex.test(value)) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
})
|
||||
return value;
|
||||
}),
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "This field is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
@@ -175,6 +178,52 @@ const imageValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const logoImageValidation = joi
|
||||
.object({
|
||||
type: joi
|
||||
.string()
|
||||
.valid("image/jpeg", "image/png")
|
||||
.allow(null) // Allow null and empty string
|
||||
.messages({
|
||||
"any.only": "Invalid file format.",
|
||||
"string.empty": "File type required.",
|
||||
})
|
||||
.optional(),
|
||||
size: joi
|
||||
.number()
|
||||
.max(3000000)
|
||||
.allow(null) // Allow null and empty string
|
||||
.messages({
|
||||
"number.base": "File size must be a number.",
|
||||
"number.max": "File size must be less than 3MB.",
|
||||
"number.empty": "File size required.",
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.allow(null)
|
||||
.optional(); // Make entire object optional
|
||||
|
||||
const statusPageValidation = joi.object({
|
||||
isPublished: joi.bool(),
|
||||
companyName: joi
|
||||
.string()
|
||||
.trim()
|
||||
.messages({ "string.empty": "Company name is required." }),
|
||||
url: joi.string().trim().messages({ "string.empty": "URL is required." }),
|
||||
timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }),
|
||||
color: joi.string().trim().messages({ "string.empty": "Color is required." }),
|
||||
theme: joi.string(),
|
||||
monitors: joi.array().min(1).required().messages({
|
||||
"string.pattern.base": "Must be a valid monitor ID",
|
||||
"array.base": "Monitors must be an array",
|
||||
"array.min": "At least one monitor is required",
|
||||
"array.empty": "At least one monitor is required",
|
||||
"any.required": "At least one monitor is required",
|
||||
}),
|
||||
logo: logoImageValidation,
|
||||
showUptimePercentage: joi.boolean(),
|
||||
showCharts: joi.boolean(),
|
||||
});
|
||||
const settingsValidation = joi.object({
|
||||
ttl: joi.number().required().messages({
|
||||
"string.empty": "TTL is required",
|
||||
@@ -289,4 +338,6 @@ export {
|
||||
maintenanceWindowValidation,
|
||||
advancedSettingsValidation,
|
||||
infrastructureMonitorValidation,
|
||||
statusPageValidation,
|
||||
logoImageValidation,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 4.5H18.3C19.4201 4.5 19.9802 4.5 20.408 4.71799C20.7843 4.90973 21.0903 5.21569 21.282 5.59202C21.5 6.01984 21.5 6.57989 21.5 7.7V9C21.5 9.93188 21.5 10.3978 21.3478 10.7654C21.1448 11.2554 20.7554 11.6448 20.2654 11.8478C19.8978 12 19.4319 12 18.5 12M13 19.5H5.7C4.5799 19.5 4.01984 19.5 3.59202 19.282C3.21569 19.0903 2.90973 18.7843 2.71799 18.408C2.5 17.9802 2.5 17.4201 2.5 16.3V15C2.5 14.0681 2.5 13.6022 2.65224 13.2346C2.85523 12.7446 3.24458 12.3552 3.73463 12.1522C4.10218 12 4.56812 12 5.5 12M10.3 14.5H13.7C13.98 14.5 14.12 14.5 14.227 14.4455C14.3211 14.3976 14.3976 14.3211 14.4455 14.227C14.5 14.12 14.5 13.98 14.5 13.7V10.3C14.5 10.02 14.5 9.87996 14.4455 9.773C14.3976 9.67892 14.3211 9.60243 14.227 9.5545C14.12 9.5 13.98 9.5 13.7 9.5H10.3C10.02 9.5 9.87996 9.5 9.773 9.5545C9.67892 9.60243 9.60243 9.67892 9.5545 9.773C9.5 9.87996 9.5 10.02 9.5 10.3V13.7C9.5 13.98 9.5 14.12 9.5545 14.227C9.60243 14.3211 9.67892 14.3976 9.773 14.4455C9.87996 14.5 10.02 14.5 10.3 14.5ZM17.8 22H21.2C21.48 22 21.62 22 21.727 21.9455C21.8211 21.8976 21.8976 21.8211 21.9455 21.727C22 21.62 22 21.48 22 21.2V17.8C22 17.52 22 17.38 21.9455 17.273C21.8976 17.1789 21.8211 17.1024 21.727 17.0545C21.62 17 21.48 17 21.2 17H17.8C17.52 17 17.38 17 17.273 17.0545C17.1789 17.1024 17.1024 17.1789 17.0545 17.273C17 17.38 17 17.52 17 17.8V21.2C17 21.48 17 21.62 17.0545 21.727C17.1024 21.8211 17.1789 21.8976 17.273 21.9455C17.38 22 17.52 22 17.8 22ZM2.8 7H6.2C6.48003 7 6.62004 7 6.727 6.9455C6.82108 6.89757 6.89757 6.82108 6.9455 6.727C7 6.62004 7 6.48003 7 6.2V2.8C7 2.51997 7 2.37996 6.9455 2.273C6.89757 2.17892 6.82108 2.10243 6.727 2.0545C6.62004 2 6.48003 2 6.2 2H2.8C2.51997 2 2.37996 2 2.273 2.0545C2.17892 2.10243 2.10243 2.17892 2.0545 2.273C2 2.37996 2 2.51997 2 2.8V6.2C2 6.48003 2 6.62004 2.0545 6.727C2.10243 6.82108 2.17892 6.89757 2.273 6.9455C2.37996 7 2.51997 7 2.8 7Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -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 |
+1
-1
@@ -17,7 +17,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
>
|
||||
<Router>
|
||||
<NetworkServiceProvider>
|
||||
<App />
|
||||
<App />
|
||||
</NetworkServiceProvider>
|
||||
</Router>
|
||||
</PersistGate>
|
||||
|
||||
Vendored
+1
@@ -4,6 +4,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
@@ -4,6 +4,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
@@ -4,6 +4,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -45,7 +46,8 @@ services:
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: uptime_database_mongo:latest
|
||||
command: ["mongod", "--quiet", "--auth"]
|
||||
restart: always
|
||||
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
|
||||
@@ -3,6 +3,7 @@ services:
|
||||
image: uptime_client:latest
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
@@ -1,16 +1,219 @@
|
||||
import { handleError } from "./controllerUtils.js";
|
||||
import { successMessages } from "../utils/messages.js";
|
||||
import Monitor from "../db/models/Monitor.js";
|
||||
import DistributedUptimeCheck from "../db/models/DistributedUptimeCheck.js";
|
||||
|
||||
const SERVICE_NAME = "DistributedUptimeQueueController";
|
||||
|
||||
class DistributedUptimeController {
|
||||
constructor() {}
|
||||
constructor(db, http, statusService) {
|
||||
this.db = db;
|
||||
this.http = http;
|
||||
this.statusService = statusService;
|
||||
this.resultsCallback = this.resultsCallback.bind(this);
|
||||
this.getDistributedUptimeMonitors = this.getDistributedUptimeMonitors.bind(this);
|
||||
this.getDistributedUptimeMonitorDetails =
|
||||
this.getDistributedUptimeMonitorDetails.bind(this);
|
||||
}
|
||||
|
||||
async resultsCallback(req, res, next) {
|
||||
try {
|
||||
const { id, result } = req.body;
|
||||
// Calculate response time
|
||||
const {
|
||||
first_byte_took,
|
||||
body_read_took,
|
||||
dns_took,
|
||||
conn_took,
|
||||
connect_took,
|
||||
tls_took,
|
||||
status_code,
|
||||
error,
|
||||
} = result;
|
||||
|
||||
// Calculate response time
|
||||
const responseTime =
|
||||
(first_byte_took +
|
||||
body_read_took +
|
||||
dns_took +
|
||||
conn_took +
|
||||
connect_took +
|
||||
tls_took) /
|
||||
1_000_000;
|
||||
|
||||
// Calculate if server is up or down
|
||||
const isErrorStatus = status_code >= 400;
|
||||
const hasError = error !== "";
|
||||
|
||||
const status = isErrorStatus || hasError ? false : true;
|
||||
|
||||
// Build response
|
||||
const distributedUptimeResponse = {
|
||||
monitorId: id,
|
||||
type: "distributed_http",
|
||||
payload: result,
|
||||
status,
|
||||
code: status_code,
|
||||
responseTime,
|
||||
first_byte_took,
|
||||
body_read_took,
|
||||
dns_took,
|
||||
conn_took,
|
||||
connect_took,
|
||||
tls_took,
|
||||
};
|
||||
if (error) {
|
||||
const code = status_code || this.NETWORK_ERROR;
|
||||
distributedUptimeResponse.code = code;
|
||||
distributedUptimeResponse.message =
|
||||
this.http.STATUS_CODES[code] || "Network Error";
|
||||
} else {
|
||||
distributedUptimeResponse.message = this.http.STATUS_CODES[status_code];
|
||||
}
|
||||
|
||||
await this.statusService.updateStatus(distributedUptimeResponse);
|
||||
|
||||
res.status(200).json({ message: "OK" });
|
||||
} catch (error) {
|
||||
throw handleError(error, SERVICE_NAME, "resultsCallback");
|
||||
next(handleError(error, SERVICE_NAME, "resultsCallback"));
|
||||
}
|
||||
}
|
||||
|
||||
async getDistributedUptimeMonitors(req, res, next) {
|
||||
try {
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
const BATCH_DELAY = 1000;
|
||||
let batchTimeout = null;
|
||||
let opInProgress = false;
|
||||
|
||||
// Do things here
|
||||
const notifyChange = async () => {
|
||||
if (opInProgress) {
|
||||
// Get data
|
||||
const monitors = await this.db.getMonitorsByTeamId(req);
|
||||
res.write(`data: ${JSON.stringify({ monitors })}\n\n`);
|
||||
opInProgress = false;
|
||||
}
|
||||
batchTimeout = null;
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
opInProgress = true;
|
||||
if (batchTimeout) clearTimeout(batchTimeout);
|
||||
batchTimeout = setTimeout(notifyChange, BATCH_DELAY);
|
||||
};
|
||||
|
||||
const monitorStream = Monitor.watch(
|
||||
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
|
||||
{ fullDocument: "updateLookup" }
|
||||
);
|
||||
|
||||
const checksStream = DistributedUptimeCheck.watch(
|
||||
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
|
||||
{ fullDocument: "updateLookup" }
|
||||
);
|
||||
|
||||
monitorStream.on("change", handleChange);
|
||||
checksStream.on("change", handleChange);
|
||||
|
||||
// Send initial data
|
||||
const monitors = await this.db.getMonitorsByTeamId(req);
|
||||
res.write(`data: ${JSON.stringify({ monitors })}\n\n`);
|
||||
|
||||
// Handle client disconnect
|
||||
req.on("close", () => {
|
||||
if (batchTimeout) {
|
||||
clearTimeout(batchTimeout);
|
||||
}
|
||||
monitorStream.close();
|
||||
checksStream.close();
|
||||
clearInterval(keepAlive);
|
||||
});
|
||||
|
||||
// Keep connection alive
|
||||
const keepAlive = setInterval(() => {
|
||||
res.write(": keepalive\n\n");
|
||||
}, 30000);
|
||||
|
||||
// Clean up on close
|
||||
req.on("close", () => {
|
||||
clearInterval(keepAlive);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitors"));
|
||||
}
|
||||
}
|
||||
|
||||
async getDistributedUptimeMonitorDetails(req, res, next) {
|
||||
try {
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
const BATCH_DELAY = 1000;
|
||||
let batchTimeout = null;
|
||||
let opInProgress = false;
|
||||
|
||||
// Do things here
|
||||
const notifyChange = async () => {
|
||||
try {
|
||||
if (opInProgress) {
|
||||
// Get data
|
||||
const monitor = await this.db.getDistributedUptimeDetailsById(req);
|
||||
res.write(`data: ${JSON.stringify({ monitor })}\n\n`);
|
||||
opInProgress = false;
|
||||
}
|
||||
batchTimeout = null;
|
||||
} catch (error) {
|
||||
console.error("Error in notifyChange:", error);
|
||||
opInProgress = false;
|
||||
batchTimeout = null;
|
||||
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
opInProgress = true;
|
||||
if (batchTimeout) clearTimeout(batchTimeout);
|
||||
batchTimeout = setTimeout(notifyChange, BATCH_DELAY);
|
||||
};
|
||||
|
||||
const checksStream = DistributedUptimeCheck.watch(
|
||||
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
|
||||
{ fullDocument: "updateLookup" }
|
||||
);
|
||||
|
||||
checksStream.on("change", handleChange);
|
||||
|
||||
// Send initial data
|
||||
const monitor = await this.db.getDistributedUptimeDetailsById(req);
|
||||
res.write(`data: ${JSON.stringify({ monitor })}\n\n`);
|
||||
|
||||
// Handle client disconnect
|
||||
req.on("close", () => {
|
||||
if (batchTimeout) {
|
||||
clearTimeout(batchTimeout);
|
||||
}
|
||||
checksStream.close();
|
||||
clearInterval(keepAlive);
|
||||
});
|
||||
|
||||
// Keep connection alive
|
||||
const keepAlive = setInterval(() => {
|
||||
res.write(": keepalive\n\n");
|
||||
}, 30000);
|
||||
|
||||
// Clean up on close
|
||||
req.on("close", () => {
|
||||
clearInterval(keepAlive);
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { handleError, handleValidationError } from "./controllerUtils.js";
|
||||
import {
|
||||
createStatusPageBodyValidation,
|
||||
getStatusPageParamValidation,
|
||||
imageValidation,
|
||||
} from "../validation/joi.js";
|
||||
import { successMessages } from "../utils/messages.js";
|
||||
import { successMessages, errorMessages } from "../utils/messages.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageController";
|
||||
|
||||
@@ -15,13 +16,14 @@ class StatusPageController {
|
||||
createStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||
await imageValidation.validateAsync(req.file);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const statusPage = await this.db.createStatusPage(req.body);
|
||||
const statusPage = await this.db.createStatusPage(req.body, req.file);
|
||||
return res.success({
|
||||
msg: successMessages.STATUS_PAGE_CREATE,
|
||||
data: statusPage,
|
||||
@@ -30,17 +32,35 @@ class StatusPageController {
|
||||
next(handleError(error, SERVICE_NAME, "createStatusPage"));
|
||||
}
|
||||
};
|
||||
getStatusPageByUrl = async (req, res, next) => {
|
||||
|
||||
updateStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
await getStatusPageParamValidation.validateAsync(req.params);
|
||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||
await imageValidation.validateAsync(req.file);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { url } = req.params;
|
||||
const statusPage = await this.db.getStatusPageByUrl(url);
|
||||
const statusPage = await this.db.updateStatusPage(req.body, req.file);
|
||||
if (statusPage === null) {
|
||||
const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND);
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
return res.success({
|
||||
msg: successMessages.STATUS_PAGE_UPDATE,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "updateStatusPage"));
|
||||
}
|
||||
};
|
||||
|
||||
getStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
const statusPage = await this.db.getStatusPage();
|
||||
return res.success({
|
||||
msg: successMessages.STATUS_PAGE_BY_URL,
|
||||
data: statusPage,
|
||||
@@ -49,6 +69,17 @@ class StatusPageController {
|
||||
next(handleError(error, SERVICE_NAME, "getStatusPage"));
|
||||
}
|
||||
};
|
||||
|
||||
deleteStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
await this.db.deleteStatusPage();
|
||||
return res.success({
|
||||
msg: successMessages.STATUS_PAGE_DELETE,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteStatusPage"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default StatusPageController;
|
||||
@@ -1,13 +1,121 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
import { BaseCheckSchema } from "./Check.js";
|
||||
|
||||
// {
|
||||
// "id": "12123",
|
||||
// "result": {
|
||||
// "task_arrived": "2025-01-13T19:21:37.463466602Z",
|
||||
// "dns_start": "2025-01-14T00:21:33.1801319+05:00",
|
||||
// "dns_end": "2025-01-14T00:21:33.4582552+05:00",
|
||||
// "conn_start": "2025-01-14T00:21:33.1801319+05:00",
|
||||
// "conn_end": "2025-01-14T00:21:33.7076318+05:00",
|
||||
// "connect_start": "2025-01-14T00:21:33.4582552+05:00",
|
||||
// "connect_end": "2025-01-14T00:21:33.541899+05:00",
|
||||
// "tls_hand_shake_start": "2025-01-14T00:21:33.541899+05:00",
|
||||
// "tls_hand_shake_end": "2025-01-14T00:21:33.7076318+05:00",
|
||||
// "body_read_start": "2025-01-14T00:21:34.1894707+05:00",
|
||||
// "body_read_end": "2025-01-14T00:21:34.1894707+05:00",
|
||||
// "wrote_request": "2025-01-14T00:21:33.7076318+05:00",
|
||||
// "got_first_response_byte": "2025-01-14T00:21:34.1327652+05:00",
|
||||
// "first_byte_took": 425133400,
|
||||
// "body_read_took": 56030000,
|
||||
// "dns_took": 278123300,
|
||||
// "conn_took": 527499900,
|
||||
// "connect_took": 83643800,
|
||||
// "tls_took": 165732800,
|
||||
// "sni_name": "uprock.com",
|
||||
// "status_code": 200,
|
||||
// "body_size": 19320,
|
||||
// "request_header_size": 95,
|
||||
// "response_header_size": 246,
|
||||
// "response_headers": "X-Vercel-Id: bom1::iad1::sm87v-1736796096856-aec270c01f23\nDate: Mon, 13 Jan 2025 19:21:37 GMT\nServer: Vercel\nStrict-Transport-Security: max-age=63072000\nVary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url\nX-Matched-Path: /\nX-Powered-By: Next.js\nX-Vercel-Cache: MISS\nAge: 0\nCache-Control: private, no-cache, no-store, max-age=0, must-revalidate\nContent-Type: text/html; charset=utf-8",
|
||||
// "error": "",
|
||||
// "device_id": "d5f578e143a2cd603dd6bf5f846a86a538bde4a8fbe2ad1fca284ad9f033daf8",
|
||||
// "ip_address": "223.123.19.0",
|
||||
// "proof": "",
|
||||
// "created_at": "2025-01-13T19:21:37.463466912Z",
|
||||
// "continent": "AS",
|
||||
// "country_code": "PK",
|
||||
// "city": "Muzaffargarh",
|
||||
// "upt_burnt" : "0.01",
|
||||
// "location": {
|
||||
// "lat": 71.0968,
|
||||
// "lng": 30.0208
|
||||
// },
|
||||
// "payload": {
|
||||
// "callback": "https://webhook.site/2a15b0af-545a-4ac2-b913-153b97592d7a",
|
||||
// "x": "y"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const LocationSchema = new mongoose.Schema(
|
||||
{
|
||||
lat: { type: Number, required: true },
|
||||
lng: { type: Number, required: true },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const DistributedUptimeCheckSchema = mongoose.Schema(
|
||||
{
|
||||
...BaseCheckSchema.obj,
|
||||
first_byte_took: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
body_read_took: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
dns_took: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
conn_took: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
connect_took: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
tls_took: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
location: {
|
||||
type: LocationSchema,
|
||||
required: false,
|
||||
},
|
||||
continent: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
countryCode: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
city: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
uptBurnt: {
|
||||
type: mongoose.Schema.Types.Decimal128,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
DistributedUptimeCheckSchema.pre("save", function (next) {
|
||||
if (this.isModified("uptBurnt") && typeof this.uptBurnt === "string") {
|
||||
this.uptBurnt = mongoose.Types.Decimal128.fromString(this.uptBurnt);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
DistributedUptimeCheckSchema.index({ createdAt: 1 });
|
||||
DistributedUptimeCheckSchema.index({ monitorId: 1, createdAt: 1 });
|
||||
DistributedUptimeCheckSchema.index({ monitorId: 1, createdAt: -1 });
|
||||
|
||||
@@ -23,11 +23,6 @@ const StatusPageSchema = mongoose.Schema(
|
||||
required: true,
|
||||
default: "#4169E1",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "light",
|
||||
},
|
||||
monitors: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
@@ -35,6 +30,22 @@ const StatusPageSchema = mongoose.Schema(
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
logo: {
|
||||
data: Buffer,
|
||||
contentType: String,
|
||||
},
|
||||
isPublished: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showCharts: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showUptimePercentage: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
@@ -2,6 +2,7 @@ import Monitor from "../../models/Monitor.js";
|
||||
import Check from "../../models/Check.js";
|
||||
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
|
||||
import HardwareCheck from "../../models/HardwareCheck.js";
|
||||
import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
|
||||
import { errorMessages } from "../../../utils/messages.js";
|
||||
import Notification from "../../models/Notification.js";
|
||||
import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js";
|
||||
@@ -11,6 +12,7 @@ import { fileURLToPath } from "url";
|
||||
import {
|
||||
buildUptimeDetailsPipeline,
|
||||
buildHardwareDetailsPipeline,
|
||||
buildDistributedUptimeDetailsPipeline,
|
||||
} from "./monitorModuleQueries.js";
|
||||
import { ObjectId } from "mongodb";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -366,6 +368,48 @@ const getUptimeDetailsById = async (req) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getDistributedUptimeDetailsById = async (req) => {
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
|
||||
}
|
||||
|
||||
const { dateRange, normalize } = req.query;
|
||||
const dates = getDateRange(dateRange);
|
||||
const formatLookup = {
|
||||
day: "%Y-%m-%dT%H:%M:00Z",
|
||||
week: "%Y-%m-%dT%H:00:00Z",
|
||||
month: "%Y-%m-%dT00:00:00Z",
|
||||
};
|
||||
|
||||
const dateString = formatLookup[dateRange];
|
||||
const results = await DistributedUptimeCheck.aggregate(
|
||||
buildDistributedUptimeDetailsPipeline(monitor, dates, dateString)
|
||||
);
|
||||
|
||||
const monitorData = results[0];
|
||||
const normalizedGroupChecks = NormalizeDataUptimeDetails(
|
||||
monitorData.groupedChecks,
|
||||
10,
|
||||
100
|
||||
);
|
||||
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
...monitorData,
|
||||
groupedChecks: normalizedGroupChecks,
|
||||
};
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getDistributedUptimeDetailsById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get stats by monitor ID
|
||||
* @async
|
||||
@@ -630,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: {
|
||||
@@ -648,6 +712,10 @@ const getMonitorsByTeamId = async (req) => {
|
||||
case: { $eq: ["$type", "hardware"] },
|
||||
then: "$hardwarechecks",
|
||||
},
|
||||
{
|
||||
case: { $eq: ["$type", "distributed_http"] },
|
||||
then: "$distributeduptimechecks",
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
@@ -815,6 +883,7 @@ export {
|
||||
getMonitorById,
|
||||
getMonitorsByTeamId,
|
||||
getUptimeDetailsById,
|
||||
getDistributedUptimeDetailsById,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
|
||||
@@ -534,4 +534,188 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
];
|
||||
};
|
||||
|
||||
export { buildUptimeDetailsPipeline, buildHardwareDetailsPipeline };
|
||||
const buildDistributedUptimeDetailsPipeline = (monitor, dates, dateString) => {
|
||||
return [
|
||||
{
|
||||
$match: {
|
||||
monitorId: monitor._id,
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
aggregateData: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
lastCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
uptBurnt: {
|
||||
$sum: "$uptBurnt",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
// For the response time chart, should return checks for date window
|
||||
// Grouped by: {day: hour}, {week: day}, {month: day}
|
||||
groupedMapChecks: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
date: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
city: "$city",
|
||||
lat: "$location.lat",
|
||||
lng: "$location.lng",
|
||||
},
|
||||
city: { $first: "$city" }, // Add this line to include city in output
|
||||
lat: { $first: "$location.lat" },
|
||||
lng: { $first: "$location.lng" },
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
"_id.date": 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedChecks: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
_id: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
// Average response time for the date window
|
||||
groupAvgResponseTime: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
latestChecks: [
|
||||
{
|
||||
$sort: { createdAt: -1 }, // Sort by newest first
|
||||
},
|
||||
{
|
||||
$limit: 5, // Get only the first 5 documents
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
responseTime: 1,
|
||||
city: 1,
|
||||
countryCode: 1,
|
||||
uptBurnt: { $toString: "$uptBurnt" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
totalUptBurnt: {
|
||||
$toString: {
|
||||
$arrayElemAt: ["$aggregateData.uptBurnt", 0],
|
||||
},
|
||||
},
|
||||
avgResponseTime: {
|
||||
$arrayElemAt: ["$aggregateData.avgResponseTime", 0],
|
||||
},
|
||||
totalChecks: {
|
||||
$arrayElemAt: ["$aggregateData.totalChecks", 0],
|
||||
},
|
||||
latestResponseTime: {
|
||||
$arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0],
|
||||
},
|
||||
timeSinceLastCheck: {
|
||||
$let: {
|
||||
vars: {
|
||||
lastCheck: {
|
||||
$arrayElemAt: ["$aggregateData.lastCheck", 0],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$lastCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$lastCheck.createdAt"],
|
||||
},
|
||||
0,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
groupedMapChecks: "$groupedMapChecks",
|
||||
groupedChecks: "$groupedChecks",
|
||||
groupedAvgResponseTime: {
|
||||
$arrayElemAt: ["$groupAvgResponseTime", 0],
|
||||
},
|
||||
latestChecks: "$latestChecks",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export {
|
||||
buildUptimeDetailsPipeline,
|
||||
buildHardwareDetailsPipeline,
|
||||
buildDistributedUptimeDetailsPipeline,
|
||||
};
|
||||
@@ -1,11 +1,18 @@
|
||||
import StatusPage from "../../models/StatusPage.js";
|
||||
import { errorMessages } from "../../../utils/messages.js";
|
||||
import { NormalizeData } from "../../../utils/dataUtils.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageModule";
|
||||
|
||||
const createStatusPage = async (statusPageData) => {
|
||||
const createStatusPage = async (statusPageData, image) => {
|
||||
try {
|
||||
const statusPage = new StatusPage({ ...statusPageData });
|
||||
if (image) {
|
||||
statusPage.logo = {
|
||||
data: image.buffer,
|
||||
contentType: image.mimetype,
|
||||
};
|
||||
}
|
||||
await statusPage.save();
|
||||
return statusPage;
|
||||
} catch (error) {
|
||||
@@ -20,16 +27,115 @@ const createStatusPage = async (statusPageData) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusPageByUrl = async (url) => {
|
||||
const updateStatusPage = async (statusPageData, image) => {
|
||||
try {
|
||||
const statusPage = await StatusPage.findOne({ url });
|
||||
if (statusPage === null || statusPage === undefined) {
|
||||
if (image) {
|
||||
statusPageData.logo = {
|
||||
data: image.buffer,
|
||||
contentType: image.mimetype,
|
||||
};
|
||||
}
|
||||
const statusPage = await StatusPage.findOneAndUpdate({}, statusPageData, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
return statusPage;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "updateStatusPage";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusPage = async () => {
|
||||
try {
|
||||
const statusPageQuery = await StatusPage.aggregate([
|
||||
{ $limit: 1 },
|
||||
{
|
||||
$set: {
|
||||
originalMonitors: "$monitors",
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "monitors",
|
||||
foreignField: "_id",
|
||||
as: "monitors",
|
||||
},
|
||||
},
|
||||
{
|
||||
$unwind: "$monitors",
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "checks",
|
||||
let: { monitorId: "$monitors._id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{ $limit: 25 },
|
||||
],
|
||||
as: "monitors.checks",
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
"monitors.orderIndex": {
|
||||
$indexOfArray: ["$originalMonitors", "$monitors._id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$_id",
|
||||
statusPage: { $first: "$$ROOT" },
|
||||
monitors: { $push: "$monitors" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
statusPage: {
|
||||
_id: 1,
|
||||
color: 1,
|
||||
companyName: 1,
|
||||
isPublished: 1,
|
||||
logo: 1,
|
||||
originalMonitors: 1,
|
||||
showCharts: 1,
|
||||
showUptimePercentage: 1,
|
||||
timezone: 1,
|
||||
url: 1,
|
||||
},
|
||||
monitors: {
|
||||
$sortArray: {
|
||||
input: "$monitors",
|
||||
sortBy: { orderIndex: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
if (!statusPageQuery.length) {
|
||||
const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND);
|
||||
error.status = 404;
|
||||
|
||||
throw error;
|
||||
}
|
||||
return statusPage;
|
||||
|
||||
const { statusPage, monitors } = statusPageQuery[0];
|
||||
|
||||
const normalizedMonitors = monitors.map((monitor) => {
|
||||
return {
|
||||
...monitor,
|
||||
checks: NormalizeData(monitor.checks, 10, 100),
|
||||
};
|
||||
});
|
||||
|
||||
return { statusPage, monitors: normalizedMonitors };
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getStatusPageByUrl";
|
||||
@@ -37,4 +143,14 @@ const getStatusPageByUrl = async (url) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { createStatusPage, getStatusPageByUrl };
|
||||
const deleteStatusPage = async () => {
|
||||
try {
|
||||
await StatusPage.deleteOne({});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createStatusPage";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { createStatusPage, updateStatusPage, getStatusPage, deleteStatusPage };
|
||||
+5
-1
@@ -252,7 +252,11 @@ const startApp = async () => {
|
||||
ServiceRegistry.get(NotificationService.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);
|
||||
|
||||
Generated
+15
-14
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bullmq": "5.40.0",
|
||||
"bullmq": "5.40.2",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.4",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -1748,9 +1748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.40.0",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.0.tgz",
|
||||
"integrity": "sha512-tmrk32EmcbtUOGPSdwlDUcc0w+nAMqCisk8vEFFmG8aOzIehz0BxTNSj6Grh0qoMugRF3VglWk8HGUBnWqU2Fw==",
|
||||
"version": "5.40.2",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.2.tgz",
|
||||
"integrity": "sha512-Cn4NUpwGAF4WnuXR2kTZCTAUEUHajSCn/IqiDG9ry1kVvAwwwg1Ati3J5HN2uZjqD5PBfNDXYnsc2+0PzakDwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.9.0",
|
||||
@@ -4206,9 +4206,10 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz",
|
||||
"integrity": "sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz",
|
||||
"integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "^1.1.1",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
@@ -5515,9 +5516,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
|
||||
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz",
|
||||
"integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.9",
|
||||
@@ -5571,14 +5572,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz",
|
||||
"integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==",
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.0.tgz",
|
||||
"integrity": "sha512-nLhk3Qrv6q/HpD2k1O7kbBqsq+/kmKpdv5KJ+LLhQlII3e1p/SSLoLP6jMuSiU6+iLK7zFw4T1niAk3mA3QVug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.10.1",
|
||||
"kareem": "2.6.3",
|
||||
"mongodb": "~6.12.0",
|
||||
"mongodb": "~6.13.0",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bullmq": "5.40.0",
|
||||
"bullmq": "5.40.2",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.4",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
@@ -8,6 +8,14 @@ class DistributedUptimeRoutes {
|
||||
}
|
||||
initRoutes() {
|
||||
this.router.post("/callback", this.distributedUptimeController.resultsCallback);
|
||||
this.router.get(
|
||||
"/monitors/:teamId",
|
||||
this.distributedUptimeController.getDistributedUptimeMonitors
|
||||
);
|
||||
this.router.get(
|
||||
"/monitors/details/:monitorId",
|
||||
this.distributedUptimeController.getDistributedUptimeMonitorDetails
|
||||
);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import { verifyJWT } from "../middleware/verifyJWT.js";
|
||||
import multer from "multer";
|
||||
const upload = multer();
|
||||
|
||||
class StatusPageRoutes {
|
||||
constructor(statusPageController) {
|
||||
@@ -10,8 +11,20 @@ class StatusPageRoutes {
|
||||
}
|
||||
|
||||
initRoutes() {
|
||||
this.router.get("/:url", this.statusPageController.getStatusPageByUrl);
|
||||
this.router.post("/:url", verifyJWT, this.statusPageController.createStatusPage);
|
||||
this.router.get("/", this.statusPageController.getStatusPage);
|
||||
this.router.post(
|
||||
"/",
|
||||
upload.single("logo"),
|
||||
verifyJWT,
|
||||
this.statusPageController.createStatusPage
|
||||
);
|
||||
this.router.put(
|
||||
"/",
|
||||
upload.single("logo"),
|
||||
verifyJWT,
|
||||
this.statusPageController.updateStatusPage
|
||||
);
|
||||
this.router.delete("/", verifyJWT, this.statusPageController.deleteStatusPage);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware"];
|
||||
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
const JOBS_PER_WORKER = 5;
|
||||
const QUEUE_LOOKUP = {
|
||||
@@ -8,6 +8,7 @@ const QUEUE_LOOKUP = {
|
||||
port: "uptime",
|
||||
docker: "uptime",
|
||||
pagespeed: "pagespeed",
|
||||
distributed_http: "distributed",
|
||||
};
|
||||
const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
|
||||
|
||||
@@ -163,6 +164,9 @@ class NewJobQueue {
|
||||
// Get the current status
|
||||
await job.updateProgress(30);
|
||||
const networkResponse = await this.networkService.getStatus(job);
|
||||
if (job.data.type === "distributed_http") {
|
||||
return;
|
||||
}
|
||||
// Handle status change
|
||||
await job.updateProgress(60);
|
||||
const { monitor, statusChanged, prevStatus } =
|
||||
@@ -219,51 +223,6 @@ class NewJobQueue {
|
||||
connection: this.connection,
|
||||
concurrency: 5,
|
||||
});
|
||||
|
||||
// worker.on("active", (job) => {
|
||||
// this.logger.info({
|
||||
// message: `Worker started processing job: ${job.id}`,
|
||||
// service: SERVICE_NAME,
|
||||
// method: "createWorker",
|
||||
// });
|
||||
// });
|
||||
|
||||
// worker.on("completed", (job) => {
|
||||
// this.logger.info({
|
||||
// message: `Worker completed job: ${job.id}`,
|
||||
// service: SERVICE_NAME,
|
||||
// method: "createWorker",
|
||||
// });
|
||||
// });
|
||||
|
||||
// // Log job progress updates
|
||||
// worker.on("progress", (job, progress) => {
|
||||
// this.logger.info({
|
||||
// message: `Job progress: ${job.id}`,
|
||||
// service: SERVICE_NAME,
|
||||
// method: "createWorker",
|
||||
// details: `Progress: ${progress}%`,
|
||||
// });
|
||||
// });
|
||||
|
||||
// // Log when a job fails
|
||||
// worker.on("failed", (job, err) => {
|
||||
// this.logger.error({
|
||||
// message: `Worker failed job: ${job.id}`,
|
||||
// service: SERVICE_NAME,
|
||||
// method: "createWorker",
|
||||
// details: `Error: ${err.message}`,
|
||||
// stack: err.stack,
|
||||
// });
|
||||
// });
|
||||
|
||||
// worker.on("stalled", (jobId) => {
|
||||
// this.logger.warn({
|
||||
// message: `Worker stalled job: ${jobId}`,
|
||||
// service: SERVICE_NAME,
|
||||
// method: "createWorker",
|
||||
// });
|
||||
// });
|
||||
return worker;
|
||||
}
|
||||
|
||||
@@ -392,7 +351,6 @@ class NewJobQueue {
|
||||
const jobs = await queue.getJobs();
|
||||
const ret = await Promise.all(
|
||||
jobs.map(async (job) => {
|
||||
console.log(job);
|
||||
const state = await job.getState();
|
||||
return { url: job.data.url, state, progress: job.progress };
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { errorMessages, successMessages } from "../utils/messages.js";
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
|
||||
|
||||
/**
|
||||
* Constructs a new NetworkService instance.
|
||||
@@ -19,6 +20,7 @@ class NetworkService {
|
||||
this.TYPE_HARDWARE = "hardware";
|
||||
this.TYPE_DOCKER = "docker";
|
||||
this.TYPE_PORT = "port";
|
||||
this.TYPE_DISTRIBUTED_HTTP = "distributed_http";
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.NETWORK_ERROR = 5000;
|
||||
this.PING_ERROR = 5001;
|
||||
@@ -323,6 +325,35 @@ class NetworkService {
|
||||
}
|
||||
}
|
||||
|
||||
async requestDistributedHttp(job) {
|
||||
try {
|
||||
const monitor = job.data;
|
||||
const CALLBACK_URL = process.env.NGROK_URL
|
||||
? process.env.NGROK_URL
|
||||
: process.env.CALLBACK_URL;
|
||||
|
||||
const response = await this.axios.post(
|
||||
UPROCK_ENDPOINT,
|
||||
{
|
||||
id: monitor._id,
|
||||
url: monitor.url,
|
||||
callback: `${CALLBACK_URL}/api/v1/distributed-uptime/callback`,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-checkmate-key": process.env.UPROCK_API_KEY,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestDistributedHttp";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unsupported job types by throwing an error with details.
|
||||
*
|
||||
@@ -406,6 +437,8 @@ class NetworkService {
|
||||
return await this.requestDocker(job);
|
||||
case this.TYPE_PORT:
|
||||
return await this.requestPort(job);
|
||||
case this.TYPE_DISTRIBUTED_HTTP:
|
||||
return await this.requestDistributedHttp(job);
|
||||
|
||||
default:
|
||||
return this.handleUnsupportedType(type);
|
||||
|
||||
@@ -25,7 +25,7 @@ class StatusService {
|
||||
* @param {Object} networkResponse - The network response containing monitorId and status.
|
||||
* @param {string} networkResponse.monitorId - The ID of the monitor.
|
||||
* @param {string} networkResponse.status - The new status of the monitor.
|
||||
* @returns {Promise<Object>} - A promise that resolves to an object containing the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred.
|
||||
* @returns {Promise<Object>} - A promise that resolves to an object containinfg the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred.
|
||||
* @returns {Promise<Object>} returnObject - The object returned by the function.
|
||||
* @returns {Object} returnObject.monitor - The monitor object.
|
||||
* @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed.
|
||||
@@ -83,8 +83,23 @@ class StatusService {
|
||||
* @returns {Object} The check object.
|
||||
*/
|
||||
buildCheck = (networkResponse) => {
|
||||
const { monitorId, teamId, type, status, responseTime, code, message, payload } =
|
||||
networkResponse;
|
||||
const {
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
status,
|
||||
responseTime,
|
||||
code,
|
||||
message,
|
||||
payload,
|
||||
first_byte_took,
|
||||
body_read_took,
|
||||
dns_took,
|
||||
conn_took,
|
||||
connect_took,
|
||||
tls_took,
|
||||
} = networkResponse;
|
||||
|
||||
const check = {
|
||||
monitorId,
|
||||
teamId,
|
||||
@@ -92,8 +107,28 @@ class StatusService {
|
||||
statusCode: code,
|
||||
responseTime,
|
||||
message,
|
||||
first_byte_took,
|
||||
body_read_took,
|
||||
dns_took,
|
||||
conn_took,
|
||||
connect_took,
|
||||
tls_took,
|
||||
};
|
||||
|
||||
if (type === "distributed_http") {
|
||||
check.continent = payload.continent;
|
||||
check.countryCode = payload.country_code;
|
||||
check.city = payload.city;
|
||||
check.location = payload.location;
|
||||
check.uptBurnt = payload.upt_burnt;
|
||||
check.first_byte_took = payload.first_byte_took;
|
||||
check.body_read_took = payload.body_read_took;
|
||||
check.dns_took = payload.dns_took;
|
||||
check.conn_took = payload.conn_took;
|
||||
check.connect_took = payload.connect_took;
|
||||
check.tls_took = payload.tls_took;
|
||||
}
|
||||
|
||||
if (type === "pagespeed") {
|
||||
const categories = payload.lighthouseResult?.categories;
|
||||
const audits = payload.lighthouseResult?.audits;
|
||||
@@ -145,6 +180,7 @@ class StatusService {
|
||||
hardware: this.db.createHardwareCheck,
|
||||
docker: this.db.createCheck,
|
||||
port: this.db.createCheck,
|
||||
distributed_http: this.db.createDistributedCheck,
|
||||
};
|
||||
const operation = operationMap[networkResponse.type];
|
||||
|
||||
|
||||
@@ -136,7 +136,8 @@ const successMessages = {
|
||||
// Status Page
|
||||
STATUS_PAGE_BY_URL: "Got status page by url successfully",
|
||||
STATUS_PAGE_CREATE: "Status page created successfully",
|
||||
|
||||
STATUS_PAGE_DELETE: "Status page deleted successfully",
|
||||
STATUS_PAGE_UPDATE: "Status page updated successfully",
|
||||
// Docker
|
||||
DOCKER_SUCCESS: "Docker container status fetched successfully",
|
||||
|
||||
|
||||
@@ -423,7 +423,6 @@ const createStatusPageBodyValidation = joi.object({
|
||||
url: joi.string().required(),
|
||||
timezone: joi.string().required(),
|
||||
color: joi.string().required(),
|
||||
theme: joi.string().required(),
|
||||
monitors: joi
|
||||
.array()
|
||||
.items(joi.string().pattern(/^[0-9a-fA-F]{24}$/))
|
||||
@@ -434,8 +433,35 @@ const createStatusPageBodyValidation = joi.object({
|
||||
"array.empty": "At least one monitor is required",
|
||||
"any.required": "Monitors are required",
|
||||
}),
|
||||
isPublished: joi.boolean(),
|
||||
showCharts: joi.boolean(),
|
||||
showUptimePercentage: joi.boolean(),
|
||||
});
|
||||
|
||||
const imageValidation = joi
|
||||
.object({
|
||||
fieldname: joi.string().required(),
|
||||
originalname: joi.string().required(),
|
||||
encoding: joi.string().required(),
|
||||
mimetype: joi
|
||||
.string()
|
||||
.valid("image/jpeg", "image/png", "image/jpg")
|
||||
.required()
|
||||
.messages({
|
||||
"string.valid": "File must be a valid image (jpeg, jpg, or png)",
|
||||
}),
|
||||
size: joi.number().max(3145728).required().messages({
|
||||
"number.max": "File size must be less than 3MB",
|
||||
}),
|
||||
buffer: joi.binary().required(),
|
||||
destination: joi.string(),
|
||||
filename: joi.string(),
|
||||
path: joi.string(),
|
||||
})
|
||||
.messages({
|
||||
"any.required": "Image file is required",
|
||||
});
|
||||
|
||||
export {
|
||||
roleValidatior,
|
||||
loginValidation,
|
||||
@@ -493,4 +519,5 @@ export {
|
||||
updateAppSettingsBodyValidation,
|
||||
createStatusPageBodyValidation,
|
||||
getStatusPageParamValidation,
|
||||
imageValidation,
|
||||
};
|
||||
Reference in new issue
Block a user