diff --git a/.github/workflows/publish-vsix.yml b/.github/workflows/publish-vsix.yml index 79793d968ea..e6d496d5907 100644 --- a/.github/workflows/publish-vsix.yml +++ b/.github/workflows/publish-vsix.yml @@ -90,7 +90,7 @@ jobs: elif [ "${{ github.event.inputs.extension }}" == "choreo" ]; then echo "repo=wso2/choreo-vscode" >> $GITHUB_OUTPUT elif [ "${{ github.event.inputs.extension }}" == "wso2-platform" ]; then - echo "repo=wso2/wso2-platform-vscode" >> $GITHUB_OUTPUT + echo "repo=wso2/vscode-extensions" >> $GITHUB_OUTPUT elif [ "${{ github.event.inputs.extension }}" == "apk" ]; then echo "repo=wso2/apk-vscode" >> $GITHUB_OUTPUT elif [ "${{ github.event.inputs.extension }}" == "micro-integrator" ]; then diff --git a/.github/workflows/release-vsix.yml b/.github/workflows/release-vsix.yml index fa6b0ff6d67..eebb8ce79f0 100644 --- a/.github/workflows/release-vsix.yml +++ b/.github/workflows/release-vsix.yml @@ -143,11 +143,11 @@ jobs: token: ${{ secrets.CHOREO_BOT_TOKEN }} chatAPI: ${{ steps.chat.outputs.chatAPI }} - - name: Create a release in wso2/platform-vscode repo + - name: Create a release in wso2/vscode-extensions repo if: ${{ github.event.inputs.wso2-platform == 'true' }} uses: ./.github/actions/release with: - repo: wso2/platform-vscode + repo: wso2/vscode-extensions name: wso2-platform token: ${{ secrets.CHOREO_BOT_TOKEN }} chatAPI: ${{ steps.chat.outputs.chatAPI }} diff --git a/.vscode/launch.json b/.vscode/launch.json index 0893a35dbb3..28d44bc5469 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -115,7 +115,8 @@ "CELL_VIEW_DEV_HOST": "http://localhost:3001/cellDiagram.js" }, "args": [ - "--extensionDevelopmentPath=${workspaceFolder}/workspaces/wso2-platform/wso2-platform-extension" + "--extensionDevelopmentPath=${workspaceFolder}/workspaces/wso2-platform/wso2-platform-extension", + "--enable-proposed-api=wso2.wso2-platform" ], "outFiles": [ "${workspaceFolder}/workspaces/wso2-platform/wso2-platform-extension/dist/**/*.js" @@ -128,8 +129,8 @@ "type": "extensionHost", "request": "launch", "env": { - "WEB_VIEW_DEV_MODE": "true", - "WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", + "WEB_VIEW_DEV_MODE_CHOREO": "true", + "WEB_VIEW_DEV_HOST_CHOREO": "http://localhost:3001/main.js", "CELL_VIEW_DEV_MODE": "true", "CELL_VIEW_DEV_HOST": "http://localhost:3001/cellDiagram.js" }, @@ -178,8 +179,8 @@ ], "envFile": "${workspaceFolder}/workspaces/choreo/choreo-extension/.env", "env": { - "WEB_VIEW_DEV_MODE": "true", - "WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", + "WEB_VIEW_DEV_MODE_CHOREO": "true", + "WEB_VIEW_DEV_HOST_CHOREO": "http://localhost:3001/main.js", }, "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 9ccdb0781eb..bb2bb7c016f 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -2720,9 +2720,6 @@ importers: yaml: specifier: ^2.6.0 version: 2.8.2 - zustand: - specifier: ^5.0.5 - version: 5.0.9(@types/react@18.2.0)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)) devDependencies: '@biomejs/biome': specifier: ^1.8.3 @@ -2733,9 +2730,6 @@ importers: '@types/byline': specifier: ^4.2.36 version: 4.2.36 - '@types/js-yaml': - specifier: ^4.0.9 - version: 4.0.9 '@types/mocha': specifier: ~10.0.1 version: 10.0.10 @@ -2799,9 +2793,6 @@ importers: '@formkit/auto-animate': specifier: 0.8.2 version: 0.8.2 - '@headlessui/react': - specifier: ^2.2.4 - version: 2.2.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@hookform/resolvers': specifier: ^5.0.1 version: 5.2.2(react-hook-form@7.56.4(react@18.2.0)) @@ -2826,9 +2817,6 @@ importers: classnames: specifier: ~2.5.1 version: 2.5.1 - clipboardy: - specifier: ^4.0.0 - version: 4.0.0 js-yaml: specifier: ^4.1.1 version: 4.1.1 @@ -2847,34 +2835,19 @@ importers: react-hook-form: specifier: 7.56.4 version: 7.56.4(react@18.2.0) - react-markdown: - specifier: ^7.1.0 - version: 7.1.2(@types/react@18.2.0)(react@18.2.0) rehype-raw: specifier: ^6.1.0 version: 6.1.1 remark-gfm: specifier: ^4.0.1 version: 4.0.1 - swagger-ui-react: - specifier: ^5.22.0 - version: 5.30.3(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - timezone-support: - specifier: ^3.1.0 - version: 3.1.0 vscode-messenger-common: specifier: ^0.5.1 version: 0.5.1 vscode-messenger-webview: specifier: ^0.5.1 version: 0.5.1 - zod: - specifier: ^3.22.4 - version: 3.25.76 devDependencies: - '@types/js-yaml': - specifier: ^4.0.5 - version: 4.0.9 '@types/lodash.debounce': specifier: ^4.0.9 version: 4.0.9 @@ -2887,9 +2860,6 @@ importers: '@types/react-dom': specifier: 18.2.0 version: 18.2.0 - '@types/swagger-ui-react': - specifier: ^5.18.0 - version: 5.18.0 '@types/vscode-webview': specifier: ^1.57.5 version: 1.57.5 @@ -4300,6 +4270,9 @@ importers: '@aws-sdk/client-s3': specifier: ^3.817.0 version: 3.943.0 + '@iarna/toml': + specifier: ^2.2.5 + version: 2.2.5 '@vscode-logging/logger': specifier: ^2.0.0 version: 2.0.0 @@ -4392,7 +4365,7 @@ importers: specifier: workspace:* version: link:../../common-libs/playwright-vscode-tester axios: - specifier: ^1.12.0 + specifier: ^1.9.0 version: 1.12.2 copy-webpack-plugin: specifier: ^13.0.0 @@ -4482,17 +4455,17 @@ importers: specifier: 7.63.0 version: 7.63.0(react@18.2.0) react-markdown: - specifier: ^7.1.0 - version: 7.1.2(@types/react@18.2.0)(react@18.2.0) + specifier: 10.1.0 + version: 10.1.0(@types/react@18.2.0)(react@18.2.0) rehype-raw: - specifier: ^6.1.0 - version: 6.1.1 + specifier: 7.0.0 + version: 7.0.0 remark-gfm: - specifier: ^4.0.1 + specifier: 4.0.1 version: 4.0.1 swagger-ui-react: - specifier: ^5.22.0 - version: 5.30.3(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: 5.22.0 + version: 5.22.0(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) timezone-support: specifier: ^3.1.0 version: 3.1.0 @@ -13543,9 +13516,6 @@ packages: dompurify@3.2.4: resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==} - dompurify@3.2.6: - resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} - domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -15188,9 +15158,6 @@ packages: hast-util-to-parse5@8.0.0: resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} - hast-util-whitespace@2.0.1: - resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} - hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -16906,10 +16873,6 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - klona@2.0.6: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} @@ -17347,18 +17310,12 @@ packages: mdast-util-definitions@4.0.0: resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==} - mdast-util-definitions@5.1.2: - resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} - mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - mdast-util-from-markdown@1.3.1: - resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} - mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} @@ -17404,9 +17361,6 @@ packages: mdast-util-to-hast@10.0.1: resolution: {integrity: sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==} - mdast-util-to-hast@11.3.0: - resolution: {integrity: sha512-4o3Cli3hXPmm1LhB+6rqhfsIUBjnKFlIUZvudaermXB+4/KONdd/W4saWWkC+LBLbPMqhFSSTSRgafHsT5fVJw==} - mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} @@ -17416,9 +17370,6 @@ packages: mdast-util-to-string@1.1.0: resolution: {integrity: sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==} - mdast-util-to-string@3.2.0: - resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} - mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} @@ -17523,9 +17474,6 @@ packages: microevent.ts@0.1.1: resolution: {integrity: sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==} - micromark-core-commonmark@1.1.0: - resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} - micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -17571,129 +17519,69 @@ packages: micromark-extension-mdxjs@3.0.0: resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} - micromark-factory-destination@1.1.0: - resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} - micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - micromark-factory-label@1.1.0: - resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} - micromark-factory-label@2.0.1: resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} micromark-factory-mdx-expression@2.0.3: resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} - micromark-factory-space@1.1.0: - resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} - micromark-factory-space@2.0.1: resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - micromark-factory-title@1.1.0: - resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} - micromark-factory-title@2.0.1: resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - micromark-factory-whitespace@1.1.0: - resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} - micromark-factory-whitespace@2.0.1: resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - micromark-util-character@1.2.0: - resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} - micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - micromark-util-chunked@1.1.0: - resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} - micromark-util-chunked@2.0.1: resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - micromark-util-classify-character@1.1.0: - resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} - micromark-util-classify-character@2.0.1: resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - micromark-util-combine-extensions@1.1.0: - resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} - micromark-util-combine-extensions@2.0.1: resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - micromark-util-decode-numeric-character-reference@1.1.0: - resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} - micromark-util-decode-numeric-character-reference@2.0.2: resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - micromark-util-decode-string@1.1.0: - resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} - micromark-util-decode-string@2.0.1: resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - micromark-util-encode@1.1.0: - resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} - micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} micromark-util-events-to-acorn@2.0.3: resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} - micromark-util-html-tag-name@1.2.0: - resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} - micromark-util-html-tag-name@2.0.1: resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - micromark-util-normalize-identifier@1.1.0: - resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} - micromark-util-normalize-identifier@2.0.1: resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - micromark-util-resolve-all@1.1.0: - resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} - micromark-util-resolve-all@2.0.1: resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - micromark-util-sanitize-uri@1.2.0: - resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} - micromark-util-sanitize-uri@2.0.1: resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - micromark-util-subtokenize@1.1.0: - resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} - micromark-util-subtokenize@2.1.0: resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - micromark-util-symbol@1.1.0: - resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} - micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - micromark-util-types@1.1.0: - resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} - micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - micromark@3.2.0: - resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} - micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} @@ -19819,12 +19707,6 @@ packages: '@types/react': '>=18' react: '>=18' - react-markdown@7.1.2: - resolution: {integrity: sha512-ibMcc0EbfmbwApqJD8AUr0yls8BSrKzIbHaUsPidQljxToCqFh34nwtu3CXNEItcVJNzpjDHrhK8A+MAh2JW3A==} - peerDependencies: - '@types/react': '>=16' - react: '>=16' - react-markdown@9.0.3: resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==} peerDependencies: @@ -19931,12 +19813,6 @@ packages: peerDependencies: react: '>= 0.14.0' - react-syntax-highlighter@16.1.0: - resolution: {integrity: sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==} - engines: {node: '>= 16.20.2'} - peerDependencies: - react: '>= 0.14.0' - react-test-renderer@19.1.1: resolution: {integrity: sha512-aGRXI+zcBTtg0diHofc7+Vy97nomBs9WHHFY1Csl3iV0x6xucjNYZZAkiVKGiNYUv23ecOex5jE67t8ZzqYObA==} peerDependencies: @@ -20098,9 +19974,6 @@ packages: refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} - refractor@5.0.0: - resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==} - regenerate-unicode-properties@10.2.2: resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} engines: {node: '>=4'} @@ -20186,9 +20059,6 @@ packages: remark-mdx@1.6.22: resolution: {integrity: sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==} - remark-parse@10.0.2: - resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} - remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -20198,9 +20068,6 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - remark-rehype@9.1.0: - resolution: {integrity: sha512-oLa6YmgAYg19zb0ZrBACh40hpBLteYROaPLhBXzLgjqyHQrN+gVP9N/FJvfzuNNuzCutktkroXEZBrxAxKhh7Q==} - remark-slug@6.1.0: resolution: {integrity: sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==} @@ -21464,11 +21331,11 @@ packages: react: '>=16.8.0 <19' react-dom: '>=16.8.0 <19' - swagger-ui-react@5.30.3: - resolution: {integrity: sha512-QIy32nPql6yiV2NVwbww1P7f6HEOAuYrnk8VEJkzPC/p6Xc5Xnz9hhmSHzXTuM70fDbVw/qPzCJ0mZMMultpiw==} + swagger-ui-react@5.22.0: + resolution: {integrity: sha512-Y0TEWg2qD4u/dgZ9q9G16yM/Edvyz0ovkIZlpACN8X/2gzSoIzS/fhSpLSJfCOxRt2UqrKmajMB11VK6cGZk2g==} peerDependencies: - react: '>=16.8.0 <20' - react-dom: '>=16.8.0 <20' + react: '>=16.8.0 <19' + react-dom: '>=16.8.0 <19' swc-loader@0.2.6: resolution: {integrity: sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg==} @@ -22293,15 +22160,9 @@ packages: unist-builder@2.0.3: resolution: {integrity: sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==} - unist-builder@3.0.1: - resolution: {integrity: sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==} - unist-util-generated@1.1.6: resolution: {integrity: sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==} - unist-util-generated@2.0.1: - resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} - unist-util-is@4.1.0: resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} @@ -22329,9 +22190,6 @@ packages: unist-util-remove@2.1.0: resolution: {integrity: sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==} - unist-util-stringify-position@3.0.3: - resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} - unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -22556,11 +22414,6 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - uvu@0.5.6: - resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} - engines: {node: '>=8'} - hasBin: true - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -37115,12 +36968,12 @@ snapshots: '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.103.0)': dependencies: webpack: 5.103.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.2.2)(webpack@5.103.0) + webpack-cli: 5.1.4(webpack@5.103.0) '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.103.0)': dependencies: - webpack: 5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.103.0) + webpack: 5.103.0(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.103.0) '@webpack-cli/info@1.5.0(webpack-cli@4.10.0)': dependencies: @@ -37130,12 +36983,12 @@ snapshots: '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.103.0)': dependencies: webpack: 5.103.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.2.2)(webpack@5.103.0) + webpack-cli: 5.1.4(webpack@5.103.0) '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.103.0)': dependencies: - webpack: 5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.103.0) + webpack: 5.103.0(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.103.0) '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)': dependencies: @@ -40406,10 +40259,6 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 - dompurify@3.2.6: - optionalDependencies: - '@types/trusted-types': 2.0.7 - domutils@2.8.0: dependencies: dom-serializer: 1.4.1 @@ -42886,8 +42735,6 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 - hast-util-whitespace@2.0.1: {} - hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -45557,8 +45404,6 @@ snapshots: kleur@3.0.3: {} - kleur@4.1.5: {} - klona@2.0.6: {} known-css-properties@0.37.0: {} @@ -46000,12 +45845,6 @@ snapshots: dependencies: unist-util-visit: 2.0.3 - mdast-util-definitions@5.1.2: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - unist-util-visit: 4.1.2 - mdast-util-directive@3.1.0: dependencies: '@types/mdast': 4.0.4 @@ -46027,23 +45866,6 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@1.3.1: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - decode-named-character-reference: 1.2.0 - mdast-util-to-string: 3.2.0 - micromark: 3.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-decode-string: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-stringify-position: 3.0.3 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 @@ -46199,18 +46021,6 @@ snapshots: unist-util-position: 3.1.0 unist-util-visit: 2.0.3 - mdast-util-to-hast@11.3.0: - dependencies: - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - '@types/mdurl': 1.0.5 - mdast-util-definitions: 5.1.2 - mdurl: 1.0.1 - unist-builder: 3.0.1 - unist-util-generated: 2.0.1 - unist-util-position: 4.0.4 - unist-util-visit: 4.1.2 - mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -46237,10 +46047,6 @@ snapshots: mdast-util-to-string@1.1.0: {} - mdast-util-to-string@3.2.0: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-string@4.0.0: dependencies: '@types/mdast': 4.0.4 @@ -46353,25 +46159,6 @@ snapshots: microevent.ts@0.1.1: {} - micromark-core-commonmark@1.1.0: - dependencies: - decode-named-character-reference: 1.2.0 - micromark-factory-destination: 1.1.0 - micromark-factory-label: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-factory-title: 1.1.0 - micromark-factory-whitespace: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-chunked: 1.1.0 - micromark-util-classify-character: 1.1.0 - micromark-util-html-tag-name: 1.2.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-subtokenize: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -46517,25 +46304,12 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-destination@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-label@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-factory-label@2.0.1: dependencies: devlop: 1.1.0 @@ -46555,23 +46329,11 @@ snapshots: unist-util-position-from-estree: 2.0.0 vfile-message: 4.0.3 - micromark-factory-space@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-types: 1.1.0 - micromark-factory-space@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-types: 2.0.2 - micromark-factory-title@1.1.0: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-title@2.0.1: dependencies: micromark-factory-space: 2.0.1 @@ -46579,13 +46341,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-whitespace@1.1.0: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-whitespace@2.0.1: dependencies: micromark-factory-space: 2.0.1 @@ -46593,61 +46348,30 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-character@1.2.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-chunked@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-chunked@2.0.1: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-classify-character@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-util-classify-character@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-combine-extensions@1.1.0: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-types: 1.1.0 - micromark-util-combine-extensions@2.0.1: dependencies: micromark-util-chunked: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-decode-numeric-character-reference@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-decode-numeric-character-reference@2.0.2: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-decode-string@1.1.0: - dependencies: - decode-named-character-reference: 1.2.0 - micromark-util-character: 1.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-decode-string@2.0.1: dependencies: decode-named-character-reference: 1.2.0 @@ -46655,8 +46379,6 @@ snapshots: micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 - micromark-util-encode@1.1.0: {} - micromark-util-encode@2.0.1: {} micromark-util-events-to-acorn@2.0.3: @@ -46669,45 +46391,22 @@ snapshots: micromark-util-types: 2.0.2 vfile-message: 4.0.3 - micromark-util-html-tag-name@1.2.0: {} - micromark-util-html-tag-name@2.0.1: {} - micromark-util-normalize-identifier@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-normalize-identifier@2.0.1: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-resolve-all@1.1.0: - dependencies: - micromark-util-types: 1.1.0 - micromark-util-resolve-all@2.0.1: dependencies: micromark-util-types: 2.0.2 - micromark-util-sanitize-uri@1.2.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-encode: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-sanitize-uri@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-encode: 2.0.1 micromark-util-symbol: 2.0.1 - micromark-util-subtokenize@1.1.0: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-util-subtokenize@2.1.0: dependencies: devlop: 1.1.0 @@ -46715,36 +46414,10 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-symbol@1.1.0: {} - micromark-util-symbol@2.0.1: {} - micromark-util-types@1.1.0: {} - micromark-util-types@2.0.2: {} - micromark@3.2.0: - dependencies: - '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@8.1.1) - decode-named-character-reference: 1.2.0 - micromark-core-commonmark: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-chunked: 1.1.0 - micromark-util-combine-extensions: 1.1.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-encode: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-subtokenize: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - micromark@4.0.2: dependencies: '@types/debug': 4.1.12 @@ -49201,27 +48874,6 @@ snapshots: transitivePeerDependencies: - supports-color - react-markdown@7.1.2(@types/react@18.2.0)(react@18.2.0): - dependencies: - '@types/hast': 2.3.10 - '@types/react': 18.2.0 - '@types/unist': 2.0.11 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 2.0.1 - prop-types: 15.8.1 - property-information: 6.5.0 - react: 18.2.0 - react-is: 17.0.2 - remark-parse: 10.0.2 - remark-rehype: 9.1.0 - space-separated-tokens: 2.0.2 - style-to-object: 0.3.0 - unified: 10.1.2 - unist-util-visit: 4.1.2 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - react-markdown@9.0.3(@types/react@18.2.0)(react@18.2.0): dependencies: '@types/hast': 3.0.4 @@ -49502,16 +49154,6 @@ snapshots: react: 18.2.0 refractor: 3.6.0 - react-syntax-highlighter@16.1.0(react@18.2.0): - dependencies: - '@babel/runtime': 7.28.4 - highlight.js: 10.7.3 - highlightjs-vue: 1.0.0 - lowlight: 1.20.0 - prismjs: 1.30.0 - react: 18.2.0 - refractor: 5.0.0 - react-test-renderer@19.1.1(react@18.2.0): dependencies: react: 18.2.0 @@ -49736,13 +49378,6 @@ snapshots: parse-entities: 2.0.0 prismjs: 1.30.0 - refractor@5.0.0: - dependencies: - '@types/hast': 3.0.4 - '@types/prismjs': 1.26.5 - hastscript: 9.0.1 - parse-entities: 4.0.2 - regenerate-unicode-properties@10.2.2: dependencies: regenerate: 1.4.2 @@ -49870,14 +49505,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-parse@10.0.2: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-from-markdown: 1.3.1 - unified: 10.1.2 - transitivePeerDependencies: - - supports-color - remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -49914,13 +49541,6 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - remark-rehype@9.1.0: - dependencies: - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-to-hast: 11.3.0 - unified: 10.1.2 - remark-slug@6.1.0: dependencies: github-slugger: 1.5.0 @@ -51513,16 +51133,15 @@ snapshots: - '@types/react' - debug - swagger-ui-react@5.30.3(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + swagger-ui-react@5.22.0(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime-corejs3': 7.28.4 '@scarf/scarf': 1.4.0 base64-js: 1.5.1 - buffer: 6.0.3 classnames: 2.5.1 css.escape: 1.5.1 deep-extend: 0.6.0 - dompurify: 3.2.6 + dompurify: 3.2.4 ieee754: 1.2.1 immutable: 3.8.2 js-file-download: 0.4.12 @@ -51539,7 +51158,7 @@ snapshots: react-immutable-pure-component: 2.2.2(immutable@3.8.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-inspector: 6.0.2(react@18.2.0) react-redux: 9.2.0(@types/react@18.2.0)(react@18.2.0)(redux@5.0.1) - react-syntax-highlighter: 16.1.0(react@18.2.0) + react-syntax-highlighter: 15.6.6(react@18.2.0) redux: 5.0.1 redux-immutable: 4.0.0(immutable@3.8.2) remarkable: 2.0.1 @@ -52675,14 +52294,8 @@ snapshots: unist-builder@2.0.3: {} - unist-builder@3.0.1: - dependencies: - '@types/unist': 2.0.11 - unist-util-generated@1.1.6: {} - unist-util-generated@2.0.1: {} - unist-util-is@4.1.0: {} unist-util-is@5.2.1: @@ -52715,10 +52328,6 @@ snapshots: dependencies: unist-util-is: 4.1.0 - unist-util-stringify-position@3.0.3: - dependencies: - '@types/unist': 2.0.11 - unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -53020,13 +52629,6 @@ snapshots: uuid@9.0.1: {} - uvu@0.5.6: - dependencies: - dequal: 2.0.3 - diff: 5.2.0 - kleur: 4.1.5 - sade: 1.8.1 - v8-compile-cache-lib@3.0.1: {} v8-compile-cache@2.4.0: {} @@ -53966,7 +53568,7 @@ snapshots: watchpack: 2.4.4 webpack-sources: 3.3.3 optionalDependencies: - webpack-cli: 5.1.4(webpack-dev-server@5.2.2)(webpack@5.103.0) + webpack-cli: 5.1.4(webpack@5.103.0) transitivePeerDependencies: - '@swc/core' - esbuild diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts b/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts index 0266888301a..c86aeebd74d 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts @@ -51,6 +51,7 @@ export const BI_COMMANDS = { BI_EDIT_TEST_FUNCTION_DEF: 'BI.test.edit.function.def', ADD_NATURAL_FUNCTION: 'BI.project-explorer.add-natural-function', TOGGLE_TRACE_LOGS: 'BI.toggle.trace.logs', + DEVANT_PUSH_TO_CLOUD: 'BI.devant.push.cloud', CREATE_BI_PROJECT: 'BI.project.createBIProjectPure', CREATE_BI_MIGRATION_PROJECT: 'BI.project.createBIProjectMigration', ADD_INTEGRATION: 'BI.project-explorer.add-integration', diff --git a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts index 5bac1d2f6e9..1040cb0d65f 100644 --- a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts +++ b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts @@ -132,6 +132,7 @@ export interface VisualizerLocation { position?: NodePosition; syntaxTree?: STNode; isBI?: boolean; + isInDevant?: boolean; focusFlowDiagramView?: FocusFlowDiagramView; serviceType?: string; type?: Type; @@ -351,18 +352,24 @@ export type AIMachineSendableEvent = export enum LoginMethod { BI_INTEL = 'biIntel', ANTHROPIC_KEY = 'anthropic_key', + DEVANT_ENV = 'devant_env', AWS_BEDROCK = 'aws_bedrock' } -interface BIIntelSecrets { +export interface BIIntelSecrets { accessToken: string; refreshToken: string; } -interface AnthropicKeySecrets { +export interface AnthropicKeySecrets { apiKey: string; } +export interface DevantEnvSecrets { + accessToken: string; + expiresAt: number; +} + interface AwsBedrockSecrets { accessKeyId: string; secretAccessKey: string; @@ -379,13 +386,23 @@ export type AuthCredentials = loginMethod: LoginMethod.ANTHROPIC_KEY; secrets: AnthropicKeySecrets; } + | { + loginMethod: LoginMethod.DEVANT_ENV; + secrets: DevantEnvSecrets; + } | { loginMethod: LoginMethod.AWS_BEDROCK; secrets: AwsBedrockSecrets; }; export interface AIUserToken { - token: string; // For BI Intel, this is the access token and for Anthropic, this is the API key + credentials: AuthCredentials; + usageToken?: string; + metadata?: { + lastRefresh?: string; + expiresAt?: string; + [key: string]: any; + }; } export interface AIMachineContext { diff --git a/workspaces/ballerina/ballerina-core/src/utils/identifier-utils.ts b/workspaces/ballerina/ballerina-core/src/utils/identifier-utils.ts index 261b8a8575e..790974eeb2c 100644 --- a/workspaces/ballerina/ballerina-core/src/utils/identifier-utils.ts +++ b/workspaces/ballerina/ballerina-core/src/utils/identifier-utils.ts @@ -19,6 +19,24 @@ import { ComponentInfo } from "../interfaces/ballerina"; import { BallerinaProjectComponents } from "../interfaces/extended-lang-client"; +import { SCOPE } from "../state-machine-types"; + +const INTEGRATION_API_MODULES = ["http", "graphql", "tcp"]; +const EVENT_INTEGRATION_MODULES = ["kafka", "rabbitmq", "salesforce", "trigger.github", "mqtt", "asb"]; +const FILE_INTEGRATION_MODULES = ["ftp", "file"]; +const AI_AGENT_MODULE = "ai"; + +export function findScopeByModule(moduleName: string): SCOPE { + if (AI_AGENT_MODULE === moduleName) { + return SCOPE.AI_AGENT; + } else if (INTEGRATION_API_MODULES.includes(moduleName)) { + return SCOPE.INTEGRATION_AS_API; + } else if (EVENT_INTEGRATION_MODULES.includes(moduleName)) { + return SCOPE.EVENT_INTEGRATION; + } else if (FILE_INTEGRATION_MODULES.includes(moduleName)) { + return SCOPE.FILE_INTEGRATION; + } +} export function getAllVariablesForAiFrmProjectComponents(projectComponents: BallerinaProjectComponents): { [key: string]: any } { const variableCollection: { [key: string]: any } = {}; diff --git a/workspaces/ballerina/ballerina-extension/.env.example b/workspaces/ballerina/ballerina-extension/.env.example index e48ddcef710..a216162b012 100644 --- a/workspaces/ballerina/ballerina-extension/.env.example +++ b/workspaces/ballerina/ballerina-extension/.env.example @@ -1,4 +1,15 @@ +# Prod Copilot BALLERINA_ROOT_URL=https://dev-tools.wso2.com/ballerina-copilot BALLERINA_AUTH_ORG= BALLERINA_AUTH_CLIENT_ID= BALLERINA_AUTH_REDIRECT_URL=https://eae690d5-80c3-4fb7-9bc5-e8d747cca11b.e1-us-east-azure.choreoapps.dev +BALLERINA_DEFAULT_COPLIOT_CODE_API_KEY= +BALLERINA_DEFAULT_COPLIOT_ASK_API_KEY= + +# Dev Copilot +BALLERINA_DEV_COPLIOT_ROOT_URL= +BALLERINA_DEV_COPLIOT_AUTH_ORG= +BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID= +BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL= +BALLERINA_DEV_COPLIOT_CODE_API_KEY= +BALLERINA_DEV_COPLIOT_ASK_API_KEY= \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-extension/package.json b/workspaces/ballerina/ballerina-extension/package.json index 2f784819fd0..62260afa485 100644 --- a/workspaces/ballerina/ballerina-extension/package.json +++ b/workspaces/ballerina/ballerina-extension/package.json @@ -697,6 +697,14 @@ "title": "Toggle Trace Logs", "category": "BI" }, + { + "command": "BI.devant.push.cloud", + "title": "Save Changes in the cloud", + "icon": "$(save)", + "group": "navigation", + "category": "BI", + "enablement": "isBIProject && devant.editor" + }, { "command": "ballerina.showTraceWindow", "title": "Show Traces", @@ -878,6 +886,12 @@ "group": "navigation@3", "title": "Debug Integration", "when": "isBalVisualizerActive && isBIProject" + }, + { + "command": "BI.devant.push.cloud", + "group": "navigation", + "title": "Save Changes in the cloud", + "when": "isBIProject && devant.editor" } ], "notebook/toolbar": [ diff --git a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts index 2550a32a896..f8c729538b4 100644 --- a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts +++ b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts @@ -130,6 +130,7 @@ async function getContext(): Promise { position: context.position, syntaxTree: context.syntaxTree, isBI: context.isBI, + isInDevant: context.isInDevant, projectPath: context.projectPath, serviceType: context.serviceType, type: context.type, diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/ask/ask.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/ask/ask.ts index a61456a1496..4e5f58413c0 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/ask/ask.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/ask/ask.ts @@ -110,7 +110,7 @@ async function fetchDocumentationFromVectorStore(query: string): Promise { library_link: `https://central.ballerina.io/${lib.name.replace(/'/g, '')}/` } as LibraryWithUrl; }); - + return apiDocs; } @@ -138,10 +138,10 @@ export async function getAskResponse(question: string): Promise try { // First, try to get tool calls from Claude const toolCallsResponse: ToolCall[] = await getToolCallsFromClaude(question); - + let centralContext: ApiDocResult[] = []; let documentationContext: Document[] = []; - + // Execute the tools if we got tool calls if (toolCallsResponse && toolCallsResponse.length > 0) { for (const toolCall of toolCallsResponse) { @@ -158,7 +158,7 @@ export async function getAskResponse(question: string): Promise const docs = await extractLearnPages(question); documentationContext.push(...docs); } - + // Build document chunks const docChunks: { [key: string]: DocChunk } = {}; if (documentationContext.length > 0) { @@ -170,13 +170,13 @@ export async function getAskResponse(question: string): Promise }; }); } - + // Build system message const systemMessage = buildLlmMessage(docChunks, documentationContext, centralContext); // Get final response from Claude const finalResponse = await getFinalResponseFromClaude(systemMessage, question); - + // Extract library links const libraryLinks: string[] = []; if (centralContext.length > 0) { @@ -184,7 +184,7 @@ export async function getAskResponse(question: string): Promise libraryLinks.push(lib.library_link); }); } - + // Extract doc IDs and add corresponding links const docIdPattern = /(.*?)<\/doc_id>/g; const docIds: string[] = []; @@ -192,25 +192,25 @@ export async function getAskResponse(question: string): Promise while ((match = docIdPattern.exec(finalResponse)) !== null) { docIds.push(match[1]); } - + // Add documentation links for referenced chunks docIds.forEach(id => { if (docChunks[id] && docChunks[id].doc_link.length > 0) { libraryLinks.push(docChunks[id].doc_link); } }); - + // Clean response const filteredResponse = finalResponse.replace(/.*?<\/doc_id>/g, '').trim(); - + // Format links const formattedLinks = libraryLinks.map(link => `<${link}>`); - + return { content: filteredResponse, references: formattedLinks }; - + } catch (error) { console.error('Error in assistantToolCall:', error); throw new Error(`Failed to process question: ${error instanceof Error ? error.message : 'Unknown error'}`); @@ -231,14 +231,14 @@ async function getToolCallsFromClaude(question: string): Promise { stopWhen: stepCountIs(1), // Limit to one step to get tool calls only abortSignal: AIPanelAbortController.getInstance().signal }); - + if (toolCalls && toolCalls.length > 0) { return toolCalls.map(toolCall => ({ name: toolCall.toolName, input: toolCall.input })); } - + return []; } @@ -255,7 +255,7 @@ async function getFinalResponseFromClaude(systemMessage: string, question: strin ], abortSignal: AIPanelAbortController.getInstance().signal }); - + return text; } @@ -264,14 +264,14 @@ function buildLlmMessage( documentationContext: Document[], centralContext: ApiDocResult[] ): string { - const documentationSection = documentationContext.length > 0 + const documentationSection = documentationContext.length > 0 ? `Information from Ballerina Learn Pages: This section includes content sourced from the Ballerina Learn pages, consisting of document chunks that cover various topics. These chunks also include sample code examples that are necessary for explaining Ballerina concepts effectively. Out of the given document chunks, you must include the chunk number(eg:- chunk1,chunk2...) of all the document chunks that you used to formulate the answer within tags and include it at the end of your response. Only include one chunk number per tag. Document chunks ${JSON.stringify(docChunks)}` : ""; - - const centralSection = centralContext.length > 0 + + const centralSection = centralContext.length > 0 ? `Information from the Ballerina API Documentation: This section provides detailed information about type definitions, clients, functions, function parameters, return types, and other library-specific details essential for answering questions related to the Ballerina programming language. ${JSON.stringify(centralContext)}` : ""; - + return `You are an AI assistant specialized in answering questions about the Ballerina programming language. Your task is to provide precise, accurate, and helpful answers based solely on the information provided below. The information provided below comes from reliable and authoritative sources on the Ballerina programming language. For every response, include your reasoning or derivation inside tags. The content within these tags should explain how you arrived at the answer. INFORMATION SOURCES: diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/connection.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/connection.ts index 471f102d90b..dd1ecc17da5 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/connection.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/connection.ts @@ -16,10 +16,10 @@ import { createAnthropic } from "@ai-sdk/anthropic"; import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; -import { getAccessToken, getLoginMethod, getRefreshedAccessToken, getAwsBedrockCredentials } from "../../../utils/ai/auth"; +import { getAccessToken, getLoginMethod, getRefreshedAccessToken, getAwsBedrockCredentials, refreshDevantToken } from "../../../utils/ai/auth"; import { AIStateMachine } from "../../../views/ai-panel/aiMachine"; import { BACKEND_URL } from "../utils"; -import { AIMachineEventType, LoginMethod } from "@wso2/ballerina-core"; +import { AIMachineEventType, AnthropicKeySecrets, LoginMethod, BIIntelSecrets, DevantEnvSecrets } from "@wso2/ballerina-core"; export const ANTHROPIC_HAIKU = "claude-3-5-haiku-20241022"; export const ANTHROPIC_SONNET_4 = "claude-sonnet-4-5-20250929"; @@ -63,32 +63,72 @@ let cachedAuthMethod: LoginMethod | null = null; */ export async function fetchWithAuth(input: string | URL | Request, options: RequestInit = {}): Promise { try { - const accessToken = await getAccessToken(); + const credentials = await getAccessToken(); + const loginMethod = credentials.loginMethod; - // Ensure headers object exists - options.headers = { - ...options.headers, - 'Authorization': `Bearer ${accessToken}`, + const headers: Record = { + "Content-Type": "application/json", 'User-Agent': 'Ballerina-VSCode-Plugin', 'Connection': 'keep-alive', }; + if (credentials && loginMethod === LoginMethod.DEVANT_ENV) { + // For DEVANT_ENV, use Bearer token (exchanged from STS token) + const secrets = credentials.secrets as DevantEnvSecrets; + if (secrets.accessToken && secrets.accessToken.trim() !== "") { + headers["Authorization"] = `Bearer ${secrets.accessToken}`; + } else { + console.warn("DevantEnv access token missing, this may cause authentication issues"); + } + } else if (credentials && loginMethod === LoginMethod.BI_INTEL) { + // For BI_INTEL, use Bearer token + const secrets = credentials.secrets as BIIntelSecrets; + headers["Authorization"] = `Bearer ${secrets.accessToken}`; + } + + // Ensure headers object exists and merge with existing headers + options.headers = { + ...options.headers, + ...headers, + }; + let response = await fetch(input, options); console.log("Response status: ", response.status); - // Handle token expiration + // Handle token expiration for both BI_INTEL and DEVANT_ENV methods if (response.status === 401) { - console.log("Token expired. Refreshing token..."); - const newToken = await getRefreshedAccessToken(); - if (newToken) { - options.headers = { - ...options.headers, - 'Authorization': `Bearer ${newToken}`, - }; - response = await fetch(input, options); - } else { - AIStateMachine.service().send(AIMachineEventType.LOGOUT); - return; + if (loginMethod === LoginMethod.BI_INTEL) { + console.log("Token expired. Refreshing BI_INTEL token..."); + const newToken = await getRefreshedAccessToken(); + if (newToken) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${newToken}`, + }; + response = await fetch(input, options); + } else { + AIStateMachine.service().send(AIMachineEventType.LOGOUT); + return; + } + } else if (loginMethod === LoginMethod.DEVANT_ENV) { + console.log("Token expired. Refreshing DEVANT_ENV token..."); + try { + const newToken = await refreshDevantToken(); + if (newToken) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${newToken}`, + }; + response = await fetch(input, options); + } else { + AIStateMachine.service().send(AIMachineEventType.LOGOUT); + return; + } + } catch (error) { + console.error("Failed to refresh Devant token:", error); + AIStateMachine.service().send(AIMachineEventType.LOGOUT); + return; + } } } @@ -121,17 +161,18 @@ export const getAnthropicClient = async (model: AnthropicModel): Promise => // Recreate client if login method has changed or no cached instance if (!cachedAnthropic || cachedAuthMethod !== loginMethod) { let url = BACKEND_URL + "/intelligence-api/v1.0/claude"; - if (loginMethod === LoginMethod.BI_INTEL) { + if (loginMethod === LoginMethod.BI_INTEL || loginMethod === LoginMethod.DEVANT_ENV) { cachedAnthropic = createAnthropic({ baseURL: url, apiKey: "xx", // dummy value; real auth is via fetchWithAuth fetch: fetchWithAuth, }); } else if (loginMethod === LoginMethod.ANTHROPIC_KEY) { - const apiKey = await getAccessToken(); + const credentials = await getAccessToken(); + const secrets = credentials.secrets as AnthropicKeySecrets; cachedAnthropic = createAnthropic({ baseURL: "https://api.anthropic.com/v1", - apiKey: apiKey, + apiKey: secrets.apiKey, }); } else if (loginMethod === LoginMethod.AWS_BEDROCK) { const awsCredentials = await getAwsBedrockCredentials(); diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts index d53bb386a00..51336418baa 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts @@ -26,14 +26,30 @@ import { AIStateMachine } from '../../../src/views/ai-panel/aiMachine'; import { AIMachineEventType } from '@wso2/ballerina-core/lib/state-machine-types'; import { CONFIG_FILE_NAME, ERROR_NO_BALLERINA_SOURCES, PROGRESS_BAR_MESSAGE_FROM_WSO2_DEFAULT_MODEL } from './constants'; import { getCurrentBallerinaProjectFromContext } from '../config-generator/configGenerator'; -import { BallerinaProject } from '@wso2/ballerina-core'; +import { BallerinaProject, LoginMethod } from '@wso2/ballerina-core'; import { BallerinaExtension } from 'src/core'; +import { getAuthCredentials } from '../../utils/ai/auth'; const config = workspace.getConfiguration('ballerina'); -export const BACKEND_URL: string = config.get('rootUrl') || process.env.BALLERINA_ROOT_URL; -export const AUTH_ORG: string = config.get('authOrg') || process.env.BALLERINA_AUTH_ORG; -export const AUTH_CLIENT_ID: string = config.get('authClientID') || process.env.BALLERINA_AUTH_CLIENT_ID; -export const AUTH_REDIRECT_URL: string = config.get('authRedirectURL') || process.env.BALLERINA_AUTH_REDIRECT_URL; +const isDevantDev = process.env.CLOUD_ENV === "dev"; +export const BACKEND_URL: string = config.get('rootUrl') || isDevantDev ? process.env.BALLERINA_DEV_COPLIOT_ROOT_URL : process.env.BALLERINA_ROOT_URL; +export const AUTH_ORG: string = config.get('authOrg') || isDevantDev ? process.env.BALLERINA_DEV_COPLIOT_AUTH_ORG : process.env.BALLERINA_AUTH_ORG; +export const AUTH_CLIENT_ID: string = config.get('authClientID') || isDevantDev ? process.env.BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID : process.env.BALLERINA_AUTH_CLIENT_ID; +export const AUTH_REDIRECT_URL: string = config.get('authRedirectURL') || isDevantDev ? process.env.BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL : process.env.BALLERINA_AUTH_REDIRECT_URL; + +export const DEVANT_STS_TOKEN_CONFIG: string = config.get('cloudStsToken') || process.env.CLOUD_STS_TOKEN; + +//TODO: Move to configs after custom URL approved +const DEVANT_DEV_EXCHANGE_URL = 'https://e95488c8-8511-4882-967f-ec3ae2a0f86f-dev.e1-us-east-azure.choreoapis.dev/ballerina-copilot/devant-token-exchange-ser/v1.0/exchange'; +const DEVANT_PROD_EXCHANGE_URL = 'https://e95488c8-8511-4882-967f-ec3ae2a0f86f-prod.e1-us-east-azure.choreoapis.dev/ballerina-copilot/devant-token-exchange-ser/v1.0/exchange'; + +export function getDevantExchangeUrl(): string { + if (isDevantDev) { + return DEVANT_DEV_EXCHANGE_URL; + } else { + return DEVANT_PROD_EXCHANGE_URL; + } +} // This refers to old backend before FE Migration. We need to eventually remove this. export const OLD_BACKEND_URL: string = BACKEND_URL + "/v2.0"; @@ -130,8 +146,26 @@ export async function getConfigFilePath(ballerinaExtInstance: BallerinaExtension export async function getTokenForDefaultModel() { try { - const token = await getRefreshedAccessToken(); - return token; + const credentials = await getAuthCredentials(); + + if (!credentials) { + throw new Error('No authentication credentials found.'); + } + + // Check login method and handle accordingly + if (credentials.loginMethod === LoginMethod.BI_INTEL) { + // Keep existing behavior for BI Intel - refresh token + const token = await getRefreshedAccessToken(); + return token; + } else if (credentials.loginMethod === LoginMethod.DEVANT_ENV) { + // For Devant, return stored access token + return credentials.secrets.accessToken; + } else { + // For anything else, show error + const errorMessage = 'This feature is only available for BI Intelligence users.'; + vscode.window.showErrorMessage(errorMessage); + throw new Error(errorMessage); + } } catch (error) { throw error; } diff --git a/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts b/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts new file mode 100644 index 00000000000..29450ad8354 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BI_COMMANDS, DIRECTORY_MAP, EVENT_TYPE, MACHINE_VIEW, SCOPE, findScopeByModule } from "@wso2/ballerina-core"; +import { + CommandIds as PlatformCommandIds, + IWso2PlatformExtensionAPI, + ICommitAndPuhCmdParams, + ICreateComponentCmdParams, +} from "@wso2/wso2-platform-core"; +import { BallerinaExtension } from "../../core"; +import { openView, StateMachine } from "../../stateMachine"; +import { commands, extensions, window } from "vscode"; +import * as path from "path"; +import * as fs from "fs"; +import { debug } from "../../utils"; + +export function activateDevantFeatures(_ballerinaExtInstance: BallerinaExtension) { + const cloudToken = process.env.CLOUD_STS_TOKEN; + if (cloudToken) { + // Set the connection token context + commands.executeCommand("setContext", "devant.editor", true); + } + + commands.registerCommand(BI_COMMANDS.DEVANT_PUSH_TO_CLOUD, handleComponentPushToDevant); +} + +const handleComponentPushToDevant = async () => { + const projectRoot = StateMachine.context().projectPath; + if (!projectRoot) { + return; + } + + const platformExt = extensions.getExtension("wso2.wso2-platform"); + if (!platformExt) { + return; + } + if (!platformExt.isActive) { + await platformExt.activate(); + } + const platformExtAPI: IWso2PlatformExtensionAPI = platformExt.exports; + if (isGitRepo(projectRoot)) { + // push changes to repo if component for the directory already exists + await commands.executeCommand(PlatformCommandIds.CommitAndPushToGit, { + componentPath: projectRoot, + } as ICommitAndPuhCmdParams); + } else if (platformExtAPI.getDirectoryComponents(projectRoot)?.length) { + debug(`project url: ${projectRoot}`); + // push changes to repo if component for the directory already exists + const hasChanges = await platformExtAPI.localRepoHasChanges(projectRoot); + if (!hasChanges) { + window.showInformationMessage("There are no new changes to push to cloud"); + return; + } + await commands.executeCommand(PlatformCommandIds.CommitAndPushToGit, { + componentPath: projectRoot, + } as ICommitAndPuhCmdParams); + } else { + // create a new component if it doesn't exist for the directory + if (!StateMachine.context().projectStructure) { + return; + } + const projectStructure = StateMachine.context().projectStructure.projects.find( + (proj) => proj.projectPath === projectRoot + ); + if (!projectStructure) { + return; + } + const services = projectStructure?.directoryMap[DIRECTORY_MAP.SERVICE]; + const automation = projectStructure?.directoryMap[DIRECTORY_MAP.AUTOMATION]; + const scopeSet = new Set(); + + if (services) { + services.find((svc) => { + const scope = findScopeByModule(svc?.moduleName); + if (scope) { + scopeSet.add(scope); + } + }); + } + + if (automation?.length > 0) { + scopeSet.add(SCOPE.AUTOMATION); + } + + let integrationType: SCOPE; + + if (scopeSet.size === 0) { + window + .showInformationMessage( + "Please add a construct and try again to deploy your integration", + "Add Construct" + ) + .then((resp) => { + if (resp === "Add Construct") { + openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.BIComponentView }); + } + }); + return; + } else if (scopeSet.size === 1) { + integrationType = [...scopeSet][0]; + } else { + const selectedScope = await window.showQuickPick([...scopeSet], { + placeHolder: "Multiple types of artifacts detected. Please select the artifact type to be deployed", + }); + integrationType = selectedScope as SCOPE; + } + + const deployementParams: ICreateComponentCmdParams = { + integrationType: integrationType as any, + buildPackLang: "ballerina", + componentDir: StateMachine.context().projectPath, + extName: "Devant", + }; + commands.executeCommand(PlatformCommandIds.CreateNewComponent, deployementParams); + } +}; + + +function isGitRepo(dir: string): boolean { + let currentDir = dir; + while (true) { + const gitDir = path.join(currentDir, ".git"); + if (fs.existsSync(gitDir)) { + return true; + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + // Reached the root directory + break; + } + currentDir = parentDir; + } + return false; +} + +// TODO: +// need to move all platform ext api calls to separate client. +// after that, delete this function +export const getDevantStsToken = async (): Promise => { + try { + const platformExt = extensions.getExtension("wso2.wso2-platform"); + if (!platformExt) { + return ""; + } + const platformExtAPI: IWso2PlatformExtensionAPI = await platformExt.activate(); + const stsToken = await platformExtAPI.getStsToken(); + return stsToken; + } catch (err) { + return ""; + } +}; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-extension/src/features/natural-programming/utils.ts b/workspaces/ballerina/ballerina-extension/src/features/natural-programming/utils.ts index f5e731c6063..49847d9fb9d 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/natural-programming/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/natural-programming/utils.ts @@ -39,8 +39,8 @@ import { } from "./constants"; import { isError, isNumber } from 'lodash'; import { HttpStatusCode } from 'axios'; +import { AIMachineEventType, BallerinaProject, BIIntelSecrets, LoginMethod } from '@wso2/ballerina-core'; import { isBallerinaProjectAsync, OLD_BACKEND_URL } from '../ai/utils'; -import { AIMachineEventType, BallerinaProject, LoginMethod } from '@wso2/ballerina-core'; import { getCurrentBallerinaProjectFromContext } from '../config-generator/configGenerator'; import { BallerinaExtension } from 'src/core'; import { getAccessToken as getAccesstokenFromUtils, getLoginMethod, getRefreshedAccessToken, REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE, TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL } from '../../../src/utils/ai/auth'; @@ -515,7 +515,9 @@ export async function getAccessToken(): Promise { let token: string; const loginMethod = await getLoginMethod(); if (loginMethod === LoginMethod.BI_INTEL) { - token = await getAccesstokenFromUtils(); + const credentials = await getAccesstokenFromUtils(); + const secrets = credentials.secrets as BIIntelSecrets; + token = secrets.accessToken; } resolve(token as string); }); diff --git a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/run.ts b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/run.ts index 28c4574c838..33743a3444b 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/run.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/run.ts @@ -108,6 +108,36 @@ function activateRunCmdCommand() { return; } + // TODO: Test in the cloud editor environment and remove the comments if working + // This should be handles automatically by the platform + + // Check if we should update auth token (only in cloud editor with private package dependencies) + // const shouldUpdate = await shouldUpdateAuthToken(); + + // if (shouldUpdate) { + // try { + // // Get the STS token from platform extension for authenticated operations + // const stsToken = await getDevantStsToken(); + // console.log("Cloud editor detected with dependencies, checking STS token..."); + + // // Only update Settings.toml if token needs updating + // if (stsToken && stsToken.trim() !== "") { + // const currentToken = await getCurrentAccessToken(); + + // if (shouldUpdateToken(currentToken, stsToken)) { + // await updateBallerinaSettingsWithStsToken(stsToken); + // console.log('Token updated in Settings.toml for cloud editor'); + // // Don't show notification in cloud editor to avoid noise + // } + // } else { + // console.warn('Unable to retrieve STS token in cloud editor environment'); + // } + // } catch (error) { + // console.warn('Failed to update authentication token in cloud editor:', error); + // // Continue execution even if token update fails + // } + // } + if (currentProject.kind !== PROJECT_TYPE.SINGLE_FILE) { const configPath: string = extension.ballerinaExtInstance.getBallerinaConfigPath(); extension.ballerinaExtInstance.setBallerinaConfigPath(''); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts index cf64500e89c..96f82979f3e 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts @@ -23,6 +23,7 @@ import { AIPanelAPI, AIPanelPrompt, AddFilesToProjectRequest, + BIIntelSecrets, BIModuleNodesRequest, BISourceCodeResponse, DeleteFromProjectRequest, @@ -123,10 +124,17 @@ export class AiPanelRpcManager implements AIPanelAPI { } try { - const workspaceFolderPath = workspace.workspaceFolders[0].uri.fsPath; + let projectIdentifier: string; + const cloudProjectId = process.env.CLOUD_INITIAL_PROJECT_ID; + + if (cloudProjectId) { + projectIdentifier = cloudProjectId; + } else { + projectIdentifier = workspace.workspaceFolders[0].uri.fsPath; + } const hash = crypto.createHash('sha256') - .update(workspaceFolderPath) + .update(projectIdentifier) .digest('hex'); resolve(hash); @@ -146,11 +154,14 @@ export class AiPanelRpcManager implements AIPanelAPI { async getAccessToken(): Promise { return new Promise(async (resolve, reject) => { try { - const accessToken = await getAccessToken(); - if (!accessToken) { + const credentials = await getAccessToken(); + + if (!credentials) { reject(new Error("Access Token is undefined")); return; } + const secrets = credentials.secrets as BIIntelSecrets; + const accessToken = secrets.accessToken; resolve(accessToken); } catch (error) { reject(error); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-manager.ts index 3ccfa493467..2f85f2c48f9 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-manager.ts @@ -23,7 +23,6 @@ import { AddFunctionRequest, AddImportItemResponse, ArtifactData, - AvailableNode, BIAiSuggestionsRequest, BIAiSuggestionsResponse, BIAvailableNodesRequest, @@ -54,11 +53,9 @@ import { BallerinaProject, BreakpointRequest, BuildMode, - Category, ClassFieldModifierRequest, Command, ComponentRequest, - ConfigVariableRequest, ConfigVariableResponse, CreateComponentResponse, CurrentBreakpointsResponse, @@ -91,14 +88,12 @@ import { GetTypeResponse, GetTypesRequest, GetTypesResponse, - Item, JsonToTypeRequest, JsonToTypeResponse, LinePosition, LoginMethod, ModelFromCodeRequest, NodeKind, - NodePosition, OpenAPIClientDeleteRequest, OpenAPIClientDeleteResponse, OpenAPIClientGenerationRequest, @@ -141,6 +136,12 @@ import { VisibleTypesResponse, WorkspaceFolder, WorkspacesResponse, + BIIntelSecrets, + ConfigVariableRequest, + AvailableNode, + Item, + Category, + NodePosition, FormDiagnosticsRequest, FormDiagnosticsResponse, ExpressionTokensRequest, @@ -720,7 +721,9 @@ export class BiDiagramRpcManager implements BIDiagramAPI { let token: string; const loginMethod = await getLoginMethod(); if (loginMethod === LoginMethod.BI_INTEL) { - token = await getAccessToken(); + const credentials = await getAccessToken(); + const secrets = credentials.secrets as BIIntelSecrets; + token = secrets.accessToken; } if (!token) { @@ -758,7 +761,9 @@ export class BiDiagramRpcManager implements BIDiagramAPI { let token: string; const loginMethod = await getLoginMethod(); if (loginMethod === LoginMethod.BI_INTEL) { - token = await getAccessToken(); + const credentials = await getAccessToken(); + const secrets = credentials.secrets as BIIntelSecrets; + token = secrets.accessToken; } if (!token) { //TODO: Do we need to prompt to login here? If so what? Copilot or Ballerina AI? diff --git a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts index 15a5831b80b..16586c0b920 100644 --- a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts @@ -48,6 +48,7 @@ import { UndoRedoManager, getOrgAndPackageName } from './utils'; +import { activateDevantFeatures } from './features/devant/activator'; import { buildProjectsStructure } from './utils/project-artifacts'; import { runCommandWithOutput } from './utils/runCommand'; import { buildOutputChannel } from './utils/logger'; @@ -66,6 +67,7 @@ interface MachineContext extends VisualizerLocation { isBISupported: boolean; errorCode: string | null; dependenciesResolved?: boolean; + isInDevant: boolean; } export let history: History; @@ -83,7 +85,8 @@ const stateMachine = createMachine( errorCode: null, isBISupported: false, view: MACHINE_VIEW.PackageOverview, - dependenciesResolved: false + dependenciesResolved: false, + isInDevant: !!process.env.CLOUD_STS_TOKEN }, on: { RESET_TO_EXTENSION_READY: { @@ -461,6 +464,7 @@ const stateMachine = createMachine( if (!ls.biSupported) { commands.executeCommand('setContext', 'BI.status', 'updateNeed'); } + activateDevantFeatures(ls); resolve({ langClient: ls.langClient, isBISupported: ls.biSupported }); } catch (error) { throw new Error("LS Activation failed", error); diff --git a/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts b/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts index 465db245f82..ae521dc1807 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts @@ -18,10 +18,12 @@ import * as vscode from 'vscode'; import { extension } from "../../BalExtensionContext"; -import { AUTH_CLIENT_ID, AUTH_ORG } from '../../features/ai/utils'; +import { AUTH_CLIENT_ID, AUTH_ORG, getDevantExchangeUrl } from '../../features/ai/utils'; import axios from 'axios'; import { jwtDecode, JwtPayload } from 'jwt-decode'; -import { AuthCredentials, LoginMethod } from '@wso2/ballerina-core'; +import { AuthCredentials, DevantEnvSecrets, LoginMethod } from '@wso2/ballerina-core'; +import { checkDevantEnvironment } from '../../views/ai-panel/utils'; +import { getDevantStsToken } from '../../features/devant/activator'; export const REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE = "Refresh token is not available."; export const TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL = "Token refresh is only supported for BI Intelligence authentication"; @@ -148,6 +150,13 @@ export const clearAuthCredentials = async (): Promise => { // BI Copilot Auth Utils // ================================== export const getLoginMethod = async (): Promise => { + // Priority 1: Check devant environment first + const devantCredentials = await checkDevantEnvironment(); + if (devantCredentials) { + return devantCredentials.loginMethod; + } + + // Priority 2: Check stored credentials if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim() !== "") { return LoginMethod.ANTHROPIC_KEY; } @@ -158,11 +167,19 @@ export const getLoginMethod = async (): Promise => { return undefined; }; -export const getAccessToken = async (): Promise => { +export const getAccessToken = async (): Promise => { return new Promise(async (resolve, reject) => { try { + // Priority 1: Check devant environment (highest priority) + const devantCredentials = await checkDevantEnvironment(); + if (devantCredentials) { + resolve(devantCredentials); + return; + } + + // Priority 2: Check stored credentials if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim() !== "") { - resolve(process.env.ANTHROPIC_API_KEY.trim()); + resolve({loginMethod: LoginMethod.ANTHROPIC_KEY, secrets: {apiKey: process.env.ANTHROPIC_API_KEY.trim()}}); return; } const credentials = await getAuthCredentials(); @@ -180,7 +197,13 @@ export const getAccessToken = async (): Promise => { if (decoded.exp && decoded.exp < now) { finalToken = await getRefreshedAccessToken(); } - resolve(finalToken); + resolve({ + loginMethod: LoginMethod.BI_INTEL, + secrets: { + accessToken: finalToken, + refreshToken: credentials.secrets.refreshToken + } + }); return; } catch (err) { if (axios.isAxiosError(err)) { @@ -195,11 +218,15 @@ export const getAccessToken = async (): Promise => { } case LoginMethod.ANTHROPIC_KEY: - resolve(credentials.secrets.apiKey); + resolve(credentials); + return; + + case LoginMethod.DEVANT_ENV: + resolve(credentials); return; case LoginMethod.AWS_BEDROCK: - resolve(credentials.secrets.accessKeyId); + resolve(credentials); return; default: @@ -276,3 +303,66 @@ export const getRefreshedAccessToken = async (): Promise => { } }); }; + +// ================================== +// Devant STS Token Exchange Utils +// ================================== + +/** + * Exchanges a Choreo STS token for a Devant Bearer token + * @param choreoStsToken The Choreo STS token to exchange + * @returns DevantEnvSecrets containing the access token and calculated expiry time + */ +export const exchangeStsToken = async (choreoStsToken: string): Promise => { + try { + const response = await axios.post(getDevantExchangeUrl(), { + choreo_sts_token: choreoStsToken + }, { + headers: { + 'Content-Type': 'application/json' + } + }); + + const { access_token, expires_in } = response.data; + const devantEnv: DevantEnvSecrets = { + accessToken: access_token, + expiresAt: Date.now() + (expires_in * 1000) // Convert seconds to milliseconds + }; + + await storeAuthCredentials({ + loginMethod: LoginMethod.DEVANT_ENV, + secrets: devantEnv + }); + return devantEnv; + } catch (error: any) { + console.error('Error exchanging STS token:', error); + throw new Error(`Failed to exchange STS token: ${error.message}`); + } +}; + +/** + * Refreshes the Devant token by fetching a new STS token and exchanging it + * This is called when a 401 error occurs during DEVANT_ENV authentication + * @returns The new access token + */ +export const refreshDevantToken = async (): Promise => { + try { + // Get fresh STS token from platform extension + const newStsToken = await getDevantStsToken(); + + if (!newStsToken) { + throw new Error('Failed to retrieve STS token from platform extension'); + } + + // Exchange for new Bearer token + const newSecrets = await exchangeStsToken(newStsToken); + + // Update stored credentials (this is in-memory only for DEVANT_ENV) + // Note: checkDevantEnvironment already handles the storage, so we just return the token + + return newSecrets.accessToken; + } catch (error: any) { + console.error('Error refreshing Devant token:', error); + throw error; + } +}; diff --git a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts index f2c0a93b88e..75c813df5fd 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts @@ -72,7 +72,7 @@ const aiMachine = createMachine({ target: 'Authenticated', actions: assign({ loginMethod: (_ctx, event) => event.data.loginMethod, - userToken: (_ctx, event) => ({ token: event.data.token }), + userToken: (_ctx, event) => ({ credentials: event.data }), errorMessage: (_ctx) => undefined, }) }, @@ -259,7 +259,7 @@ const aiMachine = createMachine({ src: 'getTokenAfterAuth', onDone: { actions: assign({ - userToken: (_ctx, event) => ({ token: event.data.token }), + userToken: (_ctx, event) => ({ credentials: event.data.credentials }), loginMethod: (_ctx, event) => event.data.loginMethod, errorMessage: (_ctx) => undefined, }) @@ -354,7 +354,7 @@ const getTokenAfterAuth = async () => { if (!result || !loginMethod) { throw new Error('No authentication credentials found'); } - return { token: result, loginMethod: loginMethod }; + return { credentials: result.secrets, loginMethod: result.loginMethod }; }; const aiStateService = interpret(aiMachine.withConfig({ diff --git a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts index d958ec2228c..8033adafaca 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts @@ -23,25 +23,26 @@ import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import { getAuthUrl, getLogoutUrl } from './auth'; import { extension } from '../../BalExtensionContext'; -import { getAccessToken, clearAuthCredentials, storeAuthCredentials, getLoginMethod } from '../../utils/ai/auth'; +import { getAccessToken, clearAuthCredentials, storeAuthCredentials, getLoginMethod, exchangeStsToken, getAuthCredentials } from '../../utils/ai/auth'; +import { DEVANT_STS_TOKEN_CONFIG } from '../../features/ai/utils'; import { getBedrockRegionalPrefix } from '../../features/ai/service/connection'; +import { getDevantStsToken } from '../../features/devant/activator'; const LEGACY_ACCESS_TOKEN_SECRET_KEY = 'BallerinaAIUser'; const LEGACY_REFRESH_TOKEN_SECRET_KEY = 'BallerinaAIRefreshToken'; -export const checkToken = async (): Promise<{ token: string; loginMethod: LoginMethod } | undefined> => { +export const checkToken = async (): Promise => { return new Promise(async (resolve, reject) => { try { // Clean up any legacy tokens on initialization await cleanupLegacyTokens(); - const token = await getAccessToken(); - const loginMethod = await getLoginMethod(); - if (!token || !loginMethod) { + const credentials = await getAccessToken(); + if (!credentials) { resolve(undefined); return; } - resolve({ token, loginMethod }); + resolve(credentials); } catch (error) { reject(error); } @@ -65,8 +66,8 @@ const cleanupLegacyTokens = async (): Promise => { export const logout = async (isUserLogout: boolean = true) => { // For user-initiated logout, check if we need to redirect to SSO logout if (isUserLogout) { - const { token, loginMethod } = await checkToken(); - if (token && loginMethod === LoginMethod.BI_INTEL) { + const credentials = await checkToken(); + if (credentials.loginMethod === LoginMethod.BI_INTEL) { const logoutURL = getLogoutUrl(); vscode.env.openExternal(vscode.Uri.parse(logoutURL)); } @@ -114,7 +115,7 @@ export const validateApiKey = async (apiKey: string, loginMethod: LoginMethod): }; await storeAuthCredentials(credentials); - return { token: apiKey }; + return { credentials: credentials }; } catch (error) { console.error('API key validation failed:', error); @@ -132,6 +133,53 @@ export const validateApiKey = async (apiKey: string, loginMethod: LoginMethod): } }; +export const checkDevantEnvironment = async (): Promise => { + // Check if CLOUD_STS_TOKEN environment variable exists (Devant flow identifier) + if (!('CLOUD_STS_TOKEN' in process.env)) { + return undefined; + } + + try { + // Check if a valid access token already exists to avoid redundant exchanges + const existingCredentials = await getAuthCredentials(); + + if (existingCredentials && existingCredentials.loginMethod === LoginMethod.DEVANT_ENV) { + // existing session, check expiry + const { expiresAt } = existingCredentials.secrets; + const now = Date.now(); + + // If token is still valid (not expired), return existing credentials + if (expiresAt && expiresAt > now) { + return existingCredentials; + } + } + if (existingCredentials && existingCredentials.loginMethod !== LoginMethod.DEVANT_ENV) { + // not devant + return undefined; + } + + // Get STS token from config or platform extension + const choreoStsToken = await getDevantStsToken() || DEVANT_STS_TOKEN_CONFIG; + + if (!choreoStsToken || choreoStsToken.trim() === '') { + console.warn('CLOUD_STS_TOKEN env variable exists but no STS token available'); + return undefined; + } + + // Exchange STS token for Bearer token (if no valid token exists or token expired) + const devantSecrets = await exchangeStsToken(choreoStsToken); + + // Return devant credentials without storing (always read from env and exchange on demand) + return { + loginMethod: LoginMethod.DEVANT_ENV, + secrets: devantSecrets + }; + } catch (error) { + console.error('Error in checkDevantEnvironment:', error); + return undefined; + } +}; + export const validateAwsCredentials = async (credentials: { accessKeyId: string; secretAccessKey: string; @@ -154,7 +202,7 @@ export const validateAwsCredentials = async (credentials: { // List of valid AWS regions const validRegions = [ - 'us-east-1', 'us-west-2', 'us-west-1', 'eu-west-1', 'eu-central-1', + 'us-east-1', 'us-west-2', 'us-west-1', 'eu-west-1', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'ca-central-1', 'sa-east-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'ap-east-1', 'me-south-1', 'af-south-1', 'ap-southeast-3' @@ -171,13 +219,13 @@ export const validateAwsCredentials = async (credentials: { secretAccessKey: secretAccessKey, sessionToken: sessionToken, }); - + // Get regional prefix based on AWS region and construct model ID const regionalPrefix = getBedrockRegionalPrefix(region); const modelId = `${regionalPrefix}.anthropic.claude-3-5-haiku-20241022-v1:0`; const bedrockClient = bedrock(modelId); - // Make a minimal test call to validate credentials + // Make a minimal test call to validate credentials await generateText({ model: bedrockClient, maxOutputTokens: 1, @@ -196,7 +244,7 @@ export const validateAwsCredentials = async (credentials: { }; await storeAuthCredentials(authCredentials); - return { token: accessKeyId }; + return { credentials: authCredentials }; } catch (error) { console.error('AWS Bedrock validation failed:', error); diff --git a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts index 93172a02b46..ecf6d46eb55 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts @@ -146,6 +146,9 @@ export class VisualizerWebview { } private getWebviewContent(webView: Webview) { + // Check if devant.editor extension is active + const isDevantEditor = vscode.commands.executeCommand('getContext', 'devant.editor') !== undefined; + const biExtension = vscode.extensions.getExtension('wso2.ballerina-integrator'); const body = `
@@ -251,6 +254,9 @@ export class VisualizerWebview { } `; const scripts = ` + // Flag to check if devant.editor is active + window.isDevantEditor = ${isDevantEditor}; + function loadedScript() { function renderDiagrams() { visualizerWebview.renderWebview("visualizer", document.getElementById("webview-container")); diff --git a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx index 52de4b84a17..147dae0c14e 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx @@ -286,6 +286,7 @@ const MainPanel = () => { setViewComponent( ); break; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/SettingsPanel/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/SettingsPanel/index.tsx index 9961fdfce02..b1622c11055 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/SettingsPanel/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/SettingsPanel/index.tsx @@ -21,7 +21,7 @@ import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { Button, Codicon, Typography } from "@wso2/ui-toolkit"; import { AIChatView } from "../styles"; -import { AIMachineEventType } from "@wso2/ballerina-core"; +import { AIMachineEventType, LoginMethod } from "@wso2/ballerina-core"; const Container = styled.div` display: flex; @@ -62,6 +62,7 @@ export const SettingsPanel = (props: { onClose: () => void }) => { const { rpcClient } = useRpcContext(); const [copilotAuthorized, setCopilotAuthorized] = React.useState(false); + const [shouldShowLogoutButton, setShouldShowLogoutButton] = React.useState(true); const messagesEndRef = createRef(); @@ -69,6 +70,14 @@ export const SettingsPanel = (props: { onClose: () => void }) => { isCopilotAuthorized().then((authorized) => { setCopilotAuthorized(authorized); }); + + rpcClient + .getAiPanelRpcClient() + .getLoginMethod() + .then((loginMethod) => { + console.log("Login Method: ", loginMethod); + setShouldShowLogoutButton(loginMethod !== LoginMethod.DEVANT_ENV); + }); }, []); const handleCopilotLogout = () => { @@ -100,16 +109,18 @@ export const SettingsPanel = (props: { onClose: () => void }) => { Connect to AI Platforms for Enhanced Features - - - Logout from BI Copilot - - Logging out will end your session and disconnect access to AI-powered tools like code - generation, completions, test generation, and data mappings. - - - - + {shouldShowLogoutButton && ( + + + Logout from BI Copilot + + Logging out will end your session and disconnect access to AI-powered tools like code + generation, completions, test generation, and data mappings. + + + + + )} Enable GitHub Copilot Integration diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx index f4583210cb7..d2653a308c0 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx @@ -518,12 +518,101 @@ function IntegrationControlPlane({ enabled, handleICP }: IntegrationControlPlane ); } +function DevantDashboard({ projectStructure, handleDeploy, goToDevant, devantMetadata }: { projectStructure: ProjectStructure, handleDeploy: () => void, goToDevant: () => void, devantMetadata: DevantMetadata }) { + const { rpcClient } = useRpcContext(); + + const handleSaveAndDeployToDevant = () => { + handleDeploy(); + } + + const handlePushChanges = () => { + rpcClient.getCommonRpcClient().executeCommand({ commands: [BI_COMMANDS.DEVANT_PUSH_TO_CLOUD] }); + } + + // Check if project has automation or service + const hasAutomationOrService = projectStructure?.directoryMap && ( + (projectStructure.directoryMap.AUTOMATION && projectStructure.directoryMap.AUTOMATION.length > 0) || + (projectStructure.directoryMap.SERVICE && projectStructure.directoryMap.SERVICE.length > 0) + ); + + console.log(">>> devantMetadata", devantMetadata); + + return ( + + {devantMetadata?.hasComponent ? Deployed in Devant : Deploy to Devant} + {!hasAutomationOrService ? ( + + Before you can deploy your integration to Devant, please add an artifact (such as a Service or Automation) to your project. + + ) : ( + <> + {devantMetadata?.hasComponent ? ( + <> + + This integration is deployed in Devant. + + + + + ) : ( + + + Deploy your integration to Devant and run it in the cloud. + + + + )} + + )} + + ); +} + + interface PackageOverviewProps { projectPath: string; + isInDevant: boolean; } export function PackageOverview(props: PackageOverviewProps) { - const { projectPath } = props; + const { projectPath, isInDevant } = props; const { rpcClient } = useRpcContext(); const [readmeContent, setReadmeContent] = useState(""); const [projectStructure, setProjectStructure] = useState(); @@ -537,7 +626,6 @@ export function PackageOverview(props: PackageOverviewProps) { }); const [showAlert, setShowAlert] = useState(false); - const fetchContext = () => { rpcClient .getBIDiagramRpcClient() @@ -853,16 +941,28 @@ export function PackageOverview(props: PackageOverviewProps) { - 0} - /> - - + {!isInDevant && + <> + 0} + /> + + + + } + {isInDevant && + + } diff --git a/workspaces/choreo/choreo-extension/.vscode/launch.json b/workspaces/choreo/choreo-extension/.vscode/launch.json index 6aa28de0ef1..bd41a76ab95 100644 --- a/workspaces/choreo/choreo-extension/.vscode/launch.json +++ b/workspaces/choreo/choreo-extension/.vscode/launch.json @@ -10,8 +10,8 @@ "type": "extensionHost", "request": "launch", "env": { - "WEB_VIEW_DEV_MODE": "true", - "WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", + "WEB_VIEW_DEV_MODE_CHOREO": "true", + "WEB_VIEW_DEV_HOST_CHOREO": "http://localhost:3001/main.js", "REQUEST_TRACE_ENABLED": "true" }, "args": [ @@ -38,8 +38,8 @@ ], "envFile": "${workspaceFolder}/.env", "env": { - "WEB_VIEW_DEV_MODE": "true", - "WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", + "WEB_VIEW_DEV_MODE_CHOREO": "true", + "WEB_VIEW_DEV_HOST_CHOREO": "http://localhost:3001/main.js", }, "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", diff --git a/workspaces/choreo/choreo-extension/README.md b/workspaces/choreo/choreo-extension/README.md index ee061dafb96..b05fbd877ab 100644 --- a/workspaces/choreo/choreo-extension/README.md +++ b/workspaces/choreo/choreo-extension/README.md @@ -17,6 +17,7 @@ The Choreo VS Code extension enhances your local development experience with [Ch - **Deploy Builds**: Deploy your component builds to any chosen [environments](https://wso2.com/choreo/docs/choreo-concepts/environments/). - **Test Services**: Verify the functionality of publicly exposed services. - **Monitor Components**: Access runtime logs to monitor your deployed components. +- **Connect Locally to Dependencies**: Link your app to dependent connections while developing. See [guide](https://wso2.com/choreo/docs/develop-components/connect-to-remote-dependencies-while-developing/). ## Screenshots @@ -52,4 +53,4 @@ Feel free to create [GitHub issues](https://github.com/wso2/choreo-vscode/issues ## License -This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/workspaces/choreo/choreo-extension/package.json b/workspaces/choreo/choreo-extension/package.json index 409b9dc9285..4e9a0b30daf 100644 --- a/workspaces/choreo/choreo-extension/package.json +++ b/workspaces/choreo/choreo-extension/package.json @@ -3,8 +3,7 @@ "displayName": "Choreo", "description": "An extension for managing your Choreo projects and components", "license": "Apache-2.0", - "version": "2.2.5-2", - "cliVersion": "v1.2.92505041530", + "version": "2.2.7", "publisher": "wso2", "bugs": { "url": "https://github.com/wso2/choreo-vscode/issues" @@ -190,7 +189,6 @@ "devDependencies": { "@playwright/test": "1.55.1", "@types/byline": "^4.2.36", - "@types/js-yaml": "^4.0.9", "@types/mocha": "~10.0.1", "@types/node": "^22.15.24", "@types/vscode": "^1.100.0", @@ -230,7 +228,6 @@ "vscode-messenger": "^0.5.1", "vscode-messenger-common": "^0.5.1", "which": "^5.0.0", - "vscode-jsonrpc": "^8.2.1", - "zustand": "^5.0.5" + "vscode-jsonrpc": "^8.2.1" } } diff --git a/workspaces/choreo/choreo-extension/src/webviews/utils.ts b/workspaces/choreo/choreo-extension/src/webviews/utils.ts index 9fd0755817b..bf57acc1a95 100644 --- a/workspaces/choreo/choreo-extension/src/webviews/utils.ts +++ b/workspaces/choreo/choreo-extension/src/webviews/utils.ts @@ -22,13 +22,13 @@ import { ProjectActivityView } from "./ProjectActivityView"; export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { if (shouldUseWebViewDevMode(pathList)) { - return process.env.WEB_VIEW_DEV_HOST; + return process.env.WEB_VIEW_DEV_HOST_CHOREO; } return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); } function shouldUseWebViewDevMode(pathList: string[]): boolean { - return pathList[pathList.length - 1] === "main.js" && process.env.WEB_VIEW_DEV_MODE === "true" && process.env.WEB_VIEW_DEV_HOST !== undefined; + return pathList[pathList.length - 1] === "main.js" && process.env.WEB_VIEW_DEV_MODE_CHOREO === "true" && process.env.WEB_VIEW_DEV_HOST_CHOREO !== undefined; } export function activateActivityWebViews(context: vscode.ExtensionContext) { diff --git a/workspaces/choreo/choreo-webviews/package.json b/workspaces/choreo/choreo-webviews/package.json index 8e4bdc65ff5..258c48efd11 100644 --- a/workspaces/choreo/choreo-webviews/package.json +++ b/workspaces/choreo/choreo-webviews/package.json @@ -7,7 +7,7 @@ "scripts": { "lint": "biome check .", "lint:fix": "biome check --write --unsafe . ", - "start": "webpack serve --mode development", + "start": "webpack serve --mode development --port 3001", "build": "tsc --pretty && webpack && npm run copy:assets", "copy:assets": "copyfiles -u 1 \"src/**/*.scss\" \"src/**/*.svg\" \"src/**/*.css\" \"src/**/*.png\" \"src/**/*.txt\" \"src/**/*.json\" \"src/assets/fonts/Gilmer/*.*\" lib/" }, @@ -26,16 +26,10 @@ "classnames": "~2.5.1", "@tanstack/react-query-persist-client": "~4.28.0", "@tanstack/react-query": "~4.28.0", - "zod": "^3.22.4", "react-hook-form": "7.56.4", "@hookform/resolvers": "^5.0.1", - "clipboardy": "^4.0.0", "@formkit/auto-animate": "0.8.2", - "timezone-support": "^3.1.0", - "swagger-ui-react": "^5.22.0", "@biomejs/biome": "^1.9.4", - "@headlessui/react": "^2.2.4", - "react-markdown": "^7.1.0", "rehype-raw": "^6.1.0", "remark-gfm": "^4.0.1", "prism-react-renderer": "^2.4.1", @@ -48,7 +42,6 @@ "@types/node": "^22.15.24", "@types/react": "18.2.0", "@types/react-dom": "18.2.0", - "@types/swagger-ui-react": "^5.18.0", "typescript": "5.8.3", "@types/vscode-webview": "^1.57.5", "css-loader": "^7.1.2", @@ -64,8 +57,7 @@ "postcss-loader" :"^8.1.1", "autoprefixer": "^10.4.21", "tailwindcss": "^3.4.3", - "@types/lodash.debounce": "^4.0.9", - "@types/js-yaml": "^4.0.5" + "@types/lodash.debounce": "^4.0.9" }, "browserslist": { "production": [ diff --git a/workspaces/wso2-platform/wso2-platform-core/src/constants.ts b/workspaces/wso2-platform/wso2-platform-core/src/constants.ts index 678ea7b0980..06f83669ce6 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/constants.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/constants.ts @@ -35,6 +35,7 @@ export const CommandIds = { CreateComponentDependency: "wso2.wso2-platform.component.create.dependency", ViewDependency: "wso2.wso2-platform.component.view.dependency", OpenCompSrcDir: "wso2.wso2-platform.open.component.src", + CommitAndPushToGit: "wso2.wso2-platform.push-to-git", // TODO: add command & code lens to delete dependency }; diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts index 60981cb8b68..653ee327623 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts @@ -31,11 +31,14 @@ import type { DeploymentLogsData, DeploymentTrack, Environment, + GitRepoMetadata, + GithubOrganization, MarketplaceItem, Pagination, Project, ProjectBuildLogsData, ProxyDeploymentInfo, + SubscriptionItem, } from "./common.types"; import type { InboundConfig } from "./config-file.types"; @@ -53,6 +56,11 @@ export interface GetCredentialsReq { orgId: string; orgUuid: string; } +export interface GetCredentialDetailsReq { + orgId: string; + orgUuid: string; + credentialId: string; +} export interface IsRepoAuthorizedReq { orgId: string; repoUrl: string; @@ -385,6 +393,10 @@ export interface GetBuildLogsReq { displayType: string; projectId: string; buildId: number; + orgUuid: string; + buildRef: string; + deploymentTrackId: string; + clusterId: string; } export interface GetBuildLogsForTypeReq { @@ -394,6 +406,76 @@ export interface GetBuildLogsForTypeReq { buildId: number; } +export interface GetSubscriptionsReq { + orgId: string; + cloudType?: string; +} + +export interface UpdateCodeServerReq { + orgId: string; + orgUuid: string; + orgHandle: string; + projectId: string; + componentId: string; + sourceCommitHash: string; +} + +export interface GetGitTokenForRepositoryReq { + orgId: string; + gitOrg: string; + gitRepo: string; + secretRef: string; +} + +export interface GetGitTokenForRepositoryResp { + token: string; + gitOrganization: string; + gitRepository: string; + vendor: string; + username: string; + serverUrl: string; +} + +export interface GetGitMetadataReq { + orgId: string; + gitOrgName: string; + gitRepoName: string; + branch: string; + relativePath: string; + secretRef: string; +} + +export interface GetGitMetadataResp { + metadata: GitRepoMetadata; +} + +export interface SubscriptionsResp { + count: number; + list: SubscriptionItem[]; + cloudType: string; + emailType: string; +} + +export interface GetAuthorizedGitOrgsReq { + orgId: string; + credRef: string; +} + +export interface GetAuthorizedGitOrgsResp { + gitOrgs: GithubOrganization[]; +} + +export interface GetCliRpcResp { + billingConsoleUrl: string; + choreoConsoleUrl: string; + devantConsoleUrl: string; + ghApp: { + installUrl: string; + authUrl: string; + clientId: string; + }; +} + export interface IChoreoRPCClient { getComponentItem(params: GetComponentItemReq): Promise; getDeploymentTracks(params: GetDeploymentTracksReq): Promise; @@ -405,7 +487,9 @@ export interface IChoreoRPCClient { getBuildPacks(params: BuildPackReq): Promise; getRepoBranches(params: GetBranchesReq): Promise; isRepoAuthorized(params: IsRepoAuthorizedReq): Promise; + getAuthorizedGitOrgs(params: GetAuthorizedGitOrgsReq): Promise; getCredentials(params: GetCredentialsReq): Promise; + getCredentialDetails(params: GetCredentialDetailsReq): Promise; deleteComponent(params: DeleteCompReq): Promise; getBuilds(params: GetBuildsReq): Promise; createBuild(params: CreateBuildReq): Promise; @@ -433,6 +517,9 @@ export interface IChoreoRPCClient { cancelApprovalRequest(params: CancelApprovalReq): Promise; requestPromoteApproval(params: RequestPromoteApprovalReq): Promise; promoteProxyDeployment(params: PromoteProxyDeploymentReq): Promise; + getSubscriptions(params: GetSubscriptionsReq): Promise; + getGitTokenForRepository(params: GetGitTokenForRepositoryReq): Promise; + getGitRepoMetadata(params: GetGitMetadataReq): Promise; } export class ChoreoRpcWebview implements IChoreoRPCClient { @@ -462,9 +549,15 @@ export class ChoreoRpcWebview implements IChoreoRPCClient { isRepoAuthorized(params: IsRepoAuthorizedReq): Promise { return this._messenger.sendRequest(ChoreoRpcIsRepoAuthorizedRequest, HOST_EXTENSION, params); } + getAuthorizedGitOrgs(params: GetAuthorizedGitOrgsReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetAuthorizedGitOrgsRequest, HOST_EXTENSION, params); + } getCredentials(params: GetCredentialsReq): Promise { return this._messenger.sendRequest(ChoreoRpcGetCredentialsRequest, HOST_EXTENSION, params); } + getCredentialDetails(params: GetCredentialDetailsReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetCredentialDetailsRequest, HOST_EXTENSION, params); + } deleteComponent(params: DeleteCompReq): Promise { return this._messenger.sendRequest(ChoreoRpcDeleteComponentRequest, HOST_EXTENSION, params); } @@ -549,6 +642,15 @@ export class ChoreoRpcWebview implements IChoreoRPCClient { promoteProxyDeployment(params: PromoteProxyDeploymentReq): Promise { return this._messenger.sendRequest(ChoreoRpcPromoteProxyDeployment, HOST_EXTENSION, params); } + getSubscriptions(params: GetSubscriptionsReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetSubscriptions, HOST_EXTENSION, params); + } + getGitTokenForRepository(params: GetGitTokenForRepositoryReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetGitTokenForRepository, HOST_EXTENSION, params); + } + getGitRepoMetadata(params: GetGitMetadataReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetGitRepoMetadata, HOST_EXTENSION, params); + } } export const ChoreoRpcGetProjectsRequest: RequestType = { method: "rpc/project/getProjects" }; @@ -559,7 +661,11 @@ export const ChoreoRpcCreateComponentRequest: RequestType = { method: "rpc/component/getBuildPacks" }; export const ChoreoRpcGetBranchesRequest: RequestType = { method: "rpc/repo/getBranches" }; export const ChoreoRpcIsRepoAuthorizedRequest: RequestType = { method: "rpc/repo/isRepoAuthorized" }; +export const ChoreoRpcGetAuthorizedGitOrgsRequest: RequestType = { + method: "rpc/repo/getAuthorizedGitOrgs", +}; export const ChoreoRpcGetCredentialsRequest: RequestType = { method: "rpc/repo/getCredentials" }; +export const ChoreoRpcGetCredentialDetailsRequest: RequestType = { method: "rpc/repo/getCredentialDetails" }; export const ChoreoRpcDeleteComponentRequest: RequestType = { method: "rpc/component/delete" }; export const ChoreoRpcCreateBuildRequest: RequestType = { method: "rpc/build/create" }; export const ChoreoRpcGetDeploymentTracksRequest: RequestType = { @@ -612,3 +718,12 @@ export const ChoreoRpcRequestPromoteApproval: RequestType = { method: "rpc/deployment/promoteProxy", }; +export const ChoreoRpcGetSubscriptions: RequestType = { + method: "rpc/auth/getSubscriptions", +}; +export const ChoreoRpcGetGitTokenForRepository: RequestType = { + method: "rpc/repo/gitTokenForRepository", +}; +export const ChoreoRpcGetGitRepoMetadata: RequestType = { + method: "rpc/repo/getRepoMetadata", +}; diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts index 4f62c0fa3a2..a7b50c84f86 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts @@ -15,6 +15,10 @@ export interface ICloneProjectCmdParams extends ICmdParamsBase { integrationDisplayType: string; } +export interface ICommitAndPuhCmdParams extends ICmdParamsBase { + componentPath: string; +} + export interface ICreateDependencyParams extends ICmdParamsBase { componentFsPath?: string; isCodeLens?: boolean; diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts index 98630317e03..e0cef78c396 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts @@ -17,17 +17,29 @@ */ import type { DeploymentStatus } from "../enums"; -import type { ContextStoreState, WebviewState } from "./store.types"; +import type { AuthState, ContextItemEnriched, ContextStoreState, WebviewState } from "./store.types"; export type ExtensionName = "WSO2" | "Choreo" | "Devant"; export interface IWso2PlatformExtensionAPI { + getAuthState(): AuthState; isLoggedIn(): boolean; getDirectoryComponents(fsPath: string): ComponentKind[]; localRepoHasChanges(fsPath: string): Promise; getWebviewStateStore(): WebviewState; getContextStateStore(): ContextStoreState; openClonedDir(params: openClonedDirReq): Promise; + getStsToken(): Promise; + getSelectedContext(): ContextItemEnriched | null; + getDevantConsoleUrl: () => Promise; + + // Auth Subscription + subscribeAuthState(callback: (state: AuthState)=>void): () => void; + subscribeIsLoggedIn(callback: (isLoggedIn: boolean)=>void): () => void; + + // Context Subscription + subscribeDirComponents(fsPath: string, callback: (comps: ComponentKind[])=>void): () => void; + subscribeContextState(callback: (state: ContextItemEnriched | undefined)=>void): () => void; } export interface openClonedDirReq { @@ -67,6 +79,7 @@ export interface ComponentKindSource { bitbucket?: ComponentKindGitProviderSource; github?: ComponentKindGitProviderSource; gitlab?: ComponentKindGitProviderSource; + secretRef?: string; } export interface ComponentKindBuildDocker { @@ -172,6 +185,8 @@ export interface BuildKind { completedAt: string; images: { id: string; createdAt: string; updatedAt: string }[]; gitCommit: { message: string; author: string; date: string; email: string }; + clusterId: string; + buildRef: string; }; } @@ -548,4 +563,45 @@ export interface CredentialItem { organizationUuid: string; type: string; referenceToken: string; + serverUrl: string; +} + +export interface SubscriptionItem { + subscriptionId: string; + tierId: string; + supportPlanId: string; + cloudType: string; + subscriptionType: string; + subscriptionBillingProvider: string; + subscriptionBillingProviderStatus: string; +} + +export interface GithubRepository { + name: string; +} + +export interface GithubOrganization { + orgName: string; + orgHandler: string; + repositories: GithubRepository[]; +} + +export interface GitRepoMetadata { + isBareRepo: boolean; + isSubPathEmpty: boolean; + isSubPathValid: boolean; + isValidRepo: boolean; + hasBallerinaTomlInPath: boolean; + hasBallerinaTomlInRoot: boolean; + isDockerfilePathValid: boolean; + hasDockerfileInPath: boolean; + isDockerContextPathValid: boolean; + isOpenApiFilePathValid: boolean; + hasOpenApiFileInPath: boolean; + hasPomXmlInPath: boolean; + hasPomXmlInRoot: boolean; + isBuildpackPathValid: boolean; + isTestRunnerPathValid: boolean; + isProcfileExists: boolean; + isEndpointYamlExists: boolean; } diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/messenger-rpc.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/messenger-rpc.types.ts index b0785c461ec..c3147d3a16f 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/messenger-rpc.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/messenger-rpc.types.ts @@ -76,6 +76,7 @@ export const CreateLocalEndpointsConfig: RequestType = { method: "createLocalProxyConfig" }; export const CreateLocalConnectionsConfig: RequestType = { method: "createLocalConnectionsConfig" }; export const DeleteLocalConnectionsConfig: RequestType = { method: "deleteLocalConnectionsConfig" }; +export const CloneRepositoryIntoCompDir: RequestType = { method: "cloneRepositoryIntoCompDir" }; const NotificationMethods = { onAuthStateChanged: "onAuthStateChanged", @@ -103,6 +104,23 @@ export interface OpenTestViewReq { endpoints: ComponentEP[]; } +export interface CloneRepositoryIntoCompDirReq { + cwd: string; + subpath: string; + org: Organization; + componentName: string; + repo: { + provider: string; + orgName: string; + orgHandler: string; + repo: string; + serverUrl?: string; + branch: string; + secretRef: string; + isBareRepo: boolean; + }; +} + export interface SubmitComponentCreateReq { org: Organization; project: Project; diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/store.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/store.types.ts index 612f7cb9e6f..3311ecb3bd9 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/store.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/store.types.ts @@ -20,7 +20,7 @@ import type { CommitHistory, ComponentKind, Environment, ExtensionName, Organiza export interface DataCacheState { orgs?: { - [orgHandle: string]: { + [orgRegionHandle: string]: { projects?: { [projectHandle: string]: { data?: Project; @@ -41,6 +41,7 @@ export interface DataCacheState { export interface AuthState { userInfo: UserInfo | null; + region: "US" | "EU"; } export interface WebviewState { diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/webview-prop.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/webview-prop.types.ts index 045cc9b28c7..b9bf3bf4ec3 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/webview-prop.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/webview-prop.types.ts @@ -30,6 +30,7 @@ export interface NewComponentWebviewProps { existingComponents: ComponentKind[]; initialValues?: { type?: string; subType?: string; buildPackLang?: string; name?: string }; extensionName?: string; + isNewCodeServerComp?: boolean; } export interface ComponentsDetailsWebviewProps { @@ -39,6 +40,7 @@ export interface ComponentsDetailsWebviewProps { component: ComponentKind; directoryFsPath?: string; initialEnvs: Environment[]; + isNewComponent?: boolean; } export interface ComponentsListActivityViewProps { diff --git a/workspaces/wso2-platform/wso2-platform-core/src/utils.ts b/workspaces/wso2-platform/wso2-platform-core/src/utils.ts index 9f8d10fa74f..4ac9bcfa06f 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/utils.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/utils.ts @@ -285,6 +285,23 @@ export const parseGitURL = (url?: string): null | [string, string, string] => { return [org, repoName, provider]; }; +export const buildGitURL = (org: string, repoName: string, provider: string, withDotGitSuffix?: boolean, serverUrl?: string): string | null => { + switch (provider) { + case GitProvider.GITHUB: + return `https://github.com/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}`; + case GitProvider.BITBUCKET: + return serverUrl + ? `${serverUrl}/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}` + : `https://bitbucket.org/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}`; + case GitProvider.GITLAB_SERVER: + return serverUrl + ? `${serverUrl}/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}` + : `https://gitlab.com/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}`; + default: + return null; + } +}; + export const getComponentKindRepoSource = (source: ComponentKindSource) => { return { repo: source?.github?.repository || source?.bitbucket?.repository || source?.gitlab?.repository || "", diff --git a/workspaces/wso2-platform/wso2-platform-extension/.env.example b/workspaces/wso2-platform/wso2-platform-extension/.env.example index 89dd6a06942..eff573cc0f2 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/.env.example +++ b/workspaces/wso2-platform/wso2-platform-extension/.env.example @@ -1,35 +1,2 @@ -#DEFAULT -PLATFORM_DEFAULT_GHAPP_INSTALL_URL=https://github.com/apps/wso2-cloud-app/installations/new -PLATFORM_DEFAULT_GHAPP_AUTH_URL=https://github.com/login/oauth/authorize -PLATFORM_DEFAULT_GHAPP_CLIENT_ID= -PLATFORM_DEFAULT_GHAPP_REDIRECT_URL=https://console.choreowww.dev/ghapp -PLATFORM_DEFAULT_GHAPP_DEVANT_REDIRECT_URL=https://console.devant.dev/ghapp -PLATFORM_DEFAULT_CHOREO_CONSOLE_BASE_URL=https://console.choreo.dev -PLATFORM_DEFAULT_BILLING_CONSOLE_BASE_URL=https://subscriptions.wso2.com -PLATFORM_DEFAULT_DEVANT_CONSOLE_BASE_URL=https://console.devant.dev -PLATFORM_DEFAULT_DEVANT_ASGARDEO_CLIENT_ID= - -# STAGE -PLATFORM_STAGE_GHAPP_INSTALL_URL=https://github.com/apps/wso2-cloud-app-stage/installations/new -PLATFORM_STAGE_GHAPP_AUTH_URL=https://github.com/login/oauth/authorize -PLATFORM_STAGE_GHAPP_CLIENT_ID= -PLATFORM_STAGE_GHAPP_REDIRECT_URL=https://console.st.choreo.dev/ghapp -PLATFORM_STAGE_GHAPP_DEVANT_REDIRECT_URL=https://preview-st.devant.dev/ghapp -PLATFORM_STAGE_CHOREO_CONSOLE_BASE_URL=https://console.st.choreo.dev -PLATFORM_STAGE_BILLING_CONSOLE_BASE_URL=https://subscriptions.st.wso2.com -PLATFORM_STAGE_DEVANT_CONSOLE_BASE_URL=https://preview-st.devant.dev -PLATFORM_STAGE_DEVANT_ASGARDEO_CLIENT_ID= - -# DEV -PLATFORM_DEV_GHAPP_INSTALL_URL=https://github.com/apps/wso2-cloud-app-dev/installations/new -PLATFORM_DEV_GHAPP_AUTH_URL=https://github.com/login/oauth/authorize -PLATFORM_DEV_GHAPP_CLIENT_ID= -PLATFORM_DEV_GHAPP_REDIRECT_URL=https://consolev2.preview-dv.choreo.dev/ghapp -PLATFORM_DEV_GHAPP_DEVANT_REDIRECT_URL=https://preview-dv.devant.dev/ghapp -PLATFORM_DEV_CHOREO_CONSOLE_BASE_URL=https://consolev2.preview-dv.choreo.dev -PLATFORM_DEV_BILLING_CONSOLE_BASE_URL=https://subscriptions.dv.wso2.com -PLATFORM_DEV_DEVANT_CONSOLE_BASE_URL=https://preview-dv.devant.dev -PLATFORM_DEV_DEVANT_ASGARDEO_CLIENT_ID= - # Common PLATFORM_CHOREO_CLI_RELEASES_BASE_URL=https://github.com/wso2/choreo-cli/releases/download/ \ No newline at end of file diff --git a/workspaces/wso2-platform/wso2-platform-extension/.gitignore b/workspaces/wso2-platform/wso2-platform-extension/.gitignore index 870f942784c..f81de3e3f57 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/.gitignore +++ b/workspaces/wso2-platform/wso2-platform-extension/.gitignore @@ -1,6 +1,7 @@ .nyc_output resources/jslibs/main.js resources/font-wso2-vscode +resources/choreo-cli/ !src/test/lib .vscode-test test-resources diff --git a/workspaces/wso2-platform/wso2-platform-extension/README.md b/workspaces/wso2-platform/wso2-platform-extension/README.md index 5909f5aa5c0..3562cf8cee6 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/README.md +++ b/workspaces/wso2-platform/wso2-platform-extension/README.md @@ -5,15 +5,14 @@ The WSO2 Platform VS Code extension enhances your local development experience w ## Getting Started -1. **Create an Account:** Sign up for an account on [Choreo](https://console.choreo.dev/) or [Devant](https://console.devant.dev/). -2. **Install the Extension:** +1. **Install the Extension:** * Open Visual Studio Code. * Navigate to the Extensions view by pressing `Ctrl+Shift+X` (or `Cmd+Shift+X` on macOS). * Search for "WSO2 Platform" and click "Install." -3. **Sign In to Choreo:** +2. **Sign In to Choreo/Devant:** * Open the Command Palette by pressing `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS). - * Type "WSO2: Sign In" and press Enter. Follow the on-screen prompts to authenticate with your Choreo account. -4. **Explore Functionality:** + * Type "WSO2: Sign In" and press Enter. Follow the on-screen prompts to authenticate with your Choreo/Devant account. +3. **Explore Functionality:** * Once signed in, open the Command Palette again (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS). * Type "WSO2:" to see a list of available commands and functionalities provided by the extension. @@ -23,7 +22,7 @@ Refer to the [Choreo documentation](https://wso2.com/choreo/docs/develop-compone ## Get Help -Feel free to create [GitHub issues](https://github.com/wso2/choreo-vscode/issues) or reach out to us on [Discord](https://discord.com/invite/wso2). +Feel free to create [GitHub issues](https://github.com/wso2/vscode-extensions/issues) or reach out to us on [Discord](https://discord.com/invite/wso2). ## License diff --git a/workspaces/wso2-platform/wso2-platform-extension/package.json b/workspaces/wso2-platform/wso2-platform-extension/package.json index 89cc0aa2a2d..6499ed7a84a 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/package.json +++ b/workspaces/wso2-platform/wso2-platform-extension/package.json @@ -3,8 +3,8 @@ "displayName": "WSO2 Platform", "description": "Manage WSO2 Choreo and Devant projects in VS Code.", "license": "Apache-2.0", - "version": "1.0.13-sts-12", - "cliVersion": "v1.2.182507031200", + "version": "1.0.17", + "cliVersion": "v1.2.212509091800", "publisher": "wso2", "bugs": { "url": "https://github.com/wso2/choreo-vscode/issues" @@ -18,7 +18,8 @@ "Other" ], "activationEvents": [ - "onStartupFinished" + "onStartupFinished", + "onLanguageModel:agent" ], "extensionDependencies": [ "redhat.vscode-yaml" @@ -123,7 +124,14 @@ "shortTitle": "Open component source", "category": "WSO2", "icon": "$(repo-clone)" + }, + { + "command": "wso2.wso2-platform.push-to-git", + "title": "Commit & push component to remote repo", + "category": "WSO2", + "icon": "$(repo-push)" } + ], "configuration": { "type": "object", @@ -160,20 +168,14 @@ "dev" ], "default": "prod", - "description": "The WSO2 Platform Enviornment to use", - "scope": "window" + "description": "The WSO2 Platform Environment to use", + "scope": "machine" }, "WSO2.WSO2-Platform.Advanced.RpcPath": { "type": "string", "default": "", "description": "The path to Choreo RPC server", "scope": "window" - }, - "WSO2.WSO2-Platform.Advanced.StsToken": { - "type": "string", - "default": "", - "description": "User STS token", - "scope": "window" } } }, @@ -185,7 +187,13 @@ "fontCharacter": "\\f147" } } - } + }, + "mcpServerDefinitionProviders": [ + { + "id": "choreo", + "label": "Choreo MCP Server" + } + ] }, "scripts": { "clean": "del-cli ./dist ./out ./resources/jslibs ./platform-*.vsix ./coverage ./.nyc_output", @@ -209,7 +217,8 @@ "package": "if [ $isPreRelease = true ]; then vsce package --no-dependencies --pre-release --baseImagesUrl https://github.com/wso2/choreo-vscode/raw/HEAD/; else vsce package --no-dependencies --baseImagesUrl https://github.com/wso2/choreo-vscode/raw/HEAD/; fi", "copyVSIX": "copyfiles *.vsix ./vsix", "copyVSIXToRoot": "copyfiles -f ./vsix/* ../../..", - "postbuild": "pnpm run package && pnpm run copyVSIX" + "postbuild": "pnpm run download-choreo-cli && pnpm run package && pnpm run copyVSIX", + "download-choreo-cli": "node scripts/download-choreo-cli.js" }, "devDependencies": { "@playwright/test": "1.55.1", @@ -221,7 +230,7 @@ "@types/which": "^3.0.4", "@vscode/vsce": "^3.7.0", "@wso2/playwright-vscode-tester": "workspace:*", - "axios": "^1.12.0", + "axios": "^1.9.0", "copyfiles": "^2.4.1", "del-cli": "^6.0.0", "mocha": "^11.5.0", @@ -249,6 +258,7 @@ "file-type": "^18.2.1", "js-yaml": "^4.1.1", "yaml": "^2.8.0", + "@iarna/toml": "^2.2.5", "jschardet": "^3.1.4", "vscode-messenger": "^0.5.1", "vscode-messenger-common": "^0.5.1", diff --git a/workspaces/wso2-platform/wso2-platform-extension/scripts/download-choreo-cli.js b/workspaces/wso2-platform/wso2-platform-extension/scripts/download-choreo-cli.js new file mode 100644 index 00000000000..729432634ca --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/scripts/download-choreo-cli.js @@ -0,0 +1,432 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const { execSync } = require('child_process'); +const os = require('os'); + +const PROJECT_ROOT = path.join(__dirname, '..'); +const REPO_ROOT = path.join(PROJECT_ROOT, '..', '..', '..'); +// Primary location used by the extension +const CLI_RESOURCES_DIR = path.join(PROJECT_ROOT, 'resources', 'choreo-cli'); +// Persistent cache that survives 'rush purge' +const CLI_CACHE_DIR = path.join(REPO_ROOT, 'common', 'temp', 'choreo-cli'); +const PACKAGE_JSON_PATH = path.join(PROJECT_ROOT, 'package.json'); +const GITHUB_REPO_URL = 'https://api.github.com/repos/wso2/choreo-cli'; + +// Platform-specific file patterns for CLI downloads +const CLI_ASSET_PATTERNS = [ + 'choreo-cli-{version}-darwin-amd64.zip', + 'choreo-cli-{version}-darwin-arm64.zip', + 'choreo-cli-{version}-linux-amd64.tar.gz', + 'choreo-cli-{version}-linux-arm64.tar.gz', + 'choreo-cli-{version}-windows-amd64.zip' +]; + +// ============================================================================ +// Version Management +// ============================================================================ + +function getCliVersion() { + const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')); + const cliVersion = packageJson.cliVersion; + + if (!cliVersion) { + throw new Error('cliVersion not found in package.json'); + } + + console.log(`Choreo CLI version for WSO2 platform extension: ${cliVersion}`); + return cliVersion; +} + +function getCombinedZipFileName(version) { + return `choreo-cli-${version}.zip`; +} + +function getCombinedZipPath(version, baseDir) { + return path.join(baseDir, getCombinedZipFileName(version)); +} + +function getResourcesZipPath(version) { + return getCombinedZipPath(version, CLI_RESOURCES_DIR); +} + +function getCacheZipPath(version) { + return getCombinedZipPath(version, CLI_CACHE_DIR); +} + +function getExpectedAssetNames(version) { + return CLI_ASSET_PATTERNS.map(pattern => pattern.replace('{version}', version)); +} + +// ============================================================================ +// File System Utilities +// ============================================================================ + +function ensureDirectoryExists(dirPath) { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +} + +function getFileSize(filePath) { + try { + const stats = fs.statSync(filePath); + return stats.size; + } catch (error) { + return 'unknown'; + } +} + +function deleteFile(filePath) { + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (error) { + console.warn(`Failed to delete file ${filePath}:`, error.message); + } +} + +function createTempDirectory(prefix) { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +function deleteDirectory(dirPath) { + try { + if (fs.existsSync(dirPath)) { + fs.rmSync(dirPath, { recursive: true, force: true }); + } + } catch (error) { + console.warn(`Failed to delete directory ${dirPath}:`, error.message); + } +} + +// ============================================================================ +// CLI File Validation & Cache Management +// ============================================================================ + +function checkExistingCLI(version) { + const resourcesZipPath = getResourcesZipPath(version); + const cacheZipPath = getCacheZipPath(version); + + const resourcesExists = fs.existsSync(resourcesZipPath); + const cacheExists = fs.existsSync(cacheZipPath); + + // Both exist - we're good + if (resourcesExists && cacheExists) { + console.log(`✓ Choreo CLI for version ${version} exists`); + return true; + } + + // Resources exists but cache doesn't (e.g., after rush purge) + if (resourcesExists && !cacheExists) { + console.log(`✓ CLI zip exists in resources/choreo-cli`); + console.log(`Restoring cache (common/temp) from resources...`); + ensureDirectoryExists(CLI_CACHE_DIR); + fs.copyFileSync(resourcesZipPath, cacheZipPath); + console.log(`✓ Restored cache from resources/choreo-cli`); + return true; + } + + // Cache exists but resources doesn't + if (!resourcesExists && cacheExists) { + console.log(`Found CLI zip in cache (common/temp), copying to resources/choreo-cli...`); + ensureDirectoryExists(CLI_RESOURCES_DIR); + fs.copyFileSync(cacheZipPath, resourcesZipPath); + console.log(`✓ Copied CLI zip to resources/choreo-cli`); + return true; + } + + // Neither exists + console.log(`CLI zip for version ${version} not found in resources or cache`); + return false; +} + +function cleanupOldFilesInDirectory(directory, currentVersion) { + if (!fs.existsSync(directory)) { + return; + } + + const currentZipName = getCombinedZipFileName(currentVersion); + const entries = fs.readdirSync(directory); + + for (const entry of entries) { + if (entry === currentZipName) { + continue; // Skip the current version + } + + const entryPath = path.join(directory, entry); + const stats = fs.statSync(entryPath); + + console.log(`Removing old ${stats.isDirectory() ? 'directory' : 'file'}: ${entry} from ${path.basename(directory)}`); + + if (stats.isDirectory()) { + fs.rmSync(entryPath, { recursive: true, force: true }); + } else { + fs.unlinkSync(entryPath); + } + } +} + +function cleanupOldFiles(currentVersion) { + // Clean up old files from both locations + cleanupOldFilesInDirectory(CLI_RESOURCES_DIR, currentVersion); + cleanupOldFilesInDirectory(CLI_CACHE_DIR, currentVersion); +} + +// ============================================================================ +// GitHub API Utilities +// ============================================================================ + +function getAuthHeaders() { + const token = process.env.CHOREO_BOT_TOKEN || process.env.GITHUB_TOKEN; + return token ? { 'Authorization': `Bearer ${token}` } : {}; +} + +function logRateLimitError(headers) { + console.error('HTTP 403: Forbidden. This may be due to GitHub API rate limiting.'); + console.error('Set GITHUB_TOKEN environment variable with a personal access token to increase rate limits.'); + + if (headers['x-ratelimit-limit']) { + console.error(`Rate limit: ${headers['x-ratelimit-remaining']}/${headers['x-ratelimit-limit']}`); + const resetTime = new Date(headers['x-ratelimit-reset'] * 1000).toLocaleString(); + console.error(`Rate limit resets at: ${resetTime}`); + } +} + +function httpsRequest(url, options = {}) { + return new Promise((resolve, reject) => { + const req = https.request(url, { + ...options, + headers: { + 'User-Agent': 'Choreo-CLI-Downloader', + 'Accept': 'application/vnd.github.v3+json', + ...getAuthHeaders(), + ...options.headers + } + }, (res) => { + if (res.statusCode === 403) { + logRateLimitError(res.headers); + } + + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve({ data, statusCode: res.statusCode, headers: res.headers }); + } else { + reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); + } + }); + }); + + req.on('error', reject); + req.end(); + }); +} + +async function getReleaseByTag(tag) { + console.log(`Fetching release information for tag: ${tag}...`); + const response = await httpsRequest(`${GITHUB_REPO_URL}/releases/tags/${tag}`); + return JSON.parse(response.data); +} + +// ============================================================================ +// File Download +// ============================================================================ + +function isRedirect(statusCode) { + return statusCode >= 300 && statusCode < 400; +} + +function isSuccess(statusCode) { + return statusCode >= 200 && statusCode < 300; +} + +function downloadFile(url, outputPath, maxRedirects = 5) { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(outputPath); + + const cleanupAndReject = (error) => { + file.close(); + deleteFile(outputPath); + reject(error); + }; + + const makeRequest = (requestUrl, redirectCount = 0) => { + const req = https.request(requestUrl, { + headers: { + 'User-Agent': 'Choreo-CLI-Downloader', + 'Accept': 'application/octet-stream' + } + }, (res) => { + if (isRedirect(res.statusCode) && res.headers.location) { + if (redirectCount >= maxRedirects) { + cleanupAndReject(new Error(`Too many redirects (${redirectCount})`)); + return; + } + makeRequest(res.headers.location, redirectCount + 1); + return; + } + + if (isSuccess(res.statusCode)) { + res.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + file.on('error', cleanupAndReject); + } else { + cleanupAndReject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); + } + }); + + req.on('error', cleanupAndReject); + req.end(); + }; + + makeRequest(url); + }); +} + +async function downloadAsset(asset, tempDir) { + const finalPath = path.join(tempDir, asset.name); + const tempPath = `${finalPath}.tmp`; + const downloadUrl = `${GITHUB_REPO_URL}/releases/assets/${asset.id}`; + + console.log(`Downloading ${asset.name}...`); + + try { + await downloadFile(downloadUrl, tempPath); + fs.renameSync(tempPath, finalPath); // Atomic operation + + const fileSize = getFileSize(finalPath); + console.log(`✓ Downloaded ${asset.name} (${fileSize} bytes)`); + } catch (error) { + deleteFile(tempPath); + console.error(`✗ Failed to download ${asset.name}: ${error.message}`); + throw error; + } +} + +function getZipCommand(files, outputZipPath, tempDir) { + const isWindows = os.platform() === 'win32'; + + if (isWindows) { + const filesArg = files.map(f => `'${f}'`).join(','); + return { + command: `powershell.exe -Command "Compress-Archive -Path ${filesArg} -DestinationPath '${outputZipPath}' -Force"`, + cwd: tempDir + }; + } + + // macOS/Linux + const filesArg = files.map(f => `'${f}'`).join(' '); + return { + command: `zip -q '${outputZipPath}' ${filesArg}`, + cwd: tempDir + }; +} + +function createCombinedZip(tempDir, outputZipPath) { + console.log('\nCreating Choreo CLI zip file...'); + const files = fs.readdirSync(tempDir).filter(f => !f.startsWith('.')); + const { command, cwd } = getZipCommand(files, outputZipPath, tempDir); + + try { + execSync(command, { cwd, stdio: 'inherit' }); + + const zipSize = getFileSize(outputZipPath); + const relativePath = path.relative(PROJECT_ROOT, outputZipPath); + console.log(`✓ Created Choreo CLI combined zip: ${relativePath} (${zipSize} bytes)`); + } catch (error) { + throw new Error(`Failed to create zip file: ${error.message}`); + } +} + +// ============================================================================ +// Main Download Logic +// ============================================================================ + +async function downloadAllAssets(releaseData, expectedAssetNames, tempDir) { + const downloadPromises = expectedAssetNames.map(assetName => { + const asset = releaseData.assets?.find(a => a.name === assetName); + + if (!asset) { + console.warn(`Warning: Choreo CLI Asset not found: ${assetName}`); + return Promise.resolve(); + } + + return downloadAsset(asset, tempDir); + }); + + await Promise.all(downloadPromises); +} + +async function downloadAndCombineCLI(version) { + const tempDir = createTempDirectory(`choreo-cli-${version}-`); + + try { + // Ensure both directories exist + ensureDirectoryExists(CLI_RESOURCES_DIR); + ensureDirectoryExists(CLI_CACHE_DIR); + + const releaseData = await getReleaseByTag(version); + const expectedAssetNames = getExpectedAssetNames(version); + + await downloadAllAssets(releaseData, expectedAssetNames, tempDir); + + // Create zip in cache directory first + const cacheZipPath = getCacheZipPath(version); + createCombinedZip(tempDir, cacheZipPath); + + // Copy to resources directory + const resourcesZipPath = getResourcesZipPath(version); + console.log('Copying CLI zip to resources/choreo-cli...'); + fs.copyFileSync(cacheZipPath, resourcesZipPath); + console.log('✓ Copied CLI zip to resources/choreo-cli'); + + } finally { + console.log('Cleaning up temporary directory...'); + deleteDirectory(tempDir); + } +} + +// ============================================================================ +// Main Entry Point +// ============================================================================ + +async function main() { + try { + const cliVersion = getCliVersion(); + + // Check if combined CLI zip already exists + if (checkExistingCLI(cliVersion)) { + console.log('✓ Combined CLI zip is already present'); + process.exit(0); + } + + console.log(`\nDownloading Choreo CLI version ${cliVersion}...`); + + // Clean up old files before downloading new one + cleanupOldFiles(cliVersion); + + // Download all CLI assets and combine into single zip + await downloadAndCombineCLI(cliVersion); + + console.log(`\n✓ Successfully created Choreo CLI zip for version ${cliVersion}`); + + } catch (error) { + console.error('\n✗ Error:', error.message); + process.exit(1); + } +} + +// Run if executed directly +if (require.main === module) { + main(); +} + +module.exports = { main, checkExistingCLI }; \ No newline at end of file diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts b/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts index 82ecd87c2a7..a3a2ae96902 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts @@ -16,24 +16,37 @@ * under the License. */ -import type { ComponentKind, IWso2PlatformExtensionAPI, openClonedDirReq } from "@wso2/wso2-platform-core"; +import type { AuthState, ComponentKind, ContextItemEnriched, ContextStoreComponentState, IWso2PlatformExtensionAPI, openClonedDirReq } from "@wso2/wso2-platform-core"; import { ext } from "./extensionVariables"; import { hasDirtyRepo } from "./git/util"; -import { authStore } from "./stores/auth-store"; import { contextStore } from "./stores/context-store"; import { webviewStateStore } from "./stores/webview-state-store"; import { openClonedDir } from "./uri-handlers"; import { isSamePath } from "./utils"; + export class PlatformExtensionApi implements IWso2PlatformExtensionAPI { - public isLoggedIn = () => !!authStore.getState().state?.userInfo; - public getDirectoryComponents = (fsPath: string) => - (contextStore - .getState() - .state?.components?.filter((item) => isSamePath(item?.componentFsPath, fsPath)) + private getComponentsOfDir = (fsPath: string, components?: ContextStoreComponentState[]) => { + return (components?.filter((item) => isSamePath(item?.componentFsPath, fsPath)) ?.map((item) => item?.component) - ?.filter((item) => !!item) as ComponentKind[]) ?? []; + ?.filter((item) => !!item) as ComponentKind[]) ?? [] + } + + public getAuthState = () => ext.authProvider?.getState().state ?? { userInfo: null, region: "US" as const }; + public isLoggedIn = () => !!ext.authProvider?.getState().state?.userInfo; + public getDirectoryComponents = (fsPath: string) => this.getComponentsOfDir(fsPath, contextStore.getState().state?.components); public localRepoHasChanges = (fsPath: string) => hasDirtyRepo(fsPath, ext.context, ["context.yaml"]); public getWebviewStateStore = () => webviewStateStore.getState().state; public getContextStateStore = () => contextStore.getState().state; public openClonedDir = (params: openClonedDirReq) => openClonedDir(params); + public getStsToken = () => ext.clients.rpcClient.getStsToken(); + public getSelectedContext = () => contextStore.getState().state?.selected || null; + public getDevantConsoleUrl = async() => (await ext.clients.rpcClient.getConfigFromCli()).devantConsoleUrl; + + // Auth state subscriptions + public subscribeAuthState = (callback: (state: AuthState)=>void) => ext.authProvider?.subscribe((state)=>callback(state.state)) ?? (() => {}); + public subscribeIsLoggedIn = (callback: (isLoggedIn: boolean)=>void) => ext.authProvider?.subscribe((state)=>callback(!!state.state?.userInfo)) ?? (() => {}); + + // Context state subscriptions + public subscribeContextState = (callback: (state: ContextItemEnriched | undefined)=>void) => contextStore.subscribe((state)=>callback(state.state?.selected)); + public subscribeDirComponents = (fsPath: string, callback: (comps: ComponentKind[])=>void) => contextStore.subscribe((state)=>callback(this.getComponentsOfDir(fsPath, state.state.components))); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/auth/wso2-auth-provider.ts b/workspaces/wso2-platform/wso2-platform-extension/src/auth/wso2-auth-provider.ts new file mode 100644 index 00000000000..bad8b1f7a26 --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/src/auth/wso2-auth-provider.ts @@ -0,0 +1,389 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { AuthState, UserInfo } from "@wso2/wso2-platform-core"; +import { + type AuthenticationProvider, + type AuthenticationProviderAuthenticationSessionsChangeEvent, + type AuthenticationProviderSessionOptions, + type AuthenticationSession, + Disposable, + EventEmitter, + type SecretStorage, + window, +} from "vscode"; +import { ext } from "../extensionVariables"; +import { getLogger } from "../logger/logger"; +import { contextStore } from "../stores/context-store"; +import { dataCacheStore } from "../stores/data-cache-store"; + +export const WSO2_AUTH_PROVIDER_ID = "wso2-platform"; +const WSO2_SESSIONS_SECRET_KEY = `${WSO2_AUTH_PROVIDER_ID}.sessions`; + +interface SessionData { + id: string; + accessToken: string; + account: { + id: string; + label: string; + }; + scopes: string[]; + userInfo: UserInfo; + region: "US" | "EU"; +} + +export class WSO2AuthenticationProvider implements AuthenticationProvider, Disposable { + private _sessionChangeEmitter = new EventEmitter(); + private _stateChangeEmitter = new EventEmitter<{ state: AuthState }>(); + private _disposable: Disposable; + private _state: AuthState = { userInfo: null, region: "US" }; + + constructor(private readonly secretStorage: SecretStorage) { + this._disposable = Disposable.from(this._sessionChangeEmitter, this._stateChangeEmitter); + } + + get onDidChangeSessions() { + return this._sessionChangeEmitter.event; + } + + /** + * Subscribe to auth state changes + */ + public subscribe(callback: (store: { state: AuthState }) => void): () => void { + const disposable = this._stateChangeEmitter.event(callback); + // Call immediately with current state + callback({ state: this._state }); + return () => disposable.dispose(); + } + + /** + * Get the current state + */ + public getState() { + return { + state: this._state, + resetState: this.resetState.bind(this), + loginSuccess: this.loginSuccess.bind(this), + logout: this.logout.bind(this), + initAuth: this.initAuth.bind(this), + }; + } + + /** + * Get current auth state + */ + get state(): AuthState { + return this._state; + } + + /** + * Get the existing sessions + */ + public async getSessions(scopes: readonly string[] | undefined, options: AuthenticationProviderSessionOptions): Promise { + const allSessions = await this.readSessions(); + + if (scopes && scopes.length > 0) { + const sessions = allSessions.filter((session) => scopes.every((scope) => session.scopes.includes(scope))); + return sessions; + } + + return allSessions; + } + + /** + * Create a new auth session - NOT USED (auth is handled by RPC) + * This is required by the AuthenticationProvider interface but not called directly + */ + public async createSession(scopes: string[]): Promise { + throw new Error("Direct session creation not supported. Use RPC authentication flow."); + } + + /** + * Reset state to initial values + */ + public resetState() { + this._state = { userInfo: null, region: "US" }; + this._stateChangeEmitter.fire({ state: this._state }); + } + + /** + * Handle successful login - updates state and stores session + */ + public async loginSuccess(userInfo: UserInfo, region: "US" | "EU") { + // Update local state + this._state = { userInfo, region }; + + // Update related stores + dataCacheStore.getState().setOrgs(userInfo.organizations); + contextStore.getState().refreshState(); + + // Store session in secure storage + await this.storeSession(userInfo, region); + + // Notify subscribers + this._stateChangeEmitter.fire({ state: this._state }); + } + + /** + * Handle logout - signs out from RPC and clears all state + */ + public async logout(silent = false, skipClearSessions = false) { + getLogger().debug("Signing out from WSO2 Platform"); + + // Call RPC signOut first + try { + await ext.clients.rpcClient.signOut(); + } catch (error) { + getLogger().error("Error during RPC signOut", error); + } + + // Clear VS Code session storage (unless already cleared by removeSession) + if (!skipClearSessions) { + try { + await this.clearSessions(); + } catch (error) { + getLogger().error("Error clearing sessions", error); + } + } + + // Clear local state + this.resetState(); + + if (!silent) { + window.showInformationMessage("Successfully signed out from WSO2 Platform!"); + } + } + + /** + * Initialize authentication on startup + */ + public async initAuth() { + try { + const userInfo = await ext.clients.rpcClient.getUserInfo(); + if (userInfo) { + const region = await ext.clients.rpcClient.getCurrentRegion(); + await this.loginSuccess(userInfo, region); + const contextStoreState = contextStore.getState().state; + if (contextStoreState.selected?.org) { + ext?.clients?.rpcClient?.changeOrgContext(contextStoreState.selected?.org?.id?.toString()); + } + } else { + await this.logout(true); + } + } catch (err) { + getLogger().error("Error during auth initialization", err); + await this.logout(true); + } + } + + /** + * Store or update a session with user info and region + * Called internally after successful RPC authentication + */ + private async storeSession(userInfo: UserInfo, region: "US" | "EU"): Promise { + // Remove any existing sessions first (single account support) + const existingSessions = await this.readSessions(); + const removedSessions = [...existingSessions]; + + const sessionId = this.generateSessionId(); + const sessionData: SessionData = { + id: sessionId, + accessToken: "rpc-authenticated", // Placeholder since RPC handles auth + account: { + label: userInfo.displayName || userInfo.userEmail, + id: userInfo.userId, + }, + scopes: [], + userInfo, + region, + }; + + const session: AuthenticationSession = { + id: sessionData.id, + accessToken: sessionData.accessToken, + account: sessionData.account, + scopes: sessionData.scopes, + }; + + await this.storeSessions([session], sessionData); + + this._sessionChangeEmitter.fire({ + added: [session], + removed: removedSessions, + changed: [] + }); + + return session; + } + + /** + * Remove an existing session + * This is called when user signs out from VS Code's Accounts menu + */ + public async removeSession(sessionId: string): Promise { + const allSessions = await this.readSessions(); + const sessionIdx = allSessions.findIndex((s) => s.id === sessionId); + const session = allSessions[sessionIdx]; + if (!session) { + return; + } + + allSessions.splice(sessionIdx, 1); + await this.storeSessions(allSessions); + this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] }); + + // Trigger full logout flow (skipClearSessions=true to avoid loop) + await this.logout(false, true); + } + + /** + * Remove all sessions + */ + public async clearSessions(): Promise { + const allSessions = await this.readSessions(); + if (allSessions.length === 0) { + return; + } + + await this.secretStorage.delete(WSO2_SESSIONS_SECRET_KEY); + + this._sessionChangeEmitter.fire({ added: [], removed: allSessions, changed: [] }); + } + + /** + * Get session data including userInfo and region + */ + public async getSessionData(sessionId?: string): Promise { + const sessions = await this.readSessionsData(); + if (sessionId) { + return sessions.find((s) => s.id === sessionId); + } + // Return the first session if no ID is provided (single account support) + return sessions[0]; + } + + /** + * Dispose the provider + */ + public async dispose() { + this._disposable.dispose(); + } + + /** + * Get the user info from stored session (for backward compatibility) + */ + public getUserInfo(): UserInfo | null { + return this._state.userInfo; + } + + /** + * Get the region from stored session (for backward compatibility) + */ + public getRegion(): "US" | "EU" { + return this._state.region; + } + + /** + * Read sessions from secret storage + */ + private async readSessions(): Promise { + try { + const sessionsJson = await this.secretStorage.get(WSO2_SESSIONS_SECRET_KEY); + if (!sessionsJson) { + return []; + } + + const sessionData: SessionData[] = JSON.parse(sessionsJson); + return sessionData.map((data) => ({ + id: data.id, + accessToken: data.accessToken, + account: data.account, + scopes: data.scopes, + })); + } catch (e) { + getLogger().error("Error reading sessions", e); + return []; + } + } + + /** + * Store sessions to secret storage + */ + private async storeSessions(sessions: readonly AuthenticationSession[], newSessionData?: SessionData): Promise { + try { + const existingSessions = await this.readSessionsData(); + let updatedSessions: SessionData[]; + + if (newSessionData) { + // Add or update session + const existingIndex = existingSessions.findIndex((s) => s.id === newSessionData.id); + if (existingIndex >= 0) { + updatedSessions = [...existingSessions]; + updatedSessions[existingIndex] = newSessionData; + } else { + updatedSessions = [...existingSessions, newSessionData]; + } + } else { + // Filter out removed sessions + const sessionIds = sessions.map((s) => s.id); + updatedSessions = existingSessions.filter((s) => sessionIds.includes(s.id)); + } + + await this.secretStorage.store(WSO2_SESSIONS_SECRET_KEY, JSON.stringify(updatedSessions)); + } catch (e) { + getLogger().error("Error storing sessions", e); + } + } + + /** + * Read full session data including userInfo and region + */ + private async readSessionsData(): Promise { + try { + const sessionsJson = await this.secretStorage.get(WSO2_SESSIONS_SECRET_KEY); + if (!sessionsJson) { + return []; + } + + return JSON.parse(sessionsJson); + } catch (e) { + getLogger().error("Error reading session data", e); + return []; + } + } + + /** + * Generate a session ID + */ + private generateSessionId(): string { + return `wso2-${Date.now()}-${Math.random().toString(36).substring(2)}`; + } +} + +/** + * Helper function to wait for user login + */ +export const waitForLogin = async (): Promise => { + return new Promise((resolve) => { + ext.authProvider?.subscribe(({ state }) => { + if (state.userInfo) { + resolve(state.userInfo); + } + }); + }); +}; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/activate.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/activate.ts index 584db3f4117..cbcaf6d29e8 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/activate.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/activate.ts @@ -20,11 +20,12 @@ import { exec } from "child_process"; import * as fs from "fs"; import { downloadCLI, getChoreoExecPath, getCliVersion } from "./cli-install"; import { RPCClient } from "./client"; +import { getLogger } from "../logger/logger"; function isChoreoCliInstalled(): Promise { return new Promise((resolve) => { const rpcPath = getChoreoExecPath(); - console.log("RPC path: ", rpcPath); + getLogger().info(`RPC path: ${rpcPath}`); if (!fs.existsSync(rpcPath)) { return resolve(false); @@ -42,9 +43,9 @@ function isChoreoCliInstalled(): Promise { const timeout = setTimeout(() => { process.kill(); // Kill the process if it exceeds 5 seconds - console.error("Timeout: Process took too long"); + getLogger().error("Timeout: Process took too long"); fs.rmSync(rpcPath); - console.error("Delete RPC path and try again", rpcPath); + getLogger().error(`Delete RPC path and try again ${rpcPath}`); resolve(false); }, 5000); @@ -55,7 +56,7 @@ function isChoreoCliInstalled(): Promise { export async function initRPCServer() { const installed = await isChoreoCliInstalled(); if (!installed) { - console.log(`WSO2 Platform RPC version ${getCliVersion()} not installed`); + getLogger().trace(`WSO2 Platform RPC version ${getCliVersion()} not installed`); await downloadCLI(); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/cli-install.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/cli-install.ts index 1ba448c6678..76afc0bc65b 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/cli-install.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/cli-install.ts @@ -20,10 +20,9 @@ import { execSync } from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import axios from "axios"; -import { ProgressLocation, window, workspace } from "vscode"; -import { choreoEnvConfig } from "../config"; +import { workspace } from "vscode"; import { ext } from "../extensionVariables"; +import { getLogger } from "../logger/logger"; export const getCliVersion = (): string => { const packageJson = JSON.parse(fs.readFileSync(path.join(ext.context.extensionPath, "package.json"), "utf8")); @@ -62,106 +61,118 @@ export const downloadCLI = async () => { const CHOREO_BIN_DIR = getChoreoBinPath(); const CHOREO_CLI_EXEC = getChoreoExecPath(); const CLI_VERSION = getCliVersion(); - const CHOREO_TMP_DIR = await fs.promises.mkdtemp(path.join(os.tmpdir(), `choreo-cli-rpc-${CLI_VERSION}`)); - - fs.mkdirSync(CHOREO_BIN_DIR, { recursive: true }); + + // Path to the combined zip file in resources + const COMBINED_ZIP_PATH = path.join(ext.context.extensionPath, "resources", "choreo-cli", `choreo-cli-${CLI_VERSION}.zip`); + + if (!fs.existsSync(COMBINED_ZIP_PATH)) { + throw new Error(`Combined CLI zip not found at: ${COMBINED_ZIP_PATH}\nPlease run 'pnpm run download-choreo-cli' to download the CLI.`); + } - const FILE_NAME = `choreo-cli-${CLI_VERSION}-${OS === "win32" ? "windows" : OS}-${ARCH}`; - let FILE_TYPE = ""; + getLogger().trace(`Extracting Choreo CLI from: ${COMBINED_ZIP_PATH}`); - if (OS === "linux") { - FILE_TYPE = ".tar.gz"; - } else if (OS === "darwin") { - FILE_TYPE = ".zip"; - } else if (OS === "win32") { - FILE_TYPE = ".zip"; - } else { - throw new Error(`Unsupported OS: ${OS}`); - } - const CHOREO_TMP_FILE_DEST = path.join(CHOREO_TMP_DIR, `${FILE_NAME}${FILE_TYPE}`); + const CHOREO_TMP_DIR = await fs.promises.mkdtemp(path.join(os.tmpdir(), `choreo-cli-rpc-${CLI_VERSION}-`)); - const INSTALLER_URL = `${choreoEnvConfig.getCliInstallUrl()}${CLI_VERSION}/${FILE_NAME}${FILE_TYPE}`; + try { + fs.mkdirSync(CHOREO_BIN_DIR, { recursive: true }); - console.log(`WSO2 Platform RPC download URL: ${INSTALLER_URL}`); + // Extract the combined zip to temp directory + getLogger().trace(`Extracting combined zip to temp dir: ${CHOREO_TMP_DIR}`); + try { + if (OS === "win32") { + execSync(`powershell.exe -Command "Expand-Archive -Path '${COMBINED_ZIP_PATH}' -DestinationPath '${CHOREO_TMP_DIR}' -Force"`); + } else { + execSync(`unzip -q '${COMBINED_ZIP_PATH}' -d '${CHOREO_TMP_DIR}'`); + } + } catch (error) { + throw new Error(`Failed to extract combined zip: ${error instanceof Error ? error.message : String(error)}`); + } - await downloadFile(INSTALLER_URL, CHOREO_TMP_FILE_DEST); + // Determine the specific file to extract based on OS and architecture + const FILE_NAME = `choreo-cli-${CLI_VERSION}-${OS === "win32" ? "windows" : OS}-${ARCH}`; + let FILE_TYPE = ""; - console.log(`Extracting archive into temp dir: ${CHOREO_TMP_DIR}`); - if (FILE_TYPE === ".tar.gz") { - execSync(`tar -xzf ${CHOREO_TMP_FILE_DEST} -C ${CHOREO_TMP_DIR}`); - } else if (FILE_TYPE === ".zip") { - if (OS === "darwin") { - execSync(`unzip -q ${CHOREO_TMP_FILE_DEST} -d ${CHOREO_TMP_DIR}`); + if (OS === "linux") { + FILE_TYPE = ".tar.gz"; + } else if (OS === "darwin") { + FILE_TYPE = ".zip"; } else if (OS === "win32") { - execSync(`powershell.exe -Command "Expand-Archive '${CHOREO_TMP_FILE_DEST}' -DestinationPath '${CHOREO_TMP_DIR}' -Force"`); + FILE_TYPE = ".zip"; + } else { + throw new Error(`Unsupported OS: ${OS}`); + } + + const PLATFORM_ARCHIVE = path.join(CHOREO_TMP_DIR, `${FILE_NAME}${FILE_TYPE}`); + + if (!fs.existsSync(PLATFORM_ARCHIVE)) { + throw new Error(`Platform-specific archive not found: ${FILE_NAME}${FILE_TYPE}`); + } + + getLogger().trace(`Extracting platform-specific archive: ${FILE_NAME}${FILE_TYPE}`); + const PLATFORM_TMP_DIR = path.join(CHOREO_TMP_DIR, "platform-extract"); + fs.mkdirSync(PLATFORM_TMP_DIR, { recursive: true }); + + // Extract the platform-specific archive + try { + if (FILE_TYPE === ".tar.gz") { + execSync(`tar -xzf '${PLATFORM_ARCHIVE}' -C '${PLATFORM_TMP_DIR}'`); + } else if (FILE_TYPE === ".zip") { + if (OS === "darwin") { + execSync(`unzip -q '${PLATFORM_ARCHIVE}' -d '${PLATFORM_TMP_DIR}'`); + } else if (OS === "win32") { + execSync(`powershell.exe -Command "Expand-Archive -Path '${PLATFORM_ARCHIVE}' -DestinationPath '${PLATFORM_TMP_DIR}' -Force"`); + } + } + } catch (error) { + throw new Error(`Failed to extract platform-specific archive: ${error instanceof Error ? error.message : String(error)}`); + } + + // Copy the executable to the bin directory + const executableName = OS === "win32" ? "choreo.exe" : "choreo"; + const extractedExecutable = path.join(PLATFORM_TMP_DIR, executableName); + + if (!fs.existsSync(extractedExecutable)) { + throw new Error(`Executable not found after extraction: ${extractedExecutable}`); } - } - console.log(`Moving executable to ${CHOREO_BIN_DIR}`); - await fs.promises.copyFile(`${CHOREO_TMP_DIR}/${OS === "win32" ? "choreo.exe" : "choreo"}`, CHOREO_CLI_EXEC); - await fs.promises.rm(`${CHOREO_TMP_DIR}/${OS === "win32" ? "choreo.exe" : "choreo"}`); + getLogger().trace(`Copying executable to ${CHOREO_BIN_DIR}`); + try { + await fs.promises.copyFile(extractedExecutable, CHOREO_CLI_EXEC); + } catch (error) { + throw new Error(`Failed to copy executable: ${error instanceof Error ? error.message : String(error)}`); + } - console.log("Cleaning up..."); - await fs.promises.rm(CHOREO_TMP_DIR, { recursive: true }); + // Set executable permissions on Unix systems + if (OS !== "win32") { + try { + await fs.promises.chmod(CHOREO_CLI_EXEC, 0o755); + } catch (error) { + throw new Error(`Failed to set executable permissions: ${error instanceof Error ? error.message : String(error)}`); + } + } - process.chdir(CHOREO_BIN_DIR); - if (OS !== "win32") { - await fs.promises.chmod(CHOREO_CLI_EXEC, 0o755); + getLogger().trace("WSO2 Platform RPC server was installed successfully 🎉"); + } catch (error) { + // Clean up temp directory on error and re-throw + getLogger().error("Error during CLI installation:", error); + await fs.promises.rm(CHOREO_TMP_DIR, { recursive: true, force: true }).catch(() => { + // Ignore cleanup errors + }); + throw error; } - console.log("WSO2 Platform RPC server was installed successfully 🎉"); + // Clean up temp directory on success + getLogger().trace("Cleaning up temporary files..."); + await fs.promises.rm(CHOREO_TMP_DIR, { recursive: true, force: true }); }; -async function downloadFile(url: string, dest: string) { - const controller = new AbortController(); - const response = await axios({ url, method: "GET", responseType: "stream", signal: controller.signal }); - await window.withProgress( - { - title: "Initializing WSO2 Platform extension", - location: ProgressLocation.Notification, - cancellable: true, - }, - async (progress, cancellationToken) => { - return new Promise((resolve, reject) => { - const writer = fs.createWriteStream(dest); - const totalSize = Number.parseInt(response.headers["content-length"], 10); - let downloadedSize = 0; - let previousPercentage = 0; - - response.data.on("data", (chunk: string) => { - downloadedSize += chunk.length; - - const progressPercentage = Math.round((downloadedSize / totalSize) * 100); - if (progressPercentage !== previousPercentage) { - progress.report({ - increment: progressPercentage - previousPercentage, - message: `${progressPercentage}%`, - }); - previousPercentage = progressPercentage; - } - }); - - response.data.pipe(writer); - - cancellationToken.onCancellationRequested(() => { - controller.abort(); - reject(); - }); - - writer.on("finish", resolve); - writer.on("error", reject); - }); - }, - ); -} - function getArchitecture() { const ARCH = os.arch(); switch (ARCH) { case "x64": return "amd64"; - case "x32": - return "386"; + // case "x32": + // return "386"; case "arm64": case "aarch64": return "arm64"; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts index aaa642d32b1..2391ba75ab2 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts @@ -41,12 +41,14 @@ import type { DeploymentLogsData, DeploymentTrack, Environment, + GetAuthorizedGitOrgsReq, GetAutoBuildStatusReq, GetAutoBuildStatusResp, GetBranchesReq, GetBuildLogsForTypeReq, GetBuildLogsReq, GetBuildsReq, + GetCliRpcResp, GetCommitsReq, GetComponentEndpointsReq, GetComponentItemReq, @@ -55,16 +57,23 @@ import type { GetConnectionGuideResp, GetConnectionItemReq, GetConnectionsReq, + GetCredentialDetailsReq, GetCredentialsReq, GetDeploymentStatusReq, GetDeploymentTracksReq, + GetGitMetadataReq, + GetGitMetadataResp, + GetGitTokenForRepositoryReq, + GetGitTokenForRepositoryResp, GetMarketplaceIdlReq, GetMarketplaceListReq, GetProjectEnvsReq, GetProxyDeploymentInfoReq, + GetSubscriptionsReq, GetSwaggerSpecReq, GetTestKeyReq, GetTestKeyResp, + GithubOrganization, IChoreoRPCClient, IsRepoAuthorizedReq, IsRepoAuthorizedResp, @@ -75,8 +84,10 @@ import type { PromoteProxyDeploymentReq, ProxyDeploymentInfo, RequestPromoteApprovalReq, + SubscriptionsResp, ToggleAutoBuildReq, ToggleAutoBuildResp, + UpdateCodeServerReq, UserInfo, } from "@wso2/wso2-platform-core"; import { workspace } from "vscode"; @@ -107,7 +118,7 @@ export class RPCClient { const resp = await this._conn.sendRequest<{}>("initialize", { clientName: "vscode", clientVersion: "1.0.0", - cloudStsToken: workspace.getConfiguration().get("WSO2.WSO2-Platform.Advanced.StsToken") || process.env.CLOUD_STS_TOKEN || "", + cloudStsToken: process.env.CLOUD_STS_TOKEN || "", }); console.log("Initialized RPC server", resp); } catch (e) { @@ -137,7 +148,6 @@ export class RPCClient { await this.init(); return this.sendRequest(method, params, timeout, true); } - getLogger().error("Error sending request", e); handlerError(e); throw e; } @@ -246,6 +256,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response; } + async getAuthorizedGitOrgs(params: GetAuthorizedGitOrgsReq) { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response = await this.client.sendRequest<{ gitOrgs: GithubOrganization[] }>("repo/getAuthorizedGitOrgs", params); + return { gitOrgs: response.gitOrgs }; + } + async getCredentials(params: GetCredentialsReq) { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -254,6 +272,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response?.credentials; } + async getCredentialDetails(params: GetCredentialDetailsReq) { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: CredentialItem = await this.client.sendRequest("repo/getCredentialDetails", params); + return response; + } + async getUserInfo(): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -262,26 +288,23 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response.userInfo; } - async getSignInUrl({ - baseUrl, - callbackUrl, - clientId, - isSignUp, - }: { callbackUrl: string; baseUrl?: string; clientId?: string; isSignUp?: boolean }): Promise { + async getSignInUrl({ callbackUrl }: { callbackUrl: string }): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response = await this.client.sendRequest<{ loginUrl: string }>("auth/getSignInUrl", { callbackUrl }, 2000); + return response.loginUrl; + } + + async getDevantSignInUrl({ callbackUrl }: { callbackUrl: string }): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); } - const response = await this.client.sendRequest<{ loginUrl: string }>("auth/getSignInUrl", { callbackUrl, baseUrl, clientId, isSignUp }, 2000); + const response = await this.client.sendRequest<{ loginUrl: string }>("auth/getDevantSignInUrl", { callbackUrl }, 2000); return response.loginUrl; } - async signInWithAuthCode( - authCode: string, - region?: string, - orgId?: string, - redirectUrl?: string, - clientId?: string, - ): Promise { + async signInWithAuthCode(authCode: string, region?: string, orgId?: string): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); } @@ -289,8 +312,18 @@ export class ChoreoRPCClient implements IChoreoRPCClient { authCode, region, orgId, - redirectUrl, - clientId, + }); + return response.userInfo; + } + + async signInDevantWithAuthCode(authCode: string, region?: string, orgId?: string): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response = await this.client.sendRequest<{ userInfo: UserInfo }>("auth/signInDevantWithAuthCode", { + authCode, + region, + orgId, }); return response.userInfo; } @@ -302,6 +335,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { await this.client.sendRequest("auth/signOut", undefined, 2000); } + async getCurrentRegion(): Promise<"US" | "EU"> { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const resp: { region: "US" | "EU" } = await this.client.sendRequest("auth/getCurrentRegion"); + return resp.region; + } + async changeOrgContext(orgId: string): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -532,6 +573,53 @@ export class ChoreoRPCClient implements IChoreoRPCClient { } await this.client.sendRequest("deployment/promoteProxy", params); } + + async getSubscriptions(params: GetSubscriptionsReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: SubscriptionsResp = await this.client.sendRequest("auth/getSubscriptions", params); + return response; + } + + async getStsToken(): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: { token: string } = await this.client.sendRequest("auth/getStsToken", {}); + return response?.token; + } + + async getGitTokenForRepository(params: GetGitTokenForRepositoryReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: GetGitTokenForRepositoryResp = await this.client.sendRequest("repo/gitTokenForRepository", params); + return response; + } + + async getGitRepoMetadata(params: GetGitMetadataReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: GetGitMetadataResp = await this.client.sendRequest("repo/getRepoMetadata", params); + return response; + } + + async updateCodeServer(params: UpdateCodeServerReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + await this.client.sendRequest("component/updateCodeServer", params); + } + + async getConfigFromCli(): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: GetCliRpcResp = await this.client.sendRequest("auth/getConfigs", {}); + return response; + } } export class ChoreoTracer implements Tracer { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/connection.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/connection.ts index bde9ee67ae0..104951d5e2d 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/connection.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/connection.ts @@ -17,10 +17,11 @@ */ import { type ChildProcessWithoutNullStreams, spawn } from "child_process"; -import { workspace } from "vscode"; import { type MessageConnection, StreamMessageReader, StreamMessageWriter, createMessageConnection } from "vscode-jsonrpc/node"; +import { ext } from "../extensionVariables"; import { getLogger } from "../logger/logger"; -import { getChoreoEnv, getChoreoExecPath } from "./cli-install"; +import { parseJwt } from "../utils"; +import { getChoreoExecPath } from "./cli-install"; export class StdioConnection { private _connection: MessageConnection; @@ -29,11 +30,16 @@ export class StdioConnection { const executablePath = getChoreoExecPath(); console.log("Starting RPC server, path:", executablePath); getLogger().debug(`Starting RPC server${executablePath}`); + let region = process.env.CLOUD_REGION; + if (!region && process.env.CLOUD_STS_TOKEN && parseJwt(process.env.CLOUD_STS_TOKEN)?.iss?.includes(".eu.")) { + region = "EU"; + } this._serverProcess = spawn(executablePath, ["start-rpc-server"], { env: { ...process.env, - SKIP_KEYRING: workspace.getConfiguration().get("WSO2.WSO2-Platform.Advanced.StsToken") || process.env.CLOUD_STS_TOKEN ? "true" : "", - CHOREO_ENV: getChoreoEnv(), + SKIP_KEYRING: ext.isDevantCloudEditor ? "true" : "", + CHOREO_ENV: ext.choreoEnv, + CHOREO_REGION: region, }, }); this._connection = createMessageConnection( diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/rpc-resolver.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/rpc-resolver.ts index f44e7424f6b..6577050bdcb 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/rpc-resolver.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/rpc-resolver.ts @@ -31,6 +31,7 @@ import { ChoreoRpcDeleteConnection, ChoreoRpcDisableAutoBuild, ChoreoRpcEnableAutoBuild, + ChoreoRpcGetAuthorizedGitOrgsRequest, ChoreoRpcGetAutoBuildStatus, ChoreoRpcGetBranchesRequest, ChoreoRpcGetBuildLogs, @@ -43,20 +44,25 @@ import { ChoreoRpcGetConnectionGuide, ChoreoRpcGetConnectionItem, ChoreoRpcGetConnections, + ChoreoRpcGetCredentialDetailsRequest, ChoreoRpcGetCredentialsRequest, ChoreoRpcGetDeploymentStatusRequest, ChoreoRpcGetDeploymentTracksRequest, ChoreoRpcGetEndpointsRequest, ChoreoRpcGetEnvsRequest, + ChoreoRpcGetGitRepoMetadata, + ChoreoRpcGetGitTokenForRepository, ChoreoRpcGetMarketplaceItemIdl, ChoreoRpcGetMarketplaceItems, ChoreoRpcGetProjectsRequest, ChoreoRpcGetProxyDeploymentInfo, + ChoreoRpcGetSubscriptions, ChoreoRpcGetSwaggerRequest, ChoreoRpcGetTestKeyRequest, ChoreoRpcIsRepoAuthorizedRequest, ChoreoRpcPromoteProxyDeployment, ChoreoRpcRequestPromoteApproval, + type GetAuthorizedGitOrgsReq, type GetAutoBuildStatusReq, type GetBranchesReq, type GetBuildLogsForTypeReq, @@ -69,13 +75,17 @@ import { type GetConnectionGuideReq, type GetConnectionItemReq, type GetConnectionsReq, + type GetCredentialDetailsReq, type GetCredentialsReq, type GetDeploymentStatusReq, type GetDeploymentTracksReq, + type GetGitMetadataReq, + type GetGitTokenForRepositoryReq, type GetMarketplaceIdlReq, type GetMarketplaceListReq, type GetProjectEnvsReq, type GetProxyDeploymentInfoReq, + type GetSubscriptionsReq, type GetSwaggerSpecReq, type GetTestKeyReq, type IChoreoRPCClient, @@ -105,7 +115,9 @@ export function registerChoreoRpcResolver(messenger: Messenger, rpcClient: IChor messenger.onRequest(ChoreoRpcGetBuildPacksRequest, (params: BuildPackReq) => rpcClient.getBuildPacks(params)); messenger.onRequest(ChoreoRpcGetBranchesRequest, (params: GetBranchesReq) => rpcClient.getRepoBranches(params)); messenger.onRequest(ChoreoRpcIsRepoAuthorizedRequest, (params: IsRepoAuthorizedReq) => rpcClient.isRepoAuthorized(params)); + messenger.onRequest(ChoreoRpcGetAuthorizedGitOrgsRequest, (params: GetAuthorizedGitOrgsReq) => rpcClient.getAuthorizedGitOrgs(params)); messenger.onRequest(ChoreoRpcGetCredentialsRequest, (params: GetCredentialsReq) => rpcClient.getCredentials(params)); + messenger.onRequest(ChoreoRpcGetCredentialDetailsRequest, (params: GetCredentialDetailsReq) => rpcClient.getCredentialDetails(params)); messenger.onRequest(ChoreoRpcDeleteComponentRequest, async (params: Parameters[0]) => { const extName = webviewStateStore.getState().state.extensionName; return window.withProgress( @@ -174,4 +186,11 @@ export function registerChoreoRpcResolver(messenger: Messenger, rpcClient: IChor rpcClient.promoteProxyDeployment(params), ); }); + messenger.onRequest(ChoreoRpcGetSubscriptions, (params: GetSubscriptionsReq) => rpcClient.getSubscriptions(params)); + messenger.onRequest(ChoreoRpcGetGitTokenForRepository, (params: GetGitTokenForRepositoryReq) => rpcClient.getGitTokenForRepository(params)); + messenger.onRequest(ChoreoRpcGetGitRepoMetadata, async (params: GetGitMetadataReq) => { + return window.withProgress({ title: "Fetching repo metadata...", location: ProgressLocation.Notification }, () => + rpcClient.getGitRepoMetadata(params), + ); + }); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/clone-project-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/clone-project-cmd.ts index 41880c2f6e8..4d7178bea0d 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/clone-project-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/clone-project-cmd.ts @@ -31,7 +31,6 @@ import { import { type ExtensionContext, ProgressLocation, type QuickPickItem, QuickPickItemKind, Uri, commands, window } from "vscode"; import { ext } from "../extensionVariables"; import { initGit } from "../git/main"; -import { authStore } from "../stores/auth-store"; import { dataCacheStore } from "../stores/data-cache-store"; import { webviewStateStore } from "../stores/webview-state-store"; import { createDirectory, openDirectory } from "../utils"; @@ -168,7 +167,11 @@ export function cloneRepoCommand(context: ExtensionContext) { ]); // set context.yaml - updateContextFile(clonedResp[0].clonedPath, authStore.getState().state.userInfo!, selectedProject, selectedOrg, projectCache); + const userInfo = ext.authProvider?.getState()?.state?.userInfo; + if (!userInfo) { + throw new Error("User information is not available. Please ensure you are logged in."); + } + updateContextFile(clonedResp[0].clonedPath, userInfo, selectedProject, selectedOrg, projectCache); const subDir = params?.component?.spec?.source ? getComponentKindRepoSource(params?.component?.spec?.source)?.path || "" : ""; const subDirFullPath = join(clonedResp[0].clonedPath, subDir); if (params?.technology === "ballerina") { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/cmd-utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/cmd-utils.ts index da900aeedcf..01f4863135d 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/cmd-utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/cmd-utils.ts @@ -19,7 +19,7 @@ import { CommandIds, type ComponentKind, type ExtensionName, type Organization, type Project, type UserInfo } from "@wso2/wso2-platform-core"; import { ProgressLocation, type QuickPickItem, QuickPickItemKind, type WorkspaceFolder, commands, window, workspace } from "vscode"; import { type ExtensionVariables, ext } from "../extensionVariables"; -import { authStore, waitForLogin } from "../stores/auth-store"; +import { waitForLogin } from "../auth/wso2-auth-provider"; import { dataCacheStore } from "../stores/data-cache-store"; import { webviewStateStore } from "../stores/webview-state-store"; @@ -326,7 +326,7 @@ export async function quickPickWithLoader(params: { } export const getUserInfoForCmd = async (message: string): Promise => { - let userInfo = authStore.getState().state.userInfo; + let userInfo = ext.authProvider?.getState().state.userInfo; const extensionName = webviewStateStore.getState().state.extensionName; if (!userInfo) { const loginSelection = await window.showInformationMessage( @@ -351,7 +351,7 @@ export const getUserInfoForCmd = async (message: string): Promise { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts new file mode 100644 index 00000000000..dc0f7ca16f8 --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts @@ -0,0 +1,253 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + CommandIds, + ComponentKind, + type ContextStoreComponentState, + GitProvider, + type ICommitAndPuhCmdParams, + type Organization, + parseGitURL, +} from "@wso2/wso2-platform-core"; +import { type ExtensionContext, ProgressLocation, type QuickPickItem, Uri, commands, env, window, workspace } from "vscode"; +import { ext } from "../extensionVariables"; +import { initGit } from "../git/main"; +import { hasDirtyRepo } from "../git/util"; +import { getLogger } from "../logger/logger"; +import { contextStore } from "../stores/context-store"; +import { webviewStateStore } from "../stores/webview-state-store"; +import { delay, isSamePath } from "../utils"; +import { getUserInfoForCmd, isRpcActive, setExtensionName } from "./cmd-utils"; + +export function commitAndPushToGitCommand(context: ExtensionContext) { + context.subscriptions.push( + commands.registerCommand(CommandIds.CommitAndPushToGit, async (params: ICommitAndPuhCmdParams) => { + setExtensionName(params?.extName); + const extensionName = webviewStateStore.getState().state.extensionName; + try { + isRpcActive(ext); + const userInfo = await getUserInfoForCmd("commit and push changes to Git"); + if (userInfo) { + const selected = contextStore.getState().state.selected; + if (!selected) { + throw new Error("project is not associated with a component directory"); + } + + let selectedComp: ContextStoreComponentState | undefined; + const getSelectedComponent = async (items: ContextStoreComponentState[]) => { + const componentItems: (QuickPickItem & { item?: ContextStoreComponentState })[] = items.map((item) => ({ + label: item?.component?.metadata?.displayName!, + item: item, + })); + const selectedComp = await window.showQuickPick(componentItems, { + title: `Multiple ${extensionName === "Devant" ? "integrations" : "components"} detected. Please select ${extensionName === "Devant" ? "an integration" : "a component"} to push`, + }); + return selectedComp?.item; + }; + + if (contextStore.getState().state?.components?.length === 0) { + throw new Error("No components in this workspace"); + } + + if (params?.componentPath) { + const matchingComponent = contextStore + .getState() + .state?.components?.filter((item) => isSamePath(item.componentFsPath, params?.componentPath)); + if (matchingComponent?.length === 0) { + selectedComp = await getSelectedComponent(contextStore.getState().state?.components!); + } else if (matchingComponent?.length === 1) { + selectedComp = matchingComponent[0]; + } else if (matchingComponent && matchingComponent?.length > 1) { + selectedComp = await getSelectedComponent(matchingComponent); + } + } else { + selectedComp = await getSelectedComponent(contextStore.getState().state?.components!); + } + + if (!selectedComp) { + throw new Error("Failed to select component fo be pushed to remote"); + } + + const haveChanges = await hasDirtyRepo(selectedComp.componentFsPath, ext.context, ["context.yaml"]); + if (!haveChanges) { + window.showErrorMessage("There are no new changes to push to cloud"); + return; + } + + const newGit = await initGit(ext.context); + if (!newGit) { + throw new Error("failed to initGit"); + } + const dotGit = await newGit?.getRepositoryDotGit(selectedComp.componentFsPath); + const repoRoot = await newGit?.getRepositoryRoot(selectedComp.componentFsPath); + const repo = newGit.open(repoRoot, dotGit); + + const remotes = await window.withProgress({ title: "Fetching remotes of the repo...", location: ProgressLocation.Notification }, () => + repo.getRemotes(), + ); + + if (remotes.length === 0) { + window.showErrorMessage("No remotes found within the directory"); + return; + } + + let matchingRemote = remotes.find((item) => { + if (item.pushUrl) { + const urlObj = new URL(item.pushUrl); + if (urlObj.password) { + return true; + } + } + }); + + if (!matchingRemote && remotes[0].fetchUrl) { + const repoUrl = remotes[0].fetchUrl; + const parsed = parseGitURL(repoUrl); + if (parsed) { + const [repoOrg, repoName, provider] = parsed; + const urlObj = new URL(repoUrl); + await enrichGitUsernamePassword( + selected.org!, + repoOrg, + repoName, + provider, + urlObj, + repoUrl, + selectedComp.component?.spec?.source?.secretRef || "", + ); + await window.withProgress({ title: "Setting new remote...", location: ProgressLocation.Notification }, async () => { + await repo.addRemote("cloud-editor-remote", urlObj.href); + const remotes = await repo.getRemotes(); + matchingRemote = remotes.find((item) => item.name === "cloud-editor-remote"); + }); + } + } + + await window.withProgress({ title: "Adding changes to be committed...", location: ProgressLocation.Notification }, async () => { + await repo.add(["."]); + }); + + const commitMessage = await window.showInputBox({ + placeHolder: "Message to describe the changes done to your integration", + title: "Enter commit message", + validateInput: (val) => { + if (!val) { + return "Commit message is required"; + } + return null; + }, + }); + + if (!commitMessage) { + window.showErrorMessage("Commit message is required in order to proceed"); + return; + } + + const headRef = await window.withProgress( + { title: "Fetching remote repo metadata...", location: ProgressLocation.Notification }, + async () => { + await repo.fetch({ silent: true, remote: matchingRemote?.name }); + await repo.commit(commitMessage); + await delay(500); + return repo.getHEADRef(); + }, + ); + + if (headRef?.ahead && (headRef?.behind === 0 || headRef?.behind === undefined)) { + await window.withProgress({ title: "Pushing changes to remote repository...", location: ProgressLocation.Notification }, () => + repo.push(matchingRemote?.name), + ); + window.showInformationMessage("Your changes have been successfully pushed to cloud"); + } else { + await commands.executeCommand("git.sync"); + } + } + } catch (err: any) { + console.error("Failed to push to remote", err); + window.showErrorMessage(err?.message || "Failed to push to remote"); + } + }), + ); +} + +export const enrichGitUsernamePassword = async ( + org: Organization, + repoOrg: string, + repoName: string, + provider: string, + urlObj: URL, + fetchUrl: string, + secretRef: string, +) => { + if (ext.isDevantCloudEditor && provider === GitProvider.GITHUB && !urlObj.password) { + try { + getLogger().debug(`Fetching PAT for org ${repoOrg} and repo ${repoName}`); + const gitPat = await window.withProgress( + { title: `Accessing the repository ${repoOrg}/${repoName}...`, location: ProgressLocation.Notification }, + () => + ext.clients.rpcClient.getGitTokenForRepository({ + orgId: org?.id?.toString()!, + gitOrg: repoOrg, + gitRepo: repoName, + secretRef: secretRef, + }), + ); + urlObj.username = gitPat.username || "x-access-token"; + urlObj.password = gitPat.token; + } catch { + getLogger().debug(`Failed to get token for ${fetchUrl}`); + } + } + + if (!urlObj.username) { + const username = await window.showInputBox({ + title: "Git Username", + ignoreFocusOut: true, + placeHolder: "username", + validateInput: (val) => { + if (!val) { + return "Git username is required"; + } + return null; + }, + }); + if (!username) { + throw new Error("Git username is required"); + } + urlObj.username = username; + } + if (!urlObj.password) { + const password = await window.showInputBox({ + title: "Git Password", + ignoreFocusOut: true, + placeHolder: "password", + password: true, + validateInput: (val) => { + if (!val) { + return "Git password is required"; + } + return null; + }, + }); + if (!password) { + throw new Error("Git password is required"); + } + urlObj.password = password; + } +}; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts index 698eafe0c37..a65bd5732ed 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts @@ -20,9 +20,9 @@ import { existsSync, readFileSync } from "fs"; import * as os from "os"; import * as path from "path"; import { - ChoreoBuildPackNames, ChoreoComponentType, CommandIds, + type ComponentKind, DevantScopes, type ExtensionName, type ICreateComponentCmdParams, @@ -35,11 +35,11 @@ import { parseGitURL, } from "@wso2/wso2-platform-core"; import { type ExtensionContext, ProgressLocation, type QuickPickItem, Uri, commands, window, workspace } from "vscode"; -import { choreoEnvConfig } from "../config"; import { ext } from "../extensionVariables"; +import { initGit } from "../git/main"; import { getGitRemotes, getGitRoot } from "../git/util"; -import { authStore } from "../stores/auth-store"; -import { contextStore } from "../stores/context-store"; +import { getLogger } from "../logger/logger"; +import { contextStore, waitForContextStoreToLoad } from "../stores/context-store"; import { dataCacheStore } from "../stores/data-cache-store"; import { webviewStateStore } from "../stores/webview-state-store"; import { convertFsPathToUriPath, isSamePath, isSubpath, openDirectory } from "../utils"; @@ -59,6 +59,7 @@ export function createNewComponentCommand(context: ExtensionContext) { isRpcActive(ext); const userInfo = await getUserInfoForCmd(`create ${extName === "Devant" ? "an integration" : "a component"}`); if (userInfo) { + await waitForContextStoreToLoad(); const selected = contextStore.getState().state.selected; let selectedProject = selected?.project; let selectedOrg = selected?.org; @@ -172,8 +173,10 @@ export function createNewComponentCommand(context: ExtensionContext) { dataCacheStore.getState().setComponents(selectedOrg.handle, selectedProject.handler, components); let gitRoot: string | undefined; + let isGitInitialized = false; try { gitRoot = await getGitRoot(context, selectedUri.fsPath); + isGitInitialized = true; } catch (err) { // ignore error } @@ -214,7 +217,13 @@ export function createNewComponentCommand(context: ExtensionContext) { ); if (resp !== "Proceed") { const projectCache = dataCacheStore.getState().getProjects(selectedOrg?.handle); - updateContextFile(gitRoot, authStore.getState().state.userInfo!, selectedProject, selectedOrg, projectCache); + const authProvider = ext.authProvider; + const userInfo = authProvider?.getState().state.userInfo; + if (!authProvider || !userInfo) { + window.showErrorMessage("User information is not available. Please sign in and try again."); + return; + } + updateContextFile(gitRoot, userInfo, selectedProject, selectedOrg, projectCache); contextStore.getState().refreshState(); return; } @@ -222,6 +231,15 @@ export function createNewComponentCommand(context: ExtensionContext) { const isWithinWorkspace = workspace.workspaceFolders?.some((item) => isSubpath(item.uri?.fsPath, selectedUri?.fsPath)); + let compInitialName = params?.name || dirName || selectedType; + const existingNames = components.map((c) => c.metadata?.name?.toLowerCase?.()); + const baseName = compInitialName; + let counter = 1; + while (existingNames.includes(compInitialName.toLowerCase())) { + compInitialName = `${baseName}-${counter}`; + counter++; + } + const createCompParams: IComponentCreateFormParams = { directoryUriPath: selectedUri.path, directoryFsPath: selectedUri.fsPath, @@ -229,11 +247,12 @@ export function createNewComponentCommand(context: ExtensionContext) { organization: selectedOrg!, project: selectedProject!, extensionName: webviewStateStore.getState().state.extensionName, + isNewCodeServerComp: isGitInitialized === false && ext.isDevantCloudEditor, initialValues: { type: selectedType, subType: selectedSubType, buildPackLang: params?.buildPackLang, - name: params?.name || dirName || "", + name: compInitialName, }, }; @@ -304,39 +323,72 @@ export const submitCreateComponentHandler = async ({ createParams, org, project } */ - if (extensionName !== "Devant") { - showComponentDetailsView(org, project, createdComponent, createParams?.componentDir); - } - - window - .showInformationMessage( - `${extensionName === "Devant" ? "Integration" : "Component"} '${createdComponent.metadata.name}' was successfully created`, - `Open in ${extensionName}`, - ) - .then(async (resp) => { - if (resp === `Open in ${extensionName}`) { - commands.executeCommand( - "vscode.open", - `${extensionName === "Devant" ? choreoEnvConfig.getDevantUrl() : choreoEnvConfig.getConsoleUrl()}/organizations/${org.handle}/projects/${project.id}/components/${createdComponent.metadata.handler}/overview`, - ); - } - }); - const compCache = dataCacheStore.getState().getComponents(org.handle, project.handler); dataCacheStore.getState().setComponents(org.handle, project.handler, [createdComponent, ...compCache]); // update the context file if needed try { - const gitRoot = await getGitRoot(ext.context, createParams.componentDir); + const newGit = await initGit(ext.context); + const gitRoot = await newGit?.getRepositoryRoot(createParams.componentDir); + const dotGit = await newGit?.getRepositoryDotGit(createParams.componentDir); const projectCache = dataCacheStore.getState().getProjects(org.handle); - if (gitRoot) { - updateContextFile(gitRoot, authStore.getState().state.userInfo!, project, org, projectCache); - contextStore.getState().refreshState(); + if (newGit && gitRoot && dotGit) { + if (ext.isDevantCloudEditor) { + // update the code server, to attach itself to the created component + const repo = newGit.open(gitRoot, dotGit); + const head = await repo.getHEAD(); + if (head.name) { + const commit = await repo.getCommit(head.name); + try { + await window.withProgress( + { title: "Updating cloud editor with newly created component...", location: ProgressLocation.Notification }, + () => + ext.clients.rpcClient.updateCodeServer({ + componentId: createdComponent.metadata.id, + orgHandle: org.handle, + orgId: org.id.toString(), + orgUuid: org.uuid, + projectId: project.id, + sourceCommitHash: commit.hash, + }), + ); + } catch (err) { + getLogger().error("Failed to updated code server after creating the component", err); + } + + // Clear code server local storage data data + try { + await commands.executeCommand("devantEditor.clearLocalStorage"); + } catch (err) { + getLogger().error(`Failed to execute devantEditor.clearLocalStorage command: ${err}`); + } + } + } else { + const userInfo = ext.authProvider?.getState().state.userInfo; + if (userInfo) { + updateContextFile(gitRoot, userInfo, project, org, projectCache); + contextStore.getState().refreshState(); + } else { + getLogger().error("Cannot update context file: userInfo is undefined."); + } + } } } catch (err) { console.error("Failed to get git details of ", createParams.componentDir); } + if (extensionName !== "Devant") { + showComponentDetailsView(org, project, createdComponent, createParams?.componentDir, undefined, true); + } + + const successMessage = `${extensionName === "Devant" ? "Integration" : "Component"} '${createdComponent.metadata.name}' was successfully created.`; + + const isWithinWorkspace = workspace.workspaceFolders?.some((item) => isSubpath(item.uri?.fsPath, createParams.componentDir)); + + if (ext.isDevantCloudEditor) { + await ext.context.globalState.update("code-server-component-id", createdComponent.metadata?.id); + } + if (workspace.workspaceFile) { const workspaceContent: WorkspaceConfig = JSON.parse(readFileSync(workspace.workspaceFile.fsPath, "utf8")); workspaceContent.folders = [ @@ -346,9 +398,24 @@ export const submitCreateComponentHandler = async ({ createParams, org, project path: path.normalize(path.relative(path.dirname(workspace.workspaceFile.fsPath), createParams.componentDir)), }, ]; + } else if (isWithinWorkspace) { + window.showInformationMessage(successMessage, `Open in ${extensionName}`).then(async (resp) => { + if (resp === `Open in ${extensionName}`) { + commands.executeCommand( + "vscode.open", + `${extensionName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl}/organizations/${org.handle}/projects/${extensionName === "Devant" ? project.id : project.handler}/components/${createdComponent.metadata.handler}/overview`, + ); + } + }); } else { - contextStore.getState().refreshState(); + window.showInformationMessage(`${successMessage} Reload workspace to continue`, { modal: true }, "Continue").then(async (resp) => { + if (resp === "Continue") { + commands.executeCommand("vscode.openFolder", Uri.file(createParams.componentDir), { forceNewWindow: false }); + } + }); } + + contextStore.getState().refreshState(); } return createdComponent; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/index.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/index.ts index c07d8d22ee9..d38ba829a65 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/index.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/index.ts @@ -18,6 +18,7 @@ import type { ExtensionContext } from "vscode"; import { cloneRepoCommand } from "./clone-project-cmd"; +import { commitAndPushToGitCommand } from "./commit-and-push-to-git-cmd"; import { createComponentDependencyCommand } from "./create-comp-dependency-cmd"; import { createNewComponentCommand } from "./create-component-cmd"; import { createDirectoryContextCommand } from "./create-directory-context-cmd"; @@ -49,4 +50,5 @@ export function activateCmds(context: ExtensionContext) { createComponentDependencyCommand(context); viewComponentDependencyCommand(context); openCompSrcCommand(context); + commitAndPushToGitCommand(context); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/open-in-console-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/open-in-console-cmd.ts index 71dea4d7872..cba6202da21 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/open-in-console-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/open-in-console-cmd.ts @@ -16,16 +16,8 @@ * under the License. */ -import { - CommandIds, - type ComponentKind, - type ICreateComponentCmdParams, - type IOpenInConsoleCmdParams, - type Organization, - type Project, -} from "@wso2/wso2-platform-core"; +import { CommandIds, type ComponentKind, type ICreateComponentCmdParams, type IOpenInConsoleCmdParams } from "@wso2/wso2-platform-core"; import { type ExtensionContext, ProgressLocation, type QuickPickItem, QuickPickItemKind, Uri, commands, env, window } from "vscode"; -import { choreoEnvConfig } from "../config"; import { ext } from "../extensionVariables"; import { contextStore } from "../stores/context-store"; import { dataCacheStore } from "../stores/data-cache-store"; @@ -66,9 +58,9 @@ export function openInConsoleCommand(context: ExtensionContext) { } } - let projectBaseUrl = `${choreoEnvConfig.getConsoleUrl()}/organizations/${selectedOrg?.handle}/projects/${selectedProject.handler}`; - if (extensionName === "Devant") { - projectBaseUrl = `${choreoEnvConfig.getDevantUrl()}/organizations/${selectedOrg?.handle}/projects/${selectedProject.id}`; + let projectBaseUrl = `${ext.config?.choreoConsoleUrl}/organizations/${selectedOrg?.handle}/projects/${selectedProject.handler}`; + if(extensionName === "Devant"){ + projectBaseUrl = `${ext.config?.devantConsoleUrl}/organizations/${selectedOrg?.handle}/projects/${selectedProject.id}`; } if (params?.component) { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/refresh-directory-context-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/refresh-directory-context-cmd.ts index 3903645d3f0..b41e159c173 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/refresh-directory-context-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/refresh-directory-context-cmd.ts @@ -19,7 +19,6 @@ import { CommandIds, type ICmdParamsBase } from "@wso2/wso2-platform-core"; import { type ExtensionContext, commands, window } from "vscode"; import { ext } from "../extensionVariables"; -import { authStore } from "../stores/auth-store"; import { contextStore } from "../stores/context-store"; import { isRpcActive, setExtensionName } from "./cmd-utils"; @@ -29,7 +28,7 @@ export function refreshContextCommand(context: ExtensionContext) { try { isRpcActive(ext); setExtensionName(params?.extName); - const userInfo = authStore.getState().state.userInfo; + const userInfo = ext.authProvider?.getState().state.userInfo; if (!userInfo) { throw new Error("You are not logged in. Please log in and retry."); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-cmd.ts index 74953c79337..589900bb5b1 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-cmd.ts @@ -19,7 +19,6 @@ import { CommandIds, type ICmdParamsBase } from "@wso2/wso2-platform-core"; import { type ExtensionContext, ProgressLocation, commands, window } from "vscode"; import * as vscode from "vscode"; -import { choreoEnvConfig } from "../config"; import { ext } from "../extensionVariables"; import { getLogger } from "../logger/logger"; import { webviewStateStore } from "../stores/webview-state-store"; @@ -34,17 +33,12 @@ export function signInCommand(context: ExtensionContext) { getLogger().debug("Signing in to WSO2 Platform"); const callbackUrl = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://wso2.wso2-platform/signin`)); - let baseUrl: string | undefined; - if (webviewStateStore.getState().state?.extensionName === "Devant") { - baseUrl = `${choreoEnvConfig.getDevantUrl()}/login`; - } - let clientId: string | undefined; - if (webviewStateStore.getState().state?.extensionName === "Devant") { - clientId = choreoEnvConfig.getDevantAsgardeoClientId(); - } console.log("Generating WSO2 Platform login URL for ", callbackUrl.toString()); const loginUrl = await window.withProgress({ title: "Generating Login URL...", location: ProgressLocation.Notification }, async () => { - return ext.clients.rpcClient.getSignInUrl({ callbackUrl: callbackUrl.toString(), baseUrl, clientId }); + if (webviewStateStore.getState().state?.extensionName === "Devant") { + return ext.clients.rpcClient.getDevantSignInUrl({ callbackUrl: callbackUrl.toString() }); + } + return ext.clients.rpcClient.getSignInUrl({ callbackUrl: callbackUrl.toString() }); }); if (loginUrl) { @@ -54,7 +48,7 @@ export function signInCommand(context: ExtensionContext) { window.showErrorMessage("Unable to open external link for authentication."); } } catch (error: any) { - getLogger().error(`Error while signing in to WSO2 Platofmr. ${error?.message}${error?.cause ? `\nCause: ${error.cause.message}` : ""}`); + getLogger().error(`Error while signing in to WSO2 Platform. ${error?.message}${error?.cause ? `\nCause: ${error.cause.message}` : ""}`); if (error instanceof Error) { window.showErrorMessage(error.message); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-with-code-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-with-code-cmd.ts index b59704676f9..159f6a2da11 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-with-code-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-with-code-cmd.ts @@ -23,7 +23,6 @@ import { ResponseError } from "vscode-jsonrpc"; import { ErrorCode } from "../choreo-rpc/constants"; import { ext } from "../extensionVariables"; import { getLogger } from "../logger/logger"; -import { authStore } from "../stores/auth-store"; import { isRpcActive, setExtensionName } from "./cmd-utils"; export function signInWithAuthCodeCommand(context: ExtensionContext) { @@ -42,9 +41,10 @@ export function signInWithAuthCodeCommand(context: ExtensionContext) { }); if (authCode) { - ext.clients.rpcClient.signInWithAuthCode(authCode).then((userInfo) => { + ext.clients.rpcClient.signInWithAuthCode(authCode).then(async (userInfo) => { if (userInfo) { - authStore.getState().loginSuccess(userInfo); + const region = await ext.clients.rpcClient.getCurrentRegion(); + ext.authProvider?.getState().loginSuccess(userInfo, region); } }); } else { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-out-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-out-cmd.ts index ccd9bf49dfb..61e0a42b46d 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-out-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-out-cmd.ts @@ -20,7 +20,6 @@ import { CommandIds } from "@wso2/wso2-platform-core"; import { type ExtensionContext, commands, window } from "vscode"; import { ext } from "../extensionVariables"; import { getLogger } from "../logger/logger"; -import { authStore } from "../stores/auth-store"; import { isRpcActive } from "./cmd-utils"; export function signOutCommand(context: ExtensionContext) { @@ -28,9 +27,7 @@ export function signOutCommand(context: ExtensionContext) { commands.registerCommand(CommandIds.SignOut, async () => { try { isRpcActive(ext); - getLogger().debug("Signing out from WSO2 Platform"); - authStore.getState().logout(); - window.showInformationMessage("Successfully signed out from WSO2 Platform!"); + ext.authProvider?.getState().logout(); } catch (error: any) { getLogger().error(`Error while signing out from WSO2 Platform. ${error?.message}${error?.cause ? `\nCause: ${error.cause.message}` : ""}`); if (error instanceof Error) { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/config.ts b/workspaces/wso2-platform/wso2-platform-extension/src/config.ts index 31b3bd7fe2c..5e18a4d5ebc 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/config.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/config.ts @@ -18,23 +18,9 @@ import { window } from "vscode"; import { z } from "zod/v3"; -import { getChoreoEnv } from "./choreo-rpc/cli-install"; +import { ext } from "./extensionVariables"; -const ghAppSchema = z.object({ - installUrl: z.string().min(1), - authUrl: z.string().min(1), - clientId: z.string().min(1), - redirectUrl: z.string().min(1), - devantRedirectUrl: z.string().min(1), -}); - -const envSchemaItem = z.object({ - ghApp: ghAppSchema, - choreoConsoleBaseUrl: z.string().min(1), - billingConsoleBaseUrl: z.string().min(1), - devantConsoleBaseUrl: z.string().min(1), - devantAsgardeoClientId: z.string().min(1), -}); +const envSchemaItem = z.object({}); const envSchema = z.object({ CLI_RELEASES_BASE_URL: z.string().min(1), @@ -45,45 +31,9 @@ const envSchema = z.object({ const _env = envSchema.safeParse({ CLI_RELEASES_BASE_URL: process.env.PLATFORM_CHOREO_CLI_RELEASES_BASE_URL, - defaultEnvs: { - ghApp: { - installUrl: process.env.PLATFORM_DEFAULT_GHAPP_INSTALL_URL ?? "", - authUrl: process.env.PLATFORM_DEFAULT_GHAPP_AUTH_URL ?? "", - clientId: process.env.PLATFORM_DEFAULT_GHAPP_CLIENT_ID ?? "", - redirectUrl: process.env.PLATFORM_DEFAULT_GHAPP_REDIRECT_URL ?? "", - devantRedirectUrl: process.env.PLATFORM_DEFAULT_GHAPP_DEVANT_REDIRECT_URL ?? "", - }, - choreoConsoleBaseUrl: process.env.PLATFORM_DEFAULT_CHOREO_CONSOLE_BASE_URL ?? "", - billingConsoleBaseUrl: process.env.PLATFORM_DEFAULT_BILLING_CONSOLE_BASE_URL ?? "", - devantConsoleBaseUrl: process.env.PLATFORM_DEFAULT_DEVANT_CONSOLE_BASE_URL ?? "", - devantAsgardeoClientId: process.env.PLATFORM_DEFAULT_DEVANT_ASGARDEO_CLIENT_ID ?? "", - }, - stageEnvs: { - ghApp: { - installUrl: process.env.PLATFORM_STAGE_GHAPP_INSTALL_URL ?? "", - authUrl: process.env.PLATFORM_STAGE_GHAPP_AUTH_URL ?? "", - clientId: process.env.PLATFORM_STAGE_GHAPP_CLIENT_ID ?? "", - redirectUrl: process.env.PLATFORM_STAGE_GHAPP_REDIRECT_URL ?? "", - devantRedirectUrl: process.env.PLATFORM_STAGE_GHAPP_DEVANT_REDIRECT_URL ?? "", - }, - choreoConsoleBaseUrl: process.env.PLATFORM_STAGE_CHOREO_CONSOLE_BASE_URL ?? "", - billingConsoleBaseUrl: process.env.PLATFORM_STAGE_BILLING_CONSOLE_BASE_URL ?? "", - devantConsoleBaseUrl: process.env.PLATFORM_STAGE_DEVANT_CONSOLE_BASE_URL ?? "", - devantAsgardeoClientId: process.env.PLATFORM_STAGE_DEVANT_ASGARDEO_CLIENT_ID ?? "", - }, - devEnvs: { - ghApp: { - installUrl: process.env.PLATFORM_DEV_GHAPP_INSTALL_URL ?? "", - authUrl: process.env.PLATFORM_DEV_GHAPP_AUTH_URL ?? "", - clientId: process.env.PLATFORM_DEV_GHAPP_CLIENT_ID ?? "", - redirectUrl: process.env.PLATFORM_DEV_GHAPP_REDIRECT_URL ?? "", - devantRedirectUrl: process.env.PLATFORM_DEV_GHAPP_DEVANT_REDIRECT_URL ?? "", - }, - choreoConsoleBaseUrl: process.env.PLATFORM_DEV_CHOREO_CONSOLE_BASE_URL ?? "", - billingConsoleBaseUrl: process.env.PLATFORM_DEV_BILLING_CONSOLE_BASE_URL ?? "", - devantConsoleBaseUrl: process.env.PLATFORM_DEV_DEVANT_CONSOLE_BASE_URL ?? "", - devantAsgardeoClientId: process.env.PLATFORM_DEV_DEVANT_ASGARDEO_CLIENT_ID ?? "", - }, + defaultEnvs: {}, + stageEnvs: {}, + devEnvs: {}, } as z.infer); if (!_env.success) { @@ -97,33 +47,11 @@ class ChoreoEnvConfig { public getCliInstallUrl() { return _env.data?.CLI_RELEASES_BASE_URL; } - - public getGHAppConfig() { - return this._config.ghApp; - } - - public getConsoleUrl(): string { - return this._config.choreoConsoleBaseUrl; - } - - public getBillingUrl(): string { - return this._config.billingConsoleBaseUrl; - } - - public getDevantUrl(): string { - return this._config.devantConsoleBaseUrl; - } - - public getDevantAsgardeoClientId(): string { - return this._config.devantAsgardeoClientId; - } } -const choreoEnv = getChoreoEnv(); - let pickedEnvConfig: z.infer; -switch (choreoEnv) { +switch (ext.choreoEnv) { case "prod": pickedEnvConfig = _env.data!.defaultEnvs; break; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts new file mode 100644 index 00000000000..48e63bf667e --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Uri, commands, window, workspace } from "vscode"; +import { ext } from "./extensionVariables"; +import { initGit } from "./git/main"; +import { getLogger } from "./logger/logger"; + +export const activateDevantFeatures = () => { + autoRefetchDevantStsToken(); + showRepoSyncNotification(); +}; + +const autoRefetchDevantStsToken = () => { + const intervalTime = 20 * 60 * 1000; // 20 minutes + const intervalId = setInterval(async () => { + try { + await ext.clients.rpcClient.getStsToken(); + } catch { + getLogger().error("Failed to refresh STS token"); + if (intervalId) { + clearInterval(intervalId); + } + } + }, intervalTime); + + ext.context.subscriptions.push({ + dispose: () => { + if (intervalId) { + clearTimeout(intervalId); + } + }, + }); +}; + +const showRepoSyncNotification = async () => { + if (workspace.workspaceFolders && workspace.workspaceFolders?.length > 0) { + try { + const componentPath = Uri.from(workspace.workspaceFolders[0].uri).fsPath; + const newGit = await initGit(ext.context); + if (!newGit) { + throw new Error("failed to initGit"); + } + const dotGit = await newGit?.getRepositoryDotGit(componentPath); + const repoRoot = await newGit?.getRepositoryRoot(componentPath); + const repo = newGit.open(repoRoot, dotGit); + await repo.fetch(); + const head = await repo.getHEADRef(); + if (head?.behind) { + window.showInformationMessage(`Your remote Git repository has ${head.behind} new changes`, "Sync Repository").then((res) => { + if (res === "Sync Repository") { + commands.executeCommand("git.sync"); + } + }); + } + } catch (err) { + getLogger().error(`Failed to check if the Git head is behind: ${(err as Error)?.message}`); + } + } +}; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/error-utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/error-utils.ts index cad5720e8ba..c8f83763278 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/error-utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/error-utils.ts @@ -16,13 +16,11 @@ * under the License. */ -import { CommandIds } from "@wso2/wso2-platform-core"; import { commands, window as w } from "vscode"; import { ResponseError } from "vscode-jsonrpc"; import { ErrorCode } from "./choreo-rpc/constants"; -import { choreoEnvConfig } from "./config"; +import { ext } from "./extensionVariables"; import { getLogger } from "./logger/logger"; -import { authStore } from "./stores/auth-store"; import { webviewStateStore } from "./stores/webview-state-store"; export function handlerError(err: any) { @@ -45,20 +43,20 @@ export function handlerError(err: any) { getLogger().error("InternalError", err); break; case ErrorCode.UnauthorizedError: - if (authStore.getState().state?.userInfo) { - authStore.getState().logout(); + if (ext.authProvider?.getState().state?.userInfo) { + ext.authProvider?.getState().logout(); w.showErrorMessage("Unauthorized. Please sign in again."); } break; case ErrorCode.TokenNotFoundError: - if (authStore.getState().state?.userInfo) { - authStore.getState().logout(); + if (ext.authProvider?.getState().state?.userInfo) { + ext.authProvider?.getState().logout(); w.showErrorMessage("Token not found. Please sign in again."); } break; case ErrorCode.InvalidTokenError: - if (authStore.getState().state?.userInfo) { - authStore.getState().logout(); + if (ext.authProvider?.getState().state?.userInfo) { + ext.authProvider?.getState().logout(); w.showErrorMessage("Invalid token. Please sign in again."); } break; @@ -66,8 +64,8 @@ export function handlerError(err: any) { getLogger().error("ForbiddenError", err); break; case ErrorCode.RefreshTokenError: - if (authStore.getState().state?.userInfo) { - authStore.getState().logout(); + if (ext.authProvider?.getState().state?.userInfo) { + ext.authProvider?.getState().logout(); w.showErrorMessage("Failed to refresh user session. Please sign in again."); } break; @@ -84,7 +82,10 @@ export function handlerError(err: any) { w.showErrorMessage("Failed to create project due to reaching maximum number of projects allowed within the free tier.", "Upgrade").then( (res) => { if (res === "Upgrade") { - commands.executeCommand("vscode.open", `${choreoEnvConfig.getBillingUrl()}/cloud/choreo/upgrade`); + commands.executeCommand( + "vscode.open", + `${ext.config?.billingConsoleUrl}/cloud/${extensionName === "Devant" ? "devant" : "choreo"}/upgrade`, + ); } }, ); @@ -95,7 +96,10 @@ export function handlerError(err: any) { "Upgrade", ).then((res) => { if (res === "Upgrade") { - commands.executeCommand("vscode.open", `${choreoEnvConfig.getBillingUrl()}/cloud/choreo/upgrade`); + commands.executeCommand( + "vscode.open", + `${ext.config?.billingConsoleUrl}/cloud/${extensionName === "Devant" ? "devant" : "choreo"}/upgrade`, + ); } }); break; @@ -117,11 +121,10 @@ export function handlerError(err: any) { case ErrorCode.NoAccountAvailable: w.showErrorMessage(`It looks like you don't have an account yet. Please sign up before logging in.`, "Sign Up").then((res) => { if (res === "Sign Up") { - if (extensionName === "Devant") { - commands.executeCommand("vscode.open", `${choreoEnvConfig.getDevantUrl()}/signup`); - } else { - commands.executeCommand("vscode.open", `${choreoEnvConfig.getConsoleUrl()}/signup`); - } + commands.executeCommand( + "vscode.open", + ` ${extensionName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl}/signup`, + ); } }); break; @@ -131,11 +134,7 @@ export function handlerError(err: any) { `Open ${extensionName} Console`, ).then((res) => { if (res === `Open ${extensionName} Console`) { - if (extensionName === "Devant") { - commands.executeCommand("vscode.open", choreoEnvConfig.getDevantUrl()); - } else { - commands.executeCommand("vscode.open", choreoEnvConfig.getConsoleUrl()); - } + commands.executeCommand("vscode.open", extensionName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl); } }); break; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/extension.ts b/workspaces/wso2-platform/wso2-platform-extension/src/extension.ts index 69bdbcfa660..fc41f6841ab 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/extension.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/extension.ts @@ -17,43 +17,48 @@ */ import * as vscode from "vscode"; -import { type ConfigurationChangeEvent, commands, window, workspace } from "vscode"; +import { type ConfigurationChangeEvent, authentication, commands, window, workspace } from "vscode"; +import { WSO2AuthenticationProvider, WSO2_AUTH_PROVIDER_ID } from "./auth/wso2-auth-provider"; import { PlatformExtensionApi } from "./PlatformExtensionApi"; import { ChoreoRPCClient } from "./choreo-rpc"; import { initRPCServer } from "./choreo-rpc/activate"; +import { getCliVersion } from "./choreo-rpc/cli-install"; import { activateCmds } from "./cmds"; import { continueCreateComponent } from "./cmds/create-component-cmd"; import { activateCodeLenses } from "./code-lens"; +import { activateDevantFeatures } from "./devant-utils"; import { ext } from "./extensionVariables"; import { getLogger, initLogger } from "./logger/logger"; +import { activateChoreoMcp } from "./mcp"; import { activateStatusbar } from "./status-bar"; -import { authStore } from "./stores/auth-store"; import { contextStore } from "./stores/context-store"; import { dataCacheStore } from "./stores/data-cache-store"; import { locationStore } from "./stores/location-store"; import { ChoreoConfigurationProvider, addTerminalHandlers } from "./tarminal-handlers"; import { activateTelemetry } from "./telemetry/telemetry"; import { activateURIHandlers } from "./uri-handlers"; +import { getExtVersion } from "./utils"; import { registerYamlLanguageServer } from "./yaml-ls"; export async function activate(context: vscode.ExtensionContext) { activateTelemetry(context); await initLogger(context); - getLogger().debug("Activating WSO2 Platform Extension"); + ext.context = context; ext.api = new PlatformExtensionApi(); - setInitialEnv(); + ext.choreoEnv = getChoreoEnv(); + + getLogger().info("Activating WSO2 Platform Extension"); + getLogger().info(`Extension version: ${getExtVersion(context)}`); + getLogger().info(`CLI version: ${getCliVersion()}`); // Initialize stores - await authStore.persist.rehydrate(); await contextStore.persist.rehydrate(); await dataCacheStore.persist.rehydrate(); await locationStore.persist.rehydrate(); // Set context values - authStore.subscribe(({ state }) => { - vscode.commands.executeCommand("setContext", "isLoggedIn", !!state.userInfo); - }); + // Note: authProvider will be set up below, so we'll subscribe to it in initAuth contextStore.subscribe(({ state }) => { vscode.commands.executeCommand("setContext", "isLoadingContextDirs", state.loading); vscode.commands.executeCommand("setContext", "hasSelectedProject", !!state.selected); @@ -66,53 +71,63 @@ export async function activate(context: vscode.ExtensionContext) { const rpcClient = new ChoreoRPCClient(); ext.clients = { rpcClient: rpcClient }; - initRPCServer() - .then(async () => { - await ext.clients.rpcClient.init(); - authStore.getState().initAuth(); - continueCreateComponent(); - addTerminalHandlers(); - context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("*", new ChoreoConfigurationProvider())); - getLogger().debug("WSO2 Platform Extension activated"); - }) - .catch((e) => { - getLogger().error("Failed to initialize rpc client", e); - }); + // Initialize and register authentication provider + const authProvider = new WSO2AuthenticationProvider(context.secrets); + ext.authProvider = authProvider; + context.subscriptions.push( + authentication.registerAuthenticationProvider(WSO2_AUTH_PROVIDER_ID, "WSO2 Platform", authProvider, { + supportsMultipleAccounts: false, + }), + ); + // Subscribe to auth state changes + authProvider.subscribe(({ state }) => { + vscode.commands.executeCommand("setContext", "isLoggedIn", !!state.userInfo); + }); + + await initRPCServer(); + await ext.clients.rpcClient.init(); + authProvider.getState().initAuth(); + continueCreateComponent(); + if (ext.isChoreoExtInstalled) { + addTerminalHandlers(); + context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("*", new ChoreoConfigurationProvider())); + activateChoreoMcp(context); + } + if (ext.isDevantCloudEditor) { + activateDevantFeatures(); + } + ext.config = await ext.clients.rpcClient.getConfigFromCli(); activateCmds(context); activateURIHandlers(); activateCodeLenses(context); registerPreInitHandlers(); registerYamlLanguageServer(); activateStatusbar(context); + getLogger().debug("WSO2 Platform Extension activated"); return ext.api; } -function setInitialEnv() { - const choreoEnv = process.env.CHOREO_ENV || process.env.CLOUD_ENV; - if ( - choreoEnv && - ["dev", "stage", "prod"].includes(choreoEnv) && - workspace.getConfiguration().get("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment") !== choreoEnv - ) { - workspace.getConfiguration().update("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment", choreoEnv); - } -} +const getChoreoEnv = (): string => { + return ( + process.env.CHOREO_ENV || + process.env.CLOUD_ENV || + workspace.getConfiguration().get("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment") || + "prod" + ); +}; function registerPreInitHandlers(): any { workspace.onDidChangeConfiguration(async ({ affectsConfiguration }: ConfigurationChangeEvent) => { - if ( - affectsConfiguration("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment") || - affectsConfiguration("WSO2.WSO2-Platform.Advanced.RpcPath") || - affectsConfiguration("WSO2.WSO2-Platform.Advanced.StsToken") - ) { + if (affectsConfiguration("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment") || affectsConfiguration("WSO2.WSO2-Platform.Advanced.RpcPath")) { + // skip showing this if cloud sts env is available const selection = await window.showInformationMessage( "WSO2 Platform extension configuration changed. Please restart vscode for changes to take effect.", "Restart Now", ); if (selection === "Restart Now") { if (affectsConfiguration("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment")) { - authStore.getState().logout(); + ext.authProvider?.getState().logout(); } commands.executeCommand("workbench.action.reloadWindow"); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/extensionVariables.ts b/workspaces/wso2-platform/wso2-platform-extension/src/extensionVariables.ts index 31d6b0d9ef5..8002472c69e 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/extensionVariables.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/extensionVariables.ts @@ -16,7 +16,9 @@ * under the License. */ -import type { ExtensionContext, StatusBarItem } from "vscode"; +import type { GetCliRpcResp } from "@wso2/wso2-platform-core"; +import { type ExtensionContext, type StatusBarItem, extensions } from "vscode"; +import type { WSO2AuthenticationProvider } from "./auth/wso2-auth-provider"; import type { PlatformExtensionApi } from "./PlatformExtensionApi"; import type { ChoreoRPCClient } from "./choreo-rpc"; @@ -25,6 +27,17 @@ export class ExtensionVariables { public context!: ExtensionContext; public api!: PlatformExtensionApi; public statusBarItem!: StatusBarItem; + public config?: GetCliRpcResp; + public choreoEnv: string; + public isChoreoExtInstalled: boolean; + public isDevantCloudEditor: boolean; + public authProvider?: WSO2AuthenticationProvider; + + public constructor() { + this.choreoEnv = "prod"; + this.isDevantCloudEditor = !!process.env.CLOUD_STS_TOKEN; + this.isChoreoExtInstalled = !!extensions.getExtension("wso2.choreo"); + } public clients!: { rpcClient: ChoreoRPCClient; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/git/git.ts b/workspaces/wso2-platform/wso2-platform-extension/src/git/git.ts index 40ce0fdf45a..b869afd6a77 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/git/git.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/git/git.ts @@ -414,6 +414,7 @@ const COMMIT_FORMAT = "%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B"; export interface ICloneOptions { readonly parentPath: string; + readonly skipCreateSubPath?: boolean; readonly progress: Progress<{ increment: number }>; readonly recursive?: boolean; readonly ref?: string; @@ -518,7 +519,13 @@ export class Git { }; try { - const command = ["clone", url.includes(" ") ? encodeURI(url) : url, folderPath, "--progress"]; + const command = ["clone", url.includes(" ") ? encodeURI(url) : url]; + if(options.skipCreateSubPath){ + command.push(".") + }else{ + command.push(folderPath) + } + command.push("--progress") if (options.recursive) { command.push("--recursive"); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/mcp.ts b/workspaces/wso2-platform/wso2-platform-extension/src/mcp.ts new file mode 100644 index 00000000000..07713649ddf --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/src/mcp.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as vscode from "vscode"; +import { getChoreoExecPath } from "./choreo-rpc/cli-install"; +import { getUserInfoForCmd } from "./cmds/cmd-utils"; +import { ext } from "./extensionVariables"; + +export function activateChoreoMcp(context: vscode.ExtensionContext) { + const didChangeEmitter = new vscode.EventEmitter(); + context.subscriptions.push( + vscode.lm.registerMcpServerDefinitionProvider("choreo", { + onDidChangeMcpServerDefinitions: didChangeEmitter.event, + provideMcpServerDefinitions: async () => { + const servers: vscode.McpServerDefinition[] = []; + servers.push( + new vscode.McpStdioServerDefinition( + "Choreo MCP Server", + getChoreoExecPath(), + ["start-mcp-server"], + { CHOREO_ENV: ext.choreoEnv, CHOREO_REGION: process.env.CLOUD_REGION || "" }, + "1.0.0", + ), + ); + return servers; + }, + resolveMcpServerDefinition: async (def, _token) => { + return def; + // Uncomment below, if we want to ask user to login when MCP server is started + /* + const userInfo = await getUserInfoForCmd("connect with Choreo MCP server"); + if (userInfo) { + return def; + } + return undefined; + */ + }, + }), + ); +} diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/status-bar.ts b/workspaces/wso2-platform/wso2-platform-extension/src/status-bar.ts index 9c7a9b95a61..fa7baa8059e 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/status-bar.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/status-bar.ts @@ -1,6 +1,6 @@ import { type AuthState, CommandIds, type ContextStoreState, type WebviewState } from "@wso2/wso2-platform-core"; import { type ExtensionContext, StatusBarAlignment, type StatusBarItem, window } from "vscode"; -import { authStore } from "./stores/auth-store"; +import { ext } from "./extensionVariables"; import { contextStore } from "./stores/context-store"; import { webviewStateStore } from "./stores/webview-state-store"; @@ -12,14 +12,14 @@ export function activateStatusbar({ subscriptions }: ExtensionContext) { subscriptions.push(statusBarItem); let webviewState: WebviewState = webviewStateStore.getState()?.state; - let authState: AuthState | null = authStore.getState()?.state; + let authState: AuthState | undefined = ext.authProvider?.getState()?.state; let contextStoreState: ContextStoreState | null = contextStore.getState()?.state; webviewStateStore.subscribe((state) => { webviewState = state.state; updateStatusBarItem(webviewState, authState, contextStoreState); }); - authStore.subscribe((state) => { + ext.authProvider?.subscribe((state) => { authState = state.state; updateStatusBarItem(webviewState, authState, contextStoreState); }); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/auth-store.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/auth-store.ts deleted file mode 100644 index 8801204ea5f..00000000000 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/auth-store.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type { AuthState, Organization, UserInfo } from "@wso2/wso2-platform-core"; -import { createStore } from "zustand"; -import { persist } from "zustand/middleware"; -import { ext } from "../extensionVariables"; -import { contextStore } from "./context-store"; -import { dataCacheStore } from "./data-cache-store"; -import { getGlobalStateStore } from "./store-utils"; - -interface AuthStore { - state: AuthState; - resetState: () => void; - loginSuccess: (userInfo: UserInfo) => void; - logout: () => Promise; - initAuth: () => Promise; -} - -const initialState: AuthState = { userInfo: null }; - -export const authStore = createStore( - persist( - (set, get) => ({ - state: initialState, - resetState: () => set(() => ({ state: initialState })), - loginSuccess: (userInfo) => { - dataCacheStore.getState().setOrgs(userInfo.organizations); - set(({ state }) => ({ state: { ...state, userInfo } })); - contextStore.getState().refreshState(); - }, - logout: async () => { - get().resetState(); - ext.clients.rpcClient.signOut().catch(() => { - // ignore error - }); - }, - initAuth: async () => { - try { - const userInfo = await ext.clients.rpcClient.getUserInfo(); - if (userInfo) { - get().loginSuccess(userInfo); - const contextStoreState = contextStore.getState().state; - if (contextStoreState.selected?.org) { - ext?.clients?.rpcClient?.changeOrgContext(contextStoreState.selected?.org?.id?.toString()); - } - } else { - get().logout(); - } - } catch (err) { - get().logout(); - } - }, - }), - getGlobalStateStore("auth-zustand-storage"), - ), -); - -export const waitForLogin = async (): Promise => { - return new Promise((resolve) => { - authStore.subscribe(({ state }) => { - if (state.userInfo) { - resolve(state.userInfo); - } - }); - }); -}; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/context-store.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/context-store.ts index 527fb108d0e..ff59fef387d 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/context-store.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/context-store.ts @@ -36,8 +36,7 @@ import { createStore } from "zustand"; import { persist } from "zustand/middleware"; import { ext } from "../extensionVariables"; import { getGitRemotes, getGitRoot } from "../git/util"; -import { isSubpath } from "../utils"; -import { authStore } from "./auth-store"; +import { isSamePath, isSubpath } from "../utils"; import { dataCacheStore } from "./data-cache-store"; import { locationStore } from "./location-store"; import { getWorkspaceStateStore } from "./store-utils"; @@ -60,14 +59,15 @@ export const contextStore = createStore( resetState: () => set(() => ({ state: initialState })), refreshState: async () => { try { - if (authStore.getState().state?.userInfo) { + if (ext.authProvider?.getState().state?.userInfo) { set(({ state }) => ({ state: { ...state, loading: true } })); let items = await getAllContexts(get().state?.items); - let selected = getSelected(items, get().state?.selected); + let selected = await getSelected(items, get().state?.selected); + set(({ state }) => ({ state: { ...state, items, selected } })); let components = await getComponentsInfoCache(selected); set(({ state }) => ({ state: { ...state, items, selected, components } })); items = await getEnrichedContexts(get().state?.items); - selected = getSelected(items, selected); + selected = await getSelected(items, selected); components = await getComponentsInfoCache(selected); set(({ state }) => ({ state: { ...state, items, selected, components } })); components = await getComponentsInfo(selected); @@ -152,7 +152,7 @@ const getAllContexts = async (previousItems: { [key: string]: ContextItemEnriche } else if (previousItems?.[key]?.org && previousItems?.[key].project) { contextItems[key] = { ...previousItems?.[key], contextDirs: [contextDir] }; } else { - const userOrgs = authStore.getState().state.userInfo?.organizations; + const userOrgs = ext.authProvider?.getState().state.userInfo?.organizations; const matchingOrg = userOrgs?.find((item) => item.handle === contextItem.org); const projectsOfOrg = dataCacheStore.getState().getProjects(contextItem.org); @@ -195,7 +195,44 @@ const getAllContexts = async (previousItems: { [key: string]: ContextItemEnriche return contextItems; }; -const getSelected = (items: { [key: string]: ContextItemEnriched }, prevSelected?: ContextItemEnriched) => { +const getSelected = async (items: { [key: string]: ContextItemEnriched }, prevSelected?: ContextItemEnriched) => { + if (ext.isDevantCloudEditor && process.env.CLOUD_INITIAL_ORG_ID && process.env.CLOUD_INITIAL_PROJECT_ID) { + // Give priority to project provided as env variable, when running in the cloud editor + const userOrgs = ext.authProvider?.getState().state.userInfo?.organizations; + const matchingOrg = userOrgs?.find( + (item) => item.uuid === process.env.CLOUD_INITIAL_ORG_ID || item.id?.toString() === process.env.CLOUD_INITIAL_ORG_ID, + ); + if (matchingOrg) { + let projectsCache = dataCacheStore.getState().getProjects(matchingOrg.handle); + if (projectsCache.length === 0) { + const projects = await ext.clients.rpcClient.getProjects(matchingOrg.id.toString()); + dataCacheStore.getState().setProjects(matchingOrg.handle, projects); + projectsCache = projects; + } + const matchingProject = projectsCache.find((item) => item.id === process.env.CLOUD_INITIAL_PROJECT_ID); + if (matchingProject) { + return { + orgHandle: matchingOrg.handle, + projectHandle: matchingProject.handler, + org: matchingOrg, + project: matchingProject, + contextDirs: + workspace.workspaceFolders?.map((item) => ({ + workspaceName: item.name, + projectRootFsPath: item.uri.fsPath, + dirFsPath: item.uri.fsPath, + })) ?? [], + } as ContextItemEnriched; + } + } + + const globalCompId: string | null | undefined = ext.context.globalState.get("code-server-component-id"); + if (globalCompId) { + await ext.context.globalState.update("code-server-component-id", null); + await ext.context.workspaceState.update("code-server-component-id", globalCompId); + } + } + let selected: ContextItemEnriched | undefined = undefined; const matchingItem = Object.values(items).find( (item) => @@ -223,7 +260,7 @@ const getSelected = (items: { [key: string]: ContextItemEnriched }, prevSelected }; const getEnrichedContexts = async (items: { [key: string]: ContextItemEnriched }) => { - const userOrgs = authStore.getState().state.userInfo?.organizations; + const userOrgs = ext.authProvider?.getState().state.userInfo?.organizations; const orgsSet = new Set(); Object.values(items).forEach((item) => { @@ -300,9 +337,20 @@ const getComponentsInfo = async (selected?: ContextItemEnriched): Promise { + const workspaceCompId: string | null | undefined = ext.context.workspaceState.get("code-server-component-id") || process.env.SOURCE_COMPONENT_ID; // + if (ext.isDevantCloudEditor && process.env.CLOUD_INITIAL_ORG_ID && process.env.CLOUD_INITIAL_PROJECT_ID && workspaceCompId) { + const filteredComps = components.filter((item) => item.metadata?.id === workspaceCompId); + if (filteredComps.length === 1) { + return filteredComps; + } + } + return components; +}; + const mapComponentList = async (components: ComponentKind[], selected?: ContextItemEnriched): Promise => { const comps: ContextStoreComponentState[] = []; - for (const componentItem of components) { + for (const componentItem of getFilteredComponents(components)) { if (selected?.contextDirs) { // biome-ignore lint/correctness/noUnsafeOptionalChaining: for (const item of selected?.contextDirs) { @@ -324,7 +372,12 @@ const mapComponentList = async (components: ComponentKind[], selected?: ContextI if (hasMatchingRemote) { const subPathDir = path.join(gitRoot, getComponentKindRepoSource(componentItem.spec.source)?.path); const isSubPath = isSubpath(item.dirFsPath, subPathDir); - if (isSubPath && existsSync(subPathDir) && !comps.some((item) => item.component?.metadata?.id === componentItem.metadata?.id)) { + const isPathSame = isSamePath(item.dirFsPath, subPathDir); + if ( + (isPathSame || isSubPath) && + existsSync(subPathDir) && + !comps.some((item) => item.component?.metadata?.id === componentItem.metadata?.id) + ) { comps.push({ component: componentItem, workspaceName: item.workspaceName, diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/data-cache-store.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/data-cache-store.ts index 3751dfecb86..c4e35b6b9a7 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/data-cache-store.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/data-cache-store.ts @@ -19,6 +19,7 @@ import type { CommitHistory, ComponentKind, DataCacheState, Environment, Organization, Project } from "@wso2/wso2-platform-core"; import { createStore } from "zustand"; import { persist } from "zustand/middleware"; +import { ext } from "../extensionVariables"; import { getGlobalStateStore } from "./store-utils"; interface DataCacheStore { @@ -54,14 +55,14 @@ export const dataCacheStore = createStore( } = {}; projects.forEach((item) => { updatedProjects[item.handler] = { - components: get().state?.orgs?.[orgHandle]?.projects?.[item.handler]?.components || {}, + components: get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[item.handler]?.components || {}, data: item, }; }); const updatedOrgs = { ...(get().state?.orgs ?? {}), - [orgHandle]: { ...(get().state?.orgs?.[orgHandle] ?? {}), projects: updatedProjects }, + [getRootKey(orgHandle)]: { ...(get().state?.orgs?.[getRootKey(orgHandle)] ?? {}), projects: updatedProjects }, }; set(({ state }) => ({ state: { ...state, orgs: updatedOrgs } })); @@ -72,11 +73,11 @@ export const dataCacheStore = createStore( ...state, orgs: { ...(get().state?.orgs ?? {}), - [orgHandle]: { - ...(get().state?.orgs?.[orgHandle] ?? {}), + [getRootKey(orgHandle)]: { + ...(get().state?.orgs?.[getRootKey(orgHandle)] ?? {}), projects: { - ...(get().state?.orgs?.[orgHandle]?.projects ?? {}), - [projectHandle]: { ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle] ?? {}), envs }, + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects ?? {}), + [projectHandle]: { ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle] ?? {}), envs }, }, }, }, @@ -84,17 +85,17 @@ export const dataCacheStore = createStore( })); }, getEnvs: (orgHandle, projectHandle) => { - return get().state.orgs?.[orgHandle]?.projects?.[projectHandle]?.envs || []; + return get().state.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.envs || []; }, getProjects: (orgHandle) => { - const projectList = Object.values(get().state.orgs?.[orgHandle]?.projects ?? {}) + const projectList = Object.values(get().state.orgs?.[getRootKey(orgHandle)]?.projects ?? {}) .filter((item) => item.data) .map((item) => item.data); return projectList as Project[]; }, setComponents: (orgHandle, projectHandle, components) => { const newComponents: { [componentHandle: string]: { data?: ComponentKind; commits?: { [branch: string]: CommitHistory[] } } } = {}; - const prevComponents = get().state.orgs?.[orgHandle]?.projects?.[projectHandle]?.components ?? {}; + const prevComponents = get().state.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components ?? {}; components.forEach((item) => { const matchingItem = prevComponents[item.metadata.name]; newComponents[item.metadata.name] = { ...matchingItem, data: item }; @@ -102,12 +103,12 @@ export const dataCacheStore = createStore( const updatedOrgs = { ...(get().state?.orgs ?? {}), - [orgHandle]: { - ...(get().state?.orgs?.[orgHandle] ?? {}), + [getRootKey(orgHandle)]: { + ...(get().state?.orgs?.[getRootKey(orgHandle)] ?? {}), projects: { - ...(get().state?.orgs?.[orgHandle]?.projects ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects ?? {}), [projectHandle]: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle] ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle] ?? {}), components: newComponents, }, }, @@ -117,7 +118,7 @@ export const dataCacheStore = createStore( set(({ state }) => ({ state: { ...state, orgs: updatedOrgs } })); }, getComponents: (orgHandle, projectHandle) => { - const componentList = Object.values(get().state.orgs?.[orgHandle]?.projects?.[projectHandle]?.components ?? {}) + const componentList = Object.values(get().state.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components ?? {}) .filter((item) => item.data) .map((item) => item.data); return componentList as ComponentKind[]; @@ -125,18 +126,18 @@ export const dataCacheStore = createStore( setCommits: (orgHandle, projectHandle, componentHandle, branch, commits) => { const updatedOrgs = { ...(get().state?.orgs ?? {}), - [orgHandle]: { - ...(get().state?.orgs?.[orgHandle] ?? {}), + [getRootKey(orgHandle)]: { + ...(get().state?.orgs?.[getRootKey(orgHandle)] ?? {}), projects: { - ...(get().state?.orgs?.[orgHandle]?.projects ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects ?? {}), [projectHandle]: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle] ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle] ?? {}), components: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle]?.components ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components ?? {}), [componentHandle]: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle]?.components?.[componentHandle] ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components?.[componentHandle] ?? {}), commits: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle]?.components?.[componentHandle]?.commits ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components?.[componentHandle]?.commits ?? {}), [branch]: commits, }, }, @@ -149,10 +150,21 @@ export const dataCacheStore = createStore( set(({ state }) => ({ state: { ...state, orgs: updatedOrgs } })); }, getCommits: (orgHandle, projectHandle, componentHandle, branch) => { - const commitList = get().state.orgs?.[orgHandle]?.projects?.[projectHandle]?.components?.[componentHandle]?.commits?.[branch] ?? []; + const commitList = + get().state.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components?.[componentHandle]?.commits?.[branch] ?? []; return commitList; }, }), - getGlobalStateStore("data-cache-zustand-storage-v1"), + getGlobalStateStore("data-cache-zustand-storage"), ), ); + +const getRootKey = (orgHandle: string) => { + const region = ext.authProvider?.getState().state.region; + const env = ext.choreoEnv; + let orgRegionHandle = `${region}-${orgHandle}`; + if (env !== "prod") { + orgRegionHandle = `${env}-${orgRegionHandle}`; + } + return orgRegionHandle; +}; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/location-store.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/location-store.ts index 831fa63881b..d68fc6776da 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/location-store.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/location-store.ts @@ -72,6 +72,6 @@ export const locationStore = createStore( .filter((item) => existsSync(item.fsPath)); }, }), - getGlobalStateStore("location-zustand-storage-v2"), + getGlobalStateStore("location-zustand-storage"), ), ); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/store-utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/store-utils.ts index 5c2da0bae69..8d3a7b97607 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/store-utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/store-utils.ts @@ -19,9 +19,11 @@ import { type PersistOptions, createJSONStorage } from "zustand/middleware"; import { ext } from "../extensionVariables"; +const version = "v4"; + export const getGlobalStateStore = (storeName: string): PersistOptions => { return { - name: storeName, + name: `${storeName}-${version}`, storage: createJSONStorage(() => ({ getItem: async (name) => { const value = await ext.context.globalState.get(name); @@ -36,7 +38,7 @@ export const getGlobalStateStore = (storeName: string): PersistOptions export const getWorkspaceStateStore = (storeName: string): PersistOptions => { return { - name: storeName, + name: `${storeName}-${version}`, storage: createJSONStorage(() => ({ getItem: async (name) => { const value = await ext.context.workspaceState.get(name); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/tarminal-handlers.ts b/workspaces/wso2-platform/wso2-platform-extension/src/tarminal-handlers.ts index dd0856dd0dd..f1763451667 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/tarminal-handlers.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/tarminal-handlers.ts @@ -20,16 +20,15 @@ import { CommandIds, type ComponentKind } from "@wso2/wso2-platform-core"; import type vscode from "vscode"; import { commands, window, workspace } from "vscode"; import { getChoreoExecPath } from "./choreo-rpc/cli-install"; -import { authStore } from "./stores/auth-store"; import { contextStore } from "./stores/context-store"; -import { dataCacheStore } from "./stores/data-cache-store"; import { delay, getSubPath } from "./utils"; +import { ext } from "./extensionVariables"; export class ChoreoConfigurationProvider implements vscode.DebugConfigurationProvider { resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration): vscode.DebugConfiguration | undefined { - if (config.request === "launch" && (config.choreo === true || typeof config.choreo === "object")) { + if (config.request === "launch" && (config.choreo === true || typeof config.choreo === "object" || config.choreoConnect === true || typeof config.choreoConnect === "object")) { config.console = "integratedTerminal"; - const choreoConfig: { project?: string; component?: string; env?: string; skipConnection?: string[] } | true = config.choreo; + const choreoConfig: { project?: string; component?: string; env?: string; skipConnection?: string[] } | true = config.choreo || config.choreoConnect; let connectCmd = "connect"; if (choreoConfig === true) { if (contextStore.getState().state?.selected?.projectHandle) { @@ -65,7 +64,7 @@ export function addTerminalHandlers() { let cliCommand = e.name.split("[choreo-shell]").pop()?.replaceAll(")", ""); const terminalPath = (e.creationOptions as any)?.cwd; const rpcPath = getChoreoExecPath(); - const userInfo = authStore.getState().state?.userInfo; + const userInfo = ext.authProvider?.getState().state?.userInfo; if (terminalPath) { if (!e.name?.includes("--project")) { window diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/telemetry/utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/telemetry/utils.ts index 35b4aa7a182..8a5f79e8407 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/telemetry/utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/telemetry/utils.ts @@ -16,7 +16,7 @@ * under the License. */ -import { authStore } from "../stores/auth-store"; +import { ext } from "../extensionVariables"; import { getTelemetryReporter } from "./telemetry"; // export async function sendProjectTelemetryEvent(eventName: string, properties?: { [key: string]: string; }, measurements?: { [key: string]: number; }) { @@ -46,8 +46,8 @@ export function sendTelemetryException(error: Error, properties?: { [key: string // Create common properties for all events export function getCommonProperties(): { [key: string]: string } { return { - idpId: authStore.getState().state?.userInfo?.userId!, + idpId: ext.authProvider?.getState().state?.userInfo?.userId ?? "", // check if the email ends with "@wso2.com" - isWSO2User: authStore.getState().state?.userInfo?.userEmail?.endsWith("@wso2.com") ? "true" : "false", + isWSO2User: ext.authProvider?.getState().state?.userInfo?.userEmail?.endsWith("@wso2.com") ? "true" : "false", }; } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts b/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts index 01b4c5564a0..3aa77705b58 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts @@ -23,6 +23,7 @@ import { type ICloneProjectCmdParams, type Organization, type Project, + type UserInfo, getComponentKindRepoSource, type openClonedDirReq, parseGitURL, @@ -32,16 +33,14 @@ import { ResponseError } from "vscode-jsonrpc"; import { ErrorCode } from "./choreo-rpc/constants"; import { getUserInfoForCmd, isRpcActive } from "./cmds/cmd-utils"; import { updateContextFile } from "./cmds/create-directory-context-cmd"; -import { choreoEnvConfig } from "./config"; import { ext } from "./extensionVariables"; import { getGitRemotes, getGitRoot } from "./git/util"; import { getLogger } from "./logger/logger"; -import { authStore } from "./stores/auth-store"; import { contextStore, getContextKey, waitForContextStoreToLoad } from "./stores/context-store"; import { dataCacheStore } from "./stores/data-cache-store"; import { locationStore } from "./stores/location-store"; import { webviewStateStore } from "./stores/webview-state-store"; -import { delay, isSamePath, openDirectory } from "./utils"; +import { isSamePath, openDirectory } from "./utils"; export function activateURIHandlers() { window.registerUriHandler({ @@ -68,9 +67,12 @@ export function activateURIHandlers() { async () => { try { const orgId = contextStore?.getState().state?.selected?.org?.id?.toString(); - const callbackUrl = extName === "Devant" ? `${choreoEnvConfig.getDevantUrl()}/vscode-auth` : undefined; - const clientId = extName === "Devant" ? choreoEnvConfig.getDevantAsgardeoClientId() : undefined; - const userInfo = await ext.clients.rpcClient.signInWithAuthCode(authCode, region, orgId, callbackUrl, clientId); + let userInfo: UserInfo | undefined; + if (extName === "Devant") { + userInfo = await ext.clients.rpcClient.signInDevantWithAuthCode(authCode, region, orgId); + } else { + userInfo = await ext.clients.rpcClient.signInWithAuthCode(authCode, region, orgId); + } if (userInfo) { if (contextStore?.getState().state?.selected) { const includesOrg = userInfo.organizations?.some((item) => item.handle === contextStore?.getState().state?.selected?.orgHandle); @@ -78,8 +80,9 @@ export function activateURIHandlers() { contextStore.getState().resetState(); } } - authStore.getState().loginSuccess(userInfo); - window.showInformationMessage(`Successfully signed into ${extName}`); + const region = await ext.clients.rpcClient.getCurrentRegion(); + await ext.authProvider?.getState().loginSuccess(userInfo, region); + window.showInformationMessage(`Successfully signed into ${extName}`); } } catch (error: any) { if (!(error instanceof ResponseError) || ![ErrorCode.NoOrgsAvailable, ErrorCode.NoAccountAvailable].includes(error.code)) { @@ -100,7 +103,7 @@ export function activateURIHandlers() { } else if (uri.path === "/ghapp") { try { isRpcActive(ext); - getLogger().info("WSO2 Platform Githup auth Callback hit"); + getLogger().info("WSO2 Platform Github auth Callback hit"); const urlParams = new URLSearchParams(uri.query); const authCode = urlParams.get("code"); // const installationId = urlParams.get("installationId"); @@ -261,7 +264,12 @@ const switchContextAndOpenDir = async (selectedPath: string, org: Organization, return; } const projectCache = dataCacheStore.getState().getProjects(org?.handle); - const contextFilePath = updateContextFile(gitRoot, authStore.getState().state.userInfo!, project, org, projectCache); + const userInfo = ext.authProvider?.getState().state.userInfo; + if (!userInfo) { + window.showErrorMessage("User information is not available. Please sign in and try again."); + return; + } + const contextFilePath = updateContextFile(gitRoot, userInfo, project, org, projectCache); const isWithinWorkspace = workspace.workspaceFolders?.some((item) => isSamePath(item.uri?.fsPath, selectedPath)); if (isWithinWorkspace) { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts index 962d0988175..6fe5a7b6682 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts @@ -196,8 +196,16 @@ export const saveFile = async ( }; export const isSamePath = (parent: string, sub: string): boolean => { - const normalizedParent = getNormalizedPath(parent).toLowerCase(); - const normalizedSub = getNormalizedPath(sub).toLowerCase(); + let normalizedParent = getNormalizedPath(parent).toLowerCase(); + if (normalizedParent.endsWith("/")) { + normalizedParent = normalizedParent.slice(0, -1); + } + + let normalizedSub = getNormalizedPath(sub).toLowerCase(); + if (normalizedSub.endsWith("/")) { + normalizedSub = normalizedSub.slice(0, -1); + } + if (normalizedParent === normalizedSub) { return true; } @@ -205,8 +213,16 @@ export const isSamePath = (parent: string, sub: string): boolean => { }; export const isSubpath = (parent: string, sub: string): boolean => { - const normalizedParent = getNormalizedPath(parent).toLowerCase(); - const normalizedSub = getNormalizedPath(sub).toLowerCase(); + let normalizedParent = getNormalizedPath(parent).toLowerCase(); + if (normalizedParent.endsWith("/")) { + normalizedParent = normalizedParent.slice(0, -1); + } + + let normalizedSub = getNormalizedPath(sub).toLowerCase(); + if (normalizedSub.endsWith("/")) { + normalizedSub = normalizedSub.slice(0, -1); + } + if (normalizedParent === normalizedSub) { return true; } @@ -413,3 +429,16 @@ export const getConfigFileDrifts = async ( return []; } }; + +export const parseJwt = (token: string): { iss: string } | null => { + try { + return JSON.parse(atob(token.split(".")[1])); + } catch (e) { + return null; + } +}; + +export const getExtVersion = (context: ExtensionContext): string => { + const packageJson = JSON.parse(readFileSync(path.join(context?.extensionPath, "package.json"), "utf8")); + return packageJson?.version; +}; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentDetailsView.ts b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentDetailsView.ts index 4fd5fd80195..d93bbe57507 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentDetailsView.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentDetailsView.ts @@ -32,10 +32,25 @@ class ComponentDetailsView { private _disposables: vscode.Disposable[] = []; private _rpcHandler: WebViewPanelRpc; - constructor(extensionUri: vscode.Uri, organization: Organization, project: Project, component: ComponentKind, directoryFsPath?: string) { + constructor( + extensionUri: vscode.Uri, + organization: Organization, + project: Project, + component: ComponentKind, + directoryFsPath?: string, + isNewComponent?: boolean, + ) { this._panel = ComponentDetailsView.createWebview(component); this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri, organization, project, component, directoryFsPath); + this._panel.webview.html = this._getWebviewContent( + this._panel.webview, + extensionUri, + organization, + project, + component, + directoryFsPath, + isNewComponent, + ); this._rpcHandler = new WebViewPanelRpc(this._panel); } @@ -67,6 +82,7 @@ class ComponentDetailsView { project: Project, component: ComponentKind, directoryFsPath?: string, + isNewComponent?: boolean, ) { // The JS file from the React build output const scriptUri = getUri(webview, extensionUri, ["resources", "jslibs", "main.js"]); @@ -99,6 +115,7 @@ class ComponentDetailsView { project, component, initialEnvs: dataCacheStore.getState().getEnvs(organization.handle, project.handler), + isNewComponent, } as WebviewProps)} ); } @@ -142,6 +159,7 @@ export const showComponentDetailsView = ( component: ComponentKind, directoryFsPath: string, viewColumn?: vscode.ViewColumn, + isNewComponent?: boolean, ) => { const webView = getComponentDetailsView(org.handle, project.handler, component.metadata.name); const componentKey = getComponentKey(org, project, component); @@ -150,7 +168,7 @@ export const showComponentDetailsView = ( webView?.reveal(viewColumn); } else { webviewStateStore.getState().onCloseComponentDrawer(getComponentKey(org, project, component)); - const componentDetailsView = new ComponentDetailsView(ext.context.extensionUri, org, project, component, directoryFsPath); + const componentDetailsView = new ComponentDetailsView(ext.context.extensionUri, org, project, component, directoryFsPath, isNewComponent); componentDetailsView.getWebview()?.reveal(viewColumn); componentViewMap.set(componentKey, componentDetailsView); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentFormView.ts b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentFormView.ts index 080786b8e50..782ca77d959 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentFormView.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentFormView.ts @@ -43,7 +43,7 @@ export class ComponentFormView { const extName = webviewStateStore.getState().state?.extensionName; const panel = vscode.window.createWebviewPanel( "create-new-component", - extName === "Devant" ? "Create Integration" : "Create Component", + extName === "Devant" ? "Deploy Integration" : "Create Component", vscode.ViewColumn.One, { enableScripts: true, diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts index ef5b696cb5e..3e181a7a8ea 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts @@ -16,11 +16,29 @@ * under the License. */ -import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs"; +import { + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + rmdirSync, + statSync, + unlinkSync, + writeFileSync, +} from "fs"; +import * as fs from "fs"; +import * as os from "os"; import { join } from "path"; +import * as toml from "@iarna/toml"; import { AuthStoreChangedNotification, ClearWebviewCache, + CloneRepositoryIntoCompDir, + type CloneRepositoryIntoCompDirReq, CloseComponentViewDrawer, CloseWebViewNotification, type CommitHistory, @@ -46,6 +64,7 @@ import { GetLocalGitData, GetSubPath, GetWebviewStoreState, + GitProvider, GoToSource, HasDirtyLocalGitRepo, JoinFsFilePaths, @@ -84,24 +103,26 @@ import { WebviewNotificationsMethodList, type WebviewQuickPickItem, WebviewStateChangedNotification, + buildGitURL, deepEqual, getShortenedHash, makeURLSafe, + parseGitURL, } from "@wso2/wso2-platform-core"; import * as yaml from "js-yaml"; -import { ProgressLocation, QuickPickItemKind, Uri, type WebviewPanel, type WebviewView, commands, env, window } from "vscode"; +import { ProgressLocation, QuickPickItemKind, Uri, type WebviewPanel, type WebviewView, commands, env, window, workspace } from "vscode"; import * as vscode from "vscode"; import { Messenger } from "vscode-messenger"; import { BROADCAST } from "vscode-messenger-common"; import { registerChoreoRpcResolver } from "../choreo-rpc"; -import { getChoreoEnv, getChoreoExecPath } from "../choreo-rpc/cli-install"; +import { getChoreoExecPath } from "../choreo-rpc/cli-install"; import { quickPickWithLoader } from "../cmds/cmd-utils"; +import { enrichGitUsernamePassword } from "../cmds/commit-and-push-to-git-cmd"; import { submitCreateComponentHandler } from "../cmds/create-component-cmd"; -import { choreoEnvConfig } from "../config"; import { ext } from "../extensionVariables"; +import { initGit } from "../git/main"; import { getGitHead, getGitRemotes, getGitRoot, hasDirtyRepo, removeCredentialsFromGitURL } from "../git/util"; import { getLogger } from "../logger/logger"; -import { authStore } from "../stores/auth-store"; import { contextStore } from "../stores/context-store"; import { dataCacheStore } from "../stores/data-cache-store"; import { webviewStateStore } from "../stores/webview-state-store"; @@ -110,11 +131,11 @@ import { getConfigFileDrifts, getNormalizedPath, getSubPath, goTosource, readLoc // Register handlers function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | WebviewView) { - authStore.subscribe((store) => messenger.sendNotification(AuthStoreChangedNotification, BROADCAST, store.state)); + ext.authProvider?.subscribe((store) => messenger.sendNotification(AuthStoreChangedNotification, BROADCAST, store.state)); webviewStateStore.subscribe((store) => messenger.sendNotification(WebviewStateChangedNotification, BROADCAST, store.state)); contextStore.subscribe((store) => messenger.sendNotification(ContextStoreChangedNotification, BROADCAST, store.state)); - messenger.onRequest(GetAuthState, () => authStore.getState().state); + messenger.onRequest(GetAuthState, () => ext.authProvider?.getState().state); messenger.onRequest(GetWebviewStoreState, async () => webviewStateStore.getState().state); messenger.onRequest(GetContextState, async () => contextStore.getState().state); @@ -168,11 +189,14 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W vscode.env.openExternal(vscode.Uri.parse(url)); }); messenger.onRequest(OpenExternalChoreo, (choreoPath: string) => { - if (webviewStateStore.getState().state.extensionName === "Devant") { - vscode.env.openExternal(vscode.Uri.joinPath(vscode.Uri.parse(choreoEnvConfig.getDevantUrl()), choreoPath)); - } else { - vscode.env.openExternal(vscode.Uri.joinPath(vscode.Uri.parse(choreoEnvConfig.getConsoleUrl()), choreoPath)); - } + vscode.env.openExternal( + vscode.Uri.joinPath( + vscode.Uri.parse( + (webviewStateStore.getState().state.extensionName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl) || "", + ), + choreoPath, + ), + ); }); messenger.onRequest(SetWebviewCache, async (params: { cacheKey: string; data: any }) => { await ext.context.workspaceState.update(params.cacheKey, params.data); @@ -250,7 +274,7 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W async (params: { orgName: string; projectName: string; componentName: string; deploymentTrackName: string; envName: string; type: string }) => { const { orgName, projectName, componentName, deploymentTrackName, envName, type } = params; // todo: export the env from here - if (getChoreoEnv() !== "prod") { + if (ext.choreoEnv !== "prod") { window.showErrorMessage( "Choreo extension currently displays runtime logs is only if 'WSO2.Platform.Advanced.ChoreoEnvironment' is set to 'prod'", ); @@ -271,16 +295,16 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W return Buffer.from(JSON.stringify(state), "binary").toString("base64"); }; messenger.onRequest(TriggerGithubAuthFlow, async (orgId: string) => { - const { authUrl, clientId, redirectUrl, devantRedirectUrl } = choreoEnvConfig.getGHAppConfig(); const extName = webviewStateStore.getState().state.extensionName; + const baseUrl = extName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl; + const redirectUrl = `${baseUrl}/ghapp`; const state = await _getGithubUrlState(orgId); - const ghURL = Uri.parse(`${authUrl}?redirect_uri=${extName === "Devant" ? devantRedirectUrl : redirectUrl}&client_id=${clientId}&state=${state}`); + const ghURL = Uri.parse(`${ext.config?.ghApp.authUrl}?redirect_uri=${redirectUrl}&client_id=${ext.config?.ghApp.clientId}&state=${state}`); await env.openExternal(ghURL); }); messenger.onRequest(TriggerGithubInstallFlow, async (orgId: string) => { - const { installUrl } = choreoEnvConfig.getGHAppConfig(); const state = await _getGithubUrlState(orgId); - const ghURL = Uri.parse(`${installUrl}?state=${state}`); + const ghURL = Uri.parse(`${ext.config?.ghApp.installUrl}?state=${state}`); await env.openExternal(ghURL); }); messenger.onRequest(SubmitComponentCreate, submitCreateComponentHandler); @@ -374,7 +398,7 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W rmSync(join(params.componentDir, ".choreo", "component-config.yaml")); } - const org = authStore?.getState().state?.userInfo?.organizations?.find((item) => item.uuid === params.marketplaceItem?.organizationId); + const org = ext.authProvider?.getState().state?.userInfo?.organizations?.find((item) => item.uuid === params.marketplaceItem?.organizationId); if (!org) { return; } @@ -552,6 +576,102 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W messenger.onRequest(GetConfigFileDrifts, async (params: GetConfigFileDriftsReq) => { return getConfigFileDrifts(params.type, params.repoUrl, params.branch, params.repoDir, ext.context); }); + messenger.onRequest(CloneRepositoryIntoCompDir, async (params: CloneRepositoryIntoCompDirReq) => { + const extName = webviewStateStore.getState().state.extensionName; + const newGit = await initGit(ext.context); + if (!newGit) { + throw new Error("failed to retrieve Git details"); + } + const _repoUrl = buildGitURL(params.repo.orgHandler, params.repo.repo, params.repo.provider, true, params.repo.serverUrl); + if (!_repoUrl || !_repoUrl.startsWith("https://")) { + throw new Error("failed to parse git details"); + } + const urlObj = new URL(_repoUrl); + + const parsed = parseGitURL(_repoUrl); + if (parsed) { + const [repoOrg, repoName, provider] = parsed; + await enrichGitUsernamePassword(params.org, repoOrg, repoName, provider, urlObj, _repoUrl, params.repo.secretRef || ""); + } + + const repoUrl = urlObj.href; + + // if ballerina toml exists, need to update the org and name + const balTomlPath = join(params.cwd, "Ballerina.toml"); + if (existsSync(balTomlPath)) { + const fileContent = await fs.promises.readFile(balTomlPath, "utf-8"); + const parsedToml: any = toml.parse(fileContent); + if (parsedToml?.package) { + parsedToml.package.org = params.org.handle; + parsedToml.package.name = params.componentName?.replaceAll("-", "_"); + } + const updatedTomlContent = toml.stringify(parsedToml); + await fs.promises.writeFile(balTomlPath, updatedTomlContent, "utf-8"); + } + + // TODO: Enable this after fixing component creation from root + /* + if (params.repo?.isBareRepo && ["", "/", "."].includes(params.subpath)) { + // if component is to be created in the root of a bare repo, + // then we can initialize the current directory as the repo root + await window.withProgress({ title: `Initializing currently opened directory as repository...`, location: ProgressLocation.Notification }, async () => { + await newGit.init(params.cwd); + const dotGit = await newGit?.getRepositoryDotGit(params.cwd); + const repo = newGit.open(params.cwd, dotGit); + await repo.addRemote("origin", repoUrl); + await repo.add(["."]); + await repo.commit(`Add source for new ${extName} ${extName === "Devant" ? "Integration" : "Component"} (${params.componentName})`); + const headRef = await repo.getHEADRef() + await repo.push("origin", headRef?.name); + }); + return params.cwd; + } + */ + + const clonedPath = await window.withProgress( + { + title: `Cloning repository ${params.repo?.orgHandler}/${params.repo.repo}`, + location: ProgressLocation.Notification, + }, + async (progress, cancellationToken) => + newGit.clone( + repoUrl, + { + recursive: true, + ref: params.repo.branch, + parentPath: join(params.cwd, ".."), + progress: { + report: ({ increment, ...rest }: { increment: number }) => progress.report({ increment: increment, ...rest }), + }, + }, + cancellationToken, + ), + ); + + // Move everything into cloned dir + const cwdFiled = readdirSync(params.cwd); + const newPath = join(clonedPath, params.subpath); + fs.mkdirSync(newPath, { recursive: true }); + + for (const file of cwdFiled) { + const cwdFilePath = join(params.cwd, file); + const destFilePath = join(newPath, file); + fs.cpSync(cwdFilePath, destFilePath, { recursive: true }); + } + + const repoRoot = await newGit?.getRepositoryRoot(newPath); + const dotGit = await newGit?.getRepositoryDotGit(newPath); + const repo = newGit.open(repoRoot, dotGit); + + await window.withProgress({ title: "Pushing the changes to your remote repository...", location: ProgressLocation.Notification }, async () => { + await repo.add(["."]); + await repo.commit(`Add source for new ${extName} ${extName === "Devant" ? "Integration" : "Component"} (${params.componentName})`); + const headRef = await repo.getHEADRef(); + await repo.push(headRef?.upstream?.remote || "origin", headRef?.name || params.repo.branch); + }); + + return newPath; + }); // Register Choreo CLL RPC handler registerChoreoRpcResolver(messenger, ext.clients.rpcClient); diff --git a/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js b/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js index 0dbaf0a01bb..0f28ec42806 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js +++ b/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js @@ -22,18 +22,19 @@ const CopyPlugin = require("copy-webpack-plugin"); const PermissionsOutputPlugin = require("webpack-permissions-plugin"); const webpack = require("webpack"); const dotenv = require("dotenv"); -const { createEnvDefinePlugin } = require('../../../common/scripts/env-webpack-helper'); +const { createEnvDefinePlugin } = require("../../../common/scripts/env-webpack-helper"); const envPath = path.resolve(__dirname, ".env"); const env = dotenv.config({ path: envPath }).parsed; + console.log("Fetching values for environment variables..."); const { envKeys, missingVars } = createEnvDefinePlugin(env); if (missingVars.length > 0) { - console.warn( - '\n⚠️ Environment Variable Configuration Warning:\n' + - `Missing required environment variables: ${missingVars.join(', ')}\n` + - `Please provide values in either .env file or runtime environment.\n` - ); + console.warn( + `\n⚠️ Environment Variable Configuration Warning:\n + Missing required environment variables: ${missingVars.join(", ")}\n + Please provide values in either .env file or runtime environment.\n`, + ); } //@ts-check @@ -81,7 +82,7 @@ const extensionConfig = { }, ], }, - devtool: !process.env.CI ? "source-map" : undefined, + devtool: "source-map", infrastructureLogging: { level: "log", }, diff --git a/workspaces/wso2-platform/wso2-platform-webviews/package.json b/workspaces/wso2-platform/wso2-platform-webviews/package.json index fd3dc000375..bf5851dde03 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/package.json +++ b/workspaces/wso2-platform/wso2-platform-webviews/package.json @@ -31,12 +31,12 @@ "clipboardy": "^4.0.0", "@formkit/auto-animate": "0.8.2", "timezone-support": "^3.1.0", - "swagger-ui-react": "^5.22.0", + "swagger-ui-react": "5.22.0", "@biomejs/biome": "^1.9.4", "@headlessui/react": "^2.1.2", - "react-markdown": "^7.1.0", - "rehype-raw": "^6.1.0", - "remark-gfm": "^4.0.1", + "react-markdown": "10.1.0", + "rehype-raw": "7.0.0", + "remark-gfm": "4.0.1", "prism-react-renderer": "^2.4.1", "lodash.debounce": "~4.0.8", "js-yaml": "^4.1.1" diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/BreadCrumb/BreadCrumb.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/BreadCrumb/BreadCrumb.tsx index f261ed1e39e..374ceb6a036 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/BreadCrumb/BreadCrumb.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/BreadCrumb/BreadCrumb.tsx @@ -33,16 +33,16 @@ export const BreadCrumb: FC = ({ items }) => { for (const [index, item] of items.entries()) { if (item.onClick) { nodes.push( - + {item.label} , ); } else { - nodes.push({item.label}); + nodes.push({item.label}); } if (index + 1 < items.length) { - nodes.push(/); + nodes.push(/); } } return
{nodes}
; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/Connections/CreateConnection/CreateConnection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/Connections/CreateConnection/CreateConnection.tsx index e3be1465d65..bd4a125d7e8 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/Connections/CreateConnection/CreateConnection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/Connections/CreateConnection/CreateConnection.tsx @@ -90,7 +90,7 @@ const getInitialVisibility = (visibilities: string[] = []) => { if (visibilities.includes(ServiceInfoVisibilityEnum.Organization)) { return ServiceInfoVisibilityEnum.Organization; } - return; + return ""; }; const getPossibleSchemas = (selectedVisibility: string, connectionSchemas: MarketplaceItemSchema[] = []) => { diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/FormElements/Dropdown/Dropdown.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/FormElements/Dropdown/Dropdown.tsx index 0e4740fe05f..3400766f102 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/FormElements/Dropdown/Dropdown.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/FormElements/Dropdown/Dropdown.tsx @@ -28,18 +28,19 @@ interface Props { required?: boolean; loading?: boolean; control?: Control; - items?: ({ value: string; label?: string } | string)[]; + items?: ({ value: string; label?: string; type?: "separator" } | string)[]; disabled?: boolean; wrapClassName?: HTMLProps["className"]; + onChange?: ((e: Event) => unknown) & React.FormEventHandler; } export const Dropdown: FC = (props) => { - const { label, required, items, loading, control, name, disabled, wrapClassName } = props; + const { label, required, items, loading, control, name, disabled, wrapClassName, onChange: onChangeRoot } = props; return ( ( + render={({ field: { onChange, ...restFields }, fieldState }) => ( = (props) => { - {items?.map((item) => ( - - {typeof item === "string" ? item : item?.label || item.value} - + {items?.map((item, index) => ( + <> + {typeof item !== "string" && item.type === "separator" ? ( + + ) : ( + + {typeof item === "string" ? item : item?.label || item.value} + + )} + ))} diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/Markdown/Markdown.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/Markdown/Markdown.tsx index 6322c87fb83..1eeca98d067 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/Markdown/Markdown.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/Markdown/Markdown.tsx @@ -82,7 +82,8 @@ const markDownOverrides: { [key: string]: FC> } = {
{children}
), // TODO: move into separate component - code: ({ inline, children, className }) => { + code: ({ children, className, node }) => { + const isInline = !className && node?.position?.end?.line === node?.position?.start?.line // Extract language from className const match = /language-(\w+)/.exec(className || ""); const language: any = match != null ? match[1] : "markdown"; @@ -92,7 +93,7 @@ const markDownOverrides: { [key: string]: FC> } = { mutationFn: (text: string) => clipboardy.write(text), onSuccess: () => ChoreoWebViewAPI.getInstance().showInfoMsg("Code has been copied to the clipboard."), }); - if (inline) { + if (isInline) { return {children}; } @@ -216,7 +217,7 @@ const markDownOverrides: { [key: string]: FC> } = { export const Markdown: FC = ({ children }) => { return ( - + {children} ); diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/SwaggerUI/SwaggerUI.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/SwaggerUI/SwaggerUI.tsx index f6e6fb1b45e..0aa9802ca2c 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/SwaggerUI/SwaggerUI.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/SwaggerUI/SwaggerUI.tsx @@ -18,7 +18,7 @@ import React, { type HTMLProps, type FC } from "react"; import SwaggerUIReact from "swagger-ui-react"; -import "@wso2/ui-toolkit/src/styles/swagger/main.scss"; +import "@wso2/ui-toolkit/src/styles/swagger/styles.css"; import classNames from "classnames"; import type SwaggerUIProps from "swagger-ui-react/swagger-ui-react"; import { Codicon } from "../Codicon/Codicon"; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/VerticalStepper/VerticalStepper.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/VerticalStepper/VerticalStepper.tsx index 322b22d49c7..d04696240cc 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/VerticalStepper/VerticalStepper.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/VerticalStepper/VerticalStepper.tsx @@ -71,22 +71,27 @@ export const VerticalStepperItem: FC = ({ return (
-
-
- {index < currentStep ? : {index + 1}} + {totalSteps > 1 && ( +
+
+ {index < currentStep ? : {index + 1}} +
+
{item.label}
-
{item.label}
-
+ )} +
-
-
-
+ {totalSteps > 1 && ( +
+
+
+ )}
{index === currentStep && (
diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/hooks/use-queries.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/hooks/use-queries.tsx index fd5993d28a4..009d5b75aac 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/hooks/use-queries.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/hooks/use-queries.tsx @@ -31,6 +31,7 @@ import { type DeploymentLogsData, type DeploymentTrack, type Environment, + type GetAuthorizedGitOrgsResp, type GetAutoBuildStatusResp, type GetTestKeyResp, type Organization, @@ -46,29 +47,30 @@ export const queryKeys = { "has-config-drift", { directoryPath, component: component?.metadata?.id, branch }, ], - getProjectEnvs: (project: Project, org: Organization) => ["get-project-envs", { organization: org.handle, project: project.handler }], + getProjectEnvs: (project: Project, org: Organization) => ["get-project-envs", { organization: org.uuid, project: project.id }], getTestKey: (endpointApimId: string, env: Environment, org: Organization) => [ "get-test-key", - { endpoint: endpointApimId, env: env.id, org: org.handle }, + { endpoint: endpointApimId, env: env.id, org: org.uuid }, ], - getSwaggerSpec: (apiRevisionId: string, org: Organization) => ["get-swagger-spec", { selectedEndpoint: apiRevisionId, org: org.handle }], + getSwaggerSpec: (apiRevisionId: string, org: Organization) => ["get-swagger-spec", { selectedEndpoint: apiRevisionId, org: org.uuid }], getBuildPacks: (selectedType: string, org: Organization) => ["build-packs", { selectedType, orgId: org?.id }], + getAuthorizedGitOrgs: (orgId: string, provider: string, credRef = "") => ["get-authorized-github-orgs", { orgId, provider, credRef }], getGitBranches: (repoUrl: string, org: Organization, credRef: string, isAccessible: boolean) => [ "get-git-branches", { repo: repoUrl, orgId: org?.id, credRef, isAccessible }, ], getDeployedEndpoints: (deploymentTrack: DeploymentTrack, component: ComponentKind, org: Organization) => [ "get-deployed-endpoints", - { organization: org.handle, component: component.metadata.id, deploymentTrackId: deploymentTrack?.id }, + { organization: org.uuid, component: component.metadata.id, deploymentTrackId: deploymentTrack?.id }, ], getProxyDeploymentInfo: (component: ComponentKind, org: Organization, env: Environment, apiVersion: ApiVersion) => [ "get-proxy-deployment-info", - { org: org.handle, component: component.metadata.id, env: env?.id, apiVersion: apiVersion?.id }, + { org: org.uuid, component: component.metadata.id, env: env?.id, apiVersion: apiVersion?.id }, ], getDeploymentStatus: (deploymentTrack: DeploymentTrack, component: ComponentKind, org: Organization, env: Environment) => [ "get-deployment-status", { - organization: org.handle, + organization: org.uuid, component: component.metadata.id, deploymentTrackId: deploymentTrack?.id, envId: env.id, @@ -77,28 +79,34 @@ export const queryKeys = { getWorkflowStatus: (org: Organization, env: Environment, buildId: string) => [ "get-workflow-status", { - organization: org?.handle, + organization: org?.uuid, envId: env?.id, buildId, }, ], getBuilds: (deploymentTrack: DeploymentTrack, component: ComponentKind, project: Project, org: Organization) => [ "get-builds", - { component: component.metadata.id, organization: org.handle, project: project.handler, branch: deploymentTrack?.branch }, + { component: component.metadata.id, organization: org.uuid, project: project.id, branch: deploymentTrack?.branch }, ], - getBuildsLogs: (component: ComponentKind, project: Project, org: Organization, build: BuildKind) => [ + getBuildsLogs: (component: ComponentKind, deploymentTrack: DeploymentTrack, project: Project, org: Organization, build: BuildKind) => [ "get-build-logs", - { component: component.metadata.id, organization: org.handle, project: project.handler, build: build?.status?.runId }, + { + component: component.metadata.id, + deploymentTrack: deploymentTrack.id, + organization: org.uuid, + project: project.id, + build: build?.status?.runId, + }, ], getComponentConnections: (component: ComponentKind, project: Project, org: Organization) => [ "get-component-connections", - { component: component.metadata.id, organization: org.handle, project: project.handler }, + { component: component.metadata.id, organization: org.uuid, project: project.id }, ], - useComponentList: (project: Project, org: Organization) => ["get-components", { organization: org.handle, project: project.handler }], - getProjectConnections: (project: Project, org: Organization) => ["get-project-connections", { organization: org.handle, project: project.handler }], + useComponentList: (project: Project, org: Organization) => ["get-components", { organization: org.uuid, project: project.id }], + getProjectConnections: (project: Project, org: Organization) => ["get-project-connections", { organization: org.uuid, project: project.id }], getAutoBuildStatus: (component: ComponentKind, deploymentTrack: DeploymentTrack, org: Organization) => [ "get-auto-build-status", - { component: component.metadata.id, organization: org.handle, versionId: deploymentTrack?.id }, + { component: component.metadata.id, organization: org.uuid, versionId: deploymentTrack?.id }, ], }; @@ -151,6 +159,13 @@ export const useGetBuildPacks = (selectedType: string, org: Organization, option options, ); +export const useGetAuthorizedGitOrgs = (orgId: string, provider: string, credRef = "", options?: UseQueryOptions) => + useQuery( + queryKeys.getAuthorizedGitOrgs(orgId, provider, credRef), + () => ChoreoWebViewAPI.getInstance().getChoreoRpcClient().getAuthorizedGitOrgs({ orgId, credRef }), + options, + ); + export const useGetGitBranches = (repoUrl: string, org: Organization, credRef = "", isAccessible = false, options?: UseQueryOptions) => useQuery( queryKeys.getGitBranches(repoUrl, org, credRef, isAccessible), @@ -386,13 +401,14 @@ export const useGoToSource = () => { export const useGetBuildLogs = ( component: ComponentKind, + deploymentTrack: DeploymentTrack, org: Organization, project: Project, build: BuildKind, options?: UseQueryOptions, ) => useQuery( - queryKeys.getBuildsLogs(component, project, org, build), + queryKeys.getBuildsLogs(component, deploymentTrack, project, org, build), async () => { try { const buildLog = await ChoreoWebViewAPI.getInstance().getChoreoRpcClient().getBuildLogs({ @@ -400,8 +416,12 @@ export const useGetBuildLogs = ( displayType: component.spec.type, orgHandler: org.handle, orgId: org.id.toString(), + orgUuid: org.uuid, projectId: project.id, buildId: build.status?.runId, + buildRef: build.status?.buildRef, + clusterId: build.status?.clusterId, + deploymentTrackId: deploymentTrack.id, }); return buildLog ?? null; } catch { diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/providers/react-query-provider.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/providers/react-query-provider.tsx index a1d782893a2..192a9ec3cc3 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/providers/react-query-provider.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/providers/react-query-provider.tsx @@ -53,7 +53,7 @@ export const ChoreoWebviewQueryClientProvider = ({ type, children }: { type: str } persistOptions={{ persister: webviewStatePersister(`react-query-persister-${type}`), - buster: "choreo-webview-cache-v2", + buster: "choreo-webview-cache-v5", }} > {children} diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/utilities/vscode-webview-rpc.ts b/workspaces/wso2-platform/wso2-platform-webviews/src/utilities/vscode-webview-rpc.ts index 2b22d597887..cc76801f7bc 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/utilities/vscode-webview-rpc.ts +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/utilities/vscode-webview-rpc.ts @@ -19,8 +19,11 @@ import { type AuthState, AuthStoreChangedNotification, + ChoreoRpcGetAuthorizedGitOrgsRequest, ChoreoRpcWebview, ClearWebviewCache, + CloneRepositoryIntoCompDir, + type CloneRepositoryIntoCompDirReq, CloseComponentViewDrawer, CloseWebViewNotification, type CommitHistory, @@ -39,6 +42,7 @@ import { ExecuteCommandRequest, FileExists, GetAuthState, + GetAuthorizedGitOrgsReq, GetConfigFileDrifts, type GetConfigFileDriftsReq, GetContextState, @@ -245,6 +249,10 @@ export class ChoreoWebViewAPI { return this._messenger.sendRequest(TriggerGithubAuthFlow, HOST_EXTENSION, orgId); } + public async cloneRepositoryIntoCompDir(params: CloneRepositoryIntoCompDirReq): Promise { + return this._messenger.sendRequest(CloneRepositoryIntoCompDir, HOST_EXTENSION, params); + } + public async triggerGithubInstallFlow(orgId: string): Promise { return this._messenger.sendRequest(TriggerGithubInstallFlow, HOST_EXTENSION, orgId); } diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/ComponentDetailsView.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/ComponentDetailsView.tsx index ab02ea7c246..d1b5edf5e26 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/ComponentDetailsView.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/ComponentDetailsView.tsx @@ -17,7 +17,7 @@ */ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { type BuildKind, ChoreoComponentType, @@ -51,9 +51,11 @@ export const ComponentDetailsView: FC = (props) = const deploymentTracks = component?.deploymentTracks ?? []; const [rightPanelRef] = useAutoAnimate(); const type = getTypeForDisplayType(props.component.spec?.type); + const queryClient = useQueryClient(); const [deploymentTrack, setDeploymentTrack] = useState(deploymentTracks?.find((item) => item.latest)); const [hasOngoingBuilds, setHasOngoingBuilds] = useState(false); + const [prevBuildList, setPrevBuildList] = useState([]); const [buildDetailsPanel, setBuildDetailsPanel] = useState<{ open: boolean; build?: BuildKind }>({ open: false, build: null }); useEffect(() => { @@ -125,12 +127,12 @@ export const ComponentDetailsView: FC = (props) = refetchOnWindowFocus: true, }); - const buildLogsQueryData = useGetBuildLogs(component, organization, project, buildDetailsPanel?.build, { + const buildLogsQueryData = useGetBuildLogs(component, deploymentTrack, organization, project, buildDetailsPanel?.build, { enabled: !!buildDetailsPanel?.build, }); const buildListQueryData = useGetBuildList(deploymentTrack, component, project, organization, { - onSuccess: (builds) => { + onSuccess: async (builds) => { setHasOngoingBuilds(builds.some((item) => item.status?.conclusion === "")); if (buildDetailsPanel?.open && buildDetailsPanel?.build) { const matchingItem = builds.find((item) => item.status?.runId === buildDetailsPanel?.build?.status?.runId); @@ -139,9 +141,27 @@ export const ComponentDetailsView: FC = (props) = } buildLogsQueryData.refetch(); } + const hasPrevSucceedBuilds = prevBuildList.filter((item) => item.status.conclusion === "success").length > 0; + if (!hasPrevSucceedBuilds && builds.length > 0 && builds[0].status?.conclusion === "success" && envs.length > 0) { + // have a new succeeded build, which should be auto deployed + await new Promise((resolve) => setTimeout(resolve, 10000)); + if (getTypeForDisplayType(component.spec?.type) === ChoreoComponentType.ApiProxy) { + queryClient.refetchQueries({ + queryKey: queryKeys.getProxyDeploymentInfo( + component, + organization, + envs[0], + component?.apiVersions?.find((item) => item.latest), + ), + }); + } else { + queryClient.refetchQueries({ queryKey: queryKeys.getDeploymentStatus(deploymentTrack, component, organization, envs[0]) }); + } + } + setPrevBuildList(builds); }, enabled: !!deploymentTrack, - refetchInterval: hasOngoingBuilds ? 5000 : false, + refetchInterval: hasOngoingBuilds || (props.isNewComponent && prevBuildList.length === 0) ? 5000 : false, }); const succeededBuilds = useMemo( diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/BuildConfigsSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/BuildConfigsSection.tsx index 8e8c5193b67..b36a3ca6c36 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/BuildConfigsSection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/BuildConfigsSection.tsx @@ -30,6 +30,9 @@ import { type IRightPanelSectionItem, RightPanelSection, RightPanelSectionItem } export const BuildConfigsSection: FC<{ component: ComponentKind }> = ({ component }) => { const buildConfigList = getBuildConfigViewList(component); + if (buildConfigList.length === 0) { + return null; + } return ( diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/DeploymentsSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/DeploymentsSection.tsx index 1b3f2a25ea6..72cdbe492fa 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/DeploymentsSection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/DeploymentsSection.tsx @@ -16,21 +16,18 @@ * under the License. */ -import { type } from "node:os"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import { zodResolver } from "@hookform/resolvers/zod"; -import { UseQueryResult, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { VSCodeDropdown, VSCodeLink, VSCodeOption } from "@vscode/webview-ui-toolkit/react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; import { type BuildKind, type CheckWorkflowStatusResp, ChoreoComponentType, type ComponentDeployment, - ComponentDisplayType, type ComponentEP, type ComponentKind, type CreateDeploymentReq, - DeploymentLogsData, DeploymentStatus, type DeploymentTrack, EndpointDeploymentStatus, @@ -38,8 +35,6 @@ import { type Organization, type Project, type ProxyDeploymentInfo, - type StateReason, - type WebviewQuickPickItem, WebviewQuickPickItemKind, WorkflowInstanceStatus, capitalizeFirstLetter, @@ -50,7 +45,6 @@ import { toTitleCase, } from "@wso2/wso2-platform-core"; import classNames from "classnames"; -import classnames from "classnames"; import clipboardy from "clipboardy"; import React, { type FC, type ReactNode, useState, useEffect } from "react"; import { type SubmitHandler, useForm } from "react-hook-form"; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/ComponentFormView.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/ComponentFormView.tsx index 8a33f962c3c..3b4111b7dff 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/ComponentFormView.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/ComponentFormView.tsx @@ -18,7 +18,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ChoreoBuildPackNames, ChoreoComponentType, @@ -29,18 +29,20 @@ import { type NewComponentWebviewProps, type SubmitComponentCreateReq, WebAppSPATypes, + buildGitURL, getComponentTypeText, getIntegrationComponentTypeText, getRandomNumber, makeURLSafe, parseGitURL, } from "@wso2/wso2-platform-core"; -import React, { type FC, useState, useEffect } from "react"; +import React, { type FC, useState } from "react"; import { useForm } from "react-hook-form"; import type { z } from "zod/v3"; import { HeaderSection } from "../../components/HeaderSection"; +import type { HeaderTag } from "../../components/HeaderSection/HeaderSection"; import { type StepItem, VerticalStepper } from "../../components/VerticalStepper"; -import { useComponentList } from "../../hooks/use-queries"; +import { queryKeys, useComponentList } from "../../hooks/use-queries"; import { useExtWebviewContext } from "../../providers/ext-vewview-ctx-provider"; import { ChoreoWebViewAPI } from "../../utilities/vscode-webview-rpc"; import { @@ -48,19 +50,23 @@ import { type componentEndpointsFormSchema, type componentGeneralDetailsSchema, type componentGitProxyFormSchema, + type componentRepoInitSchema, getComponentEndpointsFormSchema, getComponentFormSchemaBuildDetails, getComponentFormSchemaGenDetails, getComponentGitProxyFormSchema, + getRepoInitSchemaGenDetails, sampleEndpointItem, } from "./componentFormSchema"; import { ComponentFormBuildSection } from "./sections/ComponentFormBuildSection"; import { ComponentFormEndpointsSection } from "./sections/ComponentFormEndpointsSection"; import { ComponentFormGenDetailsSection } from "./sections/ComponentFormGenDetailsSection"; import { ComponentFormGitProxySection } from "./sections/ComponentFormGitProxySection"; +import { ComponentFormRepoInitSection } from "./sections/ComponentFormRepoInitSection"; import { ComponentFormSummarySection } from "./sections/ComponentFormSummarySection"; type ComponentFormGenDetailsType = z.infer; +type ComponentRepoInitType = z.infer; type ComponentFormBuildDetailsType = z.infer; type ComponentFormEndpointsType = z.infer; type ComponentFormGitProxyType = z.infer; @@ -76,6 +82,7 @@ export const ComponentFormView: FC = (props) => { existingComponents: existingComponentsCache, } = props; const type = initialValues?.type; + const queryClient = useQueryClient(); const [formSections] = useAutoAnimate(); const { extensionName } = useExtWebviewContext(); @@ -89,6 +96,20 @@ export const ComponentFormView: FC = (props) => { defaultValues: { name: initialValues?.name || "", subPath: "", gitRoot: "", repoUrl: "", branch: "", credential: "", gitProvider: "" }, }); + const repoInitForm = useForm({ + resolver: zodResolver(getRepoInitSchemaGenDetails(existingComponents)), + mode: "all", + defaultValues: { + org: "", + repo: "", + branch: "main", + subPath: "/", + name: initialValues?.name || "", + gitProvider: GitProvider.GITHUB, + serverUrl: "", + }, + }); + const name = genDetailsForm.watch("name"); const gitRoot = genDetailsForm.watch("gitRoot"); const subPath = genDetailsForm.watch("subPath"); @@ -156,16 +177,50 @@ export const ComponentFormView: FC = (props) => { }, }); - const { mutate: createComponent, isLoading: isCreatingComponent } = useMutation({ + const { mutateAsync: initializeRepoAsync, isLoading: initializingRepo } = useMutation({ mutationFn: async () => { + if (props.isNewCodeServerComp) { + const repoInitDetails = repoInitForm.getValues(); + const repoUrl = buildGitURL(repoInitDetails?.orgHandler, repoInitDetails.repo, repoInitDetails.gitProvider, false, repoInitDetails.serverUrl); + const branchesCache: string[] = queryClient.getQueryData(queryKeys.getGitBranches(repoUrl, organization, "", true)); + const newWorkspacePath = await ChoreoWebViewAPI.getInstance().cloneRepositoryIntoCompDir({ + cwd: props.directoryFsPath, + subpath: repoInitDetails.subPath, + org: props.organization, + componentName: makeURLSafe(repoInitDetails.name), + repo: { + orgHandler: repoInitDetails.orgHandler, + orgName: repoInitDetails.org, + branch: branchesCache?.length > 0 ? repoInitDetails.branch : undefined, + provider: repoInitDetails.gitProvider, + repo: repoInitDetails.repo, + serverUrl: repoInitDetails.serverUrl, + secretRef: repoInitDetails.credential || "", + isBareRepo: !(branchesCache?.length > 0), + }, + }); + + return newWorkspacePath; + } + }, + }); + + const { mutate: createComponent, isLoading: isCreatingComponent } = useMutation({ + mutationFn: async (newWorkspaceDir?: string) => { const genDetails = genDetailsForm.getValues(); + const repoInitDetails = repoInitForm.getValues(); const buildDetails = buildDetailsForm.getValues(); const gitProxyDetails = gitProxyForm.getValues(); - const componentName = makeURLSafe(genDetails.name); - + const name = props.isNewCodeServerComp ? repoInitDetails.name : genDetails.name; + const componentName = makeURLSafe(props.isNewCodeServerComp ? repoInitDetails.name : genDetails.name); + const branch = props.isNewCodeServerComp ? repoInitDetails.branch : genDetails.branch; const parsedRepo = parseGitURL(genDetails.repoUrl); - const provider = parsedRepo ? parsedRepo[2] : null; + const provider = props.isNewCodeServerComp ? repoInitDetails.gitProvider : parsedRepo[2]; + + const repoUrl = props.isNewCodeServerComp + ? buildGitURL(repoInitDetails.orgHandler, repoInitDetails.repo, repoInitDetails.gitProvider, false, repoInitDetails.serverUrl) + : genDetails.repoUrl; const createParams: Partial = { orgId: organization.id.toString(), @@ -173,21 +228,21 @@ export const ComponentFormView: FC = (props) => { projectId: project.id, projectHandle: project.handler, name: componentName, - displayName: genDetails.name, + displayName: name, type, componentSubType: initialValues?.subType || "", buildPackLang: buildDetails.buildPackLang, - componentDir: directoryFsPath, - repoUrl: genDetails.repoUrl, - gitProvider: genDetails.gitProvider, - branch: genDetails.branch, + componentDir: newWorkspaceDir || directoryFsPath, + repoUrl: repoUrl, + gitProvider: provider, + branch: branch, langVersion: buildDetails.langVersion, port: buildDetails.webAppPort, originCloud: extensionName === "Devant" ? "devant" : "choreo", }; if (provider !== GitProvider.GITHUB) { - createParams.gitCredRef = genDetails?.credential; + createParams.gitCredRef = props.isNewCodeServerComp ? repoInitDetails.credential : genDetails?.credential; } if (buildDetails.buildPackLang === ChoreoImplementationType.Docker) { @@ -248,8 +303,32 @@ export const ComponentFormView: FC = (props) => { onSuccess: () => setStepIndex(stepIndex + 1), }); - const steps: StepItem[] = [ - { + const steps: StepItem[] = []; + + if (props.isNewCodeServerComp) { + steps.push({ + label: "Repository Details", + content: ( + { + const newDirPath = await initializeRepoAsync(); + if (steps.length > 1) { + gitProxyForm.setValue("proxyContext", `/${makeURLSafe(genDetailsForm.getValues()?.name)}`); + setStepIndex(stepIndex + 1); + } else { + createComponent(newDirPath); + } + }} + /> + ), + }); + } else { + steps.push({ label: "General Details", content: ( = (props) => { form={genDetailsForm} componentType={type} onNextClick={() => { - gitProxyForm.setValue( - "proxyContext", - genDetailsForm.getValues()?.name ? `/${makeURLSafe(genDetailsForm.getValues()?.name)}` : `/path-${getRandomNumber()}`, - ); + gitProxyForm.setValue("proxyContext", `/${makeURLSafe(genDetailsForm.getValues()?.name)}`); setStepIndex(stepIndex + 1); }} /> ), - }, - ]; - - let showBuildDetails = false; - if (type !== ChoreoComponentType.ApiProxy) { - if (!initialValues?.buildPackLang) { - showBuildDetails = true; - } else { - if (initialValues?.buildPackLang === ChoreoBuildPackNames.Ballerina) { - showBuildDetails = type === ChoreoComponentType.Service; - } else if (initialValues?.buildPackLang === ChoreoBuildPackNames.MicroIntegrator) { - showBuildDetails = type === ChoreoComponentType.Service; - } else { + }); + + let showBuildDetails = false; + if (type !== ChoreoComponentType.ApiProxy) { + if (!initialValues?.buildPackLang) { + showBuildDetails = true; + } else if ( + ![ChoreoBuildPackNames.Ballerina, ChoreoBuildPackNames.MicroIntegrator].includes(initialValues?.buildPackLang as ChoreoBuildPackNames) + ) { showBuildDetails = true; } } - } - if (showBuildDetails) { - steps.push({ - label: "Build Details", - content: ( - setStepIndex(stepIndex + 1)} - onBackClick={() => setStepIndex(stepIndex - 1)} - form={buildDetailsForm} - selectedType={type} - subPath={subPath} - gitRoot={gitRoot} - baseUriPath={directoryUriPath} - /> - ), - }); - } + if (showBuildDetails) { + steps.push({ + label: "Build Details", + content: ( + setStepIndex(stepIndex + 1)} + onBackClick={() => setStepIndex(stepIndex - 1)} + form={buildDetailsForm} + selectedType={type} + subPath={subPath} + gitRoot={gitRoot} + baseUriPath={directoryUriPath} + /> + ), + }); + } - if (type === ChoreoComponentType.Service) { - if ( - ![ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildPackLang as ChoreoBuildPackNames) || - ([ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildPackLang as ChoreoBuildPackNames) && !useDefaultEndpoints) - ) { + if (type === ChoreoComponentType.Service && extensionName !== "Devant") { + if ( + ![ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildPackLang as ChoreoBuildPackNames) || + ([ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildPackLang as ChoreoBuildPackNames) && + !useDefaultEndpoints) + ) { + steps.push({ + label: "Endpoint Details", + content: ( + submitEndpoints(data.endpoints as Endpoint[])} + onBackClick={() => setStepIndex(stepIndex - 1)} + isSaving={isSubmittingEndpoints} + form={endpointDetailsForm} + /> + ), + }); + } + } + if (type === ChoreoComponentType.ApiProxy) { steps.push({ - label: "Endpoint Details", + label: "Proxy Details", content: ( - submitEndpoints(data.endpoints as Endpoint[])} + key="git-proxy-step" + onNextClick={(data) => submitProxyConfig(data)} onBackClick={() => setStepIndex(stepIndex - 1)} - isSaving={isSubmittingEndpoints} - form={endpointDetailsForm} + isSaving={isSubmittingProxyConfig} + form={gitProxyForm} /> ), }); } - } - if (type === ChoreoComponentType.ApiProxy) { + steps.push({ - label: "Proxy Details", + label: "Summary", content: ( - submitProxyConfig(data)} + key="summary-step" + genDetailsForm={genDetailsForm} + buildDetailsForm={buildDetailsForm} + endpointDetailsForm={endpointDetailsForm} + gitProxyForm={gitProxyForm} + onNextClick={() => createComponent(undefined)} onBackClick={() => setStepIndex(stepIndex - 1)} - isSaving={isSubmittingProxyConfig} - form={gitProxyForm} + isCreating={isCreatingComponent} /> ), }); } - steps.push({ - label: "Summary", - content: ( - createComponent()} - onBackClick={() => setStepIndex(stepIndex - 1)} - isCreating={isCreatingComponent} - /> - ), - }); - const componentTypeText = extensionName === "Devant" ? getIntegrationComponentTypeText(type, initialValues?.subType) : getComponentTypeText(type); + const headerTags: HeaderTag[] = []; + + if (!props.isNewCodeServerComp) { + headerTags.push({ label: "Source Directory", value: subPath && subPath !== "." ? subPath : directoryName }); + } + headerTags.push({ label: "Project", value: project.name }, { label: "Organization", value: organization.name }); + return (
+
diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/componentFormSchema.ts b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/componentFormSchema.ts index fa81275cda2..87868c0ee0a 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/componentFormSchema.ts +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/componentFormSchema.ts @@ -26,6 +26,7 @@ import { GitProvider, GoogleProviderBuildPackNames, type OpenApiSpec, + Organization, WebAppSPATypes, capitalizeFirstLetter, makeURLSafe, @@ -35,14 +36,33 @@ import * as yaml from "js-yaml"; import { z } from "zod/v3"; import { ChoreoWebViewAPI } from "../../utilities/vscode-webview-rpc"; +export const componentRepoInitSchema = z.object({ + org: z.string().min(1, "Required"), + orgHandler: z.string(), + repo: z.string().min(1, "Required"), + branch: z.string(), + subPath: z.string().regex(/^(\/)?([a-zA-Z0-9_-]+(\/)?)*$/, "Invalid path"), + name: z + .string() + .min(1, "Required") + .min(3, "Needs to be at least 3 characters") + .max(60, "Max length exceeded") + .regex(/^[A-Za-z]/, "Needs to start with alphabetic letter") + .regex(/^[A-Za-z\s\d\-_]+$/, "Cannot have special characters"), + gitProvider: z.string().min(1, "Required"), + credential: z.string(), + serverUrl: z.string(), +}); + export const componentGeneralDetailsSchema = z.object({ name: z .string() .min(1, "Required") + .min(3, "Needs to be at least 3 characters") .max(60, "Max length exceeded") .regex(/^[A-Za-z]/, "Needs to start with alphabetic letter") .regex(/^[A-Za-z\s\d\-_]+$/, "Cannot have special characters"), - subPath: z.string(), + subPath: z.string(), // todo: add regex gitRoot: z.string(), repoUrl: z.string().min(1, "Required"), gitProvider: z.string().min(1, "Required"), @@ -201,6 +221,16 @@ export const getComponentFormSchemaGenDetails = (existingComponents: ComponentKi } }); +export const getRepoInitSchemaGenDetails = (existingComponents: ComponentKind[]) => + componentRepoInitSchema.partial().superRefine(async (data, ctx) => { + if (existingComponents.some((item) => item.metadata.name === makeURLSafe(data.name))) { + ctx.addIssue({ path: ["name"], code: z.ZodIssueCode.custom, message: "Name already exists" }); + } + if (data.gitProvider !== GitProvider.GITHUB && !data.credential) { + ctx.addIssue({ path: ["credential"], code: z.ZodIssueCode.custom, message: "Required" }); + } + }); + export const getComponentFormSchemaBuildDetails = (type: string, directoryFsPath: string, gitRoot: string) => componentBuildDetailsSchema.partial().superRefine(async (data, ctx) => { if ( diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormEndpointsSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormEndpointsSection.tsx index b5847593062..bf2860f82da 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormEndpointsSection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormEndpointsSection.tsx @@ -17,7 +17,7 @@ */ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; import { EndpointType, type NewComponentWebviewProps } from "@wso2/wso2-platform-core"; import React, { type FC, type ReactNode } from "react"; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx index 39965f989f6..1d38c681b30 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx @@ -200,8 +200,7 @@ export const ComponentFormGenDetailsSection: FC = ({ onNextClick, organiz if (isRepoAuthorizedResp?.retrievedRepos) { invalidRepoMsg = ( - {extensionName} lacks access to the selected repository.{" "} - (Only public repos are allowed within the free tier.) + {extensionName} lacks access to the selected repository. ); invalidRepoAction = "Grant Access"; @@ -216,10 +215,7 @@ export const ComponentFormGenDetailsSection: FC = ({ onNextClick, organiz onInvalidRepoActionClick = () => ChoreoWebViewAPI.getInstance().openExternalChoreo(`organizations/${organization.handle}/settings/credentials`); if (isRepoAuthorizedResp?.retrievedRepos) { invalidRepoMsg = ( - - Selected Credential does not have sufficient permissions.{" "} - (Only public repos are allowed within the free tier.) - + Selected Credential does not have sufficient permissions to access the repository. ); invalidRepoAction = "Manage Credentials"; } else { diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx new file mode 100644 index 00000000000..ae34163f5e1 --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx @@ -0,0 +1,389 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { RequiredFormInput } from "@wso2/ui-toolkit"; +import { GitProvider, type NewComponentWebviewProps, buildGitURL } from "@wso2/wso2-platform-core"; +import classNames from "classnames"; +import debounce from "lodash.debounce"; +import React, { type FC, useCallback, useEffect, useState } from "react"; +import type { SubmitHandler, UseFormReturn } from "react-hook-form"; +import type { z } from "zod/v3"; +import { Banner } from "../../../components/Banner"; +import { Button } from "../../../components/Button"; +import { Dropdown } from "../../../components/FormElements/Dropdown"; +import { TextField } from "../../../components/FormElements/TextField"; +import { useGetAuthorizedGitOrgs, useGetGitBranches } from "../../../hooks/use-queries"; +import { useExtWebviewContext } from "../../../providers/ext-vewview-ctx-provider"; +import { ChoreoWebViewAPI } from "../../../utilities/vscode-webview-rpc"; +import type { componentRepoInitSchema } from "../componentFormSchema"; + +type ComponentRepoInitSchemaType = z.infer; + +interface Props extends NewComponentWebviewProps { + onNextClick: () => void; + initializingRepo?: boolean; + initialFormValues?: ComponentRepoInitSchemaType; + form: UseFormReturn; + componentType: string; +} + +const connectMoreRepoText = "Connect More Repositories"; +const createNewRpoText = "Create New Repository"; +const createNewCredText = "Create New Credential"; +const addOrganization = "Add"; + +export const ComponentFormRepoInitSection: FC = ({ onNextClick, organization, form, initializingRepo }) => { + const [compDetailsSections] = useAutoAnimate(); + const { extensionName } = useExtWebviewContext(); + const [creatingRepo, setCreatingRepo] = useState(false); + + const org = form.watch("org"); + const repo = form.watch("repo"); + const subPath = form.watch("subPath"); + const serverUrl = form.watch("serverUrl"); + const provider = form.watch("gitProvider"); + const credential = form.watch("credential"); + const orgName = [addOrganization].includes(org) ? "" : org; + const repoName = [connectMoreRepoText, createNewRpoText].includes(repo) ? "" : repo; + + const { + data: gitOrgs, + isLoading: loadingGitOrgs, + error: errorFetchingGitOrg, + } = useGetAuthorizedGitOrgs(organization.id?.toString(), provider, credential, { + refetchOnWindowFocus: true, + enabled: provider === GitProvider.GITHUB || !!credential, + }); + const matchingOrgItem = gitOrgs?.gitOrgs?.find((item) => item.orgName === orgName); + + const { data: gitCredentials = [], isLoading: isLoadingGitCred } = useQuery({ + queryKey: ["git-creds", { provider }], + queryFn: () => + ChoreoWebViewAPI.getInstance().getChoreoRpcClient().getCredentials({ orgId: organization?.id?.toString(), orgUuid: organization.uuid }), + select: (gitData) => gitData?.filter((item) => item.type === provider), + refetchOnWindowFocus: true, + enabled: provider !== GitProvider.GITHUB, + }); + + const { isLoading: isLoadingGitlabCreds } = useQuery({ + queryKey: ["gitlab-creds", { provider, credential }], + queryFn: () => + ChoreoWebViewAPI.getInstance().getChoreoRpcClient().getCredentialDetails({ + orgId: organization?.id?.toString(), + orgUuid: organization.uuid, + credentialId: credential, + }), + enabled: provider === GitProvider.GITLAB_SERVER && !!credential, + onSuccess: (data) => form.setValue("serverUrl", data?.serverUrl), + }); + + useEffect(() => { + if (gitCredentials.length > 0 && (form.getValues("credential") || !gitCredentials.some((item) => item.id === form.getValues("credential")))) { + form.setValue("credential", gitCredentials[0]?.id); + } else if (gitCredentials.length === 0 && form.getValues("credential") !== "") { + form.setValue("credential", ""); + } + }, [gitCredentials]); + + const repoUrl = matchingOrgItem && repoName && buildGitURL(matchingOrgItem?.orgHandler, repoName, provider, false, serverUrl); + useEffect(() => { + if (gitOrgs?.gitOrgs.length > 0 && (form.getValues("org") === "" || !gitOrgs?.gitOrgs.some((item) => item.orgName === form.getValues("org")))) { + form.setValue("org", gitOrgs?.gitOrgs[0]?.orgName); + } else if (gitOrgs?.gitOrgs.length === 0 && form.getValues("org") !== "") { + form.setValue("org", ""); + } + }, [gitOrgs]); + + useEffect(() => { + if (matchingOrgItem?.repositories.length > 0 && !matchingOrgItem?.repositories?.some((item) => item.name === form.getValues("repo"))) { + setTimeout(() => form.setValue("repo", ""), 1000); + } + if (matchingOrgItem) { + form.setValue("orgHandler", matchingOrgItem.orgHandler); + } + }, [matchingOrgItem]); + + const { data: branches = [], isLoading: isLoadingBranches } = useGetGitBranches( + repoUrl, + organization, + provider === GitProvider.GITHUB ? "" : credential, + !errorFetchingGitOrg, + { + enabled: !!repoName && !!provider && !!repoUrl && (provider === GitProvider.GITHUB ? !errorFetchingGitOrg : !!credential), + refetchOnWindowFocus: true, + }, + ); + + useEffect(() => { + if (branches?.length > 0 && !branches.includes(form.getValues("branch"))) { + if (branches.includes("main")) { + form.setValue("branch", "main", { shouldValidate: true }); + } + if (branches.includes("master")) { + form.setValue("branch", "master", { shouldValidate: true }); + } else { + form.setValue("branch", branches[0], { shouldValidate: true }); + } + } + }, [branches]); + + const handleCreateNewRepo = () => { + let newRepoLink = "https://github.com/new"; + if (provider === GitProvider.BITBUCKET) { + newRepoLink = `https://bitbucket.org/${orgName}/workspace/create/repository`; + } else if (provider === GitProvider.GITLAB_SERVER) { + newRepoLink = `${serverUrl}/projects/new`; + } + ChoreoWebViewAPI.getInstance().openExternal(newRepoLink); + setCreatingRepo(true); + }; + + useEffect(() => { + setCreatingRepo(false); + }, [provider]); + + const debouncedUpdateName = useCallback( + debounce((subPath: string, repo: string) => { + if (subPath) { + const paths = subPath.split("/"); + const lastPath = paths.findLast((item) => !!item); + if (lastPath) { + form.setValue("name", lastPath); + return; + } + } + if (repo) { + form.setValue("name", repo); + return; + } + }, 1000), + [], + ); + + useEffect(() => { + debouncedUpdateName(subPath, repo); + }, [repo, subPath]); + + const { mutateAsync: getRepoMetadata, isLoading: isValidatingPath } = useMutation({ + mutationFn: (data: ComponentRepoInitSchemaType) => { + const subPath = data.subPath.startsWith("/") ? data.subPath.slice(1) : data.subPath; + return ChoreoWebViewAPI.getInstance() + .getChoreoRpcClient() + .getGitRepoMetadata({ + branch: data.branch, + gitOrgName: data.org, + gitRepoName: data.repo, + relativePath: subPath, + orgId: organization?.id?.toString(), + secretRef: data.credential || "", + }); + }, + }); + + const onSubmitForm: SubmitHandler = async (data) => { + try { + const resp = await getRepoMetadata(data); + if (resp?.metadata && !resp?.metadata?.isSubPathEmpty) { + form.setError("subPath", { message: "Path isn't empty in the remote repo" }); + } else { + onNextClick(); + } + } catch { + // the API will throw an error, if branch does not exist + onNextClick(); + } + }; + + const repoDropdownItems = [{ value: createNewRpoText }]; + if (provider === GitProvider.GITHUB) { + repoDropdownItems.push({ value: connectMoreRepoText }); + } + if (matchingOrgItem?.repositories?.length > 0) { + repoDropdownItems.push( + { type: "separator", value: "" } as { value: string }, + ...matchingOrgItem?.repositories?.map((item) => ({ value: item.name })), + ); + } + + const credentialDropdownItems = [{ value: createNewCredText }]; + if (gitCredentials?.length > 0) { + credentialDropdownItems.push( + { type: "separator", value: "" } as { value: string }, + ...gitCredentials?.map((item) => ({ value: item.id, label: item.name })), + ); + } + + const orgDropdownItems = []; + if (provider === GitProvider.GITHUB) { + orgDropdownItems.push({ value: addOrganization }, { type: "separator", value: "" } as { value: string }); + } + + orgDropdownItems.push(...(gitOrgs?.gitOrgs ?? [])?.map((item) => ({ value: item.orgName }))); + + return ( + <> +
+ + + {provider === GitProvider.GITHUB && errorFetchingGitOrg && ( + ChoreoWebViewAPI.getInstance().triggerGithubAuthFlow(organization.id?.toString()), + }} + /> + )} + {provider !== GitProvider.GITHUB && ( + { + const value = (e.target as HTMLSelectElement).value; + if (credential === createNewCredText) { + form.setValue("credential", ""); + ChoreoWebViewAPI.getInstance().openExternalChoreo(`organizations/${organization.handle}/settings/credentials`); + } else { + form.setValue("credential", value); + } + }} + /> + )} + {(provider === GitProvider.GITHUB || credential) && ( + <> + { + const value = (e.target as HTMLSelectElement).value; + if (value === addOrganization) { + ChoreoWebViewAPI.getInstance().triggerGithubInstallFlow(organization.id?.toString()); + form.setValue("org", ""); + } else { + form.setValue("org", value); + } + }} + /> +
+ { + const value = (e.target as HTMLSelectElement).value; + if (value === createNewRpoText) { + handleCreateNewRepo(); + form.setValue("repo", ""); + } else if (value === connectMoreRepoText) { + ChoreoWebViewAPI.getInstance().triggerGithubInstallFlow(organization.id?.toString()); + form.setValue("repo", ""); + } else { + form.setValue("repo", value); + } + }} + /> +
+
+ + +
+
+
+ + {repoName && branches?.length > 0 && ( + + )} + + )} + + + {repo && ( +
+ +
+ )} +
+ +
+ +
+ + ); +}; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/webpack.config.js b/workspaces/wso2-platform/wso2-platform-webviews/webpack.config.js index 3fc07286d9c..7a4d27a6618 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/webpack.config.js +++ b/workspaces/wso2-platform/wso2-platform-webviews/webpack.config.js @@ -13,8 +13,8 @@ class RunTailwindCSSPlugin { module.exports = { entry: "./src/index.tsx", target: "web", - devtool: !process.env.CI ? "source-map" : undefined, - mode: !process.env.CI ? "development" : "production", + devtool: "source-map", + mode: "development", output: { path: path.resolve(__dirname, "build"), filename: "main.js",