diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs
index 7d6029d4b..86d523a6f 100644
--- a/client/.eslintrc.cjs
+++ b/client/.eslintrc.cjs
@@ -16,4 +16,8 @@ module.exports = {
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react/no-unescaped-entities": "off",
},
+ globals: {
+ __APP_VERSION__: "readonly",
+ process: "readonly",
+ },
};
diff --git a/client/package-lock.json b/client/package-lock.json
index 4050adfbb..f58f56f44 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -45,7 +45,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
- "vite": "^5.4.19"
+ "vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
@@ -492,9 +492,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
+ "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
@@ -504,13 +504,13 @@
"aix"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
+ "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
@@ -520,13 +520,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
+ "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
@@ -536,13 +536,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
+ "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
@@ -552,13 +552,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
+ "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
@@ -568,13 +568,13 @@
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
+ "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
@@ -584,13 +584,13 @@
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
@@ -600,13 +600,13 @@
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
+ "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
@@ -616,13 +616,13 @@
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
+ "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
@@ -632,13 +632,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
+ "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
@@ -648,13 +648,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
+ "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
@@ -664,13 +664,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
+ "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
@@ -680,13 +680,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
+ "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
@@ -696,13 +696,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
+ "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
@@ -712,13 +712,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
+ "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
@@ -728,13 +728,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
+ "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
@@ -744,13 +744,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
+ "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
@@ -760,13 +760,29 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
@@ -776,13 +792,29 @@
"netbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
@@ -792,13 +824,13 @@
"openbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
+ "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
@@ -808,13 +840,13 @@
"sunos"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
+ "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
@@ -824,13 +856,13 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
+ "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
@@ -840,13 +872,13 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
+ "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
@@ -856,7 +888,7 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -3040,9 +3072,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3172,23 +3204,6 @@
],
"license": "CC-BY-4.0"
},
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -3259,6 +3274,15 @@
"node": ">=10"
}
},
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3820,41 +3844,43 @@
}
},
"node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
+ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
+ "@esbuild/aix-ppc64": "0.25.5",
+ "@esbuild/android-arm": "0.25.5",
+ "@esbuild/android-arm64": "0.25.5",
+ "@esbuild/android-x64": "0.25.5",
+ "@esbuild/darwin-arm64": "0.25.5",
+ "@esbuild/darwin-x64": "0.25.5",
+ "@esbuild/freebsd-arm64": "0.25.5",
+ "@esbuild/freebsd-x64": "0.25.5",
+ "@esbuild/linux-arm": "0.25.5",
+ "@esbuild/linux-arm64": "0.25.5",
+ "@esbuild/linux-ia32": "0.25.5",
+ "@esbuild/linux-loong64": "0.25.5",
+ "@esbuild/linux-mips64el": "0.25.5",
+ "@esbuild/linux-ppc64": "0.25.5",
+ "@esbuild/linux-riscv64": "0.25.5",
+ "@esbuild/linux-s390x": "0.25.5",
+ "@esbuild/linux-x64": "0.25.5",
+ "@esbuild/netbsd-arm64": "0.25.5",
+ "@esbuild/netbsd-x64": "0.25.5",
+ "@esbuild/openbsd-arm64": "0.25.5",
+ "@esbuild/openbsd-x64": "0.25.5",
+ "@esbuild/sunos-x64": "0.25.5",
+ "@esbuild/win32-arm64": "0.25.5",
+ "@esbuild/win32-ia32": "0.25.5",
+ "@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/escalade": {
@@ -4052,6 +4078,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/eslint/node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -4184,6 +4227,20 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -6713,6 +6770,22 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -6923,20 +6996,23 @@
}
},
"node_modules/vite": {
- "version": "5.4.19",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
- "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
"dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^18.0.0 || >=20.0.0"
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -6945,19 +7021,25 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
- "terser": "^5.4.0"
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
+ "jiti": {
+ "optional": true
+ },
"less": {
"optional": true
},
@@ -6978,6 +7060,12 @@
},
"terser": {
"optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
}
}
},
@@ -7133,12 +7221,17 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
+ "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
"engines": {
- "node": ">= 6"
+ "node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
diff --git a/client/package.json b/client/package.json
index 83dfe8407..611ba5093 100644
--- a/client/package.json
+++ b/client/package.json
@@ -58,7 +58,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
- "vite": "^5.4.19"
+ "vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
diff --git a/client/src/Components/Layouts/AppLayout/index.jsx b/client/src/Components/Layouts/AppLayout/index.jsx
index 564012d72..d13f6d879 100644
--- a/client/src/Components/Layouts/AppLayout/index.jsx
+++ b/client/src/Components/Layouts/AppLayout/index.jsx
@@ -2,16 +2,17 @@ import Box from "@mui/material/Box";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import BackgroundSVG from "../../../assets/Images/background.svg";
+import { useSelector } from "react-redux";
const AppLayout = ({ children }) => {
const theme = useTheme();
-
+ const ui = useSelector((state) => state.ui);
return (
{
+const Link = ({ level, label, url, external = true }) => {
const theme = useTheme();
const levelConfig = {
@@ -49,11 +51,12 @@ const Link = ({ level, label, url }) => {
const { sx, color } = levelConfig[level];
return (
{label}
@@ -64,6 +67,7 @@ Link.propTypes = {
url: PropTypes.string.isRequired,
level: PropTypes.oneOf(["primary", "secondary", "tertiary", "error"]),
label: PropTypes.string.isRequired,
+ external: PropTypes.bool,
};
export default Link;
diff --git a/client/src/Components/MonitorActions/index.jsx b/client/src/Components/MonitorActions/index.jsx
new file mode 100644
index 000000000..ac5d89a47
--- /dev/null
+++ b/client/src/Components/MonitorActions/index.jsx
@@ -0,0 +1,116 @@
+import * as React from "react";
+import Button from "@mui/material/Button";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
+import ClickAwayListener from "@mui/material/ClickAwayListener";
+import Grow from "@mui/material/Grow";
+import Paper from "@mui/material/Paper";
+import Popper from "@mui/material/Popper";
+import MenuItem from "@mui/material/MenuItem";
+import MenuList from "@mui/material/MenuList";
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { createToast } from "../../Utils/toastUtils";
+import { useExportMonitors } from "../../Hooks/monitorHooks";
+
+const MonitorActions = ({ isLoading }) => {
+ const [open, setOpen] = React.useState(false);
+ const anchorRef = React.useRef(null);
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+ const [exportMonitors, isExporting] = useExportMonitors();
+
+ const options = [t("monitorActions.import"), t("monitorActions.export")];
+
+ const handleClick = async () => {
+ if (selectedIndex === 0) {
+ // Import
+ navigate("/uptime/bulk-import");
+ } else {
+ // Export
+ const [success, error] = await exportMonitors();
+ if (!success) {
+ createToast({ body: error || t("export.failed") });
+ }
+ }
+ };
+
+ const handleMenuItemClick = (event, index) => {
+ setSelectedIndex(index);
+ setOpen(false);
+ };
+
+ const handleToggle = () => {
+ setOpen((prevOpen) => !prevOpen);
+ };
+
+ const handleClose = (event) => {
+ if (anchorRef.current && anchorRef.current.contains(event.target)) {
+ return;
+ }
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+ {({ TransitionProps, placement }) => (
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default MonitorActions;
diff --git a/client/src/Components/MonitorCreateHeader/index.jsx b/client/src/Components/MonitorCreateHeader/index.jsx
index b630cb259..b3b50681c 100644
--- a/client/src/Components/MonitorCreateHeader/index.jsx
+++ b/client/src/Components/MonitorCreateHeader/index.jsx
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
+import MonitorActions from "../MonitorActions";
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
@@ -28,18 +29,7 @@ const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath
>
{label || t("createNew")}
- {bulkPath && (
-
- )}
+ {/* {bulkPath && } */}
);
};
diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx
index 06a140c54..e9f0d3ad7 100644
--- a/client/src/Components/Sidebar/index.jsx
+++ b/client/src/Components/Sidebar/index.jsx
@@ -8,7 +8,6 @@ import {
ListItemButton,
ListItemIcon,
ListItemText,
- ListSubheader,
Menu,
MenuItem,
Stack,
@@ -39,6 +38,7 @@ import Docs from "../../assets/icons/docs.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
import Discussions from "../../assets/icons/discussions.svg?react";
import Notifications from "../../assets/icons/notifications.svg?react";
+import Logs from "../../assets/icons/logs.svg?react";
import "./index.css";
@@ -64,6 +64,7 @@ const getMenu = (t) => [
{ name: t("menu.statusPages"), path: "status", icon: },
{ name: t("menu.maintenance"), path: "maintenance", icon: },
+ { name: t("menu.logs"), path: "logs", icon: },
{
name: t("menu.settings"),
diff --git a/client/src/Components/StatusBoxes/index.jsx b/client/src/Components/StatusBoxes/index.jsx
index 40ea2d182..d97bb6211 100644
--- a/client/src/Components/StatusBoxes/index.jsx
+++ b/client/src/Components/StatusBoxes/index.jsx
@@ -4,7 +4,7 @@ import SkeletonLayout from "./skeleton";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
-const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => {
+const StatusBoxes = ({ shouldRender = true, flexWrap = "nowrap", children }) => {
const theme = useTheme();
if (!shouldRender) {
return (
diff --git a/client/src/Components/Table/TableUtils.js b/client/src/Components/Table/TableUtils.js
new file mode 100644
index 000000000..a9f95a5b7
--- /dev/null
+++ b/client/src/Components/Table/TableUtils.js
@@ -0,0 +1,11 @@
+export const createHeaderFactory = (getCellSx = () => {}) => {
+ return ({ id, content, onClick = () => {}, render = () => {} }) => {
+ return {
+ id,
+ content,
+ onClick,
+ getCellSx,
+ render,
+ };
+ };
+};
diff --git a/client/src/Components/TextLink/index.jsx b/client/src/Components/TextLink/index.jsx
index a10839e84..83ae251a5 100644
--- a/client/src/Components/TextLink/index.jsx
+++ b/client/src/Components/TextLink/index.jsx
@@ -5,7 +5,7 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Link as RouterLink } from "react-router-dom";
-const TextLink = ({ text, linkText, href }) => {
+const TextLink = ({ text, linkText, href, target = "_self" }) => {
const theme = useTheme();
return (
@@ -18,6 +18,7 @@ const TextLink = ({ text, linkText, href }) => {
color="accent"
to={href}
component={RouterLink}
+ target={target}
>
{linkText}
@@ -29,6 +30,7 @@ TextLink.propTypes = {
text: PropTypes.string,
linkText: PropTypes.string,
href: PropTypes.string,
+ target: PropTypes.string,
};
export default TextLink;
diff --git a/client/src/Hooks/logHooks.js b/client/src/Hooks/logHooks.js
new file mode 100644
index 000000000..000e4d0f0
--- /dev/null
+++ b/client/src/Hooks/logHooks.js
@@ -0,0 +1,35 @@
+import { useState, useEffect } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
+
+const useFetchLogs = () => {
+ const { t } = useTranslation();
+ const [logs, setLogs] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ useEffect(() => {
+ const fetchLogs = async () => {
+ try {
+ setIsLoading(true);
+ const response = await networkService.getLogs();
+ setLogs(response.data.data);
+ createToast({
+ body: t("logsPage.toast.fetchLogsSuccess"),
+ });
+ } catch (error) {
+ setError(error);
+ createToast({
+ message: error.message,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchLogs();
+ }, [t]);
+ return [logs, isLoading, error];
+};
+
+export { useFetchLogs };
diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js
index 7e46438e0..013e7fc25 100644
--- a/client/src/Hooks/monitorHooks.js
+++ b/client/src/Hooks/monitorHooks.js
@@ -453,6 +453,39 @@ const useCreateBulkMonitors = () => {
return [createBulkMonitors, isLoading];
};
+const useExportMonitors = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const { t } = useTranslation();
+
+ const exportMonitors = async () => {
+ setIsLoading(true);
+ try {
+ const response = await networkService.exportMonitors();
+
+ // Create a download link
+ const url = window.URL.createObjectURL(response.data);
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", "monitors.csv");
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+
+ createToast({ body: t("export.success") });
+ return [true, null];
+ } catch (err) {
+ const errorMessage = err?.response?.data?.msg || err.message;
+ createToast({ body: errorMessage || t("export.failed") });
+ return [false, errorMessage];
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [exportMonitors, isLoading];
+};
+
export {
useFetchMonitorsWithSummary,
useFetchMonitorsWithChecks,
@@ -469,4 +502,5 @@ export {
useDeleteAllMonitors,
useDeleteMonitorStats,
useCreateBulkMonitors,
+ useExportMonitors,
};
diff --git a/client/src/Hooks/queueHooks.js b/client/src/Hooks/queueHooks.js
new file mode 100644
index 000000000..f7a25b3d3
--- /dev/null
+++ b/client/src/Hooks/queueHooks.js
@@ -0,0 +1,57 @@
+import { useState, useEffect } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+
+const useFetchQueueData = (trigger) => {
+ const [jobs, setJobs] = useState(undefined);
+ const [metrics, setMetrics] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ useEffect(() => {
+ const fetchJobs = async () => {
+ try {
+ setIsLoading(true);
+ const response = await networkService.getQueueData();
+ if (response.status === 200) {
+ setJobs(response.data.data.jobs);
+ setMetrics(response.data.data.metrics);
+ }
+ } catch (error) {
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchJobs();
+ }, [trigger]);
+
+ return [jobs, metrics, isLoading, error];
+};
+
+const useFlushQueue = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ const flushQueue = async (trigger, setTrigger) => {
+ try {
+ setIsLoading(true);
+ await networkService.flushQueue();
+ createToast({
+ body: "Queue flushed",
+ });
+ } catch (error) {
+ setError(error);
+ createToast({
+ body: error.message,
+ });
+ } finally {
+ setIsLoading(false);
+ setTrigger(!trigger);
+ }
+ };
+ return [flushQueue, isLoading, error];
+};
+
+export { useFetchQueueData, useFlushQueue };
diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js
index 303453655..cc6c3e875 100644
--- a/client/src/Hooks/useNotifications.js
+++ b/client/src/Hooks/useNotifications.js
@@ -98,12 +98,9 @@ const useGetNotificationById = (id, setNotification) => {
const notification = response?.data?.data ?? null;
const notificationData = {
- userId: notification?.userId,
- teamId: notification?.teamId,
address: notification?.address,
notificationName: notification?.notificationName,
type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id,
- config: notification?.config,
};
setNotification(notificationData);
@@ -164,7 +161,7 @@ const useTestNotification = () => {
} catch (error) {
setError(error);
createToast({
- body: t("notifications.test.failed"),
+ body: error?.response?.data?.msg || t("notifications.test.failed"),
});
} finally {
setIsLoading(false);
diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx
new file mode 100644
index 000000000..6c5e1a301
--- /dev/null
+++ b/client/src/Pages/Logs/Logs/index.jsx
@@ -0,0 +1,97 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Select from "../../../Components/Inputs/Select";
+import Typography from "@mui/material/Typography";
+
+import { useFetchLogs } from "../../../Hooks/logHooks";
+import { useTheme } from "@emotion/react";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+
+const formatLog = (theme, log, idx) => {
+ const levelColors = {
+ info: theme.palette.success.main,
+ warn: theme.palette.warning.main,
+ error: theme.palette.error.main,
+ debug: theme.palette.accent.main,
+ };
+
+ const color = levelColors[log.level] || theme.palette.primary.contrastText;
+
+ return (
+
+ [{log.timestamp}]{" "}
+ {log.level.toUpperCase()}
+ {": "}
+ {`(${log.service})`}
+ {`(${log.method})`}
+ {": "}
+ {log.message}
+
+
+ );
+};
+
+const Logs = () => {
+ // Local state
+ const [logLevel, setLogLevel] = useState("all");
+
+ // Hooks
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const [logs, isLoading, error] = useFetchLogs();
+ // Setup
+ const LOG_LEVELS = [
+ { _id: "all", name: t("logsPage.logLevelSelect.values.all") },
+ { _id: "info", name: t("logsPage.logLevelSelect.values.info") },
+ { _id: "warn", name: t("logsPage.logLevelSelect.values.warn") },
+ { _id: "error", name: t("logsPage.logLevelSelect.values.error") },
+ { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
+ ];
+ return (
+
+
+ {t("logsPage.description")}
+
+
+ {t("logsPage.logLevelSelect.title")}
+
+
+
+
+ {logs
+ ?.filter((log) => {
+ if (logLevel === "all") return true;
+ return log.level === logLevel;
+ })
+ .reverse()
+ .map((log, idx) => formatLog(theme, log, idx))}
+
+
+
+ );
+};
+
+export default Logs;
diff --git a/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx b/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx
new file mode 100644
index 000000000..94fc2f5c3
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx
@@ -0,0 +1,82 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import DataTable from "../../../../../Components/Table";
+
+import { useNavigate } from "react-router-dom";
+import { useTheme } from "@emotion/react";
+import { TypeToPathMap } from "../../../../../Utils/monitorUtils";
+import PropTypes from "prop-types";
+import { useTranslation } from "react-i18next";
+
+const FailedJobTable = ({ metrics = {} }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const jobsWithFailures = metrics?.jobsWithFailures;
+ const navigate = useNavigate();
+
+ const headers = [
+ {
+ id: "monitorId",
+ content: t("queuePage.failedJobTable.monitorIdHeader"),
+ render: (row) => {
+ return row.monitorId;
+ },
+ },
+ {
+ id: "monitorUrl",
+ content: t("queuePage.failedJobTable.monitorUrlHeader"),
+ render: (row) => {
+ return row.monitorUrl;
+ },
+ },
+ {
+ id: "failCount",
+ content: t("queuePage.failedJobTable.failCountHeader"),
+ render: (row) => {
+ return row.failCount;
+ },
+ },
+ {
+ id: "failedAt",
+ content: t("queuePage.failedJobTable.failedAtHeader"),
+ render: (row) => {
+ return row.failedAt;
+ },
+ },
+ {
+ id: "failReason",
+ content: t("queuePage.failedJobTable.failReasonHeader"),
+ render: (row) => {
+ return row.failReason;
+ },
+ },
+ ];
+ return (
+
+ {t("queuePage.failedJobTable.title")}
+ {
+ const path = TypeToPathMap[row.monitorType];
+ navigate(`/${path}/${row.monitorId}`);
+ },
+ rowSX: {
+ cursor: "pointer",
+ "&:hover td": {
+ backgroundColor: theme.palette.tertiary.main,
+ transition: "background-color .3s ease",
+ },
+ },
+ }}
+ />
+
+ );
+};
+
+FailedJobTable.propTypes = {
+ metrics: PropTypes.object,
+};
+
+export default FailedJobTable;
diff --git a/client/src/Pages/Logs/Queue/components/JobTable/index.jsx b/client/src/Pages/Logs/Queue/components/JobTable/index.jsx
new file mode 100644
index 000000000..e27dd103f
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/components/JobTable/index.jsx
@@ -0,0 +1,125 @@
+import Stack from "@mui/material/Stack";
+import DataTable from "../../../../../Components/Table";
+import Typography from "@mui/material/Typography";
+// Utils
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import { useNavigate } from "react-router-dom";
+import { TypeToPathMap } from "../../../../../Utils/monitorUtils";
+import { useTranslation } from "react-i18next";
+import { createHeaderFactory } from "../../../../../Components/Table/TableUtils";
+
+const JobTable = ({ jobs = [] }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const buildSx = (row) => {
+ if (row.lockedAt) {
+ return {
+ color: `${theme.palette.success.main} !important`,
+ };
+ }
+ if (!row.active) {
+ return {
+ color: `${theme.palette.warning.main} !important`,
+ };
+ }
+
+ if (row.failCount > 0 && row.lastFailedAt >= row.lastFinishedAt) {
+ return {
+ color: `${theme.palette.error.main} !important`,
+ };
+ }
+
+ return {};
+ };
+
+ const createHeader = createHeaderFactory(buildSx);
+ const headersData = [
+ {
+ id: "id",
+ content: t("queuePage.jobTable.idHeader"),
+ render: (row) => row.monitorId,
+ },
+ {
+ id: "url",
+ content: t("queuePage.jobTable.urlHeader"),
+ render: (row) => row.monitorUrl,
+ },
+ {
+ id: "type",
+ content: t("queuePage.jobTable.typeHeader"),
+ render: (row) => row.monitorType,
+ },
+ {
+ id: "active",
+ content: t("queuePage.jobTable.activeHeader"),
+ render: (row) => row.active.toString(),
+ },
+ {
+ id: "runCount",
+ content: t("queuePage.jobTable.runCountHeader"),
+ render: (row) => row.runCount,
+ },
+ {
+ id: "failCount",
+ content: t("queuePage.jobTable.failCountHeader"),
+ render: (row) => row.failCount,
+ },
+ {
+ id: "lastRun",
+ content: t("queuePage.jobTable.lastRunHeader"),
+ render: (row) => row.lastRunAt || "-",
+ },
+ {
+ id: "lockedAt",
+ content: t("queuePage.jobTable.lockedAtHeader"),
+ render: (row) => row.lockedAt || "-",
+ },
+
+ {
+ id: "lastFinish",
+ content: t("queuePage.jobTable.lastFinishedAtHeader"),
+ render: (row) => row.lastFinishedAt || "-",
+ },
+ {
+ id: "lastRunTook",
+ content: t("queuePage.jobTable.lastRunTookHeader"),
+ render: (row) => {
+ const value = row.lastRunTook ? row.lastRunTook + " ms" : "-";
+ return value;
+ },
+ },
+ ];
+
+ const headers = headersData.map((header) => createHeader(header));
+
+ return (
+
+ {t("queuePage.jobTable.title")}
+ {
+ const path = TypeToPathMap[row.monitorType];
+ navigate(`/${path}/${row.monitorId}`);
+ },
+ rowSX: {
+ cursor: "pointer",
+ "&:hover td": {
+ backgroundColor: theme.palette.tertiary.main,
+ transition: "background-color .3s ease",
+ },
+ },
+ }}
+ />
+
+ );
+};
+
+JobTable.propTypes = {
+ jobs: PropTypes.array,
+};
+
+export default JobTable;
diff --git a/client/src/Pages/Logs/Queue/components/Metrics/index.jsx b/client/src/Pages/Logs/Queue/components/Metrics/index.jsx
new file mode 100644
index 000000000..af1cf8010
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/components/Metrics/index.jsx
@@ -0,0 +1,43 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import StatBox from "../../../../../Components/StatBox";
+import StatusBoxes from "../../../../../Components/StatusBoxes";
+
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+
+const camelToTitle = (str) => {
+ return str
+ .replace(/([A-Z])/g, " $1")
+ .toLowerCase()
+ .replace(/^./, (m) => m.toUpperCase());
+};
+
+const Metrics = ({ metrics = {} }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ const data = Object.keys(metrics)
+ .filter((key) => key !== "jobsWithFailures")
+ .map((key) => {
+ return { key, title: camelToTitle(key), value: metrics[key] };
+ });
+
+ return (
+
+ {t("queuePage.metricsTable.title")}
+
+ {data.map((metric) => {
+ return (
+
+ );
+ })}
+
+
+ );
+};
+export default Metrics;
diff --git a/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx b/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx
new file mode 100644
index 000000000..204b90035
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx
@@ -0,0 +1,60 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import DataTable from "../../../../../Components/Table";
+
+// Utils
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+import PropTypes from "prop-types";
+
+const camelToTitle = (str) => {
+ return str
+ .replace(/([A-Z])/g, " $1")
+ .toLowerCase()
+ .replace(/^./, (m) => m.toUpperCase());
+};
+
+const Metrics = ({ metrics = {} }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const keys = Object.keys(metrics);
+
+ const headers = [
+ {
+ id: "metric",
+ content: t("queuePage.metricsTable.metricHeader"),
+ render: (row) => {
+ return {row.key};
+ },
+ },
+ {
+ id: "value",
+ content: t("queuePage.metricsTable.valueHeader"),
+ render: (row) => {
+ return {row.value};
+ },
+ },
+ ];
+
+ const data = keys
+ .filter((key) => key !== "jobsWithFailures")
+ .map((key) => {
+ return { key: camelToTitle(key), value: metrics[key] };
+ });
+
+ return (
+
+ {t("queuePage.metricsTable.title")}
+
+
+ );
+};
+
+Metrics.propTypes = {
+ metrics: PropTypes.object,
+};
+
+export default Metrics;
diff --git a/client/src/Pages/Logs/Queue/index.jsx b/client/src/Pages/Logs/Queue/index.jsx
new file mode 100644
index 000000000..729f87fe3
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/index.jsx
@@ -0,0 +1,66 @@
+// Components
+import Stack from "@mui/material/Stack";
+import JobTable from "./components/JobTable";
+import Metrics from "./components/Metrics";
+import FailedJobTable from "./components/FailedJobTable";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import Button from "@mui/material/Button";
+
+// Utils
+import { useState } from "react";
+import { useFetchQueueData, useFlushQueue } from "../../../Hooks/queueHooks";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+
+const QueueDetails = () => {
+ // Local state
+ const [trigger, setTrigger] = useState(false);
+
+ // Hooks
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const [jobs, metrics, isLoading, error] = useFetchQueueData(trigger);
+ const [flushQueue, isFlushing, flushError] = useFlushQueue();
+
+ if (isLoading) return Loading...
;
+ if (error || flushError) return Error: {error.message}
;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default QueueDetails;
diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/Logs/index.jsx
new file mode 100644
index 000000000..482491ad0
--- /dev/null
+++ b/client/src/Pages/Logs/index.jsx
@@ -0,0 +1,41 @@
+import Stack from "@mui/material/Stack";
+import Breadcrumbs from "../../Components/Breadcrumbs";
+import Tabs from "@mui/material/Tabs";
+import Tab from "@mui/material/Tab";
+import Queue from "./Queue";
+import LogsComponent from "./Logs";
+
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { useState } from "react";
+
+const Logs = () => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ // Local state
+ const [value, setValue] = useState(0);
+
+ // Handlers
+ const handleChange = (event, newValue) => {
+ setValue(newValue);
+ };
+
+ const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }];
+ return (
+
+
+
+
+
+
+ {value === 0 && }
+ {value === 1 && }
+
+ );
+};
+
+export default Logs;
diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
index a798838cb..bb5842e9f 100644
--- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
+++ b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
@@ -11,6 +11,8 @@ import { formatDurationRounded } from "../../../Utils/timeUtils";
import { StatusLabel } from "../../../Components/Label";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+import { useNavigate } from "react-router-dom";
import dayjs from "dayjs";
/**
* Component for pagination actions (first, previous, next, last).
@@ -36,7 +38,8 @@ const MaintenanceTable = ({
}) => {
const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5);
const dispatch = useDispatch();
-
+ const theme = useTheme();
+ const navigate = useNavigate();
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
@@ -180,6 +183,18 @@ const MaintenanceTable = ({
return (
<>
{
+ navigate(`/maintenance/create/${row._id}`);
+ },
+ }}
headers={headers}
data={maintenanceWindows}
/>
diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx
index 2fd83e4e1..0fb6cce08 100644
--- a/client/src/Pages/Notifications/create/index.jsx
+++ b/client/src/Pages/Notifications/create/index.jsx
@@ -17,15 +17,17 @@ import {
useEditNotification,
useTestNotification,
} from "../../../Hooks/useNotifications";
-import {
- notificationEmailValidation,
- notificationWebhookValidation,
- notificationPagerDutyValidation,
-} from "../../../Validation/validation";
+import { notificationValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
-import { NOTIFICATION_TYPES } from "../utils";
+import {
+ NOTIFICATION_TYPES,
+ TITLE_MAP,
+ DESCRIPTION_MAP,
+ LABEL_MAP,
+ PLACEHOLDER_MAP,
+} from "../utils";
// Setup
@@ -49,11 +51,6 @@ const CreateNotifications = () => {
notificationName: "",
address: "",
type: NOTIFICATION_TYPES[0]._id,
- config: {
- webhookUrl: "",
- platform: "",
- routingKey: "",
- },
});
const [errors, setErrors] = useState({});
const { t } = useTranslation();
@@ -71,42 +68,17 @@ const CreateNotifications = () => {
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
};
- if (form.type === "slack" || form.type === "discord") {
- form.type = "webhook";
- }
-
let error = null;
- if (form.type === "email") {
- error = notificationEmailValidation.validate(
- { notificationName: form.notificationName, address: form.address },
- { abortEarly: false }
- ).error;
- } else if (form.type === "webhook") {
- form.config = {
- platform: form.config.platform,
- webhookUrl: form.config.webhookUrl,
- };
- error = notificationWebhookValidation.validate(
- { notificationName: form.notificationName, config: form.config },
- { abortEarly: false }
- ).error;
- } else if (form.type === "pager_duty") {
- form.config = {
- platform: form.config.platform,
- routingKey: form.config.routingKey,
- };
- error = notificationPagerDutyValidation.validate(
- { notificationName: form.notificationName, config: form.config },
- { abortEarly: false }
- ).error;
- }
+ error = notificationValidation.validate(form, { abortEarly: false }).error;
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
+ console.log(JSON.stringify(newErrors));
+ console.log(JSON.stringify(form, null, 2));
createToast({ body: "Please check the form for errors." });
setErrors(newErrors);
return;
@@ -124,63 +96,10 @@ const CreateNotifications = () => {
const newNotification = { ...notification, [name]: value };
- // Handle config/platform initialization if type is webhook
-
- const type = NOTIFICATION_TYPES.find(
- (type) => type._id === newNotification.type
- ).value;
-
- if (type === "email") {
- newNotification.config = null;
- } else if (type === "slack" || type === "webhook" || type === "discord") {
- newNotification.address = "";
- newNotification.config = newNotification.config || {};
- if (name === "config") {
- newNotification.config = value;
- }
- if (type === "webhook") {
- newNotification.config.platform = "webhook";
- }
- if (type === "slack") {
- newNotification.config.platform = "slack";
- }
- if (type === "discord") {
- newNotification.config.platform = "discord";
- }
- } else if (type === "pager_duty") {
- newNotification.config = newNotification.config || {};
- if (name === "config") {
- newNotification.config = value;
- }
- newNotification.config.platform = "pager_duty";
- }
-
- // Field-level validation
- let fieldError;
-
- if (name === "notificationName") {
- const { error } = notificationEmailValidation.extract(name).validate(value);
- fieldError = error?.message;
- }
-
- if (type === "email" && name === "address") {
- const { error } = notificationEmailValidation.extract(name).validate(value);
- fieldError = error?.message;
- }
-
- if (
- (type === "slack" || type === "webhook" || type === "discord") &&
- name === "config"
- ) {
- // Validate only webhookUrl inside config
- const { error } = notificationWebhookValidation.extract("config").validate(value);
- fieldError = error?.message;
- }
-
- // Set field-level error
+ const { error } = notificationValidation.extract(name).validate(value);
setErrors((prev) => ({
...prev,
- [name]: fieldError,
+ [name]: error?.message,
}));
setNotification(newNotification);
@@ -192,36 +111,9 @@ const CreateNotifications = () => {
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
};
- if (form.type === "slack" || form.type === "discord") {
- form.type = "webhook";
- }
-
let error = null;
- if (form.type === "email") {
- error = notificationEmailValidation.validate(
- { notificationName: form.notificationName, address: form.address },
- { abortEarly: false }
- ).error;
- } else if (form.type === "webhook") {
- form.config = {
- platform: form.config.platform,
- webhookUrl: form.config.webhookUrl,
- };
- error = notificationWebhookValidation.validate(
- { notificationName: form.notificationName, config: form.config },
- { abortEarly: false }
- ).error;
- } else if (form.type === "pager_duty") {
- form.config = {
- platform: form.config.platform,
- routingKey: form.config.routingKey,
- };
- error = notificationPagerDutyValidation.validate(
- { notificationName: form.notificationName, config: form.config },
- { abortEarly: false }
- ).error;
- }
+ error = notificationValidation.validate(form, { abortEarly: false }).error;
if (error) {
const newErrors = {};
@@ -236,6 +128,7 @@ const CreateNotifications = () => {
testNotification(form);
};
+ const type = NOTIFICATION_TYPES.find((type) => type._id === notification.type).value;
return (
@@ -287,171 +180,31 @@ const CreateNotifications = () => {
/>
- {notification.type === 1 && (
-
-
-
- {t("createNotifications.emailSettings.title")}
-
-
- {t("createNotifications.emailSettings.description")}
-
-
-
-
-
-
- )}
- {notification.type === 2 && (
-
-
-
- {t("createNotifications.slackSettings.title")}
-
-
- {t("createNotifications.slackSettings.description")}
-
-
-
- {
- const updatedConfig = {
- ...notification.config,
- webhookUrl: e.target.value,
- };
+
+
+ {t(TITLE_MAP[type])}
+ {t(DESCRIPTION_MAP[type])}
+
+
+
+
+
- onChange({
- target: {
- name: "config",
- value: updatedConfig,
- },
- });
- }}
- />
-
-
- )}
- {notification.type === 3 && (
-
-
-
- {t("createNotifications.pagerdutySettings.title")}
-
-
- {t("createNotifications.pagerdutySettings.description")}
-
-
-
- {
- const updatedConfig = {
- ...notification.config,
- routingKey: e.target.value,
- };
-
- onChange({
- target: {
- name: "config",
- value: updatedConfig,
- },
- });
- }}
- />
-
-
- )}
- {notification.type === 4 && (
-
-
-
- {t("createNotifications.genericWebhookSettings.title")}
-
-
- {t("createNotifications.genericWebhookSettings.description")}
-
-
-
- {
- const updatedConfig = {
- ...notification.config,
- webhookUrl: e.target.value,
- };
-
- onChange({
- target: {
- name: "config",
- value: updatedConfig,
- },
- });
- }}
- />
-
-
- )}
- {notification.type === 5 && (
-
-
-
- {t("createNotifications.discordSettings.title")}
-
-
- {t("createNotifications.discordSettings.description")}
-
-
-
- {
- const updatedConfig = {
- ...notification.config,
- webhookUrl: e.target.value,
- };
-
- onChange({
- target: {
- name: "config",
- value: updatedConfig,
- },
- });
- }}
- />
-
-
- )}
- {t("common.appName")} {2.1}
+ {t("common.appName")} {__APP_VERSION__}
- {t("settingsDevelopedBy")}
+ {t("settingsPage.aboutSettings.labelDevelopedBy")}
- {t("settingsDemoMonitors")}
+ {t("settingsPage.demoMonitorsSettings.title")}
+
+
+ {t("settingsPage.demoMonitorsSettings.description")}
- {t("settingsDemoMonitorsDescription")}
- {t("settingsAddDemoMonitors")}
@@ -57,14 +58,13 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
component="h1"
variant="h2"
>
- {t("settingsSystemReset")}
+ {t("settingsPage.systemResetSettings.title")}
- {t("settingsSystemResetDescription")}
+ {t("settingsPage.systemResetSettings.description")}
- {t("settingsRemoveAllMonitors")}