diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml
index d3d4c0d6b..76cb300f1 100644
--- a/.github/workflows/check-format.yml
+++ b/.github/workflows/check-format.yml
@@ -59,4 +59,4 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr close "$PR_NUMBER" --delete-branch --comment "❌ Formatting check failed — PR auto-closed.
- Please run \`npm run format\` and push again."
+ Please run \`npm run format\` in both `client` and `server` directories and push again."
diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml
index f1d0dd7ce..cf06f7840 100644
--- a/.github/workflows/deploy-images-on-release.yml
+++ b/.github/workflows/deploy-images-on-release.yml
@@ -11,6 +11,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
- name: Extract version from tag
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
@@ -93,6 +96,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -119,12 +124,16 @@ jobs:
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
+ build-args: |
+ VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server-mono:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Extract version
id: extract_tag
@@ -143,6 +152,7 @@ jobs:
-t ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ --build-arg VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }} \
.
- name: Push Server Docker image
diff --git a/.github/workflows/deploy-images.yml b/.github/workflows/deploy-images.yml
index d3d70b90b..fa39fed85 100644
--- a/.github/workflows/deploy-images.yml
+++ b/.github/workflows/deploy-images.yml
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
@@ -18,12 +20,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-client:latest \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ --build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
@@ -85,6 +91,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -96,6 +104,10 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
+
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
@@ -107,12 +119,16 @@ jobs:
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
+ build-args: |
+ VITE_APP_VERSION=${{ env.VERSION }}
docker-build-and-push-server-mono:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
@@ -121,12 +137,17 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
+
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend-mono:latest \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ --build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Server Docker image
diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml
index 6956f5caa..7764c488b 100644
--- a/.github/workflows/production-deploy.yml
+++ b/.github/workflows/production-deploy.yml
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
@@ -18,12 +20,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-demo \
-f ./docker/prod/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ --build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml
index 34fa243c6..ea5477815 100644
--- a/.github/workflows/staging-deploy.yml
+++ b/.github/workflows/staging-deploy.yml
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
@@ -18,12 +20,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-staging \
-f ./docker/staging/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ --build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
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/MonitorDetailsControlHeader/index.jsx b/client/src/Components/MonitorDetailsControlHeader/index.jsx
index 5ad665069..1d253e3d5 100644
--- a/client/src/Components/MonitorDetailsControlHeader/index.jsx
+++ b/client/src/Components/MonitorDetailsControlHeader/index.jsx
@@ -83,23 +83,24 @@ const MonitorDetailsControlHeader = ({
>
{t("menu.incidents")}
- :
- }
- onClick={() => {
- pauseMonitor({
- monitorId: monitor?._id,
- triggerUpdate,
- });
- }}
- >
- {monitor?.isActive ? "Pause" : "Resume"}
-
-
+ {isAdmin && (
+ :
+ }
+ onClick={() => {
+ pauseMonitor({
+ monitorId: monitor?._id,
+ triggerUpdate,
+ });
+ }}
+ >
+ {monitor?.isActive ? "Pause" : "Resume"}
+
+ )}
{isAdmin && (
+ {/* */}
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/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx
index bd719bfee..e19ccecae 100644
--- a/client/src/Pages/Notifications/components/ActionMenu.jsx
+++ b/client/src/Pages/Notifications/components/ActionMenu.jsx
@@ -18,6 +18,7 @@ const ActionMenu = ({ notification, onDelete }) => {
// Handlers
const handleClick = (event) => {
+ event.stopPropagation();
setAnchorEl(event.currentTarget);
};
@@ -25,7 +26,8 @@ const ActionMenu = ({ notification, onDelete }) => {
setAnchorEl(null);
};
- const handleRemove = () => {
+ const handleRemove = (e) => {
+ e.stopPropagation();
onDelete(notification._id);
handleClose();
};
diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx
index fba30185f..0fb6cce08 100644
--- a/client/src/Pages/Notifications/create/index.jsx
+++ b/client/src/Pages/Notifications/create/index.jsx
@@ -204,7 +204,7 @@ const CreateNotifications = () => {
spacing={theme.spacing(2)}
>
- {t("settingsEmailHost")}
+ {t("settingsPage.emailSettings.labelHost")}
- {t("settingsEmailPort")}
+ {t("settingsPage.emailSettings.labelPort")}
- {t("settingsEmailUser")}
+ {t("settingsPage.emailSettings.labelUser")}
- {t("settingsEmailAddress")}
+ {t("settingsPage.emailSettings.labelAddress")}
{(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
- {t("settingsEmailPassword")}
+ {t("settingsPage.emailSettings.labelPassword")}
- {t("settingsEmailFieldResetLabel")}
+ {t("settingsPage.emailSettings.labelPasswordSet")}
{
setPassword("");
@@ -188,7 +191,7 @@ const SettingsEmail = ({
)}
- {t("settingsEmailTLSServername")}
+ {t("settingsPage.emailSettings.labelTLSServername")}
- {t("settingsEmailConnectionHost")}
+ {t("settingsPage.emailSettings.labelConnectionHost")}
- {t("settingsEmailSecure")}
-
- {t("settingsEmailPool")}
-
- {t("settingsEmailIgnoreTLS")}
-
- {t("settingsEmailRequireTLS")}
-
- {t("settingsEmailRejectUnauthorized")}
- (
+
+ {t(labelKey)}
+
+
+ ))}
+
+
+
+
+ {JSON.stringify(
+ {
+ host: systemEmailHost,
+ port: systemEmailPort,
+ secure: systemEmailSecure,
+ auth: {
+ user: systemEmailUser || systemEmailAddress,
+ pass: "",
+ },
+ name: systemEmailConnectionHost || "localhost",
+ pool: systemEmailPool,
+ tls: {
+ rejectUnauthorized: systemEmailRejectUnauthorized,
+ ignoreTLS: systemEmailIgnoreTLS,
+ requireTLS: systemEmailRequireTLS,
+ servername: systemEmailTLSServername,
+ },
+ },
+ null,
+ 2
+ )}
+
+
+
{systemEmailHost &&
systemEmailPort &&
@@ -254,7 +311,7 @@ const SettingsEmail = ({
loading={isSending}
onClick={handleSendTestEmail}
>
- {t("settingsTestEmail", "Send test e-mail")}
+ {t("settingsPage.emailSettings.buttonSendTestEmail")}
)}
diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/Settings/SettingsPagespeed.jsx
index 7d7cf0216..157c75dac 100644
--- a/client/src/Pages/Settings/SettingsPagespeed.jsx
+++ b/client/src/Pages/Settings/SettingsPagespeed.jsx
@@ -8,7 +8,7 @@ import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornmen
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
-import { useState, useEffect } from "react";
+import { useState } from "react";
import { useTranslation } from "react-i18next";
const SettingsPagespeed = ({
@@ -46,15 +46,17 @@ const SettingsPagespeed = ({
component="h1"
variant="h2"
>
- {t("pageSpeedApiKeyFieldTitle")}
+ {t("settingsPage.pageSpeedSettings.title")}
+
+
+ {t("settingsPage.pageSpeedSettings.description")}
- {t("pageSpeedApiKeyFieldDescription")}
{(isApiKeySet === false || apiKeyHasBeenReset === true) && (
- {t("pageSpeedApiKeyFieldResetLabel")}
+ {t("settingsPage.pageSpeedSettings.labelApiKeySet")}
{
setApiKey("");
diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/Settings/SettingsStats.jsx
index 333dfd229..740ac3a37 100644
--- a/client/src/Pages/Settings/SettingsStats.jsx
+++ b/client/src/Pages/Settings/SettingsStats.jsx
@@ -29,17 +29,17 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
variant="h2"
sx={HEADING_SX}
>
- {t("settingsHistoryAndMonitoring")}
+ {t("settingsPage.statsSettings.title")}
- {t("settingsHistoryAndMonitoringDescription")}
+ {t("settingsPage.statsSettings.description")}
- {t("settingsClearAllStats")}
+
+ {t("settingsPage.statsSettings.clearAllStatsDescription")}
+
setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
- {t("settingsClearAllStatsButton")}
+ {t("settingsPage.statsSettings.clearAllStatsButton")}