diff --git a/.github/workflows/docker-deploy.yml b/.github/workflows/docker-deploy.yml
index 135eaf2b..f0ba0b1d 100644
--- a/.github/workflows/docker-deploy.yml
+++ b/.github/workflows/docker-deploy.yml
@@ -45,13 +45,13 @@ jobs:
uses: docker/setup-buildx-action@v3.11.1
- name: Login to DockerHub 🔑
- uses: docker/login-action@v3.4.0
+ uses: docker/login-action@v3.5.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry 🔑
- uses: docker/login-action@v3.4.0
+ uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
diff --git a/package-lock.json b/package-lock.json
index fc91a54e..11b8971b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"@hookform/resolvers": "5.1.1",
"@tanstack/react-table": "8.21.3",
"ahooks": "3.9.0",
- "axios": "1.10.0",
+ "axios": "1.11.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -24,10 +24,10 @@
"react-day-picker": "9.8.0",
"react-dom": "19.1.0",
"react-hook-form": "7.60.0",
- "react-router-dom": "7.6.3",
+ "react-router-dom": "7.7.0",
"sonner": "2.0.6",
"tailwind-merge": "3.3.1",
- "zod": "3.25.74"
+ "zod": "4.0.5"
},
"devDependencies": {
"@commitlint/cli": "19.8.1",
@@ -39,7 +39,7 @@
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@unocss/eslint-config": "66.3.3",
- "@vitejs/plugin-react-swc": "3.10.2",
+ "@vitejs/plugin-react-swc": "4.0.0",
"browserslist": "4.25.1",
"eslint": "9.30.1",
"eslint-plugin-react": "7.37.5",
@@ -2894,9 +2894,9 @@
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-beta.11",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
- "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==",
+ "version": "1.0.0-beta.30",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz",
+ "integrity": "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==",
"dev": true,
"license": "MIT"
},
@@ -3204,9 +3204,9 @@
}
},
"node_modules/@swc/core": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.9.tgz",
- "integrity": "sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz",
+ "integrity": "sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -3222,16 +3222,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
- "@swc/core-darwin-arm64": "1.12.9",
- "@swc/core-darwin-x64": "1.12.9",
- "@swc/core-linux-arm-gnueabihf": "1.12.9",
- "@swc/core-linux-arm64-gnu": "1.12.9",
- "@swc/core-linux-arm64-musl": "1.12.9",
- "@swc/core-linux-x64-gnu": "1.12.9",
- "@swc/core-linux-x64-musl": "1.12.9",
- "@swc/core-win32-arm64-msvc": "1.12.9",
- "@swc/core-win32-ia32-msvc": "1.12.9",
- "@swc/core-win32-x64-msvc": "1.12.9"
+ "@swc/core-darwin-arm64": "1.13.3",
+ "@swc/core-darwin-x64": "1.13.3",
+ "@swc/core-linux-arm-gnueabihf": "1.13.3",
+ "@swc/core-linux-arm64-gnu": "1.13.3",
+ "@swc/core-linux-arm64-musl": "1.13.3",
+ "@swc/core-linux-x64-gnu": "1.13.3",
+ "@swc/core-linux-x64-musl": "1.13.3",
+ "@swc/core-win32-arm64-msvc": "1.13.3",
+ "@swc/core-win32-ia32-msvc": "1.13.3",
+ "@swc/core-win32-x64-msvc": "1.13.3"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
@@ -3243,9 +3243,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.9.tgz",
- "integrity": "sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.3.tgz",
+ "integrity": "sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==",
"cpu": [
"arm64"
],
@@ -3260,9 +3260,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.9.tgz",
- "integrity": "sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.3.tgz",
+ "integrity": "sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==",
"cpu": [
"x64"
],
@@ -3277,9 +3277,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.9.tgz",
- "integrity": "sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.3.tgz",
+ "integrity": "sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==",
"cpu": [
"arm"
],
@@ -3294,9 +3294,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.9.tgz",
- "integrity": "sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.3.tgz",
+ "integrity": "sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==",
"cpu": [
"arm64"
],
@@ -3311,9 +3311,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.9.tgz",
- "integrity": "sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.3.tgz",
+ "integrity": "sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==",
"cpu": [
"arm64"
],
@@ -3328,9 +3328,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.9.tgz",
- "integrity": "sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz",
+ "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==",
"cpu": [
"x64"
],
@@ -3345,9 +3345,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.9.tgz",
- "integrity": "sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.3.tgz",
+ "integrity": "sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==",
"cpu": [
"x64"
],
@@ -3362,9 +3362,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.9.tgz",
- "integrity": "sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.3.tgz",
+ "integrity": "sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==",
"cpu": [
"arm64"
],
@@ -3379,9 +3379,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.9.tgz",
- "integrity": "sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.3.tgz",
+ "integrity": "sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==",
"cpu": [
"ia32"
],
@@ -3396,9 +3396,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
- "version": "1.12.9",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.9.tgz",
- "integrity": "sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.3.tgz",
+ "integrity": "sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==",
"cpu": [
"x64"
],
@@ -4074,17 +4074,20 @@
}
},
"node_modules/@vitejs/plugin-react-swc": {
- "version": "3.10.2",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz",
- "integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.0.0.tgz",
+ "integrity": "sha512-4A1dThI578v07mpG4M+ziNn6lmlMlhtVCheL+2WLvClnLvEULi8rpAZThn2oEKn3GtFXFTOeko6eLRhx2V2kgA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@rolldown/pluginutils": "1.0.0-beta.11",
- "@swc/core": "^1.11.31"
+ "@rolldown/pluginutils": "1.0.0-beta.30",
+ "@swc/core": "^1.13.2"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
- "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0"
+ "vite": "^4 || ^5 || ^6 || ^7"
}
},
"node_modules/@vue/compiler-core": {
@@ -4506,13 +4509,13 @@
}
},
"node_modules/axios": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
- "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -4662,9 +4665,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001727",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
- "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
+ "version": "1.0.30001760",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
+ "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
"dev": true,
"funding": [
{
@@ -6148,9 +6151,9 @@
}
},
"node_modules/form-data": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
- "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -8421,9 +8424,9 @@
}
},
"node_modules/react-router": {
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz",
- "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.0.tgz",
+ "integrity": "sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -8443,12 +8446,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.3.tgz",
- "integrity": "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.0.tgz",
+ "integrity": "sha512-wwGS19VkNBkneVh9/YD0pK3IsjWxQUVMDD6drlG7eJpo1rXBtctBqDyBm/k+oKHRAm1x9XWT3JFC82QI9YOXXA==",
"license": "MIT",
"dependencies": {
- "react-router": "7.6.3"
+ "react-router": "7.7.0"
},
"engines": {
"node": ">=20.0.0"
@@ -9945,9 +9948,9 @@
}
},
"node_modules/zod": {
- "version": "3.25.74",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.74.tgz",
- "integrity": "sha512-J8poo92VuhKjNknViHRAIuuN6li/EwFbAC8OedzI8uxpEPGiXHGQu9wemIAioIpqgfB4SySaJhdk0mH5Y4ICBg==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz",
+ "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 5b1b7d74..b03af013 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
"@hookform/resolvers": "5.1.1",
"@tanstack/react-table": "8.21.3",
"ahooks": "3.9.0",
- "axios": "1.10.0",
+ "axios": "1.11.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -39,10 +39,10 @@
"react-day-picker": "9.8.0",
"react-dom": "19.1.0",
"react-hook-form": "7.60.0",
- "react-router-dom": "7.6.3",
+ "react-router-dom": "7.7.0",
"sonner": "2.0.6",
"tailwind-merge": "3.3.1",
- "zod": "3.25.74"
+ "zod": "4.0.5"
},
"devDependencies": {
"@commitlint/cli": "19.8.1",
@@ -54,7 +54,7 @@
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@unocss/eslint-config": "66.3.3",
- "@vitejs/plugin-react-swc": "3.10.2",
+ "@vitejs/plugin-react-swc": "4.0.0",
"browserslist": "4.25.1",
"eslint": "9.30.1",
"eslint-plugin-react": "7.37.5",
diff --git a/public/favicon.ico b/public/favicon.ico
index 14593e76..1f6963f9 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/src/App.jsx b/src/App.jsx
index 2ea43d7b..e93064aa 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -17,6 +17,14 @@ import { Dashboards } from '@/pages/app/Dashboards';
import { DashboardDetails } from '@/pages/app/dashboard/DashboardDetails';
import { FolderDetails } from '@/pages/app/dashboard/FolderDetails';
import { Editor } from '@/pages/app/Editor';
+import { Datasources } from '@/pages/app/datasource/Datasources';
+import { DatasourceCreate } from '@/pages/app/datasource/DatasourceCreate';
+import { DatasourceDetail } from '@/pages/app/datasource/DatasourceDetail';
+import { DatasourceEdit } from '@/pages/app/datasource/DatasourceEdit';
+import { Linkers } from '@/pages/app/linker/Linkers';
+import { LinkerCreate } from '@/pages/app/linker/LinkerCreate';
+import { LinkerDetail } from '@/pages/app/linker/LinkerDetail';
+import { LinkerEdit } from '@/pages/app/linker/LinkerEdit';
import MainLayout from '@/layouts/MainLayout';
import AppLayout from '@/layouts/AppLayout';
import { Toaster } from '@/components/ui/sonner';
@@ -63,6 +71,18 @@ function App() {
} />
} />
+
+ } />
+ } />
+ } />
+ } />
+
+
+ } />
+ } />
+ } />
+ } />
+
} />
} />
} />
diff --git a/src/components/basic-page.jsx b/src/components/basic-page.jsx
index 04a8cd5e..e28489e6 100644
--- a/src/components/basic-page.jsx
+++ b/src/components/basic-page.jsx
@@ -17,6 +17,14 @@ function generateBreadcrumbs(path, additionalData = {}) {
const isCatalogId = segments[index-1] === 'catalogs' && !isNaN(segment);
const catalogData = isCatalogId && additionalData.catalogData ? additionalData.catalogData : null;
+ // Detect if it's a datasource ID and if we have data for it
+ const isDatasourceId = segments[index-1] === 'datasources' && additionalData.datasource;
+ const datasourceData = isDatasourceId ? additionalData.datasource : null;
+
+ // Detect if it's a linker ID and if we have data for it
+ const isLinkerId = segments[index-1] === 'linkers' && additionalData.linker;
+ const linkerData = isLinkerId ? additionalData.linker : null;
+
// Detect if we are in the control details view
const isControlView = segments[index-1] === 'controls' && additionalData.control;
@@ -38,6 +46,10 @@ function generateBreadcrumbs(path, additionalData = {}) {
name = 'Home';
} else if (catalogData && isCatalogId) {
name = catalogData.name || segment;
+ } else if (datasourceData && isDatasourceId) {
+ name = datasourceData.name || segment;
+ } else if (linkerData && isLinkerId) {
+ name = linkerData.name || 'Unnamed Linker';
} else if (segment === 'controls') {
const catalogHref = '/' + segments.slice(0, index).join('/');
return { name: 'Controls', href: catalogHref, isLast, state: additionalData };
@@ -66,7 +78,7 @@ function generateBreadcrumbs(path, additionalData = {}) {
href,
isLast,
isClickable,
- state: (segments[index-1] === 'catalogs' || segments[index] === 'catalogs' || isFolder || isDashboard || isEditor) ? additionalData : null,
+ state: (segments[index-1] === 'catalogs' || segments[index] === 'catalogs' || segments[index-1] === 'datasources' || segments[index] === 'datasources' || segments[index-1] === 'linkers' || segments[index] === 'linkers' || isFolder || isDashboard || isEditor) ? additionalData : null,
uniqueKey: segments.slice(0, index + 1).join('/')
};
}).filter(crumb => crumb.name !== 'Home' || crumb.isLast);
@@ -80,6 +92,8 @@ export default function Page({ children, ...props }) {
const controlData = props.control || location.state?.control;
const folderData = props.folder || location.state?.folder;
const dashboardData = props.dashboard || location.state?.dashboard;
+ const datasourceData = props.datasource || location.state?.datasource;
+ const linkerData = props.linker || location.state?.linker;
const flowName = props.flowName || location.state?.flowName;
const computationDate = props.computationDate || location.state?.computationDate;
@@ -88,6 +102,8 @@ export default function Page({ children, ...props }) {
control: controlData,
folderData,
dashboardData,
+ datasource: datasourceData,
+ linker: linkerData,
flowName,
computationDate
};
@@ -96,7 +112,7 @@ export default function Page({ children, ...props }) {
return (
-
+
diff --git a/src/components/catalog/CatalogControlsStep.jsx b/src/components/catalog/CatalogControlsStep.jsx
index bf21b94a..21e762ca 100644
--- a/src/components/catalog/CatalogControlsStep.jsx
+++ b/src/components/catalog/CatalogControlsStep.jsx
@@ -1,26 +1,17 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Textarea } from '@/components/ui/textarea';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { Loader2, ArrowRight, Plus, Trash, Edit, AlertCircle } from 'lucide-react';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@/components/ui/form';
+import { Loader2, ArrowRight, Plus, Trash, Edit, AlertCircle, ExternalLink } from 'lucide-react';
import {
Card,
CardContent,
CardHeader,
CardTitle
} from '@/components/ui/card';
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import { NewControlForm } from '@/forms/control/new/form';
+import { getControlsByCatalogId, deleteControl, createDraftControl } from '@/services/controls';
+import { createScopeSet } from '@/services/scopes';
+import { toast } from 'sonner';
import {
Dialog,
DialogContent,
@@ -28,60 +19,45 @@ import {
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
-import { cn } from '@/lib/utils';
+import { getScopeSetsByControlId } from '@/services/scopes';
import { Badge } from '@/components/ui/badge';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
-
-// Schema for control validation
-const controlSchema = z.object({
- name: z.string().min(1, { message: 'Control name is required' }),
- description: z.string().min(1, { message: 'Description is required' }),
- type: z.string().min(1, { message: 'Control type is required' }),
- severity: z.string().min(1, { message: 'Severity is required' }),
- implementation: z.string().optional(),
-});
-
-// Control types and severity options
-const controlTypes = [
- { value: 'preventive', label: 'Preventive' },
- { value: 'detective', label: 'Detective' },
- { value: 'corrective', label: 'Corrective' },
- { value: 'deterrent', label: 'Deterrent' },
- { value: 'recovery', label: 'Recovery' },
-];
-
-const severityLevels = [
- { value: 'critical', label: 'Critical', color: 'bg-red-500' },
- { value: 'high', label: 'High', color: 'bg-orange-500' },
- { value: 'medium', label: 'Medium', color: 'bg-yellow-500' },
- { value: 'low', label: 'Low', color: 'bg-green-500' },
- { value: 'informational', label: 'Informational', color: 'bg-blue-500' },
-];
+import { saveDraftControlId, saveDraftControlIds, saveDraftCatalogId, initializeControlIdsStorage } from '@/utils/draftStorage';
+import { dashboardsService } from '@/services/grafana/dashboards';
+import { saveDraftDashboardUid } from '@/utils/draftStorage';
export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit, isSubmitting, apiError = null }) {
const [controls, setControls] = useState([]);
- const [isEditing, setIsEditing] = useState(false);
- const [currentControl, setCurrentControl] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
const [openDialog, setOpenDialog] = useState(false);
+ const [openNewControlForm, setOpenNewControlForm] = useState(false);
const [submitError, setSubmitError] = useState(null);
+ const [currentControl, setCurrentControl] = useState(null);
+ const [confirmingDelete, setConfirmingDelete] = useState(false);
+ const initialFetchCompleted = useRef(false);
- // Setup form with zod resolver
- const form = useForm({
- resolver: zodResolver(controlSchema),
- defaultValues: {
- name: '',
- description: '',
- type: '',
- severity: '',
- implementation: '',
- },
- });
+ const nodeRedUrl = import.meta.env.VITE_NODE_RED_URL || 'http://localhost:1880';
+ // Save catalogId to localStorage and initialize controls storage
useEffect(() => {
+ // Ensure the control_draft_ids key exists in localStorage
+ initializeControlIdsStorage();
+
if (initialControls && initialControls.length > 0) {
- setControls([...initialControls]);
+ setControls(initialControls);
+
+ // Save control IDs to localStorage
+ const controlIds = initialControls.map(control => control.id).filter(Boolean);
+ if (controlIds.length > 0) {
+ saveDraftControlIds(controlIds);
+ }
+ }
+ else if (catalogId && !initialFetchCompleted.current) {
+ saveDraftCatalogId(catalogId);
+ fetchDraftControls();
+ initialFetchCompleted.current = true;
}
- }, [initialControls]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [catalogId]);
// Update error message when API error changes
useEffect(() => {
@@ -90,65 +66,180 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit,
}
}, [apiError]);
+ const fetchDraftControls = async () => {
+ if (!catalogId) return;
+
+ setIsLoading(true);
+ try {
+ const response = await getControlsByCatalogId(catalogId, 'draft');
+
+ // Handle case where the response is empty or undefined
+ if (!response || response.length === 0) {
+ setControls([]);
+ // Save an empty array to localStorage to indicate we've fetched controls (even if empty)
+ saveDraftControlIds([]);
+ setIsLoading(false);
+ return;
+ }
+
+ const controlsWithScopesData = await Promise.all(
+ response.map(async (control) => {
+ try {
+ const scopeResponse = await getScopeSetsByControlId(control.id);
+ return {
+ ...control,
+ scopes: scopeResponse && scopeResponse.length > 0
+ ? scopeResponse.reduce((acc, scopeSet) => {
+ return { ...acc, ...scopeSet.scopes };
+ }, {})
+ : {}
+ };
+ } catch (error) {
+ console.error(`Error fetching scopes for control ${control.id}:`, error);
+ return { ...control, scopes: {} };
+ }
+ })
+ );
+
+ setControls(controlsWithScopesData);
+
+ // Save control IDs to localStorage
+ const controlIds = controlsWithScopesData.map(control => control.id).filter(Boolean);
+ if (controlIds.length > 0) {
+ saveDraftControlIds(controlIds);
+ } else {
+ // Initialize with empty array if no controls
+ saveDraftControlIds([]);
+ }
+ } catch (error) {
+ console.error('Error fetching draft controls:', error);
+ toast.error('Failed to load draft controls');
+ setControls([]);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
const handleAddControl = () => {
- setIsEditing(false);
- setCurrentControl(null);
- form.reset({
- name: '',
- description: '',
- type: '',
- severity: '',
- implementation: '',
- });
- setOpenDialog(true);
+ setOpenNewControlForm(true);
};
- const handleEditControl = (control, index) => {
- setIsEditing(true);
- setCurrentControl({ ...control, index });
- form.reset({
- name: control.name,
- description: control.description,
- type: control.type,
- severity: control.severity,
- implementation: control.implementation || '',
- });
- setOpenDialog(true);
+ const handleCloseNewControlForm = () => {
+ setOpenNewControlForm(false);
};
- const handleDeleteControl = (index) => {
- const updatedControls = [...controls];
- updatedControls.splice(index, 1);
- setControls(updatedControls);
+ const handleCustomSubmit = async (data) => {
+ try {
+ // Make sure the catalog ID is included in the control data
+ if (catalogId) {
+ data.catalogId = catalogId;
+ // Save catalog ID to localStorage again for safety
+ saveDraftCatalogId(catalogId);
+ // Ensure control_draft_ids is initialized
+ initializeControlIdsStorage();
+ }
+
+ let scopeSet = { scopes: {} };
+ // Use createDraftControl specifically in the catalog context
+ const createdControl = await createDraftControl(data);
+
+ // Save the new control ID to localStorage
+ if (createdControl && createdControl.id) {
+ saveDraftControlId(createdControl.id);
+ }
+
+ // Create the scope set if needed
+ if (Object.keys(data.scopes).length > 0) {
+ const scopeSetData = {
+ controlId: createdControl.id,
+ scopes: data.scopes
+ };
+
+ scopeSet = await createScopeSet(scopeSetData);
+ }
+
+ return { ...createdControl, scopes: scopeSet.scopes };
+ } catch (error) {
+ console.error('Error creating draft control:', error);
+ throw error;
+ }
};
- const handleControlSubmit = (data) => {
- if (isEditing && currentControl) {
- // Update existing control
- const updatedControls = [...controls];
- updatedControls[currentControl.index] = {
- id: currentControl.id || `temp-${Date.now()}`,
- ...data
- };
- setControls(updatedControls);
- } else {
- // Add new control
- setControls([
- ...controls,
- {
- id: `temp-${Date.now()}`,
- ...data
+ const handleControlSuccess = async (newControl) => {
+ setOpenNewControlForm(false);
+
+ if (newControl && newControl.id) {
+ try {
+ // Fetch the updated list of controls
+ if (catalogId) {
+ const response = await getControlsByCatalogId(catalogId, 'draft');
+ const controlsWithScopesData = await Promise.all(
+ response.map(async (control) => {
+ try {
+ const scopeResponse = await getScopeSetsByControlId(control.id);
+ return {
+ ...control,
+ scopes: scopeResponse.reduce((acc, scopeSet) => {
+ return { ...acc, ...scopeSet.scopes };
+ }, {})
+ };
+ } catch (error) {
+ console.error(`Error fetching scopes for control ${control.id}:`, error);
+ return { ...control, scopes: {} };
+ }
+ })
+ );
+ setControls(controlsWithScopesData || []);
+ } else {
+ // If no catalogId, manually add the new control to the current list
+ setControls(prevControls => [...prevControls, newControl]);
}
- ]);
+ } catch (error) {
+ console.error('Error updating controls list:', error);
+ toast.error('Control was created but the list could not be updated');
+
+ // As a fallback, manually add the control to the list
+ setControls(prevControls => [...prevControls, newControl]);
+ }
}
- setOpenDialog(false);
};
- const closeDialog = () => {
+ const handleEditControl = (control) => {
+ // Placeholder for edit functionality
+ toast.info('Edit functionality will be available in a future update');
+ };
+
+ const handleDeleteConfirm = (control) => {
+ setCurrentControl(control);
+ setConfirmingDelete(true);
+ setOpenDialog(true);
+ };
+
+ const handleDeleteCancel = () => {
setOpenDialog(false);
+ setConfirmingDelete(false);
+ setCurrentControl(null);
+ };
+
+ const handleDeleteControl = async () => {
+ if (!currentControl || !currentControl.id) return;
+
+ try {
+ await deleteControl(currentControl.id);
+ // After deletion, refresh the draft controls list
+ await fetchDraftControls();
+ toast.success('Control deleted successfully');
+ } catch (error) {
+ console.error('Error deleting draft control:', error);
+ toast.error('Failed to delete draft control');
+ } finally {
+ setOpenDialog(false);
+ setConfirmingDelete(false);
+ setCurrentControl(null);
+ }
};
- const handleSubmit = () => {
+ const handleSubmit = async () => {
setSubmitError(null);
if (controls.length === 0) {
@@ -156,27 +247,57 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit,
return;
}
- onSubmit(controls);
- };
+ // Save control IDs and catalog ID to localStorage before submitting
+ const controlIds = controls.map(control => control.id).filter(Boolean);
+ if (controlIds.length > 0) {
+ saveDraftControlIds(controlIds);
+ }
+
+ if (catalogId) {
+ saveDraftCatalogId(catalogId);
+ }
+
+ try {
+ const dashboardName = `tmp-${Date.now()}-${catalogId}`;
+
+ // Use the start date of the first control or the current date
+ let startDate = 'now-24h';
+ if (controls.length > 0 && controls[0].startDate) {
+ startDate = new Date(controls[0].startDate).toISOString();
+ }
+
+ // Create the template dashboard
+ const dashboardResponse = await dashboardsService.createTemplate({
+ name: dashboardName,
+ timeRange: {
+ from: startDate,
+ to: 'now'
+ }
+ });
- const getSeverityBadge = (severity) => {
- const severityInfo = severityLevels.find(level => level.value === severity);
- return (
-
- {severityInfo ? severityInfo.label : severity}
-
- );
+ // Store the dashboard UID in localStorage
+ if (dashboardResponse?.dashboard && dashboardResponse.dashboard?.uid) {
+ const dashboardUid = dashboardResponse?.dashboard?.uid;
+ saveDraftDashboardUid(dashboardUid);
+ }
+ } catch (error) {
+ console.error('Error creating template dashboard:', error);
+ toast.warning('Could not create template dashboard, panels may not display correctly');
+ }
+
+ onSubmit(controls);
};
return (
-
Security Controls
+
Draft Controls
@@ -192,32 +313,47 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit,
)}
- {/* Controls list */}
+ {/* Empty state with helpful message */}
+ {!isLoading && controls.length === 0 && (
+
+
No draft controls yet
+
Add draft controls to define what to monitor in your catalog
+
+ )}
+
+ {/* Controls list with shimmer effect when loading */}
- {controls.length === 0 ? (
-
-
No controls added yet. Click "Add Control" to start.
-
- ) : (
- controls.map((control, index) => (
-
+ {isLoading ? (
+ // Shimmer loading effect for controls
+ Array(3).fill(0).map((_, index) => (
+
+ ))
+ ) : controls.length > 0 ? (
+ controls.map((control) => (
+
-
+
{control.name}
-
- {getSeverityBadge(control.severity)}
-
- Type: {controlTypes.find(type => type.value === control.type)?.label || control.type}
+ Period: {control.period}
+ {control.mashupId && (
+
+
+
+ )}
@@ -225,167 +361,100 @@ export function CatalogControlsStep({ initialControls = [], catalogId, onSubmit,
size="sm"
variant="ghost"
className="text-red-500 hover:bg-red-50 hover:text-red-700"
- onClick={() => handleDeleteControl(index)}
+ onClick={() => handleDeleteConfirm(control)}
>
- {control.description}
- {control.implementation && (
-
-
Implementation:
-
{control.implementation}
-
- )}
+ {control.description}
+
+
+
+
+
Linker: SharePoint Linker
+
+
Start Date: {new Date(control.startDate).toLocaleDateString()}
+ {control.endDate &&
End Date: {new Date(control.endDate).toLocaleDateString()}
}
+ {control.scopes && Object.keys(control.scopes).length > 0 && (
+
+ Scopes:
+ {Object.entries(control.scopes).map(([key, value]) => (
+
+ {key}: {value}
+
+ ))}
+
+ )}
+ {control.params && Object.keys(control.params).length > 0 && (
+
+ Parameters:
+ {Object.entries(control.params).map(([key, value]) => (
+
+ {key}: {typeof value === 'object' ? JSON.stringify(value) : value}
+
+ ))}
+
+ )}
+
))
- )}
+ ) : null}
- {/* Control form dialog */}
-