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 */} - - + {/* Draft Control Form - Pass customSubmit for draft control creation */} + {openNewControlForm && ( + + )} + + {/* Delete Confirmation Dialog */} + + - {isEditing ? 'Edit Control' : 'Add New Control'} + Delete Draft Control - -
- - ( - - Control Name * - - - - - - )} - /> - -
- ( - - Type * - - - - )} - /> - - ( - - Severity * - - - - )} - /> -
- - ( - - Description * - -