diff --git a/.github/actions/pr/action.yml b/.github/actions/pr/action.yml index 95f6227006c..e857664d3ae 100644 --- a/.github/actions/pr/action.yml +++ b/.github/actions/pr/action.yml @@ -97,9 +97,10 @@ runs: shell: bash id: commit run: | + mv .git/config .git/config-old + git remote add origin https://${{ inputs.bot_token }}@github.com/${{ github.repository }}.git git diff --staged --quiet || git commit -m "Update version to ${{ inputs.version }}" git checkout -b "${{ inputs.version }}" - git remote set-url origin https://${{ inputs.bot_username }}:${{ inputs.bot_token }}@github.com/$GITHUB_REPOSITORY.git git tag ${{ inputs.version }} git push origin "refs/heads/${{ inputs.version }}" "refs/tags/${{ inputs.version }}" baseBranch="" @@ -119,7 +120,7 @@ runs: pr=$(gh pr create -B "$baseBranch" -H "${{ inputs.version }}" --title "Merge \"${{ inputs.version }}\" into \"$baseBranch\"" --body '$subject') echo "prURL=$pr" >> $GITHUB_OUTPUT env: - GITHUB_TOKEN: ${{ inputs.bot_token }} + GH_TOKEN: ${{ inputs.bot_token }} - name: "PR Notification" shell: bash diff --git a/.github/workflows/release-vsix.yml b/.github/workflows/release-vsix.yml index 0b740fc9ce4..3eb8c6b7534 100644 --- a/.github/workflows/release-vsix.yml +++ b/.github/workflows/release-vsix.yml @@ -200,6 +200,7 @@ jobs: if: ${{ always() && contains(needs.*.result, 'failure') && github.repository == 'wso2/vscode-extensions'}} runs-on: ubuntu-latest steps: + - uses: actions/checkout@v2 - name: "Failure Notification" uses: ./.github/actions/failure-notification with: diff --git a/.gitignore b/.gitignore index 7b220b9158b..9a8738831b6 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,7 @@ playwright-report/ extensions.json -!sample-mi-project.zip \ No newline at end of file +!sample-mi-project.zip +.claude/ + +pnpm-lock.yaml diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index dd035447509..b6876098cb8 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -127,6 +127,9 @@ importers: ../../workspaces/ballerina/ballerina-extension: dependencies: + '@ai-sdk/amazon-bedrock': + specifier: ^2.2.12 + version: 2.2.12(zod@3.25.76) '@ai-sdk/anthropic': specifier: ^1.2.12 version: 1.2.12(zod@3.25.76) @@ -679,7 +682,7 @@ importers: devDependencies: '@storybook/react': specifier: ^6.5.16 - version: 6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) + version: 6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) '@types/lodash': specifier: ~4.17.16 version: 4.17.17 @@ -728,12 +731,12 @@ importers: '@wso2/ballerina-core': specifier: workspace:* version: link:../ballerina-core + '@wso2/ballerina-data-mapper': + specifier: workspace:* + version: link:../data-mapper '@wso2/ballerina-graphql-design-diagram': specifier: workspace:* version: link:../graphql-design-diagram - '@wso2/ballerina-inline-data-mapper': - specifier: workspace:* - version: link:../inline-data-mapper '@wso2/ballerina-low-code-diagram': specifier: workspace:* version: link:../ballerina-low-code-diagram @@ -752,9 +755,6 @@ importers: '@wso2/component-diagram': specifier: workspace:* version: link:../component-diagram - '@wso2/data-mapper-view': - specifier: workspace:* - version: link:../data-mapper-view '@wso2/overview-view': specifier: workspace:* version: link:../overview-view @@ -951,7 +951,7 @@ importers: devDependencies: '@storybook/react': specifier: ^6.3.7 - version: 6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) + version: 6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) '@types/dagre': specifier: ~0.7.52 version: 0.7.53 @@ -1021,7 +1021,7 @@ importers: devDependencies: '@storybook/react': specifier: ^6.5.16 - version: 6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) + version: 6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) '@types/dagre': specifier: ~0.7.52 version: 0.7.53 @@ -1041,7 +1041,7 @@ importers: specifier: 5.8.3 version: 5.8.3 - ../../workspaces/ballerina/data-mapper-view: + ../../workspaces/ballerina/data-mapper: dependencies: '@emotion/css': specifier: ~11.13.5 @@ -1070,8 +1070,11 @@ importers: '@tanstack/react-query': specifier: 5.77.1 version: 5.77.1(react@18.2.0) + '@types/mousetrap': + specifier: ~1.6.15 + version: 1.6.15 '@vscode/webview-ui-toolkit': - specifier: ^1.4.0 + specifier: ^1.2.0 version: 1.4.0(react@18.2.0) '@wso2/ballerina-core': specifier: workspace:* @@ -1079,15 +1082,6 @@ importers: '@wso2/ballerina-rpc-client': specifier: workspace:* version: link:../ballerina-rpc-client - '@wso2/ballerina-statement-editor': - specifier: workspace:* - version: link:../statement-editor - '@wso2/record-creator': - specifier: workspace:* - version: link:../record-creator - '@wso2/syntax-tree': - specifier: workspace:* - version: link:../syntax-tree '@wso2/ui-toolkit': specifier: workspace:* version: link:../../common-libs/ui-toolkit @@ -1095,210 +1089,75 @@ importers: specifier: ^2.19.0 version: 2.19.0 classnames: - specifier: ^2.5.1 + specifier: ^2.2.6 version: 2.5.1 lodash: - specifier: ^4.17.21 + specifier: ^4.17.11 version: 4.17.21 lodash.debounce: specifier: ^4.0.8 version: 4.0.8 + mousetrap: + specifier: ^1.6.5 + version: 1.6.5 react: - specifier: 18.2.0 + specifier: ^18.2.0 version: 18.2.0 react-dom: - specifier: 18.2.0 + specifier: ^18.2.0 version: 18.2.0(react@18.2.0) - react-intl: - specifier: ^7.1.11 - version: 7.1.11(react@18.2.0)(typescript@5.8.3) - react-lottie: - specifier: ^1.2.10 - version: 1.2.10(react@18.2.0) - reflect-metadata: - specifier: ^0.1.14 - version: 0.1.14 resize-observer-polyfill: specifier: ^1.5.1 version: 1.5.1 - tsyringe: - specifier: ^4.10.0 - version: 4.10.0 vscode-languageserver-types: specifier: ^3.17.5 version: 3.17.5 zustand: - specifier: ^4.5.7 - version: 4.5.7(@types/react@18.2.0)(react@18.2.0) + specifier: ^5.0.4 + version: 5.0.6(@types/react@18.2.0)(react@18.2.0)(use-sync-external-store@1.5.0(react@18.2.0)) devDependencies: - '@babel/core': - specifier: ^7.27.1 - version: 7.27.7 - '@babel/preset-env': - specifier: ^7.27.2 - version: 7.27.2(@babel/core@7.27.7) - '@rollup/plugin-commonjs': - specifier: ^28.0.3 - version: 28.0.6(rollup@4.46.1) - '@rollup/plugin-json': - specifier: ^6.1.0 - version: 6.1.0(rollup@4.46.1) - '@rollup/plugin-node-resolve': - specifier: ^16.0.1 - version: 16.0.1(rollup@4.46.1) - '@storybook/addon-actions': - specifier: ^8.6.14 - version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) - '@storybook/addon-essentials': - specifier: ^8.6.14 - version: 8.6.14(@types/react@18.2.0)(storybook@8.6.14(prettier@3.5.3)) - '@storybook/addon-links': - specifier: ^8.6.14 - version: 8.6.14(react@18.2.0)(storybook@8.6.14(prettier@3.5.3)) - '@storybook/builder-webpack5': - specifier: ^8.6.14 - version: 8.6.14(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(webpack-cli@6.0.1) - '@storybook/manager-webpack5': - specifier: ^6.5.16 - version: 6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1) - '@storybook/react': - specifier: ^8.6.14 - version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) '@types/blueimp-md5': specifier: ^2.18.2 version: 2.18.2 - '@types/classnames': - specifier: ^2.3.4 - version: 2.3.4 - '@types/handlebars': - specifier: ^4.1.0 - version: 4.1.0 '@types/lodash': specifier: 4.17.16 version: 4.17.16 '@types/lodash.debounce': - specifier: ^4.0.9 + specifier: ^4.0.6 version: 4.0.9 '@types/react': - specifier: 18.2.0 + specifier: ^18.2.0 version: 18.2.0 '@types/react-dom': - specifier: 18.2.0 - version: 18.2.0 - '@types/react-lottie': - specifier: ^1.2.10 - version: 1.2.10 - '@types/uuid': - specifier: ^10.0.0 - version: 10.0.0 - '@types/webpack': - specifier: ^5.28.5 - version: 5.28.5(webpack-cli@6.0.1) + specifier: 17.0.26 + version: 17.0.26(@types/react@18.2.0) '@typescript-eslint/eslint-plugin': specifier: ~8.32.1 version: 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.27.0(jiti@2.5.1))(typescript@5.8.3) '@typescript-eslint/parser': specifier: ~8.32.1 version: 8.32.1(eslint@9.27.0(jiti@2.5.1))(typescript@5.8.3) - '@vitejs/plugin-react': - specifier: ^4.4.1 - version: 4.7.0(vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)) - babel-loader: - specifier: ^10.0.0 - version: 10.0.0(@babel/core@7.27.7)(webpack@5.101.0) - babel-plugin-transform-typescript-metadata: - specifier: ^0.3.2 - version: 0.3.2 - copy-webpack-plugin: - specifier: ^13.0.0 - version: 13.0.0(webpack@5.101.0) copyfiles: specifier: ^2.4.1 version: 2.4.1 css-loader: specifier: ^7.1.2 version: 7.1.2(webpack@5.101.0) - dagre: - specifier: ^0.8.5 - version: 0.8.5 eslint: - specifier: ^9.27.0 + specifier: ^9.26.0 version: 9.27.0(jiti@2.5.1) eslint-plugin-react-hooks: specifier: ^5.2.0 version: 5.2.0(eslint@9.27.0(jiti@2.5.1)) eslint-plugin-react-refresh: - specifier: ^0.4.20 + specifier: ^0.4.4 version: 0.4.20(eslint@9.27.0(jiti@2.5.1)) file-loader: specifier: ^6.2.0 version: 6.2.0(webpack@5.101.0) - fork-ts-checker-webpack-plugin: - specifier: ^9.1.0 - version: 9.1.0(typescript@5.8.3)(webpack@5.101.0) - monaco-editor-webpack-plugin: - specifier: ^7.1.0 - version: 7.1.0(monaco-editor@0.52.2)(webpack@5.101.0) - mousetrap: - specifier: ^1.6.5 - version: 1.6.5 - pathfinding: - specifier: ^0.4.18 - version: 0.4.18 - paths-js: - specifier: ^0.4.11 - version: 0.4.11 - react-scripts-ts: - specifier: ^3.1.0 - version: 3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.27.7))(babel-runtime@6.26.0)(typescript@5.8.3)(webpack-cli@6.0.1) - react-test-renderer: - specifier: ^19.1.0 - version: 19.1.1(react@18.2.0) - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - rollup: - specifier: ^4.41.0 - version: 4.46.1 - rollup-plugin-import-css: - specifier: ^3.5.8 - version: 3.5.8(rollup@4.46.1) - rollup-plugin-peer-deps-external: - specifier: ^2.2.4 - version: 2.2.4(rollup@4.46.1) - rollup-plugin-postcss: - specifier: ^4.0.2 - version: 4.0.2(postcss@8.5.6) - rollup-plugin-scss: - specifier: ^4.0.1 - version: 4.0.1 - rollup-plugin-svg: - specifier: ^2.0.0 - version: 2.0.0 - rollup-plugin-typescript2: - specifier: ^0.36.0 - version: 0.36.0(rollup@4.46.1)(typescript@5.8.3) - sass: - specifier: ^1.89.0 - version: 1.89.2 - sass-loader: - specifier: ^16.0.5 - version: 16.0.5(node-sass@9.0.0)(sass@1.89.2)(webpack@5.101.0) - storybook: - specifier: ^8.6.14 - version: 8.6.14(prettier@3.5.3) - style-loader: - specifier: ^4.0.0 - version: 4.0.0(webpack@5.101.0) - stylelint: - specifier: ^16.19.1 - version: 16.22.0(typescript@5.8.3) - stylelint-config-standard: - specifier: ^38.0.0 - version: 38.0.0(stylelint@16.22.0(typescript@5.8.3)) - svg-url-loader: - specifier: ^8.0.0 - version: 8.0.0(webpack@5.101.0) + react-hook-form: + specifier: ~7.56.3 + version: 7.56.4(react@18.2.0) ts-loader: specifier: ^9.5.2 version: 9.5.2(typescript@5.8.3)(webpack@5.101.0) @@ -1315,23 +1174,8 @@ importers: specifier: ^2.2.2 version: 2.2.2(tslint@6.1.3(typescript@5.8.3))(typescript@5.8.3) typescript: - specifier: 5.8.3 + specifier: ^5.8.3 version: 5.8.3 - vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) - vscode-uri: - specifier: ^3.1.0 - version: 3.1.0 - webpack: - specifier: ^5.94.0 - version: 5.101.0(webpack-cli@6.0.1) - webpack-cli: - specifier: ^6.0.1 - version: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.0) - webpack-dev-server: - specifier: ^5.2.1 - version: 5.2.2(webpack-cli@6.0.1)(webpack@5.101.0) ../../workspaces/ballerina/graphql: dependencies: @@ -1488,142 +1332,6 @@ importers: specifier: 18.2.0 version: 18.2.0 - ../../workspaces/ballerina/inline-data-mapper: - dependencies: - '@emotion/css': - specifier: ~11.13.5 - version: 11.13.5 - '@emotion/react': - specifier: ^11.14.0 - version: 11.14.0(@types/react@18.2.0)(react@18.2.0) - '@emotion/styled': - specifier: ^11.14.0 - version: 11.14.1(@emotion/react@11.14.0(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0) - '@projectstorm/geometry': - specifier: ^6.7.4 - version: 6.7.4 - '@projectstorm/react-canvas-core': - specifier: ^6.7.4 - version: 6.7.4(lodash@4.17.21)(react@18.2.0) - '@projectstorm/react-diagrams': - specifier: ^6.7.4 - version: 6.7.4(@emotion/react@11.14.0(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(dagre@0.8.5)(lodash@4.17.21)(pathfinding@0.4.18)(paths-js@0.4.11)(react@18.2.0)(resize-observer-polyfill@1.5.1) - '@projectstorm/react-diagrams-core': - specifier: ^6.7.4 - version: 6.7.4(lodash@4.17.21)(react@18.2.0)(resize-observer-polyfill@1.5.1) - '@tanstack/query-core': - specifier: ^5.77.1 - version: 5.83.0 - '@tanstack/react-query': - specifier: 5.77.1 - version: 5.77.1(react@18.2.0) - '@types/mousetrap': - specifier: ~1.6.15 - version: 1.6.15 - '@vscode/webview-ui-toolkit': - specifier: ^1.2.0 - version: 1.4.0(react@18.2.0) - '@wso2/ballerina-core': - specifier: workspace:* - version: link:../ballerina-core - '@wso2/ballerina-rpc-client': - specifier: workspace:* - version: link:../ballerina-rpc-client - '@wso2/ui-toolkit': - specifier: workspace:* - version: link:../../common-libs/ui-toolkit - blueimp-md5: - specifier: ^2.19.0 - version: 2.19.0 - classnames: - specifier: ^2.2.6 - version: 2.5.1 - lodash: - specifier: ^4.17.11 - version: 4.17.21 - lodash.debounce: - specifier: ^4.0.8 - version: 4.0.8 - mousetrap: - specifier: ^1.6.5 - version: 1.6.5 - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) - resize-observer-polyfill: - specifier: ^1.5.1 - version: 1.5.1 - vscode-languageserver-types: - specifier: ^3.17.5 - version: 3.17.5 - zustand: - specifier: ^5.0.4 - version: 5.0.6(@types/react@18.2.0)(react@18.2.0)(use-sync-external-store@1.5.0(react@18.2.0)) - devDependencies: - '@types/blueimp-md5': - specifier: ^2.18.2 - version: 2.18.2 - '@types/lodash': - specifier: 4.17.16 - version: 4.17.16 - '@types/lodash.debounce': - specifier: ^4.0.6 - version: 4.0.9 - '@types/react': - specifier: ^18.2.0 - version: 18.2.0 - '@types/react-dom': - specifier: 17.0.26 - version: 17.0.26(@types/react@18.2.0) - '@typescript-eslint/eslint-plugin': - specifier: ~8.32.1 - version: 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.27.0(jiti@2.5.1))(typescript@5.8.3) - '@typescript-eslint/parser': - specifier: ~8.32.1 - version: 8.32.1(eslint@9.27.0(jiti@2.5.1))(typescript@5.8.3) - copyfiles: - specifier: ^2.4.1 - version: 2.4.1 - css-loader: - specifier: ^7.1.2 - version: 7.1.2(webpack@5.101.0) - eslint: - specifier: ^9.26.0 - version: 9.27.0(jiti@2.5.1) - eslint-plugin-react-hooks: - specifier: ^5.2.0 - version: 5.2.0(eslint@9.27.0(jiti@2.5.1)) - eslint-plugin-react-refresh: - specifier: ^0.4.4 - version: 0.4.20(eslint@9.27.0(jiti@2.5.1)) - file-loader: - specifier: ^6.2.0 - version: 6.2.0(webpack@5.101.0) - react-hook-form: - specifier: ~7.56.3 - version: 7.56.4(react@18.2.0) - ts-loader: - specifier: ^9.5.2 - version: 9.5.2(typescript@5.8.3)(webpack@5.101.0) - tslib: - specifier: ^2.8.1 - version: 2.8.1 - tslint: - specifier: ^6.1.3 - version: 6.1.3(typescript@5.8.3) - tslint-react: - specifier: ^5.0.0 - version: 5.0.0(tslint@6.1.3(typescript@5.8.3))(typescript@5.8.3) - tslint-react-hooks: - specifier: ^2.2.2 - version: 2.2.2(tslint@6.1.3(typescript@5.8.3))(typescript@5.8.3) - typescript: - specifier: ^5.8.3 - version: 5.8.3 - ../../workspaces/ballerina/overview-view: dependencies: '@emotion/react': @@ -1871,7 +1579,7 @@ importers: version: 9.26.0(jiti@2.5.1) react-scripts-ts: specifier: 3.1.0 - version: 3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(babel-runtime@6.26.0)(typescript@5.8.3) + version: 3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.27.7))(babel-runtime@6.26.0)(typescript@5.8.3) typescript: specifier: 5.8.3 version: 5.8.3 @@ -1926,7 +1634,7 @@ importers: devDependencies: '@storybook/react': specifier: ^6.3.7 - version: 6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) + version: 6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) '@types/dagre': specifier: ~0.7.52 version: 0.7.53 @@ -2026,7 +1734,7 @@ importers: devDependencies: '@storybook/react': specifier: ^6.5.9 - version: 6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@4.9.5)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) + version: 6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@4.9.5)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) '@types/classnames': specifier: ^2.2.9 version: 2.3.4 @@ -2292,7 +2000,7 @@ importers: version: 9.26.0(jiti@2.5.1) react-scripts-ts: specifier: 3.1.0 - version: 3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(babel-runtime@6.26.0)(typescript@5.8.3) + version: 3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.27.7))(babel-runtime@6.26.0)(typescript@5.8.3) typescript: specifier: 5.8.3 version: 5.8.3 @@ -2781,7 +2489,7 @@ importers: version: 7.4.6(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) '@storybook/react-webpack5': specifier: ~7.4.0 - version: 7.4.6(@babel/core@7.28.0)(@swc/helpers@0.5.17)(@types/react-dom@18.2.0)(@types/react@18.2.0)(@types/webpack@5.28.5)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) + version: 7.4.6(@babel/core@7.27.7)(@swc/helpers@0.5.17)(@types/react-dom@18.2.0)(@types/react@18.2.0)(@types/webpack@5.28.5)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) '@types/react': specifier: 18.2.0 version: 18.2.0 @@ -2875,13 +2583,13 @@ importers: version: 8.6.14(@types/react@18.2.0)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3)) '@storybook/cli': specifier: ^9.0.12 - version: 9.0.18(@babel/preset-env@7.27.2(@babel/core@7.28.0))(@testing-library/dom@10.4.1)(prettier@3.5.3) + version: 9.0.18(@babel/preset-env@7.27.2(@babel/core@7.27.7))(@testing-library/dom@10.4.1)(prettier@3.5.3) '@storybook/react': specifier: ^9.0.12 version: 9.0.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(typescript@5.8.3) '@storybook/react-vite': specifier: ^9.0.12 - version: 9.0.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.46.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)) + version: 9.0.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.46.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(typescript@5.8.3) '@types/lodash': specifier: ~4.17.16 version: 4.17.17 @@ -2978,7 +2686,7 @@ importers: devDependencies: '@storybook/react': specifier: ^6.3.7 - version: 6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) + version: 6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) '@types/dagre': specifier: ~0.7.52 version: 0.7.53 @@ -3690,7 +3398,7 @@ importers: version: 1.52.0 '@pmmmwh/react-refresh-webpack-plugin': specifier: ~0.6.0 - version: 0.6.1(@types/webpack@5.28.5(webpack-cli@5.1.4))(react-refresh@0.17.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2)(webpack-hot-middleware@2.26.1)(webpack@5.101.0) + version: 0.6.1(@types/webpack@5.28.5(webpack-cli@5.1.4))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2)(webpack-hot-middleware@2.26.1)(webpack@5.101.0) '@tanstack/query-core': specifier: ^5.76.0 version: 5.83.0 @@ -3811,10 +3519,10 @@ importers: devDependencies: '@babel/plugin-syntax-flow': specifier: ~7.27.1 - version: 7.27.1(@babel/core@7.28.0) + version: 7.27.1(@babel/core@7.27.7) '@babel/preset-typescript': specifier: ~7.27.1 - version: 7.27.1(@babel/core@7.28.0) + version: 7.27.1(@babel/core@7.27.7) '@headlessui/react': specifier: ~2.2.4 version: 2.2.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -4236,6 +3944,12 @@ packages: '@adobe/css-tools@4.4.3': resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + '@ai-sdk/amazon-bedrock@2.2.12': + resolution: {integrity: sha512-m8gARnh45pr1s08Uu4J/Pm8913mwJPejPOm59b+kUqMsP9ilhUtH/bp8432Ra/v+vHuMoBrglG2ZvXtctAaH2g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + '@ai-sdk/anthropic@1.2.12': resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} engines: {node: '>=18'} @@ -4507,10 +4221,6 @@ packages: resolution: {integrity: sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.0': - resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.28.0': resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} engines: {node: '>=6.9.0'} @@ -5087,18 +4797,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx@7.27.1': resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} engines: {node: '>=6.9.0'} @@ -7039,9 +6737,6 @@ packages: selenium-webdriver: '>=4.6.1' typescript: '>=4.6.2' - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rollup/plugin-babel@5.3.1': resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -9677,12 +9372,6 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-react@4.7.0': - resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - '@vitest/expect@2.0.5': resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} @@ -10487,6 +10176,9 @@ packages: aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + axe-core@4.10.3: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} @@ -10828,9 +10520,6 @@ packages: babel-plugin-transform-strict-mode@6.24.1: resolution: {integrity: sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==} - babel-plugin-transform-typescript-metadata@0.3.2: - resolution: {integrity: sha512-mWEvCQTgXQf48yDqgN7CH50waTyYBeP2Lpqx4nNWab9sxEpdXVeKgfj1qYI2/TgUPQtNFZ85i3PemRtnXVYYJg==} - babel-preset-current-node-syntax@0.1.4: resolution: {integrity: sha512-5/INNCYhUGqw7VbVjT/hb3ucjgkVHKXY7lX3ZjlN4gm565VyFmJUrJ/h+h16ECVB38R/9SF6aACydpKMLZ/c9w==} peerDependencies: @@ -16593,12 +16282,6 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - monaco-editor-webpack-plugin@7.1.0: - resolution: {integrity: sha512-ZjnGINHN963JQkFqjjcBtn1XBtUATDZBMgNQhDQwd78w2ukRhFXAPNgWuacaQiDZsUr4h1rWv5Mv6eriKuOSzA==} - peerDependencies: - monaco-editor: '>= 0.31.0' - webpack: ^4.5.0 || 5.x - monaco-editor@0.44.0: resolution: {integrity: sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==} @@ -18463,10 +18146,6 @@ packages: resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==} engines: {node: '>=0.10.0'} - react-refresh@0.17.0: - resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} - engines: {node: '>=0.10.0'} - react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -18678,9 +18357,6 @@ packages: redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - reflect-metadata@0.1.14: - resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} - reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -20595,10 +20271,6 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - tsyringe@4.10.0: - resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} - engines: {node: '>= 6.0.0'} - ttf2eot@2.0.0: resolution: {integrity: sha512-U56aG2Ylw7psLOmakjemAzmpqVgeadwENg9oaDjaZG5NYX4WB6+7h74bNPcc+0BXsoU5A/XWiHabDXyzFOmsxQ==} hasBin: true @@ -21182,46 +20854,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite@6.3.5: - resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vsce@2.15.0: resolution: {integrity: sha512-P8E9LAZvBCQnoGoizw65JfGvyMqNGlHdlUXD1VAuxtvYAaHBKLBdKPnpy60XKVDAkQCfmMu53g+gq9FM+ydepw==} engines: {node: '>= 14'} @@ -21932,21 +21564,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zustand@4.5.7: - resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0.6' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - zustand@5.0.6: resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==} engines: {node: '>=12.20.0'} @@ -21975,6 +21592,15 @@ snapshots: '@adobe/css-tools@4.4.3': {} + '@ai-sdk/amazon-bedrock@2.2.12(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@smithy/eventstream-codec': 4.0.4 + '@smithy/util-utf8': 4.0.0 + aws4fetch: 1.0.20 + zod: 3.25.76 + '@ai-sdk/anthropic@1.2.12(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -22646,26 +22272,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.28.0': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.28.2 - '@babel/parser': 7.28.0 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.2 - convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/generator@7.28.0': dependencies: '@babel/parser': 7.28.0 @@ -22699,19 +22305,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.27.1 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.0 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -22719,14 +22312,6 @@ snapshots: regexpu-core: 6.2.0 semver: 6.3.1 - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - regexpu-core: 6.2.0 - semver: 6.3.1 - optional: true - '@babel/helper-define-polyfill-provider@0.0.3(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -22766,18 +22351,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1(supports-color@8.1.1) - lodash.debounce: 4.0.8 - resolve: 1.22.10 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/helper-globals@7.28.0': {} '@babel/helper-member-expression-to-functions@7.27.1': @@ -22812,15 +22385,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-optimise-call-expression@7.27.1': dependencies: '@babel/types': 7.28.2 @@ -22838,16 +22402,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -22857,15 +22411,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-member-expression-to-functions': 7.27.1 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.0 @@ -22904,37 +22449,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -22944,16 +22468,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -22962,15 +22476,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23036,11 +22541,6 @@ snapshots: dependencies: '@babel/core': 7.27.7 - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - optional: true - '@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23086,33 +22586,16 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23133,11 +22616,6 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23188,35 +22666,17 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23226,16 +22686,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23245,38 +22695,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23285,15 +22713,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23302,15 +22721,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-classes@7.28.0(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23323,32 +22733,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-globals': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 '@babel/template': 7.27.2 - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/template': 7.27.2 - optional: true - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23357,97 +22747,44 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.27.7) - '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23456,15 +22793,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23474,60 +22802,26 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23536,15 +22830,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23553,14 +22838,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23571,17 +22848,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23590,61 +22856,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23656,18 +22888,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23676,26 +22896,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23704,15 +22909,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.12.9)': dependencies: '@babel/core': 7.12.9 @@ -23723,12 +22919,6 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23737,15 +22927,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23755,37 +22936,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23793,23 +22953,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23821,75 +22964,33 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/types': 7.28.2 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23898,48 +22999,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -23951,67 +23025,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.7) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.27.1 - optional: true - '@babel/preset-env@7.27.2(@babel/core@7.27.7)': dependencies: '@babel/compat-data': 7.28.0 @@ -24087,82 +23123,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-env@7.27.2(@babel/core@7.28.0)': - dependencies: - '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0) - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-regenerator': 7.28.1(@babel/core@7.28.0) - '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.0) - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0) - babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.28.0) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) - core-js-compat: 3.44.0 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - optional: true - '@babel/preset-flow@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -24170,13 +23130,6 @@ snapshots: '@babel/helper-validator-option': 7.27.1 '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.27.7) - '@babel/preset-flow@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.0) - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -24184,14 +23137,6 @@ snapshots: '@babel/types': 7.28.2 esutils: 2.0.3 - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.2 - esutils: 2.0.3 - optional: true - '@babel/preset-react@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -24204,18 +23149,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-react@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -24227,17 +23160,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - '@babel/register@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -25313,12 +24235,11 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)': dependencies: glob: 10.4.5 magic-string: 0.30.17 react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) optionalDependencies: typescript: 5.8.3 @@ -25776,13 +24697,13 @@ snapshots: webpack-dev-server: 5.2.2(webpack@5.101.0) webpack-hot-middleware: 2.26.1 - '@pmmmwh/react-refresh-webpack-plugin@0.6.1(@types/webpack@5.28.5(webpack-cli@5.1.4))(react-refresh@0.17.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2)(webpack-hot-middleware@2.26.1)(webpack@5.101.0)': + '@pmmmwh/react-refresh-webpack-plugin@0.6.1(@types/webpack@5.28.5(webpack-cli@5.1.4))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2)(webpack-hot-middleware@2.26.1)(webpack@5.101.0)': dependencies: anser: 2.3.2 core-js-pure: 3.44.0 error-stack-parser: 2.1.4 html-entities: 2.6.0 - react-refresh: 0.17.0 + react-refresh: 0.11.0 schema-utils: 4.3.2 source-map: 0.7.6 webpack: 5.101.0(webpack-cli@5.1.4) @@ -26728,8 +25649,6 @@ snapshots: type-fest: 4.41.0 typescript: 5.8.3 - '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/plugin-babel@5.3.1(@babel/core@7.27.7)(@types/babel__core@7.20.5)(rollup@1.32.1)': dependencies: '@babel/core': 7.27.7 @@ -28266,12 +27185,11 @@ snapshots: - encoding - supports-color - '@storybook/builder-vite@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))': + '@storybook/builder-vite@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))': dependencies: '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3)) storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) '@storybook/builder-webpack4@6.5.16(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)': dependencies: @@ -28788,42 +27706,6 @@ snapshots: - uglify-js - webpack-cli - '@storybook/builder-webpack5@8.6.14(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(webpack-cli@6.0.1)': - dependencies: - '@storybook/core-webpack': 8.6.14(storybook@8.6.14(prettier@3.5.3)) - '@types/semver': 7.7.0 - browser-assert: 1.2.1 - case-sensitive-paths-webpack-plugin: 2.4.0 - cjs-module-lexer: 1.4.3 - constants-browserify: 1.0.0 - css-loader: 6.11.0(webpack@5.101.0) - es-module-lexer: 1.7.0 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.101.0) - html-webpack-plugin: 5.6.3(webpack@5.101.0) - magic-string: 0.30.17 - path-browserify: 1.0.1 - process: 0.11.10 - semver: 7.7.2 - storybook: 8.6.14(prettier@3.5.3) - style-loader: 3.3.4(webpack@5.101.0) - terser-webpack-plugin: 5.3.14(webpack@5.101.0) - ts-dedent: 2.2.0 - url: 0.11.4 - util: 0.12.5 - util-deprecate: 1.0.2 - webpack: 5.101.0(webpack-cli@6.0.1) - webpack-dev-middleware: 6.1.3(webpack@5.101.0) - webpack-hot-middleware: 2.26.1 - webpack-virtual-modules: 0.6.2 - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - '@rspack/core' - - '@swc/core' - - esbuild - - uglify-js - - webpack-cli - '@storybook/builder-webpack5@8.6.14(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(typescript@5.8.3)(webpack-cli@5.1.4)': dependencies: '@storybook/core-webpack': 8.6.14(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3)) @@ -28950,14 +27832,14 @@ snapshots: - supports-color - utf-8-validate - '@storybook/cli@9.0.18(@babel/preset-env@7.27.2(@babel/core@7.28.0))(@testing-library/dom@10.4.1)(prettier@3.5.3)': + '@storybook/cli@9.0.18(@babel/preset-env@7.27.2(@babel/core@7.27.7))(@testing-library/dom@10.4.1)(prettier@3.5.3)': dependencies: - '@storybook/codemod': 9.0.18(@babel/preset-env@7.27.2(@babel/core@7.28.0))(@testing-library/dom@10.4.1) + '@storybook/codemod': 9.0.18(@babel/preset-env@7.27.2(@babel/core@7.27.7))(@testing-library/dom@10.4.1) '@types/semver': 7.7.0 commander: 12.1.0 create-storybook: 9.0.18 giget: 1.2.5 - jscodeshift: 0.15.2(@babel/preset-env@7.27.2(@babel/core@7.28.0)) + jscodeshift: 0.15.2(@babel/preset-env@7.27.2(@babel/core@7.27.7)) storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3) ts-dedent: 2.2.0 transitivePeerDependencies: @@ -29055,13 +27937,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/codemod@9.0.18(@babel/preset-env@7.27.2(@babel/core@7.28.0))(@testing-library/dom@10.4.1)': + '@storybook/codemod@9.0.18(@babel/preset-env@7.27.2(@babel/core@7.27.7))(@testing-library/dom@10.4.1)': dependencies: '@types/cross-spawn': 6.0.6 cross-spawn: 7.0.6 es-toolkit: 1.39.8 globby: 14.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.27.2(@babel/core@7.28.0)) + jscodeshift: 0.15.2(@babel/preset-env@7.27.2(@babel/core@7.27.7)) prettier: 3.5.3 storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3) tiny-invariant: 1.3.3 @@ -30672,10 +29554,10 @@ snapshots: '@storybook/postinstall@7.4.6': {} - '@storybook/preset-react-webpack@7.4.6(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': + '@storybook/preset-react-webpack@7.4.6(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': dependencies: - '@babel/preset-flow': 7.27.1(@babel/core@7.28.0) - '@babel/preset-react': 7.27.1(@babel/core@7.28.0) + '@babel/preset-flow': 7.27.1(@babel/core@7.27.7) + '@babel/preset-react': 7.27.1(@babel/core@7.27.7) '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@5.28.5)(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)(webpack@5.101.0) '@storybook/core-webpack': 7.4.6(encoding@0.1.13) '@storybook/docs-tools': 7.4.6(encoding@0.1.13) @@ -30693,7 +29575,7 @@ snapshots: semver: 7.7.2 webpack: 5.101.0(webpack-cli@5.1.4) optionalDependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.27.7 typescript: 5.8.3 transitivePeerDependencies: - '@swc/core' @@ -30924,11 +29806,11 @@ snapshots: react-dom: 19.1.0(react@19.1.0) storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3) - '@storybook/react-vite@9.0.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.46.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))': + '@storybook/react-vite@9.0.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.46.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(typescript@5.8.3)': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3) '@rollup/pluginutils': 5.2.0(rollup@4.46.1) - '@storybook/builder-vite': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)) + '@storybook/builder-vite': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3)) '@storybook/react': 9.0.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3))(typescript@5.8.3) find-up: 7.0.0 magic-string: 0.30.17 @@ -30938,22 +29820,21 @@ snapshots: resolve: 1.22.10 storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.5.3) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react-webpack5@7.4.6(@babel/core@7.28.0)(@swc/helpers@0.5.17)(@types/react-dom@18.2.0)(@types/react@18.2.0)(@types/webpack@5.28.5)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': + '@storybook/react-webpack5@7.4.6(@babel/core@7.27.7)(@swc/helpers@0.5.17)(@types/react-dom@18.2.0)(@types/react@18.2.0)(@types/webpack@5.28.5)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': dependencies: '@storybook/builder-webpack5': 7.4.6(@swc/helpers@0.5.17)(@types/react-dom@18.2.0)(@types/react@18.2.0)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) - '@storybook/preset-react-webpack': 7.4.6(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) + '@storybook/preset-react-webpack': 7.4.6(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1) '@storybook/react': 7.4.6(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) '@types/node': 16.18.126 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.27.7 typescript: 5.8.3 transitivePeerDependencies: - '@rspack/core' @@ -31050,11 +29931,199 @@ snapshots: require-from-string: 2.0.2 ts-dedent: 2.2.0 util-deprecate: 1.0.2 - webpack: 5.101.0(webpack-cli@4.10.0) + webpack: 5.101.0(webpack-cli@4.10.0) + optionalDependencies: + '@babel/core': 7.27.7 + '@storybook/builder-webpack5': 6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) + '@storybook/manager-webpack5': 6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) + typescript: 5.8.3 + transitivePeerDependencies: + - '@storybook/mdx2-csf' + - '@swc/core' + - '@types/webpack' + - bufferutil + - encoding + - esbuild + - eslint + - sockjs-client + - supports-color + - type-fest + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-dev-server + - webpack-hot-middleware + - webpack-plugin-serve + + '@storybook/react@6.5.16(@babel/core@7.27.7)(@storybook/builder-webpack5@6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1))(@storybook/manager-webpack5@6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1))(@types/webpack@5.28.5(webpack-cli@6.0.1))(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack-hot-middleware@2.26.1)': + dependencies: + '@babel/preset-flow': 7.27.1(@babel/core@7.27.7) + '@babel/preset-react': 7.27.1(@babel/core@7.27.7) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@5.28.5(webpack-cli@6.0.1))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2)(webpack-hot-middleware@2.26.1)(webpack@5.101.0) + '@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@storybook/client-logger': 6.5.16 + '@storybook/core': 6.5.16(@storybook/builder-webpack5@6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1))(@storybook/manager-webpack5@6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1))(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1)(webpack@5.101.0) + '@storybook/core-common': 6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1) + '@storybook/csf': 0.0.2--canary.4566f4d.1 + '@storybook/docs-tools': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@storybook/node-logger': 6.5.16 + '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.101.0) + '@storybook/semver': 7.3.2 + '@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@types/estree': 0.0.51 + '@types/node': 16.18.126 + '@types/webpack-env': 1.18.8 + acorn: 7.4.1 + acorn-jsx: 5.3.2(acorn@7.4.1) + acorn-walk: 7.2.0 + babel-plugin-add-react-displayname: 0.0.5 + babel-plugin-react-docgen: 4.2.1 + core-js: 3.44.0 + escodegen: 2.1.0 + fs-extra: 9.1.0 + global: 4.4.0 + html-tags: 3.3.1 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-element-to-jsx-string: 14.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-refresh: 0.11.0 + read-pkg-up: 7.0.1 + regenerator-runtime: 0.13.11 + require-from-string: 2.0.2 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + webpack: 5.101.0(webpack-cli@6.0.1) + optionalDependencies: + '@babel/core': 7.27.7 + '@storybook/builder-webpack5': 6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1) + '@storybook/manager-webpack5': 6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1) + typescript: 5.8.3 + transitivePeerDependencies: + - '@storybook/mdx2-csf' + - '@swc/core' + - '@types/webpack' + - bufferutil + - encoding + - esbuild + - eslint + - sockjs-client + - supports-color + - type-fest + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-dev-server + - webpack-hot-middleware + - webpack-plugin-serve + + '@storybook/react@6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': + dependencies: + '@babel/preset-flow': 7.27.1(@babel/core@7.27.7) + '@babel/preset-react': 7.27.1(@babel/core@7.27.7) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@5.28.5)(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)(webpack@5.101.0) + '@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@storybook/client-logger': 6.5.16 + '@storybook/core': 6.5.16(encoding@0.1.13)(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.101.0) + '@storybook/core-common': 6.5.16(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3) + '@storybook/csf': 0.0.2--canary.4566f4d.1 + '@storybook/docs-tools': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@storybook/node-logger': 6.5.16 + '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.101.0) + '@storybook/semver': 7.3.2 + '@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@types/estree': 0.0.51 + '@types/node': 16.18.126 + '@types/webpack-env': 1.18.8 + acorn: 7.4.1 + acorn-jsx: 5.3.2(acorn@7.4.1) + acorn-walk: 7.2.0 + babel-plugin-add-react-displayname: 0.0.5 + babel-plugin-react-docgen: 4.2.1 + core-js: 3.44.0 + escodegen: 2.1.0 + fs-extra: 9.1.0 + global: 4.4.0 + html-tags: 3.3.1 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-element-to-jsx-string: 14.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-refresh: 0.11.0 + read-pkg-up: 7.0.1 + regenerator-runtime: 0.13.11 + require-from-string: 2.0.2 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + webpack: 5.101.0(webpack-cli@5.1.4) + optionalDependencies: + '@babel/core': 7.27.7 + typescript: 5.8.3 + transitivePeerDependencies: + - '@storybook/mdx2-csf' + - '@swc/core' + - '@types/webpack' + - bufferutil + - encoding + - esbuild + - eslint + - sockjs-client + - supports-color + - type-fest + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-dev-server + - webpack-hot-middleware + - webpack-plugin-serve + + '@storybook/react@6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': + dependencies: + '@babel/preset-flow': 7.27.1(@babel/core@7.27.7) + '@babel/preset-react': 7.27.1(@babel/core@7.27.7) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@5.28.5)(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)(webpack@5.101.0) + '@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@storybook/client-logger': 6.5.16 + '@storybook/core': 6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.101.0) + '@storybook/core-common': 6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3) + '@storybook/csf': 0.0.2--canary.4566f4d.1 + '@storybook/docs-tools': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@storybook/node-logger': 6.5.16 + '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.101.0) + '@storybook/semver': 7.3.2 + '@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@types/estree': 0.0.51 + '@types/node': 16.18.126 + '@types/webpack-env': 1.18.8 + acorn: 7.4.1 + acorn-jsx: 5.3.2(acorn@7.4.1) + acorn-walk: 7.2.0 + babel-plugin-add-react-displayname: 0.0.5 + babel-plugin-react-docgen: 4.2.1 + core-js: 3.44.0 + escodegen: 2.1.0 + fs-extra: 9.1.0 + global: 4.4.0 + html-tags: 3.3.1 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-element-to-jsx-string: 14.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-refresh: 0.11.0 + read-pkg-up: 7.0.1 + regenerator-runtime: 0.13.11 + require-from-string: 2.0.2 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + webpack: 5.101.0(webpack-cli@5.1.4) optionalDependencies: '@babel/core': 7.27.7 - '@storybook/builder-webpack5': 6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) - '@storybook/manager-webpack5': 6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) typescript: 5.8.3 transitivePeerDependencies: - '@storybook/mdx2-csf' @@ -31075,198 +30144,10 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/react@6.5.16(@babel/core@7.27.7)(@storybook/builder-webpack5@6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1))(@storybook/manager-webpack5@6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1))(@types/webpack@5.28.5(webpack-cli@6.0.1))(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack-hot-middleware@2.26.1)': + '@storybook/react@6.5.16(@babel/core@7.27.7)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@4.9.5)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': dependencies: '@babel/preset-flow': 7.27.1(@babel/core@7.27.7) '@babel/preset-react': 7.27.1(@babel/core@7.27.7) - '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@5.28.5(webpack-cli@6.0.1))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2)(webpack-hot-middleware@2.26.1)(webpack@5.101.0) - '@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@storybook/client-logger': 6.5.16 - '@storybook/core': 6.5.16(@storybook/builder-webpack5@6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1))(@storybook/manager-webpack5@6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1))(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1)(webpack@5.101.0) - '@storybook/core-common': 6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1) - '@storybook/csf': 0.0.2--canary.4566f4d.1 - '@storybook/docs-tools': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@storybook/node-logger': 6.5.16 - '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.101.0) - '@storybook/semver': 7.3.2 - '@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@types/estree': 0.0.51 - '@types/node': 16.18.126 - '@types/webpack-env': 1.18.8 - acorn: 7.4.1 - acorn-jsx: 5.3.2(acorn@7.4.1) - acorn-walk: 7.2.0 - babel-plugin-add-react-displayname: 0.0.5 - babel-plugin-react-docgen: 4.2.1 - core-js: 3.44.0 - escodegen: 2.1.0 - fs-extra: 9.1.0 - global: 4.4.0 - html-tags: 3.3.1 - lodash: 4.17.21 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-element-to-jsx-string: 14.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - react-refresh: 0.11.0 - read-pkg-up: 7.0.1 - regenerator-runtime: 0.13.11 - require-from-string: 2.0.2 - ts-dedent: 2.2.0 - util-deprecate: 1.0.2 - webpack: 5.101.0(webpack-cli@6.0.1) - optionalDependencies: - '@babel/core': 7.27.7 - '@storybook/builder-webpack5': 6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1) - '@storybook/manager-webpack5': 6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@6.0.1) - typescript: 5.8.3 - transitivePeerDependencies: - - '@storybook/mdx2-csf' - - '@swc/core' - - '@types/webpack' - - bufferutil - - encoding - - esbuild - - eslint - - sockjs-client - - supports-color - - type-fest - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - webpack-dev-server - - webpack-hot-middleware - - webpack-plugin-serve - - '@storybook/react@6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': - dependencies: - '@babel/preset-flow': 7.27.1(@babel/core@7.28.0) - '@babel/preset-react': 7.27.1(@babel/core@7.28.0) - '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@5.28.5)(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)(webpack@5.101.0) - '@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@storybook/client-logger': 6.5.16 - '@storybook/core': 6.5.16(encoding@0.1.13)(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.101.0) - '@storybook/core-common': 6.5.16(eslint@9.26.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3) - '@storybook/csf': 0.0.2--canary.4566f4d.1 - '@storybook/docs-tools': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@storybook/node-logger': 6.5.16 - '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.101.0) - '@storybook/semver': 7.3.2 - '@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@types/estree': 0.0.51 - '@types/node': 16.18.126 - '@types/webpack-env': 1.18.8 - acorn: 7.4.1 - acorn-jsx: 5.3.2(acorn@7.4.1) - acorn-walk: 7.2.0 - babel-plugin-add-react-displayname: 0.0.5 - babel-plugin-react-docgen: 4.2.1 - core-js: 3.44.0 - escodegen: 2.1.0 - fs-extra: 9.1.0 - global: 4.4.0 - html-tags: 3.3.1 - lodash: 4.17.21 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-element-to-jsx-string: 14.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - react-refresh: 0.11.0 - read-pkg-up: 7.0.1 - regenerator-runtime: 0.13.11 - require-from-string: 2.0.2 - ts-dedent: 2.2.0 - util-deprecate: 1.0.2 - webpack: 5.101.0(webpack-cli@5.1.4) - optionalDependencies: - '@babel/core': 7.28.0 - typescript: 5.8.3 - transitivePeerDependencies: - - '@storybook/mdx2-csf' - - '@swc/core' - - '@types/webpack' - - bufferutil - - encoding - - esbuild - - eslint - - sockjs-client - - supports-color - - type-fest - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - webpack-dev-server - - webpack-hot-middleware - - webpack-plugin-serve - - '@storybook/react@6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@5.8.3)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': - dependencies: - '@babel/preset-flow': 7.27.1(@babel/core@7.28.0) - '@babel/preset-react': 7.27.1(@babel/core@7.28.0) - '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@5.28.5)(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)(webpack@5.101.0) - '@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@storybook/client-logger': 6.5.16 - '@storybook/core': 6.5.16(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.101.0) - '@storybook/core-common': 6.5.16(eslint@9.27.0(jiti@2.5.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3) - '@storybook/csf': 0.0.2--canary.4566f4d.1 - '@storybook/docs-tools': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@storybook/node-logger': 6.5.16 - '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.101.0) - '@storybook/semver': 7.3.2 - '@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@types/estree': 0.0.51 - '@types/node': 16.18.126 - '@types/webpack-env': 1.18.8 - acorn: 7.4.1 - acorn-jsx: 5.3.2(acorn@7.4.1) - acorn-walk: 7.2.0 - babel-plugin-add-react-displayname: 0.0.5 - babel-plugin-react-docgen: 4.2.1 - core-js: 3.44.0 - escodegen: 2.1.0 - fs-extra: 9.1.0 - global: 4.4.0 - html-tags: 3.3.1 - lodash: 4.17.21 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-element-to-jsx-string: 14.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - react-refresh: 0.11.0 - read-pkg-up: 7.0.1 - regenerator-runtime: 0.13.11 - require-from-string: 2.0.2 - ts-dedent: 2.2.0 - util-deprecate: 1.0.2 - webpack: 5.101.0(webpack-cli@5.1.4) - optionalDependencies: - '@babel/core': 7.28.0 - typescript: 5.8.3 - transitivePeerDependencies: - - '@storybook/mdx2-csf' - - '@swc/core' - - '@types/webpack' - - bufferutil - - encoding - - esbuild - - eslint - - sockjs-client - - supports-color - - type-fest - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - webpack-dev-server - - webpack-hot-middleware - - webpack-plugin-serve - - '@storybook/react@6.5.16(@babel/core@7.28.0)(@types/webpack@5.28.5)(encoding@0.1.13)(eslint@9.27.0(jiti@2.5.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(require-from-string@2.0.2)(type-fest@4.41.0)(typescript@4.9.5)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)': - dependencies: - '@babel/preset-flow': 7.27.1(@babel/core@7.28.0) - '@babel/preset-react': 7.27.1(@babel/core@7.28.0) '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@5.28.5)(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.2(webpack@5.101.0))(webpack-hot-middleware@2.26.1)(webpack@5.101.0) '@storybook/addons': 6.5.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@storybook/client-logger': 6.5.16 @@ -31304,7 +30185,7 @@ snapshots: util-deprecate: 1.0.2 webpack: 5.101.0(webpack-cli@5.1.4) optionalDependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.27.7 typescript: 4.9.5 transitivePeerDependencies: - '@storybook/mdx2-csf' @@ -33560,18 +32441,6 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))': - dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) - '@rolldown/pluginutils': 1.0.0-beta.27 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) - transitivePeerDependencies: - - supports-color - '@vitest/expect@2.0.5': dependencies: '@vitest/spy': 2.0.5 @@ -33884,7 +32753,7 @@ snapshots: '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.101.0)': dependencies: webpack: 5.101.0(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.101.0) + webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.0) '@webpack-cli/info@1.5.0(webpack-cli@4.10.0)': dependencies: @@ -33899,7 +32768,7 @@ snapshots: '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.101.0)': dependencies: webpack: 5.101.0(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.101.0) + webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.0) '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)': dependencies: @@ -34500,6 +33369,8 @@ snapshots: aws4@1.13.2: {} + aws4fetch@1.0.20: {} + axe-core@4.10.3: {} axios@1.9.0: @@ -34554,10 +33425,6 @@ snapshots: dependencies: '@babel/core': 7.27.7 - babel-core@7.0.0-bridge.0(@babel/core@7.28.0): - dependencies: - '@babel/core': 7.28.0 - babel-eslint@10.1.0(eslint@6.8.0): dependencies: '@babel/code-frame': 7.27.1 @@ -34711,14 +33578,6 @@ snapshots: mkdirp: 0.5.6 webpack: 5.101.0(webpack-cli@6.0.1) - babel-loader@7.1.2(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(webpack@5.101.0): - dependencies: - babel-core: 7.0.0-bridge.0(@babel/core@7.28.0) - find-cache-dir: 1.0.0 - loader-utils: 1.4.2 - mkdirp: 0.5.6 - webpack: 5.101.0(webpack-cli@5.1.4) - babel-loader@8.4.1(@babel/core@7.27.7)(webpack@5.101.0): dependencies: '@babel/core': 7.27.7 @@ -34826,16 +33685,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.0): - dependencies: - '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - optional: true - babel-plugin-polyfill-corejs3@0.1.7(@babel/core@7.27.7): dependencies: '@babel/core': 7.27.7 @@ -34852,15 +33701,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.28.0): - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) - core-js-compat: 3.44.0 - transitivePeerDependencies: - - supports-color - optional: true - babel-plugin-polyfill-regenerator@0.0.4(@babel/core@7.27.7): dependencies: '@babel/core': 7.27.7 @@ -34875,14 +33715,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.0): - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - optional: true - babel-plugin-react-docgen@4.2.1: dependencies: ast-types: 0.14.2 @@ -35099,10 +33931,6 @@ snapshots: babel-runtime: 6.26.0 babel-types: 6.26.0 - babel-plugin-transform-typescript-metadata@0.3.2: - dependencies: - '@babel/helper-plugin-utils': 7.27.1 - babel-preset-current-node-syntax@0.1.4(@babel/core@7.27.7): dependencies: '@babel/core': 7.27.7 @@ -36484,7 +35312,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.101.0(webpack-cli@6.0.1) + webpack: 5.101.0(webpack-cli@5.1.4) css-loader@7.1.2(webpack@5.101.0): dependencies: @@ -38611,7 +37439,7 @@ snapshots: semver: 7.7.2 tapable: 2.2.2 typescript: 5.8.3 - webpack: 5.101.0(webpack-cli@6.0.1) + webpack: 5.101.0(webpack-cli@5.1.4) fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.101.0): dependencies: @@ -40849,10 +39677,7 @@ snapshots: pretty-format: 25.5.0 throat: 5.0.0 transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - utf-8-validate jest-leak-detector@25.5.0: dependencies: @@ -41455,33 +40280,6 @@ snapshots: transitivePeerDependencies: - supports-color - jscodeshift@0.15.2(@babel/preset-env@7.27.2(@babel/core@7.28.0)): - dependencies: - '@babel/core': 7.27.7 - '@babel/parser': 7.28.0 - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.7) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.7) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.7) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.27.7) - '@babel/preset-flow': 7.27.1(@babel/core@7.27.7) - '@babel/preset-typescript': 7.27.1(@babel/core@7.27.7) - '@babel/register': 7.27.1(@babel/core@7.27.7) - babel-core: 7.0.0-bridge.0(@babel/core@7.27.7) - chalk: 4.1.2 - flow-parser: 0.277.1 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - neo-async: 2.6.2 - node-dir: 0.1.17 - recast: 0.23.11 - temp: 0.8.4 - write-file-atomic: 2.4.3 - optionalDependencies: - '@babel/preset-env': 7.27.2(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - jsdoc-type-pratt-parser@4.1.0: {} jsdom@11.12.0: @@ -43088,12 +41886,6 @@ snapshots: moment@2.30.1: {} - monaco-editor-webpack-plugin@7.1.0(monaco-editor@0.52.2)(webpack@5.101.0): - dependencies: - loader-utils: 2.0.4 - monaco-editor: 0.52.2 - webpack: 5.101.0(webpack-cli@6.0.1) - monaco-editor@0.44.0: {} monaco-editor@0.46.0: {} @@ -45236,8 +44028,6 @@ snapshots: react-refresh@0.11.0: {} - react-refresh@0.17.0: {} - react-remove-scroll-bar@2.3.8(@types/react@18.2.0)(react@18.2.0): dependencies: react: 18.2.0 @@ -45276,7 +44066,7 @@ snapshots: optionalDependencies: '@types/react': 18.2.0 - react-scripts-ts@3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.27.7))(babel-runtime@6.26.0)(typescript@5.8.3)(webpack-cli@6.0.1): + react-scripts-ts@3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.27.7))(babel-runtime@6.26.0)(typescript@5.8.3): dependencies: autoprefixer: 7.1.6 babel-jest: 20.0.3 @@ -45312,8 +44102,8 @@ snapshots: typescript: 5.8.3 uglifyjs-webpack-plugin: 1.2.5(webpack@5.101.0) url-loader: 0.6.2(file-loader@1.1.5(webpack@5.101.0)) - webpack: 5.101.0(webpack-cli@6.0.1) - webpack-dev-server: 5.2.2(webpack-cli@6.0.1)(webpack@5.101.0) + webpack: 5.101.0(webpack-cli@5.1.4) + webpack-dev-server: 5.2.2(webpack@5.101.0) webpack-manifest-plugin: 1.3.2(webpack@5.101.0) whatwg-fetch: 2.0.3 optionalDependencies: @@ -45330,11 +44120,11 @@ snapshots: - utf-8-validate - webpack-cli - react-scripts-ts@3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(babel-runtime@6.26.0)(typescript@5.8.3): + react-scripts-ts@3.1.0(babel-core@7.0.0-bridge.0(@babel/core@7.27.7))(babel-runtime@6.26.0)(typescript@5.8.3)(webpack-cli@6.0.1): dependencies: autoprefixer: 7.1.6 babel-jest: 20.0.3 - babel-loader: 7.1.2(babel-core@7.0.0-bridge.0(@babel/core@7.28.0))(webpack@5.101.0) + babel-loader: 7.1.2(babel-core@7.0.0-bridge.0(@babel/core@7.27.7))(webpack@5.101.0) babel-preset-react-app: 3.1.2(babel-runtime@6.26.0) case-sensitive-paths-webpack-plugin: 2.1.1 chalk: 1.1.3 @@ -45366,8 +44156,8 @@ snapshots: typescript: 5.8.3 uglifyjs-webpack-plugin: 1.2.5(webpack@5.101.0) url-loader: 0.6.2(file-loader@1.1.5(webpack@5.101.0)) - webpack: 5.101.0(webpack-cli@5.1.4) - webpack-dev-server: 5.2.2(webpack@5.101.0) + webpack: 5.101.0(webpack-cli@6.0.1) + webpack-dev-server: 5.2.2(webpack-cli@6.0.1)(webpack@5.101.0) webpack-manifest-plugin: 1.3.2(webpack@5.101.0) whatwg-fetch: 2.0.3 optionalDependencies: @@ -45624,8 +44414,6 @@ snapshots: redux@5.0.1: {} - reflect-metadata@0.1.14: {} - reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -47083,7 +45871,7 @@ snapshots: style-loader@3.3.4(webpack@5.101.0): dependencies: - webpack: 5.101.0(webpack-cli@6.0.1) + webpack: 5.101.0(webpack-cli@5.1.4) style-loader@4.0.0(webpack@5.101.0): dependencies: @@ -48179,10 +46967,6 @@ snapshots: tslib: 1.14.1 typescript: 5.8.3 - tsyringe@4.10.0: - dependencies: - tslib: 1.14.1 - ttf2eot@2.0.0: dependencies: argparse: 1.0.10 @@ -48810,22 +47594,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@6.3.5(@types/node@22.15.35)(jiti@2.5.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0): - dependencies: - esbuild: 0.25.8 - fdir: 6.4.6(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.46.1 - tinyglobby: 0.2.14 - optionalDependencies: - '@types/node': 22.15.35 - fsevents: 2.3.3 - jiti: 2.5.1 - sass: 1.89.2 - terser: 5.43.1 - yaml: 2.8.0 - vsce@2.15.0: dependencies: azure-devops-node-api: 11.2.0 @@ -49183,7 +47951,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.2 optionalDependencies: - webpack: 5.101.0(webpack-cli@6.0.1) + webpack: 5.101.0(webpack-cli@5.1.4) webpack-dev-middleware@7.4.2(webpack@5.101.0): dependencies: @@ -49541,7 +48309,7 @@ snapshots: watchpack: 2.4.4 webpack-sources: 3.3.3 optionalDependencies: - webpack-cli: 6.0.1(webpack@5.101.0) + webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.0) transitivePeerDependencies: - '@swc/core' - esbuild @@ -49980,13 +48748,6 @@ snapshots: zod@3.25.76: {} - zustand@4.5.7(@types/react@18.2.0)(react@18.2.0): - dependencies: - use-sync-external-store: 1.5.0(react@18.2.0) - optionalDependencies: - '@types/react': 18.2.0 - react: 18.2.0 - zustand@5.0.6(@types/react@18.2.0)(react@18.2.0)(use-sync-external-store@1.5.0(react@18.2.0)): optionalDependencies: '@types/react': 18.2.0 diff --git a/rush.json b/rush.json index 659531a05d5..e66b7a74a49 100644 --- a/rush.json +++ b/rush.json @@ -491,8 +491,8 @@ "versionPolicyName": "ballerina-extension" }, { - "packageName": "@wso2/data-mapper-view", - "projectFolder": "workspaces/ballerina/data-mapper-view", + "packageName": "@wso2/ballerina-data-mapper", + "projectFolder": "workspaces/ballerina/data-mapper", "versionPolicyName": "ballerina-extension" }, { @@ -590,12 +590,7 @@ "packageName": "@wso2/type-editor", "projectFolder": "workspaces/ballerina/type-editor", "versionPolicyName": "ballerina-extension" - }, - { - "packageName": "@wso2/ballerina-inline-data-mapper", - "projectFolder": "workspaces/ballerina/inline-data-mapper", - "versionPolicyName": "ballerina-extension" - }, + } // { // "packageName": "@wso2/api-designer-core", // "projectFolder": "workspaces/api-designer/api-designer-core" diff --git a/workspaces/ballerina/ballerina-core/src/index.ts b/workspaces/ballerina/ballerina-core/src/index.ts index 62823e8ee77..2709acffb34 100644 --- a/workspaces/ballerina/ballerina-core/src/index.ts +++ b/workspaces/ballerina/ballerina-core/src/index.ts @@ -35,7 +35,7 @@ export * from "./interfaces/store"; export * from "./interfaces/performance"; export * from "./interfaces/extended-lang-client"; export * from "./interfaces/service"; -export * from "./interfaces/inline-data-mapper"; +export * from "./interfaces/data-mapper"; // ------ LS Utils --------> export * from "./ls-utils/WSConnection"; @@ -71,6 +71,9 @@ export * from "./rpc-types/lang-client/interfaces"; export * from "./rpc-types/library-browser"; export * from "./rpc-types/library-browser/rpc-type"; export * from "./rpc-types/library-browser/interfaces"; +export * from "./rpc-types/migrate-integration"; +export * from "./rpc-types/migrate-integration/rpc-type"; +export * from "./rpc-types/migrate-integration/interfaces"; export * from "./rpc-types/common"; export * from "./rpc-types/common/rpc-type"; export * from "./rpc-types/common/interfaces"; @@ -79,9 +82,8 @@ export * from "./rpc-types/persist-diagram/rpc-type"; export * from "./rpc-types/ai-panel"; export * from "./rpc-types/ai-panel/rpc-type"; export * from "./rpc-types/ai-panel/interfaces"; -export * from "./rpc-types/inline-data-mapper"; -export * from "./rpc-types/inline-data-mapper/rpc-type"; -export * from "./rpc-types/inline-data-mapper/interfaces"; +export * from "./rpc-types/data-mapper"; +export * from "./rpc-types/data-mapper/rpc-type"; export * from "./rpc-types/test-manager"; export * from "./rpc-types/test-manager/rpc-type"; export * from "./rpc-types/icp-service"; diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/ai-panel.ts b/workspaces/ballerina/ballerina-core/src/interfaces/ai-panel.ts index a94028af5bc..54415539d0a 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/ai-panel.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/ai-panel.ts @@ -25,6 +25,7 @@ export enum Command { Ask = '/ask', NaturalProgramming = '/natural-programming (experimental)', OpenAPI = '/openapi', + Doc = '/doc' } export enum TemplateId { @@ -52,4 +53,7 @@ export enum TemplateId { GenerateCodeFromRequirements = 'generate-code-from-requirements', GenerateTestFromRequirements = 'generate-test-from-requirements', GenerateCodeFromFollowingRequirements = 'generate-code-from-following-requirements', + + // Command.Doc + GenerateUserDoc = 'generate-user-doc' } diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts b/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts index 9c1c2db2be4..7f4a0d5b61d 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts @@ -87,6 +87,7 @@ export type NodeMetadata = { agent?: AgentData; paramsToHide?: string[]; // List of properties keys to to hide from forms module?: string; + type?: string; }; export type ParentMetadata = { @@ -307,6 +308,7 @@ export type NodePropertyKey = | "collection" | "comment" | "condition" + | "matchTarget" | "configValue" | "connection" | "defaultable" @@ -393,6 +395,10 @@ export type NodeKind = | "VECTOR_KNOWLEDGE_BASES" | "EMBEDDING_PROVIDER" | "EMBEDDING_PROVIDERS" + | "DATA_LOADER" + | "DATA_LOADERS" + | "CHUNKER" + | "CHUNKERS" | "NEW_CONNECTION" | "NEW_DATA" | "NP_FUNCTION" @@ -414,7 +420,8 @@ export type NodeKind = | "VARIABLE" | "WAIT" | "WHILE" - | "WORKER"; + | "WORKER" + | "VARIABLE"; export type OverviewFlow = { entryPoints: EntryPoint[]; diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/common.ts b/workspaces/ballerina/ballerina-core/src/interfaces/common.ts index fda696e7ab3..c698130f541 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/common.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/common.ts @@ -30,7 +30,6 @@ export declare enum BallerinaComponentTypes { } export enum SubPanelView { - INLINE_DATA_MAPPER = "inlineDataMapper", HELPER_PANEL = "helperPanel", ADD_NEW_FORM = "addNewForm", UNDEFINED = undefined, @@ -75,7 +74,6 @@ export interface SubPanel { } export interface SubPanelViewProps { - inlineDataMapper?: InlineDataMapperProps; sidePanelData?: SidePanelData; } @@ -93,11 +91,3 @@ export interface ConfigurePanelData { documentation?: string; value?: string; } - -interface InlineDataMapperProps { - filePath: string; - flowNode: FlowNode; - propertyKey: string; - editorKey: string; - position: LinePosition; -} diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts b/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts index c75642b9b13..b35efce4625 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts @@ -35,6 +35,7 @@ export const BI_COMMANDS = { FOCUS_PROJECT_EXPLORER: 'BI.project-explorer.focus', PROJECT_EXPLORER: 'BI.project-explorer', ADD_CONNECTIONS: 'BI.project-explorer.add-connection', + ADD_CUSTOM_CONNECTOR: 'BI.project-explorer.add-custom-connector', DELETE_COMPONENT: 'BI.project-explorer.delete', ADD_ENTRY_POINT: 'BI.project-explorer.add-entry-point', ADD_TYPE: 'BI.project-explorer.add-type', @@ -51,4 +52,5 @@ export const BI_COMMANDS = { BI_ADD_TEST_FUNCTION: 'BI.test.add.function', 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', }; diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/inline-data-mapper.ts b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts similarity index 83% rename from workspaces/ballerina/ballerina-core/src/interfaces/inline-data-mapper.ts rename to workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts index bd5c2e849fd..c6f50761ac7 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/inline-data-mapper.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts @@ -28,6 +28,7 @@ export enum TypeKind { Decimal = "decimal", Boolean = "boolean", Enum = "enum", + Union = "union", Unknown = "$CompilationError$", Anydata = "anydata", Byte = "byte", @@ -56,7 +57,7 @@ export enum ResultClauseType { COLLECT = "collect" } -export interface IDMDiagnostic { +export interface DMDiagnostic { kind: string; message: string; range: { @@ -83,14 +84,18 @@ export interface IOType { category?: InputCategory; kind?: TypeKind; typeName?: string; - variableName?: string; + name?: string; + displayName?: string; fields?: IOType[]; member?: IOType; - members?: EnumMember[]; + members?: IOType[]; defaultValue?: unknown; optional?: boolean; focusedMemberId?: string; isFocused?: boolean; + isRecursive?: boolean; + isDeepNested?: boolean; + ref?: string; moduleInfo? : ModuleInfo; } @@ -99,7 +104,7 @@ export interface Mapping { inputs?: string[]; expression: string; elements?: MappingElement[]; - diagnostics?: IDMDiagnostic[]; + diagnostics?: DMDiagnostic[]; isComplex?: boolean; isQueryExpression?: boolean; isFunctionCall?: boolean; @@ -112,7 +117,7 @@ export interface ExpandedDMModel { subMappings?: IOType[]; mappings: Mapping[]; source: string; - view: string; + rootViewId: string; query?: Query; mapping_fields?: Record; } @@ -121,10 +126,11 @@ export interface DMModel { inputs: IORoot[]; output: IORoot; subMappings?: IORoot[]; - types: Record; + refs: Record; mappings: Mapping[]; view: string; query?: Query; + focusInputs?: Record; mapping_fields?: Record; } @@ -135,12 +141,13 @@ export interface ModelState { } export interface IORoot extends IOTypeField { - id: string; category?: InputCategory; } export interface RecordType { fields: IOTypeField[]; + typeName: string; + kind: TypeKind; } export interface EnumType { @@ -150,11 +157,14 @@ export interface EnumType { export interface IOTypeField { typeName?: string; kind: TypeKind; - fieldName?: string; + name: string; + displayName?: string; member?: IOTypeField; + members?: IOTypeField[]; defaultValue?: unknown; optional?: boolean; ref?: string; + focusExpression?: string; } export interface EnumMember { @@ -170,7 +180,7 @@ export interface MappingElement { export interface Query { output: string, inputs: string[]; - diagnostics?: IDMDiagnostic[]; + diagnostics?: DMDiagnostic[]; fromClause: FromClause; intermediateClauses?: IntermediateClause[]; resultClause: ResultClause; @@ -203,12 +213,12 @@ export interface ResultClause { query?: Query; } -export interface CustomFnMetadata { - returnType: string, - parameters: CustomFnParams[] +export interface FnMetadata { + returnType: FnReturnType, + parameters: FnParams[] } -export interface CustomFnParams{ +export interface FnParams{ name: string, type: string, isOptional: boolean, @@ -216,18 +226,23 @@ export interface CustomFnParams{ kind: TypeKind } -export interface IDMFormProps { +export interface FnReturnType { + type: string; + kind: TypeKind; +} + +export interface DMFormProps { targetLineRange: LineRange; - fields: IDMFormField[]; + fields: DMFormField[]; submitText?: string; cancelText?: string; nestedForm?: boolean; - onSubmit: (data: IDMFormFieldValues, formImports?: IDMFormFieldValues, importsCodedata?: CodeData) => void; + onSubmit: (data: DMFormFieldValues, formImports?: DMFormFieldValues, importsCodedata?: CodeData) => void; onCancel?: () => void; isSaving?: boolean; } -export interface IDMFormField { +export interface DMFormField { key: string; label: string; type: null | string; @@ -240,11 +255,11 @@ export interface IDMFormField { items?: string[]; } -export interface IDMFormFieldValues { +export interface DMFormFieldValues { [key: string]: any; } -export interface IDMViewState { +export interface DMViewState { viewId: string; codedata?: CodeData; isSubMapping?: boolean; diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts index 2c822f7056a..a4dc5841246 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts @@ -28,9 +28,9 @@ import { ConnectorRequest, ConnectorResponse } from "../rpc-types/connector-wiza import { SqFlow } from "../rpc-types/sequence-diagram/interfaces"; import { FieldType, FunctionModel, ListenerModel, ServiceClassModel, ServiceModel } from "./service"; import { CDModel } from "./component-diagram"; -import { DMModel, ExpandedDMModel, IntermediateClause, Mapping, VisualizableField, CustomFnMetadata, ResultClauseType } from "./inline-data-mapper"; +import { DMModel, ExpandedDMModel, IntermediateClause, Mapping, VisualizableField, FnMetadata, ResultClauseType, IOType } from "./data-mapper"; import { DataMapperMetadata, SCOPE } from "../state-machine-types"; -import { Attachment } from "../rpc-types/ai-panel/interfaces"; +import { Attachment, DataMappingRecord, ImportInfo } from "../rpc-types/ai-panel/interfaces"; import { ToolParameters } from "../rpc-types/ai-agent/interfaces"; export interface DidOpenParams { @@ -293,26 +293,27 @@ export interface InitialIDMSourceResponse { codedata?: CodeData; } -export interface InlineDataMapperModelRequest { +export interface DataMapperModelRequest { filePath: string; codedata: CodeData; position: LinePosition; targetField?: string; } -export interface InlineDataMapperBase { +export interface DataMapperBase { filePath: string; codedata: CodeData; varName?: string; targetField?: string; + position?: LinePosition; } -export interface InlineDataMapperSourceRequest extends InlineDataMapperBase { +export interface DataMapperSourceRequest extends DataMapperBase { mapping: Mapping; withinSubMapping?: boolean; } -export interface InlineAllDataMapperSourceRequest extends InlineDataMapperBase { +export interface AllDataMapperSourceRequest extends DataMapperBase { mappings: Mapping[]; } @@ -322,7 +323,8 @@ export interface ExtendedDataMapperMetadata extends DataMapperMetadata { export interface MetadataWithAttachments { metadata: ExtendedDataMapperMetadata; - attachment?: Attachment[]; + attachments?: Attachment[]; + useTemporaryFile?: boolean; } export interface VisualizableFieldsRequest { @@ -330,11 +332,11 @@ export interface VisualizableFieldsRequest { codedata: CodeData; } -export interface InlineDataMapperModelResponse { +export interface DataMapperModelResponse { mappingsModel: ExpandedDMModel | DMModel; } -export interface InlineDataMapperSourceResponse { +export interface DataMapperSourceResponse { textEdits?: { [key: string]: TextEdit[]; }; @@ -342,6 +344,51 @@ export interface InlineDataMapperSourceResponse { userAborted?: boolean; } +export interface CreateTempFileRequest { + inputs: DataMappingRecord[]; + output: DataMappingRecord; + functionName: string; + inputNames: string[]; + imports: ImportInfo[]; +} + +export interface DatamapperModelContext { + documentUri?: string; + identifier?: string; + dataMapperMetadata?: any; +} + +export interface ExpandModelOptions { + processInputs?: boolean; + processOutput?: boolean; + processSubMappings?: boolean; + previousModel?: ExpandedDMModel; +} + +export interface DMModelRequest { + model: DMModel; + rootViewId: string; + options?: ExpandModelOptions; +} + +export interface ExpandedDMModelResponse { + expandedModel: ExpandedDMModel; + success: boolean; + error?: string; +} +export interface ProcessTypeReferenceRequest { + ref: string; + fieldId: string; + model: DMModel; + visitedRefs?: Set; +} + +export interface ProcessTypeReferenceResponse { + result: Partial; + success: boolean; + error?: string; +} + export interface VisualizableFieldsResponse { visualizableProperties: VisualizableField; } @@ -391,16 +438,16 @@ export interface DeleteMappingRequest { targetField: string; } -export interface MapWithCustomFnRequest { +export interface MapWithFnRequest { filePath: string; codedata: CodeData; mapping: Mapping; - functionMetadata: CustomFnMetadata; + functionMetadata: FnMetadata; varName?: string; targetField: string; } -export interface GetInlineDataMapperCodedataRequest { +export interface GetDataMapperCodedataRequest { filePath: string; codedata: CodeData; name: string; @@ -412,7 +459,7 @@ export interface GetSubMappingCodedataRequest { view: string; } -export interface GetInlineDataMapperCodedataResponse { +export interface GetDataMapperCodedataResponse { codedata: CodeData; } @@ -837,16 +884,32 @@ export type SearchQueryParams = { q?: string; limit?: number; offset?: number; + orgName?: string; includeAvailableFunctions?: string; includeCurrentOrganizationInSearch?: boolean; -} - -export type SearchKind = 'FUNCTION' | 'CONNECTOR' | 'TYPE' | "NP_FUNCTION" | "MODEL_PROVIDER" | "VECTOR_STORE" | "EMBEDDING_PROVIDER" | "VECTOR_KNOWLEDGE_BASE"; + filterByCurrentOrg?: boolean; +} + +export type SearchKind = + | "FUNCTION" + | "CONNECTOR" + | "TYPE" + | "NP_FUNCTION" + | "MODEL_PROVIDER" + | "VECTOR_STORE" + | "EMBEDDING_PROVIDER" + | "VECTOR_KNOWLEDGE_BASE" + | "DATA_LOADER" + | "CHUNKER" + | "AGENT" + | "MEMORY_MANAGER" + | "AGENT_TOOL" + | "CLASS_INIT"; export type BISearchRequest = { - position: LineRange; + position?: LineRange; filePath: string; - queryMap: SearchQueryParams; + queryMap?: SearchQueryParams; searchKind: SearchKind; } @@ -938,6 +1001,7 @@ export type DeleteConfigVariableResponseV2 = { export interface GetConfigVariableNodeTemplateRequest { isNew: boolean; + isEnvVariable?: boolean; } export interface OpenConfigTomlRequest { @@ -1133,6 +1197,22 @@ export interface RenameIdentifierRequest { newName: string; } +export interface ImportIntegrationRequest { + packageName: string; + orgName: string; + sourcePath: string; + parameters?: Record; +} + +export interface ImportIntegrationResponse { + error: string; + textEdits: { + [key: string]: string; + }; + report: string; + jsonReport: string; +} + // <-------- Trigger Related -------> export interface TriggerModelsRequest { organization?: string; @@ -1323,6 +1403,7 @@ export interface Member { defaultValue?: string; optional?: boolean; imports?: Imports; + readonly?: boolean; } export interface GetGraphqlTypeRequest { @@ -1363,6 +1444,30 @@ export interface UpdateTypesResponse { stacktrace?: string; } +export interface DeleteTypeRequest { + filePath: string; + lineRange: LineRange; +} + +export interface DeleteTypeResponse { + textEdits: { + [filePath: string]: TextEdit[]; + }; + errorMsg?: string; + stacktrace?: string; +} + +export interface VerifyTypeDeleteRequest { + filePath: string; + startPosition: LinePosition; +} + +export interface VerifyTypeDeleteResponse { + canDelete: boolean; + errorMsg?: string; + stacktrace?: string; +} + export interface GetTypesResponse { types: Type[]; } diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/service.ts b/workspaces/ballerina/ballerina-core/src/interfaces/service.ts index 151d31767c0..68ab6d3d887 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/service.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/service.ts @@ -83,6 +83,7 @@ export interface FunctionModel { // accessor will be used by resource functions accessor?: PropertyModel; + properties?: ConfigProperties; name: PropertyModel; parameters: ParameterModel[]; schema?: ConfigProperties; @@ -144,6 +145,7 @@ export interface PropertyModel { httpParamType?: "QUERY" | "Header" | "PAYLOAD"; diagnostics?: DiagnosticMessage[]; imports?: Imports; + hidden?: boolean; } export interface ParameterModel extends PropertyModel { diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts index a722377e4ae..f0e1ad75686 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts @@ -1,4 +1,3 @@ - /** * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. * @@ -16,9 +15,9 @@ * specific language governing permissions and limitations * under the License. */ -import { InlineAllDataMapperSourceRequest, MetadataWithAttachments } from "../../interfaces/extended-lang-client"; +import { AllDataMapperSourceRequest, CreateTempFileRequest, DatamapperModelContext, DataMapperModelResponse, ExtendedDataMapperMetadata, MetadataWithAttachments } from "../../interfaces/extended-lang-client"; import { LoginMethod } from "../../state-machine-types"; -import { AddToProjectRequest, GetFromFileRequest, DeleteFromProjectRequest, GenerateMappingsRequest, GenerateMappingsResponse, NotifyAIMappingsRequest, ProjectSource, ProjectDiagnostics, GenerateMappingsFromRecordRequest, GenerateMappingFromRecordResponse, PostProcessRequest, PostProcessResponse, GenerateTypesFromRecordRequest, GenerateTypesFromRecordResponse, FetchDataRequest, FetchDataResponse, TestGenerationRequest, TestGenerationResponse, TestGenerationMentions, AIChatSummary, DeveloperDocument, RequirementSpecification, LLMDiagnostics, GetModuleDirParams, AIPanelPrompt, AIMachineSnapshot, SubmitFeedbackRequest, RelevantLibrariesAndFunctionsRequest, GenerateOpenAPIRequest, GenerateCodeRequest, TestPlanGenerationRequest, TestGeneratorIntermediaryState, RepairParams, RelevantLibrariesAndFunctionsResponse, CodeSegment } from "./interfaces"; +import { AddToProjectRequest, GetFromFileRequest, DeleteFromProjectRequest, GenerateMappingsResponse, NotifyAIMappingsRequest, ProjectSource, ProjectDiagnostics, PostProcessRequest, PostProcessResponse, GenerateTypesFromRecordRequest, GenerateTypesFromRecordResponse, FetchDataRequest, FetchDataResponse, TestGenerationRequest, TestGenerationResponse, TestGenerationMentions, AIChatSummary, DeveloperDocument, RequirementSpecification, LLMDiagnostics, GetModuleDirParams, AIPanelPrompt, AIMachineSnapshot, SubmitFeedbackRequest, RelevantLibrariesAndFunctionsRequest, GenerateOpenAPIRequest, GenerateCodeRequest, TestPlanGenerationRequest, TestGeneratorIntermediaryState, RepairParams, RelevantLibrariesAndFunctionsResponse, CodeSegment, DocGenerationRequest, AddFilesToProjectRequest } from "./interfaces"; export interface AIPanelAPI { // ================================== @@ -32,18 +31,21 @@ export interface AIPanelAPI { getDefaultPrompt: () => Promise; getAIMachineSnapshot: () => Promise; fetchData: (params: FetchDataRequest) => Promise; - addToProject: (params: AddToProjectRequest) => void; + addToProject: (params: AddToProjectRequest) => Promise; getFromFile: (params: GetFromFileRequest) => Promise; getFileExists: (params: GetFromFileRequest) => Promise; deleteFromProject: (params: DeleteFromProjectRequest) => void; - generateMappings: (params: GenerateMappingsRequest) => Promise; notifyAIMappings: (params: NotifyAIMappingsRequest) => Promise; stopAIMappings: () => Promise; getShadowDiagnostics: (params: ProjectSource) => Promise; checkSyntaxError: (params: ProjectSource) => Promise; clearInitialPrompt: () => void; - openInlineMappingChatWindow: () => void; - getMappingsFromModel: (params: MetadataWithAttachments) => Promise; + openAIMappingChatWindow: (params: DataMapperModelResponse) => void; + generateDataMapperModel: (params: DatamapperModelContext) => Promise; + getTypesFromRecord: (params: GenerateTypesFromRecordRequest) => Promise; + createTempFileAndGenerateMetadata: (params: CreateTempFileRequest) => Promise; + generateMappings: (params: MetadataWithAttachments) => Promise; + addCodeSegmentToWorkspace: (params: CodeSegment) => Promise; addInlineCodeSegmentToWorkspace: (params: CodeSegment) => void; // Test-generator related functions getGeneratedTests: (params: TestGenerationRequest) => Promise; @@ -53,8 +55,6 @@ export interface AIPanelAPI { getServiceNames: () => Promise; getResourceMethodAndPaths: () => Promise; abortTestGeneration: () => void; - getMappingsFromRecord: (params: GenerateMappingsFromRecordRequest) => Promise; - getTypesFromRecord: (params: GenerateTypesFromRecordRequest) => Promise; applyDoOnFailBlocks: () => void; postProcess: (params: PostProcessRequest) => Promise; getActiveFile:() => Promise; @@ -84,4 +84,9 @@ export interface AIPanelAPI { generateFunctionTests: (params: TestGeneratorIntermediaryState) => void; generateHealthcareCode: (params: GenerateCodeRequest) => void; abortAIGeneration: () => void; + // ================================== + // Doc Generation Related Functions + // ================================== + getGeneratedDocumentation: (params: DocGenerationRequest) => Promise; + addFilesToProject: (params: AddFilesToProjectRequest) => Promise; } diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/interfaces.ts index ac5b80a4290..03b5ebfea36 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/interfaces.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/interfaces.ts @@ -20,7 +20,7 @@ import { NodePosition } from "@wso2/syntax-tree"; import { AIMachineContext, AIMachineStateValue } from "../../state-machine-types"; import { Command, TemplateId } from "../../interfaces/ai-panel"; -import { FormField } from "../../interfaces/config-spec"; +import { DataMapperSourceResponse, ExtendedDataMapperMetadata } from "../../interfaces/extended-lang-client"; // ================================== // General Interfaces @@ -88,6 +88,15 @@ export interface AddToProjectRequest { isTestCode: boolean; } +export interface AddFilesToProjectRequest { + fileChanges: FileChanges[]; +} + +export interface FileChanges { + filePath: string; + content: string; +} + export interface GetFromFileRequest { filePath: string; } @@ -118,6 +127,34 @@ export interface NotifyAIMappingsRequest { export interface CodeSegment { segmentText: string; filePath: string; + metadata?: ExtendedDataMapperMetadata; + textEdit?: DataMapperSourceResponse; +} + +export interface DataMappingRecord { + type: string; + isArray: boolean; + filePath: string; +} + +export interface GenerateTypesFromRecordRequest { + attachment?: Attachment[] +} + +export interface GenerateTypesFromRecordResponse { + typesCode: string; +} + +export interface MappingParameters { + inputRecord: string[]; + outputRecord: string, + functionName?: string; +} + +export interface ImportInfo { + moduleName: string; + alias?: string; + recordName?: string; } // Test-generator related interfaces @@ -155,43 +192,6 @@ export interface TestGeneratorIntermediaryState { testPlan: string; } - -export interface DataMappingRecord { - type: string; - isArray: boolean; - filePath: string; -} - -export interface GenerateMappingsFromRecordRequest { - backendUri: string; - token: string; - inputRecordTypes: DataMappingRecord[]; - outputRecordType: DataMappingRecord; - functionName: string; - imports: { moduleName: string; alias?: string }[]; - inputNames?: string[]; - attachment?: Attachment[] -} - -export interface GenerateTypesFromRecordRequest { - backendUri: string; - token: string; - attachment?: Attachment[] -} - -export interface GenerateMappingFromRecordResponse { - mappingCode: string; -} -export interface GenerateTypesFromRecordResponse { - typesCode: string; -} -export interface MappingParameters { - inputRecord: string[]; - outputRecord: string, - functionName?: string; -} - - export interface PostProcessRequest { assistant_response: string; } @@ -342,5 +342,17 @@ export interface CopilotFilterLibrariesResponse { libraries: any[]; } +// ================================== +// Doc Generation Related Interfaces +// ================================== +export enum DocGenerationType { + User = "user", +} + +export interface DocGenerationRequest { + type: DocGenerationType; + serviceName: string; +} + export const GENERATE_TEST_AGAINST_THE_REQUIREMENT = "Generate tests against the requirements"; export const GENERATE_CODE_AGAINST_THE_REQUIREMENT = "Generate code based on the requirements"; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts index 1ba3d835f43..6b6140ce5fe 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts @@ -17,9 +17,9 @@ * * THIS FILE INCLUDES AUTO GENERATED CODE */ -import { InlineAllDataMapperSourceRequest, MetadataWithAttachments } from "../../interfaces/extended-lang-client"; +import { AllDataMapperSourceRequest, CreateTempFileRequest, DatamapperModelContext, DataMapperModelResponse, ExtendedDataMapperMetadata, MetadataWithAttachments } from "../../interfaces/extended-lang-client"; import { LoginMethod } from "../../state-machine-types"; -import { AddToProjectRequest, GetFromFileRequest, DeleteFromProjectRequest, GenerateMappingsRequest, GenerateMappingsResponse, NotifyAIMappingsRequest, ProjectSource, ProjectDiagnostics, GenerateMappingsFromRecordRequest, GenerateMappingFromRecordResponse, PostProcessRequest, PostProcessResponse, GenerateTypesFromRecordRequest, GenerateTypesFromRecordResponse, FetchDataRequest, FetchDataResponse, TestGenerationRequest, TestGenerationResponse, TestGenerationMentions, AIChatSummary, DeveloperDocument, RequirementSpecification, LLMDiagnostics, GetModuleDirParams, AIPanelPrompt, AIMachineSnapshot, SubmitFeedbackRequest, RelevantLibrariesAndFunctionsRequest, GenerateOpenAPIRequest, GenerateCodeRequest, TestPlanGenerationRequest, TestGeneratorIntermediaryState, RepairParams, RelevantLibrariesAndFunctionsResponse, CodeSegment } from "./interfaces"; +import { AddToProjectRequest, GetFromFileRequest, DeleteFromProjectRequest, GenerateMappingsResponse, NotifyAIMappingsRequest, ProjectSource, ProjectDiagnostics, PostProcessRequest, PostProcessResponse, GenerateTypesFromRecordRequest, GenerateTypesFromRecordResponse, FetchDataRequest, FetchDataResponse, TestGenerationRequest, TestGenerationResponse, TestGenerationMentions, AIChatSummary, DeveloperDocument, RequirementSpecification, LLMDiagnostics, GetModuleDirParams, AIPanelPrompt, AIMachineSnapshot, SubmitFeedbackRequest, RelevantLibrariesAndFunctionsRequest, GenerateOpenAPIRequest, GenerateCodeRequest, TestPlanGenerationRequest, TestGeneratorIntermediaryState, RepairParams, RelevantLibrariesAndFunctionsResponse, CodeSegment, DocGenerationRequest, AddFilesToProjectRequest } from "./interfaces"; import { RequestType, NotificationType } from "vscode-messenger-common"; const _preFix = "ai-panel"; @@ -31,18 +31,21 @@ export const getRefreshedAccessToken: RequestType = { method: `${_ export const getDefaultPrompt: RequestType = { method: `${_preFix}/getDefaultPrompt` }; export const getAIMachineSnapshot: RequestType = { method: `${_preFix}/getAIMachineSnapshot` }; export const fetchData: RequestType = { method: `${_preFix}/fetchData` }; -export const addToProject: NotificationType = { method: `${_preFix}/addToProject` }; +export const addToProject: RequestType = { method: `${_preFix}/addToProject` }; export const getFromFile: RequestType = { method: `${_preFix}/getFromFile` }; export const getFileExists: RequestType = { method: `${_preFix}/getFileExists` }; export const deleteFromProject: NotificationType = { method: `${_preFix}/deleteFromProject` }; -export const generateMappings: RequestType = { method: `${_preFix}/generateMappings` }; export const notifyAIMappings: RequestType = { method: `${_preFix}/notifyAIMappings` }; export const stopAIMappings: RequestType = { method: `${_preFix}/stopAIMappings` }; export const getShadowDiagnostics: RequestType = { method: `${_preFix}/getShadowDiagnostics` }; export const checkSyntaxError: RequestType = { method: `${_preFix}/checkSyntaxError` }; export const clearInitialPrompt: NotificationType = { method: `${_preFix}/clearInitialPrompt` }; -export const openInlineMappingChatWindow: NotificationType = { method: `${_preFix}/openInlineMappingChatWindow` }; -export const getMappingsFromModel: RequestType = { method: `${_preFix}/getMappingsFromModel` }; +export const openAIMappingChatWindow: NotificationType = { method: `${_preFix}/openAIMappingChatWindow` }; +export const generateDataMapperModel: RequestType = { method: `${_preFix}/generateDataMapperModel` }; +export const getTypesFromRecord: RequestType = { method: `${_preFix}/getTypesFromRecord` }; +export const createTempFileAndGenerateMetadata: RequestType = { method: `${_preFix}/createTempFileAndGenerateMetadata` }; +export const generateMappings: RequestType = { method: `${_preFix}/generateMappings` }; +export const addCodeSegmentToWorkspace: RequestType = { method: `${_preFix}/addCodeSegmentToWorkspace` }; export const addInlineCodeSegmentToWorkspace: NotificationType = { method: `${_preFix}/addInlineCodeSegmentToWorkspace` }; export const getGeneratedTests: RequestType = { method: `${_preFix}/getGeneratedTests` }; export const getTestDiagnostics: RequestType = { method: `${_preFix}/getTestDiagnostics` }; @@ -51,8 +54,6 @@ export const getResourceSourceForMethodAndPath: RequestType = { export const getServiceNames: RequestType = { method: `${_preFix}/getServiceNames` }; export const getResourceMethodAndPaths: RequestType = { method: `${_preFix}/getResourceMethodAndPaths` }; export const abortTestGeneration: NotificationType = { method: `${_preFix}/abortTestGeneration` }; -export const getMappingsFromRecord: RequestType = { method: `${_preFix}/getMappingsFromRecord` }; -export const getTypesFromRecord: RequestType = { method: `${_preFix}/getTypesFromRecord` }; export const applyDoOnFailBlocks: NotificationType = { method: `${_preFix}/applyDoOnFailBlocks` }; export const postProcess: RequestType = { method: `${_preFix}/postProcess` }; export const getActiveFile: RequestType = { method: `${_preFix}/getActiveFile` }; @@ -82,3 +83,5 @@ export const generateTestPlan: NotificationType = { m export const generateFunctionTests: NotificationType = { method: `${_preFix}/generateFunctionTests` }; export const generateHealthcareCode: NotificationType = { method: `${_preFix}/generateHealthcareCode` }; export const abortAIGeneration: NotificationType = { method: `${_preFix}/abortAIGeneration` }; +export const getGeneratedDocumentation: NotificationType = { method: `${_preFix}/getGeneratedDocumentation` }; +export const addFilesToProject: RequestType = { method: `${_preFix}/addFilesToProject` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/index.ts index 8018c78d59a..ea03067547e 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/index.ts @@ -88,7 +88,12 @@ import { DeleteConfigVariableResponseV2, DeleteConfigVariableRequestV2, JsonToTypeRequest, - JsonToTypeResponse + JsonToTypeResponse, + ConfigVariableRequest, + DeleteTypeRequest, + DeleteTypeResponse, + VerifyTypeDeleteRequest, + VerifyTypeDeleteResponse } from "../../interfaces/extended-lang-client"; import { ProjectRequest, @@ -123,6 +128,8 @@ export interface BIDiagramAPI { getAvailableVectorStores: (params: BIAvailableNodesRequest) => Promise; getAvailableEmbeddingProviders: (params: BIAvailableNodesRequest) => Promise; getAvailableVectorKnowledgeBases: (params: BIAvailableNodesRequest) => Promise; + getAvailableDataLoaders: (params: BIAvailableNodesRequest) => Promise; + getAvailableChunkers: (params: BIAvailableNodesRequest) => Promise; getEnclosedFunction: (params: BIGetEnclosedFunctionRequest) => Promise; getNodeTemplate: (params: BINodeTemplateRequest) => Promise; getAiSuggestions: (params: BIAiSuggestionsRequest) => Promise; @@ -136,7 +143,7 @@ export interface BIDiagramAPI { getExpressionCompletions: (params: ExpressionCompletionsRequest) => Promise; getConfigVariables: () => Promise; updateConfigVariables: (params: UpdateConfigVariableRequest) => Promise; - getConfigVariablesV2: () => Promise; + getConfigVariablesV2: (params: ConfigVariableRequest) => Promise; updateConfigVariablesV2: (params: UpdateConfigVariableRequestV2) => Promise; deleteConfigVariableV2: (params: DeleteConfigVariableRequestV2) => Promise; getConfigVariableNodeTemplate: (params: GetConfigVariableNodeTemplateRequest) => Promise; @@ -162,6 +169,8 @@ export interface BIDiagramAPI { getType: (params: GetTypeRequest) => Promise; updateType: (params: UpdateTypeRequest) => Promise; updateTypes: (params: UpdateTypesRequest) => Promise; + deleteType: (params: DeleteTypeRequest) => Promise; + verifyTypeDelete: (params: VerifyTypeDeleteRequest) => Promise; getTypeFromJson: (params: JsonToTypeRequest) => Promise; getServiceClassModel: (params: ModelFromCodeRequest) => Promise; updateClassField: (params: ClassFieldModifierRequest) => Promise; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/interfaces.ts index 203e8524bf2..1e756452a72 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/interfaces.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/interfaces.ts @@ -23,8 +23,11 @@ import { RemoteFunction, ServiceType } from "../../interfaces/ballerina"; export interface ProjectRequest { projectName: string; + packageName: string; projectPath: string; - isService: boolean; + createDirectory: boolean; + orgName?: string; + version?: string; } export interface WorkspacesResponse { diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/rpc-type.ts index 91ee5a70a44..deced924ad1 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/rpc-type.ts @@ -89,7 +89,12 @@ import { DeleteConfigVariableRequestV2, DeleteConfigVariableResponseV2, JsonToTypeRequest, - JsonToTypeResponse + JsonToTypeResponse, + ConfigVariableRequest, + DeleteTypeRequest, + DeleteTypeResponse, + VerifyTypeDeleteRequest, + VerifyTypeDeleteResponse } from "../../interfaces/extended-lang-client"; import { ProjectRequest, @@ -125,6 +130,8 @@ export const getAvailableModelProviders: RequestType = { method: `${_preFix}/getAvailableVectorStores` }; export const getAvailableEmbeddingProviders: RequestType = { method: `${_preFix}/getAvailableEmbeddingProviders` }; export const getAvailableVectorKnowledgeBases: RequestType = { method: `${_preFix}/getAvailableVectorKnowledgeBases` }; +export const getAvailableDataLoaders: RequestType = { method: `${_preFix}/getAvailableDataLoaders` }; +export const getAvailableChunkers: RequestType = { method: `${_preFix}/getAvailableChunkers` }; export const getEnclosedFunction: RequestType = { method: `${_preFix}/getEnclosedFunction` }; export const getNodeTemplate: RequestType = { method: `${_preFix}/getNodeTemplate` }; export const getAiSuggestions: RequestType = { method: `${_preFix}/getAiSuggestions` }; @@ -138,7 +145,7 @@ export const getVisibleVariableTypes: RequestType = { method: `${_preFix}/getExpressionCompletions` }; export const getConfigVariables: RequestType = { method: `${_preFix}/getConfigVariables` }; export const updateConfigVariables: RequestType = { method: `${_preFix}/updateConfigVariables` }; -export const getConfigVariablesV2: RequestType = { method: `${_preFix}/getConfigVariablesV2` }; +export const getConfigVariablesV2: RequestType = { method: `${_preFix}/getConfigVariablesV2` }; export const updateConfigVariablesV2: RequestType = { method: `${_preFix}/updateConfigVariablesV2` }; export const deleteConfigVariableV2: RequestType = { method: `${_preFix}/deleteConfigVariableV2` }; export const getConfigVariableNodeTemplate: RequestType = { method: `${_preFix}/getConfigVariableNodeTemplate` }; @@ -164,6 +171,8 @@ export const getTypes: RequestType = { method export const getType: RequestType = { method: `${_preFix}/getType` }; export const updateType: RequestType = { method: `${_preFix}/updateType` }; export const updateTypes: RequestType = { method: `${_preFix}/updateTypes` }; +export const deleteType: RequestType = { method: `${_preFix}/deleteType` }; +export const verifyTypeDelete: RequestType = { method: `${_preFix}/verifyTypeDelete` }; export const getTypeFromJson: RequestType = { method: `${_preFix}/getTypeFromJson` }; export const getServiceClassModel: RequestType = { method: `${_preFix}/getServiceClassModel` }; export const updateClassField: RequestType = { method: `${_preFix}/updateClassField` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts index ba9b90912db..964b6cea6d6 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts @@ -43,8 +43,11 @@ export interface CommonRPCAPI { runBackgroundTerminalCommand: (params: RunExternalCommandRequest) => Promise; openExternalUrl: (params: OpenExternalUrlRequest) => void; selectFileOrDirPath: (params: FileOrDirRequest) => Promise; + selectFileOrFolderPath: () => Promise; experimentalEnabled: () => Promise; isNPSupported: () => Promise; getWorkspaceRoot: () => Promise; showErrorMessage: (params: ShowErrorMessageRequest) => void; + getCurrentProjectTomlValues: () => Promise>; + } diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/common/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/common/interfaces.ts index 06ff50fbf14..6ccd232ffcc 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/common/interfaces.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/common/interfaces.ts @@ -90,3 +90,14 @@ export interface FileOrDirRequest { export interface ShowErrorMessageRequest { message: string; } + +export interface TomlPackage { + org: string; + name: string; + version: string; + title: string; +} + +export interface TomlValues { + package: TomlPackage; +} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/common/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/common/rpc-type.ts index 8c5c2d67ed2..c1816ec958b 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/common/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/common/rpc-type.ts @@ -45,7 +45,9 @@ export const executeCommand: RequestType = { export const runBackgroundTerminalCommand: RequestType = { method: `${_preFix}/runBackgroundTerminalCommand` }; export const openExternalUrl: NotificationType = { method: `${_preFix}/openExternalUrl` }; export const selectFileOrDirPath: RequestType = { method: `${_preFix}/selectFileOrDirPath` }; +export const selectFileOrFolderPath: RequestType = { method: `${_preFix}/selectFileOrFolderPath` }; export const experimentalEnabled: RequestType = { method: `${_preFix}/experimentalEnabled` }; export const isNPSupported: RequestType = { method: `${_preFix}/isNPSupported` }; export const getWorkspaceRoot: RequestType = { method: `${_preFix}/getWorkspaceRoot` }; export const showErrorMessage: NotificationType = { method: `${_preFix}/showErrorMessage` }; +export const getCurrentProjectTomlValues: RequestType = { method: `${_preFix}/getCurrentProjectTomlValues` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/index.ts new file mode 100644 index 00000000000..978ec1bc4b2 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/index.ts @@ -0,0 +1,63 @@ +/** + * 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 { + AddArrayElementRequest, + ConvertToQueryRequest, + AddClausesRequest, + DataMapperModelRequest, + DataMapperModelResponse, + DataMapperSourceRequest, + DataMapperSourceResponse, + VisualizableFieldsRequest, + VisualizableFieldsResponse, + PropertyRequest, + PropertyResponse, + InitialIDMSourceResponse, + InitialIDMSourceRequest, + GetDataMapperCodedataRequest, + GetDataMapperCodedataResponse, + GetSubMappingCodedataRequest, + AllDataMapperSourceRequest, + AddSubMappingRequest, + DeleteMappingRequest, + MapWithFnRequest, + DMModelRequest, + ProcessTypeReferenceResponse, + ProcessTypeReferenceRequest, + ExpandedDMModelResponse +} from "../../interfaces/extended-lang-client"; + +export interface DataMapperAPI { + getInitialIDMSource: (params: InitialIDMSourceRequest) => Promise; + getDataMapperModel: (params: DataMapperModelRequest) => Promise; + getDataMapperSource: (params: DataMapperSourceRequest) => Promise; + getVisualizableFields: (params: VisualizableFieldsRequest) => Promise; + addNewArrayElement: (params: AddArrayElementRequest) => Promise; + convertToQuery: (params: ConvertToQueryRequest) => Promise; + addClauses: (params: AddClausesRequest) => Promise; + addSubMapping: (params: AddSubMappingRequest) => Promise; + deleteMapping: (params: DeleteMappingRequest) => Promise; + mapWithCustomFn: (params: MapWithFnRequest) => Promise; + mapWithTransformFn: (params: MapWithFnRequest) => Promise; + getDataMapperCodedata: (params: GetDataMapperCodedataRequest) => Promise; + getSubMappingCodedata: (params: GetSubMappingCodedataRequest) => Promise; + getAllDataMapperSource: (params: AllDataMapperSourceRequest) => Promise; + getProperty: (params: PropertyRequest) => Promise; + getExpandedDMFromDMModel: (params: DMModelRequest) => Promise; + getProcessTypeReference: (params: ProcessTypeReferenceRequest) => Promise; +} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/rpc-type.ts new file mode 100644 index 00000000000..c02ee3b4e1d --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/rpc-type.ts @@ -0,0 +1,65 @@ +/** + * 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. + * + * THIS FILE INCLUDES AUTO GENERATED CODE + */ +import { + AddArrayElementRequest, + ConvertToQueryRequest, + AddClausesRequest, + DataMapperModelRequest, + DataMapperModelResponse, + DataMapperSourceRequest, + DataMapperSourceResponse, + VisualizableFieldsRequest, + VisualizableFieldsResponse, + PropertyRequest, + PropertyResponse, + InitialIDMSourceResponse, + InitialIDMSourceRequest, + GetDataMapperCodedataRequest, + GetDataMapperCodedataResponse, + GetSubMappingCodedataRequest, + AllDataMapperSourceRequest, + AddSubMappingRequest, + DeleteMappingRequest, + MapWithFnRequest, + DMModelRequest, + ProcessTypeReferenceResponse, + ProcessTypeReferenceRequest, + ExpandedDMModelResponse +} from "../../interfaces/extended-lang-client"; +import { RequestType } from "vscode-messenger-common"; + +const _preFix = "data-mapper"; +export const getInitialIDMSource: RequestType = { method: `${_preFix}/getInitialIDMSource` }; +export const getDataMapperModel: RequestType = { method: `${_preFix}/getDataMapperModel` }; +export const getDataMapperSource: RequestType = { method: `${_preFix}/getDataMapperSource` }; +export const getVisualizableFields: RequestType = { method: `${_preFix}/getVisualizableFields` }; +export const addNewArrayElement: RequestType = { method: `${_preFix}/addNewArrayElement` }; +export const convertToQuery: RequestType = { method: `${_preFix}/convertToQuery` }; +export const addClauses: RequestType = { method: `${_preFix}/addClauses` }; +export const addSubMapping: RequestType = { method: `${_preFix}/addSubMapping` }; +export const deleteMapping: RequestType = { method: `${_preFix}/deleteMapping` }; +export const mapWithCustomFn: RequestType = { method: `${_preFix}/mapWithCustomFn` }; +export const mapWithTransformFn: RequestType = { method: `${_preFix}/mapWithTransformFn` }; +export const getDataMapperCodedata: RequestType = { method: `${_preFix}/getDataMapperCodedata` }; +export const getSubMappingCodedata: RequestType = { method: `${_preFix}/getSubMappingCodedata` }; +export const getAllDataMapperSource: RequestType = { method: `${_preFix}/getAllDataMapperSource` }; +export const getProperty: RequestType = { method: `${_preFix}/getProperty` }; +export const getExpandedDMFromDMModel: RequestType = { method: `${_preFix}/getExpandedDMFromDMModel` }; +export const getProcessTypeReference: RequestType = { method: `${_preFix}/getProcessTypeReference` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/index.ts deleted file mode 100644 index a4062524c5e..00000000000 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/index.ts +++ /dev/null @@ -1,56 +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 { - AddArrayElementRequest, - ConvertToQueryRequest, - AddClausesRequest, - InlineDataMapperModelRequest, - InlineDataMapperModelResponse, - InlineDataMapperSourceRequest, - InlineDataMapperSourceResponse, - VisualizableFieldsRequest, - VisualizableFieldsResponse, - PropertyRequest, - PropertyResponse, - InitialIDMSourceResponse, - InitialIDMSourceRequest, - GetInlineDataMapperCodedataRequest, - GetInlineDataMapperCodedataResponse, - GetSubMappingCodedataRequest, - InlineAllDataMapperSourceRequest, - AddSubMappingRequest, - DeleteMappingRequest, - MapWithCustomFnRequest -} from "../../interfaces/extended-lang-client"; - -export interface InlineDataMapperAPI { - getInitialIDMSource: (params: InitialIDMSourceRequest) => Promise; - getDataMapperModel: (params: InlineDataMapperModelRequest) => Promise; - getDataMapperSource: (params: InlineDataMapperSourceRequest) => Promise; - getVisualizableFields: (params: VisualizableFieldsRequest) => Promise; - addNewArrayElement: (params: AddArrayElementRequest) => Promise; - convertToQuery: (params: ConvertToQueryRequest) => Promise; - addClauses: (params: AddClausesRequest) => Promise; - addSubMapping: (params: AddSubMappingRequest) => Promise; - deleteMapping: (params: DeleteMappingRequest) => Promise; - mapWithCustomFn: (params: MapWithCustomFnRequest) => Promise; - getDataMapperCodedata: (params: GetInlineDataMapperCodedataRequest) => Promise; - getSubMappingCodedata: (params: GetSubMappingCodedataRequest) => Promise; - getAllDataMapperSource: (params:InlineAllDataMapperSourceRequest) => Promise; - getProperty: (params: PropertyRequest) => Promise; -} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/rpc-type.ts deleted file mode 100644 index 6faf8d7e445..00000000000 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/rpc-type.ts +++ /dev/null @@ -1,58 +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. - * - * THIS FILE INCLUDES AUTO GENERATED CODE - */ -import { - AddArrayElementRequest, - ConvertToQueryRequest, - AddClausesRequest, - InlineDataMapperModelRequest, - InlineDataMapperModelResponse, - InlineDataMapperSourceRequest, - InlineDataMapperSourceResponse, - VisualizableFieldsRequest, - VisualizableFieldsResponse, - PropertyRequest, - PropertyResponse, - InitialIDMSourceResponse, - InitialIDMSourceRequest, - GetInlineDataMapperCodedataRequest, - GetInlineDataMapperCodedataResponse, - GetSubMappingCodedataRequest, - InlineAllDataMapperSourceRequest, - AddSubMappingRequest, - DeleteMappingRequest, - MapWithCustomFnRequest -} from "../../interfaces/extended-lang-client"; -import { RequestType } from "vscode-messenger-common"; - -const _preFix = "inline-data-mapper"; -export const getInitialIDMSource: RequestType = { method: `${_preFix}/getInitialIDMSource` }; -export const getDataMapperModel: RequestType = { method: `${_preFix}/getDataMapperModel` }; -export const getDataMapperSource: RequestType = { method: `${_preFix}/getDataMapperSource` }; -export const getVisualizableFields: RequestType = { method: `${_preFix}/getVisualizableFields` }; -export const addNewArrayElement: RequestType = { method: `${_preFix}/addNewArrayElement` }; -export const convertToQuery: RequestType = { method: `${_preFix}/convertToQuery` }; -export const addClauses: RequestType = { method: `${_preFix}/addClauses` }; -export const addSubMapping: RequestType = { method: `${_preFix}/addSubMapping` }; -export const deleteMapping: RequestType = { method: `${_preFix}/deleteMapping` }; -export const mapWithCustomFn: RequestType = { method: `${_preFix}/mapWithCustomFn` }; -export const getDataMapperCodedata: RequestType = { method: `${_preFix}/getDataMapperCodedata` }; -export const getSubMappingCodedata: RequestType = { method: `${_preFix}/getSubMappingCodedata` }; -export const getAllDataMapperSource: RequestType = { method: `${_preFix}/getAllDataMapperSource` }; -export const getProperty: RequestType = { method: `${_preFix}/getProperty` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/index.ts new file mode 100644 index 00000000000..520a5cc3301 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/index.ts @@ -0,0 +1,29 @@ +/** + * 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 { ImportIntegrationResponse } from "../../interfaces/extended-lang-client"; +import { GetMigrationToolsResponse, ImportIntegrationRPCRequest, MigrateRequest, MigrationToolPullRequest, OpenMigrationReportRequest, SaveMigrationReportRequest } from "./interfaces"; + +export interface MigrateIntegrationAPI { + getMigrationTools: () => Promise; + pullMigrationTool: (params: MigrationToolPullRequest) => void; + importIntegration: (params: ImportIntegrationRPCRequest) => Promise; + openMigrationReport: (params: OpenMigrationReportRequest) => void; + saveMigrationReport: (params: SaveMigrationReportRequest) => void; + migrateProject: (params: MigrateRequest) => void; +} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/interfaces.ts new file mode 100644 index 00000000000..01af97bd31b --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/interfaces.ts @@ -0,0 +1,69 @@ +/** + * 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 { ProjectRequest } from "../bi-diagram/interfaces"; + +export interface MigrationTool { + id: number; + title: string; + needToPull: boolean; + commandName: string; + description: string; + requiredVersion: string; + parameters: Array<{ + key: string; + label: string; + description: string; + valueType: "boolean" | "string" | "number" | "enum"; + defaultValue?: boolean | string | number; + options?: string[]; + }>; +} + +export interface GetMigrationToolsResponse { + tools: MigrationTool[]; +} + +export interface MigrationToolPullRequest { + toolName: string; + version: string; +} + +export interface ImportIntegrationRPCRequest { + commandName: string; + packageName: string; + sourcePath: string; + parameters?: Record; +} + +export interface OpenMigrationReportRequest { + reportContent: string; + fileName: string; +} + +export interface SaveMigrationReportRequest { + reportContent: string; + defaultFileName: string; +} + +export interface MigrateRequest { + project: ProjectRequest; + textEdits: { + [key: string]: string; + }; +} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/rpc-type.ts new file mode 100644 index 00000000000..a328ab47652 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/migrate-integration/rpc-type.ts @@ -0,0 +1,30 @@ +/** + * 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. + * + * THIS FILE INCLUDES AUTO GENERATED CODE + */ +import { ImportIntegrationResponse } from "../../interfaces/extended-lang-client"; +import { GetMigrationToolsResponse, ImportIntegrationRPCRequest, MigrateRequest, MigrationToolPullRequest, OpenMigrationReportRequest, SaveMigrationReportRequest } from "./interfaces"; +import { RequestType, NotificationType } from "vscode-messenger-common"; + +const _preFix = "migrate-integration"; +export const getMigrationTools: RequestType = { method: `${_preFix}/getMigrationTools` }; +export const pullMigrationTool: NotificationType = { method: `${_preFix}/pullMigrationTool` }; +export const importIntegration: RequestType = { method: `${_preFix}/importIntegration` }; +export const openMigrationReport: NotificationType = { method: `${_preFix}/openMigrationReport` }; +export const saveMigrationReport: NotificationType = { method: `${_preFix}/saveMigrationReport` }; +export const migrateProject: NotificationType = { method: `${_preFix}/migrateProject` }; diff --git a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts index 4a329063405..3311a7a5baa 100644 --- a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts +++ b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts @@ -30,7 +30,7 @@ export type MachineStateValue = | 'lsReady' | 'viewActive' | 'disabled' - | { viewActive: 'viewInit' } | { viewActive: 'webViewLoaded' } | { viewActive: 'viewReady' } | { viewActive: 'viewEditing' }; + | { viewActive: 'viewInit' } | { viewActive: 'webViewLoaded' } | { viewActive: 'viewReady' } | { viewActive: 'viewEditing' } | { viewActive: 'resolveMissingDependencies' }; export type PopupMachineStateValue = 'initialize' | 'ready' | { open: 'active'; @@ -75,8 +75,10 @@ export enum MACHINE_VIEW { BIDiagram = "BI Diagram", BIWelcome = "BI Welcome", BIProjectForm = "BI Project SKIP", + BIImportIntegration = "BI Import Integration SKIP", BIComponentView = "BI Component View", AddConnectionWizard = "Add Connection Wizard", + AddCustomConnector = "Add Custom Connector", ViewConfigVariables = "View Config Variables", EditConfigVariables = "Edit Config Variables", AddConfigVariables = "Add Config Variables", @@ -92,7 +94,8 @@ export enum MACHINE_VIEW { BIServiceClassConfigView = "Service Class Config View", BIDataMapperForm = "Add Data Mapper SKIP", AIAgentDesigner = "AI Agent Designer", - AIChatAgentWizard = "AI Chat Agent Wizard" + AIChatAgentWizard = "AI Chat Agent Wizard", + ResolveMissingDependencies = "Resolve Missing Dependencies" } export interface MachineEvent { @@ -219,6 +222,8 @@ export interface ChatError { export const stateChanged: NotificationType = { method: 'stateChanged' }; export const onDownloadProgress: NotificationType = { method: 'onDownloadProgress' }; export const onChatNotify: NotificationType = { method: 'onChatNotify' }; +export const onMigrationToolLogs: NotificationType = { method: 'onMigrationToolLogs' }; +export const onMigrationToolStateChanged: NotificationType = { method: 'onMigrationToolStateChanged' }; export const projectContentUpdated: NotificationType = { method: 'projectContentUpdated' }; export const getVisualizerLocation: RequestType = { method: 'getVisualizerLocation' }; export const webviewReady: NotificationType = { method: `webviewReady` }; @@ -238,7 +243,7 @@ export const breakpointChanged: NotificationType = { method: 'breakpoin export type AIMachineStateValue = | 'Initialize' // (checking auth, first load) | 'Unauthenticated' // (show login window) - | { Authenticating: 'determineFlow' | 'ssoFlow' | 'apiKeyFlow' | 'validatingApiKey' } // hierarchical substates + | { Authenticating: 'determineFlow' | 'ssoFlow' | 'apiKeyFlow' | 'validatingApiKey' | 'awsBedrockFlow' | 'validatingAwsCredentials' } // hierarchical substates | 'Authenticated' // (ready, main view) | 'Disabled'; // (optional: if AI Chat is globally unavailable) @@ -247,6 +252,8 @@ export enum AIMachineEventType { LOGIN = 'LOGIN', AUTH_WITH_API_KEY = 'AUTH_WITH_API_KEY', SUBMIT_API_KEY = 'SUBMIT_API_KEY', + AUTH_WITH_AWS_BEDROCK = 'AUTH_WITH_AWS_BEDROCK', + SUBMIT_AWS_CREDENTIALS = 'SUBMIT_AWS_CREDENTIALS', LOGOUT = 'LOGOUT', SILENT_LOGOUT = "SILENT_LOGOUT", COMPLETE_AUTH = 'COMPLETE_AUTH', @@ -260,6 +267,13 @@ export type AIMachineEventMap = { [AIMachineEventType.LOGIN]: undefined; [AIMachineEventType.AUTH_WITH_API_KEY]: undefined; [AIMachineEventType.SUBMIT_API_KEY]: { apiKey: string }; + [AIMachineEventType.AUTH_WITH_AWS_BEDROCK]: undefined; + [AIMachineEventType.SUBMIT_AWS_CREDENTIALS]: { + accessKeyId: string; + secretAccessKey: string; + region: string; + sessionToken?: string; + }; [AIMachineEventType.LOGOUT]: undefined; [AIMachineEventType.SILENT_LOGOUT]: undefined; [AIMachineEventType.COMPLETE_AUTH]: undefined; @@ -276,7 +290,8 @@ export type AIMachineSendableEvent = export enum LoginMethod { BI_INTEL = 'biIntel', - ANTHROPIC_KEY = 'anthropic_key' + ANTHROPIC_KEY = 'anthropic_key', + AWS_BEDROCK = 'aws_bedrock' } interface BIIntelSecrets { @@ -288,6 +303,13 @@ interface AnthropicKeySecrets { apiKey: string; } +interface AwsBedrockSecrets { + accessKeyId: string; + secretAccessKey: string; + region: string; + sessionToken?: string; +} + export type AuthCredentials = | { loginMethod: LoginMethod.BI_INTEL; @@ -296,6 +318,10 @@ export type AuthCredentials = | { loginMethod: LoginMethod.ANTHROPIC_KEY; secrets: AnthropicKeySecrets; + } + | { + loginMethod: LoginMethod.AWS_BEDROCK; + secrets: AwsBedrockSecrets; }; export interface AIUserToken { diff --git a/workspaces/ballerina/ballerina-extension/package.json b/workspaces/ballerina/ballerina-extension/package.json index 81ffc3eb098..304a328967c 100644 --- a/workspaces/ballerina/ballerina-extension/package.json +++ b/workspaces/ballerina/ballerina-extension/package.json @@ -2,7 +2,7 @@ "name": "ballerina", "displayName": "Ballerina", "description": "Ballerina Language support, debugging, graphical visualization, AI-based data-mapping and many more.", - "version": "5.3.1", + "version": "5.3.3", "publisher": "wso2", "icon": "resources/images/ballerina.png", "homepage": "https://wso2.com/ballerina/vscode/docs", @@ -220,6 +220,26 @@ "tags": [ "experimental" ] + }, + "ballerina.showAdvancedAiNodes": { + "type": "boolean", + "default": false, + "description": "Show advanced AI nodes (Chunker, Vector Store, Embedding Provider, Recursive Document Chunker) in the Flow Diagram View's node palette." + }, + "ballerina-vscode.trace.server": { + "type": "string", + "default": "off", + "description": "Traces the communication between VS Code and the Ballerina language server.", + "enum": [ + "off", + "messages", + "verbose" + ], + "enumDescriptions": [ + "No traces", + "Trace request/response messages only", + "Trace request/response messages with parameters and content" + ] } } }, @@ -612,6 +632,13 @@ "group": "navigation", "category": "BI" }, + { + "command": "BI.project-explorer.add-custom-connector", + "title": "Add Custom Connector", + "icon": "$(add)", + "group": "navigation", + "category": "BI" + }, { "command": "BI.project-explorer.delete", "title": "Delete", @@ -694,6 +721,11 @@ "icon": "$(add)", "group": "navigation", "category": "BI" + }, + { + "command": "BI.toggle.trace.logs", + "title": "Toggle Trace Logs", + "category": "BI" } ], "views": { @@ -1110,21 +1142,24 @@ "copyJSLibs": "copyfiles -f ../ballerina-visualizer/build/*.js resources/jslibs" }, "dependencies": { - "@types/lodash": "^4.14.200", + "@ai-sdk/amazon-bedrock": "^2.2.12", "@ai-sdk/anthropic": "^1.2.12", - "ai": "^4.3.16", + "@types/lodash": "^4.14.200", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^2.22.0", "@wso2/ballerina-core": "workspace:*", "@wso2/ballerina-visualizer": "workspace:*", - "@wso2/wso2-platform-core": "workspace:*", "@wso2/font-wso2-vscode": "workspace:*", "@wso2/syntax-tree": "workspace:*", + "@wso2/wso2-platform-core": "workspace:*", + "ai": "^4.3.16", "cors-anywhere": "^0.4.4", "del-cli": "^5.1.0", + "dotenv": "~16.5.0", "file-uri-to-path": "^2.0.0", "glob": "^7.2.3", "handlebars": "~4.7.8", + "jwt-decode": "^4.0.0", "lodash": "^4.17.21", "monaco-languageclient": "0.13.1-next.9", "node-fetch": "^3.3.2", @@ -1132,6 +1167,7 @@ "portfinder": "^1.0.32", "source-map-support": "^0.5.21", "toml": "^3.0.0", + "uuid": "^11.1.0", "vscode-debugadapter": "^1.51.0", "vscode-debugprotocol": "^1.51.0", "vscode-extension-telemetry": "^0.1.7", @@ -1144,9 +1180,6 @@ "vscode-uri": "^3.0.8", "xml-js": "^1.6.11", "xstate": "^4.38.3", - "uuid": "^11.1.0", - "jwt-decode": "^4.0.0", - "dotenv": "~16.5.0", "zod": "^3.25.74" }, "devDependencies": { @@ -1157,6 +1190,7 @@ "@types/tcp-port-used": "^1.0.3", "@types/vscode": "^1.83.1", "@types/vscode-notebook-renderer": "~1.72.2", + "adm-zip": "^0.5.16", "axios": "^1.6.0", "chai": "^4.3.10", "copyfiles": "^2.4.1", @@ -1183,8 +1217,7 @@ "webpack": "^5.89.0", "webpack-cli": "^6.0.1", "webpack-merge-and-include-globally": "^2.3.4", - "yarn": "^1.22.19", - "adm-zip": "^0.5.16" + "yarn": "^1.22.19" }, "extensionPack": [ "be5invis.toml" diff --git a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts index 573e1298dc2..690545292ac 100644 --- a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts +++ b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts @@ -38,12 +38,13 @@ import { StateMachinePopup } from './stateMachinePopup'; import { registerAiAgentRpcHandlers } from './rpc-managers/ai-agent/rpc-handler'; import { registerConnectorWizardRpcHandlers } from './rpc-managers/connector-wizard/rpc-handler'; import { registerSequenceDiagramRpcHandlers } from './rpc-managers/sequence-diagram/rpc-handler'; -import { registerInlineDataMapperRpcHandlers } from './rpc-managers/inline-data-mapper/rpc-handler'; +import { registerDataMapperRpcHandlers } from './rpc-managers/data-mapper/rpc-handler'; import { registerTestManagerRpcHandlers } from './rpc-managers/test-manager/rpc-handler'; import { registerIcpServiceRpcHandlers } from './rpc-managers/icp-service/rpc-handler'; import { extension } from './BalExtensionContext'; import { registerAgentChatRpcHandlers } from './rpc-managers/agent-chat/rpc-handler'; import { ArtifactsUpdated, ArtifactNotificationHandler } from './utils/project-artifacts-handler'; +import { registerMigrateIntegrationRpcHandlers } from './rpc-managers/migrate-integration/rpc-handler'; export class RPCLayer { static _messenger: Messenger = new Messenger(); @@ -96,12 +97,15 @@ export class RPCLayer { registerAiPanelRpcHandlers(RPCLayer._messenger); RPCLayer._messenger.onRequest(sendAIStateEvent, (event: AIMachineEventType | AIMachineSendableEvent) => AIStateMachine.sendEvent(event)); - // ----- Inline Data Mapper Webview RPC Methods - registerInlineDataMapperRpcHandlers(RPCLayer._messenger); + // ----- Data Mapper Webview RPC Methods + registerDataMapperRpcHandlers(RPCLayer._messenger); // ----- Popup Views RPC Methods RPCLayer._messenger.onRequest(getPopupVisualizerState, () => getPopupContext()); + // ----- Register Integration Migration RPC Methods + registerMigrateIntegrationRpcHandlers(RPCLayer._messenger); + // ----- Artifact Updated Common Notification RPCLayer._messenger.onRequest(onArtifactUpdatedRequest, (artifactData: ArtifactData) => { // Get the notification handler instance diff --git a/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts b/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts index d484b167161..d6661add2e3 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts @@ -160,10 +160,10 @@ import { AddFunctionRequest, AddImportItemResponse, UpdateImportsRequest, - InlineDataMapperModelRequest, - InlineDataMapperSourceRequest, - InlineDataMapperSourceResponse, - InlineDataMapperModelResponse, + DataMapperModelRequest, + DataMapperSourceRequest, + DataMapperSourceResponse, + DataMapperModelResponse, VisualizableFieldsRequest, VisualizableFieldsResponse, AddArrayElementRequest, @@ -241,20 +241,31 @@ import { GetConfigVariableNodeTemplateRequest, FunctionFromSourceRequest, FunctionFromSourceResponse, - GetInlineDataMapperCodedataRequest, - GetInlineDataMapperCodedataResponse, + GetDataMapperCodedataRequest, + GetDataMapperCodedataResponse, GetSubMappingCodedataRequest, AddSubMappingRequest, DeleteMappingRequest, - MapWithCustomFnRequest, + MapWithFnRequest, AIToolResponse, - AIToolRequest + AIToolRequest, + VerifyTypeDeleteRequest, + VerifyTypeDeleteResponse, + DeleteTypeRequest, + DeleteTypeResponse, + ImportIntegrationRequest, + ImportIntegrationResponse, + onMigrationToolStateChanged, + onMigrationToolLogs, + GetMigrationToolsResponse } from "@wso2/ballerina-core"; import { BallerinaExtension } from "./index"; import { debug, handlePullModuleProgress } from "../utils"; import { CMP_LS_CLIENT_COMPLETIONS, CMP_LS_CLIENT_DIAGNOSTICS, getMessageObject, sendTelemetryEvent, TM_EVENT_LANG_CLIENT } from "../features/telemetry"; import { DefinitionParams, InitializeParams, InitializeResult, Location, LocationLink, TextDocumentPositionParams } from 'vscode-languageserver-protocol'; import { updateProjectArtifacts } from "../utils/project-artifacts"; +import { RPCLayer } from "../../src/RPCLayer"; +import { VisualizerWebview } from "../../src/views/visualizer/webview"; export const CONNECTOR_LIST_CACHE = "CONNECTOR_LIST_CACHE"; export const HTTP_CONNECTOR_LIST_CACHE = "HTTP_CONNECTOR_LIST_CACHE"; @@ -289,6 +300,7 @@ enum EXTENDED_APIS { EXAMPLE_LIST = 'ballerinaExample/list', PERF_ANALYZER_RESOURCES_ENDPOINTS = 'performanceAnalyzer/getResourcesWithEndpoints', RESOLVE_MISSING_DEPENDENCIES = 'ballerinaDocument/resolveMissingDependencies', + RESOLVE_MODULE_DEPENDENCIES = 'ballerinaDocument/resolveModuleDependencies', BALLERINA_TO_OPENAPI = 'openAPILSExtension/generateOpenAPI', NOTEBOOK_RESULT = "balShell/getResult", NOTEBOOK_FILE_SOURCE = "balShell/getShellFileSource", @@ -312,11 +324,15 @@ enum EXTENDED_APIS { BI_SOURCE_CODE = 'flowDesignService/getSourceCode', BI_DELETE_NODE = 'flowDesignService/deleteFlowNode', BI_DELETE_BY_COMPONENT_INFO = 'flowDesignService/deleteComponent', + BI_VERIFY_TYPE_DELETE = 'typesManager/verifyTypeDelete', + BI_DELETE_TYPE = 'typesManager/deleteType', BI_AVAILABLE_NODES = 'flowDesignService/getAvailableNodes', BI_AVAILABLE_MODEL_PROVIDERS = 'flowDesignService/getAvailableModelProviders', BI_AVAILABLE_VECTOR_STORES = 'flowDesignService/getAvailableVectorStores', BI_AVAILABLE_EMBEDDING_PROVIDERS = 'flowDesignService/getAvailableEmbeddingProviders', BI_AVAILABLE_VECTOR_KNOWLEDGE_BASES = 'flowDesignService/getAvailableVectorKnowledgeBases', + BI_AVAILABLE_DATA_LOADERS = 'flowDesignService/getAvailableDataLoaders', + BI_AVAILABLE_CHUNKS = 'flowDesignService/getAvailableChunkers', BI_NODE_TEMPLATE = 'flowDesignService/getNodeTemplate', BI_GEN_OPEN_API = 'flowDesignService/generateServiceFromOpenApiContract', BI_MODULE_NODES = 'flowDesignService/getModuleNodes', @@ -333,6 +349,7 @@ enum EXTENDED_APIS { DATA_MAPPER_ADD_SUB_MAPPING = 'dataMapper/addSubMapping', DATA_MAPPER_DELETE_MAPPING = 'dataMapper/deleteMapping', DATA_MAPPER_MAP_WITH_CUSTOM_FN = 'dataMapper/customFunction', + DATA_MAPPER_MAP_WITH_TRANSFORM_FN = 'dataMapper/transformationFunction', DATA_MAPPER_CODEDATA = 'dataMapper/nodePosition', DATA_MAPPER_SUB_MAPPING_CODEDATA = 'dataMapper/subMapping', DATA_MAPPER_PROPERTY = 'dataMapper/fieldPosition', @@ -416,7 +433,12 @@ enum EXTENDED_APIS { GET_ARTIFACTS = 'designModelService/artifacts', PUBLISH_ARTIFACTS = 'designModelService/publishArtifacts', COPILOT_ALL_LIBRARIES = 'copilotLibraryManager/getLibrariesList', - COPILOT_FILTER_LIBRARIES = 'copilotLibraryManager/getFilteredLibraries' + COPILOT_FILTER_LIBRARIES = 'copilotLibraryManager/getFilteredLibraries', + GET_MIGRATION_TOOLS = 'projectService/getMigrationTools', + TIBCO_TO_BI = 'projectService/importTibco', + MULE_TO_BI = 'projectService/importMule', + MIGRATION_TOOL_STATE = 'projectService/stateCallback', + MIGRATION_TOOL_LOG = 'projectService/logCallback', } enum EXTENDED_APIS_ORG { @@ -515,6 +537,32 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl }); } + registerMigrationToolCallbacks(): void { + this.onNotification(EXTENDED_APIS.MIGRATION_TOOL_STATE, (res: ArtifactsNotification) => { + try { + RPCLayer._messenger.sendNotification( + onMigrationToolStateChanged, + { type: "webview", webviewType: VisualizerWebview.viewType }, + res + ); + } catch (error) { + console.error("Error in MIGRATION_TOOL_STATE handler:", error); + } + }); + + this.onNotification(EXTENDED_APIS.MIGRATION_TOOL_LOG, (res: ArtifactsNotification) => { + try { + RPCLayer._messenger.sendNotification( + onMigrationToolLogs, + { type: "webview", webviewType: VisualizerWebview.viewType }, + res + ); + } catch (error) { + console.error("Error in MIGRATION_TOOL_LOG handler:", error); + } + }); + } + async getProjectArtifacts(params: ProjectArtifactsRequest): Promise { return this.sendRequest(EXTENDED_APIS.GET_ARTIFACTS, params); } @@ -695,48 +743,52 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.VISIBLE_VARIABLE_TYPES, params); } - async getInlineDataMapperMappings(params: InlineDataMapperModelRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_MAPPINGS, params); + async getDataMapperMappings(params: DataMapperModelRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_MAPPINGS, params); } - async getInlineDataMapperSource(params: InlineDataMapperSourceRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_GET_SOURCE, params); + async getDataMapperSource(params: DataMapperSourceRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_GET_SOURCE, params); } async getVisualizableFields(params: VisualizableFieldsRequest): Promise { return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_VISUALIZABLE, params); } - async addArrayElement(params: AddArrayElementRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_ADD_ELEMENT, params); + async addArrayElement(params: AddArrayElementRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_ADD_ELEMENT, params); } - async convertToQuery(params: ConvertToQueryRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CONVERT_TO_QUERY, params); + async convertToQuery(params: ConvertToQueryRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CONVERT_TO_QUERY, params); } - async addClauses(params: AddClausesRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_ADD_CLAUSES, params); + async addClauses(params: AddClausesRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_ADD_CLAUSES, params); } - async addSubMapping(params: AddSubMappingRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_ADD_SUB_MAPPING, params); + async addSubMapping(params: AddSubMappingRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_ADD_SUB_MAPPING, params); } - async deleteMapping(params: DeleteMappingRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_DELETE_MAPPING, params); + async deleteMapping(params: DeleteMappingRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_DELETE_MAPPING, params); } - async mapWithCustomFn(params: MapWithCustomFnRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_MAP_WITH_CUSTOM_FN, params); + async mapWithCustomFn(params: MapWithFnRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_MAP_WITH_CUSTOM_FN, params); } - async getDataMapperCodedata(params: GetInlineDataMapperCodedataRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CODEDATA, params); + async mapWithTransformFn(params: MapWithFnRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_MAP_WITH_TRANSFORM_FN, params); } - async getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { - return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_SUB_MAPPING_CODEDATA, params); + async getDataMapperCodedata(params: GetDataMapperCodedataRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CODEDATA, params); + } + + async getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_SUB_MAPPING_CODEDATA, params); } async getProperty(params: PropertyRequest): Promise { @@ -915,7 +967,12 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl async resolveMissingDependencies(req: SyntaxTreeParams): Promise { handlePullModuleProgress(); - return this.sendRequest(EXTENDED_APIS.RESOLVE_MISSING_DEPENDENCIES, req); + const response = await this.sendRequest(EXTENDED_APIS.RESOLVE_MISSING_DEPENDENCIES, req); + return response; + } + + async resolveModuleDependencies(req: SyntaxTreeParams): Promise { + return this.sendRequest(EXTENDED_APIS.RESOLVE_MODULE_DEPENDENCIES, req); } async convertToOpenAPI(params: OpenAPIConverterParams): Promise { @@ -954,6 +1011,14 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.BI_AVAILABLE_VECTOR_KNOWLEDGE_BASES, params); } + async getAvailableDataLoaders(params: BIAvailableNodesRequest): Promise { + return this.sendRequest(EXTENDED_APIS.BI_AVAILABLE_DATA_LOADERS, params); + } + + async getAvailableChunkers(params: BIAvailableNodesRequest): Promise { + return this.sendRequest(EXTENDED_APIS.BI_AVAILABLE_CHUNKS, params); + } + async getEnclosedFunctionDef(params: BIGetEnclosedFunctionRequest): Promise { return this.sendRequest(EXTENDED_APIS.BI_GET_ENCLOSED_FUNCTION, params); } @@ -1010,6 +1075,14 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.BI_DELETE_BY_COMPONENT_INFO, params); } + async verifyTypeDelete(params: VerifyTypeDeleteRequest): Promise { + return this.sendRequest(EXTENDED_APIS.BI_VERIFY_TYPE_DELETE, params); + } + + async deleteType(params: DeleteTypeRequest): Promise { + return this.sendRequest(EXTENDED_APIS.BI_DELETE_TYPE, params); + } + async getSequenceDiagramModel(params: SequenceModelRequest): Promise { // const isSupported = await this.isExtendedServiceSupported(EXTENDED_APIS.SEQUENCE_DIAGRAM_MODEL); return this.sendRequest(EXTENDED_APIS.SEQUENCE_DIAGRAM_MODEL, params); @@ -1247,6 +1320,19 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.COPILOT_FILTER_LIBRARIES, params); } + async getMigrationTools(): Promise { + return this.sendRequest(EXTENDED_APIS.GET_MIGRATION_TOOLS); + } + + async importTibcoToBI(params: ImportIntegrationRequest): Promise { + debug(`Importing Tibco to Ballerina: ${JSON.stringify(params)}`); + return this.sendRequest(EXTENDED_APIS.TIBCO_TO_BI, params); + } + + async importMuleToBI(params: ImportIntegrationRequest): Promise { + debug(`Importing Mule to Ballerina: ${JSON.stringify(params)}`); + return this.sendRequest(EXTENDED_APIS.MULE_TO_BI, params); + } // <------------ BI APIS END ---------------> diff --git a/workspaces/ballerina/ballerina-extension/src/core/extension.ts b/workspaces/ballerina/ballerina-extension/src/core/extension.ts index b4d625a1223..96b4e1b3bf3 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extension.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extension.ts @@ -33,7 +33,7 @@ import { exec, spawnSync } from 'child_process'; import { LanguageClientOptions, State as LS_STATE, RevealOutputChannelOn, ServerOptions } from "vscode-languageclient/node"; import { getServerOptions } from '../utils/server/server'; import { ExtendedLangClient } from './extended-language-client'; -import { debug, log, getOutputChannel, outputChannel, isWindows, isSupportedVersion, VERSION, isSupportedSLVersion } from '../utils'; +import { debug, log, getOutputChannel, outputChannel, isWindows, isWSL, isSupportedVersion, VERSION, isSupportedSLVersion } from '../utils'; import { AssertionError } from "assert"; import { BALLERINA_HOME, ENABLE_ALL_CODELENS, ENABLE_TELEMETRY, ENABLE_SEMANTIC_HIGHLIGHTING, OVERRIDE_BALLERINA_HOME, @@ -48,7 +48,8 @@ import { SHOW_LIBRARY_CONFIG_VARIABLES, LANG_SERVER_PATH, USE_BALLERINA_CLI_LANG_SERVER, - INCLUDE_CURRENT_ORGANIZATION_IN_SEARCH + INCLUDE_CURRENT_ORGANIZATION_IN_SEARCH, + SHOW_ADVANCED_AI_NODES } from "./preferences"; import TelemetryReporter from "vscode-extension-telemetry"; @@ -155,7 +156,6 @@ export class BallerinaExtension { private isOpenedOnce: boolean; private ballerinaUserHome: string; private ballerinaUserHomeName: string; - private ballerinaIntegratorVersion: string; private ballerinaIntegratorReleaseUrl: string; private ballerinaHomeCustomDirName: string; private ballerinaInstallationDir: string; @@ -163,66 +163,162 @@ export class BallerinaExtension { private ballerinaUpdateToolUserAgent: string; constructor() { - this.ballerinaHome = ''; - this.ballerinaCmd = ''; - this.ballerinaVersion = ''; - this.biSupported = false; - this.isNPSupported = false; - this.isPersist = false; - this.ballerinaUserHomeName = '.ballerina'; - this.ballerinaUserHome = path.join(this.getUserHomeDirectory(), this.ballerinaUserHomeName); - this.ballerinaIntegratorReleaseUrl = "https://api.github.com/repos/ballerina-platform/ballerina-distribution/releases"; - this.ballerinaHomeCustomDirName = "ballerina-home"; - this.ballerinaInstallationDir = path.join(this.getBallerinaUserHome(), this.ballerinaHomeCustomDirName); - - this.updateToolServerUrl = "https://api.central.ballerina.io/2.0/update-tool"; - if (this.overrideBallerinaHome()) { - this.updateToolServerUrl = "https://api.staging-central.ballerina.io/2.0/update-tool"; - } - this.ballerinaUpdateToolUserAgent = this.getUpdateToolUserAgent(); - this.showStatusBarItem(); - // Load the extension - this.extension = extensions.getExtension(EXTENSION_ID)!; - this.clientOptions = { - documentSelector: [{ scheme: 'file', language: LANGUAGE.BALLERINA }, { - scheme: 'file', language: - LANGUAGE.TOML - }], - synchronize: { configurationSection: LANGUAGE.BALLERINA }, - outputChannel: getOutputChannel(), - revealOutputChannelOn: RevealOutputChannelOn.Never, - initializationOptions: { - "enableSemanticHighlighting": workspace.getConfiguration().get(ENABLE_SEMANTIC_HIGHLIGHTING), - "enableBackgroundDriftCheck": workspace.getConfiguration().get(ENABLE_BACKGROUND_DRIFT_CHECK), - "enableInlayHints": workspace.getConfiguration().get(ENABLE_INLAY_HINTS), - "supportBalaScheme": "true", - "supportQuickPick": "true", - "supportPositionalRenamePopup": "true" + debug("[EXTENSION] Starting constructor initialization..."); + + try { + // Initialize basic properties + this.ballerinaHome = ''; + this.ballerinaCmd = ''; + this.ballerinaVersion = ''; + this.biSupported = false; + this.isNPSupported = false; + this.isPersist = false; + this.ballerinaUserHomeName = '.ballerina'; + + debug("[EXTENSION] Basic properties initialized"); + + // Set up directory paths + try { + const userHomeDir = this.getUserHomeDirectory(); + debug(`[EXTENSION] User home directory: ${userHomeDir}`); + this.ballerinaUserHome = path.join(userHomeDir, this.ballerinaUserHomeName); + debug(`[EXTENSION] Ballerina user home: ${this.ballerinaUserHome}`); + } catch (error) { + debug(`[EXTENSION] Error setting up user home directory: ${error}`); + throw error; } - }; - this.telemetryReporter = createTelemetryReporter(this); - this.documentContext = new DocumentContext(); - this.codeServerContext = { - codeServerEnv: this.isCodeServerEnv(), - manageChoreoRedirectUri: process.env.VSCODE_CHOREO_DEPLOY_URI, - infoMessageStatus: { - sourceControlMessage: true, - messageFirstEdit: true + this.ballerinaIntegratorReleaseUrl = "https://api.github.com/repos/ballerina-platform/ballerina-distribution/releases"; + this.ballerinaHomeCustomDirName = "ballerina-home"; + + try { + this.ballerinaInstallationDir = path.join(this.getBallerinaUserHome(), this.ballerinaHomeCustomDirName); + debug(`[EXTENSION] Ballerina installation directory: ${this.ballerinaInstallationDir}`); + } catch (error) { + debug(`[EXTENSION] Error setting installation directory: ${error}`); + throw error; } - }; - if (this.isCodeServerEnv()) { - commands.executeCommand('workbench.action.closeAllEditors'); - this.showCookieConsentMessage(); - this.getCodeServerContext().telemetryTracker = new TelemetryTracker(); - } - this.webviewContext = { isOpen: false }; - this.perfForecastContext = { - infoMessageStatus: { - signinChoreo: true + + // Set up server URLs + this.updateToolServerUrl = "https://api.central.ballerina.io/2.0/update-tool"; + try { + if (this.overrideBallerinaHome()) { + this.updateToolServerUrl = "https://api.staging-central.ballerina.io/2.0/update-tool"; + debug("[EXTENSION] Using staging update tool server URL"); + } + debug(`[EXTENSION] Update tool server URL: ${this.updateToolServerUrl}`); + } catch (error) { + debug(`[EXTENSION] Error setting update tool server URL: ${error}`); } - }; - this.ballerinaConfigPath = ''; + + try { + this.ballerinaUpdateToolUserAgent = this.getUpdateToolUserAgent(); + debug(`[EXTENSION] Update tool user agent: ${this.ballerinaUpdateToolUserAgent}`); + } catch (error) { + debug(`[EXTENSION] Error getting update tool user agent: ${error}`); + this.ballerinaUpdateToolUserAgent = null; + } + + try { + this.showStatusBarItem(); + debug("[EXTENSION] Status bar item initialized"); + } catch (error) { + debug(`[EXTENSION] Error initializing status bar: ${error}`); + } + + // Load the extension + try { + this.extension = extensions.getExtension(EXTENSION_ID)!; + if (this.extension) { + debug(`[EXTENSION] Extension loaded successfully: ${EXTENSION_ID}`); + } else { + throw new Error(`Extension ${EXTENSION_ID} not found`); + } + } catch (error) { + debug(`[EXTENSION] Error loading extension: ${error}`); + throw error; + } + + // Set up client options + try { + this.clientOptions = { + documentSelector: [{ scheme: 'file', language: LANGUAGE.BALLERINA }, { + scheme: 'file', language: + LANGUAGE.TOML + }], + synchronize: { configurationSection: LANGUAGE.BALLERINA }, + outputChannel: getOutputChannel(), + revealOutputChannelOn: RevealOutputChannelOn.Never, + initializationOptions: { + "enableSemanticHighlighting": workspace.getConfiguration().get(ENABLE_SEMANTIC_HIGHLIGHTING), + "enableBackgroundDriftCheck": workspace.getConfiguration().get(ENABLE_BACKGROUND_DRIFT_CHECK), + "enableInlayHints": workspace.getConfiguration().get(ENABLE_INLAY_HINTS), + "supportBalaScheme": "true", + "supportQuickPick": "true", + "supportPositionalRenamePopup": "true" + } + }; + debug("[EXTENSION] Client options configured"); + } catch (error) { + debug(`[EXTENSION] Error setting up client options: ${error}`); + throw error; + } + + try { + this.telemetryReporter = createTelemetryReporter(this); + debug("[EXTENSION] Telemetry reporter created"); + } catch (error) { + debug(`[EXTENSION] Error creating telemetry reporter: ${error}`); + // Don't throw here, telemetry is not critical + } + + try { + this.documentContext = new DocumentContext(); + debug("[EXTENSION] Document context initialized"); + } catch (error) { + debug(`[EXTENSION] Error initializing document context: ${error}`); + throw error; + } + + try { + this.codeServerContext = { + codeServerEnv: this.isCodeServerEnv(), + manageChoreoRedirectUri: process.env.VSCODE_CHOREO_DEPLOY_URI, + infoMessageStatus: { + sourceControlMessage: true, + messageFirstEdit: true + } + }; + debug(`[EXTENSION] Code server context initialized. Code server env: ${this.codeServerContext.codeServerEnv}`); + } catch (error) { + debug(`[EXTENSION] Error initializing code server context: ${error}`); + throw error; + } + + if (this.isCodeServerEnv()) { + try { + commands.executeCommand('workbench.action.closeAllEditors'); + this.showCookieConsentMessage(); + this.getCodeServerContext().telemetryTracker = new TelemetryTracker(); + debug("[EXTENSION] Code server environment setup completed"); + } catch (error) { + debug(`[EXTENSION] Error setting up code server environment: ${error}`); + } + } + + this.webviewContext = { isOpen: false }; + this.perfForecastContext = { + infoMessageStatus: { + signinChoreo: true + } + }; + this.ballerinaConfigPath = ''; + + debug("[EXTENSION] Constructor completed successfully"); + } catch (error) { + debug(`[EXTENSION] Fatal error in constructor: ${error}`); + throw error; + } } setContext(context: ExtensionContext) { @@ -230,137 +326,285 @@ export class BallerinaExtension { } init(_onBeforeInit: Function): Promise { - if (extensions.getExtension(PREV_EXTENSION_ID)) { - this.showUninstallOldVersion(); - } - // Register show logs command. - const showLogs = commands.registerCommand('ballerina.showLogs', () => { - outputChannel.show(); - }); - this.context!.subscriptions.push(showLogs); + debug("[INIT] Starting extension initialization..."); + debug(`[INIT] Platform: ${process.platform}, Architecture: ${process.arch}`); + debug(`[INIT] Node version: ${process.version}`); + debug(`[INIT] VS Code version: ${env.appName} ${env.appHost}`); + debug(`[INIT] Extension version: ${this.getVersion()}`); + + // Log environment information for WSL debugging + debug(`[INIT] Environment variables:`); + debug(`[INIT] WSL_DISTRO_NAME: ${process.env.WSL_DISTRO_NAME || 'Not set'}`); + debug(`[INIT] WSLENV: ${process.env.WSLENV || 'Not set'}`); + debug(`[INIT] PATH: ${process.env.PATH || 'Not set'}`); + debug(`[INIT] HOME: ${process.env.HOME || 'Not set'}`); + debug(`[INIT] USERPROFILE: ${process.env.USERPROFILE || 'Not set'}`); - commands.registerCommand(showMessageInstallBallerinaCommand, () => { - this.showMessageInstallBallerina(); - }); + try { + // Check for old extension version + if (extensions.getExtension(PREV_EXTENSION_ID)) { + debug("[INIT] Found old extension version, showing uninstall message"); + this.showUninstallOldVersion(); + } else { + debug("[INIT] No old extension version found"); + } - commands.registerCommand('ballerina.setup-ballerina', () => { // Install ballerina from central for new users. This should set the ballerina to system path - this.installBallerina(); - }); + // Register show logs command + try { + const showLogs = commands.registerCommand('ballerina.showLogs', () => { + outputChannel.show(); + }); + this.context!.subscriptions.push(showLogs); + debug("[INIT] Show logs command registered successfully"); + } catch (error) { + debug(`[INIT] Error registering show logs command: ${error}`); + throw error; + } - commands.registerCommand('ballerina.update-ballerina', () => { // Update release pack from ballerina update tool with terminal - this.updateBallerina(); - }); + // Register other commands + try { + commands.registerCommand(showMessageInstallBallerinaCommand, () => { + this.showMessageInstallBallerina(); + }); + debug("[INIT] Install Ballerina message command registered"); - commands.registerCommand('ballerina.update-ballerina-visually', () => { // Update release pack from ballerina update tool with webview - this.updateBallerinaVisually(); - }); + commands.registerCommand('ballerina.setup-ballerina', () => { + this.installBallerina(); + }); + debug("[INIT] Setup Ballerina command registered"); - try { - // Register pre init handlers. - this.registerPreInitHandlers(); - - // Check if ballerina home is set. - if (this.overrideBallerinaHome()) { - if (!this.getConfiguredBallerinaHome()) { - const message = "Trying to get ballerina version without setting ballerina home."; - sendTelemetryEvent(this, TM_EVENT_ERROR_INVALID_BAL_HOME_CONFIGURED, CMP_EXTENSION_CORE, getMessageObject(message)); - throw new AssertionError({ - message: message - }); - } + commands.registerCommand('ballerina.update-ballerina', () => { + this.updateBallerina(); + }); + debug("[INIT] Update Ballerina command registered"); + + commands.registerCommand('ballerina.update-ballerina-visually', () => { + this.updateBallerinaVisually(); + }); + debug("[INIT] Update Ballerina visually command registered"); + } catch (error) { + debug(`[INIT] Error registering commands: ${error}`); + throw error; + } + + // Register pre init handlers + try { + debug("[INIT] Registering pre-initialization handlers..."); + this.registerPreInitHandlers(); + debug("[INIT] Pre-initialization handlers registered successfully"); + } catch (error) { + debug(`[INIT] Error registering pre-init handlers: ${error}`); + throw error; + } + + // Check and configure Ballerina home + try { + if (this.overrideBallerinaHome()) { + debug("[INIT] Override Ballerina home is enabled"); + const configuredHome = this.getConfiguredBallerinaHome(); + if (!configuredHome) { + const message = "Trying to get ballerina version without setting ballerina home."; + debug(`[INIT] Error: ${message}`); + sendTelemetryEvent(this, TM_EVENT_ERROR_INVALID_BAL_HOME_CONFIGURED, CMP_EXTENSION_CORE, getMessageObject(message)); + throw new AssertionError({ + message: message + }); + } - debug("Ballerina home is configured in settings."); - this.ballerinaHome = this.getConfiguredBallerinaHome(); + debug(`[INIT] Configured Ballerina home: ${configuredHome}`); + this.ballerinaHome = configuredHome; + } else { + debug("[INIT] Override Ballerina home is disabled, will auto-detect"); + } + } catch (error) { + debug(`[INIT] Error configuring Ballerina home: ${error}`); + throw error; } - // Validate the ballerina version. + debug(`[INIT] Current Ballerina home: ${this.ballerinaHome}`); + debug(`[INIT] Override Ballerina home setting: ${this.overrideBallerinaHome()}`); + debug("[INIT] Starting Ballerina version validation..."); + + // Validate the ballerina version return this.getBallerinaVersion(this.ballerinaHome, this.overrideBallerinaHome()).then(async runtimeVersion => { debug("=".repeat(60)); - this.ballerinaVersion = runtimeVersion; - log(`Plugin version: ${this.getVersion()}`); - log(`Ballerina version: ${this.ballerinaVersion}`); - - this.biSupported = isSupportedSLVersion(this, 2201123); // Minimum supported version for BI - this.isNPSupported = isSupportedSLVersion(this, 2201130) && this.enabledExperimentalFeatures(); // Minimum supported requirements for NP - const { home } = this.autoDetectBallerinaHome(); - this.ballerinaHome = home; - debug(`Ballerina Home: ${this.ballerinaHome}`); - debug(`Plugin Dev Mode: ${this.overrideBallerinaHome()}`); - debug(`Debug Mode: ${this.enableLSDebug()}`); - debug(`Feature flags - Experimental: ${this.enabledExperimentalFeatures()}, BI: ${this.biSupported}, NP: ${this.isNPSupported}`); - - if (!this.ballerinaVersion.match(SWAN_LAKE_REGEX) || (this.ballerinaVersion.match(SWAN_LAKE_REGEX) && - !isSupportedVersion(this, VERSION.BETA, 3))) { - this.showMessageOldBallerina(); - const message = `Ballerina version ${this.ballerinaVersion} is not supported. - The extension supports Ballerina Swan Lake Beta 3+ versions.`; - sendTelemetryEvent(this, TM_EVENT_ERROR_OLD_BAL_HOME_DETECTED, CMP_EXTENSION_CORE, getMessageObject(message)); - return; + debug("[INIT] Ballerina version retrieved successfully"); + + try { + this.ballerinaVersion = runtimeVersion; + log(`Plugin version: ${this.getVersion()}`); + log(`Ballerina version: ${this.ballerinaVersion}`); + debug(`[INIT] Version information logged`); + } catch (error) { + debug(`[INIT] Error logging version information: ${error}`); + throw error; } - // if Home is found load Language Server. - let serverOptions: ServerOptions; - serverOptions = getServerOptions(this); - this.langClient = new ExtendedLangClient('ballerina-vscode', 'Ballerina LS Client', serverOptions, - this.clientOptions, this, false); - - _onBeforeInit(this.langClient); - - await this.langClient.start(); - debug(`Language Server Started`); - - // Following was put in to handle server startup failures. - if (this.langClient.state === LS_STATE.Stopped) { - const message = "Couldn't establish language server connection."; - sendTelemetryEvent(this, TM_EVENT_EXTENSION_INI_FAILED, CMP_EXTENSION_CORE, getMessageObject(message)); - log(message); - this.showPluginActivationError(); - } else if (this.langClient.state === LS_STATE.Running) { - await this.langClient?.registerExtendedAPICapabilities(); - this.updateStatusBar(this.ballerinaVersion); - sendTelemetryEvent(this, TM_EVENT_EXTENSION_INIT, CMP_EXTENSION_CORE); + try { + this.biSupported = isSupportedSLVersion(this, 2201123); // Minimum supported version for BI + this.isNPSupported = isSupportedSLVersion(this, 2201130) && this.enabledExperimentalFeatures(); // Minimum supported requirements for NP + debug(`[INIT] Feature support calculated - BI: ${this.biSupported}, NP: ${this.isNPSupported}`); + } catch (error) { + debug(`[INIT] Error calculating feature support: ${error}`); + // Don't throw here, we can continue without these features + } + + try { + const { home, isOldBallerinaDist, isBallerinaNotFound } = this.autoDetectBallerinaHome(); + this.ballerinaHome = home; + debug(`[INIT] Auto-detected Ballerina Home: ${this.ballerinaHome}`); + debug(`[INIT] Is old Ballerina distribution: ${isOldBallerinaDist}`); + debug(`[INIT] Is Ballerina not found: ${isBallerinaNotFound}`); + } catch (error) { + debug(`[INIT] Error auto-detecting Ballerina home: ${error}`); + throw error; + } + + debug(`[INIT] Final Ballerina Home: ${this.ballerinaHome}`); + debug(`[INIT] Plugin Dev Mode: ${this.overrideBallerinaHome()}`); + debug(`[INIT] Debug Mode: ${this.enableLSDebug()}`); + debug(`[INIT] Feature flags - Experimental: ${this.enabledExperimentalFeatures()}, BI: ${this.biSupported}, NP: ${this.isNPSupported}`); + + // Check version compatibility + try { + if (!this.ballerinaVersion.match(SWAN_LAKE_REGEX) || (this.ballerinaVersion.match(SWAN_LAKE_REGEX) && + !isSupportedVersion(this, VERSION.BETA, 3))) { + debug(`[INIT] Unsupported Ballerina version detected: ${this.ballerinaVersion}`); + this.showMessageOldBallerina(); + const message = `Ballerina version ${this.ballerinaVersion} is not supported. + The extension supports Ballerina Swan Lake Beta 3+ versions.`; + sendTelemetryEvent(this, TM_EVENT_ERROR_OLD_BAL_HOME_DETECTED, CMP_EXTENSION_CORE, getMessageObject(message)); + debug("[INIT] Returning early due to unsupported version"); + return; + } + debug("[INIT] Ballerina version is compatible"); + } catch (error) { + debug(`[INIT] Error checking version compatibility: ${error}`); + throw error; + } + + // Set up and start Language Server + try { + debug("[INIT] Setting up Language Server..."); + let serverOptions: ServerOptions; + serverOptions = getServerOptions(this); + debug("[INIT] Server options retrieved"); + + this.langClient = new ExtendedLangClient('ballerina-vscode', 'Ballerina LS Client', serverOptions, + this.clientOptions, this, false); + debug("[INIT] Extended Language Client created"); + + _onBeforeInit(this.langClient); + debug("[INIT] Before init callback executed"); + + await this.langClient.start(); + debug(`[INIT] Language Server started with state: ${this.langClient.state}`); + } catch (error) { + debug(`[INIT] Error setting up/starting Language Server: ${error}`); + throw error; + } + + // Handle server startup results + try { + if (this.langClient.state === LS_STATE.Stopped) { + const message = "Couldn't establish language server connection."; + debug(`[INIT] Language server failed to start: ${message}`); + sendTelemetryEvent(this, TM_EVENT_EXTENSION_INI_FAILED, CMP_EXTENSION_CORE, getMessageObject(message)); + log(message); + this.showPluginActivationError(); + } else if (this.langClient.state === LS_STATE.Running) { + debug("[INIT] Language server is running, registering extended API capabilities"); + await this.langClient?.registerExtendedAPICapabilities(); + this.updateStatusBar(this.ballerinaVersion); + sendTelemetryEvent(this, TM_EVENT_EXTENSION_INIT, CMP_EXTENSION_CORE); + debug("[INIT] Extension initialization completed successfully"); + } + } catch (error) { + debug(`[INIT] Error handling server startup results: ${error}`); + throw error; + } + + // Register stop command + try { + commands.registerCommand('ballerina.stopLangServer', () => { + debug("[INIT] Stop Language Server command executed"); + this.langClient.stop(); + }); + debug("[INIT] Stop Language Server command registered"); + } catch (error) { + debug(`[INIT] Error registering stop command: ${error}`); + // Don't throw here, this is not critical } - commands.registerCommand('ballerina.stopLangServer', () => { - this.langClient.stop(); - }); debug("=".repeat(60)); + debug("[INIT] Extension initialization completed successfully"); }, (reason) => { + debug(`[INIT] Error getting ballerina version: ${reason.message || reason}`); sendTelemetryException(this, reason, CMP_EXTENSION_CORE); this.showMessageInstallBallerina(); throw new Error(reason); }).catch(e => { + debug(`[INIT] Caught error during initialization: ${e.message || e}`); const msg = `Error when checking ballerina version. ${e.message}`; sendTelemetryException(this, e, CMP_EXTENSION_CORE, getMessageObject(msg)); - this.telemetryReporter.dispose(); + this.telemetryReporter?.dispose(); throw new Error(msg); }); } catch (ex) { - let msg = "Error happened."; + debug(`[INIT] Fatal error initializing Ballerina Extension: ${ex}`); + let msg = "Fatal error during extension initialization."; if (ex instanceof Error) { msg = "Error while activating plugin. " + (ex.message ? ex.message : ex); + debug(`[INIT] Error details: ${msg}`); // If any failure occurs while initializing show an error message this.showPluginActivationError(); sendTelemetryException(this, ex, CMP_EXTENSION_CORE, getMessageObject(msg)); - this.telemetryReporter.dispose(); + this.telemetryReporter?.dispose(); } + debug(`[INIT] Rejecting promise with: ${msg}`); return Promise.reject(msg); } } private getUpdateToolUserAgent(): string { + debug("[USER_AGENT] Detecting platform for user agent..."); + const platform = os.platform(); + const arch = os.arch(); + + debug(`[USER_AGENT] Platform: ${platform}, Architecture: ${arch}`); + + // Log additional environment info for WSL debugging + if (process.env.WSL_DISTRO_NAME) { + debug(`[USER_AGENT] WSL environment detected: ${process.env.WSL_DISTRO_NAME}`); + } + + let userAgent: string | null = null; + if (platform === 'win32') { - return "ballerina/2201.11.0 (win-64) Updater/1.4.5"; + userAgent = "ballerina/2201.11.0 (win-64) Updater/1.4.5"; + debug("[USER_AGENT] Selected Windows user agent"); } else if (platform === 'linux') { - return "ballerina/2201.11.0 (linux-64) Updater/1.4.5"; + userAgent = "ballerina/2201.11.0 (linux-64) Updater/1.4.5"; + debug("[USER_AGENT] Selected Linux user agent"); + + if (process.env.WSL_DISTRO_NAME) { + debug("[USER_AGENT] Note: Running in WSL environment"); + } } else if (platform === 'darwin') { - if (os.arch() === 'arm64') { - return "ballerina/2201.11.0 (macos-arm-64) Updater/1.4.5"; + if (arch === 'arm64') { + userAgent = "ballerina/2201.11.0 (macos-arm-64) Updater/1.4.5"; + debug("[USER_AGENT] Selected macOS ARM64 user agent"); + } else { + userAgent = "ballerina/2201.11.0 (macos-64) Updater/1.4.5"; + debug("[USER_AGENT] Selected macOS x64 user agent"); } - return "ballerina/2201.11.0 (macos-64) Updater/1.4.5"; + } else { + debug(`[USER_AGENT] Unknown platform: ${platform}, returning null`); } - return null; + + debug(`[USER_AGENT] Final user agent: ${userAgent}`); + return userAgent; } async getLatestBallerinaVersion(): Promise { @@ -542,7 +786,7 @@ export class BallerinaExtension { // Show a message to the user that they'll need to respond to the UAC prompt window.showInformationMessage('Please confirm the User Account Control (UAC) prompt to run this command with administrator privileges'); - await new Promise((resolve, reject) => { + await new Promise(() => { try { // Execute the PowerShell command const childProcess = exec(psCommand, { maxBuffer: 1024 * 1024 }); @@ -555,7 +799,7 @@ export class BallerinaExtension { throw new Error(errorMessage); } }); - childProcess.on('close', async (code, signal) => { + childProcess.on('close', async (code) => { // Note: with Windows UAC, the actual admin process is detached, so this code // only confirms the elevation request was successful, not the command itself if (code === 0) { @@ -602,152 +846,6 @@ export class BallerinaExtension { }); } - // TODO: This can be removed. - // Alternative method that uses a temporary script to execute sudo commands - private async executeSudoCommandWithScript(command: string): Promise { - - // macOS/Linux: Get password for sudo - const password = await window.showInputBox({ - prompt: 'Enter your sudo password', - password: true, - ignoreFocusOut: true - }); - - if (password === undefined) { - window.showErrorMessage('Password required for sudo command'); - return; - } - - let progressStep = 0; - - // Send initial progress notification - let res: DownloadProgress = { - message: `Starting execution of sudo command...`, - percentage: 0, - success: false, - step: progressStep - }; - RPCLayer._messenger.sendNotification(onDownloadProgress, { type: 'webview', webviewType: VisualizerWebview.viewType }, res); - - return new Promise((resolve, reject) => { - try { - // Create a temporary file for the command - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-ballerina-')); - const scriptPath = path.join(tempDir, 'sudo-script.sh'); - - // Extract the actual command (remove 'sudo' prefix) - const actualCommand = command.replace(/^sudo\s+/, ''); - - // Create a script file that will be executed with sudo - fs.writeFileSync(scriptPath, `#!/bin/bash\n${actualCommand}\n`, { mode: 0o755 }); - - // Execute the script with sudo and pipe the password - console.log(`Executing sudo command using script: ${scriptPath}`); - - // Use echo to pass the password to sudo - const sudoCmd = `echo ${password} | sudo -S ${scriptPath}`; - const childProcess = exec(sudoCmd); - - childProcess.stdout.on('data', (data) => { - const output = data.toString(); - console.log('Command output:', output); - - progressStep++; - const percentage = Math.min(progressStep * 10, 90); - - res = { - message: `Executing: ${output.trim()}`, - percentage: percentage, - success: false, - step: progressStep - }; - RPCLayer._messenger.sendNotification(onDownloadProgress, { type: 'webview', webviewType: VisualizerWebview.viewType }, res); - }); - - childProcess.stderr.on('data', (data) => { - const errorOutput = data.toString(); - console.error('Command error:', errorOutput); - - // Handle common sudo errors - if (errorOutput.includes('is not in the sudoers file') || errorOutput.includes('not allowed to execute')) { - res = { - message: `Sudo permission error: You don't have sudo privileges for this command`, - percentage: 0, - success: false, - step: -1 - }; - RPCLayer._messenger.sendNotification(onDownloadProgress, { type: 'webview', webviewType: VisualizerWebview.viewType }, res); - window.showErrorMessage(`Sudo permission error: You don't have privileges for this command`); - reject(new Error('Sudo permission error: insufficient privileges')); - return; - } - - if (errorOutput.includes('incorrect password') || errorOutput.includes('Sorry, try again')) { - res = { - message: `Sudo authentication failed: Incorrect password`, - percentage: 0, - success: false, - step: -1 - }; - RPCLayer._messenger.sendNotification(onDownloadProgress, { type: 'webview', webviewType: VisualizerWebview.viewType }, res); - window.showErrorMessage(`Sudo authentication failed: Incorrect password`); - reject(new Error('Sudo authentication failed: Incorrect password')); - return; - } - - res = { - message: `Error: ${errorOutput.trim()}`, - percentage: 0, - success: false, - step: -1 - }; - RPCLayer._messenger.sendNotification(onDownloadProgress, { type: 'webview', webviewType: VisualizerWebview.viewType }, res); - }); - - childProcess.on('close', (code) => { - console.log(`Command exited with code ${code}`); - - // Clean up the temporary files - try { - fs.unlinkSync(scriptPath); - fs.rmdirSync(tempDir); - } catch (err) { - console.error('Error cleaning up temporary files:', err); - } - - res = { - message: code === 0 ? 'Command completed successfully' : `Command failed with code ${code}`, - percentage: 100, - success: code === 0, - step: code === 0 ? progressStep + 1 : -1 - }; - RPCLayer._messenger.sendNotification(onDownloadProgress, { type: 'webview', webviewType: VisualizerWebview.viewType }, res); - - if (code === 0) { - window.showInformationMessage('Command executed successfully'); - commands.executeCommand('workbench.action.reloadWindow'); - resolve(); - } else { - window.showErrorMessage(`Command failed with exit code ${code}`); - reject(new Error(`Command failed with exit code ${code}`)); - } - }); - } catch (error) { - console.error('Error executing sudo command with script:', error); - const errorMessage = error instanceof Error ? error.message : String(error); - window.showErrorMessage(`Error executing sudo command: ${errorMessage}`); - - res = { - message: `Error executing sudo command: ${errorMessage}`, - percentage: 0, - success: false, - step: -1 - }; - RPCLayer._messenger.sendNotification(onDownloadProgress, { type: 'webview', webviewType: VisualizerWebview.viewType }, res); - reject(error); - } - }); - } // Install ballerina from the central private async installBallerina(restartWindow?: boolean) { @@ -1305,7 +1403,31 @@ export class BallerinaExtension { } private getUserHomeDirectory(): string { - return os.homedir(); + debug("[HOME_DIR] Getting user home directory..."); + + const homeDir = os.homedir(); + debug(`[HOME_DIR] OS homedir(): ${homeDir}`); + + // Log environment variables for debugging WSL issues + debug(`[HOME_DIR] Environment variables:`); + debug(`[HOME_DIR] - HOME: ${process.env.HOME || 'Not set'}`); + debug(`[HOME_DIR] - USERPROFILE: ${process.env.USERPROFILE || 'Not set'}`); + debug(`[HOME_DIR] - WSL_DISTRO_NAME: ${process.env.WSL_DISTRO_NAME || 'Not set'}`); + + // Validate the home directory exists + try { + const fs = require('fs'); + const homeStats = fs.statSync(homeDir); + if (homeStats.isDirectory()) { + debug(`[HOME_DIR] Home directory is valid and accessible: ${homeDir}`); + } else { + debug(`[HOME_DIR] Warning: Home path exists but is not a directory: ${homeDir}`); + } + } catch (error) { + debug(`[HOME_DIR] Warning: Cannot access home directory ${homeDir}: ${error}`); + } + + return homeDir; } getBallerinaUserHome(): string { @@ -1313,22 +1435,46 @@ export class BallerinaExtension { } showStatusBarItem() { - this.sdkVersion = window.createStatusBarItem(StatusBarAlignment.Right, 100); - this.updateStatusBar("Detecting"); - this.sdkVersion.command = "ballerina.showLogs"; - this.sdkVersion.show(); - - window.onDidChangeActiveTextEditor((editor) => { - this.sdkVersion.text = this.sdkVersion.text.replace(SDK_PREFIX, ''); - if (!editor) { - this.updateStatusBar(this.sdkVersion.text); - this.sdkVersion.show(); - } else if (editor.document.uri.scheme === 'file' && editor.document.languageId === 'ballerina') { - this.sdkVersion.show(); - } else { - this.sdkVersion.hide(); - } - }); + debug("[STATUS_BAR] Creating status bar item..."); + + try { + this.sdkVersion = window.createStatusBarItem(StatusBarAlignment.Right, 100); + debug("[STATUS_BAR] Status bar item created successfully"); + + this.updateStatusBar("Detecting"); + debug("[STATUS_BAR] Status bar text set to 'Detecting'"); + + this.sdkVersion.command = "ballerina.showLogs"; + debug("[STATUS_BAR] Status bar command set to 'ballerina.showLogs'"); + + this.sdkVersion.show(); + debug("[STATUS_BAR] Status bar item shown successfully"); + + window.onDidChangeActiveTextEditor((editor) => { + debug("[STATUS_BAR] Active text editor changed"); + try { + this.sdkVersion.text = this.sdkVersion.text.replace(SDK_PREFIX, ''); + if (!editor) { + debug("[STATUS_BAR] No active editor"); + this.updateStatusBar(this.sdkVersion.text); + this.sdkVersion.show(); + } else if (editor.document.uri.scheme === 'file' && editor.document.languageId === 'ballerina') { + debug("[STATUS_BAR] Ballerina file is active"); + this.sdkVersion.show(); + } else { + debug(`[STATUS_BAR] Non-Ballerina file is active: ${editor.document.languageId}`); + this.sdkVersion.hide(); + } + } catch (error) { + debug(`[STATUS_BAR] Error updating status bar on editor change: ${error}`); + } + }); + + debug("[STATUS_BAR] Status bar initialization completed successfully"); + } catch (error) { + debug(`[STATUS_BAR] Error initializing status bar: ${error}`); + throw error; + } } updateStatusBar(text: string) { @@ -1393,61 +1539,188 @@ export class BallerinaExtension { } async getBallerinaVersion(ballerinaHome: string, overrideBallerinaHome: boolean): Promise { - // Initialize with fresh environment - await this.syncEnvironment(); + debug("[VERSION] Starting Ballerina version detection..."); + debug(`[VERSION] Input parameters - ballerinaHome: '${ballerinaHome}', overrideBallerinaHome: ${overrideBallerinaHome}`); + + try { + // Initialize with fresh environment + debug("[VERSION] Syncing environment variables..."); + await this.syncEnvironment(); + debug("[VERSION] Environment sync completed"); + } catch (error) { + debug(`[VERSION] Warning: Failed to sync environment: ${error}`); + // Continue anyway, don't fail the whole process + } + + // Log current environment for debugging + debug(`[VERSION] Current working directory: ${process.cwd()}`); + debug(`[VERSION] Current PATH: ${process.env.PATH?.substring(0, 200)}...`); + debug(`[VERSION] Shell: ${process.env.SHELL || 'Not set'}`); // if ballerina home is overridden, use ballerina cmd inside distribution // otherwise use wrapper command if (ballerinaHome) { - debug(`Ballerina Home: ${ballerinaHome}`); + debug(`[VERSION] Ballerina Home provided: ${ballerinaHome}`); + + // Check if the directory exists + try { + const fs = require('fs'); + const homeStats = fs.statSync(ballerinaHome); + if (!homeStats.isDirectory()) { + throw new Error(`Ballerina home path is not a directory: ${ballerinaHome}`); + } + debug(`[VERSION] Ballerina home directory exists and is accessible`); + } catch (error) { + debug(`[VERSION] Error accessing Ballerina home directory: ${error}`); + throw new Error(`Cannot access Ballerina home directory: ${ballerinaHome}. ${error instanceof Error ? error.message : String(error)}`); + } + } else { + debug("[VERSION] No Ballerina home provided, will use system PATH"); } + let distPath = ""; if (overrideBallerinaHome) { - distPath = join(ballerinaHome, "bin") + sep; + try { + distPath = join(ballerinaHome, "bin") + sep; + debug(`[VERSION] Using distribution path: ${distPath}`); + + // Check if bin directory exists + const fs = require('fs'); + const binPath = join(ballerinaHome, "bin"); + if (!fs.existsSync(binPath)) { + throw new Error(`Ballerina bin directory not found: ${binPath}`); + } + debug(`[VERSION] Bin directory exists: ${binPath}`); + } catch (error) { + debug(`[VERSION] Error setting up distribution path: ${error}`); + throw error; + } } + let exeExtension = ""; if (isWindows()) { exeExtension = ".bat"; + debug("[VERSION] Windows platform detected, using .bat extension"); + } else { + debug("[VERSION] Non-Windows platform detected, no extension needed"); } + const ballerinaCommand = distPath + 'bal' + exeExtension + ' version'; + debug(`[VERSION] Executing command: '${ballerinaCommand}'`); + let ballerinaExecutor = ''; return new Promise((resolve, reject) => { - exec(distPath + 'bal' + exeExtension + ' version', (err, stdout, stderr) => { + const { exec } = require('child_process'); + const execOptions = { + timeout: 30000, // 30 second timeout + maxBuffer: 1024 * 1024, // 1MB buffer + env: { ...process.env }, // Use current environment + cwd: process.cwd() // Use current working directory + }; + + debug(`[VERSION] Exec options: timeout=${execOptions.timeout}ms, maxBuffer=${execOptions.maxBuffer}, cwd=${execOptions.cwd}`); + + const startTime = Date.now(); + exec(ballerinaCommand, execOptions, (err, stdout, stderr) => { + const executionTime = Date.now() - startTime; + debug(`[VERSION] Command execution completed in ${executionTime}ms`); + if (stdout) { - debug(`bal command stdout: ${stdout}`); + debug(`[VERSION] stdout (${stdout.length} chars): ${stdout.substring(0, 500)}${stdout.length > 500 ? '...' : ''}`); } if (stderr) { - debug(`bal command _stderr: ${stderr}`); + debug(`[VERSION] stderr (${stderr.length} chars): ${stderr.substring(0, 500)}${stderr.length > 500 ? '...' : ''}`); } if (err) { - debug(`bal command err: ${err}`); - reject(err); + debug(`[VERSION] Command error: ${err}`); + debug(`[VERSION] Error code: ${err.code}`); + debug(`[VERSION] Error signal: ${err.signal}`); + debug(`[VERSION] Error killed: ${err.killed}`); + + // Provide more specific error messages for WSL environment + let errorMessage = `Failed to execute 'bal version' command: ${err.message}`; + + if (process.env.WSL_DISTRO_NAME) { + errorMessage += `\n[WSL Environment Detected: ${process.env.WSL_DISTRO_NAME}]`; + errorMessage += `\nCommon WSL issues: Path case sensitivity, Windows/Linux path mixing, file permissions`; + } + + if (err.code === 'ENOENT') { + errorMessage += `\nCommand not found. Check if Ballerina is installed and in PATH.`; + errorMessage += `\nSearched for: ${ballerinaCommand}`; + } else if (err.code === 'EACCES') { + errorMessage += `\nPermission denied. Check file permissions for: ${ballerinaCommand}`; + } + + reject(new Error(errorMessage)); return; } - if (stdout.length === 0 || stdout.startsWith(ERROR) || stdout.includes(NO_SUCH_FILE) || - stdout.includes(COMMAND_NOT_FOUND)) { - reject(stdout); + // Check for common error patterns in stdout + if (stdout.length === 0) { + debug("[VERSION] Empty stdout received"); + reject(new Error("Empty response from 'bal version' command")); + return; + } + + if (stdout.startsWith(ERROR)) { + debug(`[VERSION] Error response detected: ${stdout}`); + reject(new Error(`Ballerina command returned error: ${stdout}`)); + return; + } + + if (stdout.includes(NO_SUCH_FILE)) { + debug(`[VERSION] 'No such file' error detected`); + reject(new Error(`Ballerina executable not found. Output: ${stdout}`)); + return; + } + + if (stdout.includes(COMMAND_NOT_FOUND)) { + debug(`[VERSION] 'Command not found' error detected`); + reject(new Error(`Ballerina command not found in PATH. Output: ${stdout}`)); return; } ballerinaExecutor = 'bal'; - debug(`'bal' executor is picked up by the plugin.`); + debug(`[VERSION] 'bal' executor is picked up by the plugin.`); - this.ballerinaCmd = (distPath + ballerinaExecutor + exeExtension).trim(); try { - debug(`Ballerina version output: ${stdout}`); - const implVersionLine = stdout.split('\n')[0]; + this.ballerinaCmd = (distPath + ballerinaExecutor + exeExtension).trim(); + debug(`[VERSION] Ballerina command set to: '${this.ballerinaCmd}'`); + + debug(`[VERSION] Parsing version from output: ${stdout}`); + const lines = stdout.split('\n'); + debug(`[VERSION] Output has ${lines.length} lines`); + + if (lines.length === 0) { + throw new Error("No lines in version output"); + } + + const implVersionLine = lines[0]; + debug(`[VERSION] First line: '${implVersionLine}'`); + + if (!implVersionLine || implVersionLine.trim().length === 0) { + throw new Error("First line of version output is empty"); + } + const replacePrefix = implVersionLine.startsWith("jBallerina") ? /jBallerina / : /Ballerina /; + + debug(`[VERSION] Using prefix pattern: ${replacePrefix}`); const parsedVersion = implVersionLine.replace(replacePrefix, '').replace(/[\n\t\r]/g, ''); - return resolve(parsedVersion); - } catch (error) { - if (error instanceof Error) { - sendTelemetryException(this, error, CMP_EXTENSION_CORE); + debug(`[VERSION] Parsed version: '${parsedVersion}'`); + + if (!parsedVersion || parsedVersion.trim().length === 0) { + throw new Error(`Unable to parse version from: '${implVersionLine}'`); } - return reject(error); + + debug(`[VERSION] Successfully resolved Ballerina version: '${parsedVersion}'`); + return resolve(parsedVersion); + } catch (parseError) { + debug(`[VERSION] Error parsing version output: ${parseError}`); + reject(new Error(`Failed to parse Ballerina version from output: ${stdout}. Error: ${parseError instanceof Error ? parseError.message : String(parseError)}`)); + return; } }); }); @@ -1594,54 +1867,166 @@ export class BallerinaExtension { } autoDetectBallerinaHome(): { home: string, isOldBallerinaDist: boolean, isBallerinaNotFound: boolean } { + debug("[AUTO_DETECT] Starting Ballerina home auto-detection..."); + let balHomeOutput = "", isBallerinaNotFound = false, isOldBallerinaDist = false; + try { const args = ['home']; + debug(`[AUTO_DETECT] Executing command with args: ${JSON.stringify(args)}`); + debug(`[AUTO_DETECT] Using ballerinaCmd: '${this.ballerinaCmd}'`); + let response; + const execOptions = { + shell: false, + encoding: 'utf8' as const, + timeout: 15000, // 15 second timeout + maxBuffer: 1024 * 1024, // 1MB buffer + env: { ...process.env } + }; + if (isWindows()) { + debug("[AUTO_DETECT] Windows platform detected, using cmd.exe to run .bat files"); // On Windows, use cmd.exe to run .bat files - response = spawnSync('cmd.exe', ['/c', this.ballerinaCmd, ...args], { shell: true }); + execOptions.shell = true; + response = spawnSync('cmd.exe', ['/c', this.ballerinaCmd, ...args], execOptions); + debug(`[AUTO_DETECT] Windows command executed: cmd.exe /c ${this.ballerinaCmd} ${args.join(' ')}`); } else { + debug("[AUTO_DETECT] Non-Windows platform, using spawnSync directly"); // On other platforms, use spawnSync directly - response = spawnSync(this.ballerinaCmd, args, { shell: false }); + response = spawnSync(this.ballerinaCmd, args, execOptions); + debug(`[AUTO_DETECT] Unix command executed: ${this.ballerinaCmd} ${args.join(' ')}`); } - if (response.stdout.length > 0) { + + debug(`[AUTO_DETECT] Command execution completed`); + debug(`[AUTO_DETECT] Exit code: ${response.status}`); + debug(`[AUTO_DETECT] Error code: ${response.error?.code}`); + debug(`[AUTO_DETECT] Signal: ${response.signal}`); + debug(`[AUTO_DETECT] PID: ${response.pid}`); + + if (response.stdout && response.stdout.length > 0) { balHomeOutput = response.stdout.toString().trim(); - } else if (response.stderr.length > 0) { + debug(`[AUTO_DETECT] stdout (${balHomeOutput.length} chars): '${balHomeOutput}'`); + + // Validate the output path + if (balHomeOutput) { + try { + const fs = require('fs'); + const homeStats = fs.statSync(balHomeOutput); + if (homeStats.isDirectory()) { + debug(`[AUTO_DETECT] Detected home directory is valid: ${balHomeOutput}`); + } else { + debug(`[AUTO_DETECT] Warning: Detected path is not a directory: ${balHomeOutput}`); + } + } catch (pathError) { + debug(`[AUTO_DETECT] Warning: Cannot access detected path ${balHomeOutput}: ${pathError}`); + } + } + } else { + debug("[AUTO_DETECT] No stdout received"); + } + + if (response.stderr && response.stderr.length > 0) { let message = response.stderr.toString(); - // ballerina is installed, but ballerina home command is not found - isOldBallerinaDist = message.includes("bal: unknown command 'home'"); - // ballerina is not installed - isBallerinaNotFound = message.includes('command not found') - || message.includes('unknown command') - || message.includes('is not recognized as an internal or external command'); - log(`Error executing 'bal home'.\n<---- cmd output ---->\n${message}<---- cmd output ---->\n`); + debug(`[AUTO_DETECT] stderr (${message.length} chars): '${message}'`); + + // Check for specific error patterns + const unknownCommandPattern = "bal: unknown command 'home'"; + const commandNotFoundPatterns = [ + 'command not found', + 'unknown command', + 'is not recognized as an internal or external command' + ]; + + if (message.includes(unknownCommandPattern)) { + debug("[AUTO_DETECT] Detected old Ballerina distribution (unknown home command)"); + isOldBallerinaDist = true; + } else if (commandNotFoundPatterns.some(pattern => message.includes(pattern))) { + debug("[AUTO_DETECT] Detected Ballerina not found"); + isBallerinaNotFound = true; + } + + // Special handling for WSL environments + if (process.env.WSL_DISTRO_NAME) { + debug(`[AUTO_DETECT] WSL environment detected: ${process.env.WSL_DISTRO_NAME}`); + if (message.includes('Permission denied') || message.includes('EACCES')) { + debug("[AUTO_DETECT] WSL permission issue detected"); + message += `\n[WSL] Try running with proper permissions or check file system mount options`; + } + } + + log(`[AUTO_DETECT] Error executing 'bal home'.\n<---- cmd output ---->\n${message}<---- cmd output ---->\n`); + } else { + debug("[AUTO_DETECT] No stderr received"); + } + + // Handle command execution errors + if (response.error) { + debug(`[AUTO_DETECT] Spawn error occurred: ${response.error}`); + if (response.error.code === 'ENOENT') { + debug("[AUTO_DETECT] Command not found (ENOENT)"); + isBallerinaNotFound = true; + } else if (response.error.code === 'EACCES') { + debug("[AUTO_DETECT] Permission denied (EACCES)"); + isBallerinaNotFound = true; + } else if (response.error.code === 'ETIMEDOUT') { + debug("[AUTO_DETECT] Command timed out"); + // Don't mark as not found, might be a temporary issue + } } - // specially handle unknown ballerina command scenario for windows - if (balHomeOutput === "" && isWindows()) { + // Special handling for Windows when no output is received + if (balHomeOutput === "" && isWindows() && !response.error) { + debug("[AUTO_DETECT] Windows special case: no output received, assuming old Ballerina distribution"); isOldBallerinaDist = true; } + } catch (er) { + debug(`[AUTO_DETECT] Exception caught during execution: ${er}`); if (er instanceof Error) { - const { message } = er; - // ballerina is installed, but ballerina home command is not found - isOldBallerinaDist = message.includes("bal: unknown command 'home'"); - // ballerina is not installed - isBallerinaNotFound = message.includes('command not found') - || message.includes('unknown command') - || message.includes('is not recognized as an internal or external command'); - log(`Error executing 'bal home'.\n<---- cmd output ---->\n${message}<---- cmd output ---->\n`); + const { message, code, errno } = er as any; + debug(`[AUTO_DETECT] Exception details - message: ${message}, code: ${code}, errno: ${errno}`); + + // Check for specific error patterns in exception + const unknownCommandPattern = "bal: unknown command 'home'"; + const commandNotFoundPatterns = [ + 'command not found', + 'unknown command', + 'is not recognized as an internal or external command' + ]; + + if (message.includes(unknownCommandPattern)) { + debug("[AUTO_DETECT] Exception indicates old Ballerina distribution"); + isOldBallerinaDist = true; + } else if (commandNotFoundPatterns.some(pattern => message.includes(pattern))) { + debug("[AUTO_DETECT] Exception indicates Ballerina not found"); + isBallerinaNotFound = true; + } else if (code === 'ENOENT') { + debug("[AUTO_DETECT] Exception ENOENT - command not found"); + isBallerinaNotFound = true; + } else if (code === 'EACCES') { + debug("[AUTO_DETECT] Exception EACCES - permission denied"); + isBallerinaNotFound = true; + } + + log(`[AUTO_DETECT] Error executing 'bal home'.\n<---- cmd output ---->\n${message}<---- cmd output ---->\n`); } } - return { + const result = { home: isBallerinaNotFound || isOldBallerinaDist ? '' : balHomeOutput, isBallerinaNotFound, isOldBallerinaDist }; + + debug(`[AUTO_DETECT] Auto-detection completed:`); + debug(`[AUTO_DETECT] - home: '${result.home}'`); + debug(`[AUTO_DETECT] - isBallerinaNotFound: ${result.isBallerinaNotFound}`); + debug(`[AUTO_DETECT] - isOldBallerinaDist: ${result.isOldBallerinaDist}`); + + return result; } public overrideBallerinaHome(): boolean { @@ -1728,6 +2113,10 @@ export class BallerinaExtension { return workspace.getConfiguration().get(INCLUDE_CURRENT_ORGANIZATION_IN_SEARCH); } + public getShowAdvancedAiNodes(): boolean { + return workspace.getConfiguration().get(SHOW_ADVANCED_AI_NODES); + } + public getDocumentContext(): DocumentContext { return this.documentContext; } @@ -1802,12 +2191,21 @@ export class BallerinaExtension { * This is especially important after Ballerina installation when PATH has been updated */ private async syncEnvironment(): Promise { + debug("[SYNC_ENV] Starting environment synchronization..."); try { const freshEnv = await getShellEnvironment(); - debug('Syncing process environment with shell environment'); + debug('[SYNC_ENV] Syncing process environment with shell environment'); updateProcessEnv(freshEnv); + debug('[SYNC_ENV] Environment synchronization completed successfully'); } catch (error) { - debug(`Failed to sync environment: ${error}`); + debug(`[SYNC_ENV] Failed to sync environment: ${error}`); + // Log more details about the error + if (error instanceof Error) { + debug(`[SYNC_ENV] Error name: ${error.name}`); + debug(`[SYNC_ENV] Error message: ${error.message}`); + debug(`[SYNC_ENV] Error stack: ${error.stack}`); + } + // Don't throw the error, as this is not critical for basic functionality } } } @@ -1907,68 +2305,181 @@ export class TelemetryTracker { } function updateProcessEnv(newEnv: NodeJS.ProcessEnv): void { + debug("[UPDATE_ENV] Starting process environment update..."); + debug(`[UPDATE_ENV] Received ${Object.keys(newEnv).length} environment variables`); + + const originalPath = isWindows() ? process.env.Path : process.env.PATH; + debug(`[UPDATE_ENV] Original PATH length: ${originalPath?.length || 0} chars`); + // Update PATH/Path specially to preserve existing values that might not be in shell env if (isWindows() && newEnv.Path) { + debug(`[UPDATE_ENV] Updating Windows Path (${newEnv.Path.length} chars)`); process.env.Path = newEnv.Path; } else if (newEnv.PATH) { + debug(`[UPDATE_ENV] Updating Unix PATH (${newEnv.PATH.length} chars)`); process.env.PATH = newEnv.PATH; + } else { + debug("[UPDATE_ENV] No PATH variable found in new environment"); } // Update other environment variables + let updatedCount = 0; + let skippedCount = 0; + for (const key in newEnv) { // Skip PATH as we've already handled it, and skip some internal variables if (key !== 'PATH' && key !== 'Path' && !key.startsWith('npm_') && !key.startsWith('_')) { - process.env[key] = newEnv[key]; + const oldValue = process.env[key]; + const newValue = newEnv[key]; + + if (oldValue !== newValue) { + process.env[key] = newValue; + updatedCount++; + + // Log important variable changes + if (['HOME', 'USERPROFILE', 'WSL_DISTRO_NAME', 'JAVA_HOME'].includes(key)) { + debug(`[UPDATE_ENV] Updated ${key}: ${oldValue || '(unset)'} -> ${newValue || '(unset)'}`); + } + } + } else { + skippedCount++; } } + + const finalPath = isWindows() ? process.env.Path : process.env.PATH; + debug(`[UPDATE_ENV] Final PATH length: ${finalPath?.length || 0} chars`); + debug(`[UPDATE_ENV] Updated ${updatedCount} variables, skipped ${skippedCount} variables`); + debug("[UPDATE_ENV] Process environment update completed"); } function getShellEnvironment(): Promise { return new Promise((resolve, reject) => { + debug('[SHELL_ENV] Starting shell environment retrieval...'); + let command = ''; + const isWindowsPlatform = isWindows(); - if (isWindows()) { + if (isWindowsPlatform) { + debug('[SHELL_ENV] Windows platform detected'); // Windows: use PowerShell to get environment command = 'powershell.exe -Command "[Environment]::GetEnvironmentVariables(\'Process\') | ConvertTo-Json"'; + debug(`[SHELL_ENV] Windows command: ${command}`); + } else if (isWSL()) { + debug("[SHELL_ENV] Windows WSL platform, using non-interactive shell"); + // WSL: Use non-interactive shell to avoid job control issues + const shell = process.env.SHELL || '/bin/bash'; + if (shell.includes('zsh')) { + // For zsh in WSL, source profile and get environment without interactive mode + command = 'zsh -c "test -f ~/.zshrc && source ~/.zshrc > /dev/null 2>&1; env"'; + debug(`[SHELL_ENV] Windows zsh in WSL: ${command}`); + } else { + // For bash in WSL, source profile and get environment without interactive mode + command = 'bash -c "test -f ~/.bashrc && source ~/.bashrc > /dev/null 2>&1; env"'; + debug(`[SHELL_ENV] Windows bash in WSL: ${command}`); + } } else { + debug('[SHELL_ENV] Unix-like platform detected'); // Unix-like systems: source profile files and print environment const shell = process.env.SHELL || '/bin/bash'; + debug(`[SHELL_ENV] Detected shell: ${shell}`); + if (shell.includes('zsh')) { command = 'zsh -i -c "source ~/.zshrc > /dev/null 2>&1; env"'; + debug('[SHELL_ENV] Using zsh command'); } else { command = 'bash -i -c "source ~/.bashrc > /dev/null 2>&1; env"'; + debug('[SHELL_ENV] Using bash command'); } + debug(`[SHELL_ENV] Unix command: ${command}`); } - exec(command, (error, stdout, stderr) => { + const execOptions = { + timeout: 10000, // 10 second timeout + maxBuffer: 2 * 1024 * 1024, // 2MB buffer for environment + env: { ...process.env } + }; + + debug(`[SHELL_ENV] Exec options: timeout=${execOptions.timeout}ms, maxBuffer=${execOptions.maxBuffer}`); + + const startTime = Date.now(); + exec(command, execOptions, (error, stdout, stderr) => { + const executionTime = Date.now() - startTime; + debug(`[SHELL_ENV] Command execution completed in ${executionTime}ms`); + if (error) { - debug(`Error getting shell environment: ${error.message}`); + debug(`[SHELL_ENV] Error getting shell environment: ${error.message}`); + debug(`[SHELL_ENV] Error code: ${(error as any).code}`); + debug(`[SHELL_ENV] Error signal: ${(error as any).signal}`); + + // Provide WSL-specific debugging information + if (process.env.WSL_DISTRO_NAME) { + debug(`[SHELL_ENV] WSL environment detected: ${process.env.WSL_DISTRO_NAME}`); + debug(`[SHELL_ENV] WSL may have issues with shell initialization`); + } + return reject(error); } + if (stderr && stderr.trim().length > 0) { + debug(`[SHELL_ENV] Warning - stderr output (${stderr.length} chars): ${stderr.substring(0, 200)}${stderr.length > 200 ? '...' : ''}`); + } + const env = { ...process.env }; // Start with current env + debug(`[SHELL_ENV] Starting with ${Object.keys(env).length} existing environment variables`); try { - if (isWindows()) { + if (isWindowsPlatform) { + debug('[SHELL_ENV] Parsing Windows PowerShell JSON output...'); // Parse PowerShell JSON output const envVars = JSON.parse(stdout); + const parsedVarCount = Object.keys(envVars).length; + debug(`[SHELL_ENV] Parsed ${parsedVarCount} environment variables from PowerShell`); + Object.keys(envVars).forEach(key => { - env[key] = envVars[key].toString(); + const value = envVars[key].toString(); + env[key] = value; + + // Log important environment variables + if (['PATH', 'Path', 'HOME', 'USERPROFILE', 'WSL_DISTRO_NAME'].includes(key)) { + debug(`[SHELL_ENV] Important var ${key}: ${value.length > 100 ? value.substring(0, 100) + '...' : value}`); + } }); } else { + debug('[SHELL_ENV] Parsing Unix env output...'); // Parse Unix env output (KEY=value format) - stdout.split('\n').forEach(line => { + const lines = stdout.split('\n'); + let parsedCount = 0; + + lines.forEach(line => { const match = line.match(/^([^=]+)=(.*)$/); if (match) { - env[match[1]] = match[2]; + const [, key, value] = match; + env[key] = value; + parsedCount++; + + // Log important environment variables + if (['PATH', 'HOME', 'SHELL', 'WSL_DISTRO_NAME'].includes(key)) { + debug(`[SHELL_ENV] Important var ${key}: ${value.length > 100 ? value.substring(0, 100) + '...' : value}`); + } } }); + + debug(`[SHELL_ENV] Parsed ${parsedCount} environment variables from shell`); } - debug('Successfully retrieved fresh environment variables'); + debug(`[SHELL_ENV] Final environment contains ${Object.keys(env).length} variables`); + debug('[SHELL_ENV] Successfully retrieved fresh environment variables'); resolve(env); } catch (parseError) { - debug(`Error parsing environment output: ${parseError}`); + debug(`[SHELL_ENV] Error parsing environment output: ${parseError}`); + debug(`[SHELL_ENV] Raw output length: ${stdout.length} chars`); + debug(`[SHELL_ENV] Output sample: ${stdout.substring(0, 500)}${stdout.length > 500 ? '...' : ''}`); + + if (parseError instanceof Error) { + debug(`[SHELL_ENV] Parse error name: ${parseError.name}`); + debug(`[SHELL_ENV] Parse error message: ${parseError.message}`); + } + reject(parseError); } }); diff --git a/workspaces/ballerina/ballerina-extension/src/core/preferences.ts b/workspaces/ballerina/ballerina-extension/src/core/preferences.ts index b8e62863575..b04986c3658 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/preferences.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/preferences.ts @@ -24,6 +24,8 @@ export const ENABLE_SEMANTIC_HIGHLIGHTING = "ballerina.enableSemanticHighlightin export const ENABLE_BACKGROUND_DRIFT_CHECK = "ballerina.enableBackgroundDriftCheck"; export const ENABLE_PERFORMANCE_FORECAST = "ballerina.enablePerformanceForecast"; export const ENABLE_DEBUG_LOG = "ballerina.debugLog"; +export const ENABLE_TRACE_LOG = "ballerina.traceLog"; +export const TRACE_SERVER = "ballerina-vscode.trace.server"; export const ENABLE_LIVE_RELOAD = "ballerina.enableLiveReload"; export const ENABLE_BALLERINA_LS_DEBUG = "ballerina.enableLanguageServerDebug"; export const ENABLE_EXPERIMENTAL_FEATURES = "ballerina.experimental"; @@ -39,3 +41,4 @@ export const SHOW_LIBRARY_CONFIG_VARIABLES = "ballerina.showLibraryConfigVariabl export const LANG_SERVER_PATH = "ballerina.langServerPath"; // this setting is not visible to the extension user export const USE_BALLERINA_CLI_LANG_SERVER = "ballerina.useDistributionLanguageServer"; export const INCLUDE_CURRENT_ORGANIZATION_IN_SEARCH = "ballerina.includeCurrentOrganizationInSearch"; +export const SHOW_ADVANCED_AI_NODES = "ballerina.showAdvancedAiNodes"; diff --git a/workspaces/ballerina/ballerina-extension/src/extension.ts b/workspaces/ballerina/ballerina-extension/src/extension.ts index 51411b44a53..f172e4cc194 100644 --- a/workspaces/ballerina/ballerina-extension/src/extension.ts +++ b/workspaces/ballerina/ballerina-extension/src/extension.ts @@ -16,7 +16,7 @@ * under the License. */ -import { ExtensionContext, commands, window, Location, Uri, TextEditor, extensions } from 'vscode'; +import { ExtensionContext, commands, window, Location, Uri, TextEditor, extensions, workspace } from 'vscode'; import { BallerinaExtension } from './core'; import { activate as activateBBE } from './views/bbe'; import { @@ -119,13 +119,24 @@ export async function activateBallerina(): Promise { const ballerinaExtInstance = new BallerinaExtension(); extension.ballerinaExtInstance = ballerinaExtInstance; debug('Active the Ballerina VS Code extension.'); - sendTelemetryEvent(ballerinaExtInstance, TM_EVENT_EXTENSION_ACTIVATE, CMP_EXTENSION_CORE); + try { + debug('Sending telemetry event.'); + sendTelemetryEvent(ballerinaExtInstance, TM_EVENT_EXTENSION_ACTIVATE, CMP_EXTENSION_CORE); + } catch (error) { + debug('Error sending telemetry event.'); + } + debug('Setting context.'); ballerinaExtInstance.setContext(extension.context); + await updateCodeServerConfig(); // Enable URI handlers + debug('Activating URI handlers.'); activateUriHandlers(ballerinaExtInstance); // Activate Subscription Commands + debug('Activating subscription commands.'); activateSubscriptions(); + debug('Starting ballerina extension initialization.'); await ballerinaExtInstance.init(onBeforeInit).then(() => { + debug('Ballerina extension activated successfully.'); // <------------ CORE FUNCTIONS -----------> // Activate Library Browser activateLibraryBrowser(ballerinaExtInstance); @@ -187,6 +198,7 @@ export async function activateBallerina(): Promise { }); isPluginStartup = false; }).catch((e) => { + debug('Failed to activate Ballerina extension.'); log("Failed to activate Ballerina extension. " + (e.message ? e.message : e)); const cmds: any[] = ballerinaExtInstance.extension.packageJSON.contributes.commands; @@ -239,6 +251,15 @@ export async function activateBallerina(): Promise { return ballerinaExtInstance; } +async function updateCodeServerConfig() { + if (!('CLOUD_STS_TOKEN' in process.env)) { + return; + } + log("Code server environment detected") + const config = workspace.getConfiguration('ballerina'); + await config.update('enableRunFast', true); +} + export function deactivate(): Thenable | undefined { debug('Deactive the Ballerina VS Code extension.'); if (!langClient) { diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/completions.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/completions.ts index d143bc84852..79595fa7fb0 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/completions.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/completions.ts @@ -23,7 +23,7 @@ import { sendTelemetryException } from "./../telemetry"; import { PALETTE_COMMANDS } from "./../project/cmds/cmd-runner"; -import { loginGithubCopilot } from '../../utils/ai/auth'; +import { clearAuthCredentials, loginGithubCopilot } from '../../utils/ai/auth'; import { RPCLayer } from "../../RPCLayer"; // import { VisualizerWebview } from "../../views/visualizer/webview"; import { AiPanelWebview } from "../../views/ai-panel/webview"; @@ -47,8 +47,6 @@ export function activateCopilotLoginCommand() { export function resetBIAuth() { commands.registerCommand(PALETTE_COMMANDS.RESET_BI, async () => { - await extension.ballerinaExtInstance.context.secrets.delete('GITHUB_TOKEN'); - await extension.ballerinaExtInstance.context.secrets.delete('GITHUB_COPILOT_TOKEN'); - await extension.ballerinaExtInstance.context.secrets.delete('LOGIN_ALERT_SHOWN'); + await clearAuthCredentials(); }); } diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/dataMapping.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/dataMapping.ts index df465995ab5..86c291ac26d 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/dataMapping.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/dataMapping.ts @@ -16,34 +16,17 @@ * under the License. */ -import { Attachment, createFunctionSignature, DataMappingRecord, ErrorCode, GenerateMappingFromRecordResponse, GenerateMappingsFromRecordRequest, GenerateTypesFromRecordRequest, GenerateTypesFromRecordResponse, getSource, PartialST, ProjectSource, SyntaxTree } from "@wso2/ballerina-core"; -import { FunctionDefinition, ModulePart, RequiredParam, STKindChecker } from "@wso2/syntax-tree"; -import { camelCase, memoize } from "lodash"; +import { createFunctionSignature, DataMappingRecord, GenerateTypesFromRecordRequest, GenerateTypesFromRecordResponse, getSource, ImportInfo } from "@wso2/ballerina-core"; +import { camelCase } from "lodash"; import path from "path"; import * as fs from 'fs'; import * as os from 'os'; -import { Uri, workspace } from "vscode"; -import { langClient } from "./activator"; -import { getFunction, processMappings, typesFileParameterDefinitions } from "../../rpc-managers/ai-panel/utils"; -import { MODIFIYING_ERROR } from "../../views/ai-panel/errorCodes"; -import { writeBallerinaFileDidOpen } from "../../utils/modification"; - - -export async function generateDataMapping( - projectRoot: string, - request: GenerateMappingsFromRecordRequest -): Promise { - const source = createDataMappingFunctionSource(request.inputRecordTypes, request.outputRecordType, request.functionName, - request.inputNames); - const file = request.attachment && request.attachment.length > 0 - ? request.attachment[0] - : undefined; - const updatedSource = await getUpdatedFunctionSource(projectRoot, source, request.imports, file); - return Promise.resolve({mappingCode: updatedSource}); -} +import { typesFileParameterDefinitions } from "../../rpc-managers/ai-panel/utils"; +import { writeBallerinaFileDidOpenTemp } from "../../utils/modification"; +import { PrimitiveType } from "../../../src/rpc-managers/ai-panel/constants"; +// Generate Ballerina types from a record request export async function generateTypeCreation( - projectRoot: string, request: GenerateTypesFromRecordRequest ): Promise { const file = request.attachment && request.attachment.length > 0 @@ -58,110 +41,128 @@ export async function generateTypeCreation( return Promise.resolve({ typesCode: updatedSource }); } -async function getUpdatedFunctionSource( - projectRoot: string, - funcSource: string, - imports: { moduleName: string; alias?: string }[], - file?: Attachment +// Create a temporary Ballerina file with a generated data mapping function +export async function createTempDataMappingFile( + projectRoot: string, + inputs: DataMappingRecord[], + output: DataMappingRecord, + functionName: string, + inputNames: string[], + imports: ImportInfo[] +): Promise { + const funcSource = createDataMappingFunctionSource(inputs, output, functionName, inputNames); + const tempFilePath = await createTempBallerinaFile(projectRoot, funcSource, imports); + return tempFilePath; +} + +// Create a temporary Ballerina file with optional imports +async function createTempBallerinaFile( + projectRoot: string, + funcSource: string, + imports?: ImportInfo[] ): Promise { + let fullSource = funcSource; + + if (imports && imports.length > 0) { const importsString = imports .map(({ moduleName, alias }) => alias ? `import ${moduleName} as ${alias};` : `import ${moduleName};` ) .join("\n"); - const fullSource = `${importsString}\n\n${funcSource}`; - - const tempDir = fs.mkdtempSync( - path.join(os.tmpdir(), "ballerina-data-mapping-temp-") - ); - fs.cpSync(projectRoot, tempDir, { recursive: true }); - const tempTestFilePath = path.join(tempDir, "temp.bal"); - writeBallerinaFileDidOpen(tempTestFilePath, fullSource); - - const fileUri = Uri.file(tempTestFilePath).toString(); - const st = (await langClient.getSyntaxTree({ - documentIdentifier: { - uri: fileUri, - }, - })) as SyntaxTree; - - let funcDefinitionNode: FunctionDefinition = null; - const modulePart = st.syntaxTree as ModulePart; - - modulePart.members.forEach((member) => { - if (STKindChecker.isFunctionDefinition(member)) { - funcDefinitionNode = member; - } - }); - - if (!funcDefinitionNode) { - throw new Error("Function definition not found in syntax tree"); - } - - const processedST = await processMappings( - funcDefinitionNode, - fileUri, - file - ); - - const { parseSuccess, source, syntaxTree } = processedST as SyntaxTree; - if (!parseSuccess) { - throw new Error("No valid fields were identified for mapping between the given input and output records."); - } - - const fn = await getFunction( - syntaxTree as ModulePart, - funcDefinitionNode.functionName.value - ); - - return fn.source; -} - -function createDataMappingFunctionSource(inputParams: DataMappingRecord[], outputParam: DataMappingRecord, functionName: string, inputNames:string[]): string { - const finalFunctionName = functionName || "transform"; - - function processType(type: string): string { - let typeName = type.includes('/') ? type.split('/').pop()! : type; - if (typeName.includes(':')) { - const [modulePart, typePart] = typeName.split(':'); - typeName = `${modulePart.split('.').pop()}:${typePart}`; - } - return typeName; + fullSource = `${importsString}\n\n${funcSource}`; } - function getDefaultParamName(type: string, isArray: boolean): string { - const processedType = processType(type); - - // Handle primitive types with special naming - if (processedType === 'string') { return isArray ? 'strArr' : 'str'; } - if (processedType === 'int') { return isArray ? 'numArr' : 'num'; } - if (processedType === 'float') { return isArray ? 'fltArr' : 'flt'; } - if (processedType === 'decimal') { return isArray ? 'decArr' : 'dec'; } - if (processedType === 'boolean') { return isArray ? 'flagArr' : 'flag'; } - - // Default to camelCase for non-primitive types - return camelCase(processedType); - } + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), "ballerina-data-mapping-temp-") + ); + fs.cpSync(projectRoot, tempDir, { recursive: true }); + const tempTestFilePath = path.join(tempDir, "temp.bal"); + writeBallerinaFileDidOpenTemp(tempTestFilePath, fullSource); - const parametersStr = inputParams - .map((item, index) => { - const paramName = inputNames[index] || getDefaultParamName(item.type, item.isArray); - return `${processType(item.type)}${item.isArray ? '[]' : ''} ${paramName}`; - }) - .join(", "); + return tempTestFilePath; +} - const returnTypeStr = `returns ${processType(outputParam.type)}${outputParam.isArray ? '[]' : ''}`; +// Generate the Ballerina source for a data mapping function +export function createDataMappingFunctionSource( + inputParams: DataMappingRecord[], + outputParam: DataMappingRecord, + functionName: string, + inputNames: string[] +): string { + const parametersStr = buildParametersString(inputParams, inputNames); + const returnTypeStr = buildReturnTypeString(outputParam); const modification = createFunctionSignature( "", - finalFunctionName, + functionName, parametersStr, returnTypeStr, { startLine: 0, startColumn: 0 }, false, true, - '{}' + "{}" ); - const source = getSource(modification); - return source; + + return getSource(modification); +} + +// Generate parameters string for function signature +function buildParametersString( + inputParams: DataMappingRecord[], + inputNames: string[] +): string { + return inputParams + .map((item, index) => { + const paramName = + inputNames[index] || getDefaultParamName(item.type, item.isArray); + return formatParameter(item, paramName); + }) + .join(", "); +} + +// Generate a default parameter name for primitives and custom types +function getDefaultParamName(type: string, isArray: boolean): string { + const processedType = processType(type); + + switch (processedType) { + case PrimitiveType.STRING: + return isArray ? "strArr" : "str"; + case PrimitiveType.INT: + return isArray ? "numArr" : "num"; + case PrimitiveType.FLOAT: + return isArray ? "fltArr" : "flt"; + case PrimitiveType.DECIMAL: + return isArray ? "decArr" : "dec"; + case PrimitiveType.BOOLEAN: + return isArray ? "flagArr" : "flag"; + default: + return camelCase(processedType); + } +} + +// Extract the actual type name from a fully qualified type +function processType(type: string): string { + let typeName = type.includes("/") ? type.split("/").pop()! : type; + + if (typeName.includes(":")) { + const [modulePart, typePart] = typeName.split(":"); + typeName = `${modulePart.split(".").pop()}:${typePart}`; + } + + return typeName; +} + +// Format a single function parameter +function formatParameter( + item: DataMappingRecord, + paramName: string +): string { + return `${processType(item.type)}${item.isArray ? "[]" : ""} ${paramName}`; +} + +// Generate return type string +function buildReturnTypeString(outputParam: DataMappingRecord): string { + return `returns ${processType(outputParam.type)}${ + outputParam.isArray ? "[]" : "" + }`; } diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/code/code.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/code/code.ts index 73d399641a0..ce678145d92 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/code/code.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/code/code.ts @@ -15,7 +15,7 @@ // under the License. import { CoreMessage, generateText, streamText } from "ai"; -import { getAnthropicClient, ANTHROPIC_SONNET_4 } from "../connection"; +import { getAnthropicClient, ANTHROPIC_SONNET_4, getProviderCacheControl } from "../connection"; import { GenerationType, getRelevantLibrariesAndFunctions } from "../libs/libs"; import { getRewrittenPrompt, populateHistory, transformProjectSource, getErrorMessage, extractResourceDocumentContent } from "../utils"; import { getMaximizedSelectedLibs, selectRequiredFunctions, toMaximizedLibrariesFromLibJson } from "./../libs/funcs"; @@ -53,6 +53,7 @@ export async function generateCodeCore(params: GenerateCodeRequest, eventHandler ).libraries; const historyMessages = populateHistory(params.chatHistory); + const cacheOptions = await getProviderCacheControl(); const allMessages: CoreMessage[] = [ { role: "system", @@ -61,17 +62,13 @@ export async function generateCodeCore(params: GenerateCodeRequest, eventHandler { role: "system", content: getSystemPromptSuffix(LANGLIBS), - providerOptions: { - anthropic: { cacheControl: { type: "ephemeral" } }, - }, + providerOptions: cacheOptions, }, ...historyMessages, { role: "user", content: getUserPrompt(prompt, sourceFiles, params.fileAttachmentContents, packageName, params.operationType), - providerOptions: { - anthropic: { cacheControl: { type: "ephemeral" } }, - }, + providerOptions: cacheOptions, }, ]; 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 01553b83af6..ea1f71410a9 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/connection.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/connection.ts @@ -15,19 +15,42 @@ // under the License. import { createAnthropic } from "@ai-sdk/anthropic"; -import { getAccessToken, getLoginMethod, getRefreshedAccessToken } from "../../../utils/ai/auth"; +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; +import { getAccessToken, getLoginMethod, getRefreshedAccessToken, getAwsBedrockCredentials } from "../../../utils/ai/auth"; import { AIStateMachine } from "../../../views/ai-panel/aiMachine"; import { BACKEND_URL } from "../utils"; import { AIMachineEventType, LoginMethod } from "@wso2/ballerina-core"; export const ANTHROPIC_HAIKU = "claude-3-5-haiku-20241022"; export const ANTHROPIC_SONNET_4 = "claude-sonnet-4-20250514"; -export const ANTHROPIC_SONNET_3_5 = "claude-3-5-sonnet-20241022"; type AnthropicModel = | typeof ANTHROPIC_HAIKU - | typeof ANTHROPIC_SONNET_4 - | typeof ANTHROPIC_SONNET_3_5; + | typeof ANTHROPIC_SONNET_4; + +/** + * Maps AWS regions to their corresponding Bedrock inference profile prefixes + * @param region - AWS region string (e.g., 'us-east-1', 'eu-west-1', 'ap-southeast-1') + * @returns The appropriate regional prefix for Bedrock model IDs + */ +export function getBedrockRegionalPrefix(region: string): string { + const regionPrefix = region.split('-')[0].toLowerCase(); + + switch (regionPrefix) { + case 'us': + return region.startsWith('us-gov-') ? 'us-gov' : 'us'; + case 'eu': + return 'eu'; + case 'ap': + return 'apac'; + case 'ca': + case 'sa': + return 'us'; // Canada and South America regions use US prefix + default: + console.warn(`Unknown region prefix: ${regionPrefix}, defaulting to 'us'`); + return 'us'; + } +} let cachedAnthropic: ReturnType | null = null; let cachedAuthMethod: LoginMethod | null = null; @@ -82,7 +105,7 @@ export async function fetchWithAuth(input: string | URL | Request, options: Requ * Returns a singleton Anthropic client instance. * Re-initializes the client if the login method has changed. */ -export const getAnthropicClient = async (model: AnthropicModel) => { +export const getAnthropicClient = async (model: AnthropicModel): Promise => { const loginMethod = await getLoginMethod(); // Recreate client if login method has changed or no cached instance @@ -99,6 +122,35 @@ export const getAnthropicClient = async (model: AnthropicModel) => { baseURL: "https://api.anthropic.com/v1", apiKey: apiKey, }); + } else if (loginMethod === LoginMethod.AWS_BEDROCK) { + const awsCredentials = await getAwsBedrockCredentials(); + if (!awsCredentials) { + throw new Error('AWS Bedrock credentials not found'); + } + + const bedrock = createAmazonBedrock({ + region: awsCredentials.region, + accessKeyId: awsCredentials.accessKeyId, + secretAccessKey: awsCredentials.secretAccessKey, + sessionToken: awsCredentials.sessionToken, + }); + + // Map Anthropic model names to AWS Bedrock model IDs (base models without region prefix) + const baseModelMap: Record = { + [ANTHROPIC_HAIKU]: "anthropic.claude-3-5-haiku-20241022-v1:0", + [ANTHROPIC_SONNET_4]: "anthropic.claude-sonnet-4-20250514-v1:0", + }; + + const baseModelId = baseModelMap[model]; + if (!baseModelId) { + throw new Error(`Unsupported model for AWS Bedrock: ${model}`); + } + + // Get regional prefix based on AWS region + const regionalPrefix = getBedrockRegionalPrefix(awsCredentials.region); + const bedrockModelId = `${regionalPrefix}.${baseModelId}`; + + return bedrock(bedrockModelId); } else { throw new Error(`Unsupported login method: ${loginMethod}`); } @@ -106,5 +158,23 @@ export const getAnthropicClient = async (model: AnthropicModel) => { cachedAuthMethod = loginMethod; } - return cachedAnthropic(model); + // For AWS Bedrock, we return directly above, so this is for other methods + return cachedAnthropic!(model); +}; + +/** + * Returns provider-aware cache control options for prompt caching + * @returns Cache control options based on the current login method + */ +export const getProviderCacheControl = async () => { + const loginMethod = await getLoginMethod(); + + switch (loginMethod) { + case LoginMethod.AWS_BEDROCK: + return { bedrock: { cachePoint: { type: 'default' } } }; + case LoginMethod.ANTHROPIC_KEY: + case LoginMethod.BI_INTEL: + default: + return { anthropic: { cacheControl: { type: "ephemeral" } } }; + } }; diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/datamapper.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/datamapper.ts index 5d4e01fb7fc..3c55fe1c317 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/datamapper.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/datamapper.ts @@ -14,30 +14,30 @@ // specific language governing permissions and limitations // under the License. -import { generateText, CoreMessage, generateObject } from "ai"; -import { getDataMappingPrompt } from "./prompt"; +import { CoreMessage, generateObject } from "ai"; import { getAnthropicClient, ANTHROPIC_SONNET_4 } from "../connection"; import { - Payload, DatamapperResponse, AIDataMappings, MappingJson, MappingRecord, - Inputs, MappingOperation, FieldMetadata, - MetadataField, - MetadataType, - Metadata, ParameterMetadata, MappingFields, + Mapping, Operation, - Structure, - ChatResponse, + DataMappingRequest, + DataMappingResponse, + IOTypeVisitor, + VisitorContext, + Payload } from "./types"; -import { MappingSchema } from "./schema"; +import { MappingSchema } from "./schema"; import { AIPanelAbortController } from "../../../../../src/rpc-managers/ai-panel/utils"; -import { ADDITION, DIRECT, DIVISION, LENGTH, MODULAR, MULTIPLICATION, NAME, PARAMETER_1, PARAMETER_2, SPLIT, SUBTRACTION } from "./constant"; +import { ADDITION, DIRECT, DIVISION, LENGTH, MODULAR, MULTIPLICATION, NAME, PARAMETER_1, SPLIT, SUBTRACTION } from "./constant"; +import { ExpandedDMModel, DataMapperModelResponse, IOType } from "@wso2/ballerina-core"; +import { getDataMappingPrompt } from "./prompt"; // Operations table - In a real implementation, this would be loaded from JSON files const operationsTable: Map = new Map([ @@ -156,427 +156,532 @@ const operationsTable: Map = new Map([ ]); // ============================================================================= -// MAIN ORCHESTRATOR FUNCTION +// UTILITY FUNCTIONS FOR SCHEMA PROCESSING // ============================================================================= /** - * Main function for AI-powered data mapping generation - * Coordinates the entire data mapping workflow with retry logic and error handling + * Finds a schema type by path in the input schemas array */ -async function mapData(payload: Payload): Promise { - const maxRetries = 6; - let retries = 0; - while (retries < maxRetries) { - if (retries > 1) { - console.debug("Retrying to generate mappings for the payload."); +class IOTypeVisitorImpl implements IOTypeVisitor { + visitIOType(ioType: IOType, context: VisitorContext): IOType | null { + if (ioType.id === context.targetPath) { + context.found = ioType; + return ioType; } - try { - // Extract existing mapping field hints - const mappingFields: { [key: string]: MappingFields } = payload.mapping_fields || {}; + // Visit fields + if (ioType.fields && Array.isArray(ioType.fields)) { + for (const field of ioType.fields) { + const result = this.visitIOType(field, context); + if (result) { return result; } + } + } - // STEP 1: Generate AI-powered mappings using Claude - const generatedMappings = await getAutoMappings(payload.inputs, payload.output, mappingFields); + // Visit members (for enums, unions) + if (ioType.members && Array.isArray(ioType.members)) { + for (const member of ioType.members) { + const result = this.visitIOType(member, context); + if (result) { return result; } + } + } - // STEP 2: Prepare metadata for validation - const input: Inputs = { - input: payload.inputMetadata, - output: payload.outputMetadata, - }; + // Visit member + if (ioType.member) { + const result = this.visitIOType(ioType.member, context); + if (result) { return result; } + } - // STEP 3: Validate and process AI-generated mappings - const evaluateMappingsResult = await evaluateMappings([], generatedMappings, operationsTable, input); + return null; + } +} - if (evaluateMappingsResult) { - // STEP 4: Extract and structure the validated mappings - const mappings = extractMappings(evaluateMappingsResult); - return mappings; - } else { - throw new Error("Failed to generate mappings for the payload."); - } - } catch (error) { - console.error(`Error occurred while generating mappings: ${error}`); - retries += 1; - continue; - } +function findSchemaTypeByPath(inputs: IOType[], path: string): IOType | null { + const context: VisitorContext = { + targetPath: path, + found: null + }; + + const visitor = new IOTypeVisitorImpl(); + + for (const inputSchema of inputs) { + const result = visitor.visitIOType(inputSchema, context); + if (result) { return result; } } - throw new Error("Failed to generate mappings for the payload after all retries."); + return null; } -// ============================================================================= -// MAPPING EXTRACTION FUNCTION -// ============================================================================= - /** - * Recursive mapping extraction and structuring function - * Processes AI-generated and validated mappings into clean, hierarchical structure + * Removes array indices from path to normalize field paths */ -function extractMappings(evaluateMappingsResult: MappingJson): DatamapperResponse { - const mappings: { [key: string]: MappingJson } = {}; +function removeArrayIndicesFromPath(path: string): string { + const pathParts = path.split('.'); + const cleanParts: string[] = []; - // Guard clause: Ensure we have map-type data (not single mapping record) - if (isMappingRecord(evaluateMappingsResult)) { - throw new Error("EvaluateMappingsResult is a MappingRecord, expected map structure."); - } - - // Process nested mapping structure - if (typeof evaluateMappingsResult === "object" && evaluateMappingsResult !== null) { - for (const [key, value] of Object.entries(evaluateMappingsResult)) { - if (isMappingRecord(value)) { - // Direct mapping record - add to results - mappings[key] = value; - } else if (typeof value === "object" && value !== null) { - // Nested mapping structure - recursively process - const nestedMappingsResult = extractMappings(value as MappingJson); - mappings[key] = nestedMappingsResult.mappings; - } + for (const part of pathParts) { + // Skip numeric indices + if (!isNaN(parseInt(part))) { + continue; } + cleanParts.push(part); } - - return { mappings }; + + return cleanParts.join('.'); } -// ============================================================================= -// AI-POWERED DATA MAPPING GENERATION -// ============================================================================= - /** - * Generates intelligent data transformation mappings by analyzing input and output schemas + * Removes output record prefix from field name */ -async function getAutoMappings( - inputJsonRecord: { [key: string]: any }, - outputJsonRecord: { [key: string]: any }, - mappingFields: { [key: string]: MappingFields } -): Promise { - // STEP 1: Construct AI prompt with schema information - const prompt = getDataMappingPrompt( - JSON.stringify(inputJsonRecord), - JSON.stringify(outputJsonRecord), - JSON.stringify(mappingFields) - ); - - // STEP 3: Call Claude API using AI SDK - const messages: CoreMessage[] = [ - { role: "user", content: prompt } - ]; - - try { - const { object } = await generateObject({ - model: await getAnthropicClient(ANTHROPIC_SONNET_4), - maxTokens: 4096, - temperature: 0, - messages: messages, - schema: MappingSchema, - abortSignal: AIPanelAbortController.getInstance().signal, - }); - - const generatedMappings = object.generatedMappings as AIDataMappings; - return generatedMappings; - } catch (error) { - console.error("Failed to parse response:", error); - throw new Error(`Failed to parse mapping response: ${error}`); +function removeOutputRecordPrefix(fieldName: string, output: IOType): string { + const outputRecordName = output.name; + if (outputRecordName) { + const prefix = outputRecordName + "."; + if (fieldName.startsWith(prefix)) { + return fieldName.substring(prefix.length); + } } + return fieldName; } // ============================================================================= -// MAPPING VALIDATION AND PROCESSING ENGINE +// MAPPING EVALUATION FUNCTIONS // ============================================================================= /** - * Recursively validates and processes AI-generated mappings against supported operations + * Main function to evaluate mappings with enhanced schema support */ async function evaluateMappings( - path: string[], - input: AIDataMappings, - operations: Map, - initialRecords: Inputs + path: string[], + llmGeneratedMappings: AIDataMappings, + initialRecords: DataMappingRequest ): Promise { - const returnRec: { [key: string]: MappingJson } = {}; + if (isMapping(llmGeneratedMappings)) { + return await processMappingOperation(path, llmGeneratedMappings, initialRecords); + } else { + return await processNestedMappings(path, llmGeneratedMappings as { [key: string]: AIDataMappings }, initialRecords); + } +} - if (isMapping(input)) { - // STEP 1: Extract operation record from AI-generated mapping - const operationRecord = input.OPERATION; +/** + * Processes individual mapping operations with schema validation + */ +async function processMappingOperation( + path: string[], + llmGeneratedMappings: Mapping, + initialRecords: DataMappingRequest +): Promise { + try { + const operationRecord = llmGeneratedMappings.OPERATION; const parametersTypes: { [key: string]: ParameterMetadata } = {}; let validParameters = false; - // STEP 2: Process and validate each parameter in the operation for (const subKey of Object.keys(operationRecord)) { - if (subKey !== NAME) { - const subPathString = operationRecord[subKey as keyof MappingOperation] as string; - if (!subPathString) { - continue; - } - - // Extract operation details and input path - const operationName = operationRecord.NAME; - const paths = subPathString.split("."); - if (paths.length <= 1) { - continue; - } - - // STEP 3: Validate input record instance exists - const recordInstance = paths.shift()!; - if (!initialRecords.input[recordInstance]) { - continue; - } - - // STEP 4: Extract and validate field type metadata - const inputFields = initialRecords.input[recordInstance]; - const inputType = getTypeMetadataOfField(inputFields, [...paths].reverse(), subKey, operationName); - if (!inputType) { - continue; - } - - // STEP 5: Store validated parameter metadata - parametersTypes[subKey] = { - type: inputType.type, - input: subPathString, - optional: inputType.optional, - nullable: inputType.nullable, - }; - validParameters = true; + if (subKey === NAME) { + continue; + } + + const subPathString = operationRecord[subKey as keyof MappingOperation] as string; + if (!subPathString) { + continue; + } + + const operationName = operationRecord.NAME; + const inputType = findSchemaTypeByPath(initialRecords.input, subPathString); + if (!inputType) { + continue; } + + const fieldMetadata = getFieldMetadataFromSchemaType(inputType, subKey, operationName); + if (!fieldMetadata) { + continue; + } + + parametersTypes[subKey] = { + type: fieldMetadata.type, + input: subPathString, + optional: fieldMetadata.optional, + nullable: fieldMetadata.nullable + }; + validParameters = true; } - // STEP 6: Validate operation if parameters are valid if (validParameters) { - const outputFields = initialRecords.output; - const outputType = getTypeMetadataOfField(outputFields, [...path].reverse()); + const outputType = getOutputFieldMetadata(initialRecords.output, path); if (!outputType) { return null; } - - // STEP 7: Perform comprehensive operation validation + const mapping = validateMappingOperation( - operationRecord, - operations, - parametersTypes, - outputType, - path[path.length - 1], + operationRecord, + parametersTypes, + outputType, + path[path.length - 1], initialRecords.input ); return mapping; } return null; - } else if (typeof input === "object" && input !== null) { - // STEP 8: Recursively process nested mapping structures - for (const [key, value] of Object.entries(input)) { - if (value === null || value === undefined) { - continue; - } + } catch (error) { + console.error('Error processing mapping operation:', error); + return null; + } +} - // Process nested mapping with extended path - const newPath = [...path, key]; - const temporaryRecord = await evaluateMappings( - newPath, - value as AIDataMappings, - operations, - initialRecords - ); +/** + * Processes nested mapping structures + */ +async function processNestedMappings( + path: string[], + llmGeneratedMappings: { [key: string]: AIDataMappings }, + initialRecords: DataMappingRequest +): Promise { + const returnRec: { [key: string]: MappingJson } = {}; - if (temporaryRecord) { - if ( - isMappingRecord(temporaryRecord) || - (typeof temporaryRecord === "object" && Object.keys(temporaryRecord).length > 0) - ) { - returnRec[key] = temporaryRecord; - } + for (const [key, value] of Object.entries(llmGeneratedMappings)) { + if (value === null || value === undefined) { + continue; + } + + const newPath = [...path]; + let cleanKey = removeOutputRecordPrefix(key, initialRecords.output); + newPath.push(key); + + const temporaryRecord = await evaluateMappings(newPath, value, initialRecords); + if (temporaryRecord) { + if (isMappingRecord(temporaryRecord) || + (typeof temporaryRecord === 'object' && Object.keys(temporaryRecord).length > 0)) { + returnRec[removeArrayIndicesFromPath(cleanKey)] = temporaryRecord; } } - return returnRec; - } else { - throw new Error("Invalid input type"); } + + return convertFlatToNestedMap(returnRec); } // ============================================================================= -// OPERATION-SPECIFIC VALIDATION ENGINE +// MAPPING OPERATION VALIDATION // ============================================================================= /** - * Validates individual mapping operations against their specific requirements + * Validates mapping operations with enhanced error handling */ function validateMappingOperation( mapping: MappingOperation, - operations: Map, inputType: { [key: string]: ParameterMetadata }, outputType: FieldMetadata, name: string, - inputs: { [key: string]: Metadata } + inputs: IOType[] ): MappingJson | null { const operation = mapping.NAME; - // STEP 1: Verify operation exists in operations database - const op = operations.get(operation); + const op = operationsTable.get(operation); + if (!op) { return null; } - // STEP 2: Validate DIRECT mapping operation - if (op.name === DIRECT) { - const paramOne = inputType[PARAMETER_1]; - if (!paramOne) { - return null; - } - - const paths = paramOne.input.split("."); - if (paths.length === 0) { - throw new Error("Invalid path in input type for DIRECT operation"); - } - - const recordInstance = paths[0]; - if (!inputs[recordInstance]) { - throw new Error("Record instance not found in inputs for DIRECT operation"); + switch (op.name) { + // STEP 2: Validate DIRECT mapping operation + case DIRECT: { + const param = inputType[PARAMETER_1]; + if (!param) { + return null; + } + + if (!findSchemaTypeByPath(inputs, param.input)) { + throw new Error(`Input path not found for DIRECT operation: ${param.input}`); + } + + return { + operation: DIRECT, + targetType: outputType.type, + parameters: [removeArrayIndicesFromPath(param.input)] + }; } - - return { - operation: DIRECT, - targetType: outputType.type, - parameters: [paramOne.input], - }; - // STEP 3: Validate LENGTH operation - } else if (op.name === LENGTH) { - const paramOne = inputType[PARAMETER_1]; - if (!paramOne) { - throw new Error("Parameter 1 not found in input type for LENGTH operation"); - } - - const pathString = paramOne.input; - if (outputType.type === "int" || outputType.type === "int|()") { - const paths = pathString.split("."); - if (paths.length === 0) { - throw new Error("Invalid path in input type for LENGTH operation"); + case LENGTH: { + const param = inputType[PARAMETER_1]; + if (!param) { + throw new Error("Parameter 1 not found in input type for LENGTH operation"); } - - const recordInstance = paths[0]; - if (!inputs[recordInstance]) { - throw new Error("Record instance not found in inputs for LENGTH operation"); + + if (!(outputType.type === "int" || outputType.type === "int|()")) { + throw new Error("Invalid output type for LENGTH operation"); } - + + if (!findSchemaTypeByPath(inputs, param.input)) { + throw new Error(`Input path not found for LENGTH operation: ${param.input}`); + } + return { operation: LENGTH, targetType: outputType.type, - parameters: [pathString], + parameters: [removeArrayIndicesFromPath(param.input)] }; - } else { - throw new Error("Invalid input or output type for LENGTH operation"); } - // STEP 4: Validate SPLIT operation - } else if (op.name === SPLIT) { - let paramOne: ParameterMetadata, paramTwo: string | MappingJson; - if (inputType[PARAMETER_1] && mapping.PARAMETER_2) { - paramOne = inputType[PARAMETER_1]; - paramTwo = mapping.PARAMETER_2; - } else if (inputType[PARAMETER_2] && mapping.PARAMETER_1) { - paramOne = inputType[PARAMETER_2]; - paramTwo = mapping.PARAMETER_1; - } else { - throw new Error("Required parameters not found in input type for SPLIT operation"); + case SPLIT: { + const paramOne = inputType[PARAMETER_1]; + const paramTwo = mapping.PARAMETER_2; + + if (!paramOne || !paramTwo) { + throw new Error("Required parameters not found in input type for SPLIT operation"); + } + + if (paramOne.type !== "regex" || + !( + outputType.type === "string[]" || + outputType.type === "string[]|()" || + outputType.type === "(string|())[]" || + outputType.type === "(string|())[]|()" + ) + ) { + throw new Error("Invalid input or output type for SPLIT operation"); + } + + if (!findSchemaTypeByPath(inputs, paramOne.input)) { + throw new Error(`Input path not found for SPLIT operation: ${paramOne.input}`); + } + + return { + operation: SPLIT, + targetType: outputType.type, + parameters: [paramOne.input, paramTwo] + }; } + + default: + return null; + } +} - if ( - paramOne.type !== "regex" || - !( - outputType.type === "string[]" || - outputType.type === "string[]|()" || - outputType.type === "(string|())[]" || - outputType.type === "(string|())[]|()" - ) - ) { - throw new Error("Invalid input or output type for SPLIT operation"); - } +// ============================================================================= +// FIELD METADATA EXTRACTION FUNCTIONS +// ============================================================================= - const paths = paramOne.input.split("."); - if (paths.length === 0) { - throw new Error("Invalid path in input type for SPLIT operation"); +/** + * Extracts field metadata from schema type with enhanced type checking + */ +function getFieldMetadataFromSchemaType( + inputType: IOType, + paramName?: string, + operationName?: string +): FieldMetadata | null { + if (paramName && operationName) { + if (operationName === SPLIT && paramName === PARAMETER_1) { + return { type: "regex", optional: inputType.optional, nullable: false }; } + } - const recordInstance = paths[0]; - if (!inputs[recordInstance]) { - throw new Error("Record instance not found in inputs for SPLIT operation"); - } + const kind = inputType.kind; + const typeName = inputType.typeName; - return { - operation: "SPLIT", - targetType: outputType.type, - parameters: [paramOne.input, paramTwo], - }; + if (!kind || !typeName) { + throw new Error("Missing kind or typeName in SchemaType"); } - return null; + const typeString = typeName; + const isOptional = inputType.optional; + let isNullable = false; + + // Check if type is nullable (contains ? or ()) + if (typeName.includes("?") || typeName.includes("()")) { + isNullable = true; + } + + return { + type: typeString, + optional: isOptional, + nullable: isNullable + }; } -// ============================================================================= -// TYPE METADATA EXTRACTION ENGINE -// ============================================================================= +/** + * Gets output field metadata from schema type + */ +function getOutputFieldMetadata(output: IOType, path: string[]): FieldMetadata | null { + let current = output; + + for (const pathSegment of path) { + const found = findFieldInSchemaType(current, pathSegment); + if (!found) { + return null; + } + current = found; + } + + return getFieldMetadataFromSchemaType(current); +} /** - * Extracts type information from schema metadata for validation purposes + * Finds a field within a schema type structure */ -function getTypeMetadataOfField( - input: MetadataType, - pathParameters: string[], - paramName?: string, - operationName?: string -): FieldMetadata | null { - // STEP 1: Handle special parameter types (e.g., regex for SPLIT operation) - if (paramName && operationName) { - if (operationName === SPLIT && paramName === PARAMETER_2) { - return { type: "regex", optional: false, nullable: false }; +function findFieldInSchemaType(schemaType: IOType, fieldId: string): IOType | null { + const fields = schemaType.fields || schemaType.members; + if (fields && Array.isArray(fields)) { + for (const field of fields) { + if (field.id === fieldId || field.name === fieldId) { + return field; + } + + const found = findFieldInSchemaType(field, fieldId); + if (found) { + return found; + } } } - // STEP 2: Process nested field path navigation - if (pathParameters.length > 0) { - let modifiedInputs: { [key: string]: MetadataField } | undefined; - - // Extract field map from different metadata types - if ("fields" in input && input.fields && typeof input.fields === "object" && !("typeName" in input.fields)) { - modifiedInputs = input.fields as { [key: string]: MetadataField }; - } else if ( - "members" in input && - input.members && - typeof input.members === "object" && - !("typeName" in input.members) - ) { - modifiedInputs = input.members as { [key: string]: MetadataField }; - } else if (typeof input === "object" && !("typeName" in input)) { - modifiedInputs = input as { [key: string]: MetadataField }; + const member = schemaType.member; + if (member) { + if (member.id === fieldId || member.name === fieldId) { + return member; } + return findFieldInSchemaType(member, fieldId); + } + + return null; +} - if (!modifiedInputs) { - throw new Error("No fields found in MetadataField"); +/** + * Converts flat mapping structure to nested format + */ +function convertFlatToNestedMap(flatMap: { [key: string]: MappingJson }): { [key: string]: MappingJson } { + const nested: { [key: string]: MappingJson } = {}; + + for (const [flatKey, value] of Object.entries(flatMap)) { + const parts = flatKey.split('.'); + let current = nested; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!(part in current)) { + current[part] = {}; + } else if (typeof current[part] !== 'object' || current[part] === null) { + current[part] = {}; + } + current = current[part] as { [key: string]: MappingJson }; } - // STEP 3: Navigate to next level in field hierarchy - const index = pathParameters.pop()!; - const temporaryRecord = modifiedInputs[index]; - if (temporaryRecord) { - return getTypeMetadataOfField(temporaryRecord, pathParameters, paramName, operationName); - } else { - return null; + const lastPart = parts[parts.length - 1]; + current[lastPart] = value; + } + + return nested; +} + +// ============================================================================= +// ENHANCED MAIN ORCHESTRATOR FUNCTION +// ============================================================================= + +/** + * Enhanced main function for AI-powered data mapping generation with schema support + */ +async function mapData(payload: DataMapperModelResponse): Promise { + const maxRetries = 3; + let retries = 0; + let lastError: Error; + + while (retries < maxRetries) { + if (retries > 0) { + console.debug("Retrying to generate mappings for the payload."); } - } else { - // STEP 4: Extract final field type metadata + try { - if ("typeName" in input && "optional" in input && "nullable" in input) { - const metadataField = input as MetadataField; - return { - type: metadataField.typeName, - optional: metadataField.optional, - nullable: metadataField.nullable, - }; + // Extract existing mapping field hints + const mappingFields: { [key: string]: MappingFields } = payload.mappingsModel.mapping_fields || {}; + + // STEP 1: Generate AI-powered mappings using Claude + const generatedMappings = await getMappings((payload.mappingsModel as ExpandedDMModel)?.inputs, (payload.mappingsModel as ExpandedDMModel)?.output, payload.mappingsModel.mappings, mappingFields); + + if (Object.keys(generatedMappings).length === 0) { + const error = new Error("No valid fields were identified for mapping between the given input and output records."); + lastError = error; + retries += 1; + continue; + } + + // STEP 2: Prepare inputs for validation + const inputs: DataMappingRequest = { + input: (payload.mappingsModel as ExpandedDMModel)?.inputs, + output: (payload.mappingsModel as ExpandedDMModel)?.output + }; + + // STEP 3: Validate and process AI-generated mappings with schema + const evaluateMappingsResult = await evaluateMappings([], generatedMappings, inputs); + + if (evaluateMappingsResult) { + // STEP 4: Extract and structure the validated mappings + const mappings = extractMappings(evaluateMappingsResult); + return mappings; } else { - throw new Error("Invalid metadata structure"); + throw new Error("Failed to generate mappings for the payload."); } } catch (error) { - throw new Error(`Error occurred while getting the type metadata of the field: ${error}`); + console.error(`Error occurred while generating mappings: ${error}`); + lastError = error as Error; + retries += 1; + continue; + } + } + throw lastError; +} + +// Import all existing functions from the original implementation +async function getMappings( + inputJsonRecord: IOType[], + outputJsonRecord: IOType, + userMappings: DataMappingResponse[], + mappingTips: { [key: string]: MappingFields } +): Promise { + const prompt = getDataMappingPrompt( + JSON.stringify(inputJsonRecord), + JSON.stringify(outputJsonRecord), + JSON.stringify(userMappings), + JSON.stringify(mappingTips) + ); + + const messages: CoreMessage[] = [ + { role: "user", content: prompt } + ]; + + try { + const { object } = await generateObject({ + model: await getAnthropicClient(ANTHROPIC_SONNET_4), + maxTokens: 4096, + temperature: 0, + messages: messages, + schema: MappingSchema, + abortSignal: AIPanelAbortController.getInstance().signal, + }); + + const generatedMappings = object.generatedMappings as AIDataMappings; + return generatedMappings; + } catch (error) { + console.error("Failed to parse response:", error); + throw new Error(`Failed to parse mapping response: ${error}`); + } +} + +function extractMappings(evaluateMappingsResult: MappingJson): DatamapperResponse { + const mappings: { [key: string]: MappingJson } = {}; + + if (isMappingRecord(evaluateMappingsResult)) { + throw new Error("EvaluateMappingsResult is a MappingRecord, expected map structure."); + } + + if (typeof evaluateMappingsResult === "object" && evaluateMappingsResult !== null) { + for (const [key, value] of Object.entries(evaluateMappingsResult)) { + if (isMappingRecord(value)) { + mappings[key] = value; + } else if (typeof value === "object" && value !== null) { + const nestedMappingsResult = extractMappings(value as MappingJson); + mappings[key] = nestedMappingsResult.mappings; + } } } + + return { mappings }; } // ============================================================================= @@ -601,11 +706,7 @@ function isMapping(value: any): value is { OPERATION: MappingOperation } { // MAIN EXPORT FUNCTION // ============================================================================= -/** - * Main export function for generating auto mappings - * This function matches the original signature and provides a simple interface - */ -export async function generateAutoMappings(payload?: Payload): Promise { +export async function generateAutoMappings(payload?: DataMapperModelResponse): Promise { if (!payload) { throw new Error("Payload is required for generating auto mappings"); } @@ -613,7 +714,7 @@ export async function generateAutoMappings(payload?: Payload): Promise { - if (isMapping(llmGeneratedMappings)) { - return await processMappingOperation(path, llmGeneratedMappings, initialRecords); - } else { - return await processNestedMappings(path, llmGeneratedMappings as { [key: string]: AIDataMappings }, initialRecords); - } -} - -/** - * Processes individual mapping operations with inline schema validation - */ -async function processMappingOperation( - path: string[], - llmGeneratedMappings: Mapping, - initialRecords: InlineInputs -): Promise { - try { - const operationRecord = llmGeneratedMappings.OPERATION; - const parametersTypes: { [key: string]: ParameterMetadata } = {}; - let validParameters = false; - - for (const subKey of Object.keys(operationRecord)) { - if (subKey === NAME) { - continue; - } - - const subPathString = operationRecord[subKey as keyof MappingOperation] as string; - if (!subPathString) { - continue; - } - - const operationName = operationRecord.NAME; - const inputType = findSchemaTypeByPath(initialRecords.input, subPathString); - if (!inputType) { - continue; - } - - const fieldMetadata = getFieldMetadataFromSchemaType(inputType, subKey, operationName); - if (!fieldMetadata) { - continue; - } - - parametersTypes[subKey] = { - type: fieldMetadata.type, - input: subPathString, - optional: fieldMetadata.optional, - nullable: fieldMetadata.nullable - }; - validParameters = true; - } - - if (validParameters) { - const outputType = getOutputFieldMetadata(initialRecords.output, path); - if (!outputType) { - return null; - } - - const mapping = validateInlineMappingOperation( - operationRecord, - parametersTypes, - outputType, - path[path.length - 1], - initialRecords.input - ); - return mapping; - } - return null; - } catch (error) { - console.error('Error processing mapping operation:', error); - return null; - } -} - -/** - * Processes nested mapping structures - */ -async function processNestedMappings( - path: string[], - llmGeneratedMappings: { [key: string]: AIDataMappings }, - initialRecords: InlineInputs -): Promise { - const returnRec: { [key: string]: MappingJson } = {}; - - for (const [key, value] of Object.entries(llmGeneratedMappings)) { - if (value === null || value === undefined) { - continue; - } - - const newPath = [...path]; - const cleanKey = removeOutputRecordPrefix(key, initialRecords.output); - newPath.push(key); - - const temporaryRecord = await evaluateInlineMappings(newPath, value, initialRecords); - if (temporaryRecord) { - if (isMappingRecord(temporaryRecord) || - (typeof temporaryRecord === 'object' && Object.keys(temporaryRecord).length > 0)) { - returnRec[cleanKey] = temporaryRecord; - } - } - } - - return convertFlatToNestedMap(returnRec); -} - -// ============================================================================= -// INLINE MAPPING OPERATION VALIDATION -// ============================================================================= - -/** - * Validates inline mapping operations with enhanced error handling - */ -function validateInlineMappingOperation( - mapping: MappingOperation, - inputType: { [key: string]: ParameterMetadata }, - outputType: FieldMetadata, - name: string, - inputs: IOType[] -): MappingJson | null { - const operation = mapping.NAME; - const op = operationsTable.get(operation); - - if (!op) { - return null; - } - - switch (op.name) { - case DIRECT: { - const param = inputType[PARAMETER_1]; - if (!param) { - return null; - } - - if (!findSchemaTypeByPath(inputs, param.input)) { - throw new Error(`Input path not found for DIRECT operation: ${param.input}`); - } - - return { - operation: DIRECT, - targetType: outputType.type, - parameters: [removeArrayIndicesFromPath(param.input)] - }; - } - - case LENGTH: { - const param = inputType[PARAMETER_1]; - if (!param) { - throw new Error("Parameter 1 not found in input type for LENGTH operation"); - } - - if (!(outputType.type === "int" || outputType.type === "int|()")) { - throw new Error("Invalid output type for LENGTH operation"); - } - - if (!findSchemaTypeByPath(inputs, param.input)) { - throw new Error(`Input path not found for LENGTH operation: ${param.input}`); - } - - return { - operation: LENGTH, - targetType: outputType.type, - parameters: [removeArrayIndicesFromPath(param.input)] - }; - } - - case SPLIT: { - const paramOne = inputType[PARAMETER_1]; - const paramTwo = mapping.PARAMETER_2; - - if (!paramOne || !paramTwo) { - throw new Error("Required parameters not found in input type for SPLIT operation"); - } - - if (paramOne.type !== "regex" || - !(outputType.type === "string[]" || outputType.type === "string[]|()" || - outputType.type === "(string|())[]" || outputType.type === "(string|())[]|()")) { - throw new Error("Invalid input or output type for SPLIT operation"); - } - - if (!findSchemaTypeByPath(inputs, paramOne.input)) { - throw new Error(`Input path not found for SPLIT operation: ${paramOne.input}`); - } - - return { - operation: SPLIT, - targetType: outputType.type, - parameters: [removeArrayIndicesFromPath(paramOne.input), paramTwo] - }; - } - - default: - return null; - } -} - -// ============================================================================= -// FIELD METADATA EXTRACTION FUNCTIONS -// ============================================================================= - -/** - * Extracts field metadata from schema type with enhanced type checking - */ -function getFieldMetadataFromSchemaType( - inputType: IOType, - paramName?: string, - operationName?: string -): FieldMetadata | null { - if (paramName && operationName) { - if (operationName === SPLIT && paramName === PARAMETER_2) { - return { type: "regex", optional: false, nullable: false }; - } - } - - const kind = inputType.kind; - const typeName = inputType.typeName; - - if (!kind || !typeName) { - throw new Error("Missing kind or typeName in SchemaType"); - } - - const typeString = typeName; - const isOptional = false; // TODO: Handle optional types - let isNullable = false; // TODO: Handle nullable types - - // Check if type is nullable (contains ? or ()) - if (typeName.includes("?") || typeName.includes("()")) { - isNullable = true; - } - - return { - type: typeString, - optional: isOptional, - nullable: isNullable - }; -} - -/** - * Gets output field metadata from schema type - */ -function getOutputFieldMetadata(output: IOType, path: string[]): FieldMetadata | null { - let current = output; - - for (const pathSegment of path) { - const found = findFieldInSchemaType(current, pathSegment); - if (!found) { - return null; - } - current = found; - } - - return getFieldMetadataFromSchemaType(current); -} - -/** - * Finds a field within a schema type structure - */ -function findFieldInSchemaType(schemaType: IOType, fieldId: string): IOType | null { - const fields = schemaType.fields; - if (fields && Array.isArray(fields)) { - for (const field of fields) { - if (field.id === fieldId || field.variableName === fieldId) { - return field; - } - - const found = findFieldInSchemaType(field, fieldId); - if (found) { - return found; - } - } - } - - const member = schemaType.member; - if (member) { - if (member.id === fieldId || member.variableName === fieldId) { - return member; - } - return findFieldInSchemaType(member, fieldId); - } - - return null; -} - -/** - * Converts flat mapping structure to nested format - */ -function convertFlatToNestedMap(flatMap: { [key: string]: MappingJson }): { [key: string]: MappingJson } { - const nested: { [key: string]: MappingJson } = {}; - - for (const [flatKey, value] of Object.entries(flatMap)) { - const parts = flatKey.split('.'); - let current = nested; - - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i]; - if (!(part in current)) { - current[part] = {}; - } else if (typeof current[part] !== 'object' || current[part] === null) { - current[part] = {}; - } - current = current[part] as { [key: string]: MappingJson }; - } - - const lastPart = parts[parts.length - 1]; - current[lastPart] = value; - } - - return nested; -} - -// ============================================================================= -// ENHANCED MAIN ORCHESTRATOR FUNCTION -// ============================================================================= - -/** - * Enhanced main function for AI-powered data mapping generation with inline schema support - */ -async function mapInlineData(payload: InlineDataMapperModelResponse): Promise { - const maxRetries = 3; - let retries = 0; - let lastError: Error; - - while (retries < maxRetries) { - if (retries > 0) { - console.debug("Retrying to generate mappings for the payload."); - } - - try { - // Extract existing mapping field hints - const mappingFields: { [key: string]: MappingFields } = payload.mappingsModel.mapping_fields || {}; - - // STEP 1: Generate AI-powered mappings using Claude - const generatedMappings = await getInlineMappings((payload.mappingsModel as ExpandedDMModel)?.inputs, (payload.mappingsModel as ExpandedDMModel)?.output, payload.mappingsModel.mappings, mappingFields); - - if (Object.keys(generatedMappings).length === 0) { - const error = new Error("No valid fields were identified for mapping between the given input and output records."); - lastError = error; - retries += 1; - continue; - } - - // STEP 2: Prepare inline inputs for validation - const inlineInputs: InlineInputs = { - input: (payload.mappingsModel as ExpandedDMModel)?.inputs, - output: (payload.mappingsModel as ExpandedDMModel)?.output - }; - - // STEP 3: Validate and process AI-generated mappings with inline schema - const evaluateMappingsResult = await evaluateInlineMappings([], generatedMappings, inlineInputs); - - if (evaluateMappingsResult) { - // STEP 4: Extract and structure the validated mappings - const mappings = extractMappings(evaluateMappingsResult); - return mappings; - } else { - throw new Error("Failed to generate mappings for the payload."); - } - } catch (error) { - console.error(`Error occurred while generating mappings: ${error}`); - lastError = error as Error; - retries += 1; - continue; - } - } - throw lastError; -} - -// Import all existing functions from the original implementation -async function getInlineMappings( - inputJsonRecord: IOType[], - outputJsonRecord: IOType, - userMappings: InlineDataMapping[], - mappingTips: { [key: string]: MappingFields } -): Promise { - const prompt = getInlineDataMappingPrompt( - JSON.stringify(inputJsonRecord), - JSON.stringify(outputJsonRecord), - JSON.stringify(userMappings), - JSON.stringify(mappingTips) - ); - - const messages: CoreMessage[] = [ - { role: "user", content: prompt } - ]; - - try { - const { object } = await generateObject({ - model: await getAnthropicClient(ANTHROPIC_SONNET_4), - maxTokens: 4096, - temperature: 0, - messages: messages, - schema: MappingSchema, - abortSignal: AIPanelAbortController.getInstance().signal, - }); - - const generatedMappings = object.generatedMappings as AIDataMappings; - return generatedMappings; - } catch (error) { - console.error("Failed to parse response:", error); - throw new Error(`Failed to parse mapping response: ${error}`); - } -} - -function extractMappings(evaluateMappingsResult: MappingJson): DatamapperResponse { - const mappings: { [key: string]: MappingJson } = {}; - - if (isMappingRecord(evaluateMappingsResult)) { - throw new Error("EvaluateMappingsResult is a MappingRecord, expected map structure."); - } - - if (typeof evaluateMappingsResult === "object" && evaluateMappingsResult !== null) { - for (const [key, value] of Object.entries(evaluateMappingsResult)) { - if (isMappingRecord(value)) { - mappings[key] = value; - } else if (typeof value === "object" && value !== null) { - const nestedMappingsResult = extractMappings(value as MappingJson); - mappings[key] = nestedMappingsResult.mappings; - } - } - } - - return { mappings }; -} - -// ============================================================================= -// UTILITY FUNCTIONS -// ============================================================================= - -function isMappingRecord(value: any): value is MappingRecord { - return value && typeof value === "object" && "operation" in value && "targetType" in value && "parameters" in value; -} - -function isMapping(value: any): value is { OPERATION: MappingOperation } { - return value && typeof value === "object" && "OPERATION" in value; -} - -// ============================================================================= -// MAIN EXPORT FUNCTION -// ============================================================================= - -export async function generateInlineAutoMappings(payload?: InlineDataMapperModelResponse): Promise { - if (!payload) { - throw new Error("Payload is required for generating auto mappings"); - } - try { - return await mapInlineData(payload); - } catch (error) { - console.error(`Error generating auto mappings: ${error}`); - throw error; - } -} diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/inline_prompt.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/inline_prompt.ts deleted file mode 100644 index 0918903a181..00000000000 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/inline_prompt.ts +++ /dev/null @@ -1,146 +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. - -/** - * Generates the inline data mapping prompt for AI - */ -export function getInlineDataMappingPrompt(inputJson: string, outputJson: string, userMappings: string, mappingTips: string): string { - return `You are an assistant that can help to map attributes between multiple JSON objects (data-mapping). - -## Instructions - -Before starting the mapping process, consider the mappings provided by the user mappings and mapping tips below. Use the user's and mapping tips as a guide/tip to do the mapping process, ensuring that they are relevant to input and output JSON. Only use the tips in user's mappings and mapping tips that have input and output records and their fields and subfields are in input and output JSON. Otherwise omit the irrelevant mapping guides. - -## Input JSON - -${inputJson} - -## Output JSON - -${outputJson} - -## User's Mappings - -${userMappings} - -## Mapping Tips - -${mappingTips} - -## Mapping Rules - -Follow these rules during data mapping: - -1. One or more input JSON can be given -2. Only a single output JSON can be given -3. Mapping the fields requires performing operations on the data. Most common operation is to do a one-to-one mapping with no transformations -4. One or more fields in the input JSON may be required to construct the output field value in-case we have complex operations that require multiple input fields -5. Some input fields may not participate in any mappings if they are irrelevant to the output field -6. Some output fields may not participate in any mappings if they are irrelevant to the input field -7. Field access uses dot notation for JSON format. To access subfield "abc" from object "xyz", use "xyz.abc". For accessing fields with IDs like "input.contactInfo.email", use the exact ID path as provided in the schema. -8. Strictly follow data types accepted and returned by the operations when mapping input fields -9. When mapping, you must use operators which return the expected data type -10. When Mapping, consider the information mentioned in the comments -11. DO NOT use the value in the field "optional" when mapping the fields -12. DO NOT map anything if you aren't sure -13. If both input and output are records type, DO mapping for all its fields and subfields but DO NOT map in the root level -14. Consider constants, configurables, variables, enum values, and module variables when mapping fields -15. Constants, variables, module variables and configurables can be mapped directly using their defined values -16. Enum values should be mapped using their exact enum identifiers -17. Consider both user's mappings and mapping tips when determining field relationships and transformations -18. Mapping tips provide additional mapping context from previous operations or related mappings that can be used as reference - -## Available Operations - -### 0) Direct Mapping -- ${"DIRECT(x)"} - used to substitute with x without any transformations -- **For input fields, variables, and module variables: use field path (e.g., "input.fieldName")** -- **For constants and configurables: use their defined values** -- **For enums: use their exact enum identifiers** - -### 1) Arithmetic Expressions -- ${"ADDITION(x, y, z, ...)"} - add variables x, y and z and so on -- ${"SUBTRACTION(x, y)"} - subtract y from x -- ${"MULTIPLICATION(x, y, z, ...)"} - multiply x, y and z and so on -- ${"DIVISION(x, y)"} - divide x by y -- ${"MODULAR(x, y)"} - get the modular division between x and y i.e. x%y - -### 2) Equality Expressions -- ${"EQUAL(x, y)"} - return true if x and y are equal -- ${"NOTEQUAL(x, y)"} - return true if x and y are not equal - -### 3) Relational Expressions -- ${"LESS_THAN(x, y)"} - return true if x is less than y -- ${"LESS_THAN_OR_EQUAL(x, y)"} - return true if x is less than or equals to y - -### 4) Logical Expressions -- ${"AND(x, y)"} - return x AND y value -- ${"OR(x, y)"} - return x OR y value - -### 5) Member Access Expressions -- ${"x[y]"} - access y th element of x array object in the json - -### 6) Regex Operations -- ${"SPLIT(regex, text)"} - Split the string text based on the regex and returns an array of strings (string[]) - - Example: ${"SPLIT(\",\", \"word1, word2, word3\")"} will return a string array ["word1", "word2", "word3"] - - Example: ${"SPLIT(\" \", \"word1 word2 word3\")"} will return a string array ["word1", "word2", "word3"] -- ${"REPLACE_ALL(regex, text, replacement)"} - Replace all the instances of regex in the text using string replacement - - Example: ${"REPLACE_ALL(\" \", \"word1 word2 word3\", \"\")"} will return a string "word1word2word3" - -For above two operations, regex value must be one or combination of the following: [" ", "_", "-", "\\n", ",", "\\."], here "\\" is used to escape special characters. - -### 7) Numerical Operations -- ${"AVERAGE(x, TYPE)"} - get the average over x. x is a single array of variables of TYPE (ex - [12, 13, 14]) when TYPE is INTEGER. TYPE can be either INT, DECIMAL, or FLOAT -- ${"MAXIMUM(x, TYPE)"} - get the maximum over x. x is an array of variables of TYPE(ex - [12, 13, 14]) when TYPE is INTEGER. TYPE can be either INT, DECIMAL, or FLOAT -- ${"MINIMUM(x, TYPE)"} - get the minimum over x. x is a single array of variables of TYPE (ex - [12, 13, 14]) when TYPE is INTEGER. TYPE can be either INT, DECIMAL, or FLOAT -- ${"SUMMATION(x, TYPE)"} - get the summation over x. x is a single array of variables of TYPE(ex - [12, 13, 14]) when TYPE is INTEGER. TYPE can be either INT, DECIMAL, or FLOAT -- ${"ABSOLUTE(x, TYPE)"} - get the absolute value of the given variable of TYPE, x. TYPE can be either INT, DECIMAL, or FLOAT - -### 8) Array Operations -- ${"LENGTH(x)"} - Get the length of an array named x - -## Response Format - -Always use the following json format to respond without any markdown formatting: - -{ - "": { - "OPERATION": { - "NAME": "", - "PARAMETER_1": "", - "PARAMETER_2": "" - // ...additional parameters as needed - } - } - // ...additional fields as needed -} - -## IMPORTANT NOTES: - -- **DO NOT RETURN ANYTHING OTHER THAN THE MAPPING JSON!** -- **DO NOT ENCLOSE THE RESULT JSON WITH ANYTHING.** -- **DO NOT USE MARKDOWN CODE BLOCKS OR BACKTICKS.** -- **RETURN ONLY RAW JSON WITHOUT ANY FORMATTING OR WRAPPER.** -- **FOR DIRECT MAPPINGS:** - - **Input fields, variables, constants, configurables and module variables: use field ID/path from the input schema** - - **Enum values: use their exact enum identifiers** - - **DEFAULT VALUES AND NULL LIKE VALUES MUST NOT BE MAPPED DIRECT.** -- **Use the exact field IDs as provided in the input/output schema (e.g., "input.contactInfo.email", "output.salaryInfo.baseSalary")** -- **Consider mapping tips as additional reference for understanding field relationships and mapping patterns** -`; -} - - diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/prompt.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/prompt.ts index 2a123808886..1b9b2cb8a0e 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/prompt.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/prompt.ts @@ -17,332 +17,438 @@ /** * Generates the main data mapping prompt for AI */ -export function getDataMappingPrompt(inputJson: string, outputJson: string, mappingFields: string): string { - return `You are an assistant that can help to map attributes between multiple json (a.k.a data-mapping). -Before starting the mapping process, consider the mappings provided by the user below. -Use the user's mappings as a guide/tip to do the mapping process, ensuring that they are relevant to input and output json. -Only use the tips in user's mappings that is input and output records and their fields and subfields are in input and output json. Otherwise omit the irrelevant user's mapping guides. -Also, use the below rules to do the data mapping. - -Following rules should be followed during data mapping. -1) One or more input json can be given -2) Only a single output json can be given -3) Mapping the fields requires performing operations on the data. Most common operation is to do a one-to-one mapping with no transformations. -4) One or more fields in the input json may be required to construct the output field value in-case we have complex operations that require multiple input fields. -5) Some input fields may not participate in any mappings if they are irrelevant to the output field. -6) Some output fields may not participate in any mappings if they are irrelevant to the input field. -7) Accessing the subfield "abc" from object "xyz" can be denoted as "xyz.abc". If the field contains a single quote at the beginning of the field name, include that field with the single quote in front of it in the path. -8) Strictly follow data types accepted and returned by the operations when mapping input fields. -9) When mapping, you must use operators which return the expected data type. -10) When Mapping, consider the information mentioned in the comments. -11) DO NOT use the value in the field "optional" when mapping the fields. -12) DO NOT map anything if you aren't sure. -13) If both input and output are records type, DO mapping for all its fields and subfields but DO NOT map in the root level. - -Following operations/transformations can be used during a mapping between input and output fields. Use ONLY the operations listed below. -0) Direct Mapping - DIRECT(x) used to substitute with x without any transformations -1) Arithmetic Expressions - 1.1) ADDITION(x, y, z, ...) : add variables x, y and z and so on - 1.2) SUBTRACTION(x, y) : subtract y from x - 1.3) MULTIPLICATION(x, y, z, ...) : multiply x, y and z and so on - 1.4) DIVISION(x, y) : divide x by y - 1.5) MODULAR(x, y) : get the modular division between x and y i.e. x%y -2) Equality Expressions - 2.1) EQUAL(x, y) : return true if x and y are equal - 2.2) NOTEQUAL(x, y) : return true if x and y are not equal -3) Relational Expressions - 3.1) LESS_THAN(x, y) : return true if x is less than y - 3.2) LESS_THAN_OR_EQUAL(x, y) : return true if x is less than or equals to y -4) Logical Expressions - 4.1) AND(x, y) : return x AND y value - 4.2) OR(x, y) : return x OR y value -5) Member Access Expressions - 5.1) x[y] : access y th element of x array object in the json -6) Regex Operations - 6.1) SPLIT(text, regex) : Split the string text based on the regex and returns an array of strings (string[]). - Example: - SPLIT("word1, word2, word3", ",") will return a string array ["word1", "word2", "word3"]. - SPLIT("word1 word2 word3", " ") will return a string array ["word1", "word2", "word3"]. - 6.2) REPLACE_ALL(text, regex, replacement) : Replace all the instances of regex in the text using string replacement. - Example - REPLACE_ALL("word1 word2 word3", " ", "") will return a string "word1word2word3" - For above two operations, regex value must be one or combination of the following : [" ", "_", "-", "\n", ",", "\." ], here "\" is used to escape special characters. - -7) Numerical Operations - 7.1) AVERAGE(x, TYPE) : get the average over x. x is a single array of variables of TYPE (ex - [12, 13, 14]) when TYPE is INTEGER .TYPE can be either INT, DECIMAL, or FLOAT. - 7.2) MAXIMUM(x, TYPE) : get the maximum over x. x is an array of variables of TYPE(ex - [12, 13, 14]) when TYPE is INTEGER .TYPE can be either INT, DECIMAL, or FLOAT. - 7.3) MINIMUM(x, TYPE) : get the minimum over x. x is a single array of variables of TYPE (ex - [12, 13, 14]) when TYPE is INTEGER .TYPE can be either INT, DECIMAL, or FLOAT. - 7.4) SUMMATION(x, TYPE) : get the summation over x. x is a single array of variables of TYPE(ex - [12, 13, 14]) when TYPE is INTEGER .TYPE can be either INT, DECIMAL, or FLOAT. - 7.5) ABSOLUTE(x, TYPE) : get the absolute value of the given variable of TYPE, x .TYPE can be either INT, DECIMAL, or FLOAT. - -8) Array Operations - 8.1) LENGTH(x) : Get the length of an array named x - -Always use the following json format to respond. -\`\`\` +export function getDataMappingPrompt(inputJson: string, outputJson: string, userMappings: string, mappingTips: string): string { + return `You are an assistant that can help to map attributes between multiple JSON objects (data-mapping). + +## Instructions + +Before starting the mapping process, consider the mappings provided by the user mappings and mapping tips below. Use the user's and mapping tips as a guide/tip to do the mapping process, ensuring that they are relevant to input and output JSON. Only use the tips in user's mappings and mapping tips that have input and output records and their fields and subfields are in input and output JSON. Otherwise omit the irrelevant mapping guides. + +## Input JSON + +${inputJson} + +## Output JSON + +${outputJson} + +## User's Mappings + +${userMappings} + +## Mapping Tips + +${mappingTips} + +## Mapping Rules + +Follow these rules during data mapping: + +1. One or more input JSON can be given +2. Only a single output JSON can be given +3. Mapping the fields requires performing operations on the data. Most common operation is to do a one-to-one mapping with no transformations +4. One or more fields in the input JSON may be required to construct the output field value in-case we have complex operations that require multiple input fields +5. Some input fields may not participate in any mappings if they are irrelevant to the output field +6. Some output fields may not participate in any mappings if they are irrelevant to the input field +7. Field access uses dot notation for JSON format. To access subfield "abc" from object "xyz", use "xyz.abc". For accessing fields with IDs like "input.contactInfo.email", use the exact ID path as provided in the schema. +8. Strictly follow data types accepted and returned by the operations when mapping input fields +9. When mapping, you must use operators which return the expected data type +10. When Mapping, consider the information mentioned in the comments +11. DO NOT use the value in the field "optional" when mapping the fields +12. DO NOT map anything if you aren't sure +13. When both input and output are records, recursively traverse ALL nested fields until you reach primitive types (int, string, boolean, float, decimal, etc.) and map ONLY those primitive fields. NEVER map at the record level. +14. Consider constants, configurables, variables, enum values, and module variables when mapping fields +15. Constants, variables, module variables and configurables can be mapped directly using their defined values +16. Enum values should be mapped using their exact enum identifiers +17. Consider both user's mappings and mapping tips when determining field relationships and transformations +18. Mapping tips provide additional mapping context from previous operations or related mappings that can be used as reference + +## Available Operations + +### 0) Direct Mapping +- ${"DIRECT(x)"} - used to substitute with x without any transformations +- **For input fields, variables, and module variables: use field path (e.g., "input.fieldName")** +- **For constants and configurables: use their defined values** +- **For enums: use their exact enum identifiers** + +### 1) Arithmetic Expressions +- ${"ADDITION(x, y, z, ...)"} - add variables x, y and z and so on +- ${"SUBTRACTION(x, y)"} - subtract y from x +- ${"MULTIPLICATION(x, y, z, ...)"} - multiply x, y and z and so on +- ${"DIVISION(x, y)"} - divide x by y +- ${"MODULAR(x, y)"} - get the modular division between x and y i.e. x%y + +### 2) Equality Expressions +- ${"EQUAL(x, y)"} - return true if x and y are equal +- ${"NOTEQUAL(x, y)"} - return true if x and y are not equal + +### 3) Relational Expressions +- ${"LESS_THAN(x, y)"} - return true if x is less than y +- ${"LESS_THAN_OR_EQUAL(x, y)"} - return true if x is less than or equals to y + +### 4) Logical Expressions +- ${"AND(x, y)"} - return x AND y value +- ${"OR(x, y)"} - return x OR y value + +### 5) Member Access Expressions +- ${"x[y]"} - access y th element of x array object in the json + +### 6) Regex Operations +- ${"SPLIT(text, regex)"} - Split the string text based on the regex and returns an array of strings (string[]) + - Example: ${"SPLIT(\"word1, word2, word3\", \",\")"} will return a string array ["word1", "word2", "word3"] + - Example: ${"SPLIT(\"word1, word2, word3\", \" \")"} will return a string array ["word1", "word2", "word3"] +- ${"REPLACE_ALL(text, regex, replacement)"} - Replace all the instances of regex in the text using string replacement + - Example: ${"REPLACE_ALL(\"word1 word2 word3\", \" \", \"\")"} will return a string "word1word2word3" + +### 7) Numerical Operations +- ${"AVERAGE(x, TYPE)"} - get the average over x. x is a single array of variables of TYPE (ex - [12, 13, 14]) when TYPE is INTEGER. TYPE can be either INT, DECIMAL, or FLOAT +- ${"MAXIMUM(x, TYPE)"} - get the maximum over x. x is an array of variables of TYPE(ex - [12, 13, 14]) when TYPE is INTEGER. TYPE can be either INT, DECIMAL, or FLOAT +- ${"MINIMUM(x, TYPE)"} - get the minimum over x. x is a single array of variables of TYPE (ex - [12, 13, 14]) when TYPE is INTEGER. TYPE can be either INT, DECIMAL, or FLOAT +- ${"SUMMATION(x, TYPE)"} - get the summation over x. x is a single array of variables of TYPE(ex - [12, 13, 14]) when TYPE is INTEGER. TYPE can be either INT, DECIMAL, or FLOAT +- ${"ABSOLUTE(x, TYPE)"} - get the absolute value of the given variable of TYPE, x. TYPE can be either INT, DECIMAL, or FLOAT + +### 8) Array Operations +- ${"LENGTH(x)"} - Get the length of an array named x + +## Response Format + +Always use the following json format to respond without any markdown formatting: + { - "": { - "OPERATION": { - "NAME": "", - "PARAMETER_1" : , - "PARAMETER_2" : , - ... - ... - ... - } - }, - ... + "": { + "OPERATION": { + "NAME": "", + "PARAMETER_1": "", + "PARAMETER_2": "" + // ...additional parameters as needed + } + } + // ...additional fields as needed } -\`\`\` + Following is an example of the input, output and the mapping: Example Input json : -\`\`\` -{ - "studentDetails": { - "id":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"student id" +[ + { + "fields":[ + { + "id":"studentDetails.id", + "name":"id", + "typeName":"string", + "kind":"string", + "optional":false }, - "tags":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"student tags" + { + "id":"studentDetails.tags", + "name":"tags", + "typeName":"string", + "kind":"string", + "optional":false }, - "bio":{ - "firstName": { - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"first name of the student" - }, - "lastName":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"Last name of the student" - }, - "age":{ - "type":"int", - "optional" : false, - "nullable" : false, - "comment":"age in years" - } + { + "fields":[ + { + "id":"studentDetails.bio.firstName", + "name":"firstName", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "id":"studentDetails.bio.lastName", + "name":"lastName", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "id":"studentDetails.bio.age", + "name":"age", + "typeName":"int", + "kind":"int", + "optional":false + } + ], + "id":"studentDetails.bio", + "name":"bio", + "typeName":"Bio", + "kind":"record", + "optional":false }, - "address":{ - "address1": { - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"address line 1" - }, - "address2":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"address line 2" - }, - "city":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"city of the address" - }, - "country":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"country of residence" - }, - "zip":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"zip code" - } + { + "fields":[ + { + "id":"studentDetails.address.address1", + "name":"address1", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "id":"studentDetails.address.address2", + "name":"address2", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "id":"studentDetails.address.city", + "name":"city", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "id":"studentDetails.address.country", + "name":"country", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "id":"studentDetails.address.zipcode", + "name":"zipcode", + "typeName":"string", + "kind":"string", + "optional":false + } + ], + "id":"studentDetails.address", + "name":"address", + "typeName":"Address", + "kind":"record", + "optional":false }, - "academicDetails": { - "major":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"major of the degree" - }, - "subjects":{ - "type":"string[]", - "optional" : false, - "nullable" : false, - "comment":"enrolled subjects" - } - } - }, - "studentProgress": { - "studentId":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"student id" + { + "fields":[ + { + "id":"studentDetails.academicDetails.major", + "name":"major", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "member":{ + "id":"studentDetails.academicDetails.subjects.0", + "name":"", + "typeName":"string", + "kind":"string", + "optional":false + }, + "id":"studentDetails.academicDetails.subjects", + "name":"subjects", + "typeName":"string[]", + "kind":"array", + "optional":false + } + ], + "id":"studentDetails.academicDetails", + "name":"academicDetails", + "typeName":"AcademicDetails", + "kind":"record", + "optional":false }, - "currentLevel":{ - "type": "string", - "optional" : false, - "nullable" : false, - "comment": "current grade of the student" + { + "fields":[ + { + "id":"studentDetails.studentProgress.studentId", + "name":"studentId", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "id":"studentDetails.studentProgress.currentLevel", + "name":"currentLevel", + "typeName":"float", + "kind":"float", + "optional":false + } + ], + "id":"studentDetails.studentProgress", + "name":"studentProgress", + "typeName":"StudentProgress", + "kind":"record", + "optional":false } - } -} - -\`\`\` + ], + "id":"studentDetails", + "name":"studentDetails", + "typeName":"StudentDetails", + "kind":"record", + "category":"parameter", + "optional":false + } +] Example Output json : -\`\`\` { - "studentId":{ - "type":"int", - "optional" : true, - "nullable" : true, - "comment":"reservation id" - }, - "studentTags":{ - "type":"string[]", - "optional" : false, - "nullable" : false, - "comment":"student tags" + "fields":[ + { + "id":"student.studentId", + "name":"studentId", + "typeName":"int", + "kind":"int", + "optional":false }, - "bio":{ - "fullName": { - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"full name of the student" + { + "member":{ + "id":"student.studentTags", + "name":"", + "typeName":"string", + "kind":"string", + "optional":false }, - "age":{ - "type":"int", - "optional" : false, - "nullable" : false, - "comment":"age in years" - } + "id":"student.studentTags", + "name":"studentTags", + "typeName":"string[]", + "kind":"array", + "optional":false }, - "address":{ - "type":"string", - "optional" : false, - "nullable" : false, - "comment":"address of the student" + { + "fields":[ + { + "id":"student.studentBio.fullName", + "name":"fullName", + "typeName":"string", + "kind":"string", + "optional":false + }, + { + "id":"student.studentBio.age", + "name":"age", + "typeName":"int", + "kind":"int", + "optional":false + } + ], + "id":"student.studentBio", + "name":"studentBio", + "typeName":"StudentBio", + "kind":"record", + "optional":false }, - "AcademicMajor":{ - "type":"string", - "optional" : true, - "comment":"major of the degree" + { + "id":"student.studentAddress", + "name":"studentAddress", + "typeName":"string", + "kind":"string", + "optional":false }, - "subjects":{ - "type":"string[]", - "optional" : false, - "nullable" : false, - "comment":"enrolled subjects" + { + "id":"student.academicMajor", + "name":"academicMajor", + "typeName":"string", + "kind":"string", + "optional":false }, - "currentLevel": { - "type": "string", - "optional" : true, - "nullable" : true, - "comment": "current grade of the student" + { + "member":{ + "id":"student.subjects", + "name":"", + "typeName":"string", + "kind":"string", + "optional":false + }, + "id":"student.subjects", + "name":"subjects", + "typeName":"string[]", + "kind":"array", + "optional":false + }, + { + "id":"student.currentLevel", + "name":"currentLevel", + "typeName":"string", + "kind":"string", + "optional":false } + ], + "id":"student", + "name":"student", + "typeName":"Student", + "kind":"record", + "optional":false } -\`\`\` - Example Mapping: -\`\`\` + { - "studentId": { - "OPERATION": { - "NAME": "DIRECT", - "PARAMETER_1": "studentDetails.id", - "PARAMETER_2": "INT" - } - }, - "studentTags": { - "OPERATION": { - "NAME": "SPLIT", - "PARAMETER_1": "studentDetails.tags", - "PARAMETER_2": "," - } - }, - "bio": { - "fullName": { - "OPERATION": { - "NAME": "ADDITION", - "PARAMETER_1": "studentDetails.bio.firstName", - "PARAMETER_2": " ", - "PARAMETER_3": "studentDetails.bio.lastName" - } - }, - "age": { - "OPERATION": { - "NAME": "DIRECT", - "PARAMETER_1": "studentDetails.bio.age" - } - } - }, - "address": { - "OPERATION": { - "NAME": "ADDITION", - "PARAMETER_1": "studentDetails.address.address1", - "PARAMETER_2": ", ", - "PARAMETER_3": "studentDetails.address.address2", - "PARAMETER_4": ", ", - "PARAMETER_5": "studentDetails.address.country", - "PARAMETER_6": ", ", - "PARAMETER_7": "studentDetails.address.zip" - } - }, - "academicMajor": { - "OPERATION": { - "NAME": "DIRECT", - "PARAMETER_1": "studentDetails.academicDetails.major" - } - }, - "subjects": { - "OPERATION": { - "NAME": "DIRECT", - "PARAMETER_1": "studentDetails.academicDetails.subjects" - } - }, - "currentLevel": { - "OPERATION": { - "NAME": "DIRECT", - "PARAMETER_1": "studentDetails.studentProgress.currentLevel" - } + "student.studentId":{ + "OPERATION":{ + "NAME":"DIRECT", + "PARAMETER_1":"studentDetails.id" + } + }, + "student.studentTags":{ + "OPERATION":{ + "NAME":"DIRECT", + "PARAMETER_1":"studentDetails.tags" + } + }, + "student.studentBio.fullName":{ + "OPERATION":{ + "NAME":"ADDITION", + "PARAMETER_1":"studentDetails.bio.firstName", + "PARAMETER_2":" ", + "PARAMETER_3":"studentDetails.bio.lastName" + } + }, + "student.studentBio.age":{ + "OPERATION":{ + "NAME":"DIRECT", + "PARAMETER_1":"studentDetails.bio.age" + } + }, + "student.studentAddress":{ + "OPERATION":{ + "NAME":"ADDITION", + "PARAMETER_1":"studentDetails.address.address1", + "PARAMETER_2":", ", + "PARAMETER_3":"studentDetails.address.address2", + "PARAMETER_4":", ", + "PARAMETER_5":"studentDetails.address.city", + "PARAMETER_6":", ", + "PARAMETER_7":"studentDetails.address.country", + "PARAMETER_8":" ", + "PARAMETER_9":"studentDetails.address.zipcode" } + }, + "student.academicMajor":{ + "OPERATION":{ + "NAME":"DIRECT", + "PARAMETER_1":"studentDetails.academicDetails.major" + } + }, + "student.subjects":{ + "OPERATION":{ + "NAME":"DIRECT", + "PARAMETER_1":"studentDetails.academicDetails.subjects" + } + }, + "student.currentLevel":{ + "OPERATION":{ + "NAME":"DIRECT", + "PARAMETER_1":"studentDetails.studentProgress.currentLevel" + } + } } -\`\`\` -Now using the above rules find the mappings for the following input and output records. - -IMPORTANT : -DO NOT RETURN ANYTHING OTHER THAN THE MAPPING JSON! -DO NOT ENCLOSE THE RESULT JSON WITH ANYTHING. -FOR DIRECT MAPPINGS THE PARAMETER MUST BE A FIELD PATH IN THE INPUT. DEFAULT VALUES AND NULL LIKE VALUES MUST NOT BE MAPPED DIRECT. - -User's mappings -\`\`\` -${mappingFields} -\`\`\` +## IMPORTANT NOTES: - -Input json -\`\`\` -${inputJson} -\`\`\` - - -Output json -\`\`\` -${outputJson} -\`\`\` +- **DO NOT RETURN ANYTHING OTHER THAN THE MAPPING JSON!** +- **DO NOT ENCLOSE THE RESULT JSON WITH ANYTHING.** +- **DO NOT USE MARKDOWN CODE BLOCKS OR BACKTICKS.** +- **RETURN ONLY RAW JSON WITHOUT ANY FORMATTING OR WRAPPER.** +- **FOR DIRECT MAPPINGS:** + - **Input fields, variables, constants, configurables and module variables: use field ID/path from the input schema** + - **Enum values: use their exact enum identifiers** + - **DEFAULT VALUES AND NULL LIKE VALUES MUST NOT BE MAPPED DIRECT.** +- **Use the exact field IDs as provided in the input/output schema (e.g., "input.contactInfo.email", "output.salaryInfo.baseSalary")** +- **Consider mapping tips as additional reference for understanding field relationships and mapping patterns** `; } diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/types.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/types.ts index 28c761ba9f5..b52871f49d3 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/types.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/types.ts @@ -89,6 +89,17 @@ export interface MetadataField { members?: { [key: string]: MetadataField }; } +export interface DataMappingResponse { + output: string; + inputs?: string[]; + expression: string; +} + +export interface DataMappingRequest { + input: IOType[]; + output: IOType; +} + // ============================================================================= // MAPPING HINT TYPES // ============================================================================= @@ -102,11 +113,6 @@ export type MappingFields = MappingField | { [key: string]: MappingFields }; export type MetadataType = Metadata | MetadataField | { [key: string]: MetadataField }; -export interface Inputs { - input: { [key: string]: Metadata }; - output: { [key: string]: MetadataField }; -} - // ============================================================================= // OPERATION METADATA STRUCTURES // ============================================================================= @@ -163,16 +169,14 @@ export interface ChatResponse { } // ============================================================================= -// INLINE DATAMAPPING TYPES +// VISITOR PATTERN TYPES // ============================================================================= -export interface InlineDataMapping { - output: string; - inputs?: string[]; - expression: string; +export interface VisitorContext { + targetPath: string; + found: IOType | null; } -export interface InlineInputs { - input: IOType[]; - output: IOType; +export interface IOTypeVisitor { + visitIOType(ioType: IOType, context: VisitorContext): IOType | null; } diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/doc_generator.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/doc_generator.ts new file mode 100644 index 00000000000..3cd3b3dd1a3 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/doc_generator.ts @@ -0,0 +1,54 @@ +/** + * 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 { DocGenerationRequest } from '@wso2/ballerina-core'; +import { getServiceDeclaration } from '../../testGenerator'; +import { getBallerinaProjectRoot } from '../../../../rpc-managers/ai-panel/rpc-manager'; +import { generateDocumentation, DocumentationGenerationRequest } from './documentation'; +import { getProjectSource, getOpenAPISpecification } from '../../utils'; + +// Main documentation generator function that handles all the logic +export async function generateDocumentationForService(params: DocGenerationRequest): Promise { + try { + // Get the project root + const projectRoot = await getBallerinaProjectRoot(); + + // Get the project source files + const projectSource = await getProjectSource(projectRoot); + if (!projectSource) { + throw new Error("The current project is not recognized as a valid Ballerina project. Please ensure you have opened a Ballerina project."); + } + + // Find the service declaration and get OpenAPI spec + const { serviceDocFilePath } = await getServiceDeclaration(projectRoot, params.serviceName); + const openApiSpec = await getOpenAPISpecification(serviceDocFilePath); + + // Create the documentation generation request + const docRequest: DocumentationGenerationRequest = { + serviceName: params.serviceName, + projectSource: projectSource, + openApiSpec: openApiSpec + }; + + // Generate the documentation with streaming + await generateDocumentation(docRequest); + } catch (error) { + console.error("Error during documentation generation:", error); + throw error; + } +} diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/documentation.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/documentation.ts new file mode 100644 index 00000000000..10cf18d8676 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/documentation.ts @@ -0,0 +1,92 @@ +// 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 { Command, ProjectSource } from "@wso2/ballerina-core"; +import { streamText, CoreMessage } from "ai"; +import { getAnthropicClient, ANTHROPIC_SONNET_4 } from "../connection"; +import { + getDocumentationGenerationSystemPrompt, + createDocumentationGenMessages +} from "./prompts"; +import { CopilotEventHandler, createWebviewEventHandler } from "../event"; +import { getErrorMessage } from "../utils"; +import { AIPanelAbortController } from "../../../../../src/rpc-managers/ai-panel/utils"; + +export type DocumentationGenerationRequest = { + serviceName: string; + projectSource: ProjectSource; + openApiSpec?: string; +}; + +// Core documentation generation function that emits events +export async function generateDocumentationCore( + params: DocumentationGenerationRequest, + eventHandler: CopilotEventHandler +): Promise { + const systemPrompt = getDocumentationGenerationSystemPrompt(); + const userMessages: CoreMessage[] = createDocumentationGenMessages(params); + + const allMessages: CoreMessage[] = [ + { + role: "system", + content: systemPrompt, + }, + ...userMessages + ]; + + const { fullStream } = streamText({ + model: await getAnthropicClient(ANTHROPIC_SONNET_4), + maxTokens: 16384, + temperature: 0, + messages: allMessages, + abortSignal: AIPanelAbortController.getInstance().signal, + }); + + eventHandler({ type: "start" }); + let assistantResponse: string = ""; + + for await (const part of fullStream) { + switch (part.type) { + case "text-delta": { + const textPart = part.textDelta; + assistantResponse += textPart; + eventHandler({ type: "content_block", content: textPart }); + break; + } + case "error": { + const error = part.error; + console.error("Error during documentation generation:", error); + eventHandler({ type: "error", content: getErrorMessage(error) }); + break; + } + case "finish": { + eventHandler({ type: "stop", command: Command.Doc }); + break; + } + } + } +} + +// Main public function that uses the default event handler +export async function generateDocumentation(params: DocumentationGenerationRequest): Promise { + const eventHandler = createWebviewEventHandler(Command.Doc); + try { + await generateDocumentationCore(params, eventHandler); + } catch (error) { + console.error("Error during documentation generation:", error); + eventHandler({ type: "error", content: getErrorMessage(error) }); + } +} diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/prompts.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/prompts.ts new file mode 100644 index 00000000000..abc222a0fe8 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/prompts.ts @@ -0,0 +1,241 @@ +// 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 { DocumentationGenerationRequest } from "./documentation"; +import { CoreMessage } from "ai"; +import { flattenProjectToText, getExternalTypesAsJsonSchema } from "./utils"; + +// ============================================== +// SYSTEM PROMPTS +// ============================================== + +export function getDocumentationGenerationSystemPrompt(): string { + return `You are an expert technical writer and API documentation specialist with deep expertise in Ballerina services and REST API documentation. Your task is to analyze Ballerina source code and OpenAPI specifications to generate comprehensive, developer-focused documentation for APIs. + +Your documentation should be: +1. **Developer-Centric**: Written for API consumers and integrators +2. **Comprehensive**: Cover all endpoints, request/response formats, and error scenarios +3. **Practical**: Include short, real examples - not comprehensive but concise and realistic +4. **Professional**: Follow industry standards for API documentation +5. **Clear and Concise**: Easy to understand and navigate + +Focus on creating documentation that helps developers quickly understand: +- What the API does and its purpose +- How to authenticate and get started +- Available endpoints and their functionality +- Request/response formats with short, realistic examples +- Error handling and status codes +- Integration patterns and best practices`; +} + +// ============================================== +// DOCUMENTATION GENERATION PROMPTS +// ============================================== + +export function getDocumentationGenUser1Prompt(serviceCode: string, typeSchemas: string, serviceName: string): string { + return `Generate professional developer documentation for a Ballerina REST API service. I will provide you with: +1. The complete Ballerina service implementation +2. OpenAPI type schemas for external dependencies +3. The target service name for documentation + +Please analyze the code and create clean, professional API documentation following industry standards. + +**Service Name**: ${serviceName} + +**Ballerina Source Code**: +[BEGIN_SOURCE] +${serviceCode} +[END_SOURCE] + +**Type Schemas for External Libraries**: +[BEGIN_SCHEMAS] +${typeSchemas} +[END_SCHEMAS] + +Generate documentation following this professional structure: + +--- + +# [SERVICE_NAME] Service API – Developer Documentation + +The [SERVICE_NAME] Service API enables developers to [describe core functionality]. The API is designed to be simple, secure, and scalable. + +[Determine the actual service name from the code context - look at service class names, comments, resource paths, data models, and business logic to infer what this service does (e.g., "Pizza Shop", "User Management", "Inventory", etc.) rather than using the base path directly] + +[Only include Base URLs if they can be determined from the service configuration or listener setup in the code] +* **Format:** JSON over HTTP/HTTPS +* **Authentication:** [Only include if authentication is implemented in the code] + +--- + +## Getting Started + +### Authentication + +[Only include this section if authentication mechanisms are actually implemented in the Ballerina service code] + +--- + +## Core Concepts + +* **[Concept 1]** – [Brief description of key domain objects/concepts] +* **[Concept 2]** – [Brief description of key domain objects/concepts] +* **[Concept 3]** – [Brief description of key domain objects/concepts] + +--- + +## API Reference + +### [Resource Group Name] + +**[Endpoint Description]** +\`[HTTP_METHOD] [/endpoint/path]\` + +[Brief description of what this endpoint does] + +**Example:** + +\`\`\`bash +curl [BASE_URL_FROM_CODE]/[endpoint] \\ + -H "Content-Type: application/json" +\`\`\` + +**Response:** + +\`\`\`json +{ + "example": "response data" +} +\`\`\` + +**Parameters:** +- **Path Parameters:** + - \`param_name\` (type) – Description +- **Query Parameters:** + - \`param_name\` (type) – Description +- **Request Body:** + +\`\`\`json +{ + "field": "value" +} +\`\`\` + +[Repeat for each endpoint grouped logically] + +--- + +## Error Handling + +All error responses follow a standard structure: + +\`\`\`json +{ + "error": "error_code", + "message": "Human readable error message." +} +\`\`\` + +**Error Codes:** + +* \`400 Bad Request\` – Invalid request format. +* \`401 Unauthorized\` – Missing or invalid credentials. +* \`404 Not Found\` – Resource not found. +* \`429 Too Many Requests\` – Rate limit exceeded. +* \`500 Internal Server Error\` – Unexpected server error. + +--- + +## Data Models + +### [ModelName] + +\`\`\`json +{ + "field1": "string", + "field2": 123, + "field3": true, + "field4": { + "nested_field": "value" + } +} +\`\`\` + +**Field Descriptions:** +- \`field1\` (string) – Description of the field +- \`field2\` (number) – Description of the field +- \`field3\` (boolean) – Description of the field + +[Document all key data structures] + +--- + +## Best Practices + +* Use the **development environment** for testing before switching to production. +* Implement **retries with exponential backoff** for transient errors. +* Use **pagination** when retrieving large datasets. +* Store API keys securely; never commit them to version control. +* Handle errors gracefully and provide meaningful feedback to users. + +--- + +Important Guidelines: +1. **Determine Service Name Intelligently**: Analyze the code to infer what the service does rather than using base paths like "/". Look for: + - Service class names and comments + - Resource endpoint patterns (e.g., /pizzas, /orders → "Pizza Shop") + - Data model names (e.g., Pizza, Customer, Order → "Pizza Shop") + - Business logic and functionality + - Variable names and types + - Generate meaningful names like "Pizza Shop", "User Management", "Inventory Management" instead of "/" or generic terms +2. Remove any informal language, emojis, or casual greetings +3. Extract actual endpoint paths, methods, and parameters from the Ballerina code +4. Generate realistic example data based on the types defined in the code - keep examples SHORT and REAL, not comprehensive +5. Include all resource functions as separate endpoint documentation +6. Use proper HTTP status codes based on the service implementation +7. Make examples practical, concise, and realistic - avoid overly complex or lengthy examples +8. Focus on what developers need to know to integrate successfully +9. Group related endpoints logically under sections +10. Use clean, professional language suitable for technical documentation +11. Follow the structure shown above strictly for consistency +12. DO NOT include hardcoded URLs - only include Base URLs if they can be determined from service listeners/configuration in the actual code +13. DO NOT include Authentication section unless authentication is actually implemented in the service +14. Generate ONLY the documentation content - no completion messages or additional text`; +} + +// ============================================== +// MESSAGE CREATION FUNCTIONS +// ============================================== + +export function createDocumentationGenMessages(request: DocumentationGenerationRequest): CoreMessage[] { + const docGenUser1 = createDocumentationGenUser1Message(request); + return [docGenUser1]; +} + +export function createDocumentationGenUser1Message(request: DocumentationGenerationRequest): CoreMessage { + const flattenedProject = flattenProjectToText(request.projectSource); + const typeSchemas = request.openApiSpec ? getExternalTypesAsJsonSchema(request.openApiSpec) : "{}"; + + const prompt = getDocumentationGenUser1Prompt(flattenedProject, typeSchemas, request.serviceName); + + return { + role: "user", + content: prompt, + providerOptions: { + anthropic: { cacheControl: { type: "ephemeral" } }, + }, + }; +} diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/utils.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/utils.ts new file mode 100644 index 00000000000..58a757cb95a --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/documentation/utils.ts @@ -0,0 +1,99 @@ +// 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 { ProjectSource } from "@wso2/ballerina-core"; + +// ============================================== +// UTILITY FUNCTIONS +// ============================================== + +export function extractDocumentationFromResponse(response: string): string { + // For now, return the full response as documentation + // In the future, we might want to extract specific sections or format it + return response.trim(); +} + +export function flattenProjectToText(projectSource: ProjectSource): string { + let flattenedProject = ""; + + const modules = projectSource.projectModules; + if (modules) { + for (const module of modules) { + let moduleSource = ""; + for (const sourceFile of module.sourceFiles) { + moduleSource += `\`\`\`ballerina +# modules/${module.moduleName}/${sourceFile.filePath} + +${sourceFile.content} +\`\`\` + +`; + } + flattenedProject += moduleSource; + } + } + + for (const sourceFile of projectSource.sourceFiles) { + flattenedProject += `\`\`\`ballerina +# ${sourceFile.filePath} + +${sourceFile.content} +\`\`\` + +`; + } + + return flattenedProject; +} + +export function getExternalTypesAsJsonSchema(openApiSpec: string): string { + try { + const externalTypes: Record = {}; + + const openApiSpecObj = JSON.parse(openApiSpec); + const components = openApiSpecObj.components; + + if (components && components.schemas) { + for (const componentName in components.schemas) { + const componentSchema = components.schemas[componentName]; + if (componentSchema && componentSchema['x-ballerina-type'] !== undefined) { + externalTypes[componentName] = componentSchema; + } + } + } + + return JSON.stringify(externalTypes, null, 2); + } catch (error) { + // Return empty object if parsing fails + return "{}"; + } +} + +export function getTypesAsJsonSchema(openApiSpec: string): string { + try { + const openApiSpecObj = JSON.parse(openApiSpec); + const components = openApiSpecObj.components; + + if (components) { + return JSON.stringify(components, null, 2); + } + + return "{}"; + } catch (error) { + // Return empty object if parsing fails + return "{}"; + } +} diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/healthcare/healthcare.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/healthcare/healthcare.ts index df736b7f877..8667d964865 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/healthcare/healthcare.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/healthcare/healthcare.ts @@ -15,7 +15,7 @@ // under the License. import { CoreMessage, generateObject, streamText } from "ai"; -import { getAnthropicClient, ANTHROPIC_HAIKU, ANTHROPIC_SONNET_4 } from "../connection"; +import { getAnthropicClient, ANTHROPIC_HAIKU, ANTHROPIC_SONNET_4, getProviderCacheControl } from "../connection"; import { GenerationType, getRelevantLibrariesAndFunctions } from "../libs/libs"; import { getRewrittenPrompt, populateHistory, transformProjectSource, getErrorMessage } from "../utils"; import { libraryContains } from "../libs/funcs"; @@ -57,6 +57,7 @@ export async function generateHealthcareCodeCore( ).libraries; const historyMessages = populateHistory(params.chatHistory); + const cacheOptions = await getProviderCacheControl(); const allMessages: CoreMessage[] = [ { @@ -66,17 +67,13 @@ export async function generateHealthcareCodeCore( { role: "system", content: getSystemPromptSuffix(LANGLIBS), - providerOptions: { - anthropic: { cacheControl: { type: "ephemeral" } }, - }, + providerOptions: cacheOptions, }, ...historyMessages, { role: "user", content: getUserPrompt(prompt, sourceFiles, params.fileAttachmentContents, packageName, params.operationType), - providerOptions: { - anthropic: { cacheControl: { type: "ephemeral" } }, - }, + providerOptions: cacheOptions, }, ]; diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/libs/libs.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/libs/libs.ts index a0dc70913f6..248e09adf1c 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/libs/libs.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/libs/libs.ts @@ -23,7 +23,7 @@ import { } from "@wso2/ballerina-core"; import { Library } from "./libs_types"; import { selectRequiredFunctions } from "./funcs"; -import { getAnthropicClient, ANTHROPIC_HAIKU } from "../connection"; +import { getAnthropicClient, ANTHROPIC_HAIKU, getProviderCacheControl } from "../connection"; import { langClient } from "../../activator"; import { getGenerationMode } from "../utils"; import { AIPanelAbortController } from "../../../../../src/rpc-managers/ai-panel/utils"; @@ -63,13 +63,12 @@ export async function getSelectedLibraries(prompt: string, generationType: Gener if (allLibraries.length === 0) { return []; } + const cacheOptions = await getProviderCacheControl(); const messages: CoreMessage[] = [ { role: "system", content: getSystemPrompt(allLibraries), - providerOptions: { - anthropic: { cacheControl: { type: "ephemeral" } }, - }, + providerOptions: cacheOptions, }, { role: "user", diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/openapi/openapi.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/openapi/openapi.ts index 6f6cf2cb37c..a85e64399dd 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/openapi/openapi.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/openapi/openapi.ts @@ -16,7 +16,7 @@ import { GenerateOpenAPIRequest, Command } from "@wso2/ballerina-core"; import { streamText } from "ai"; -import { getAnthropicClient, ANTHROPIC_HAIKU } from "../connection"; +import { getAnthropicClient, ANTHROPIC_HAIKU, getProviderCacheControl } from "../connection"; import { getErrorMessage, populateHistory } from "../utils"; import { CopilotEventHandler, createWebviewEventHandler } from "../event"; import { AIPanelAbortController } from "../../../../../src/rpc-managers/ai-panel/utils"; @@ -28,6 +28,7 @@ export async function generateOpenAPISpecCore( ): Promise { // Populate chat history and add user message const historyMessages = populateHistory(params.chatHistory); + const cacheOptions = await getProviderCacheControl(); const { fullStream } = streamText({ model: await getAnthropicClient(ANTHROPIC_HAIKU), maxTokens: 8192, @@ -36,9 +37,7 @@ export async function generateOpenAPISpecCore( { role: "system", content: getSystemPrompt(), - providerOptions: { - anthropic: { cacheControl: { type: "ephemeral" } }, - }, + providerOptions: cacheOptions, }, ...historyMessages, { diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/service/test/test.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/service/test/test.ts index 16eb1389328..7df7675b270 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/service/test/test.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/service/test/test.ts @@ -16,7 +16,7 @@ import { Command } from "@wso2/ballerina-core"; import { generateText, CoreMessage } from "ai"; -import { getAnthropicClient } from "../connection"; +import { getAnthropicClient, getProviderCacheControl } from "../connection"; import { getServiceTestGenerationSystemPrompt, getServiceTestDiagnosticsSystemPrompt, @@ -103,6 +103,19 @@ async function getStreamedTestResponse(request: TestGenerationRequest1): Promise throw new Error(`Unsupported target type specified: ${request.targetType}. Please use 'service' or 'function'.`); } + // Apply provider-aware cache control to messages that have cacheControl + const cacheOptions = await getProviderCacheControl(); + messages = messages.map(message => { + if (message.providerOptions && + (message.providerOptions as any).anthropic?.cacheControl) { + return { + ...message, + providerOptions: cacheOptions + }; + } + return message; + }); + const { text } = await generateText({ model: await getAnthropicClient("claude-sonnet-4-20250514"), maxTokens: 16384, diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/testGenerator.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/testGenerator.ts index a74a3da6287..059ee5c5a12 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/testGenerator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/testGenerator.ts @@ -25,7 +25,7 @@ import { langClient } from './activator'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { writeBallerinaFileDidOpen } from '../../utils/modification'; +import { writeBallerinaFileDidOpenTemp } from '../../utils/modification'; import { closeAllBallerinaFiles } from './utils'; import { generateTestFromLLM, TestGenerationRequest1 } from './service/test/test'; @@ -230,7 +230,7 @@ export async function getDiagnostics( fs.mkdirSync(tempTestFolderPath, { recursive: true }); } const tempTestFilePath = path.join(tempTestFolderPath, 'test.bal'); - writeBallerinaFileDidOpen(tempTestFilePath, generatedTestSource.testSource); + writeBallerinaFileDidOpenTemp(tempTestFilePath, generatedTestSource.testSource); const diagnosticsResult = await langClient.getDiagnostics({ documentIdentifier: { uri: Uri.file(tempTestFilePath).toString() } }); await closeAllBallerinaFiles(tempDir); diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts index b608b4ed2ff..60ba64e058c 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts @@ -30,13 +30,13 @@ import { BallerinaProject } from '@wso2/ballerina-core'; import { BallerinaExtension } from 'src/core'; 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; +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; // This refers to old backend before FE Migration. We need to eventually remove this. -export const OLD_BACKEND_URL : string = BACKEND_URL + "/v2.0"; +export const OLD_BACKEND_URL: string = BACKEND_URL + "/v2.0"; export async function closeAllBallerinaFiles(dirPath: string): Promise { // Check if the directory exists @@ -273,3 +273,135 @@ export async function isBallerinaProjectAsync(rootPath: string): Promise { + if (dirPath === null) { + return null; + } + + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders) { + return null; + } + + // Check if the directory is within any of the workspace folders + const workspaceFolder = workspaceFolders.find(folder => dirPath.startsWith(folder.uri.fsPath)); + if (!workspaceFolder) { + return null; + } + + let currentDir = dirPath; + + while (currentDir.startsWith(workspaceFolder.uri.fsPath)) { + const ballerinaTomlPath = path.join(currentDir, 'Ballerina.toml'); + if (fs.existsSync(ballerinaTomlPath)) { + return currentDir; + } + currentDir = path.dirname(currentDir); + } + + return null; +} + +/** + * Gets the project source including all .bal files and modules + */ +export async function getProjectSource(dirPath: string): Promise { + const projectRoot = await findBallerinaProjectRoot(dirPath); + + if (!projectRoot) { + return null; + } + + const projectSource: ProjectSource = { + sourceFiles: [], + projectTests: [], + projectModules: [], + projectName: "" + }; + + // Read root-level .bal files + const rootFiles = fs.readdirSync(projectRoot); + for (const file of rootFiles) { + if (file.endsWith('.bal')) { + const filePath = path.join(projectRoot, file); + const content = await fs.promises.readFile(filePath, 'utf-8'); + projectSource.sourceFiles.push({ filePath, content }); + } + } + + // Read modules + const modulesDir = path.join(projectRoot, 'modules'); + if (fs.existsSync(modulesDir)) { + const modules = fs.readdirSync(modulesDir, { withFileTypes: true }); + for (const moduleDir of modules) { + if (moduleDir.isDirectory()) { + const projectModule: ProjectModule = { + moduleName: moduleDir.name, + sourceFiles: [], + isGenerated: false, + }; + + const moduleFiles = fs.readdirSync(path.join(modulesDir, moduleDir.name)); + for (const file of moduleFiles) { + if (file.endsWith('.bal')) { + const filePath = path.join(modulesDir, moduleDir.name, file); + const content = await fs.promises.readFile(filePath, 'utf-8'); + projectModule.sourceFiles.push({ filePath, content }); + } + } + + projectSource.projectModules.push(projectModule); + } + } + } + + return projectSource; +} + +/** + * Gets the project source including test files + */ +export async function getProjectSourceWithTests(dirPath: string): Promise { + const projectRoot = await findBallerinaProjectRoot(dirPath); + + if (!projectRoot) { + return null; + } + + const projectSourceWithTests: ProjectSource = await getProjectSource(dirPath); + + // Read tests + const testsDir = path.join(projectRoot, 'tests'); + if (fs.existsSync(testsDir)) { + const testFiles = fs.readdirSync(testsDir); + for (const file of testFiles) { + if (file.endsWith('.bal') || file.endsWith('Config.toml')) { + const filePath = path.join(testsDir, file); + const content = await fs.promises.readFile(filePath, 'utf-8'); + projectSourceWithTests.projectTests.push({ filePath, content }); + } + } + } + + return projectSourceWithTests; +} + +/** + * Gets the OpenAPI specification for a given Ballerina service file + */ +export async function getOpenAPISpecification(documentFilePath: string): Promise { + const response = await langClient.convertToOpenAPI({ documentFilePath, enableBalExtension: true }) as OpenAPISpec; + if (response.error) { + throw new Error(response.error); + } + return JSON.stringify(response.content[0].spec); +} diff --git a/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts b/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts index 156b22c07b6..5585a32e6d3 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts @@ -15,7 +15,7 @@ * specific language governing permissions and limitations * under the License. */ -import { commands, Uri } from "vscode"; +import { commands, Uri, workspace, window, ConfigurationTarget } from "vscode"; import { BI_COMMANDS, BIDeleteByComponentInfoRequest, @@ -28,6 +28,7 @@ import { } from "@wso2/ballerina-core"; import { BallerinaExtension } from "../../core"; import { openView } from "../../stateMachine"; +import { ENABLE_DEBUG_LOG, ENABLE_TRACE_LOG, TRACE_SERVER } from "../../core/preferences"; import { prepareAndGenerateConfig } from "../config-generator/configGenerator"; import { StateMachine } from "../../stateMachine"; import { BiDiagramRpcManager } from "../../rpc-managers/bi-diagram/rpc-manager"; @@ -37,6 +38,8 @@ import { isPositionEqual, isPositionWithinDeletedComponent } from "../../utils/h import { startDebugging } from "../editor-support/activator"; const FOCUS_DEBUG_CONSOLE_COMMAND = 'workbench.debug.action.focusRepl'; +const TRACE_SERVER_OFF = "off"; +const TRACE_SERVER_VERBOSE = "verbose"; export function activate(context: BallerinaExtension) { commands.registerCommand(BI_COMMANDS.BI_RUN_PROJECT, () => { @@ -52,6 +55,10 @@ export function activate(context: BallerinaExtension) { openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.AddConnectionWizard }); }); + commands.registerCommand(BI_COMMANDS.ADD_CUSTOM_CONNECTOR, () => { + openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.AddCustomConnector }); + }); + commands.registerCommand(BI_COMMANDS.ADD_ENTRY_POINT, () => { openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.BIComponentView }); }); @@ -74,7 +81,7 @@ export function activate(context: BallerinaExtension) { commands.registerCommand(BI_COMMANDS.VIEW_CONFIGURATION, () => { openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.ViewConfigVariables }); - }); + }); commands.registerCommand(BI_COMMANDS.SHOW_OVERVIEW, () => { openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.Overview }); @@ -95,9 +102,11 @@ export function activate(context: BallerinaExtension) { commands.registerCommand(BI_COMMANDS.SWITCH_PROJECT, async () => { // Hack to switch the project. This will reload the window and prompt the user to select the project. // This is a temporary solution until we provide the support for multi root workspaces. - commands.executeCommand('workbench.action.reloadWindow'); + StateMachine.sendEvent("SWITCH_PROJECT" as any); }); + commands.registerCommand(BI_COMMANDS.TOGGLE_TRACE_LOGS, toggleTraceLogs); + commands.registerCommand(BI_COMMANDS.DELETE_COMPONENT, async (item: any) => { console.log(">>> delete component", item); @@ -111,7 +120,7 @@ export function activate(context: BallerinaExtension) { }); //HACK: Open all Ballerina files in the project - openAllBallerinaFiles(context); + // openAllBallerinaFiles(context); } function openAllBallerinaFiles(context: BallerinaExtension) { @@ -295,3 +304,25 @@ function hasNoComponentsOpenInDiagram() { function isFilePathsEqual(filePath1: string, filePath2: string) { return path.normalize(filePath1) === path.normalize(filePath2); } + +function toggleTraceLogs() { + const config = workspace.getConfiguration(); + + const currentTraceServer = config.get(TRACE_SERVER); + const currentDebugLog = config.get(ENABLE_DEBUG_LOG); + const currentTraceLog = config.get(ENABLE_TRACE_LOG); + + const isTraceEnabled = currentTraceServer === TRACE_SERVER_VERBOSE && currentDebugLog && currentTraceLog; + + if (isTraceEnabled) { + config.update(TRACE_SERVER, TRACE_SERVER_OFF, ConfigurationTarget.Global); + config.update(ENABLE_DEBUG_LOG, false, ConfigurationTarget.Global); + config.update(ENABLE_TRACE_LOG, false, ConfigurationTarget.Global); + window.showInformationMessage('BI extension trace logs disabled'); + } else { + config.update(TRACE_SERVER, TRACE_SERVER_VERBOSE, ConfigurationTarget.Global); + config.update(ENABLE_DEBUG_LOG, true, ConfigurationTarget.Global); + config.update(ENABLE_TRACE_LOG, true, ConfigurationTarget.Global); + window.showInformationMessage('BI extension trace logs enabled'); + } +} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/inline-utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/inline-utils.ts deleted file mode 100644 index f9e9e88d7d1..00000000000 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/inline-utils.ts +++ /dev/null @@ -1,416 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. - * - * This software is the property of WSO2 LLC. and its suppliers, if any. - * Dissemination of any information or reproduction of any material contained - * herein in any form is strictly forbidden, unless permitted by WSO2 expressly. - * You may not alter or remove any copyright or other notice from copies of this content. - */ - -import { Attachment, ExpandedDMModel, FormField, InlineDataMapperModelResponse, InputCategory, IOType, Mapping, MappingElement, TypeKind } from "@wso2/ballerina-core"; -import { generateBallerinaCode, mappingFileInlineDataMapperModel, navigateTypeInfo } from "./utils"; -import { DatamapperResponse } from "../../../src/features/ai/service/datamapper/types"; -import { generateInlineAutoMappings } from "../../../src/features/ai/service/datamapper/inline_datamapper"; -import { FieldMetadata, ParameterDefinitions, ParameterField, ParameterMetadata } from "./types"; - -function transformIOType(input: IOType): FormField { - const name = input.variableName || extractNameFromId(input.id); - - let typeName: string; - if (input.kind && input.typeName && input.kind !== input.typeName && input.category) { - typeName = input.kind; - } else if (!input.typeName) { - typeName = input.kind || "unknown"; - } else { - typeName = input.typeName; - } - - const baseField = { - id: input.id, - name, - typeName, - optional: input.optional || false - }; - - // Handle arrays - if (input.kind === "array" && input.member) { - const memberTransformed = transformIOType(input.member) as FormField; - const { name, ...memberWithoutName } = memberTransformed; - - return { - ...baseField, - typeName: "array", - memberType: memberWithoutName as FormField - } as FormField; - } - - // Handle records - if (input.kind === "record" && input.fields) { - const recordField: FormField = { - ...baseField, - typeName: "record", - fields: input.fields.map(transformIOType) as FormField[] - }; - - if ( - input.typeName && - input.kind !== input.typeName && - !input.category - ) { - recordField.typeInfo = { - orgName: "", - moduleName: "", - name: input.typeName - }; - } - - return recordField; - } - - // Handle primitive types - const primitiveField: FormField = { ...baseField }; - - // Add typeInfo if conditions are met - if ( - input.typeName && - input.kind !== input.typeName && - !input.category - ) { - primitiveField.typeInfo = { - orgName: "", - moduleName: "", - name: input.typeName - }; - } - - return primitiveField; -} - -function extractNameFromId(id: string): string { - const parts = id.split('.').filter(part => !/^\d+$/.test(part)); - return parts[parts.length - 1]; -} - -function transformInputs(inputs: IOType[]): { - constants: Record; - configurables: Record; - variables: Record; - parameters: ParameterField[]; - parameterFields: { [parameterName: string]: FormField[] }; -} { - const constants: Record = {}; - const configurables: Record = {}; - const variables: Record = {}; - const parameters: ParameterField[] = []; - const parameterFields: { [parameterName: string]: FormField[] } = {}; - - inputs.forEach((input) => { - // Helper function to create ParameterField - const createParameterField = (input: IOType): ParameterField => { - const name = input.id; - let typeName: string; - - if (input.kind !== input.typeName) { - typeName = input.typeName; - } else if (!input.typeName) { - typeName = input.kind || "unknown"; - } else { - typeName = input.typeName; - } - - // Determine if it's an array type - const isArrayType = input.kind === TypeKind.Array; - - // Determine the type string - let type: string; - if (isArrayType) { - // If it's an array, get the member type and append [] - if (input.member) { - const memberTypeName = input.member.typeName || input.member.kind || "unknown"; - type = `${memberTypeName}[]`; - } else { - type = `${typeName}[]`; - } - } else { - type = input.kind; - } - - return { - isArrayType, - parameterName: name, - parameterType: typeName, - type - }; - }; - - const createFieldConfig = (input: IOType): FieldMetadata => { - if (!input.typeName) { - throw new Error("TypeName is missing"); - } - return { - typeName: input.kind || "unknown", - type: input.kind || "unknown", - typeInstance: input.id, - nullable: false, // default to false TODO: Update if needed - optional: false // default to false TODO: Update if needed - }; - }; - - // Handle different categories - if (input.category === InputCategory.Constant) { - constants[input.id] = createFieldConfig(input); - return; - } - - if (input.category === InputCategory.Configurable) { - configurables[input.id] = createFieldConfig(input); - return; - } - - if (input.category === InputCategory.Variable) { - variables[input.id] = createFieldConfig(input); - return; - } - - if (input.category === InputCategory.Parameter) { - const parameterField = createParameterField(input); - parameters.push(parameterField); - - const parameterName = input.id; - if (input.fields) { - parameterFields[parameterName] = input.fields.map(transformIOType); - } else { - parameterFields[parameterName] = [transformIOType(input)]; - } - } - }); - - return { constants, configurables, variables, parameters, parameterFields }; -} - -function transformOutput(output: IOType): FormField[] { - if (output.fields) { - return output.fields.map(transformIOType); - } - return [transformIOType(output)]; -} - -// Utility function to check if a value is null or undefined -function isNullOrUndefined(value: any): boolean { - return value === null || value === undefined; -} - -// Clean IOType by removing null/undefined fields and filtering arrays -function cleanIOType(ioType: IOType | null | undefined): IOType | null { - if (isNullOrUndefined(ioType)) { - return null; - } - - const cleaned = ioType; - - // Clean fields array - remove null/undefined elements and recursively clean - if (ioType.fields && Array.isArray(ioType.fields)) { - const cleanedFields = ioType.fields - .filter(field => !isNullOrUndefined(field)) - .map(field => cleanIOType(field)) - .filter(field => field !== null) as IOType[]; - - if (cleanedFields.length > 0) { - cleaned.fields = cleanedFields; - } - } - - // Clean member recursively - if (ioType.member && !isNullOrUndefined(ioType.member)) { - const cleanedMember = cleanIOType(ioType.member); - if (cleanedMember !== null) { - cleaned.member = cleanedMember; - } - } - - // Clean members array - remove null/undefined elements - if (ioType.members && Array.isArray(ioType.members)) { - const cleanedMembers = ioType.members.filter(member => - !isNullOrUndefined(member) && - !isNullOrUndefined(member.id) && - !isNullOrUndefined(member.typeName) - ); - - if (cleanedMembers.length > 0) { - cleaned.members = cleanedMembers; - } - } - - return cleaned; -} - -// Clean ExpandedDMModel by removing null fields and cleaning nested structures -function cleanExpandedDMModel(model: ExpandedDMModel): ExpandedDMModel { - const cleaned = model as ExpandedDMModel; - - // Clean inputs array - remove null/undefined elements - if (model.inputs && Array.isArray(model.inputs)) { - const cleanedInputs = model.inputs - .filter(input => !isNullOrUndefined(input)) - .map(input => cleanIOType(input)) - .filter(input => input !== null) as IOType[]; - - cleaned.inputs = cleanedInputs; - } - - // Clean output - if (model.output && !isNullOrUndefined(model.output)) { - const cleanedOutput = cleanIOType(model.output); - if (cleanedOutput !== null) { - cleaned.output = cleanedOutput; - } - } - - // Clean subMappings array if it exists - if (model.subMappings && Array.isArray(model.subMappings)) { - const cleanedSubMappings = model.subMappings - .filter(subMapping => !isNullOrUndefined(subMapping)) - .map(subMapping => cleanIOType(subMapping)) - .filter(subMapping => subMapping !== null) as IOType[]; - - if (cleanedSubMappings.length > 0) { - cleaned.subMappings = cleanedSubMappings; - } - } - - // Clean mappings array - remove null/undefined elements - if (model.mappings && Array.isArray(model.mappings)) { - const cleanedMappings = model.mappings.filter(mapping => - !isNullOrUndefined(mapping) && - !isNullOrUndefined(mapping.output) && - !isNullOrUndefined(mapping.expression) - ); - - // Also clean inputs array within each mapping - cleanedMappings.forEach(mapping => { - if (mapping.inputs && Array.isArray(mapping.inputs)) { - mapping.inputs = mapping.inputs.filter(input => !isNullOrUndefined(input)); - } - }); - - cleaned.mappings = cleanedMappings; - } - - // Include query if it exists and is not null - if (model.query && !isNullOrUndefined(model.query)) { - cleaned.query = model.query; - } - - return cleaned; -} - -// Main function to clean the entire InlineDataMapperModelResponse -function cleanInlineDataMapperModelResponse( - response: ExpandedDMModel -): InlineDataMapperModelResponse { - if (!response) { - throw new Error("Invalid response: missing mappingsModel"); - } - - const cleanedResponse: InlineDataMapperModelResponse = { - mappingsModel: cleanExpandedDMModel(response as ExpandedDMModel) - }; - - return cleanedResponse; -} - -function transformCodeObjectToMappings(codeObject: any, request: InlineDataMapperModelResponse): Mapping[] { - const mappings: Mapping[] = []; - - // Get the output variable name from the request - const { output: mappingOutput } = request.mappingsModel as ExpandedDMModel; - const outputVariableName = mappingOutput.variableName || extractNameFromId(mappingOutput.id); - - // Iterate through each property in codeObject - Object.keys(codeObject).forEach(key => { - const mapping: Mapping = { - output: `${outputVariableName}.${key}`, - expression: codeObject[key] - }; - mappings.push(mapping); - }); - - return mappings; -} - -export async function getInlineParamDefinitions( - inlineDataMapperResponse: InlineDataMapperModelResponse -): Promise { - const inputs: { [key: string]: any } = {}; - const inputMetadata: { [key: string]: any } = {}; - - const { inputs: mappingInputs, output: mappingOutput } = inlineDataMapperResponse.mappingsModel as ExpandedDMModel; - const transformedInputs = transformInputs(mappingInputs); - const transformedOutputs = transformOutput(mappingOutput); - - for (const parameter of transformedInputs.parameters) { - const inputDefinition = navigateTypeInfo(transformedInputs.parameterFields[parameter.parameterName], false); - - inputs[parameter.parameterName] = inputDefinition.recordFields; - inputMetadata[parameter.parameterName] = { - "isArrayType": parameter.isArrayType, - "parameterName": parameter.parameterName, - "parameterType": parameter.parameterType, - "type": parameter.type, - "fields": inputDefinition.recordFieldsMetadata - }; - } - - const outputDefinition = navigateTypeInfo(transformedOutputs, false); - const output = { ...outputDefinition.recordFields }; - const outputMetadata = { ...outputDefinition.recordFieldsMetadata }; - - return { - parameterMetadata: { - inputs, - output, - inputMetadata, - outputMetadata, - constants: transformedInputs.constants, - configurables: transformedInputs.configurables, - variables: transformedInputs.variables - }, - errorStatus: false - }; -} - -async function sendInlineDatamapperRequest(inlineDataMapperResponse: InlineDataMapperModelResponse): Promise { - const response: DatamapperResponse = await generateInlineAutoMappings(inlineDataMapperResponse); - return response; -} - -async function getInlineDatamapperCode(inlineDataMapperResponse: InlineDataMapperModelResponse, parameterDefinitions: ParameterMetadata): Promise> { - let nestedKeyArray: string[] = []; - try { - let response: DatamapperResponse = await sendInlineDatamapperRequest(inlineDataMapperResponse); - let intermediateMapping = response.mappings; - let finalCode = await generateBallerinaCode(intermediateMapping, parameterDefinitions, "", nestedKeyArray); - return finalCode; - } catch (error) { - console.error(error); - throw error; - } -} - -export async function processInlineMappings( - request: ExpandedDMModel, - file?: Attachment -): Promise { - let inlineDataMapperResponse = cleanInlineDataMapperModelResponse(request); - const result = await getInlineParamDefinitions(inlineDataMapperResponse); - const parameterDefinitions = (result as ParameterDefinitions).parameterMetadata; - - if (file) { - const mappedResult = await mappingFileInlineDataMapperModel(file, inlineDataMapperResponse); - inlineDataMapperResponse = mappedResult as InlineDataMapperModelResponse; - } - - const codeObject = await getInlineDatamapperCode(inlineDataMapperResponse, parameterDefinitions); - const mappings: Mapping[] = transformCodeObjectToMappings(codeObject, inlineDataMapperResponse); - return { mappings }; -} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/repair-utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/repair-utils.ts index 9a1a9c38570..4dd24747114 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/repair-utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/repair-utils.ts @@ -21,7 +21,7 @@ import { ExtendedLangClient } from "../../core"; import { Uri, workspace } from "vscode"; import { TextDocumentEdit } from "vscode-languageserver-types"; import { fileURLToPath } from "url"; -import { modifyFileContent } from "../../utils/modification"; +import { writeBallerinaFileDidOpenTemp } from "../../utils/modification"; export async function attemptRepairProject(langClient: ExtendedLangClient, tempDir: string): Promise { // check project diagnostics @@ -166,7 +166,7 @@ export async function addMissingImports(diagnosticsResult: Diagnostics[], langCl // Update file content const { source } = syntaxTree as SyntaxTree; const absolutePath = fileURLToPath(fielUri); - await modifyFileContent({ filePath: absolutePath, content: source, updateViewFlag: false }); + writeBallerinaFileDidOpenTemp(absolutePath, source); if (astModifications.length > 0) { projectModified = true; } @@ -214,7 +214,7 @@ export async function removeUnusedImports(diagnosticsResult: Diagnostics[], lang // Update file content const { source } = syntaxTree as SyntaxTree; const absolutePath = fileURLToPath(fielUri); - await modifyFileContent({ filePath: absolutePath, content: source, updateViewFlag: false }); + writeBallerinaFileDidOpenTemp(absolutePath, source); projectModified = true; } return projectModified; diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts index 132045a020a..8404ed51eb1 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts @@ -21,6 +21,9 @@ import { abortAIGeneration, abortTestGeneration, addChatSummary, + addCodeSegmentToWorkspace, + addFilesToProject, + AddFilesToProjectRequest, addInlineCodeSegmentToWorkspace, addToProject, AddToProjectRequest, @@ -29,19 +32,23 @@ import { checkSyntaxError, clearInitialPrompt, CodeSegment, + createTempFileAndGenerateMetadata, + CreateTempFileRequest, createTestDirecoryIfNotExists, + DatamapperModelContext, + DataMapperModelResponse, deleteFromProject, DeleteFromProjectRequest, DeveloperDocument, + DocGenerationRequest, fetchData, FetchDataRequest, generateCode, GenerateCodeRequest, + generateDataMapperModel, generateFunctionTests, generateHealthcareCode, generateMappings, - GenerateMappingsFromRecordRequest, - GenerateMappingsRequest, generateOpenAPI, GenerateOpenAPIRequest, generateTestPlan, @@ -57,10 +64,9 @@ import { getFromDocumentation, getFromFile, GetFromFileRequest, + getGeneratedDocumentation, getGeneratedTests, getLoginMethod, - getMappingsFromModel, - getMappingsFromRecord, getModuleDirectory, GetModuleDirParams, getProjectUuid, @@ -81,7 +87,7 @@ import { MetadataWithAttachments, notifyAIMappings, NotifyAIMappingsRequest, - openInlineMappingChatWindow, + openAIMappingChatWindow, postProcess, PostProcessRequest, ProjectSource, @@ -116,18 +122,21 @@ export function registerAiPanelRpcHandlers(messenger: Messenger) { messenger.onRequest(getDefaultPrompt, () => rpcManger.getDefaultPrompt()); messenger.onRequest(getAIMachineSnapshot, () => rpcManger.getAIMachineSnapshot()); messenger.onRequest(fetchData, (args: FetchDataRequest) => rpcManger.fetchData(args)); - messenger.onNotification(addToProject, (args: AddToProjectRequest) => rpcManger.addToProject(args)); + messenger.onRequest(addToProject, (args: AddToProjectRequest) => rpcManger.addToProject(args)); messenger.onRequest(getFromFile, (args: GetFromFileRequest) => rpcManger.getFromFile(args)); messenger.onRequest(getFileExists, (args: GetFromFileRequest) => rpcManger.getFileExists(args)); messenger.onNotification(deleteFromProject, (args: DeleteFromProjectRequest) => rpcManger.deleteFromProject(args)); - messenger.onRequest(generateMappings, (args: GenerateMappingsRequest) => rpcManger.generateMappings(args)); messenger.onRequest(notifyAIMappings, (args: NotifyAIMappingsRequest) => rpcManger.notifyAIMappings(args)); messenger.onRequest(stopAIMappings, () => rpcManger.stopAIMappings()); messenger.onRequest(getShadowDiagnostics, (args: ProjectSource) => rpcManger.getShadowDiagnostics(args)); messenger.onRequest(checkSyntaxError, (args: ProjectSource) => rpcManger.checkSyntaxError(args)); messenger.onNotification(clearInitialPrompt, () => rpcManger.clearInitialPrompt()); - messenger.onNotification(openInlineMappingChatWindow, () => rpcManger.openInlineMappingChatWindow()); - messenger.onRequest(getMappingsFromModel, (args: MetadataWithAttachments) => rpcManger.getMappingsFromModel(args)); + messenger.onNotification(openAIMappingChatWindow, (args: DataMapperModelResponse) => rpcManger.openAIMappingChatWindow(args)); + messenger.onRequest(generateDataMapperModel, (args: DatamapperModelContext) => rpcManger.generateDataMapperModel(args)); + messenger.onRequest(getTypesFromRecord, (args: GenerateTypesFromRecordRequest) => rpcManger.getTypesFromRecord(args)); + messenger.onRequest(createTempFileAndGenerateMetadata, (args: CreateTempFileRequest) => rpcManger.createTempFileAndGenerateMetadata(args)); + messenger.onRequest(generateMappings, (args: MetadataWithAttachments) => rpcManger.generateMappings(args)); + messenger.onRequest(addCodeSegmentToWorkspace, (args: CodeSegment) => rpcManger.addCodeSegmentToWorkspace(args)); messenger.onNotification(addInlineCodeSegmentToWorkspace, (args: CodeSegment) => rpcManger.addInlineCodeSegmentToWorkspace(args)); messenger.onRequest(getGeneratedTests, (args: TestGenerationRequest) => rpcManger.getGeneratedTests(args)); messenger.onRequest(getTestDiagnostics, (args: TestGenerationResponse) => rpcManger.getTestDiagnostics(args)); @@ -136,8 +145,6 @@ export function registerAiPanelRpcHandlers(messenger: Messenger) { messenger.onRequest(getServiceNames, () => rpcManger.getServiceNames()); messenger.onRequest(getResourceMethodAndPaths, () => rpcManger.getResourceMethodAndPaths()); messenger.onNotification(abortTestGeneration, () => rpcManger.abortTestGeneration()); - messenger.onRequest(getMappingsFromRecord, (args: GenerateMappingsFromRecordRequest) => rpcManger.getMappingsFromRecord(args)); - messenger.onRequest(getTypesFromRecord, (args: GenerateTypesFromRecordRequest) => rpcManger.getTypesFromRecord(args)); messenger.onNotification(applyDoOnFailBlocks, () => rpcManger.applyDoOnFailBlocks()); messenger.onRequest(postProcess, (args: PostProcessRequest) => rpcManger.postProcess(args)); messenger.onRequest(getActiveFile, () => rpcManger.getActiveFile()); @@ -167,4 +174,6 @@ export function registerAiPanelRpcHandlers(messenger: Messenger) { messenger.onNotification(generateFunctionTests, (args: TestGeneratorIntermediaryState) => rpcManger.generateFunctionTests(args)); messenger.onNotification(generateHealthcareCode, (args: GenerateCodeRequest) => rpcManger.generateHealthcareCode(args)); messenger.onNotification(abortAIGeneration, () => rpcManger.abortAIGeneration()); + messenger.onNotification(getGeneratedDocumentation, (args: DocGenerationRequest) => rpcManger.getGeneratedDocumentation(args)); + messenger.onRequest(addFilesToProject, (args: AddFilesToProjectRequest) => rpcManger.addFilesToProject(args)); } 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 22e657304e9..ace851f24d5 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 @@ -22,31 +22,35 @@ import { AIMachineSnapshot, AIPanelAPI, AIPanelPrompt, + AddFilesToProjectRequest, AddToProjectRequest, + AllDataMapperSourceRequest, BIModuleNodesRequest, BISourceCodeResponse, CodeSegment, Command, + CreateTempFileRequest, + DMModel, + DataMapperModelResponse, + DatamapperModelContext, DeleteFromProjectRequest, DeveloperDocument, DiagnosticEntry, Diagnostics, + DocGenerationRequest, ExpandedDMModel, + ExtendedDataMapperMetadata, FetchDataRequest, FetchDataResponse, GenerateCodeRequest, - GenerateMappingFromRecordResponse, - GenerateMappingsFromRecordRequest, - GenerateMappingsRequest, GenerateMappingsResponse, GenerateOpenAPIRequest, GenerateTypesFromRecordRequest, GenerateTypesFromRecordResponse, GetFromFileRequest, GetModuleDirParams, - InlineAllDataMapperSourceRequest, - InlineDataMapperModelResponse, LLMDiagnostics, + LinePosition, LoginMethod, MappingElement, MetadataWithAttachments, @@ -72,7 +76,6 @@ import { TestPlanGenerationRequest, TextEdit } from "@wso2/ballerina-core"; -import { STKindChecker, STNode } from "@wso2/syntax-tree"; import * as crypto from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; @@ -80,15 +83,17 @@ import path from "path"; import { parse } from 'toml'; import { Uri, commands, window, workspace } from 'vscode'; +import { FunctionDefinition, ModulePart, STKindChecker, STNode } from "@wso2/syntax-tree"; import { isNumber } from "lodash"; import { URI } from "vscode-uri"; +import { NOT_SUPPORTED } from "../../../src/core/extended-language-client"; import { fetchWithAuth } from "../../../src/features/ai/service/connection"; import { generateOpenAPISpec } from "../../../src/features/ai/service/openapi/openapi"; import { AIStateMachine } from "../../../src/views/ai-panel/aiMachine"; import { extension } from "../../BalExtensionContext"; -import { NOT_SUPPORTED } from "../../core"; -import { generateDataMapping, generateTypeCreation } from "../../features/ai/dataMapping"; +import { createTempDataMappingFile, generateTypeCreation } from "../../features/ai/dataMapping"; import { generateCode, triggerGeneratedCodeRepair } from "../../features/ai/service/code/code"; +import { generateDocumentationForService } from "../../features/ai/service/documentation/doc_generator"; import { generateHealthcareCode } from "../../features/ai/service/healthcare/healthcare"; import { selectRequiredFunctions } from "../../features/ai/service/libs/funcs"; import { GenerationType, getSelectedLibraries } from "../../features/ai/service/libs/libs"; @@ -100,10 +105,9 @@ import { OLD_BACKEND_URL, closeAllBallerinaFiles } from "../../features/ai/utils import { getLLMDiagnosticArrayAsString, handleChatSummaryFailure } from "../../features/natural-programming/utils"; import { StateMachine, updateView } from "../../stateMachine"; import { getAccessToken, getLoginMethod, getRefreshedAccessToken, loginGithubCopilot } from "../../utils/ai/auth"; -import { modifyFileContent, writeBallerinaFileDidOpen } from "../../utils/modification"; +import { modifyFileContent, writeBallerinaFileDidOpen, writeBallerinaFileDidOpenTemp } from "../../utils/modification"; import { updateSourceCode } from "../../utils/source-utils"; -import { PARSING_ERROR, UNKNOWN_ERROR } from "../../views/ai-panel/errorCodes"; -import { refreshDataMapper, updateAndRefreshDataMapper } from "../inline-data-mapper/utils"; +import { expandDMModel, refreshDataMapper, updateAndRefreshDataMapper } from "../data-mapper/utils"; import { DEVELOPMENT_DOCUMENT, NATURAL_PROGRAMMING_DIR_NAME, REQUIREMENT_DOC_PREFIX, @@ -111,9 +115,8 @@ import { REQUIREMENT_TEXT_DOCUMENT, REQ_KEY, TEST_DIR_NAME } from "./constants"; -import { processInlineMappings } from "./inline-utils"; import { attemptRepairProject, checkProjectDiagnostics } from "./repair-utils"; -import { AIPanelAbortController, cleanDiagnosticMessages, handleStop, isErrorCode, requirementsSpecification, searchDocumentation } from "./utils"; +import { AIPanelAbortController, addToIntegration, cleanDiagnosticMessages, handleStop, isErrorCode, processMappings, requirementsSpecification, searchDocumentation } from "./utils"; import { fetchData } from "./utils/fetch-data-utils"; export class AiPanelRpcManager implements AIPanelAPI { @@ -198,7 +201,7 @@ export class AiPanelRpcManager implements AIPanelAPI { }; } - async addToProject(req: AddToProjectRequest): Promise { + async addToProject(req: AddToProjectRequest): Promise { const workspaceFolders = workspace.workspaceFolders; if (!workspaceFolders) { @@ -224,6 +227,7 @@ export class AiPanelRpcManager implements AIPanelAPI { updateView(); const datamapperMetadata = StateMachine.context().dataMapperMetadata; await refreshDataMapper(balFilePath, datamapperMetadata.codeData, datamapperMetadata.name); + return true; } async getFromFile(req: GetFromFileRequest): Promise { @@ -291,57 +295,6 @@ export class AiPanelRpcManager implements AIPanelAPI { return false; } - async generateMappings(params: GenerateMappingsRequest): Promise { - let { filePath, position } = params; - - const fileUri = Uri.file(filePath).toString(); - - const fnSTByRange = await StateMachine.langClient().getSTByRange( - { - lineRange: { - start: { - line: position.startLine, - character: position.startColumn - }, - end: { - line: position.endLine, - character: position.endColumn - } - }, - documentIdentifier: { - uri: fileUri - } - } - ); - - if (fnSTByRange === NOT_SUPPORTED) { - return { error: UNKNOWN_ERROR }; - } - - const { - parseSuccess: fnSTByRangeParseSuccess, - syntaxTree: fnSTByRangeSyntaxTree, - source: oldSource - } = fnSTByRange as SyntaxTree; - const fnSt = fnSTByRangeSyntaxTree as STNode; - - if (!fnSTByRangeParseSuccess || !STKindChecker.isFunctionDefinition(fnSt)) { - return { error: PARSING_ERROR }; - } - - const functionName = fnSt.functionName?.value || ""; - - commands.executeCommand("ballerina.close.ai.panel"); - commands.executeCommand("ballerina.open.ai.panel", { - type: 'command-template', - command: Command.DataMap, - templateId: TemplateId.MappingsForFunction, - params: { - functionName: functionName - } - }); - } - async notifyAIMappings(params: NotifyAIMappingsRequest): Promise { const { newFnPosition, prevFnSource, filePath } = params; const fileUri = Uri.file(filePath).toString(); @@ -362,7 +315,7 @@ export class AiPanelRpcManager implements AIPanelAPI { }); const { source } = res as SyntaxTree; - modifyFileContent({ filePath, content: source }); + await modifyFileContent({ filePath, content: source }); updateView(); } @@ -503,14 +456,8 @@ export class AiPanelRpcManager implements AIPanelAPI { AIPanelAbortController.getInstance().abort(); } - async getMappingsFromRecord(params: GenerateMappingsFromRecordRequest): Promise { - const projectRoot = await getBallerinaProjectRoot(); - return await generateDataMapping(projectRoot, params); - } - async getTypesFromRecord(params: GenerateTypesFromRecordRequest): Promise { - const projectRoot = await getBallerinaProjectRoot(); - return await generateTypeCreation(projectRoot, params); + return await generateTypeCreation(params); } async postProcess(req: PostProcessRequest): Promise { @@ -727,7 +674,7 @@ export class AiPanelRpcManager implements AIPanelAPI { async getContentFromFile(content: GetFromFileRequest): Promise { return new Promise(async (resolve) => { const projectFsPath = URI.parse(content.filePath).fsPath; - const fileContent = fs.promises.readFile(projectFsPath, 'utf-8'); + const fileContent = await fs.promises.readFile(projectFsPath, 'utf-8'); resolve(fileContent); }); } @@ -798,59 +745,242 @@ export class AiPanelRpcManager implements AIPanelAPI { async abortAIGeneration(): Promise { AIPanelAbortController.getInstance().abort(); } - - async openInlineMappingChatWindow(): Promise { + + async createTempFileAndGenerateMetadata(params: CreateTempFileRequest): Promise { + const projectRoot = await getBallerinaProjectRoot(); + const filePath = await createTempDataMappingFile( + projectRoot, + params.inputs, + params.output, + params.functionName, + params.inputNames, + params.imports + ); + + // Get the complete syntax tree + const fileUri = Uri.file(filePath).toString(); + const st = (await StateMachine.langClient().getSyntaxTree({ + documentIdentifier: { + uri: fileUri, + }, + })) as SyntaxTree; + + let funcDefinitionNode: FunctionDefinition = null; + const modulePart = st.syntaxTree as ModulePart; + + // Find the function definition by name + modulePart.members.forEach((member) => { + if (STKindChecker.isFunctionDefinition(member)) { + const funcDef = member as FunctionDefinition; + if (funcDef.functionName?.value === params.functionName) { + funcDefinitionNode = funcDef; + } + } + }); + + if (!funcDefinitionNode) { + throw new Error(`Function ${params.functionName} not found in the generated file`); + } + + // Create dataMapperMetadata with the found positions + const dataMapperMetadata = { + name: params.functionName, + codeData: { + lineRange: { + fileName: filePath, + startLine: { + line: funcDefinitionNode.position.startLine, + offset: funcDefinitionNode.position.startColumn, + }, + endLine: { + line: funcDefinitionNode.position.endLine, + offset: funcDefinitionNode.position.endColumn, + }, + }, + }, + }; + + const dataMapperModel = await this.generateDataMapperModel({ + documentUri: filePath, + identifier: params.functionName, + dataMapperMetadata: dataMapperMetadata + }); + + return { + mappingsModel: dataMapperModel.mappingsModel as ExpandedDMModel, + name: params.functionName, + codeData: dataMapperMetadata.codeData + }; + } + + async generateDataMapperModel(params: DatamapperModelContext): Promise { try { - let filePath = StateMachine.context().documentUri; - const datamapperMetadata = StateMachine.context().dataMapperMetadata; - const dataMapperModel = await StateMachine + let filePath: string; + let identifier: string; + let dataMapperMetadata: any; + + if (params && params.documentUri && params.identifier) { + filePath = params.documentUri; + identifier = params.identifier; + dataMapperMetadata = params.dataMapperMetadata; + } else { + const context = StateMachine.context(); + filePath = context.documentUri; + identifier = context.identifier || context.dataMapperMetadata.name; + dataMapperMetadata = context.dataMapperMetadata; + } + + let position: LinePosition = { + line: dataMapperMetadata.codeData.lineRange.startLine.line, + offset: dataMapperMetadata.codeData.lineRange.startLine.offset + }; + + if (dataMapperMetadata.codeData.node !== "VARIABLE") { + const fileUri = Uri.file(filePath).toString(); + const fnSTByRange = await StateMachine.langClient().getSTByRange({ + lineRange: { + start: { + line: dataMapperMetadata.codeData.lineRange.startLine.line, + character: dataMapperMetadata.codeData.lineRange.startLine.offset + }, + end: { + line: dataMapperMetadata.codeData.lineRange.endLine.line, + character: dataMapperMetadata.codeData.lineRange.endLine.offset + } + }, + documentIdentifier: { uri: fileUri } + }); + + if (fnSTByRange === NOT_SUPPORTED) { + throw new Error("Syntax tree retrieval not supported"); + } + + const fnSt = (fnSTByRange as SyntaxTree).syntaxTree as STNode; + + if (STKindChecker.isFunctionDefinition(fnSt) && + STKindChecker.isExpressionFunctionBody(fnSt.functionBody)) { + position = { + line: fnSt.functionBody.expression.position.startLine, + offset: fnSt.functionBody.expression.position.startColumn + }; + } + } + + let dataMapperModel = await StateMachine .langClient() - .getInlineDataMapperMappings({ + .getDataMapperMappings({ filePath, - codedata: datamapperMetadata.codeData, - position: { - line: datamapperMetadata.codeData.lineRange.startLine.line, - offset: datamapperMetadata.codeData.lineRange.startLine.offset + codedata: dataMapperMetadata.codeData, + targetField: identifier, + position: position + }) as DataMapperModelResponse; + + return { + mappingsModel: expandDMModel( + dataMapperModel.mappingsModel as DMModel, + identifier + ) + }; + } catch (error) { + console.error("Failed to generate data mapper model:", error); + throw error; + } + } + + async addCodeSegmentToWorkspace(params: CodeSegment): Promise { + try { + let filePath = params.filePath && params.filePath.trim() !== '' + ? params.filePath + : StateMachine.context().documentUri; + const datamapperMetadata = params.metadata + ? params.metadata + : StateMachine.context().dataMapperMetadata; + + let allTextEdits: { [key: string]: TextEdit[] }; + + if (params.textEdit && params.textEdit.textEdits) { + allTextEdits = params.textEdit.textEdits; + } else { + const textEdit: TextEdit = { + newText: params.segmentText, + range: { + start: { + line: datamapperMetadata.codeData.lineRange.startLine.line, + character: datamapperMetadata.codeData.lineRange.startLine.offset + }, + end: { + line: datamapperMetadata.codeData.lineRange.endLine.line, + character: datamapperMetadata.codeData.lineRange.endLine.offset + } } - }) as InlineDataMapperModelResponse; + }; + allTextEdits = { + [filePath]: [textEdit] + }; + } + await updateSourceCode({ textEdits: allTextEdits }); + return true; + } catch (error) { + console.error(">>> Failed to add code segment to the workspace", error); + throw error; + } + } + + async openAIMappingChatWindow(params: DataMapperModelResponse): Promise { + try { + const context = StateMachine.context(); + const { identifier, dataMapperMetadata } = context; + commands.executeCommand("ballerina.close.ai.panel"); commands.executeCommand("ballerina.open.ai.panel", { type: 'command-template', command: Command.DataMap, - templateId: TemplateId.InlineMappings, + templateId: identifier ? TemplateId.MappingsForFunction : TemplateId.InlineMappings, + ...(identifier && { params: { functionName: identifier } }), metadata: { - ...datamapperMetadata, - mappingsModel: dataMapperModel.mappingsModel as ExpandedDMModel + ...dataMapperMetadata, + mappingsModel: params.mappingsModel as ExpandedDMModel } }); } catch (error) { - console.error("Failed to open AI chat window for inline mapping:", error); + console.error("Failed to open AI chat window for mapping:", error); throw error; } } - async getMappingsFromModel(params: MetadataWithAttachments): Promise { - let filePath = StateMachine.context().documentUri; - const file = params.attachment && params.attachment.length > 0 - ? params.attachment[0] - : undefined; - const mappingElement = await processInlineMappings(params.metadata.mappingsModel as ExpandedDMModel, file); - const allMappingsRequest = { - filePath, - codedata: params.metadata.codeData, - varName: params.metadata.name, - position: { - line: params.metadata.codeData.lineRange.startLine.line, - offset: params.metadata.codeData.lineRange.startLine.offset - }, - mappings: (mappingElement as MappingElement).mappings - }; - return allMappingsRequest; + async generateMappings(params: MetadataWithAttachments): Promise { + try { + const filePath = params.useTemporaryFile + ? params.metadata.codeData.lineRange.fileName + : StateMachine.context().documentUri; + + const file = params.attachments && params.attachments.length > 0 + ? params.attachments[0] + : undefined; + + const mappingElement = await processMappings(params.metadata.mappingsModel as ExpandedDMModel, file); + + const allMappingsRequest: AllDataMapperSourceRequest = { + filePath, + codedata: params.metadata.codeData, + varName: params.metadata.name, + position: { + line: params.metadata.codeData.lineRange.startLine.line, + offset: params.metadata.codeData.lineRange.startLine.offset + }, + mappings: (mappingElement as MappingElement).mappings + }; + + return allMappingsRequest; + } catch (error) { + console.error("Failed to generate mappings:", error); + throw error; + } } async addInlineCodeSegmentToWorkspace(params: CodeSegment): Promise { try { - let filePath = StateMachine.context().documentUri; + let filePath = StateMachine.context().documentUri; const datamapperMetadata = StateMachine.context().dataMapperMetadata; const textEdit: TextEdit = { newText: params.segmentText, @@ -868,12 +998,45 @@ export class AiPanelRpcManager implements AIPanelAPI { const allTextEdits: { [key: string]: TextEdit[] } = { [filePath]: [textEdit] }; - await updateAndRefreshDataMapper(allTextEdits, filePath, datamapperMetadata.codeData, datamapperMetadata.name); + + await updateAndRefreshDataMapper( + allTextEdits, + filePath, + datamapperMetadata.codeData, + datamapperMetadata.name, + datamapperMetadata.name + ); } catch (error) { console.error(">>> Failed to add inline code segment to the workspace", error); throw error; } } + + async getGeneratedDocumentation(params: DocGenerationRequest): Promise { + await generateDocumentationForService(params); + } + + async addFilesToProject(params: AddFilesToProjectRequest): Promise { + try { + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders) { + throw new Error("No workspaces found."); + } + + const workspaceFolderPath = workspaceFolders[0].uri.fsPath; + + const ballerinaProjectFile = path.join(workspaceFolderPath, "Ballerina.toml"); + if (!fs.existsSync(ballerinaProjectFile)) { + throw new Error("Not a Ballerina project."); + } + await addToIntegration(workspaceFolderPath, params.fileChanges); + updateView(); + return true; + } catch (error) { + console.error(">>> Failed to add files to the project", error); + return false; //silently fail for timeout issues. + } + } } function getModifiedAssistantResponse(originalAssistantResponse: string, tempDir: string, project: ProjectSource): string { @@ -932,7 +1095,7 @@ async function setupProjectEnvironment(project: ProjectSource): Promise<{ langCl // Update lastUpdatedBalFile if it's a .bal file if (sourceFile.filePath.endsWith('.bal')) { const tempFilePath = path.join(tempDir, sourceFile.filePath); - writeBallerinaFileDidOpen(tempFilePath, sourceFile.content); + writeBallerinaFileDidOpenTemp(tempFilePath, sourceFile.content); } } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/utils.ts index 0b9e82a1d19..c9f2e12f0f7 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/utils.ts @@ -16,32 +16,28 @@ * under the License. */ -import { ArrayTypeDesc, FunctionDefinition, ModulePart, QualifiedNameReference, RequiredParam, STKindChecker } from "@wso2/syntax-tree"; -import { FormField, STModification, SyntaxTree, Attachment, AttachmentStatus, keywords, DiagnosticEntry, InlineDataMapperModelResponse } from "@wso2/ballerina-core"; -import { window } from 'vscode'; - -import { StateMachine } from "../../stateMachine"; -import { - INVALID_PARAMETER_TYPE, - INVALID_PARAMETER_TYPE_MULTIPLE_ARRAY, - INVALID_RECORD_UNION_TYPE -} from "../../views/ai-panel/errorCodes"; +import { FunctionDefinition, ModulePart, STKindChecker } from "@wso2/syntax-tree"; +import { FormField, Attachment, AttachmentStatus, keywords, DiagnosticEntry, DataMapperModelResponse, ExpandedDMModel, MappingElement, Mapping, IOType, InputCategory, TypeKind, FileChanges } from "@wso2/ballerina-core"; +import { Position, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode'; + import path from "path"; import * as fs from 'fs'; import { BACKEND_URL } from "../../features/ai/utils"; import { AIChatError } from "./utils/errors"; -import { generateAutoMappings } from "../../../src/features/ai/service/datamapper/datamapper"; -import { DatamapperResponse, Payload } from "../../../src/features/ai/service/datamapper/types"; +import { DatamapperResponse } from "../../../src/features/ai/service/datamapper/types"; import { DataMapperRequest, DataMapperResponse, FileData, processDataMapperInput } from "../../../src/features/ai/service/datamapper/context_api"; import { getAskResponse } from "../../../src/features/ai/service/ask/ask"; import { ArrayEnumUnionType, ArrayRecordType, MetadataType, NUMERIC_AND_BOOLEAN_TYPES, Operation, PrimitiveType, RecordType, UnionEnumIntersectionType } from "./constants"; -import { FieldMetadata, InputMetadata, IntermediateMapping, MappingData, MappingFileRecord, NestedFieldDescriptor, OutputMetadata, ParameterDefinitions, ParameterField, ParameterMetadata, ProcessCombinedKeyResult, ProcessParentKeyResult, RecordDefinitonObject } from "./types"; +import { FieldMetadata, IntermediateMapping, MappingData, MappingFileRecord, ParameterDefinitions, ParameterField, ParameterMetadata, ProcessCombinedKeyResult, ProcessParentKeyResult, RecordDefinitonObject } from "./types"; +import { generateAutoMappings } from "../../../src/features/ai/service/datamapper/datamapper"; +import { ArtifactNotificationHandler, ArtifactsUpdated } from "../../utils/project-artifacts-handler"; +import { writeFileSync } from "fs"; -const BACKEND_BASE_URL = BACKEND_URL.replace(/\/v2\.0$/, ""); +// const BACKEND_BASE_URL = BACKEND_URL.replace(/\/v2\.0$/, ""); //TODO: Temp workaround as custom domain seem to block file uploads const CONTEXT_UPLOAD_URL_V1 = "https://e95488c8-8511-4882-967f-ec3ae2a0f86f-prod.e1-us-east-azure.choreoapis.dev/ballerina-copilot/context-upload-api/v1.0"; // const CONTEXT_UPLOAD_URL_V1 = BACKEND_BASE_URL + "/context-api/v1.0"; -const ASK_API_URL_V1 = BACKEND_BASE_URL + "/ask-api/v1.0"; +// const ASK_API_URL_V1 = BACKEND_BASE_URL + "/ask-api/v1.0"; export const REQUEST_TIMEOUT = 2000000; @@ -97,282 +93,9 @@ const isArrayEnumUnion = (type: string): boolean => { return Object.values(ArrayEnumUnionType).includes(type as ArrayEnumUnionType); }; -export async function getParamDefinitions( - fnSt: FunctionDefinition, - fileUri: string -): Promise { - try { - const inputs: NestedFieldDescriptor = {}; - const inputMetadata: InputMetadata = {}; - let output: NestedFieldDescriptor = {}; - let outputMetadata: OutputMetadata = {}; - let hasArrayParams = false; - let arrayParams = 0; - let isErrorExists = false; - - for (const parameter of fnSt.functionSignature.parameters) { - if (!STKindChecker.isRequiredParam(parameter)) { - continue; - } - - const param = parameter as RequiredParam; - let paramName = param.paramName.value; - let paramType = ""; - - if (STKindChecker.isArrayTypeDesc(param.typeName) && STKindChecker.isArrayTypeDesc(fnSt.functionSignature.returnTypeDesc.type)) { - paramName = `${paramName}Item`; - arrayParams++; - } - - if (param.typeData.typeSymbol.typeKind === "array") { - paramType = param.typeName.source; - } else if (param.typeData.typeSymbol.typeKind === "typeReference") { - paramType = param.typeData.typeSymbol.name; - } else { - paramType = param.typeName.source; - } - - const position = STKindChecker.isQualifiedNameReference(param.typeName) - ? { - line: (param.typeName as QualifiedNameReference).identifier.position.startLine, - offset: (param.typeName as QualifiedNameReference).identifier.position.startColumn - } - : STKindChecker.isArrayTypeDesc(param.typeName) && STKindChecker.isQualifiedNameReference( - (param.typeName as ArrayTypeDesc).memberTypeDesc) - ? { - line: ((param.typeName as ArrayTypeDesc).memberTypeDesc as QualifiedNameReference).identifier.position.startLine, - offset: ((param.typeName as ArrayTypeDesc).memberTypeDesc as QualifiedNameReference).identifier.position.startColumn - } - : { - line: parameter.position.startLine, - offset: parameter.position.startColumn - }; - - const inputTypeDefinition = await StateMachine.langClient().getTypeFromSymbol({ - documentIdentifier: { - uri: fileUri - }, - positions: [position] - }); - - if ('types' in inputTypeDefinition && inputTypeDefinition.types.length > 1) { - throw new Error(INVALID_PARAMETER_TYPE.message); - } - - if ('types' in inputTypeDefinition && !inputTypeDefinition.types[0].hasOwnProperty('type')) { - if (STKindChecker.isQualifiedNameReference(parameter.typeName)) { - throw new Error(`"${parameter.typeName["identifier"].value}" does not exist in the package "${parameter.typeName["modulePrefix"].value}". Please verify the record name or ensure that the correct package is imported.`); - } - throw new Error(INVALID_PARAMETER_TYPE.message); - } - - const inputType = inputTypeDefinition["types"]?.[0].type; - if (inputType?.typeName === "union" && inputType.members?.some((m: { fields: string | any[]; }) => m.fields?.length > 0)) { - throw new Error(INVALID_RECORD_UNION_TYPE.message); - } - - let inputDefinition: RecordDefinitonObject; - if (inputType?.fields) { - inputDefinition = navigateTypeInfo(inputType.fields, false); - } else { - inputDefinition = { - "recordFields": { [paramName]: { "type": inputType.typeName, "comment": "" } }, - "recordFieldsMetadata": { - [paramName]: { - "typeName": inputType.typeName, - "type": inputType.typeName, - "typeInstance": paramName, - "nullable": false, - "optional": false - } - } - }; - } - - inputs[paramName] = inputDefinition.recordFields; - inputMetadata[paramName] = { - isArrayType: STKindChecker.isArrayTypeDesc(parameter.typeName), - parameterName: paramName, - parameterType: paramType, - type: STKindChecker.isArrayTypeDesc(parameter.typeName) ? "record[]" : "record", - fields: inputDefinition.recordFieldsMetadata, - }; - if (STKindChecker.isArrayTypeDesc(parameter.typeName)) { - hasArrayParams = true; - } - } - - if (STKindChecker.isUnionTypeDesc(fnSt.functionSignature.returnTypeDesc.type)) { - let unionType = fnSt.functionSignature.returnTypeDesc.type; - let leftType = unionType.leftTypeDesc; - let rightType = unionType.rightTypeDesc; - - if (STKindChecker.isArrayTypeDesc(leftType) && STKindChecker.isErrorTypeDesc(rightType)) { - if (!STKindChecker.isSimpleNameReference(leftType.memberTypeDesc)) { - throw new Error(INVALID_PARAMETER_TYPE.message); - } - isErrorExists = true; - } else if (STKindChecker.isArrayTypeDesc(rightType) && STKindChecker.isErrorTypeDesc(leftType)) { - if (!STKindChecker.isSimpleNameReference(rightType.memberTypeDesc)) { - throw new Error(INVALID_PARAMETER_TYPE.message); - } - isErrorExists = true; - } else if ( - (STKindChecker.isSimpleNameReference(leftType) || STKindChecker.isQualifiedNameReference(leftType)) && - STKindChecker.isErrorTypeDesc(rightType)) { - isErrorExists = true; - } else if ( - (STKindChecker.isSimpleNameReference(rightType) || STKindChecker.isQualifiedNameReference(rightType)) && - STKindChecker.isErrorTypeDesc(leftType)) { - isErrorExists = true; - } else { - throw new Error(INVALID_PARAMETER_TYPE.message); - } - } else if (STKindChecker.isArrayTypeDesc(fnSt.functionSignature.returnTypeDesc.type)) { - if (arrayParams > 1) { - throw new Error(INVALID_PARAMETER_TYPE_MULTIPLE_ARRAY.message); - } - if (!hasArrayParams) { - throw new Error(INVALID_PARAMETER_TYPE_MULTIPLE_ARRAY.message); - } - if (!(STKindChecker.isSimpleNameReference(fnSt.functionSignature.returnTypeDesc.type.memberTypeDesc) || - STKindChecker.isQualifiedNameReference(fnSt.functionSignature.returnTypeDesc.type.memberTypeDesc))) { - throw new Error(INVALID_PARAMETER_TYPE.message); - } - } else { - if (!STKindChecker.isSimpleNameReference(fnSt.functionSignature.returnTypeDesc.type) && - !STKindChecker.isQualifiedNameReference(fnSt.functionSignature.returnTypeDesc.type)) { - throw new Error(INVALID_PARAMETER_TYPE.message); - } - } - - let returnType = fnSt.functionSignature.returnTypeDesc.type; - - const returnTypePosition = STKindChecker.isUnionTypeDesc(returnType) - ? { - line: STKindChecker.isErrorTypeDesc(returnType.leftTypeDesc) - ? returnType.rightTypeDesc.position.startLine - : returnType.leftTypeDesc.position.startLine, - offset: STKindChecker.isErrorTypeDesc(returnType.leftTypeDesc) - ? returnType.rightTypeDesc.position.startColumn - : returnType.leftTypeDesc.position.startColumn - } - : STKindChecker.isArrayTypeDesc(returnType) && STKindChecker.isQualifiedNameReference(returnType.memberTypeDesc) - ? { - line: returnType.memberTypeDesc.identifier.position.startLine, - offset: returnType.memberTypeDesc.identifier.position.startColumn - } - : STKindChecker.isQualifiedNameReference(returnType) - ? { - line: returnType.identifier.position.startLine, - offset: returnType.identifier.position.startColumn - } - : { - line: returnType.position.startLine, - offset: returnType.position.startColumn - }; - - const outputTypeDefinition = await StateMachine.langClient().getTypeFromSymbol({ - documentIdentifier: { - uri: fileUri - }, - positions: [returnTypePosition] - }); - - if ('types' in outputTypeDefinition && !outputTypeDefinition.types[0].hasOwnProperty('type')) { - if (STKindChecker.isQualifiedNameReference(returnType)) { - throw new Error(`"${returnType["identifier"].value}" does not exist in the package "${returnType["modulePrefix"].value}". Please verify the record name or ensure that the correct package is imported.`); - } - throw new Error(INVALID_PARAMETER_TYPE.message); - } - - const outputType = outputTypeDefinition["types"]?.[0].type; - if (outputType?.typeName === "union" && outputType.members?.some((m) => m.fields)) { - throw new Error(INVALID_RECORD_UNION_TYPE.message); - } - - const outputDefinition = navigateTypeInfo('types' in outputTypeDefinition && outputTypeDefinition.types[0].type.fields, false); - output = { ...outputDefinition.recordFields }; - outputMetadata = { ...outputDefinition.recordFieldsMetadata }; - - const response = { - inputs, - output, - inputMetadata, - outputMetadata - }; - - return { - parameterMetadata: response, - errorStatus: isErrorExists - }; - } catch (error) { - throw error; - } -} - -export async function processMappings( - fnSt: FunctionDefinition, - fileUri: string, - file?: Attachment -): Promise { - let result = await getParamDefinitions(fnSt, fileUri); - let parameterDefinitions = result.parameterMetadata; - const isErrorExists = result.errorStatus; - - if (file) { - let mappedResult = await mappingFileParameterDefinitions(file, parameterDefinitions); - parameterDefinitions = mappedResult as ParameterMetadata; - } - - const codeObject = await getDatamapperCode(parameterDefinitions); - const { recordString, isCheckError } = await constructRecord(codeObject); - let codeString: string; - const parameter = fnSt.functionSignature.parameters[0] as RequiredParam; - const paramName = parameter.paramName.value; - const formattedRecordString = recordString.startsWith(":") ? recordString.substring(1) : recordString; - - let returnType = fnSt.functionSignature.returnTypeDesc.type; - - if (STKindChecker.isUnionTypeDesc(returnType)) { - const { leftTypeDesc: leftType, rightTypeDesc: rightType } = returnType; - - if (STKindChecker.isArrayTypeDesc(leftType) || STKindChecker.isArrayTypeDesc(rightType)) { - codeString = isCheckError && !isErrorExists - ? `|error => from var ${paramName}Item in ${paramName}\n select ${formattedRecordString};` - : `=> from var ${paramName}Item in ${paramName}\n select ${formattedRecordString};`; - } else { - codeString = isCheckError && !isErrorExists ? `|error => ${recordString};` : `=> ${recordString};`; - } - } else if (STKindChecker.isArrayTypeDesc(returnType)) { - codeString = isCheckError - ? `|error => from var ${paramName}Item in ${paramName}\n select ${formattedRecordString};` - : `=> from var ${paramName}Item in ${paramName}\n select ${formattedRecordString};`; - } else { - codeString = isCheckError ? `|error => ${recordString};` : `=> ${recordString};`; - } - - const modifications: STModification[] = []; - modifications.push({ - type: "INSERT", - config: { STATEMENT: codeString }, - endColumn: fnSt.functionBody.position.endColumn, - endLine: fnSt.functionBody.position.endLine, - startColumn: fnSt.functionBody.position.startColumn, - startLine: fnSt.functionBody.position.startLine, - }); - - const stModifyResponse = await StateMachine.langClient().stModify({ - astModifications: modifications, - documentIdentifier: { - uri: fileUri - } - }); - - return stModifyResponse as SyntaxTree; -} - -function isMappingData(obj: MappingData | IntermediateMapping): obj is MappingData { +function isMappingData( + obj: MappingData | IntermediateMapping +): obj is MappingData { return ( typeof obj === "object" && obj !== null && @@ -439,13 +162,19 @@ async function processMappingData( ): Promise> { const parameters = mappingData.parameters; const paths = parameters[0].split("."); + let path: string = ""; - const path = await getMappingString( - mappingData, - parameterDefinitions, - nestedKey, - nestedKeyArray - ); + try { + path = await getMappingString( + mappingData, + parameterDefinitions, + nestedKey, + nestedKeyArray + ); + } catch (error) { + console.log(`Error in processMapping:`, error); + throw new Error(`Failed to process mappings`); + } if (typeof path !== "string" || path === "") { return {}; @@ -512,7 +241,7 @@ async function getMappingString(mapping: MappingData, parameterDefinitions: Para if (paths.length > 2) { modifiedInput = await getNestedType(paths.slice(1), parameterDefinitions.inputMetadata[recordObjectName]); } else if (paths.length === 2) { - modifiedInput = parameterDefinitions.inputMetadata[recordObjectName]["fields"][paths[1]]; + modifiedInput = parameterDefinitions.inputMetadata[recordObjectName]?.["fields"]?.[paths[1]] || parameterDefinitions.inputMetadata[recordObjectName + 'Item']?.["fields"]?.[paths[1]]; } else { modifiedInput = parameterDefinitions.configurables[recordObjectName] || parameterDefinitions.constants[recordObjectName] || @@ -1035,7 +764,7 @@ class TypeInfoVisitorImpl implements TypeInfoVisitor { if (member.hasOwnProperty("typeName")) { memberName = member.typeName; - if (member.hasOwnProperty("name")) { + if (member.hasOwnProperty("name") && !member.hasOwnProperty("id")) { this.addNamedSimpleMember(member, memberName, context); } else { this.addUnnamedSimpleMember(memberName, member, context); @@ -1302,10 +1031,10 @@ export function getBalRecFieldName(fieldName: string) { return keywords.includes(fieldName) ? `'${fieldName}` : fieldName; } -export async function getDatamapperCode(parameterDefinitions: ParameterMetadata): Promise> { +async function getDatamapperCode(dataMapperResponse: DataMapperModelResponse, parameterDefinitions: ParameterMetadata): Promise> { let nestedKeyArray: string[] = []; try { - let response: DatamapperResponse = await sendDatamapperRequest(parameterDefinitions); + let response: DatamapperResponse = await sendDatamapperRequest(dataMapperResponse); let intermediateMapping = response.mappings; let finalCode = await generateBallerinaCode(intermediateMapping, parameterDefinitions, "", nestedKeyArray); return finalCode; @@ -1357,8 +1086,8 @@ export function notifyNoGeneratedMappings() { window.showInformationMessage(msg); } -async function sendDatamapperRequest(parameterDefinitions: ParameterMetadata): Promise { - const response: DatamapperResponse = await generateAutoMappings(parameterDefinitions as Payload); +async function sendDatamapperRequest(dataMapperResponse: DataMapperModelResponse): Promise { + const response: DatamapperResponse = await generateAutoMappings(dataMapperResponse); return response; } @@ -1416,8 +1145,8 @@ export async function mappingFileParameterDefinitions(file: Attachment, paramete }; } -export async function mappingFileInlineDataMapperModel(file: Attachment, inlineDataMapperResponse: InlineDataMapperModelResponse): Promise { - if (!file) { return inlineDataMapperResponse; } +export async function mappingFileDataMapperModel(file: Attachment, dataMapperResponse: DataMapperModelResponse): Promise { + if (!file) { return dataMapperResponse; } const fileData = await attatchmentToFileData(file); const params: DataMapperRequest = { file: fileData, @@ -1427,9 +1156,9 @@ export async function mappingFileInlineDataMapperModel(file: Attachment, inlineD let mappingFile: MappingFileRecord = JSON.parse(resp.fileContent) as MappingFileRecord; return { - ...inlineDataMapperResponse, + ...dataMapperResponse, mappingsModel: { - ...inlineDataMapperResponse.mappingsModel, + ...dataMapperResponse.mappingsModel, mapping_fields: mappingFile.mapping_fields } }; @@ -1476,7 +1205,7 @@ async function accessMetadata( } if (isArrayRecord(inputObject.typeName) || isArrayEnumUnion(inputObject.type)) { isUsingArray = inputObject.nullableArray; - } + } if (isUsingArray && isRecordType(inputObject.typeName)) { newPath[index] = `${paths[index]}?`; } @@ -1491,14 +1220,20 @@ async function accessMetadata( } if (inputObject.typeName.includes("[]") && operation === Operation.LENGTH) { let lastInputObject = await getMetadata(parameterDefinitions, paths, paths[paths.length - 1], MetadataType.INPUT_METADATA); - let inputDataType = lastInputObject.typeName.replace(/\|\(\)$/, ""); + let inputDataType = lastInputObject.typeName + // remove |() from union types + .replace(/\|\(\)/g, "") + // remove wrapping parentheses before [] + .replace(/^\((.*)\)\[\]$/, "$1[]") + // remove single wrapping parentheses (non-array cases) + .replace(/^\((.*)\)$/, "$1"); defaultValue = await getDefaultValue(inputDataType); newPath[paths.length - 1] = `${paths[paths.length - 1]}?:${defaultValue}`; } if (inputObject.nullable && inputObject.optional) { newPath[index - 1] = `${paths[index - 1]}?`; } - // Handle enum, union, and intersection types + // Handle enum, union, and intersection types } else if (isUnionEnumIntersectionType(inputObject.type)) { if (inputObject.nullable && inputObject.optional) { newPath[index - 1] = `${paths[index - 1]}?`; @@ -1861,6 +1596,510 @@ async function processCombinedKey( return { isinputRecordArrayNullable, isinputRecordArrayOptional, isinputArrayNullable, isinputArrayOptional, isinputNullableArray }; } +export async function processMappings( + request: ExpandedDMModel, + file?: Attachment +): Promise { + let dataMapperResponse = cleanDataMapperModelResponse(request); + const result = await getParamDefinitions(dataMapperResponse); + const parameterDefinitions = (result as ParameterDefinitions).parameterMetadata; + + if (file) { + const mappedResult = await mappingFileDataMapperModel(file, dataMapperResponse); + dataMapperResponse = mappedResult as DataMapperModelResponse; + } + + const codeObject = await getDatamapperCode(dataMapperResponse, parameterDefinitions); + const mappings: Mapping[] = transformCodeObjectToMappings(codeObject, dataMapperResponse); + return { mappings }; +} + +// Main function to clean the entire DataMapperModelResponse +function cleanDataMapperModelResponse( + response: ExpandedDMModel +): DataMapperModelResponse { + if (!response) { + throw new Error("Invalid response: missing mappingsModel"); + } + + // Check if both input and output are arrays + const hasInputArrays = response.inputs && response.inputs.some(input => input.kind === "array"); + const isOutputArray = response.output && response.output.kind === "array"; + + let processedResponse = response; + + // Transform the structure if both input and output are arrays + if (hasInputArrays && isOutputArray) { + processedResponse = transformArrayStructure(response); + } + + const cleanedResponse: DataMapperModelResponse = { + mappingsModel: cleanExpandedDMModel(processedResponse as ExpandedDMModel) + }; + + return cleanedResponse; +} + +function transformArrayStructure(response: ExpandedDMModel): ExpandedDMModel { + const transformed = { ...response }; + + if (transformed.inputs && transformed.inputs.length > 0) { + transformed.inputs = transformed.inputs.map(input => { + // Only transform inputs that are arrays + if (input.kind === "array" && input.member) { + const originalName = input.name; + + // Deep clone and transform IDs using JSON stringify/parse + const transformedInput = JSON.parse( + JSON.stringify({ + ...input, + id: `${input.id}Item` + }).replace( + new RegExp(`"id":"${originalName}\\.`, 'g'), + `"id":"${originalName}Item.` + ) + ); + + return transformedInput; + } + // Return non-array inputs unchanged + return input; + }); + } + return transformed; +} + +// Clean ExpandedDMModel by removing null fields and cleaning nested structures +function cleanExpandedDMModel(model: ExpandedDMModel): ExpandedDMModel { + const cleaned = model as ExpandedDMModel; + + // Clean inputs array - remove null/undefined elements + if (model.inputs && Array.isArray(model.inputs)) { + const cleanedInputs = model.inputs + .filter(input => !isNullOrUndefined(input)) + .map(input => cleanIOType(input)) + .filter(input => input !== null) as IOType[]; + + cleaned.inputs = cleanedInputs; + } + + // Clean output + if (model.output && !isNullOrUndefined(model.output)) { + const cleanedOutput = cleanIOType(model.output); + if (cleanedOutput !== null) { + cleaned.output = cleanedOutput; + } + } + + // Clean subMappings array if it exists + if (model.subMappings && Array.isArray(model.subMappings)) { + const cleanedSubMappings = model.subMappings + .filter(subMapping => !isNullOrUndefined(subMapping)) + .map(subMapping => cleanIOType(subMapping)) + .filter(subMapping => subMapping !== null) as IOType[]; + + if (cleanedSubMappings.length > 0) { + cleaned.subMappings = cleanedSubMappings; + } + } + + // Clean mappings array - remove null/undefined elements + if (model.mappings && Array.isArray(model.mappings)) { + const cleanedMappings = model.mappings.filter(mapping => + !isNullOrUndefined(mapping) && + !isNullOrUndefined(mapping.output) && + !isNullOrUndefined(mapping.expression) + ); + + // Also clean inputs array within each mapping + cleanedMappings.forEach(mapping => { + if (mapping.inputs && Array.isArray(mapping.inputs)) { + mapping.inputs = mapping.inputs.filter(input => !isNullOrUndefined(input)); + } + }); + + cleaned.mappings = cleanedMappings; + } + + // Include query if it exists and is not null + if (model.query && !isNullOrUndefined(model.query)) { + cleaned.query = model.query; + } + + return cleaned; +} + +// Utility function to check if a value is null or undefined +function isNullOrUndefined(value: any): boolean { + return value === null || value === undefined; +} + +// Clean IOType by removing null/undefined fields and filtering arrays +function cleanIOType(ioType: IOType | null | undefined): IOType | null { + if (isNullOrUndefined(ioType)) { + return null; + } + + // Remove array records without fields + if (ioType.kind === "array" && ioType.typeName === "record" && + (!ioType.fields || ioType.fields.length === 0)) { + return null; + } + + // Remove records without fields + if (ioType.kind === "record" && (!ioType.fields || ioType.fields.length === 0)) { + return null; + } + + const cleaned = ioType; + + // Clean fields array - remove null/undefined elements and recursively clean + if (ioType.fields && Array.isArray(ioType.fields)) { + const cleanedFields = ioType.fields + .filter(field => !isNullOrUndefined(field)) + .map(field => cleanIOType(field)) + .filter(field => field !== null) as IOType[]; + + if (cleanedFields.length > 0) { + cleaned.fields = cleanedFields; + } + } + + // Clean member recursively + if (ioType.member && !isNullOrUndefined(ioType.member)) { + const cleanedMember = cleanIOType(ioType.member); + if (cleanedMember !== null) { + cleaned.member = cleanedMember; + } + } + + // Clean members array - remove null/undefined elements + if (ioType.members && Array.isArray(ioType.members)) { + const cleanedMembers = ioType.members.filter(member => + !isNullOrUndefined(member) && + !isNullOrUndefined(member.id) && + !isNullOrUndefined(member.typeName) + ); + + if (cleanedMembers.length > 0) { + cleaned.members = cleanedMembers; + } + } + + return cleaned; +} + +export async function getParamDefinitions( + dataMapperResponse: DataMapperModelResponse +): Promise { + const inputs: { [key: string]: any } = {}; + const inputMetadata: { [key: string]: any } = {}; + + const { inputs: mappingInputs, output: mappingOutput } = dataMapperResponse.mappingsModel as ExpandedDMModel; + const transformedInputs = transformInputs(mappingInputs); + const transformedOutputs = transformOutput(mappingOutput); + + for (const parameter of transformedInputs.parameters) { + const inputDefinition = navigateTypeInfo(transformedInputs.parameterFields[parameter.parameterName], false); + + inputs[parameter.parameterName] = inputDefinition.recordFields; + inputMetadata[parameter.parameterName] = { + "isArrayType": parameter.isArrayType, + "parameterName": parameter.parameterName, + "parameterType": parameter.parameterType, + "type": parameter.type, + "fields": inputDefinition.recordFieldsMetadata + }; + } + + const outputDefinition = navigateTypeInfo(transformedOutputs, false); + const output = { ...outputDefinition.recordFields }; + const outputMetadata = { ...outputDefinition.recordFieldsMetadata }; + + return { + parameterMetadata: { + inputs, + output, + inputMetadata, + outputMetadata, + constants: transformedInputs.constants, + configurables: transformedInputs.configurables, + variables: transformedInputs.variables + }, + errorStatus: false + }; +} + +function transformInputs(inputs: IOType[]): { + constants: Record; + configurables: Record; + variables: Record; + parameters: ParameterField[]; + parameterFields: { [parameterName: string]: FormField[] }; +} { + const constants: Record = {}; + const configurables: Record = {}; + const variables: Record = {}; + const parameters: ParameterField[] = []; + const parameterFields: { [parameterName: string]: FormField[] } = {}; + + inputs.forEach((input) => { + // Helper function to create ParameterField + const createParameterField = (input: IOType): ParameterField => { + let typeName: string; + + if (input.kind !== input.typeName) { + typeName = input.typeName; + } else if (!input.typeName) { + typeName = input.kind || "unknown"; + } else { + typeName = input.typeName; + } + + // Determine if it's an array type + const isArrayType = input.kind === TypeKind.Array; + const name = input.id; + + // Determine the type string + let type: string; + if (isArrayType) { + // If it's an array, get the member type and append [] + if (input.member) { + const memberTypeName = input.member.kind || "unknown"; + type = `${memberTypeName}[]`; + } else { + type = `${typeName}[]`; + } + } else { + type = input.kind; + } + + return { + isArrayType, + parameterName: name, + parameterType: typeName, + type + }; + }; + + const createFieldConfig = (input: IOType): FieldMetadata => { + if (!input.typeName) { + throw new Error("TypeName is missing"); + } + return { + typeName: input.kind || "unknown", + type: input.kind || "unknown", + typeInstance: input.id, + nullable: false, + optional: input.optional + }; + }; + + // Handle different categories + if (input.category === InputCategory.Constant) { + constants[input.id] = createFieldConfig(input); + return; + } + + if (input.category === InputCategory.Configurable) { + configurables[input.id] = createFieldConfig(input); + return; + } + + if (input.category === InputCategory.Variable) { + variables[input.id] = createFieldConfig(input); + return; + } + + if (input.category === InputCategory.Parameter) { + const parameterField = createParameterField(input); + parameters.push(parameterField); + + const parameterName = input.id; + + if (input.kind === "array" && input.member) { + if (input.member.fields) { + parameterFields[parameterName] = input.member.fields.map(transformIOType); + } else { + parameterFields[parameterName] = [transformIOType(input.member)]; + } + } else { + // Handle non-array parameters + if (input.fields) { + parameterFields[parameterName] = input.fields.map(transformIOType); + } else { + parameterFields[parameterName] = [transformIOType(input)]; + } + } + } + }); + + return { constants, configurables, variables, parameters, parameterFields }; +} + +function transformIOType(input: IOType): FormField { + const name = input.name || extractNameFromId(input.id); + + let typeName: string; + if (input.kind && input.typeName && input.kind !== input.typeName && input.category) { + typeName = input.kind; + } else if (!input.typeName) { + typeName = input.kind || "unknown"; + } else { + typeName = input.typeName; + } + + const baseField = { + id: input.id, + name, + typeName, + optional: input.optional || false + }; + + // Handle arrays + if (input.kind === "array" && input.member) { + const memberTransformed = transformIOType(input.member) as FormField; + const { name, ...memberWithoutName } = memberTransformed; + + return { + ...baseField, + typeName: "array", + memberType: memberWithoutName as FormField + } as FormField; + } + + // Handle unions + if (input.kind === "union" && input.members) { + return { + ...baseField, + typeName: "union", + members: input.members.map(transformIOType) as FormField[] + } as FormField; + } + + // Handle records + if (input.kind === "record" && input.fields) { + const recordField: FormField = { + ...baseField, + typeName: "record", + fields: input.fields.map(transformIOType) as FormField[] + }; + + if ( + input.typeName && + input.kind !== input.typeName && + !input.category + ) { + recordField.typeInfo = { + orgName: "", + moduleName: "", + name: input.typeName + }; + } + + return recordField; + } + + // Handle primitive types + const primitiveField: FormField = { ...baseField }; + + // Add typeInfo if conditions are met + if ( + input.typeName && + input.kind !== input.typeName && + !input.category + ) { + primitiveField.typeInfo = { + orgName: "", + moduleName: "", + name: input.typeName + }; + } + + return primitiveField; +} + +function extractNameFromId(id: string): string { + const parts = id.split('.').filter(part => !/^\d+$/.test(part)); + return parts[parts.length - 1]; +} + +function transformOutput(output: IOType): FormField[] { + if (output.fields) { + return output.fields.map(transformIOType); + } else if (output.member) { + return output.member.fields.map(transformIOType); + } else { + return [transformIOType(output)]; + } +} + +function transformCodeObjectToMappings(codeObject: Record, request: DataMapperModelResponse): Mapping[] { + const mappings: Mapping[] = []; + + // Get the output variable name from the request + const { output: mappingOutput, inputs } = request.mappingsModel as ExpandedDMModel; + const outputVariableName = mappingOutput.name || extractNameFromId(mappingOutput.id); + + // Check if any input is an array + const arrayInputs = inputs.filter(input => input.kind === "array"); + const hasInputArrays = arrayInputs.length > 0; + const isOutputArray = mappingOutput.kind === "array"; + + if (hasInputArrays && isOutputArray) { + // If multiple array inputs, we might need to handle joins or multiple from clauses + if (arrayInputs.length === 1) { + // Single array input - simple select + const arrayInput = arrayInputs[0]; + const inputName = arrayInput.name; + const itemVariableName = `${inputName}Item`; + + // Build the select object from codeObject + const selectFields = Object.keys(codeObject).map(key => { + return `${[key]}: ${codeObject[key]}`; + }).join(',\n '); + + const mapping: Mapping = { + output: outputVariableName, + expression: `from var ${itemVariableName} in ${inputName} + select { + ${selectFields} + }` + }; + mappings.push(mapping); + } else { + const primaryArrayInput = arrayInputs[0]; + const primaryInputName = primaryArrayInput.name; + const primaryItemVariableName = `${primaryInputName}Item`; + + // Build the select object from codeObject + const selectFields = Object.keys(codeObject).map(key => { + return `${[key]}: ${codeObject[key]}`; + }).join(',\n '); + + const mapping: Mapping = { + output: outputVariableName, + expression: `from var ${primaryItemVariableName} in ${primaryInputName} + select { + ${selectFields} + }` + }; + mappings.push(mapping); + } + } else { + // Handle non-array mappings (original logic) + Object.keys(codeObject).forEach(key => { + const mapping: Mapping = { + output: `${outputVariableName}.${key}`, + expression: codeObject[key] + }; + mappings.push(mapping); + }); + } + + return mappings; +} + export async function requirementsSpecification(filepath: string): Promise { if (!filepath) { throw new Error("File is undefined"); @@ -1889,3 +2128,66 @@ export function cleanDiagnosticMessages(entries: DiagnosticEntry[]): DiagnosticE message: entry.message, })); } + + +export async function addToIntegration(workspaceFolderPath: string, fileChanges: FileChanges[]) { + const formattedWorkspaceEdit = new WorkspaceEdit(); + const nonBalFiles: FileChanges[] = []; + + for (const fileChange of fileChanges) { + let balFilePath = path.join(workspaceFolderPath, fileChange.filePath); + const fileUri = Uri.file(balFilePath); + if (!fileChange.filePath.endsWith('.bal')) { + nonBalFiles.push(fileChange); + continue; + } + + formattedWorkspaceEdit.createFile(fileUri, { ignoreIfExists: true }); + + formattedWorkspaceEdit.replace( + fileUri, + new Range( + new Position(0, 0), + new Position(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) + ), + fileChange.content + ); + } + + // Apply all formatted changes at once + await workspace.applyEdit(formattedWorkspaceEdit); + + // Write non ballerina files separately as ls doesn't need to be notified of those changes + for (const fileChange of nonBalFiles) { + let absoluteFilePath = path.join(workspaceFolderPath, fileChange.filePath); + const directory = path.dirname(absoluteFilePath); + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + } + fs.writeFileSync(absoluteFilePath, fileChange.content, 'utf8'); + } + return new Promise((resolve, reject) => { + // Get the artifact notification handler instance + const notificationHandler = ArtifactNotificationHandler.getInstance(); + // Subscribe to artifact updated notifications + let unsubscribe = notificationHandler.subscribe(ArtifactsUpdated.method, undefined, async (payload) => { + clearTimeout(timeoutId); + resolve(payload.data); + unsubscribe(); + }); + + // Set a timeout to reject if no notification is received within 10 seconds + const timeoutId = setTimeout(() => { + console.log("No artifact update notification received within 10 seconds"); + reject(new Error("Operation timed out. Please try again.")); + unsubscribe(); + }, 10000); + + // Clear the timeout when notification is received + const originalUnsubscribe = unsubscribe; + unsubscribe = () => { + clearTimeout(timeoutId); + originalUnsubscribe(); + }; + }); +} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-handler.ts index aac325f1039..0bee6a351da 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-handler.ts @@ -18,9 +18,12 @@ * THIS FILE INCLUDES AUTO GENERATED CODE */ import { - AIChatRequest, + addBreakpointToSource, + addClassField, AddFieldRequest, + addFunction, AddFunctionRequest, + AIChatRequest, BIAiSuggestionsRequest, BIAvailableNodesRequest, BIDeleteByComponentInfoRequest, @@ -33,41 +36,7 @@ import { BuildMode, ClassFieldModifierRequest, ComponentRequest, - DeploymentRequest, - EndOfFileRequest, - ExpressionCompletionsRequest, - ExpressionDiagnosticsRequest, - FormDidCloseParams, - FormDidOpenParams, - FunctionNodeRequest, - GetConfigVariableNodeTemplateRequest, - GetRecordConfigRequest, - GetRecordModelFromSourceRequest, - GetTypeRequest, - GetTypesRequest, - getTypeFromJson, - JsonToTypeRequest, - ModelFromCodeRequest, - OpenAPIClientDeleteRequest, - OpenAPIClientGenerationRequest, - OpenAPIGeneratedModulesRequest, - OpenConfigTomlRequest, - ProjectRequest, - ReadmeContentRequest, - RecordSourceGenRequest, - RenameIdentifierRequest, - ServiceClassSourceRequest, - SignatureHelpRequest, - UpdateConfigVariableRequest, - UpdateConfigVariableRequestV2, - UpdateImportsRequest, - UpdateRecordConfigRequest, - UpdateTypeRequest, - UpdateTypesRequest, - VisibleTypesRequest, - addBreakpointToSource, - addClassField, - addFunction, + MigrateRequest, buildProject, createComponent, createGraphqlClassType, @@ -76,12 +45,17 @@ import { deleteConfigVariableV2, deleteFlowNode, deleteOpenApiGeneratedModules, + deleteType, + DeleteTypeRequest, + DeploymentRequest, deployProject, formDidClose, formDidOpen, generateOpenApiClient, getAiSuggestions, getAllImports, + getAvailableChunkers, + getAvailableDataLoaders, getAvailableEmbeddingProviders, getAvailableModelProviders, getAvailableNodes, @@ -134,7 +108,41 @@ import { updateServiceClass, updateType, updateTypes, - DeleteConfigVariableRequestV2 + DeleteConfigVariableRequestV2, + ConfigVariableRequest, + ProjectRequest, + EndOfFileRequest, + ExpressionCompletionsRequest, + ExpressionDiagnosticsRequest, + FormDidCloseParams, + FormDidOpenParams, + FunctionNodeRequest, + GetConfigVariableNodeTemplateRequest, + GetRecordConfigRequest, + GetRecordModelFromSourceRequest, + getTypeFromJson, + GetTypeRequest, + GetTypesRequest, + JsonToTypeRequest, + ModelFromCodeRequest, + OpenAPIClientDeleteRequest, + OpenAPIClientGenerationRequest, + OpenAPIGeneratedModulesRequest, + OpenConfigTomlRequest, + ReadmeContentRequest, + RecordSourceGenRequest, + RenameIdentifierRequest, + ServiceClassSourceRequest, + SignatureHelpRequest, + UpdateConfigVariableRequest, + UpdateConfigVariableRequestV2, + UpdateImportsRequest, + UpdateRecordConfigRequest, + UpdateTypeRequest, + UpdateTypesRequest, + verifyTypeDelete, + VerifyTypeDeleteRequest, + VisibleTypesRequest } from "@wso2/ballerina-core"; import { Messenger } from "vscode-messenger"; import { BiDiagramRpcManager } from "./rpc-manager"; @@ -150,6 +158,8 @@ export function registerBiDiagramRpcHandlers(messenger: Messenger) { messenger.onRequest(getAvailableVectorStores, (args: BIAvailableNodesRequest) => rpcManger.getAvailableVectorStores(args)); messenger.onRequest(getAvailableEmbeddingProviders, (args: BIAvailableNodesRequest) => rpcManger.getAvailableEmbeddingProviders(args)); messenger.onRequest(getAvailableVectorKnowledgeBases, (args: BIAvailableNodesRequest) => rpcManger.getAvailableVectorKnowledgeBases(args)); + messenger.onRequest(getAvailableDataLoaders, (args: BIAvailableNodesRequest) => rpcManger.getAvailableDataLoaders(args)); + messenger.onRequest(getAvailableChunkers, (args: BIAvailableNodesRequest) => rpcManger.getAvailableChunkers(args)); messenger.onRequest(getEnclosedFunction, (args: BIGetEnclosedFunctionRequest) => rpcManger.getEnclosedFunction(args)); messenger.onRequest(getNodeTemplate, (args: BINodeTemplateRequest) => rpcManger.getNodeTemplate(args)); messenger.onRequest(getAiSuggestions, (args: BIAiSuggestionsRequest) => rpcManger.getAiSuggestions(args)); @@ -163,7 +173,7 @@ export function registerBiDiagramRpcHandlers(messenger: Messenger) { messenger.onRequest(getExpressionCompletions, (args: ExpressionCompletionsRequest) => rpcManger.getExpressionCompletions(args)); messenger.onRequest(getConfigVariables, () => rpcManger.getConfigVariables()); messenger.onRequest(updateConfigVariables, (args: UpdateConfigVariableRequest) => rpcManger.updateConfigVariables(args)); - messenger.onRequest(getConfigVariablesV2, () => rpcManger.getConfigVariablesV2()); + messenger.onRequest(getConfigVariablesV2, (args: ConfigVariableRequest) => rpcManger.getConfigVariablesV2(args)); messenger.onRequest(updateConfigVariablesV2, (args: UpdateConfigVariableRequestV2) => rpcManger.updateConfigVariablesV2(args)); messenger.onRequest(deleteConfigVariableV2, (args: DeleteConfigVariableRequestV2) => rpcManger.deleteConfigVariableV2(args)); messenger.onRequest(getConfigVariableNodeTemplate, (args: GetConfigVariableNodeTemplateRequest) => rpcManger.getConfigVariableNodeTemplate(args)); @@ -190,6 +200,8 @@ export function registerBiDiagramRpcHandlers(messenger: Messenger) { messenger.onRequest(getType, (args: GetTypeRequest) => rpcManger.getType(args)); messenger.onRequest(updateType, (args: UpdateTypeRequest) => rpcManger.updateType(args)); messenger.onRequest(updateTypes, (args: UpdateTypesRequest) => rpcManger.updateTypes(args)); + messenger.onRequest(deleteType, (args: DeleteTypeRequest) => rpcManger.deleteType(args)); + messenger.onRequest(verifyTypeDelete, (args: VerifyTypeDeleteRequest) => rpcManger.verifyTypeDelete(args)); messenger.onRequest(getTypeFromJson, (args: JsonToTypeRequest) => rpcManger.getTypeFromJson(args)); messenger.onRequest(getServiceClassModel, (args: ModelFromCodeRequest) => rpcManger.getServiceClassModel(args)); messenger.onRequest(updateClassField, (args: ClassFieldModifierRequest) => rpcManger.updateClassField(args)); 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 6976c2f1760..99100204a61 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 @@ -58,18 +58,21 @@ import { CreateComponentResponse, CurrentBreakpointsResponse, DIRECTORY_MAP, + DeleteConfigVariableRequestV2, + DeleteConfigVariableResponseV2, + DeleteTypeRequest, + DeleteTypeResponse, DeploymentRequest, DeploymentResponse, DevantMetadata, + Diagnostics, EndOfFileRequest, ExpressionCompletionsRequest, ExpressionCompletionsResponse, ExpressionDiagnosticsRequest, ExpressionDiagnosticsResponse, - FlowNode, FormDidCloseParams, FormDidOpenParams, - FunctionNode, FunctionNodeRequest, FunctionNodeResponse, GeneratedClientSaveResponse, @@ -87,6 +90,7 @@ import { JsonToTypeRequest, JsonToTypeResponse, LinePosition, + LoginMethod, ModelFromCodeRequest, NodeKind, OpenAPIClientDeleteRequest, @@ -130,12 +134,14 @@ import { UpdatedArtifactsResponse, VisibleTypesRequest, VisibleTypesResponse, + VerifyTypeDeleteRequest, + VerifyTypeDeleteResponse, WorkspaceFolder, WorkspacesResponse, - DeleteConfigVariableRequestV2, - DeleteConfigVariableResponseV2, - LoginMethod, - Diagnostics, + ConfigVariableRequest, + AvailableNode, + Item, + Category, } from "@wso2/ballerina-core"; import * as fs from "fs"; import * as path from 'path'; @@ -159,11 +165,11 @@ import { OLD_BACKEND_URL } from "../../features/ai/utils"; import { cleanAndValidateProject, getCurrentBIProject } from "../../features/config-generator/configGenerator"; import { BreakpointManager } from "../../features/debugger/breakpoint-manager"; import { StateMachine, updateView } from "../../stateMachine"; +import { getAccessToken, getLoginMethod } from "../../utils/ai/auth"; import { getCompleteSuggestions } from '../../utils/ai/completions'; import { README_FILE, createBIAutomation, createBIFunction, createBIProjectPure } from "../../utils/bi"; import { writeBallerinaFileDidOpen } from "../../utils/modification"; import { updateSourceCode } from "../../utils/source-utils"; -import { getAccessToken, getLoginMethod } from "../../utils/ai/auth"; import { checkProjectDiagnostics, removeUnusedImports } from "../ai-panel/repair-utils"; export class BiDiagramRpcManager implements BIDiagramAPI { OpenConfigTomlRequest: (params: OpenConfigTomlRequest) => Promise; @@ -303,6 +309,96 @@ export class BiDiagramRpcManager implements BIDiagramAPI { } } + private filterAdvancedAiNodes(response: BIAvailableNodesResponse): BIAvailableNodesResponse { + const showAdvancedAiNodes = extension.ballerinaExtInstance.getShowAdvancedAiNodes(); + if (showAdvancedAiNodes || !response) { + return response; + } + + // List of node types/labels to hide when advanced AI nodes are disabled + const hiddenNodeTypes = [ + 'CHUNKERS', + 'VECTOR_STORES', + 'EMBEDDING_PROVIDERS' + ]; + + const hiddenNodeLabels = [ + 'Recursive Document Chunker', + 'Chunker', + 'Vector Store', + 'Embedding Provider' + ]; + + const filterItems = (items: Item[]): Item[] => { + if (!items) { return items; } + + return items.filter(item => { + if ((item as AvailableNode).codedata?.node && hiddenNodeTypes.includes((item as AvailableNode).codedata.node)) { + return false; + } + + if (item.metadata?.label && hiddenNodeLabels.includes(item.metadata.label)) { + return false; + } + + if ((item as Category).items) { + (item as Category).items = filterItems((item as Category).items); + } + + return true; + }); + }; + + if (response.categories) { + response.categories = response.categories.map(category => { + if (category.items) { + category.items = filterItems(category.items); + } + return category; + }); + } + + return response; + } + + private updateNodeDescriptions(availableNodes: BIAvailableNodesResponse): BIAvailableNodesResponse { + if (!availableNodes) { + return availableNodes; + } + // Adding descriptions for AI nodes + const updateItems = (items: Item[], isInAICategory: boolean): Item[] => { + if (!items) { return items; } + + return items.map(item => { + if ((item as AvailableNode).enabled === false && isInAICategory) { + item.metadata = { + ...item.metadata, + description: "Please update AI package version to latest version to use this feature" + }; + } + + // Recursively handle nested items + if ((item as Category).items) { + (item as Category).items = updateItems((item as Category).items, isInAICategory); + } + + return item; + }); + }; + + if (availableNodes.categories) { + availableNodes.categories = availableNodes.categories.map(category => { + const isAICategory = category.metadata?.label === "AI"; + if (category.items) { + category.items = updateItems(category.items, isAICategory); + } + return category; + }); + } + + return availableNodes; + } + async getAvailableNodes(params: BIAvailableNodesRequest): Promise { console.log(">>> requesting bi available nodes from ls", params); return new Promise((resolve) => { @@ -310,7 +406,9 @@ export class BiDiagramRpcManager implements BIDiagramAPI { .getAvailableNodes(params) .then((model) => { console.log(">>> bi available nodes from ls", model); - resolve(model); + const filteredModel = this.filterAdvancedAiNodes(model); + const updatedModel = this.updateNodeDescriptions(filteredModel); + resolve(updatedModel); }) .catch((error) => { console.log(">>> error fetching available nodes from ls", error); @@ -394,6 +492,43 @@ export class BiDiagramRpcManager implements BIDiagramAPI { }); } + + async getAvailableDataLoaders(params: BIAvailableNodesRequest): Promise { + console.log(">>> requesting bi available data loaders from ls", params); + return new Promise((resolve) => { + StateMachine.langClient() + .getAvailableDataLoaders(params) + .then((model) => { + console.log(">>> bi available data loaders from ls", model); + resolve(model); + }) + .catch((error) => { + console.log(">>> error fetching available data loaders from ls", error); + return new Promise((resolve) => { + resolve(undefined); + }); + }); + }); + } + + async getAvailableChunkers(params: BIAvailableNodesRequest): Promise { + console.log(">>> requesting bi available chunkers from ls", params); + return new Promise((resolve) => { + StateMachine.langClient() + .getAvailableChunkers(params) + .then((model) => { + console.log(">>> bi available chunkers from ls", model); + resolve(model); + }) + .catch((error) => { + console.log(">>> error fetching available chunkers from ls", error); + return new Promise((resolve) => { + resolve(undefined); + }); + }); + }); + } + async getNodeTemplate(params: BINodeTemplateRequest): Promise { console.log(">>> requesting bi node template from ls", params); params.forceAssign = true; // TODO: remove this @@ -422,7 +557,7 @@ export class BiDiagramRpcManager implements BIDiagramAPI { } async createProject(params: ProjectRequest): Promise { - createBIProjectPure(params.projectName, params.projectPath); + createBIProjectPure(params); } async getWorkspaces(): Promise { @@ -690,17 +825,23 @@ export class BiDiagramRpcManager implements BIDiagramAPI { }); } - async getConfigVariablesV2(): Promise { - return new Promise(async (resolve) => { - const projectPath = path.join(StateMachine.context().projectUri); - const showLibraryConfigVariables = extension.ballerinaExtInstance.showLibraryConfigVariables(); - const variables = await StateMachine.langClient().getConfigVariablesV2({ - projectPath: projectPath, - includeLibraries: showLibraryConfigVariables !== false - }) as ConfigVariableResponse; - resolve(variables); - }); - } +async getConfigVariablesV2(params: ConfigVariableRequest): Promise { + return new Promise(async (resolve) => { + const projectPath = path.join(StateMachine.context().projectUri); + const showLibraryConfigVariables = extension.ballerinaExtInstance.showLibraryConfigVariables(); + + // if params includeLibraries is not set, then use settings + const includeLibraries = params?.includeLibraries !== undefined + ? params.includeLibraries + : showLibraryConfigVariables !== false; + + const variables = await StateMachine.langClient().getConfigVariablesV2({ + projectPath: projectPath, + includeLibraries + }) as ConfigVariableResponse; + resolve(variables); + }); +} async updateConfigVariablesV2(params: UpdateConfigVariableRequestV2): Promise { return new Promise(async (resolve) => { @@ -1817,6 +1958,43 @@ export class BiDiagramRpcManager implements BIDiagramAPI { }); }); } + + async deleteType(params: DeleteTypeRequest): Promise { + return new Promise((resolve, reject) => { + const projectUri = StateMachine.context().projectUri; + const filePath = path.join(projectUri, params.filePath); + StateMachine.langClient().deleteType({ filePath: filePath, lineRange: params.lineRange }) + .then(async (deleteTypeResponse: DeleteTypeResponse) => { + if (deleteTypeResponse.textEdits) { + await updateSourceCode({ textEdits: deleteTypeResponse.textEdits }); + resolve(deleteTypeResponse); + } else { + reject(deleteTypeResponse.errorMsg); + } + }).catch((error) => { + reject(error); + }); + }); + } + + async verifyTypeDelete(params: VerifyTypeDeleteRequest): Promise { + const projectUri = StateMachine.context().projectUri; + const filePath = path.join(projectUri, params.filePath); + + const request: VerifyTypeDeleteRequest = { + filePath: filePath, + startPosition: params.startPosition, + }; + return new Promise((resolve, reject) => { + StateMachine.langClient().verifyTypeDelete(request) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); + } } export function getRepoRoot(projectRoot: string): string | undefined { diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts index 66434b3c407..82ab5d908c4 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts @@ -29,6 +29,7 @@ import { executeCommand, experimentalEnabled, getBallerinaDiagnostics, + getCurrentProjectTomlValues, getTypeCompletions, getWorkspaceFiles, getWorkspaceRoot, @@ -37,6 +38,7 @@ import { openExternalUrl, runBackgroundTerminalCommand, selectFileOrDirPath, + selectFileOrFolderPath, showErrorMessage } from "@wso2/ballerina-core"; import { Messenger } from "vscode-messenger"; @@ -52,8 +54,10 @@ export function registerCommonRpcHandlers(messenger: Messenger) { messenger.onRequest(runBackgroundTerminalCommand, (args: RunExternalCommandRequest) => rpcManger.runBackgroundTerminalCommand(args)); messenger.onNotification(openExternalUrl, (args: OpenExternalUrlRequest) => rpcManger.openExternalUrl(args)); messenger.onRequest(selectFileOrDirPath, (args: FileOrDirRequest) => rpcManger.selectFileOrDirPath(args)); + messenger.onRequest(selectFileOrFolderPath, () => rpcManger.selectFileOrFolderPath()); messenger.onRequest(experimentalEnabled, () => rpcManger.experimentalEnabled()); messenger.onRequest(isNPSupported, () => rpcManger.isNPSupported()); messenger.onRequest(getWorkspaceRoot, () => rpcManger.getWorkspaceRoot()); messenger.onNotification(showErrorMessage, (args: ShowErrorMessageRequest) => rpcManger.showErrorMessage(args)); + messenger.onRequest(getCurrentProjectTomlValues, () => rpcManger.getCurrentProjectTomlValues()); } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-manager.ts index 9d0f7f4da52..5207d4d34a5 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-manager.ts @@ -35,6 +35,7 @@ import { RunExternalCommandResponse, ShowErrorMessageRequest, SyntaxTree, + TomlValues, TypeResponse, WorkspaceFileRequest, WorkspaceRootResponse, @@ -46,8 +47,10 @@ import { URI } from "vscode-uri"; import { extension } from "../../BalExtensionContext"; import { StateMachine } from "../../stateMachine"; import { goToSource } from "../../utils"; -import { askFilePath, askProjectPath, BALLERINA_INTEGRATOR_ISSUES_URL, getUpdatedSource } from "./utils"; -import path from 'path'; +import { askFileOrFolderPath, askFilePath, askProjectPath, BALLERINA_INTEGRATOR_ISSUES_URL, getUpdatedSource } from "./utils"; +import { parse } from 'toml'; +import * as fs from 'fs'; +import path from "path"; export class CommonRpcManager implements CommonRPCAPI { async getTypeCompletions(): Promise { @@ -186,6 +189,19 @@ export class CommonRpcManager implements CommonRPCAPI { }); } + async selectFileOrFolderPath(): Promise { + return new Promise(async (resolve) => { + const selectedFileOrFolder = await askFileOrFolderPath(); + if (!selectedFileOrFolder || selectedFileOrFolder.length === 0) { + window.showErrorMessage('A file or folder must be selected'); + resolve({ path: "" }); + } else { + const fileOrFolderPath = selectedFileOrFolder[0].fsPath; + resolve({ path: fileOrFolderPath }); + } + }); + } + async experimentalEnabled(): Promise { return extension.ballerinaExtInstance.enabledExperimentalFeatures(); } @@ -229,4 +245,33 @@ export class CommonRpcManager implements CommonRPCAPI { async isNPSupported(): Promise { return extension.ballerinaExtInstance.isNPSupported; } + + async getBallerinaProjectRoot(): Promise { + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders) { + throw new Error("No workspaces found."); + } + const workspaceFolderPath = workspaceFolders[0].uri.fsPath; + // Check if workspaceFolderPath is a Ballerina project + // Assuming a Ballerina project must contain a 'Ballerina.toml' file + const ballerinaProjectFile = path.join(workspaceFolderPath, 'Ballerina.toml'); + if (fs.existsSync(ballerinaProjectFile)) { + return workspaceFolderPath; + } + return null; + } + + async getCurrentProjectTomlValues(): Promise { + const projectRoot = await this.getBallerinaProjectRoot(); + const ballerinaTomlPath = path.join(projectRoot, 'Ballerina.toml'); + if (fs.existsSync(ballerinaTomlPath)) { + const tomlContent = await fs.promises.readFile(ballerinaTomlPath, 'utf-8'); + try { + return parse(tomlContent); + } catch (error) { + console.error("Failed to load Ballerina.toml content", error); + return; + } + } + } } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts index ba7ebd76b3b..73d9100ae37 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts @@ -85,6 +85,16 @@ export async function askFilePath() { }); } +export async function askFileOrFolderPath() { + return await window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: true, + canSelectMany: false, + defaultUri: Uri.file(os.homedir()), + title: "Select a file or folder" + }); +} + export async function applyBallerinaTomlEdit(tomlPath: Uri, textEdit: TextEdit) { const workspaceEdit = new WorkspaceEdit(); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/VariableFindingVisitor.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/VariableFindingVisitor.ts similarity index 100% rename from workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/VariableFindingVisitor.ts rename to workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/VariableFindingVisitor.ts diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-handler.ts similarity index 61% rename from workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-handler.ts rename to workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-handler.ts index 1a7c02d385d..537d334da66 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-handler.ts @@ -22,48 +22,56 @@ import { addClauses, AddClausesRequest, addNewArrayElement, + addSubMapping, + AddSubMappingRequest, + AllDataMapperSourceRequest, convertToQuery, ConvertToQueryRequest, + DataMapperModelRequest, + DataMapperSourceRequest, + deleteMapping, + DeleteMappingRequest, + DMModelRequest, getAllDataMapperSource, getDataMapperCodedata, - getProperty, - PropertyRequest, + GetDataMapperCodedataRequest, getDataMapperModel, getDataMapperSource, + getExpandedDMFromDMModel, getInitialIDMSource, - GetInlineDataMapperCodedataRequest, + getProcessTypeReference, + getProperty, getSubMappingCodedata, GetSubMappingCodedataRequest, getVisualizableFields, - addSubMapping, - AddSubMappingRequest, InitialIDMSourceRequest, - InlineAllDataMapperSourceRequest, - InlineDataMapperModelRequest, - InlineDataMapperSourceRequest, - VisualizableFieldsRequest, - deleteMapping, - DeleteMappingRequest, mapWithCustomFn, - MapWithCustomFnRequest + MapWithFnRequest, + mapWithTransformFn, + ProcessTypeReferenceRequest, + PropertyRequest, + VisualizableFieldsRequest } from "@wso2/ballerina-core"; import { Messenger } from "vscode-messenger"; -import { InlineDataMapperRpcManager } from "./rpc-manager"; +import { DataMapperRpcManager } from "./rpc-manager"; -export function registerInlineDataMapperRpcHandlers(messenger: Messenger) { - const rpcManger = new InlineDataMapperRpcManager(); +export function registerDataMapperRpcHandlers(messenger: Messenger) { + const rpcManger = new DataMapperRpcManager(); messenger.onRequest(getInitialIDMSource, (args: InitialIDMSourceRequest) => rpcManger.getInitialIDMSource(args)); - messenger.onRequest(getDataMapperModel, (args: InlineDataMapperModelRequest) => rpcManger.getDataMapperModel(args)); - messenger.onRequest(getDataMapperSource, (args: InlineDataMapperSourceRequest) => rpcManger.getDataMapperSource(args)); + messenger.onRequest(getDataMapperModel, (args: DataMapperModelRequest) => rpcManger.getDataMapperModel(args)); + messenger.onRequest(getDataMapperSource, (args: DataMapperSourceRequest) => rpcManger.getDataMapperSource(args)); messenger.onRequest(getVisualizableFields, (args: VisualizableFieldsRequest) => rpcManger.getVisualizableFields(args)); messenger.onRequest(addNewArrayElement, (args: AddArrayElementRequest) => rpcManger.addNewArrayElement(args)); messenger.onRequest(convertToQuery, (args: ConvertToQueryRequest) => rpcManger.convertToQuery(args)); messenger.onRequest(addClauses, (args: AddClausesRequest) => rpcManger.addClauses(args)); messenger.onRequest(addSubMapping, (args: AddSubMappingRequest) => rpcManger.addSubMapping(args)); messenger.onRequest(deleteMapping, (args: DeleteMappingRequest) => rpcManger.deleteMapping(args)); - messenger.onRequest(mapWithCustomFn, (args: MapWithCustomFnRequest) => rpcManger.mapWithCustomFn(args)); - messenger.onRequest(getDataMapperCodedata, (args: GetInlineDataMapperCodedataRequest) => rpcManger.getDataMapperCodedata(args)); + messenger.onRequest(mapWithCustomFn, (args: MapWithFnRequest) => rpcManger.mapWithCustomFn(args)); + messenger.onRequest(mapWithTransformFn, (args: MapWithFnRequest) => rpcManger.mapWithTransformFn(args)); + messenger.onRequest(getDataMapperCodedata, (args: GetDataMapperCodedataRequest) => rpcManger.getDataMapperCodedata(args)); messenger.onRequest(getSubMappingCodedata, (args: GetSubMappingCodedataRequest) => rpcManger.getSubMappingCodedata(args)); - messenger.onRequest(getAllDataMapperSource, (args: InlineAllDataMapperSourceRequest) => rpcManger.getAllDataMapperSource(args)); + messenger.onRequest(getAllDataMapperSource, (args: AllDataMapperSourceRequest) => rpcManger.getAllDataMapperSource(args)); messenger.onRequest(getProperty, (args: PropertyRequest) => rpcManger.getProperty(args)); + messenger.onRequest(getExpandedDMFromDMModel, (args: DMModelRequest) => rpcManger.getExpandedDMFromDMModel(args)); + messenger.onRequest(getProcessTypeReference, (args: ProcessTypeReferenceRequest) => rpcManger.getProcessTypeReference(args)); } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-manager.ts similarity index 60% rename from workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-manager.ts rename to workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-manager.ts index 3c0087940cd..48c7116ad36 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-manager.ts @@ -21,48 +21,53 @@ import { AddArrayElementRequest, AddClausesRequest, AddSubMappingRequest, + AllDataMapperSourceRequest, ConvertToQueryRequest, + DataMapperAPI, + DataMapperModelRequest, + DataMapperModelResponse, + DataMapperSourceRequest, + DataMapperSourceResponse, DeleteMappingRequest, - EVENT_TYPE, - GetInlineDataMapperCodedataRequest, - GetInlineDataMapperCodedataResponse, + DMModelRequest, + ExpandedDMModel, + ExpandedDMModelResponse, + GetDataMapperCodedataRequest, + GetDataMapperCodedataResponse, GetSubMappingCodedataRequest, InitialIDMSourceRequest, InitialIDMSourceResponse, - InlineAllDataMapperSourceRequest, - InlineDataMapperAPI, - InlineDataMapperModelRequest, - InlineDataMapperModelResponse, - InlineDataMapperSourceRequest, - InlineDataMapperSourceResponse, - MACHINE_VIEW, - MapWithCustomFnRequest, + MapWithFnRequest, + ProcessTypeReferenceRequest, + ProcessTypeReferenceResponse, PropertyRequest, PropertyResponse, VisualizableFieldsRequest, VisualizableFieldsResponse } from "@wso2/ballerina-core"; -import { openView, StateMachine } from "../../stateMachine"; +import { StateMachine } from "../../stateMachine"; import { buildSourceRequests, consolidateTextEdits, + expandDMModel, processSourceRequests, + processTypeReference, setHasStopped, updateAndRefreshDataMapper, updateSource } from "./utils"; -export class InlineDataMapperRpcManager implements InlineDataMapperAPI { +export class DataMapperRpcManager implements DataMapperAPI { async getInitialIDMSource(params: InitialIDMSourceRequest): Promise { - console.log(">>> requesting inline data mapper initial source from ls", params); + console.log(">>> requesting data mapper initial source from ls", params); return new Promise((resolve) => { StateMachine .langClient() .getSourceCode(params) .then(model => { - console.log(">>> inline data mapper initial source from ls", model); + console.log(">>> Data mapper initial source from ls", model); const varName = params.flowNode.properties?.variable?.value as string ?? null; updateSource(model.textEdits, params.filePath, params.flowNode.codedata, varName) .then(codeData => { @@ -70,7 +75,7 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); }) .catch((error) => { - console.log(">>> error fetching inline data mapper initial source from ls", error); + console.log(">>> error fetching data mapper initial source from ls", error); return new Promise((resolve) => { resolve({ artifacts: [], error: error }); }); @@ -78,23 +83,22 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); } - async getDataMapperModel(params: InlineDataMapperModelRequest): Promise { + async getDataMapperModel(params: DataMapperModelRequest): Promise { return new Promise(async (resolve) => { const dataMapperModel = await StateMachine .langClient() - .getInlineDataMapperMappings(params); - - resolve(dataMapperModel as InlineDataMapperModelResponse); + .getDataMapperMappings(params); + resolve(dataMapperModel as DataMapperModelResponse); }); } - async getDataMapperSource(params: InlineDataMapperSourceRequest): Promise { + async getDataMapperSource(params: DataMapperSourceRequest): Promise { return new Promise(async (resolve) => { StateMachine .langClient() - .getInlineDataMapperSource(params) + .getDataMapperSource(params) .then((resp) => { - console.log(">>> inline data mapper initial source from ls", resp); + console.log(">>> Data mapper initial source from ls", resp); updateAndRefreshDataMapper( resp.textEdits, params.filePath, @@ -120,7 +124,7 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); } - async addNewArrayElement(params: AddArrayElementRequest): Promise { + async addNewArrayElement(params: AddArrayElementRequest): Promise { return new Promise(async (resolve) => { await StateMachine .langClient() @@ -131,7 +135,7 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { propertyKey: params.propertyKey }) .then((resp) => { - console.log(">>> inline data mapper add array element response", resp); + console.log(">>> Data mapper add array element response", resp); updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName) .then(() => { resolve({ textEdits: resp.textEdits }); @@ -140,13 +144,13 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); } - async convertToQuery(params: ConvertToQueryRequest): Promise { + async convertToQuery(params: ConvertToQueryRequest): Promise { return new Promise(async (resolve) => { await StateMachine .langClient() .convertToQuery(params) .then((resp) => { - console.log(">>> inline data mapper convert to query response", resp); + console.log(">>> Data mapper convert to query response", resp); updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName) .then(() => { resolve({ textEdits: resp.textEdits }); @@ -155,13 +159,13 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); } - async addClauses(params: AddClausesRequest): Promise { + async addClauses(params: AddClausesRequest): Promise { return new Promise(async (resolve) => { await StateMachine .langClient() .addClauses(params) .then((resp) => { - console.log(">>> inline data mapper add clauses response", resp); + console.log(">>> Data mapper add clauses response", resp); updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName) .then(() => { resolve({ textEdits: resp.textEdits }); @@ -170,13 +174,13 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); } - async addSubMapping(params: AddSubMappingRequest): Promise { + async addSubMapping(params: AddSubMappingRequest): Promise { return new Promise(async (resolve) => { await StateMachine .langClient() .addSubMapping(params) .then((resp) => { - console.log(">>> inline data mapper add sub mapping response", resp); + console.log(">>> Data mapper add sub mapping response", resp); updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName) .then(() => { resolve({ textEdits: resp.textEdits }); @@ -185,27 +189,27 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); } - async getDataMapperCodedata(params: GetInlineDataMapperCodedataRequest): Promise { + async getDataMapperCodedata(params: GetDataMapperCodedataRequest): Promise { return new Promise(async (resolve) => { const dataMapperCodedata = await StateMachine .langClient() - .getDataMapperCodedata(params) as GetInlineDataMapperCodedataResponse; + .getDataMapperCodedata(params) as GetDataMapperCodedataResponse; resolve(dataMapperCodedata); }); } - async getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { + async getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { return new Promise(async (resolve) => { const dataMapperCodedata = await StateMachine .langClient() - .getSubMappingCodedata(params) as GetInlineDataMapperCodedataResponse; + .getSubMappingCodedata(params) as GetDataMapperCodedataResponse; resolve(dataMapperCodedata); }); } - async getAllDataMapperSource(params: InlineAllDataMapperSourceRequest): Promise { + async getAllDataMapperSource(params: AllDataMapperSourceRequest): Promise { return new Promise(async (resolve) => { setHasStopped(false); @@ -226,13 +230,13 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); } - async deleteMapping(params: DeleteMappingRequest): Promise { + async deleteMapping(params: DeleteMappingRequest): Promise { return new Promise(async (resolve) => { await StateMachine .langClient() .deleteMapping(params) .then((resp) => { - console.log(">>> inline data mapper delete mapping response", resp); + console.log(">>> Data mapper delete mapping response", resp); updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName) .then(() => { resolve({ textEdits: resp.textEdits }); @@ -241,13 +245,80 @@ export class InlineDataMapperRpcManager implements InlineDataMapperAPI { }); } - async mapWithCustomFn(params: MapWithCustomFnRequest): Promise { + async mapWithCustomFn(params: MapWithFnRequest): Promise { return new Promise(async (resolve) => { await StateMachine .langClient() .mapWithCustomFn(params) .then((resp) => { - console.log(">>> inline data mapper map with custom fn response", resp); + console.log(">>> Data mapper map with custom fn response", resp); + updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName) + .then(() => { + resolve({ textEdits: resp.textEdits }); + }); + }); + }); + } + + async getExpandedDMFromDMModel(params: DMModelRequest): Promise { + try { + const { model, rootViewId, options = {} } = params; + + // Validate input parameters + if (!model) { + throw new Error("DMModel is required for transformation"); + } + + if (!rootViewId) { + throw new Error("rootViewId is required for transformation"); + } + + // Transform the model using the existing expansion logic + const expandedModel = expandDMModel(model, rootViewId); + + return { + expandedModel, + success: true + }; + } catch (error) { + return { + expandedModel: {} as ExpandedDMModel, + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred during transformation" + }; + } + } + + async getProcessTypeReference(params: ProcessTypeReferenceRequest): Promise { + try { + const { ref, fieldId, model, visitedRefs = new Set() } = params; + + if (!ref || !fieldId || !model) { + throw new Error("ref, fieldId, and model are required parameters"); + } + + const result = processTypeReference(ref, fieldId, model, visitedRefs); + + return { + result, + success: true + }; + } catch (error) { + return { + result: {}, + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred during type reference processing" + }; + } + } + + async mapWithTransformFn(params: MapWithFnRequest): Promise { + return new Promise(async (resolve) => { + await StateMachine + .langClient() + .mapWithTransformFn(params) + .then((resp) => { + console.log(">>> Data mapper map with transform fn response", resp); updateAndRefreshDataMapper(resp.textEdits, params.filePath, params.codedata, params.varName) .then(() => { resolve({ textEdits: resp.textEdits }); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts similarity index 57% rename from workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/utils.ts rename to workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts index 5ea6638b966..1ff56d44fee 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/inline-data-mapper/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts @@ -19,18 +19,28 @@ import { CodeData, ELineRange, Flow, - InlineAllDataMapperSourceRequest, - InlineDataMapperSourceRequest, - InlineDataMapperSourceResponse, + AllDataMapperSourceRequest, + DataMapperSourceRequest, + DataMapperSourceResponse, NodePosition, ProjectStructureArtifactResponse, TextEdit, - traverseFlow + traverseFlow, + RecordType, + DMModel, + IOType, + TypeKind, + IOTypeField, + IORoot, + ExpandModelOptions, + ExpandedDMModel } from "@wso2/ballerina-core"; import { updateSourceCode, UpdateSourceCodeRequest } from "../../utils"; -import { StateMachine, updateInlineDataMapperView } from "../../stateMachine"; +import { StateMachine, updateDataMapperView } from "../../stateMachine"; import { VariableFindingVisitor } from "./VariableFindingVisitor"; +const MAX_NESTED_DEPTH = 4; + /** * Shared state for data mapper operations */ @@ -102,11 +112,11 @@ export async function updateSourceCodeIteratively(updateSourceCodeRequest: Updat return await updateSourceCode(updateSourceCodeRequest); } - // need to prioritize if file path ends with functions.bal + // need to prioritize if file path ends with functions.bal or data_mappings.bal filePaths.sort((a, b) => { - // Prioritize files ending with functions.bal - const aEndsWithFunctions = a.endsWith('functions.bal') ? 1 : 0; - const bEndsWithFunctions = b.endsWith('functions.bal') ? 1 : 0; + // Prioritize files ending with functions.bal or data_mappings.bal + const aEndsWithFunctions = (a.endsWith("functions.bal") || a.endsWith("data_mappings.bal")) ? 1 : 0; + const bEndsWithFunctions = (b.endsWith("functions.bal") || b.endsWith("data_mappings.bal")) ? 1 : 0; return bEndsWithFunctions - aEndsWithFunctions; // Sort descending }); @@ -147,6 +157,23 @@ export async function updateSource( throw new Error(`No artifact found for file: ${filePath} within the specified line range`); } + // If the artifact is a data mapper(reusable), return the code data for the data mapper + if (relevantArtifact.type === "DATA_MAPPER") { + return { + lineRange: { + fileName: relevantArtifact.path, + startLine: { + line: relevantArtifact.position?.startLine, + offset: relevantArtifact.position?.startColumn + }, + endLine: { + line: relevantArtifact.position?.endLine, + offset: relevantArtifact.position?.endColumn + } + } + }; + } + // Get the flow model for the updated artifact const flowModel = await getFlowModelForArtifact(relevantArtifact, filePath); if (!flowModel) { @@ -234,7 +261,7 @@ async function getFlowModelForArtifact(artifact: ProjectStructureArtifactRespons } }); - console.log("Flow model retrieved for inline data mapper:", flowModelResponse); + console.log("Flow model retrieved for data mapper:", flowModelResponse); return flowModelResponse.flowModel || null; } catch (error) { @@ -272,14 +299,14 @@ function applySourceCodeHack(codeData: CodeData): void { /** * Updates the data mapper view with the provided code data after applying necessary transformations. */ -function updateDataMapperView(codeData: CodeData | null, varName: string): void { +function updateView(codeData: CodeData | null, varName: string): void { if (!codeData) { console.warn(`No code data available for variable: ${varName}`); return; } applySourceCodeHack(codeData); - updateInlineDataMapperView(codeData, varName); + updateDataMapperView(codeData, varName); } /** @@ -297,7 +324,7 @@ export async function updateAndRefreshDataMapper( const newCodeData = withinSubMapping ? await updateSubMappingSource(textEdits, filePath, codedata, targetField) : await updateSource(textEdits, filePath, codedata, varName); - updateDataMapperView(newCodeData, varName); + updateView(newCodeData, varName); } catch (error) { console.error(`Failed to update and refresh data mapper for variable "${varName}":`, error); throw new Error(`Data mapper update failed`); @@ -314,7 +341,7 @@ export async function refreshDataMapper( ): Promise { try { const newCodeData = await fetchDataMapperCodeData(filePath, codedata, varName); - updateDataMapperView(newCodeData, varName); + updateView(newCodeData, varName); } catch (error) { console.error(`Failed to refresh data mapper for variable "${varName}":`, error); throw new Error(`Data mapper refresh failed.`); @@ -324,7 +351,7 @@ export async function refreshDataMapper( /** * Builds individual source requests from the provided parameters by mapping over each mapping. */ -export function buildSourceRequests(params: InlineAllDataMapperSourceRequest): InlineDataMapperSourceRequest[] { +export function buildSourceRequests(params: AllDataMapperSourceRequest): DataMapperSourceRequest[] { return params.mappings.map(mapping => ({ filePath: params.filePath, codedata: params.codedata, @@ -337,14 +364,14 @@ export function buildSourceRequests(params: InlineAllDataMapperSourceRequest): I /** * Handles operation cancellation and error logging for each request. */ -export async function processSourceRequests(requests: InlineDataMapperSourceRequest[]): Promise[]> { +export async function processSourceRequests(requests: DataMapperSourceRequest[]): Promise[]> { return Promise.allSettled( requests.map(async (request) => { if (getHasStopped()) { throw new Error("Operation was stopped"); } try { - return await StateMachine.langClient().getInlineDataMapperSource(request); + return await StateMachine.langClient().getDataMapperSource(request); } catch (error) { console.error("Error in getDataMapperSource:", error); throw error; @@ -357,7 +384,7 @@ export async function processSourceRequests(requests: InlineDataMapperSourceRequ * Consolidates text edits from multiple source responses into a single optimized collection. */ export function consolidateTextEdits( - responses: PromiseSettledResult[], + responses: PromiseSettledResult[], totalMappings: number ): { [key: string]: TextEdit[] } { const allTextEdits: { [key: string]: TextEdit[] } = {}; @@ -443,7 +470,7 @@ function isWithinArtifact( artifactPath: string, filePath: string, artifactPosition: NodePosition, - varDeclRange: ELineRange + originalRange: ELineRange ) { if (artifactPath !== filePath) { return false; @@ -451,7 +478,289 @@ function isWithinArtifact( const artifactStartLine = artifactPosition.startLine; const artifactEndLine = artifactPosition.endLine; - const varDeclStartLine = varDeclRange.startLine.line; + const originalStartLine = originalRange.startLine.line; + + return artifactStartLine <= originalStartLine && artifactEndLine >= originalStartLine; +} + +/** + * Expands a DMModel into an ExpandedDMModel + */ +export function expandDMModel( + model: DMModel, + rootViewId: string, + options: ExpandModelOptions = {} +): ExpandedDMModel { + const { + processInputs = true, + processOutput = true, + processSubMappings = true, + previousModel + } = options; + + return { + inputs: processInputs + ? processInputRoots(model) + : previousModel?.inputs || [], + output: processOutput + ? processIORoot(model.output, model) + : previousModel?.output!, + subMappings: processSubMappings + ? model.subMappings?.map(subMapping => processIORoot(subMapping, model)) + : previousModel?.subMappings || [], + mappings: model.mappings, + query: model.query, + source: "", + rootViewId + }; +} + +/** + * Preprocesses inputs of the DMModel (separates focus inputs from regular inputs) + * Processes each regular input into an IOType + */ +function processInputRoots(model: DMModel): IOType[] { + const inputs: IORoot[] = []; + const focusInputs: Record = {}; + for (const input of model.inputs) { + if (input.focusExpression) { + focusInputs[input.focusExpression] = input as IOTypeField; + } else { + inputs.push(input); + } + } + const preProcessedModel: DMModel = { + ...model, + inputs, + focusInputs + }; + + return inputs.map(input => processIORoot(input, preProcessedModel)); +} + +/** + * Processes an IORoot (input or output) into an IOType + */ +function processIORoot(root: IORoot, model: DMModel): IOType { + const ioType = createBaseIOType(root); + + if (root.kind === TypeKind.Array && root.member) { + return { + ...ioType, + member: processArray(root, root.name, root.member, model, new Set()) + }; + } + + if (root.ref) { + return { + ...ioType, + ...processTypeReference(root.ref, root.name, model, new Set()) + }; + } + + return ioType; +} + +/** + * Creates a base IOType from an IORoot + */ +function createBaseIOType(root: IORoot): IOType { + return { + id: root.name, + name: root.name, + displayName: root.displayName, + typeName: root.typeName, + kind: root.kind, + ...(root.category && { category: root.category }), + ...(root.optional !== undefined && { optional: root.optional }) + }; +} + +/** + * Processes array type fields and their members + */ +function processArray( + field: IOTypeField, + parentId: string, + member: IOTypeField, + model: DMModel, + visitedRefs: Set +): IOType { + let fieldId = generateFieldId(parentId, member.name); + + let isFocused = false; + if (model.focusInputs) { + const focusMember = model.focusInputs[parentId]; + if (focusMember) { + member = focusMember; + parentId = member.name; + fieldId = member.name; + isFocused = true; + } + } - return artifactStartLine <= varDeclStartLine && artifactEndLine >= varDeclStartLine; + const ioType: IOType = { + id: fieldId, + name: member.name, + displayName: member.displayName, + typeName: member.typeName!, + kind: member.kind, + ...(isFocused && { isFocused }), + ...(member.optional !== undefined && { optional: member.optional }) + }; + + if (member.kind === TypeKind.Array && member.member) { + return { + ...ioType, + member: processArray(field, parentId, member.member, model, visitedRefs) + }; + } + + if (member.kind === TypeKind.Union && member.members) { + return { + ...ioType, + members: processUnion(member.members, fieldId, model, visitedRefs) + }; + } + + if (member.ref) { + return { + ...ioType, + ...processTypeReference(member.ref, parentId, model, visitedRefs) + }; + } + + return ioType; +} + +/** + * Generates a unique field ID by combining parent ID and field name + */ +function generateFieldId(parentId: string, fieldName: string): string { + return `${parentId}.${fieldName}`; +} + +/** + * Processes union type members and returns an array of IOType objects + */ +function processUnion( + unionMembers: IOTypeField[], + parentFieldId: string, + model: DMModel, + visitedRefs: Set +): IOType[] { + return unionMembers.map(unionMember => { + const unionMemberType: IOType = { + id: generateFieldId(parentFieldId, unionMember.name || 'member'), + name: unionMember.name, + displayName: unionMember.displayName, + typeName: unionMember.typeName, + kind: unionMember.kind, + ...(unionMember.optional !== undefined && { optional: unionMember.optional }) + }; + + // Process union member recursively if it has a reference + if (unionMember.ref) { + return { + ...unionMemberType, + ...processTypeReference(unionMember.ref, parentFieldId, model, visitedRefs) + }; + } + + // Process union member if it's an array + if (unionMember.kind === TypeKind.Array && unionMember.member) { + return { + ...unionMemberType, + member: processArray(unionMember, parentFieldId, unionMember.member, model, visitedRefs) + }; + } + + return unionMemberType; + }); +} + +/** + * Processes a type reference and returns the appropriate IOType structure + */ +export function processTypeReference( + ref: string, + fieldId: string, + model: DMModel, + visitedRefs: Set +): Partial { + const refType = model.refs[ref]; + if ('fields' in refType) { + if (visitedRefs.has(ref)) { + return { + ref: ref, + fields: [], + isRecursive: true, + isDeepNested: true, + }; + } + visitedRefs.add(ref); + if (visitedRefs.size > MAX_NESTED_DEPTH) { + return { + ref: ref, + fields: [], + isDeepNested: true + }; + } + return { + fields: processTypeFields(refType, fieldId, model, visitedRefs) + }; + } + if ('members' in refType) { + return { + members: refType.members || [] + }; + } + return {}; +} + +/** + * Processes fields of a record type + */ +function processTypeFields( + type: RecordType, + parentId: string, + model: DMModel, + visitedRefs: Set +): IOType[] { + if (!type.fields) { return []; } + + return type.fields.map(field => { + const fieldId = generateFieldId(parentId, field.name!); + const ioType: IOType = { + id: fieldId, + name: field.name, + displayName: field.displayName, + typeName: field.typeName, + kind: field.kind, + ...(field.optional !== undefined && { optional: field.optional }) + }; + + if (field.kind === TypeKind.Record && field.ref) { + return { + ...ioType, + ...processTypeReference(field.ref, fieldId, model, new Set(visitedRefs)) + }; + } + + if (field.kind === TypeKind.Array && field.member) { + return { + ...ioType, + member: processArray(field, fieldId, field.member, model, new Set(visitedRefs)) + }; + } + + if (field.kind === TypeKind.Union && field.members) { + return { + ...ioType, + members: processUnion(field.members, fieldId, model, new Set(visitedRefs)) + }; + } + + return ioType; + }); } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/migrate-integration/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/migrate-integration/rpc-handler.ts new file mode 100644 index 00000000000..157bd86354d --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/migrate-integration/rpc-handler.ts @@ -0,0 +1,44 @@ +/** + * 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. + * + * THIS FILE INCLUDES AUTO GENERATED CODE + */ +import { + getMigrationTools, + importIntegration, + ImportIntegrationRPCRequest, + migrateProject, + MigrateRequest, + MigrationToolPullRequest, + openMigrationReport, + OpenMigrationReportRequest, + pullMigrationTool, + saveMigrationReport, + SaveMigrationReportRequest +} from "@wso2/ballerina-core"; +import { Messenger } from "vscode-messenger"; +import { MigrateIntegrationRpcManager } from "./rpc-manager"; + +export function registerMigrateIntegrationRpcHandlers(messenger: Messenger) { + const rpcManger = new MigrateIntegrationRpcManager(); + messenger.onRequest(getMigrationTools, () => rpcManger.getMigrationTools()); + messenger.onNotification(pullMigrationTool, (args: MigrationToolPullRequest) => rpcManger.pullMigrationTool(args)); + messenger.onRequest(importIntegration, (args: ImportIntegrationRPCRequest) => rpcManger.importIntegration(args)); + messenger.onNotification(openMigrationReport, (args: OpenMigrationReportRequest) => rpcManger.openMigrationReport(args)); + messenger.onNotification(saveMigrationReport, (args: SaveMigrationReportRequest) => rpcManger.saveMigrationReport(args)); + messenger.onNotification(migrateProject, (args: MigrateRequest) => rpcManger.migrateProject(args)); +} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/migrate-integration/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/migrate-integration/rpc-manager.ts new file mode 100644 index 00000000000..65d64dc91f5 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/migrate-integration/rpc-manager.ts @@ -0,0 +1,95 @@ +/** + * 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. + * + * THIS FILE INCLUDES AUTO GENERATED CODE + */ +import { + GetMigrationToolsResponse, + ImportIntegrationRequest, + ImportIntegrationResponse, + ImportIntegrationRPCRequest, + MigrateIntegrationAPI, + MigrateRequest, + OpenMigrationReportRequest, + SaveMigrationReportRequest +} from "@wso2/ballerina-core"; +import { StateMachine } from "../../stateMachine"; +import { createBIProjectFromMigration, getUsername, sanitizeName } from "../../utils/bi"; +import { pullMigrationTool } from "../../utils/migrate-integration"; +import { MigrationReportWebview } from "../../views/migration-report/webview"; + +export class MigrateIntegrationRpcManager implements MigrateIntegrationAPI { + async pullMigrationTool(args: { toolName: string; version: string }): Promise { + try { + await pullMigrationTool(args.toolName, args.version); + } catch (error) { + console.error(`Failed to pull migration tool '${args.toolName}' version '${args.version}':`, error); + throw error; + } + } + + async importIntegration(params: ImportIntegrationRPCRequest): Promise { + const orgName = getUsername(); + const langParams: ImportIntegrationRequest = { + orgName: orgName, + packageName: sanitizeName(params.packageName), + sourcePath: params.sourcePath, + parameters: params.parameters, + }; + StateMachine.langClient().registerMigrationToolCallbacks(); + switch (params.commandName) { + case "migrate-tibco": + return StateMachine.langClient().importTibcoToBI(langParams); + case "migrate-mule": + return StateMachine.langClient().importMuleToBI(langParams); + default: + console.error(`Unsupported integration type: ${params.commandName}`); + throw new Error(`Unsupported integration type: ${params.commandName}`); + } + } + + async getMigrationTools(): Promise { + return StateMachine.langClient().getMigrationTools(); + } + + async openMigrationReport(params: OpenMigrationReportRequest): Promise { + MigrationReportWebview.createOrShow(params.fileName, params.reportContent); + } + + async saveMigrationReport(params: SaveMigrationReportRequest): Promise { + const vscode = await import('vscode'); + + // Show save dialog + const saveUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(params.defaultFileName), + filters: { + 'HTML files': ['html'], + 'All files': ['*'] + } + }); + + if (saveUri) { + // Write the report content to the selected file + await vscode.workspace.fs.writeFile(saveUri, Buffer.from(params.reportContent, 'utf8')); + vscode.window.showInformationMessage(`Migration report saved to ${saveUri.fsPath}`); + } + } + + async migrateProject(params: MigrateRequest): Promise { + createBIProjectFromMigration(params); + } +} diff --git a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts index 37fe9b3494d..e9fe3a0a824 100644 --- a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts @@ -2,10 +2,10 @@ import { ExtendedLangClient } from './core'; import { createMachine, assign, interpret } from 'xstate'; import { activateBallerina } from './extension'; -import { EVENT_TYPE, SyntaxTree, History, HistoryEntry, MachineStateValue, STByRangeRequest, SyntaxTreeResponse, UndoRedoManager, VisualizerLocation, webviewReady, MACHINE_VIEW, DIRECTORY_MAP, SCOPE, ProjectStructureResponse, ArtifactData, ProjectStructureArtifactResponse, CodeData } from "@wso2/ballerina-core"; +import { EVENT_TYPE, SyntaxTree, History, HistoryEntry, MachineStateValue, STByRangeRequest, SyntaxTreeResponse, UndoRedoManager, VisualizerLocation, webviewReady, MACHINE_VIEW, DIRECTORY_MAP, SCOPE, ProjectStructureResponse, ArtifactData, ProjectStructureArtifactResponse, CodeData, getVisualizerLocation, ProjectDiagnosticsResponse } from "@wso2/ballerina-core"; import { fetchAndCacheLibraryData } from './features/library-browser'; import { VisualizerWebview } from './views/visualizer/webview'; -import { commands, extensions, Uri, window, workspace, WorkspaceFolder } from 'vscode'; +import { commands, extensions, ShellExecution, Task, TaskDefinition, tasks, Uri, window, workspace, WorkspaceFolder } from 'vscode'; import { notifyCurrentWebview, RPCLayer } from './RPCLayer'; import { generateUid, getComponentIdentifier, getNodeByIndex, getNodeByName, getNodeByUid, getView } from './utils/state-machine-utils'; import * as path from 'path'; @@ -21,6 +21,7 @@ interface MachineContext extends VisualizerLocation { langClient: ExtendedLangClient | null; isBISupported: boolean; errorCode: string | null; + dependenciesResolved?: boolean; } export let history: History; @@ -36,7 +37,8 @@ const stateMachine = createMachine( langClient: null, errorCode: null, isBISupported: false, - view: MACHINE_VIEW.Overview + view: MACHINE_VIEW.Overview, + dependenciesResolved: false }, on: { RESET_TO_EXTENSION_READY: { @@ -48,7 +50,10 @@ const stateMachine = createMachine( projectStructure: (context, event) => event.payload, }), () => { - commands.executeCommand("BI.project-explorer.refresh"); + // Use queueMicrotask to ensure context is updated before command execution + queueMicrotask(() => { + commands.executeCommand("BI.project-explorer.refresh"); + }); } ] }, @@ -58,11 +63,38 @@ const stateMachine = createMachine( documentUri: (context, event) => event.viewLocation.documentUri ? event.viewLocation.documentUri : context.documentUri, position: (context, event) => event.viewLocation.position ? event.viewLocation.position : context.position, identifier: (context, event) => event.viewLocation.identifier ? event.viewLocation.identifier : context.identifier, + addType: (context, event) => event.viewLocation?.addType !== undefined ? event.viewLocation.addType : context?.addType, }) ] + }, + SWITCH_PROJECT: { + target: "switch_project" } }, states: { + switch_project: { + invoke: { + src: checkForProjects, + onDone: [ + { + target: "viewActive.viewReady", + actions: [ + assign({ + isBI: (context, event) => event.data.isBI, + projectUri: (context, event) => event.data.projectPath, + scope: (context, event) => event.data.scope, + org: (context, event) => event.data.orgName, + package: (context, event) => event.data.packageName, + }), + async (context, event) => { + await buildProjectArtifactsStructure(event.data.projectPath, StateMachine.langClient(), true); + notifyCurrentWebview(); + } + ] + } + ], + } + }, initialize: { invoke: { src: checkForProjects, @@ -171,9 +203,26 @@ const stateMachine = createMachine( viewInit: { invoke: { src: 'openWebView', + onDone: [ + { + target: "resolveMissingDependencies", + cond: (context) => !context.dependenciesResolved + }, + { + target: "webViewLoading" + } + ] + } + }, + resolveMissingDependencies: { + invoke: { + src: 'resolveMissingDependencies', onDone: { - target: "webViewLoading" - }, + target: "webViewLoading", + actions: assign({ + dependenciesResolved: true + }) + } } }, webViewLoading: { @@ -313,6 +362,85 @@ const stateMachine = createMachine( } }); }, + resolveMissingDependencies: (context, event) => { + return new Promise(async (resolve, reject) => { + if (context?.projectUri) { + const diagnostics: ProjectDiagnosticsResponse = await StateMachine.langClient().getProjectDiagnostics({ + projectRootIdentifier: { + uri: Uri.file(context.projectUri).toString(), + } + }); + + // Check if there are any "cannot resolve module" diagnostics + const hasMissingModuleDiagnostics = diagnostics.errorDiagnosticMap && + Object.values(diagnostics.errorDiagnosticMap).some(fileDiagnostics => + fileDiagnostics.some(diagnostic => + diagnostic.message.includes('cannot resolve module') + ) + ); + + // Only proceed with build if there are missing module diagnostics + if (!hasMissingModuleDiagnostics) { + resolve(true); + return; + } + + const taskDefinition: TaskDefinition = { + type: 'shell', + task: 'run' + }; + + let buildCommand = 'bal build'; + + const config = workspace.getConfiguration('ballerina'); + const ballerinaHome = config.get('home'); + if (ballerinaHome) { + buildCommand = path.join(ballerinaHome, 'bin', buildCommand); + } + + const execution = new ShellExecution(buildCommand); + + if (!workspace.workspaceFolders || workspace.workspaceFolders.length === 0) { + resolve(true); + return; + } + + + const task = new Task( + taskDefinition, + workspace.workspaceFolders![0], + 'Ballerina Build', + 'ballerina', + execution + ); + + try { + const taskExecution = await tasks.executeTask(task); + + // Wait for task completion + await new Promise((taskResolve) => { + // Listen for task completion + const disposable = tasks.onDidEndTask((taskEndEvent) => { + if (taskEndEvent.execution === taskExecution) { + console.log('Build task completed'); + + // Close the terminal pane on completion + commands.executeCommand('workbench.action.closePanel'); + + disposable.dispose(); + taskResolve(); + } + }); + }); + + } catch (error) { + window.showErrorMessage(`Failed to build Ballerina package: ${error}`); + } + } + + resolve(true); + }); + }, findView(context, event): Promise { return new Promise(async (resolve, reject) => { if (!context.view && context.langClient) { @@ -498,7 +626,7 @@ export function openView(type: EVENT_TYPE, viewLocation: VisualizerLocation, res stateService.send({ type: type, viewLocation: viewLocation }); } -export function updateView(refreshTreeView?: boolean) { +export function updateView(refreshTreeView?: boolean, projectUri?: string) { let lastView = getLastHistory(); // Step over to the next location if the last view is skippable if (!refreshTreeView && lastView?.location.view.includes("SKIP")) { @@ -538,12 +666,12 @@ export function updateView(refreshTreeView?: boolean) { stateService.send({ type: "VIEW_UPDATE", viewLocation: lastView ? newLocation : { view: "Overview" } }); if (refreshTreeView) { - buildProjectArtifactsStructure(StateMachine.context().projectUri, StateMachine.langClient(), true); + buildProjectArtifactsStructure(projectUri || StateMachine.context().projectUri, StateMachine.langClient(), true); } notifyCurrentWebview(); } -export function updateInlineDataMapperView(codedata?: CodeData, variableName?: string) { +export function updateDataMapperView(codedata?: CodeData, variableName?: string) { let lastView: HistoryEntry = getLastHistory(); lastView.location.dataMapperMetadata = { codeData: codedata, diff --git a/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts b/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts index 7dcc552440f..3b258df7b44 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts @@ -191,6 +191,10 @@ export const getAccessToken = async (): Promise => { resolve(credentials.secrets.apiKey); return; + case LoginMethod.AWS_BEDROCK: + resolve(credentials.secrets.accessKeyId); + return; + default: const { loginMethod }: AuthCredentials = credentials; reject(new Error(`Unsupported login method: ${loginMethod}`)); @@ -205,6 +209,19 @@ export const getAccessToken = async (): Promise => { }); }; +export const getAwsBedrockCredentials = async (): Promise<{ + accessKeyId: string; + secretAccessKey: string; + region: string; + sessionToken?: string; +} | undefined> => { + const credentials = await getAuthCredentials(); + if (!credentials || credentials.loginMethod !== LoginMethod.AWS_BEDROCK) { + return undefined; + } + return credentials.secrets; +}; + export const getRefreshedAccessToken = async (): Promise => { return new Promise(async (resolve, reject) => { const CommonReqHeaders = { diff --git a/workspaces/ballerina/ballerina-extension/src/utils/bi.ts b/workspaces/ballerina/ballerina-extension/src/utils/bi.ts index fbe6f502c97..5a4610ab88f 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/bi.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/bi.ts @@ -15,20 +15,77 @@ * specific language governing permissions and limitations * under the License. */ + import { exec } from "child_process"; import { window, commands, workspace, Uri } from "vscode"; import * as fs from 'fs'; import path from "path"; -import { BallerinaProjectComponents, ComponentRequest, CreateComponentResponse, createFunctionSignature, EVENT_TYPE, NodePosition, STModification, SyntaxTreeResponse } from "@wso2/ballerina-core"; +import { BallerinaProjectComponents, ComponentRequest, CreateComponentResponse, createFunctionSignature, EVENT_TYPE, MigrateRequest, NodePosition, ProjectRequest, STModification, SyntaxTreeResponse } from "@wso2/ballerina-core"; import { StateMachine, history, openView } from "../stateMachine"; import { applyModifications, modifyFileContent, writeBallerinaFileDidOpen } from "./modification"; import { ModulePart, STKindChecker } from "@wso2/syntax-tree"; import { URI } from "vscode-uri"; +import { debug } from "./logger"; export const README_FILE = "readme.md"; export const FUNCTIONS_FILE = "functions.bal"; export const DATA_MAPPING_FILE = "data_mappings.bal"; +const settingsJsonContent = ` +{ + "ballerina.isBI": true +} +`; + +const launchJsonContent = ` +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Ballerina Debug", + "type": "ballerina", + "request": "launch", + "programArgs": [], + "commandOptions": [], + "env": {} + }, + { + "name": "Ballerina Test", + "type": "ballerina", + "request": "launch", + "debugTests": true, + "programArgs": [], + "commandOptions": [], + "env": {} + }, + { + "name": "Ballerina Remote", + "type": "ballerina", + "request": "attach", + "debuggeeHost": "127.0.0.1", + "debuggeePort": "5005" + } + ] +} +`; + +const gitignoreContent = ` +# Ballerina generates this directory during the compilation of a package. +# It contains compiler-generated artifacts and the final executable if this is an application package. +target/ + +# Ballerina maintains the compiler-generated source code here. +# Remove this if you want to commit generated sources. +generated/ + +# Contains configuration values used during development time. +# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. +Config.toml +`; + export function openBIProject() { window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, openLabel: 'Open Integration' }) .then(uri => { @@ -77,97 +134,67 @@ export function createBIProject(name: string, isService: boolean) { }); } -export function createBIProjectPure(name: string, projectPath: string) { - const projectLocation = projectPath; +export function getUsername(): string { + // Get current username from the system across different OS platforms + let username: string; + if (process.platform === 'win32') { + // Windows + username = process.env.USERNAME || 'myOrg'; + } else { + // macOS and Linux + username = process.env.USER || 'myOrg'; + } + return username; +} - name = sanitizeName(name); - const projectRoot = path.join(projectLocation, name); - // Create project root directory - if (!fs.existsSync(projectRoot)) { - fs.mkdirSync(projectRoot); +function setupProjectInfo(projectRequest: ProjectRequest) { + const sanitizedPackageName = sanitizeName(projectRequest.packageName); + + const projectRoot = projectRequest.createDirectory + ? path.join(projectRequest.projectPath, sanitizedPackageName) + : projectRequest.projectPath; + + // Create project root directory if needed + if (projectRequest.createDirectory && !fs.existsSync(projectRoot)) { + fs.mkdirSync(projectRoot, { recursive: true }); } - // Get current username from the system across different OS platforms - let username; - try { - if (process.platform === 'win32') { - // Windows - username = process.env.USERNAME || 'myOrg'; - } else { - // macOS and Linux - username = process.env.USER || 'myOrg'; - } - } catch (error) { - console.error('Error getting username:', error); + let finalOrgName = projectRequest.orgName; + if (!finalOrgName) { + finalOrgName = getUsername(); } + const finalVersion = projectRequest.version || "0.1.0"; + + return { + sanitizedPackageName, + projectRoot, + finalOrgName, + finalVersion, + packageName: projectRequest.packageName, + integrationName: projectRequest.projectName + }; +} + +export function createBIProjectPure(projectRequest: ProjectRequest) { + const projectInfo = setupProjectInfo(projectRequest); + const { projectRoot, finalOrgName, finalVersion, packageName: finalPackageName, integrationName } = projectInfo; + const EMPTY = "\n"; const ballerinaTomlContent = ` [package] -org = "${username}" -name = "${name}" -version = "0.1.0" +org = "${finalOrgName}" +name = "${finalPackageName}" +version = "${finalVersion}" +title = "${integrationName}" [build-options] sticky = true `; - const settingsJsonContent = ` -{ - "ballerina.isBI": true -} -`; - const launchJsonContent = ` -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Ballerina Debug", - "type": "ballerina", - "request": "launch", - "programArgs": [], - "commandOptions": [], - "env": {} - }, - { - "name": "Ballerina Test", - "type": "ballerina", - "request": "launch", - "debugTests": true, - "programArgs": [], - "commandOptions": [], - "env": {} - }, - { - "name": "Ballerina Remote", - "type": "ballerina", - "request": "attach", - "debuggeeHost": "127.0.0.1", - "debuggeePort": "5005" - } - ] -} -`; - - const gitignoreContent = ` -# Ballerina generates this directory during the compilation of a package. -# It contains compiler-generated artifacts and the final executable if this is an application package. -target/ - -# Ballerina maintains the compiler-generated source code here. -# Remove this if you want to commit generated sources. -generated/ - -# Contains configuration values used during development time. -# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. -Config.toml -`; // Create Ballerina.toml file const ballerinaTomlPath = path.join(projectRoot, 'Ballerina.toml'); @@ -223,6 +250,47 @@ Config.toml commands.executeCommand('vscode.openFolder', Uri.file(path.resolve(projectRoot))); } +export async function createBIProjectFromMigration(params: MigrateRequest) { + const projectInfo = setupProjectInfo(params.project); + const { projectRoot, sanitizedPackageName } = projectInfo; + + const EMPTY = "\n"; + // Write files based on keys in params.textEdits + for (const [fileName, fileContent] of Object.entries(params.textEdits)) { + let content = fileContent; + const filePath = path.join(projectRoot, fileName); + + if (fileName === "Ballerina.toml") { + content = content.replace(/name = ".*?"/, `name = "${sanitizedPackageName}"`); + content = content.replace(/org = ".*?"/, `org = "${projectInfo.finalOrgName}"`); + content = content.replace(/version = ".*?"/, `version = "${projectInfo.finalVersion}"\ntitle = "${projectInfo.integrationName}"`); + } + + writeBallerinaFileDidOpen(filePath, content || EMPTY); + } + + // Create a .vscode folder + const vscodeDir = path.join(projectRoot, '.vscode'); + if (!fs.existsSync(vscodeDir)) { + fs.mkdirSync(vscodeDir); + } + + // Create launch.json file + const launchPath = path.join(vscodeDir, 'launch.json'); + fs.writeFileSync(launchPath, launchJsonContent.trim()); + + // Create settings.json file + const settingsPath = path.join(vscodeDir, 'settings.json'); + fs.writeFileSync(settingsPath, settingsJsonContent); + + // Create .gitignore file + const gitignorePath = path.join(projectRoot, '.gitignore'); + fs.writeFileSync(gitignorePath, gitignoreContent.trim()); + + debug(`BI project created successfully at ${projectRoot}`); + commands.executeCommand('vscode.openFolder', Uri.file(path.resolve(projectRoot))); +} + export async function createBIAutomation(params: ComponentRequest): Promise { return new Promise(async (resolve) => { const functionFile = await handleAutomationCreation(params); @@ -350,7 +418,7 @@ export async function handleFunctionCreation(targetFile: string, params: Compone // <---------- Function Source Generation END--------> // Test_Integration test_integration Test Integration testIntegration -> testintegration export function sanitizeName(name: string): string { - return name.replace(/[^a-z0-9]/gi, '_').toLowerCase(); // Replace invalid characters with underscores + return name.replace(/[^a-z0-9]_./gi, '_').toLowerCase(); // Replace invalid characters with underscores } // ------------------- HACKS TO MANIPULATE PROJECT FILES ----------------> diff --git a/workspaces/ballerina/ballerina-extension/src/utils/config.ts b/workspaces/ballerina/ballerina-extension/src/utils/config.ts index 47ca4ad8bc9..53073684086 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/config.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/config.ts @@ -62,6 +62,16 @@ export function isWindows(): boolean { return process.platform === "win32"; } +export function isWSL(): boolean { + // Check for WSL environment indicators + return process.platform === "linux" && ( + process.env.WSL_DISTRO_NAME !== undefined || + process.env.WSLENV !== undefined || + (process.env.PATH && process.env.PATH.includes('/mnt/c/')) || + (process.env.TERM_PROGRAM && process.env.TERM_PROGRAM.includes('vscode')) + ); +} + export function isSupportedVersion(ballerinaExtInstance: BallerinaExtension, supportedRelease: VERSION, supportedVersion: number): boolean { const ballerinaVersion: string = ballerinaExtInstance.ballerinaVersion.toLocaleLowerCase(); @@ -123,26 +133,26 @@ export function checkIsBallerina(uri: Uri): boolean { export function getOrgPackageName(projectPath: string): { orgName: string, packageName: string } { const ballerinaTomlPath = path.join(projectPath, 'Ballerina.toml'); - + // Regular expressions for Ballerina.toml parsing const ORG_REGEX = /\[package\][\s\S]*?org\s*=\s*["']([^"']*)["']/; const NAME_REGEX = /\[package\][\s\S]*?name\s*=\s*["']([^"']*)["']/; if (!fs.existsSync(ballerinaTomlPath)) { - return {orgName: '', packageName: ''}; + return { orgName: '', packageName: '' }; } - + try { const tomlContent = fs.readFileSync(ballerinaTomlPath, 'utf8'); - + // Extract org name and package name const orgName = tomlContent.match(ORG_REGEX)?.[1] || ''; const packageName = tomlContent.match(NAME_REGEX)?.[1] || ''; - - return {orgName, packageName}; + + return { orgName, packageName }; } catch (error) { console.error(`Error reading Ballerina.toml: ${error}`); - return {orgName: '', packageName: ''}; + return { orgName: '', packageName: '' }; } } diff --git a/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts b/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts new file mode 100644 index 00000000000..5851b19636c --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts @@ -0,0 +1,265 @@ +/** + * 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 { DownloadProgress, onDownloadProgress } from "@wso2/ballerina-core"; +import { exec } from "child_process"; +import { extension } from "../BalExtensionContext"; +import { RPCLayer } from "../RPCLayer"; +import { VisualizerWebview } from "../views/visualizer/webview"; +import { debug } from "./logger"; + +const PROGRESS_COMPLETE = 100; + +/** + * Executes the `bal tool pull` command and sends progress notifications to the webview client via RPC. + * Includes 5-minute timeout. + * + * @param migrationToolName The alias for the Ballerina tool to pull (e.g., "migrate-tibco", "migrate-mule"). + * @param version The version of the tool to pull (e.g., "1.1.1"). + * @returns A promise that resolves when the operation is complete or rejects on failure. + */ +export async function pullMigrationTool(migrationToolName: string, version: string): Promise { + // 1. Initial validation and command mapping + if (!migrationToolName) { + const errorMessage = "Migration tool name is required"; + return Promise.reject(new Error(errorMessage)); + } + + if (!version) { + const errorMessage = "Migration tool version is required"; + return Promise.reject(new Error(errorMessage)); + } + + const toolCommandSet = new Set(["migrate-tibco", "migrate-mule"]); + + if (!toolCommandSet.has(migrationToolName)) { + const errorMessage = `Unsupported migration tool: ${migrationToolName}`; + return Promise.reject(new Error(errorMessage)); + } + + const ballerinaCmd = extension.ballerinaExtInstance.getBallerinaCmd(); + const command = `${ballerinaCmd} tool pull ${migrationToolName}:${version}`; + debug(`Executing migration tool pull command: ${command}`); + + // 2. This function now returns a promise that wraps the exec lifecycle + return new Promise((resolve, reject) => { + // Helper to send notifications to the webview + const sendProgress = (progress: DownloadProgress) => { + RPCLayer._messenger.sendNotification( + onDownloadProgress, + { type: "webview", webviewType: VisualizerWebview.viewType }, + progress + ); + }; + + // Send initial progress update + sendProgress({ + message: "Initializing tool download...", + percentage: 0, + success: false, + step: 1, + }); + + const childProcess = exec(command, { + maxBuffer: 1024 * 1024, + timeout: 300000 // 5 minutes timeout + }); + + let accumulatedStdout = ""; + let progressReported = 0; + + // 3. Process the command's standard output with carriage return handling + childProcess.stdout?.on("data", (data: Buffer) => { + const output = data.toString(); + accumulatedStdout += output; + debug(`Tool pull stdout chunk: ${output.replace(/\r/g, '\\r').replace(/\n/g, '\\n')}`); + + // Handle carriage return progress updates - split by \r and take the last meaningful line + const lines = output.split('\r'); + const lastLine = lines[lines.length - 1] || lines[lines.length - 2] || ''; + + // Case A: Tool is already installed (high-priority check) + if (accumulatedStdout.includes("is already available locally")) { + if (progressReported < PROGRESS_COMPLETE) { + progressReported = PROGRESS_COMPLETE; + sendProgress({ + message: "Tool is already installed.", + percentage: PROGRESS_COMPLETE, + success: true, + step: 3, + }); + } + } + // Case B: Download is complete (check for success message) + else if (accumulatedStdout.includes("pulled from central successfully")) { + if (progressReported < PROGRESS_COMPLETE) { + progressReported = PROGRESS_COMPLETE; + sendProgress({ + message: "Download complete. Finalizing...", + percentage: PROGRESS_COMPLETE, + success: false, // Not fully successful until the process closes with code 0 + step: 2, + }); + } + } + // Case C: Parse the percentage from the progress bar output + else { + // Look for percentage in the current line (handles carriage return updates) + const percentageMatch = lastLine.match(/\s+(\d{1,3})\s*%/); + + if (percentageMatch) { + const currentPercentage = parseInt(percentageMatch[1], 10); + + // Update progress if it's a valid number and moving forward + if (!isNaN(currentPercentage) && currentPercentage > progressReported && currentPercentage <= PROGRESS_COMPLETE) { + progressReported = currentPercentage; + + // Extract download info from progress line if available + const sizeMatch = lastLine.match(/(\d+)\/(\d+)\s+KB/); + let message = `Downloading... ${currentPercentage}%`; + + if (sizeMatch) { + const downloaded = parseInt(sizeMatch[1], 10); + const total = parseInt(sizeMatch[2], 10); + message = `Downloading... ${currentPercentage}% (${Math.round(downloaded / 1024)}/${Math.round(total / 1024)} MB)`; + } + + sendProgress({ + message, + percentage: currentPercentage, + success: false, + step: 2, + }); + } + } + // Also check for any percentage in the accumulated output as fallback + else { + const allPercentageMatches = output.match(/(\d{1,3})\s*%/g); + if (allPercentageMatches) { + const lastMatch = allPercentageMatches[allPercentageMatches.length - 1]; + const currentPercentage = parseInt(lastMatch, 10); + + if (!isNaN(currentPercentage) && currentPercentage > progressReported && currentPercentage <= PROGRESS_COMPLETE) { + progressReported = currentPercentage; + sendProgress({ + message: `Downloading... ${currentPercentage}%`, + percentage: currentPercentage, + success: false, + step: 2, + }); + } + } + } + } + }); + + // 4. Handle standard error output with improved filtering + childProcess.stderr?.on("data", (data: Buffer) => { + const errorOutput = data.toString().trim(); + debug(`Tool pull stderr: ${errorOutput}`); + + // Filter out non-critical messages that shouldn't cause failure + const nonCriticalPatterns = [ + /is already active/i, + /warning:/i, + /deprecated/i + ]; + + const isNonCritical = nonCriticalPatterns.some(pattern => pattern.test(errorOutput)); + + if (isNonCritical) { + debug(`Ignoring non-critical stderr: ${errorOutput}`); + return; + } + + // Only treat as error if it's a real error message + if (errorOutput.length > 0) { + sendProgress({ + message: `Error: ${errorOutput}`, + success: false, + step: -1, + }); + reject(new Error(errorOutput)); + } + }); + + // 5. Handle the definitive end of the process + childProcess.on("close", (code) => { + debug(`Tool pull command exited with code ${code}`); + + // Success conditions: code 0, or code 1 with "already available" message + const isAlreadyInstalled = accumulatedStdout.includes("is already available locally"); + const isSuccessfulDownload = accumulatedStdout.includes("pulled from central successfully") || + accumulatedStdout.includes("successfully set as the active version"); + + if (code === 0 || (code === 1 && isAlreadyInstalled)) { + let finalMessage: string; + + if (isAlreadyInstalled) { + finalMessage = `Tool '${migrationToolName}' is already installed.`; + } else if (isSuccessfulDownload) { + finalMessage = `Successfully pulled '${migrationToolName}'.`; + } else { + finalMessage = `Tool pull completed with code ${code}. Please check the logs for more details.`; + } + + sendProgress({ + message: finalMessage, + percentage: PROGRESS_COMPLETE, + success: true, + step: 3, + }); + resolve(); + } else { + const errorMessage = `Tool pull failed with exit code ${code}. Check logs for details.`; + sendProgress({ + message: errorMessage, + success: false, + step: -1, + }); + reject(new Error(errorMessage)); + } + }); + + // Handle process execution errors (e.g., command not found) + childProcess.on("error", (error) => { + debug(`Tool pull process error: ${error.message}`); + + const errorMessage = `Failed to execute command: ${error.message}`; + sendProgress({ + message: errorMessage, + success: false, + step: -1, + }); + reject(new Error(errorMessage)); + }); + + // Handle timeout from exec options + childProcess.on("timeout", () => { + debug("Tool pull process timed out after 5 minutes"); + + const errorMessage = "Download timed out after 5 minutes"; + sendProgress({ + message: errorMessage, + success: false, + step: -1, + }); + reject(new Error("Migration tool pull timed out after 5 minutes")); + }); + }); +} diff --git a/workspaces/ballerina/ballerina-extension/src/utils/modification.ts b/workspaces/ballerina/ballerina-extension/src/utils/modification.ts index 8b44c9a135e..83c14f3eca2 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/modification.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/modification.ts @@ -54,7 +54,7 @@ export async function modifyFileContent(params: UpdateFileContentRequest): Promi } return doc.save(); } else { - writeBallerinaFileDidOpen(normalizedFilePath, content); + await writeBallerinaFileDidOpen(normalizedFilePath, content); StateMachine.langClient().updateStatusBar(); if (updateViewFlag) { updateView(); @@ -64,6 +64,26 @@ export async function modifyFileContent(params: UpdateFileContentRequest): Promi return false; } +export function writeBallerinaFileDidOpenTemp(filePath: string, content: string) { + writeFileSync(filePath, content.trim()); + StateMachine.langClient().didChange({ + textDocument: { uri: filePath, version: 1 }, + contentChanges: [ + { + text: content, + }, + ], + }); + StateMachine.langClient().didOpen({ + textDocument: { + uri: Uri.file(filePath).toString(), + languageId: 'ballerina', + version: 1, + text: content.trim() + } + }); +} + export async function writeBallerinaFileDidOpen(filePath: string, content: string) { writeFileSync(filePath, content.trim()); StateMachine.langClient().didChange({ diff --git a/workspaces/ballerina/ballerina-extension/src/utils/project-artifacts.ts b/workspaces/ballerina/ballerina-extension/src/utils/project-artifacts.ts index 7b3d06b5752..2986b4af249 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/project-artifacts.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/project-artifacts.ts @@ -15,18 +15,18 @@ * specific language governing permissions and limitations * under the License. */ - +import * as vscode from "vscode"; import { URI, Utils } from "vscode-uri"; -import { ARTIFACT_TYPE, Artifacts, ArtifactsNotification, BaseArtifact, DIRECTORY_MAP, EVENT_TYPE, MACHINE_VIEW, NodePosition, ProjectStructureArtifactResponse, ProjectStructureResponse, VisualizerLocation } from "@wso2/ballerina-core"; +import { ARTIFACT_TYPE, Artifacts, ArtifactsNotification, BaseArtifact, DIRECTORY_MAP, NodePosition, ProjectStructureArtifactResponse, ProjectStructureResponse } from "@wso2/ballerina-core"; import { StateMachine } from "../stateMachine"; import * as fs from 'fs'; import * as path from 'path'; import { ExtendedLangClient } from "../core/extended-language-client"; import { ServiceDesignerRpcManager } from "../rpc-managers/service-designer/rpc-manager"; import { AiAgentRpcManager } from "../rpc-managers/ai-agent/rpc-manager"; -import { injectAgentCode, injectImportIfMissing } from "./source-utils"; -import { tmpdir } from "os"; +import { injectAgentCode } from "./source-utils"; import { ArtifactsUpdated, ArtifactNotificationHandler } from "./project-artifacts-handler"; +import { CommonRpcManager } from "../rpc-managers/common/rpc-manager"; export async function buildProjectArtifactsStructure(projectDir: string, langClient: ExtendedLangClient, isUpdate: boolean = false): Promise { const result: ProjectStructureResponse = { @@ -51,6 +51,17 @@ export async function buildProjectArtifactsStructure(projectDir: string, langCli traverseComponents(designArtifacts.artifacts, result); await populateLocalConnectors(projectDir, result); } + // Attempt to get the project name from the workspace folder as a fallback if not found in Ballerina.toml + const workspace = vscode.workspace.workspaceFolders?.find(folder => folder.uri.fsPath === projectDir); + let projectName = workspace?.name; + // Get the project name from the ballerina.toml file + const commonRpcManager = new CommonRpcManager(); + const tomlValues = await commonRpcManager.getCurrentProjectTomlValues(); + if (tomlValues && tomlValues.package.title) { + projectName = tomlValues.package.title; + } + result.projectName = projectName; + if (isUpdate) { StateMachine.updateProjectStructure({ ...result }); } @@ -71,6 +82,13 @@ export async function updateProjectArtifacts(publishedArtifacts: ArtifactsNotifi timestamp: Date.now() }); StateMachine.updateProjectStructure({ ...currentProjectStructure }); // Update the project structure and refresh the tree + } else { + const notificationHandler = ArtifactNotificationHandler.getInstance(); + // Publish a notification to the artifact handler + notificationHandler.publish(ArtifactsUpdated.method, { + data: [], + timestamp: Date.now() + }); } } diff --git a/workspaces/ballerina/ballerina-extension/src/utils/server/server.ts b/workspaces/ballerina/ballerina-extension/src/utils/server/server.ts index 7a83aa4a6ef..3f68ce24cf4 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/server/server.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/server/server.ts @@ -55,19 +55,19 @@ function findJarsExcludingPatterns(directory: string, excludePatterns: string[]) } const files = fs.readdirSync(directory); const matchingJars: string[] = []; - + const compiledPatterns = excludePatterns.map(pattern => new RegExp(pattern.replace(/\*/g, '.*'))); - + files.forEach(file => { if (file.endsWith('.jar')) { const shouldExclude = compiledPatterns.some(regex => regex.test(file)); - + if (!shouldExclude) { matchingJars.push(path.join(directory, file)); } } }); - + return matchingJars; } catch (error) { console.error(`Error reading directory ${directory}:`, error); @@ -77,13 +77,13 @@ function findJarsExcludingPatterns(directory: string, excludePatterns: string[]) function parseJdkVersion(versionString: string): { parsedVersion: number[], buildNumber: number } { const [mainVersion, buildPart] = versionString.split('+'); - + const parsedVersion = mainVersion .split('.') .map(num => parseInt(num, 10) || 0); - + const buildNumber = parseInt(buildPart || '0', 10); - + return { parsedVersion, buildNumber }; } @@ -93,10 +93,10 @@ function extractJdkInfo(fileName: string, directory: string): JdkInfo | null { if (!match) { return null; } - + const versionString = match[1]; const { parsedVersion, buildNumber } = parseJdkVersion(versionString); - + return { name: fileName, version: versionString, @@ -112,19 +112,19 @@ export function findHighestVersionJdk(directory: string): string | null { debug(`Dependencies directory not found: ${directory}`); return null; } - + const files = fs.readdirSync(directory); debug(`Found files in dependencies directory: ${files.join(', ')}`); - + const jdkInfos = files .map(file => extractJdkInfo(file, directory)) .filter((jdk): jdk is JdkInfo => jdk !== null); - + if (jdkInfos.length === 0) { debug(`No JDK directories found matching pattern in: ${directory}`); return null; } - + const sortedJdks = orderBy(jdkInfos, [ // sort by major version (descending) (jdk: JdkInfo) => jdk.parsedVersion[0] || 0, @@ -135,12 +135,12 @@ export function findHighestVersionJdk(directory: string): string | null { // sort by build number (descending) (jdk: JdkInfo) => jdk.buildNumber ], ['desc', 'desc', 'desc', 'desc']); - + const highestVersionJdk = sortedJdks[0]; - + debug(`Selected JDK: ${highestVersionJdk.name} at ${highestVersionJdk.fullPath}`); return highestVersionJdk.fullPath; - + } catch (error) { console.error(`Error reading directory ${directory}:`, error); return null; @@ -148,9 +148,10 @@ export function findHighestVersionJdk(directory: string): string | null { } export function getServerOptions(extension: BallerinaExtension): ServerOptions { + debug('Getting server options.'); // Check if user wants to use Ballerina CLI language server or if version requires it const BI_SUPPORTED_MINIMUM_VERSION = 2201123; // 2201.12.3 - if (extension?.useDistributionLanguageServer() ||!isSupportedSLVersion(extension, BI_SUPPORTED_MINIMUM_VERSION) ) { + if (extension?.useDistributionLanguageServer() || !isSupportedSLVersion(extension, BI_SUPPORTED_MINIMUM_VERSION)) { return getServerOptionsUsingCLI(extension); } else { return getServerOptionsUsingJava(extension); @@ -161,13 +162,13 @@ function getServerOptionsUsingCLI(extension: BallerinaExtension): ServerOptions const ballerinaCmd = extension.getBallerinaCmd(); debug(`Using bal command to start language server.`); debug(`Using Ballerina CLI command '${ballerinaCmd}'`); - + let cmd = isWindows() ? 'cmd.exe' : ballerinaCmd; let args = ["start-language-server"]; if (isWindows()) { args = ['/c', ballerinaCmd, 'start-language-server']; } - + let opt: ExecutableOptions = {}; opt.env = Object.assign({}, process.env); @@ -178,7 +179,7 @@ function getServerOptionsUsingCLI(extension: BallerinaExtension): ServerOptions opt.env.BALLERINA_CLASSPATH_EXT = process.env.LS_EXTENSIONS_PATH; } } - + if (process.env.LSDEBUG === "true" || extension?.enableLSDebug()) { debug('Language Server is starting in debug mode.'); let debugPort = 5005; @@ -215,7 +216,7 @@ function getServerOptionsUsingJava(extension: BallerinaExtension): ServerOptions opt.env.BALLERINA_CLASSPATH_EXT = process.env.LS_EXTENSIONS_PATH; } } - + let debugOpts = ''; if (process.env.LSDEBUG === "true" || extension?.enableLSDebug()) { let debugPort = 5005; @@ -225,10 +226,10 @@ function getServerOptionsUsingJava(extension: BallerinaExtension): ServerOptions const ballerinaHome = isWindows() ? fs.realpathSync.native(extension?.getBallerinaHome()) : extension?.getBallerinaHome(); // Get the base ballerina home by removing the distribution part - const baseHome = ballerinaHome.includes('distributions') + const baseHome = ballerinaHome.includes('distributions') ? ballerinaHome.substring(0, ballerinaHome.indexOf('distributions')) : ballerinaHome; - + // jar patterns to exclude const excludeJarPatterns = [ 'architecture-model*', @@ -250,20 +251,21 @@ function getServerOptionsUsingJava(extension: BallerinaExtension): ServerOptions join(ballerinaHome, 'lib', 'tools', 'lang-server', 'lib'), join(ballerinaHome, 'lib', 'tools', 'debug-adapter', 'lib') ]; - - const ballerinaJarPaths = directoriesToSearch.flatMap(directory => + + const ballerinaJarPaths = directoriesToSearch.flatMap(directory => findJarsExcludingPatterns(directory, excludeJarPatterns) ); - + ballerinaJarPaths.forEach(jarPath => { if (!fs.existsSync(jarPath)) { + debug(`Ballerina jar not found in ${jarPath}`); log(`Ballerina jar not found in ${jarPath}`); } }); let ballerinaLanguageServerJar: string | null = null; const configuredLangServerPath = extension?.getConfiguredLangServerPath(); - + if (configuredLangServerPath && configuredLangServerPath.trim() !== "") { debug(`Using custom language server path: ${configuredLangServerPath}`); // User provided custom language server path @@ -276,9 +278,9 @@ function getServerOptionsUsingJava(extension: BallerinaExtension): ServerOptions } else { debug(`Using bundled language server from ls directory.`); // Use bundled language server from ls directory - const lsDir = extension?.context.asAbsolutePath("ls"); + const lsDir = extension?.context.asAbsolutePath("ls"); ballerinaLanguageServerJar = findFileByPattern(lsDir, /^ballerina-language-server.*\.jar$/); - + if (!ballerinaLanguageServerJar || !fs.existsSync(ballerinaLanguageServerJar)) { debug(`No ballerina language server jar found in: ${lsDir}`); throw new Error(`Language server JAR not found in ${lsDir}`); @@ -286,6 +288,7 @@ function getServerOptionsUsingJava(extension: BallerinaExtension): ServerOptions } const jarName = path.basename(configuredLangServerPath || ballerinaLanguageServerJar); + debug(`Language Server JAR: ${jarName}`); const versionMatch = jarName.match(/ballerina-language-server-(.+)\.jar$/); if (versionMatch) { log(`Language Server Version: ${versionMatch[1]}`); @@ -294,17 +297,18 @@ function getServerOptionsUsingJava(extension: BallerinaExtension): ServerOptions } const customPaths = [...ballerinaJarPaths, ballerinaLanguageServerJar]; + // debug(`Custom paths: ${customPaths}`); if (process.env.LS_CUSTOM_CLASSPATH) { debug(`LS_CUSTOM_CLASSPATH: ${process.env.LS_CUSTOM_CLASSPATH}`); customPaths.push(process.env.LS_CUSTOM_CLASSPATH); } - + const classpath = customPaths.join(delimiter); - + // Find any JDK in the dependencies directory const dependenciesDir = join(baseHome, 'dependencies'); const jdkDir = findHighestVersionJdk(dependenciesDir); - + debug(`JDK Directory: ${jdkDir}`); if (!jdkDir) { debug(`No JDK found in dependencies directory: ${dependenciesDir}`); throw new Error(`JDK not found in ${dependenciesDir}`); @@ -314,27 +318,28 @@ function getServerOptionsUsingJava(extension: BallerinaExtension): ServerOptions if (jdkVersionMatch) { log(`JDK Version: ${jdkVersionMatch[1]}`); } - + debug(`JDK Version: ${jdkVersionMatch[1]}`); const javaExecutable = isWindows() ? 'java.exe' : 'java'; const cmd = join(jdkDir, 'bin', javaExecutable); const args = ['-cp', classpath, `-Dballerina.home=${ballerinaHome}`, 'org.ballerinalang.langserver.launchers.stdio.Main']; + debug(`Java Executable: ${cmd}`); // Include debug options in the Java arguments if in debug mode if (debugOpts) { args.unshift(debugOpts); } - + debug(`Debug Options: ${debugOpts}`); // Add custom JVM arguments from environment variable ( Example: LS_CUSTOM_ARGS="-arg1 -arg2=value") if (process.env.LS_CUSTOM_ARGS) { debug(`LS_CUSTOM_ARGS: ${process.env.LS_CUSTOM_ARGS}`); args.unshift(...process.env.LS_CUSTOM_ARGS.split(' ')); } - + const serverOptions = { command: cmd, args, options: opt }; - + return serverOptions; } diff --git a/workspaces/ballerina/ballerina-extension/src/utils/state-machine-utils.ts b/workspaces/ballerina/ballerina-extension/src/utils/state-machine-utils.ts index 45aa5bf4909..ff4c2f843d4 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/state-machine-utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/state-machine-utils.ts @@ -403,7 +403,23 @@ function findViewByArtifact(dir: ProjectStructureArtifactResponse, position: Nod identifier: dir.name, documentUri: normalizedDocumentUri, position: position, - artifactType: DIRECTORY_MAP.DATA_MAPPER + artifactType: DIRECTORY_MAP.DATA_MAPPER, + dataMapperMetadata: { + name: dir.name, + codeData: { + lineRange: { + fileName: normalizedDocumentUri, + startLine: { + line: dir.position.startLine, + offset: dir.position.startColumn + }, + endLine: { + line: dir.position.endLine, + offset: dir.position.endColumn + } + } + } + }, }, dataMapperDepth: 0 }; 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 cd22228ef0c..f2c0a93b88e 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts @@ -21,8 +21,8 @@ import { createMachine, assign, interpret } from 'xstate'; import { AIMachineStateValue, AIPanelPrompt, AIMachineEventType, AIMachineContext, AIUserToken, AIMachineSendableEvent, LoginMethod } from '@wso2/ballerina-core'; import { AiPanelWebview } from './webview'; import { extension } from '../../BalExtensionContext'; -import { getAccessToken } from '../../utils/ai/auth'; -import { checkToken, initiateInbuiltAuth, logout, validateApiKey } from './utils'; +import { getAccessToken, getLoginMethod } from '../../utils/ai/auth'; +import { checkToken, initiateInbuiltAuth, logout, validateApiKey, validateAwsCredentials } from './utils'; export const USER_CHECK_BACKEND_URL = '/user/usage'; @@ -122,6 +122,12 @@ const aiMachine = createMachine({ actions: assign({ loginMethod: (_ctx) => LoginMethod.ANTHROPIC_KEY }) + }, + [AIMachineEventType.AUTH_WITH_AWS_BEDROCK]: { + target: 'Authenticating', + actions: assign({ + loginMethod: (_ctx) => LoginMethod.AWS_BEDROCK + }) } } }, @@ -138,6 +144,10 @@ const aiMachine = createMachine({ cond: (context) => context.loginMethod === LoginMethod.ANTHROPIC_KEY, target: 'apiKeyFlow' }, + { + cond: (context) => context.loginMethod === LoginMethod.AWS_BEDROCK, + target: 'awsBedrockFlow' + }, { target: 'ssoFlow' // default } @@ -205,6 +215,41 @@ const aiMachine = createMachine({ }) } } + }, + awsBedrockFlow: { + on: { + [AIMachineEventType.SUBMIT_AWS_CREDENTIALS]: { + target: 'validatingAwsCredentials', + actions: assign({ + errorMessage: (_ctx) => undefined + }) + }, + [AIMachineEventType.CANCEL_LOGIN]: { + target: '#ballerina-ai.Unauthenticated', + actions: assign({ + loginMethod: (_ctx) => undefined, + errorMessage: (_ctx) => undefined, + }) + } + } + }, + validatingAwsCredentials: { + invoke: { + id: 'validateAwsCredentials', + src: 'validateAwsCredentials', + onDone: { + target: '#ballerina-ai.Authenticated', + actions: assign({ + errorMessage: (_ctx) => undefined, + }) + }, + onError: { + target: 'awsBedrockFlow', + actions: assign({ + errorMessage: (_ctx, event) => event.data?.message || 'AWS credentials validation failed' + }) + } + } } } }, @@ -290,12 +335,26 @@ const validateApiKeyService = async (_context: AIMachineContext, event: any) => return await validateApiKey(apiKey, LoginMethod.ANTHROPIC_KEY); }; +const validateAwsCredentialsService = async (_context: AIMachineContext, event: any) => { + const { accessKeyId, secretAccessKey, region, sessionToken } = event.payload || {}; + if (!accessKeyId || !secretAccessKey || !region) { + throw new Error('AWS access key ID, secret access key, and region are required'); + } + return await validateAwsCredentials({ + accessKeyId, + secretAccessKey, + region, + sessionToken + }); +}; + const getTokenAfterAuth = async () => { const result = await getAccessToken(); - if (!result) { + const loginMethod = await getLoginMethod(); + if (!result || !loginMethod) { throw new Error('No authentication credentials found'); } - return { token: result, loginMethod: LoginMethod.BI_INTEL }; + return { token: result, loginMethod: loginMethod }; }; const aiStateService = interpret(aiMachine.withConfig({ @@ -303,6 +362,7 @@ const aiStateService = interpret(aiMachine.withConfig({ checkToken: checkToken, openLogin: openLogin, validateApiKey: validateApiKeyService, + validateAwsCredentials: validateAwsCredentialsService, getTokenAfterAuth: getTokenAfterAuth, }, actions: { 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 a0920213486..ce70bf23016 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts @@ -19,10 +19,12 @@ import * as vscode from 'vscode'; import { AIUserToken, LoginMethod, AuthCredentials } from '@wso2/ballerina-core'; import { createAnthropic } from '@ai-sdk/anthropic'; +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 { getBedrockRegionalPrefix } from '../../features/ai/service/connection'; const LEGACY_ACCESS_TOKEN_SECRET_KEY = 'BallerinaAIUser'; const LEGACY_REFRESH_TOKEN_SECRET_KEY = 'BallerinaAIRefreshToken'; @@ -129,3 +131,75 @@ export const validateApiKey = async (apiKey: string, loginMethod: LoginMethod): throw new Error('Validation failed. Please try again.'); } }; + +export const validateAwsCredentials = async (credentials: { + accessKeyId: string; + secretAccessKey: string; + region: string; + sessionToken?: string; +}): Promise => { + const { accessKeyId, secretAccessKey, region, sessionToken } = credentials; + + if (!accessKeyId || !secretAccessKey || !region) { + throw new Error('AWS access key ID, secret access key, and region are required.'); + } + + if (!accessKeyId.startsWith('AKIA') && !accessKeyId.startsWith('ASIA')) { + throw new Error('Please enter a valid AWS access key ID.'); + } + + if (secretAccessKey.length < 20) { + throw new Error('Please enter a valid AWS secret access key.'); + } + + // List of valid AWS regions + const validRegions = [ + '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' + ]; + + if (!validRegions.includes(region)) { + throw new Error(`Invalid AWS region. Please select a valid region like us-east-1, us-west-2, etc.`); + } + + try { + const bedrock = createAmazonBedrock({ + region: region, + accessKeyId: accessKeyId, + 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 + await generateText({ + model: bedrockClient, + maxTokens: 1, + messages: [{ role: 'user', content: 'Hi' }] + }); + + // Store credentials + const authCredentials: AuthCredentials = { + loginMethod: LoginMethod.AWS_BEDROCK, + secrets: { + accessKeyId, + secretAccessKey, + region, + sessionToken + } + }; + await storeAuthCredentials(authCredentials); + + return { token: accessKeyId }; + + } catch (error) { + console.error('AWS Bedrock validation failed:', error); + throw new Error('Validation failed. Please check the log for more details.'); + } +}; diff --git a/workspaces/ballerina/ballerina-extension/src/views/migration-report/webview.ts b/workspaces/ballerina/ballerina-extension/src/views/migration-report/webview.ts new file mode 100644 index 00000000000..0e4c1dd6ec1 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/views/migration-report/webview.ts @@ -0,0 +1,99 @@ +/** + * 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 { Disposable, Uri, ViewColumn, WebviewPanel, window } from "vscode"; +import { extension } from "../../BalExtensionContext"; +import path from "path"; + +export class MigrationReportWebview { + public static currentPanel: MigrationReportWebview | undefined; + private _panel: WebviewPanel; + private _disposables: Disposable[] = []; + + private constructor(panel: WebviewPanel, reportContent: string) { + this._panel = panel; + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + const htmlWithStyleRemoval = reportContent.replace( + "", + `` + ); + + this._panel.webview.html = htmlWithStyleRemoval; + } + + public static createOrShow(fileName: string, reportContent: string): void { + if (MigrationReportWebview.currentPanel) { + MigrationReportWebview.currentPanel._panel.reveal(ViewColumn.Active); + MigrationReportWebview.currentPanel.updateContent(reportContent); + return; + } + + const panel = window.createWebviewPanel( + "migrationReport", + `Migration Report`, + ViewColumn.Active, + { + enableScripts: true, + retainContextWhenHidden: true, + } + ); + + panel.iconPath = { + light: Uri.file(path.join(extension.context.extensionPath, "resources", "icons", "light-icon.svg")), + dark: Uri.file(path.join(extension.context.extensionPath, "resources", "icons", "dark-icon.svg")), + }; + + MigrationReportWebview.currentPanel = new MigrationReportWebview(panel, reportContent); + } + + private updateContent(reportContent: string): void { + const htmlWithStyleRemoval = reportContent.replace( + "", + `` + ); + + this._panel.webview.html = htmlWithStyleRemoval; + } + + public dispose(): void { + MigrationReportWebview.currentPanel = undefined; + this._panel.dispose(); + + while (this._disposables.length) { + const disposable = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } +} diff --git a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts index 197aae51331..ffa3423d621 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts @@ -26,7 +26,7 @@ import { extension } from "../../BalExtensionContext"; import { StateMachine, updateView } from "../../stateMachine"; import { LANGUAGE } from "../../core"; import { CodeData, MACHINE_VIEW } from "@wso2/ballerina-core"; -import { refreshDataMapper } from "../../rpc-managers/inline-data-mapper/utils"; +import { refreshDataMapper } from "../../rpc-managers/data-mapper/utils"; import { AiPanelWebview } from "../ai-panel/webview"; export class VisualizerWebview { @@ -74,7 +74,10 @@ export class VisualizerWebview { editor.document.fileName === document.document.fileName ); const dataMapperModified = balFileModified && - StateMachine.context().view === MACHINE_VIEW.InlineDataMapper && + ( + StateMachine.context().view === MACHINE_VIEW.InlineDataMapper || + StateMachine.context().view === MACHINE_VIEW.DataMapper + ) && document.document.fileName === StateMachine.context().documentUri; if (dataMapperModified) { @@ -165,13 +168,13 @@ export class VisualizerWebview { padding-top: 30vh; } .loader { - width: 32px; + width: 36px; aspect-ratio: 1; border-radius: 50%; - border: 4px solid var(--vscode-button-background); + border: 6px solid var(--vscode-button-background); animation: - l20-1 0.8s infinite linear alternate, - l20-2 1.6s infinite linear; + l20-1 0.5s infinite linear alternate, + l20-2 1s infinite linear; } @keyframes l20-1{ 0% {clip-path: polygon(50% 50%,0 0, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0% )} diff --git a/workspaces/ballerina/ballerina-extension/webpack.config.js b/workspaces/ballerina/ballerina-extension/webpack.config.js index 1591d571bd3..cbf4aad0946 100644 --- a/workspaces/ballerina/ballerina-extension/webpack.config.js +++ b/workspaces/ballerina/ballerina-extension/webpack.config.js @@ -31,7 +31,7 @@ module.exports = { libraryTarget: 'commonjs2', devtoolModuleFilenameTemplate: '../[resource-path]' }, - devtool: 'source-map', + devtool: !process.env.CI ? "source-map" : undefined, externals: { keytar: "commonjs keytar", vscode: 'commonjs vscode', diff --git a/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts b/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts index 96ac9561a9a..0ad25d5aaa1 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts @@ -38,6 +38,8 @@ import { PopupVisualizerLocation, getPopupVisualizerState, onDownloadProgress, + onMigrationToolLogs, + onMigrationToolStateChanged, DownloadProgress, breakpointChanged, AIMachineEventType, @@ -54,11 +56,11 @@ import { import { LangClientRpcClient } from "./rpc-clients/lang-client/rpc-client"; import { LibraryBrowserRpcClient } from "./rpc-clients/library-browser/rpc-client"; import { HOST_EXTENSION } from "vscode-messenger-common"; -import { CommonRpcClient, GraphqlDesignerRpcClient, PersistDiagramRpcClient, RecordCreatorRpcClient, ServiceDesignerRpcClient, AiPanelRpcClient } from "./rpc-clients"; +import { CommonRpcClient, GraphqlDesignerRpcClient, PersistDiagramRpcClient, RecordCreatorRpcClient, ServiceDesignerRpcClient, AiPanelRpcClient, MigrateIntegrationRpcClient } from "./rpc-clients"; import { BiDiagramRpcClient } from "./rpc-clients/bi-diagram/rpc-client"; import { ConnectorWizardRpcClient } from "./rpc-clients/connector-wizard/rpc-client"; import { SequenceDiagramRpcClient } from "./rpc-clients/sequence-diagram/rpc-client"; -import { InlineDataMapperRpcClient } from "./rpc-clients/inline-data-mapper/rpc-client"; +import { DataMapperRpcClient } from "./rpc-clients/data-mapper/rpc-client"; import { TestManagerServiceRpcClient } from "./rpc-clients"; import { AiAgentRpcClient } from "./rpc-clients/ai-agent/rpc-client"; import { ICPServiceRpcClient } from "./rpc-clients/icp-service/rpc-client"; @@ -79,7 +81,8 @@ export class BallerinaRpcClient { private _SequenceDiagram: SequenceDiagramRpcClient; private _aiPanel: AiPanelRpcClient; private _connectorWizard: ConnectorWizardRpcClient; - private _inlineDataMapper: InlineDataMapperRpcClient; + private _dataMapper: DataMapperRpcClient; + private _migrateIntegration: MigrateIntegrationRpcClient; private _testManager: TestManagerServiceRpcClient; private _aiAgent: AiAgentRpcClient; private _icpManager: ICPServiceRpcClient; @@ -100,7 +103,8 @@ export class BallerinaRpcClient { this._SequenceDiagram = new SequenceDiagramRpcClient(this.messenger); this._aiPanel = new AiPanelRpcClient(this.messenger); this._connectorWizard = new ConnectorWizardRpcClient(this.messenger); - this._inlineDataMapper = new InlineDataMapperRpcClient(this.messenger); + this._dataMapper = new DataMapperRpcClient(this.messenger); + this._migrateIntegration = new MigrateIntegrationRpcClient(this.messenger); this._testManager = new TestManagerServiceRpcClient(this.messenger); this._aiAgent = new AiAgentRpcClient(this.messenger); this._icpManager = new ICPServiceRpcClient(this.messenger); @@ -167,8 +171,12 @@ export class BallerinaRpcClient { return this._aiPanel; } - getInlineDataMapperRpcClient(): InlineDataMapperRpcClient { - return this._inlineDataMapper; + getDataMapperRpcClient(): DataMapperRpcClient { + return this._dataMapper; + } + + getMigrateIntegrationRpcClient(): MigrateIntegrationRpcClient { + return this._migrateIntegration; } getVisualizerLocation(): Promise { @@ -221,6 +229,14 @@ export class BallerinaRpcClient { this.messenger.onNotification(onChatNotify, callback); } + onMigrationToolLogs(callback: (message: string) => void) { + this.messenger.onNotification(onMigrationToolLogs, callback); + } + + onMigrationToolStateChanged(callback: (state: string) => void) { + this.messenger.onNotification(onMigrationToolStateChanged, callback); + } + getPopupVisualizerState(): Promise { return this.messenger.sendRequest(getPopupVisualizerState, HOST_EXTENSION); } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts index 9a5f0cbe1df..b70e51a425a 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts @@ -22,23 +22,26 @@ import { AIMachineSnapshot, AIPanelAPI, AIPanelPrompt, + AddFilesToProjectRequest, AddToProjectRequest, + AllDataMapperSourceRequest, CodeSegment, + CreateTempFileRequest, + DataMapperModelResponse, + DatamapperModelContext, DeleteFromProjectRequest, DeveloperDocument, + DocGenerationRequest, + ExtendedDataMapperMetadata, FetchDataRequest, FetchDataResponse, GenerateCodeRequest, - GenerateMappingFromRecordResponse, - GenerateMappingsFromRecordRequest, - GenerateMappingsRequest, GenerateMappingsResponse, GenerateOpenAPIRequest, GenerateTypesFromRecordRequest, GenerateTypesFromRecordResponse, GetFromFileRequest, GetModuleDirParams, - InlineAllDataMapperSourceRequest, LLMDiagnostics, LoginMethod, MetadataWithAttachments, @@ -60,15 +63,19 @@ import { abortAIGeneration, abortTestGeneration, addChatSummary, + addCodeSegmentToWorkspace, + addFilesToProject, addInlineCodeSegmentToWorkspace, addToProject, applyDoOnFailBlocks, checkSyntaxError, clearInitialPrompt, + createTempFileAndGenerateMetadata, createTestDirecoryIfNotExists, deleteFromProject, fetchData, generateCode, + generateDataMapperModel, generateFunctionTests, generateHealthcareCode, generateMappings, @@ -84,10 +91,9 @@ import { getFileExists, getFromDocumentation, getFromFile, + getGeneratedDocumentation, getGeneratedTests, getLoginMethod, - getMappingsFromModel, - getMappingsFromRecord, getModuleDirectory, getProjectUuid, getRefreshedAccessToken, @@ -105,7 +111,7 @@ import { isRequirementsSpecificationFileExist, markAlertShown, notifyAIMappings, - openInlineMappingChatWindow, + openAIMappingChatWindow, postProcess, promptGithubAuthorize, promptWSO2AILogout, @@ -159,8 +165,8 @@ export class AiPanelRpcClient implements AIPanelAPI { return this._messenger.sendRequest(fetchData, HOST_EXTENSION, params); } - addToProject(params: AddToProjectRequest): void { - return this._messenger.sendNotification(addToProject, HOST_EXTENSION, params); + addToProject(params: AddToProjectRequest): Promise { + return this._messenger.sendRequest(addToProject, HOST_EXTENSION, params); } getFromFile(params: GetFromFileRequest): Promise { @@ -175,10 +181,6 @@ export class AiPanelRpcClient implements AIPanelAPI { return this._messenger.sendNotification(deleteFromProject, HOST_EXTENSION, params); } - generateMappings(params: GenerateMappingsRequest): Promise { - return this._messenger.sendRequest(generateMappings, HOST_EXTENSION, params); - } - notifyAIMappings(params: NotifyAIMappingsRequest): Promise { return this._messenger.sendRequest(notifyAIMappings, HOST_EXTENSION, params); } @@ -199,12 +201,28 @@ export class AiPanelRpcClient implements AIPanelAPI { return this._messenger.sendNotification(clearInitialPrompt, HOST_EXTENSION); } - openInlineMappingChatWindow(): void { - return this._messenger.sendNotification(openInlineMappingChatWindow, HOST_EXTENSION); + openAIMappingChatWindow(params: DataMapperModelResponse): void { + return this._messenger.sendNotification(openAIMappingChatWindow, HOST_EXTENSION, params); + } + + generateDataMapperModel(params: DatamapperModelContext): Promise { + return this._messenger.sendRequest(generateDataMapperModel, HOST_EXTENSION, params); + } + + getTypesFromRecord(params: GenerateTypesFromRecordRequest): Promise { + return this._messenger.sendRequest(getTypesFromRecord, HOST_EXTENSION, params); + } + + createTempFileAndGenerateMetadata(params: CreateTempFileRequest): Promise { + return this._messenger.sendRequest(createTempFileAndGenerateMetadata, HOST_EXTENSION, params); + } + + generateMappings(params: MetadataWithAttachments): Promise { + return this._messenger.sendRequest(generateMappings, HOST_EXTENSION, params); } - getMappingsFromModel(params: MetadataWithAttachments): Promise { - return this._messenger.sendRequest(getMappingsFromModel, HOST_EXTENSION, params); + addCodeSegmentToWorkspace(params: CodeSegment): Promise { + return this._messenger.sendRequest(addCodeSegmentToWorkspace, HOST_EXTENSION, params); } addInlineCodeSegmentToWorkspace(params: CodeSegment): void { @@ -239,14 +257,6 @@ export class AiPanelRpcClient implements AIPanelAPI { return this._messenger.sendNotification(abortTestGeneration, HOST_EXTENSION); } - getMappingsFromRecord(params: GenerateMappingsFromRecordRequest): Promise { - return this._messenger.sendRequest(getMappingsFromRecord, HOST_EXTENSION, params); - } - - getTypesFromRecord(params: GenerateTypesFromRecordRequest): Promise { - return this._messenger.sendRequest(getTypesFromRecord, HOST_EXTENSION, params); - } - applyDoOnFailBlocks(): void { return this._messenger.sendNotification(applyDoOnFailBlocks, HOST_EXTENSION); } @@ -362,4 +372,12 @@ export class AiPanelRpcClient implements AIPanelAPI { abortAIGeneration(): void { return this._messenger.sendNotification(abortAIGeneration, HOST_EXTENSION); } + + getGeneratedDocumentation(params: DocGenerationRequest): Promise { + return this._messenger.sendRequest(getGeneratedDocumentation, HOST_EXTENSION, params); + } + + addFilesToProject(params: AddFilesToProjectRequest): Promise { + return this._messenger.sendRequest(addFilesToProject, HOST_EXTENSION, params); + } } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts index 7ca25fb3e75..b34d257d169 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts @@ -48,6 +48,8 @@ import { ConfigVariableResponse, CreateComponentResponse, CurrentBreakpointsResponse, + DeleteTypeRequest, + DeleteTypeResponse, DeploymentRequest, DeploymentResponse, DevantMetadata, @@ -121,12 +123,15 @@ import { deleteConfigVariableV2, deleteFlowNode, deleteOpenApiGeneratedModules, + deleteType, deployProject, formDidClose, formDidOpen, generateOpenApiClient, getAiSuggestions, getAllImports, + getAvailableChunkers, + getAvailableDataLoaders, getAvailableEmbeddingProviders, getAvailableModelProviders, getAvailableNodes, @@ -182,6 +187,10 @@ import { updateTypes, DeleteConfigVariableRequestV2, DeleteConfigVariableResponseV2, + VerifyTypeDeleteRequest, + VerifyTypeDeleteResponse, + verifyTypeDelete, + ConfigVariableRequest, } from "@wso2/ballerina-core"; import { HOST_EXTENSION } from "vscode-messenger-common"; import { Messenger } from "vscode-messenger-webview"; @@ -229,6 +238,14 @@ export class BiDiagramRpcClient implements BIDiagramAPI { return this._messenger.sendRequest(getAvailableVectorKnowledgeBases, HOST_EXTENSION, params); } + getAvailableDataLoaders(params: BIAvailableNodesRequest): Promise { + return this._messenger.sendRequest(getAvailableDataLoaders, HOST_EXTENSION, params); + } + + getAvailableChunkers(params: BIAvailableNodesRequest): Promise { + return this._messenger.sendRequest(getAvailableChunkers, HOST_EXTENSION, params); + } + getEnclosedFunction(params: BIGetEnclosedFunctionRequest): Promise { return this._messenger.sendRequest(getEnclosedFunction, HOST_EXTENSION, params); } @@ -281,8 +298,8 @@ export class BiDiagramRpcClient implements BIDiagramAPI { return this._messenger.sendRequest(updateConfigVariables, HOST_EXTENSION, params); } - getConfigVariablesV2(): Promise { - return this._messenger.sendRequest(getConfigVariablesV2, HOST_EXTENSION); + getConfigVariablesV2(params: ConfigVariableRequest): Promise { + return this._messenger.sendRequest(getConfigVariablesV2, HOST_EXTENSION, params); } updateConfigVariablesV2(params: UpdateConfigVariableRequestV2): Promise { @@ -389,6 +406,10 @@ export class BiDiagramRpcClient implements BIDiagramAPI { return this._messenger.sendRequest(updateTypes, HOST_EXTENSION, params); } + deleteType(params: DeleteTypeRequest): Promise { + return this._messenger.sendRequest(deleteType, HOST_EXTENSION, params); + } + getTypeFromJson(params: JsonToTypeRequest): Promise { return this._messenger.sendRequest(getTypeFromJson, HOST_EXTENSION, params); } @@ -472,4 +493,8 @@ export class BiDiagramRpcClient implements BIDiagramAPI { deleteOpenApiGeneratedModules(params: OpenAPIClientDeleteRequest): Promise { return this._messenger.sendRequest(deleteOpenApiGeneratedModules, HOST_EXTENSION, params); } + + verifyTypeDelete(params: VerifyTypeDeleteRequest): Promise { + return this._messenger.sendRequest(verifyTypeDelete, HOST_EXTENSION, params); + } } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/common/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/common/rpc-client.ts index bdd57977e85..cbbcbeafe3a 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/common/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/common/rpc-client.ts @@ -45,6 +45,9 @@ import { openExternalUrl, runBackgroundTerminalCommand, selectFileOrDirPath, + getCurrentProjectTomlValues, + TomlValues, + selectFileOrFolderPath, showErrorMessage } from "@wso2/ballerina-core"; import { HOST_EXTENSION } from "vscode-messenger-common"; @@ -89,6 +92,10 @@ export class CommonRpcClient implements CommonRPCAPI { return this._messenger.sendRequest(selectFileOrDirPath, HOST_EXTENSION, params); } + selectFileOrFolderPath(): Promise { + return this._messenger.sendRequest(selectFileOrFolderPath, HOST_EXTENSION); + } + experimentalEnabled(): Promise { return this._messenger.sendRequest(experimentalEnabled, HOST_EXTENSION); } @@ -104,4 +111,8 @@ export class CommonRpcClient implements CommonRPCAPI { showErrorMessage(params: ShowErrorMessageRequest): void { return this._messenger.sendNotification(showErrorMessage, HOST_EXTENSION, params); } + + getCurrentProjectTomlValues(): Promise { + return this._messenger.sendRequest(getCurrentProjectTomlValues, HOST_EXTENSION); + } } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/inline-data-mapper/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/data-mapper/rpc-client.ts similarity index 62% rename from workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/inline-data-mapper/rpc-client.ts rename to workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/data-mapper/rpc-client.ts index 59db65cb634..49b2804fd10 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/inline-data-mapper/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/data-mapper/rpc-client.ts @@ -21,20 +21,24 @@ import { AddArrayElementRequest, AddClausesRequest, AddSubMappingRequest, + AllDataMapperSourceRequest, ConvertToQueryRequest, + DMModelRequest, + DataMapperAPI, + DataMapperModelRequest, + DataMapperModelResponse, + DataMapperSourceRequest, + DataMapperSourceResponse, DeleteMappingRequest, - GetInlineDataMapperCodedataRequest, - GetInlineDataMapperCodedataResponse, + ExpandedDMModelResponse, + GetDataMapperCodedataRequest, + GetDataMapperCodedataResponse, GetSubMappingCodedataRequest, InitialIDMSourceRequest, InitialIDMSourceResponse, - InlineAllDataMapperSourceRequest, - InlineDataMapperAPI, - InlineDataMapperModelRequest, - InlineDataMapperModelResponse, - InlineDataMapperSourceRequest, - InlineDataMapperSourceResponse, - MapWithCustomFnRequest, + MapWithFnRequest, + ProcessTypeReferenceRequest, + ProcessTypeReferenceResponse, PropertyRequest, PropertyResponse, VisualizableFieldsRequest, @@ -48,16 +52,19 @@ import { getDataMapperCodedata, getDataMapperModel, getDataMapperSource, + getExpandedDMFromDMModel, getInitialIDMSource, + getProcessTypeReference, getProperty, getSubMappingCodedata, getVisualizableFields, - mapWithCustomFn + mapWithCustomFn, + mapWithTransformFn } from "@wso2/ballerina-core"; import { HOST_EXTENSION } from "vscode-messenger-common"; import { Messenger } from "vscode-messenger-webview"; -export class InlineDataMapperRpcClient implements InlineDataMapperAPI { +export class DataMapperRpcClient implements DataMapperAPI { private _messenger: Messenger; constructor(messenger: Messenger) { @@ -68,11 +75,11 @@ export class InlineDataMapperRpcClient implements InlineDataMapperAPI { return this._messenger.sendRequest(getInitialIDMSource, HOST_EXTENSION, params); } - getDataMapperModel(params: InlineDataMapperModelRequest): Promise { + getDataMapperModel(params: DataMapperModelRequest): Promise { return this._messenger.sendRequest(getDataMapperModel, HOST_EXTENSION, params); } - getDataMapperSource(params: InlineDataMapperSourceRequest): Promise { + getDataMapperSource(params: DataMapperSourceRequest): Promise { return this._messenger.sendRequest(getDataMapperSource, HOST_EXTENSION, params); } @@ -80,43 +87,55 @@ export class InlineDataMapperRpcClient implements InlineDataMapperAPI { return this._messenger.sendRequest(getVisualizableFields, HOST_EXTENSION, params); } - addNewArrayElement(params: AddArrayElementRequest): Promise { + addNewArrayElement(params: AddArrayElementRequest): Promise { return this._messenger.sendRequest(addNewArrayElement, HOST_EXTENSION, params); } - convertToQuery(params: ConvertToQueryRequest): Promise { + convertToQuery(params: ConvertToQueryRequest): Promise { return this._messenger.sendRequest(convertToQuery, HOST_EXTENSION, params); } - addClauses(params: AddClausesRequest): Promise { + addClauses(params: AddClausesRequest): Promise { return this._messenger.sendRequest(addClauses, HOST_EXTENSION, params); } - addSubMapping(params: AddSubMappingRequest): Promise { + addSubMapping(params: AddSubMappingRequest): Promise { return this._messenger.sendRequest(addSubMapping, HOST_EXTENSION, params); } - deleteMapping(params: DeleteMappingRequest): Promise { + deleteMapping(params: DeleteMappingRequest): Promise { return this._messenger.sendRequest(deleteMapping, HOST_EXTENSION, params); } - mapWithCustomFn(params: MapWithCustomFnRequest): Promise { + mapWithCustomFn(params: MapWithFnRequest): Promise { return this._messenger.sendRequest(mapWithCustomFn, HOST_EXTENSION, params); } - getDataMapperCodedata(params: GetInlineDataMapperCodedataRequest): Promise { + mapWithTransformFn(params: MapWithFnRequest): Promise { + return this._messenger.sendRequest(mapWithTransformFn, HOST_EXTENSION, params); + } + + getDataMapperCodedata(params: GetDataMapperCodedataRequest): Promise { return this._messenger.sendRequest(getDataMapperCodedata, HOST_EXTENSION, params); } - getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { + getSubMappingCodedata(params: GetSubMappingCodedataRequest): Promise { return this._messenger.sendRequest(getSubMappingCodedata, HOST_EXTENSION, params); } - getAllDataMapperSource(params: InlineAllDataMapperSourceRequest): Promise { + getAllDataMapperSource(params: AllDataMapperSourceRequest): Promise { return this._messenger.sendRequest(getAllDataMapperSource, HOST_EXTENSION, params); } getProperty(params: PropertyRequest): Promise { return this._messenger.sendRequest(getProperty, HOST_EXTENSION, params); } + + getExpandedDMFromDMModel(params: DMModelRequest): Promise { + return this._messenger.sendRequest(getExpandedDMFromDMModel, HOST_EXTENSION, params); + } + + getProcessTypeReference(params: ProcessTypeReferenceRequest): Promise { + return this._messenger.sendRequest(getProcessTypeReference, HOST_EXTENSION, params); + } } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/index.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/index.ts index 3acbdc42388..81ed5771f57 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/index.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/index.ts @@ -26,3 +26,4 @@ export * from "./record-creator/rpc-client"; export * from "./ai-panel/rpc-client"; export * from "./connector-wizard/rpc-client"; export * from "./test-manager/rpc-client"; +export * from "./migrate-integration/rpc-client"; diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/migrate-integration/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/migrate-integration/rpc-client.ts new file mode 100644 index 00000000000..8c91f99e201 --- /dev/null +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/migrate-integration/rpc-client.ts @@ -0,0 +1,69 @@ +/** + * 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. + * + * THIS FILE INCLUDES AUTO GENERATED CODE + */ +import { + GetMigrationToolsResponse, + ImportIntegrationRPCRequest, + ImportIntegrationResponse, + MigrateIntegrationAPI, + MigrateRequest, + MigrationToolPullRequest, + OpenMigrationReportRequest, + SaveMigrationReportRequest, + getMigrationTools, + importIntegration, + migrateProject, + openMigrationReport, + pullMigrationTool, + saveMigrationReport +} from "@wso2/ballerina-core"; +import { HOST_EXTENSION } from "vscode-messenger-common"; +import { Messenger } from "vscode-messenger-webview"; + +export class MigrateIntegrationRpcClient implements MigrateIntegrationAPI { + private _messenger: Messenger; + + constructor(messenger: Messenger) { + this._messenger = messenger; + } + + getMigrationTools(): Promise { + return this._messenger.sendRequest(getMigrationTools, HOST_EXTENSION); + } + + pullMigrationTool(params: MigrationToolPullRequest): void { + return this._messenger.sendNotification(pullMigrationTool, HOST_EXTENSION, params); + } + + importIntegration(params: ImportIntegrationRPCRequest): Promise { + return this._messenger.sendRequest(importIntegration, HOST_EXTENSION, params); + } + + openMigrationReport(params: OpenMigrationReportRequest): void { + return this._messenger.sendNotification(openMigrationReport, HOST_EXTENSION, params); + } + + saveMigrationReport(params: SaveMigrationReportRequest): void { + return this._messenger.sendNotification(saveMigrationReport, HOST_EXTENSION, params); + } + + migrateProject(params: MigrateRequest): void { + return this._messenger.sendNotification(migrateProject, HOST_EXTENSION, params); + } +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/CardList/index.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/CardList/index.tsx index 03e976836d8..64b594411c5 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/CardList/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/CardList/index.tsx @@ -324,25 +324,27 @@ function CardList(props: CardListProps) { }); const hasContent = filteredCategories.some((category) => category?.items && category.items.length > 0); - + const shouldShowHeaderActions = (onBack && title) || onClose; return ( - - {onBack && title && ( - - - - - {title} - - )} - {onClose && ( - - - - )} - + {shouldShowHeaderActions && ( + + {onBack && title && ( + + + + + {title} + + )} + {onClose && ( + + + + )} + + )} void; scopeFieldAddon?: React.ReactNode; newServerUrl?: string; onChange?: (fieldKey: string, value: any, allValues: FormValues) => void; @@ -384,6 +387,7 @@ export const Form = forwardRef((props: FormProps, ref) => { concertMessage, formImports, preserveOrder = false, + handleSelectedTypeChange, scopeFieldAddon, newServerUrl, mcpTools, @@ -431,12 +435,16 @@ export const Form = forwardRef((props: FormProps, ref) => { formFields.forEach((field) => { if (isDropdownField(field)) { defaultValues[field.key] = getValueForDropdown(field) ?? ""; + } else if (field.type === "FLAG") { + defaultValues[field.key] = field.value === "true" || (typeof field.value === "boolean" && field.value); } else if (typeof field.value === "string") { defaultValues[field.key] = formatJSONLikeString(field.value) ?? ""; } else { defaultValues[field.key] = field.value ?? ""; } - + if (field.key === "variable") { + defaultValues[field.key] = formValues[field.key] ?? ""; + } if (field.key === "parameters" && field.value.length === 0) { defaultValues[field.key] = formValues[field.key] ?? []; } @@ -448,7 +456,7 @@ export const Form = forwardRef((props: FormProps, ref) => { if (existingType !== newType) { setValue(field.key, newType); - mergeFormDataWithFlowNode && getVisualiableFields(); + getVisualiableFields(); } } @@ -480,33 +488,6 @@ export const Form = forwardRef((props: FormProps, ref) => { } }, [formFields, reset]); - useEffect(() => { - if (updatedExpressionField) { - if (subPanelView === SubPanelView.INLINE_DATA_MAPPER) { - const { key, value } = updatedExpressionField; - // Update the form field value - setValue(key, value); - resetUpdatedExpressionField && resetUpdatedExpressionField(); - // Update the inline data mapper view - handleOpenSubPanel({ - view: SubPanelView.INLINE_DATA_MAPPER, - props: { - inlineDataMapper: { - filePath: fileName, - flowNode: undefined, - position: { - line: updatedExpressionField.cursorPosition.line, - offset: updatedExpressionField.cursorPosition.offset, - }, - propertyKey: updatedExpressionField.key, - editorKey: updatedExpressionField.key, - }, - }, - }); - } - } - }, [updatedExpressionField]); - const handleOnSave = (data: FormValues) => { console.log(">>> saved form fields", { data }); onSubmit && onSubmit(data, dirtyFields); @@ -544,32 +525,17 @@ export const Form = forwardRef((props: FormProps, ref) => { setDiagnosticsInfo([...otherDiagnostics, diagnostics]); }; - const handleOpenSubPanel = (subPanel: SubPanel) => { - let updatedSubPanel = subPanel; - if (subPanel.view === SubPanelView.INLINE_DATA_MAPPER) { - const flowNode = mergeFormDataWithFlowNode(getValues(), targetLineRange); - const inlineDMProps = { - ...subPanel.props.inlineDataMapper, - flowNode: flowNode, - }; - updatedSubPanel = { - ...subPanel, - props: { - ...subPanel.props, - inlineDataMapper: inlineDMProps, - }, - }; - } - openSubPanel(updatedSubPanel); - }; - - const handleOnTypeChange = () => { + const handleOnTypeChange = (value?: string) => { getVisualiableFields(); }; + const handleNewTypeSelected = (type: CompletionItem) => { + handleSelectedTypeChange && handleSelectedTypeChange(type); + } + const getVisualiableFields = () => { const typeName = watch("type"); - handleVisualizableFields && handleVisualizableFields(fileName, typeName); + typeName && handleVisualizableFields && handleVisualizableFields(fileName, typeName); }; const handleGetExpressionDiagnostics = async ( @@ -791,7 +757,6 @@ export const Form = forwardRef((props: FormProps, ref) => { openRecordEditor && ((open: boolean) => handleOpenRecordEditor(open, updatedField)) } - openSubPanel={handleOpenSubPanel} subPanelView={subPanelView} handleOnFieldFocus={handleOnFieldFocus} autoFocus={firstEditableFieldIndex === formFields.indexOf(updatedField)} @@ -799,6 +764,7 @@ export const Form = forwardRef((props: FormProps, ref) => { onIdentifierEditingStateChange={handleIdentifierEditingStateChange} handleOnTypeChange={handleOnTypeChange} setSubComponentEnabled={setIsSubComponentEnabled} + handleNewTypeSelected={handleNewTypeSelected} newServerUrl={newServerUrl} mcpTools={mcpTools} onToolsChange={onToolsChange} @@ -864,7 +830,6 @@ export const Form = forwardRef((props: FormProps, ref) => { openRecordEditor && ((open: boolean) => handleOpenRecordEditor(open, updatedField)) } - openSubPanel={handleOpenSubPanel} subPanelView={subPanelView} handleOnFieldFocus={handleOnFieldFocus} recordTypeFields={recordTypeFields} @@ -894,11 +859,12 @@ export const Form = forwardRef((props: FormProps, ref) => { openRecordEditor={ openRecordEditor && ((open: boolean) => handleOpenRecordEditor(open, typeField)) } - openSubPanel={handleOpenSubPanel} handleOnFieldFocus={handleOnFieldFocus} handleOnTypeChange={handleOnTypeChange} recordTypeFields={recordTypeFields} onIdentifierEditingStateChange={handleIdentifierEditingStateChange} + handleNewTypeSelected={handleNewTypeSelected} + /> )} {targetTypeField && ( @@ -908,6 +874,7 @@ export const Form = forwardRef((props: FormProps, ref) => { handleOnFieldFocus={handleOnFieldFocus} recordTypeFields={recordTypeFields} onIdentifierEditingStateChange={handleIdentifierEditingStateChange} + handleNewTypeSelected={handleNewTypeSelected} handleOnTypeChange={handleOnTypeChange} /> {typeField && ( diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts b/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts index 4f8244af31c..e2222fe028f 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts @@ -55,6 +55,8 @@ export type FormField = { metadata?: Metadata; codedata?: { [key: string]: any }; imports?: { [key: string]: string }; + actionLabel?: string | JSX.Element; + actionCallback?: () => void; onValueChange?: (value: string) => void; }; @@ -147,6 +149,7 @@ type FormTypeConditionalProps = { changeTypeHelperState: (isOpen: boolean) => void, helperPaneHeight: HelperPaneHeight, onTypeCreate: () => void, + exprRef?: RefObject, ) => JSX.Element; helperPaneOrigin?: HelperPaneOrigin; helperPaneHeight: HelperPaneHeight; @@ -170,7 +173,8 @@ type FormHelperPaneConditionalProps = { changeHelperPaneState: (isOpen: boolean) => void, helperPaneHeight: HelperPaneHeight, recordTypeField?: RecordTypeField, - isAssignIdentifier?: boolean + isAssignIdentifier?: boolean, + valueTypeConstraint?: string | string[] ) => JSX.Element; helperPaneOrigin?: HelperPaneOrigin; helperPaneHeight: HelperPaneHeight; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/categoryConfig.ts b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/categoryConfig.ts new file mode 100644 index 00000000000..eeca926019b --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/categoryConfig.ts @@ -0,0 +1,174 @@ +/** + * 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. + */ + +export type CategoryActionType = 'connection' | 'function' | 'add'; + +export interface CategoryAction { + type: CategoryActionType; + tooltip: string; + emptyStateLabel: string; + handlerKey: 'onAddConnection' | 'onAddFunction' | 'onAdd'; + condition?: (title: string) => boolean; // For special conditions like data mapper +} + +export interface CategoryConfig { + title: string; + actions: CategoryAction[]; + showWhenEmpty: boolean; + useConnectionContainer: boolean; // Whether to use getConnectionContainer for rendering +} + +// Configuration for all categories with their specific behaviors +export const CATEGORY_CONFIGS: Record = { + "Connections": { + title: "Connections", + actions: [{ + type: 'connection', + tooltip: "Add Connection", + emptyStateLabel: "Add Connection", + handlerKey: 'onAddConnection' + }], + showWhenEmpty: true, + useConnectionContainer: true + }, + "Current Integration": { + title: "Current Integration", + actions: [ + { + type: 'function', + tooltip: "Create Data Mapper", + emptyStateLabel: "Create Data Mapper", + handlerKey: 'onAddFunction', + condition: (title) => title === "Data Mappers" + }, + { + type: 'function', + tooltip: "Create Natural Function", + emptyStateLabel: "Create Natural Function", + handlerKey: 'onAddFunction', + condition: (title) => title === "Natural Functions" + }, + { + type: 'function', + tooltip: "Create Function", + emptyStateLabel: "Create Function", + handlerKey: 'onAddFunction', + condition: (title) => title !== "Data Mappers" && title !== "Natural Functions" + } + ], + showWhenEmpty: true, + useConnectionContainer: false + }, + "Agents": { + title: "Agents", + actions: [], + showWhenEmpty: true, + useConnectionContainer: false + }, + "Model Providers": { + title: "Model Providers", + actions: [{ + type: 'add', + tooltip: "Add Model Provider", // Will use addButtonLabel from props + emptyStateLabel: "", // Will use addButtonLabel from props + handlerKey: 'onAdd' + }], + showWhenEmpty: true, + useConnectionContainer: true + }, + "Vector Stores": { + title: "Vector Stores", + actions: [{ + type: 'add', + tooltip: "", + emptyStateLabel: "", + handlerKey: 'onAdd' + }], + showWhenEmpty: true, + useConnectionContainer: true + }, + "Embedding Providers": { + title: "Embedding Providers", + actions: [{ + type: 'add', + tooltip: "", + emptyStateLabel: "", + handlerKey: 'onAdd' + }], + showWhenEmpty: true, + useConnectionContainer: true + }, + "Data Loaders": { + title: "Data Loaders", + actions: [{ + type: 'add', + tooltip: "", + emptyStateLabel: "", + handlerKey: 'onAdd' + }], + showWhenEmpty: true, + useConnectionContainer: true + }, + "Chunkers": { + title: "Chunkers", + actions: [{ + type: 'add', + tooltip: "", + emptyStateLabel: "", + handlerKey: 'onAdd' + }], + showWhenEmpty: true, + useConnectionContainer: true + }, + "Vector Knowledge Bases": { + title: "Vector Knowledge Bases", + actions: [{ + type: 'add', + tooltip: "", + emptyStateLabel: "", + handlerKey: 'onAdd' + }], + showWhenEmpty: true, + useConnectionContainer: true + } +}; + +// Helper functions for category configuration +export const getCategoryConfig = (title: string): CategoryConfig | undefined => { + return CATEGORY_CONFIGS[title]; +}; + +export const shouldShowEmptyCategory = (title: string): boolean => { + const config = getCategoryConfig(title); + return config?.showWhenEmpty ?? false; +}; + +export const shouldUseConnectionContainer = (title: string): boolean => { + const config = getCategoryConfig(title); + return config?.useConnectionContainer ?? false; +}; + +export const getCategoryActions = (title: string, contextTitle?: string): CategoryAction[] => { + const config = getCategoryConfig(title); + if (!config) return []; + + return config.actions.filter(action => { + if (!action.condition) return true; + return action.condition(contextTitle || title); + }); +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx index 4cfe3773c16..40fd2115da7 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx @@ -32,8 +32,11 @@ import styled from "@emotion/styled"; import { BackIcon, CloseIcon, LogIcon } from "../../resources"; import { Category, Item, Node } from "./types"; import { cloneDeep, debounce } from "lodash"; +import { GroupListSkeleton } from "../Skeletons"; import GroupList from "../GroupList"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { getExpandedCategories, setExpandedCategories, getDefaultExpandedState } from "../../utils/localStorage"; +import { getCategoryConfig, shouldShowEmptyCategory, shouldUseConnectionContainer, getCategoryActions, CategoryAction } from "./categoryConfig"; namespace S { export const Container = styled.div<{}>` @@ -63,10 +66,10 @@ namespace S { justify-content: flex-start; align-items: flex-start; width: 100%; - margin-top: 8px; - margin-bottom: ${({ showBorder }) => (showBorder ? "20px" : "12px")}; - padding-bottom: 8px; - border-bottom: ${({ showBorder }) => (showBorder ? `1px solid ${ThemeColors.OUTLINE_VARIANT}` : "none")}; + margin-top: 0; + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; `; export const Row = styled.div<{}>` @@ -86,16 +89,17 @@ namespace S { export const Grid = styled.div<{ columns: number }>` display: grid; - grid-template-columns: repeat(${({ columns }) => columns}, 1fr); + grid-template-columns: repeat(${({ columns }) => columns}, minmax(0, 1fr)); gap: 8px; width: 100%; margin-top: 8px; + margin-bottom: 12px; `; export const Title = styled.div<{}>` font-size: 14px; font-family: GilmerBold; - text-wrap: nowrap; + white-space: nowrap; &:first { margin-top: 0; } @@ -122,6 +126,8 @@ namespace S { height: 36px; cursor: ${({ enabled }) => (enabled ? "pointer" : "not-allowed")}; font-size: 14px; + min-width: 160px; + max-width: 100%; ${({ enabled }) => !enabled && "opacity: 0.5;"} &:hover { ${({ enabled }) => @@ -135,7 +141,8 @@ namespace S { export const ComponentTitle = styled.div` white-space: nowrap; - width: 124px; + flex: 1; + min-width: 0; overflow: hidden; text-overflow: ellipsis; display: block; @@ -143,10 +150,11 @@ namespace S { `; export const IconContainer = styled.div` - padding: 0 8px; + padding: 0 4px; display: flex; align-items: center; justify-content: center; + flex-shrink: 0; & svg { height: 16px; width: 16px; @@ -173,6 +181,7 @@ namespace S { export const HighlightedButton = styled.div` margin-top: 10px; + margin-bottom: 12px; width: 100%; display: flex; flex-direction: row; @@ -190,13 +199,6 @@ namespace S { } `; - export const AddConnectionButton = styled(Button)` - display: flex; - flex-direction: row; - justify-content: center; - width: 100%; - `; - export const AiContainer = styled.div` display: flex; flex-direction: column; @@ -241,6 +243,53 @@ namespace S { color: ${ThemeColors.ON_SURFACE_VARIANT}; transition: all 0.2s ease; `; + + export const CategoryHeader = styled.div<{ fullWidth?: boolean }>` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: -webkit-fill-available; + padding: 12px; + cursor: pointer; + transition: all 0.2s ease; + border-radius: 5px; + margin: ${({ fullWidth }) => fullWidth ? '0 -12px' : '0'}; + + &:hover { + background-color: ${ThemeColors.PRIMARY_CONTAINER}; + } + `; + + export const CategoryCard = styled.div<{ hasBackground?: boolean }>` + background-color: ${({ hasBackground }) => hasBackground ? `rgba(255, 255, 255, 0.02)` : 'transparent'}; + border-radius: 5px; + padding: ${({ hasBackground }) => hasBackground ? '0 12px' : '0'}; + margin-bottom: 16px; + border: ${({ hasBackground }) => hasBackground ? `1px solid ${ThemeColors.OUTLINE_VARIANT}` : 'none'}; + transition: all 0.2s ease; + + &:hover { + ${({ hasBackground }) => hasBackground && ` + background-color: rgba(255, 255, 255, 0.04); + `} + } + `; + + export const ChevronIcon = styled.div<{ isExpanded: boolean }>` + transition: transform 0.2s ease; + transform: ${({ isExpanded }) => isExpanded ? 'rotate(-90deg)' : 'rotate(90deg)'}; + display: flex; + align-items: center; + justify-content: center; + `; + + export const CategorySeparator = styled.div` + width: 100%; + height: 1px; + background-color: ${ThemeColors.OUTLINE_VARIANT}; + margin: 16px 0; + `; } interface NodeListProps { @@ -278,6 +327,7 @@ export function NodeList(props: NodeListProps) { const [showGeneratePanel, setShowGeneratePanel] = useState(false); const [isSearching, setIsSearching] = useState(false); const [expandedMoreSections, setExpandedMoreSections] = useState>({}); + const [expandedCategories, setExpandedCategoriesState] = useState>({}); const { rpcClient } = useRpcContext(); const [isNPSupported, setIsNPSupported] = useState(false); @@ -290,6 +340,19 @@ export function NodeList(props: NodeListProps) { }); }, []); + // Initialize expanded categories state from localStorage + useEffect(() => { + if (categories && categories.length > 0) { + const categoryTitles = categories.map(cat => cat.title); + const storedState = getExpandedCategories(); + const defaultState = getDefaultExpandedState(categoryTitles); + + // Merge stored state with defaults, prioritizing stored state + const mergedState = { ...defaultState, ...storedState }; + setExpandedCategoriesState(mergedState); + } + }, [categories]); + useEffect(() => { if (onSearchTextChange) { setIsSearching(true); @@ -319,8 +382,19 @@ export function NodeList(props: NodeListProps) { })); }; + const toggleCategory = (categoryTitle: string) => { + const newExpandedState = { + ...expandedCategories, + [categoryTitle]: !expandedCategories[categoryTitle], + }; + setExpandedCategoriesState(newExpandedState); + setExpandedCategories(newExpandedState); + }; + const handleAddNode = (node: Node, category?: string) => { - onSelect(node.id, { node: node.metadata, category }); + if (node.enabled) { + onSelect(node.id, { node: node.metadata, category }); + } }; const handleAddConnection = () => { @@ -355,25 +429,34 @@ export function NodeList(props: NodeListProps) { } return ( - handleAddNode(node)} - title={node.label} + - {node.icon || } - { - if (el && el.scrollWidth > el.clientWidth) { - el.style.fontSize = "13px"; - el.style.wordBreak = "break-word"; - el.style.whiteSpace = "nowrap"; - } - }} + handleAddNode(node)} > - {node.label} - - + {node.icon || } + { + if (el && el.scrollWidth > el.clientWidth) { + el.style.fontSize = "13px"; + el.style.wordBreak = "break-word"; + el.style.whiteSpace = "nowrap"; + } + }} + > + {node.label} + + + ); })} @@ -424,14 +507,18 @@ export function NodeList(props: NodeListProps) { const getConnectionContainer = (categories: Category[]) => ( - {categories.map((category, index) => ( - 0} - onSelect={handleAddNode} - /> - ))} + {categories.map((category, index) => + category.isLoading ? ( + + ) : ( + 0} + onSelect={handleAddNode} + /> + )) + } ); @@ -440,31 +527,44 @@ export function NodeList(props: NodeListProps) { .flatMap((group) => group?.items || []) .filter((item) => item != null) .find((item) => "id" in item && item.id === "FUNCTION"); + + // Configuration for special categories + const categoryConfig = { + "Connections": { hasBackground: false }, + "Statement": { hasBackground: true, showSeparatorBefore: true }, // Show separator before Statement + "AI": { hasBackground: true, targetPosition: 3 }, // 4th position (0-indexed) + "Control": { hasBackground: true }, + "Error Handling": { hasBackground: true }, + "Concurrency": { hasBackground: true }, + "Logging": { hasBackground: true }, + "Model Providers": { hasBackground: false }, + "Embedding Providers": { hasBackground: false }, + "Vector Knowledge Bases": { hasBackground: false }, + "Vector Stores": { hasBackground: false }, + "Data Loaders": { hasBackground: false }, + "Chunkers": { hasBackground: false }, + }; + + // Reorder categories to move AI as 4th category + const reorderedGroups = [...groups]; + const aiCategoryIndex = reorderedGroups.findIndex(group => group.title === "AI"); + if (aiCategoryIndex !== -1 && aiCategoryIndex < 3) { + const aiCategory = reorderedGroups.splice(aiCategoryIndex, 1)[0]; + reorderedGroups.splice(3, 0, aiCategory); + } + const content = ( <> - {groups.map((group, index) => { - const isConnectionCategory = group.title === "Connections"; - const isProjectFunctionsCategory = group.title === "Current Integration"; - const isDataMapperCategory = isProjectFunctionsCategory && title === "Data Mappers"; - const isAgentCategory = group.title === "Agents"; - const isNpFunctionCategory = isProjectFunctionsCategory && title === "Natural Functions"; - const isModelProviderCategory = group.title === "Model Providers"; - const isVectorStoreCategory = group.title === "Vector Stores"; - const isEmbeddingProviderCategory = group.title === "Embedding Providers"; - const isVectorKnowledgeBaseCategory = group.title === "Vector Knowledge Bases"; + {reorderedGroups.map((group, index) => { + const categoryActions = getCategoryActions(group.title, title); + const config = categoryConfig[group.title] || { hasBackground: true }; + const shouldShowSeparator = config.showSeparatorBefore; + const isLoggingCategory = group.title === "Logging"; + // Hide categories that don't have items, except for special categories that can add items if (!group || !group.items || group.items.length === 0) { // Only show empty categories if they have add functionality - if ( - !isConnectionCategory && - !isProjectFunctionsCategory && - !isAgentCategory && - !isNpFunctionCategory && - !isModelProviderCategory && - !isVectorStoreCategory && - !isEmbeddingProviderCategory && - !isVectorKnowledgeBaseCategory - ) { + if (!shouldShowEmptyCategory(group.title)) { return null; } } @@ -472,150 +572,140 @@ export function NodeList(props: NodeListProps) { return null; } // skip current integration category if onAddFunction is not provided and items are empty - if (!onAddFunction && isProjectFunctionsCategory && (!group.items || group.items.length === 0)) { + if (!onAddFunction && group.title === "Current Integration" && (!group.items || group.items.length === 0)) { return null; } + const isCategoryExpanded = shouldExpandAll || expandedCategories[group.title] !== false; + return ( - - - {isSubCategory && ( - - {group.title} - - )} - {!isSubCategory && ( - <> - {group.title} + + {shouldShowSeparator && } + + + {!isSubCategory ? ( + toggleCategory(group.title)}> + + {group.title} +
+ {categoryActions.map((action, actionIndex) => { + const handlers = { + onAddConnection: handleAddConnection, + onAddFunction: handleAddFunction, + onAdd: handleAdd + }; + + const handler = handlers[action.handlerKey]; + const propsHandler = props[action.handlerKey]; + + // Only render if the handler exists in props + if (!propsHandler || !handler) return null; + + const tooltipText = action.tooltip || addButtonLabel || ""; + + return ( + + + + ); + })} + + + + + +
+
+
+ ) : ( + + + {group.title} + + + )} + {isCategoryExpanded && ( <> - {onAddConnection && isConnectionCategory && ( - - )} - {onAddFunction && isDataMapperCategory && ( - - )} - {onAddFunction && - isProjectFunctionsCategory && - !isDataMapperCategory && - !isNpFunctionCategory && ( - - )} - {onAddFunction && isNpFunctionCategory && ( - - )} - {onAdd && addButtonLabel && ( - + {(!group.items || group.items.length === 0) && + !searchText && + !isSearching && + categoryActions.map((action, actionIndex) => { + const handlers = { + onAddConnection: handleAddConnection, + onAddFunction: handleAddFunction, + onAdd: handleAdd + }; + + const handler = handlers[action.handlerKey]; + const propsHandler = props[action.handlerKey]; + + // Only render if the handler exists in props + if (!propsHandler || !handler) return null; + + const buttonLabel = action.emptyStateLabel || addButtonLabel || "Add"; + + return ( + + + {buttonLabel} + + ); + })} + {group.items && + group.items.length > 0 && + // 1. If parent group uses connection container and ALL items don't have id, use getConnectionContainer + shouldUseConnectionContainer(group.title) && + group.items.filter((item) => item != null).every((item) => !("id" in item)) + ? getConnectionContainer(group.items as Category[]) + : // 2. If ALL items don't have id (all are categories), use getCategoryContainer + group.items.filter((item) => item != null).every((item) => !("id" in item)) + ? getCategoryContainer( + group.items as Category[], + true, + !isSubCategory ? group.title : parentCategoryTitle + ) + : // 3. Otherwise (has items with id or mixed), use getNodesContainer + getNodesContainer( + group.items as (Node | Category)[], + !isSubCategory ? group.title : parentCategoryTitle + )} + {/* Add Show More Functions under Logging category */} + {isLoggingCategory && callFunctionNode && isCategoryExpanded && ( + + handleAddNode(callFunctionNode as Node)}> + Show More Functions + + + )} - - )} -
- {onAddConnection && isConnectionCategory && (!group.items || group.items.length === 0) && ( - - - Add Connection - - )} - {onAddFunction && - isProjectFunctionsCategory && - (!group.items || group.items.length === 0) && - !searchText && - !isSearching && ( - - - {`Create ${ - isDataMapperCategory - ? "Data Mapper" - : isNpFunctionCategory - ? "Natural Function" - : "Function" - }`} - - )} - {onAdd && - addButtonLabel && - (isModelProviderCategory || isVectorStoreCategory || isEmbeddingProviderCategory || isVectorKnowledgeBaseCategory) && - (!group.items || group.items.length === 0) && - !searchText && - !isSearching && ( - - - {addButtonLabel} - - )} - {group.items && - group.items.length > 0 && - // 1. If parent group is "Connections", "Model Providers", "Vector Stores", "Embedding Providers", or "Vector Knowledge Bases" and ALL items don't have id, use getConnectionContainer - (group.title === "Connections" || group.title === "Model Providers" || group.title === "Vector Stores" || group.title === "Embedding Providers" || group.title === "Vector Knowledge Bases") && - group.items.filter((item) => item != null).every((item) => !("id" in item)) - ? getConnectionContainer(group.items as Category[]) - : // 2. If ALL items don't have id (all are categories), use getCategoryContainer - group.items.filter((item) => item != null).every((item) => !("id" in item)) - ? getCategoryContainer( - group.items as Category[], - true, - !isSubCategory ? group.title : parentCategoryTitle - ) - : // 3. Otherwise (has items with id or mixed), use getNodesContainer - getNodesContainer( - group.items as (Node | Category)[], - !isSubCategory ? group.title : parentCategoryTitle - )} -
+ )} + + + ); })} - {callFunctionNode && ( - - handleAddNode(callFunctionNode as Node)}> - Show More Functions - - - - )} ); @@ -671,6 +761,9 @@ export function NodeList(props: NodeListProps) { return category; }); + // When searching, expand all categories + const shouldExpandAll = searchText && searchText.length > 0; + return ( diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/types.ts b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/types.ts index e34c3314b90..2decc078280 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/types.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/types.ts @@ -23,6 +23,7 @@ export type Category = { description: string; icon?: JSX.Element; items: Item[]; + isLoading?: boolean; }; export type Node = { diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamEditor.tsx index 1956efa083f..bb2bc268d26 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamEditor.tsx @@ -79,7 +79,8 @@ export function ParamEditor(props: ParamProps) { onChange: (newType: string, newCursorPosition: number) => void, changeTypeHelperState: (isOpen: boolean) => void, helperPaneHeight: HelperPaneHeight, - closeCompletions: () => void + closeCompletions: () => void, + exprRef: RefObject ) => { return expressionEditor?.getTypeHelper( propertyKey, @@ -91,7 +92,8 @@ export function ParamEditor(props: ParamProps) { onChange, changeTypeHelperState, helperPaneHeight, - closeCompletions + closeCompletions, + exprRef ); }; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamManager.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamManager.tsx index 551901dbae5..357f80bd888 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamManager.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamManager.tsx @@ -25,7 +25,7 @@ import { Codicon, ErrorBanner, LinkButton, RequiredFormInput, ThemeColors } from import { FormField, FormValues } from '../Form/types'; import { Controller } from 'react-hook-form'; import { useFormContext } from '../../context'; -import { NodeKind } from '@wso2/ballerina-core'; +import { Imports, NodeKind } from '@wso2/ballerina-core'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; import { EditorFactory } from '../editors/EditorFactory'; import { getFieldKeyForAdvanceProp } from '../editors/utils'; @@ -38,6 +38,8 @@ export interface Parameter { icon: string; identifierEditable: boolean; identifierRange: any; + hidden?: boolean; + imports?: Imports; } @@ -324,7 +326,7 @@ export function ParamManager(props: ParamManagerProps) { openRecordEditor={openRecordEditor} /> ) - } else if ((editingSegmentId !== index)) { + } else if ((editingSegmentId !== index && !(param.hidden ?? false))) { render.push( { + return ( + + + + + + + + ); +}; + +export default GroupListSkeleton; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/ValueConfigButton/index.ts b/workspaces/ballerina/ballerina-side-panel/src/components/Skeletons/index.tsx similarity index 92% rename from workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/ValueConfigButton/index.ts rename to workspaces/ballerina/ballerina-side-panel/src/components/Skeletons/index.tsx index ada066a87ca..e04fca2ef9d 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/ValueConfigButton/index.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Skeletons/index.tsx @@ -15,4 +15,5 @@ * specific language governing permissions and limitations * under the License. */ -export * from "./ValueConfigMenu"; + +export { GroupListSkeleton } from "./GroupListSkeleton"; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/Skeletons/styles.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/Skeletons/styles.tsx new file mode 100644 index 00000000000..6e753802bb9 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Skeletons/styles.tsx @@ -0,0 +1,59 @@ +/** + * 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 { keyframes } from "@emotion/react"; +import styled from "@emotion/styled"; + +// Skeleton pulse animation +export const skeletonPulse = keyframes` + 0% { + opacity: 0.3; + } + 50% { + opacity: 0.9; + } + 100% { + opacity: 0.3; + } +`; + +// Base skeleton element +export const SkeletonBase = styled.div<{ + width?: string | number; + height?: string | number; + borderRadius?: string | number; + margin?: string; + position?: string; + top?: string | number; + left?: string | number; +}>` + background-color: var(--vscode-editor-inactiveSelectionBackground); + border-radius: ${({ borderRadius = "4px" }) => + typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius}; + animation: ${skeletonPulse} 1.5s ease-in-out infinite; + width: ${({ width = "100%" }) => + typeof width === "number" ? `${width}px` : width}; + height: ${({ height = "16px" }) => + typeof height === "number" ? `${height}px` : height}; + margin: ${({ margin = "0" }) => margin}; + position: ${({ position = "static" }) => position}; + top: ${({ top }: { top?: string | number }) => + typeof top === "number" ? `${top}px` : top}; + left: ${({ left }: { left?: string | number }) => + typeof left === "number" ? `${left}px` : left}; +`; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ActionExpressionEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ActionExpressionEditor.tsx new file mode 100644 index 00000000000..e7916549762 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ActionExpressionEditor.tsx @@ -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 React from "react"; +import { useFormContext } from "../../context"; +import { ContextAwareExpressionEditorProps, ExpressionEditor } from "./ExpressionEditor"; +import { LinkButton } from "@wso2/ui-toolkit/lib/components/LinkButton/LinkButton"; +import styled from "@emotion/styled"; + +const Row = styled.div` + display: flex; + flex-direction: column; + margin: 0; +`; + +const actionButtonStyles = { + padding: "4px 6px", + margin: 0, + marginTop: "6px", + fontSize: "13px", +}; + +export const ActionExpressionEditor = (props: ContextAwareExpressionEditorProps) => { + const { form, expressionEditor, targetLineRange, fileName } = useFormContext(); + + return ( + <> + + + {props.field.actionLabel} + + + ); +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/CheckBoxEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/CheckBoxEditor.tsx index a89ca94b648..6e8667ab960 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/CheckBoxEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/CheckBoxEditor.tsx @@ -41,7 +41,6 @@ const LabelGroup = styled.div` const BoxGroup = styled.div` display: flex; flex-direction: row; - gap: 12px; width: 100%; align-items: flex-start; `; @@ -56,10 +55,21 @@ export function CheckBoxEditor(props: TextEditorProps) { const { form } = useFormContext(); const { register, control } = form; + const getBooleanValue = (value: any) => { + if (field.type === "FLAG") { + return value === "true" || value === true; + } + return value; + }; + return ( - + {field.documentation} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx index f41cd238eb8..c5f30bb04d0 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx @@ -41,7 +41,9 @@ import { ContextAwareRawExpressionEditor } from "./RawExpressionEditor"; import { IdentifierField } from "./IdentifierField"; import { PathEditor } from "./PathEditor"; import { HeaderSetEditor } from "./HeaderSetEditor"; +import { CompletionItem } from "@wso2/ui-toolkit"; import { CustomDropdownEditor } from "./CustomDropdownEditor"; +import { ActionExpressionEditor } from "./ActionExpressionEditor"; interface FormFieldEditorProps { field: FormField; @@ -55,6 +57,8 @@ interface FormFieldEditorProps { recordTypeFields?: RecordTypeField[]; onIdentifierEditingStateChange?: (isEditing: boolean) => void; setSubComponentEnabled?: (isAdding: boolean) => void; + handleNewTypeSelected?: (type: CompletionItem) => void; + scopeFieldAddon?: React.ReactNode; newServerUrl?: string; mcpTools?: { name: string; description?: string }[]; @@ -74,6 +78,7 @@ export const EditorFactory = (props: FormFieldEditorProps) => { recordTypeFields, onIdentifierEditingStateChange, setSubComponentEnabled, + handleNewTypeSelected, scopeFieldAddon, newServerUrl } = props; @@ -130,6 +135,8 @@ export const EditorFactory = (props: FormFieldEditorProps) => { handleOnFieldFocus={handleOnFieldFocus} autoFocus={autoFocus} handleOnTypeChange={handleOnTypeChange} + handleNewTypeSelected={handleNewTypeSelected} + /> ); } else if (!field.items && (field.type === "EXPRESSION" || field.type === "LV_EXPRESSION" || field.type == "ACTION_OR_EXPRESSION") && field.editable) { @@ -171,6 +178,17 @@ export const EditorFactory = (props: FormFieldEditorProps) => { return ; } else if (field.type === "SERVICE_PATH" || field.type === "ACTION_PATH") { return ; + } else if (!field.items && field.type === "ACTION_EXPRESSION") { + return ( + recordField.key === field.key)} + /> + ); } else { // Default to text editor // Readonly fields are also treated as text editor diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx index d2759099846..59baf4ee640 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { debounce } from 'lodash'; import React, { useEffect, useRef, useState } from 'react'; import { Control, Controller, FieldValues, UseFormWatch } from 'react-hook-form'; import styled from '@emotion/styled'; @@ -39,14 +38,15 @@ import { LineRange, RecordTypeField, SubPanel, - SubPanelView, - SubPanelViewProps + SubPanelView } from '@wso2/ballerina-core'; import ReactMarkdown from 'react-markdown'; +import { FieldProvider } from "./FieldContext"; export type ContextAwareExpressionEditorProps = { id?: string; fieldKey?: string; + valueTypeConstraint?: string; placeholder?: string; required?: boolean; showHeader?: boolean; @@ -56,6 +56,8 @@ export type ContextAwareExpressionEditorProps = { handleOnFieldFocus?: (key: string) => void; autoFocus?: boolean; recordTypeField?: RecordTypeField; + helperPaneZIndex?: number; + }; type ExpressionEditorProps = ContextAwareExpressionEditorProps & @@ -72,7 +74,7 @@ export namespace S { display: 'flex', flexDirection: 'column', gap: '4px', - fontFamily: 'var(--font-family)' + fontFamily: 'var(--font-family)', }); export const Ribbon = styled.div({ @@ -289,6 +291,7 @@ export const ContextAwareExpressionEditor = (props: ContextAwareExpressionEditor { onSave, onCancel, onRemove, - openSubPanel, handleOnFieldFocus, - subPanelView, targetLineRange, fileName, - helperPaneOrigin, helperPaneHeight, recordTypeField, + helperPaneZIndex, growRange = { start: 1, offset: 9 }, rawExpression, // original expression sanitizedExpression // sanitized expression that will be rendered in the editor @@ -381,34 +382,6 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { await onCompletionItemSelect?.(value, key, item.additionalTextEdits); }; - const handleOpenSubPanel = (view: SubPanelView, subPanelInfo: SubPanelViewProps) => { - openSubPanel({ - view: view, - props: view === SubPanelView.UNDEFINED ? undefined : subPanelInfo - }); - }; - - const handleInlineDataMapperOpen = (isUpdate: boolean) => { - if (subPanelView === SubPanelView.INLINE_DATA_MAPPER && !isUpdate) { - openSubPanel({ view: SubPanelView.UNDEFINED }); - } else { - handleOpenSubPanel(SubPanelView.INLINE_DATA_MAPPER, { - inlineDataMapper: { - filePath: effectiveFileName, - flowNode: undefined, // This will be updated in the Form component - position: { - line: effectiveTargetLineRange.startLine.line, - offset: effectiveTargetLineRange.startLine.offset - }, - propertyKey: key, - editorKey: key - } - }); - handleOnFieldFocus?.(key); - } - }; - - const handleChangeHelperPaneState = (isOpen: boolean) => { setIsHelperPaneOpen(isOpen); }; @@ -436,22 +409,15 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { handleChangeHelperPaneState, helperPaneHeight, recordTypeField, - field.type === "LV_EXPRESSION" + field.type === "LV_EXPRESSION", + field.valueTypeConstraint, ); }; - const updateSubPanelData = (value: string) => { - if (subPanelView === SubPanelView.INLINE_DATA_MAPPER) { - handleInlineDataMapperOpen(true); - } - }; - const handleExtractArgsFromFunction = async (value: string, cursorPosition: number) => { return await extractArgsFromFunction(value, getPropertyFromFormField(field), cursorPosition); }; - const debouncedUpdateSubPanelData = debounce(updateSubPanelData, 300); - const defaultValueText = field.defaultValue ? Defaults to {field.defaultValue} : null; @@ -461,103 +427,111 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { : `${field.documentation}.` : ''; + return ( - - {showHeader && ( - - - - {field.label} - {(required ?? !field.optional) && } - - {field.valueTypeConstraint && ( - - {sanitizeType(field.valueTypeConstraint as string)} - - )} - + + + {showHeader && ( + + + + {field.label} + {(required ?? !field.optional) && } + + {field.valueTypeConstraint && ( + + {sanitizeType(field.valueTypeConstraint as string)} + + )} + {documentation && {documentation}} {defaultValueText} - )} - ( -
- } - ariaLabel={field.label} - onChange={async (updatedValue: string, updatedCursorPosition: number) => { - if (updatedValue === value) { - return; - } + )} + ( +
+ } + ariaLabel={field.label} + onChange={async (updatedValue: string, updatedCursorPosition: number) => { + if (updatedValue === value) { + return; + } const rawValue = rawExpression ? rawExpression(updatedValue) : updatedValue; onChange(rawValue); - debouncedUpdateSubPanelData(rawValue); - - if (getExpressionEditorDiagnostics) { - getExpressionEditorDiagnostics( - (required ?? !field.optional) || rawValue !== '', - rawValue, - key, - getPropertyFromFormField(field) - ); - } - - // Check if the current character is a trigger character - const triggerCharacter = - updatedCursorPosition > 0 - ? triggerCharacters.find((char) => rawValue[updatedCursorPosition - 1] === char) - : undefined; - if (triggerCharacter) { - await retrieveCompletions( - rawValue, - getPropertyFromFormField(field), - updatedCursorPosition, - triggerCharacter - ); - } else { - await retrieveCompletions( - rawValue, - getPropertyFromFormField(field), - updatedCursorPosition - ); - } - }} - extractArgsFromFunction={handleExtractArgsFromFunction} - onCompletionSelect={handleCompletionSelect} - onFocus={handleFocus} - onBlur={handleBlur} - onSave={onSave} - onCancel={onCancel} - onRemove={onRemove} - enableExIcon={false} - isHelperPaneOpen={isHelperPaneOpen} - changeHelperPaneState={handleChangeHelperPaneState} - helperPaneOrigin={helperPaneOrigin} - getHelperPane={handleGetHelperPane} - helperPaneHeight={helperPaneHeight} - helperPaneWidth={recordTypeField ? 400 : undefined} - growRange={growRange} - sx={{ paddingInline: '0' }} - placeholder={placeholder} - /> - {error && } -
- )} - /> - + + if (getExpressionEditorDiagnostics) { + getExpressionEditorDiagnostics( + (required ?? !field.optional) || rawValue !== '', + rawValue, + key, + getPropertyFromFormField(field) + ); + } + + // Check if the current character is a trigger character + const triggerCharacter = + updatedCursorPosition > 0 + ? triggerCharacters.find((char) => rawValue[updatedCursorPosition - 1] === char) + : undefined; + if (triggerCharacter) { + await retrieveCompletions( + rawValue, + getPropertyFromFormField(field), + updatedCursorPosition, + triggerCharacter + ); + } else { + await retrieveCompletions( + rawValue, + getPropertyFromFormField(field), + updatedCursorPosition + ); + } + }} + extractArgsFromFunction={handleExtractArgsFromFunction} + onCompletionSelect={handleCompletionSelect} + onFocus={async () => { + handleFocus(); + }} + onBlur={handleBlur} + onSave={onSave} + onCancel={onCancel} + onRemove={onRemove} + enableExIcon={false} + isHelperPaneOpen={isHelperPaneOpen} + changeHelperPaneState={handleChangeHelperPaneState} + helperPaneOrigin="vertical" + getHelperPane={handleGetHelperPane} + helperPaneHeight={helperPaneHeight} + helperPaneWidth={recordTypeField ? 400 : undefined} + growRange={growRange} + sx={{ paddingInline: '0' }} + placeholder={placeholder} + helperPaneZIndex={helperPaneZIndex} + /> + {error && } +
+ )} + /> +
+
); }; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/FieldContext.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FieldContext.tsx new file mode 100644 index 00000000000..076f2455972 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FieldContext.tsx @@ -0,0 +1,46 @@ +/** + * 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 React, { createContext, useContext, useState, ReactNode } from "react"; +import { FormField } from "../Form/types"; + +type FieldContextType = { + field: FormField | null; + setField: (field: FormField) => void; + triggerCharacters: readonly string[]; +}; + +const FieldContext = createContext(undefined); + +export const useFieldContext = () => { + const context = useContext(FieldContext); + if (!context) { + throw new Error("useFieldContext must be used within a FieldProvider"); + } + return context; +}; + +export const FieldProvider = ({ children, initialField, triggerCharacters }: { children: ReactNode; initialField?: FormField, triggerCharacters: readonly string[] }) => { + const [field, setField] = useState(initialField ?? null); + + return ( + + {children} + + ); +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/TypeEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/TypeEditor.tsx index 2a8091460e4..8b858232e6f 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/TypeEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/TypeEditor.tsx @@ -27,7 +27,8 @@ import { RequiredFormInput, ThemeColors, Tooltip, - Typography + Typography, + CompletionItem, } from "@wso2/ui-toolkit"; import { FormField } from "../Form/types"; import { useFormContext } from "../../context"; @@ -42,7 +43,8 @@ interface TypeEditorProps { field: FormField; openRecordEditor: (open: boolean) => void; handleOnFieldFocus?: (key: string) => void; - handleOnTypeChange?: () => void; + handleOnTypeChange?: (value?: string) => void; + handleNewTypeSelected?: (type: CompletionItem) => void; autoFocus?: boolean; } @@ -66,10 +68,10 @@ const EditorRibbon = ({ onClick }: { onClick: () => void }) => { return ( - @@ -89,7 +91,7 @@ const getDefaultCompletion = (newType: string) => { } export function TypeEditor(props: TypeEditorProps) { - const { field, openRecordEditor, handleOnFieldFocus, handleOnTypeChange, autoFocus } = props; + const { field, openRecordEditor, handleOnFieldFocus, handleOnTypeChange, autoFocus, handleNewTypeSelected } = props; const { form, expressionEditor } = useFormContext(); const { control } = form; const { @@ -103,7 +105,7 @@ export function TypeEditor(props: TypeEditorProps) { onBlur, onCompletionItemSelect, onSave, - onCancel, + onCancel } = expressionEditor; const exprRef = useRef(null); @@ -154,7 +156,7 @@ export function TypeEditor(props: TypeEditorProps) { } const handleTypeEdit = (value: string) => { - handleOnTypeChange && handleOnTypeChange(); + handleOnTypeChange && handleOnTypeChange(value); }; const debouncedTypeEdit = debounce(handleTypeEdit, 300); @@ -186,7 +188,8 @@ export function TypeEditor(props: TypeEditorProps) { onChange, handleChangeTypeHelperState, helperPaneHeight, - handleCancel + handleCancel, + exprRef ); } @@ -228,7 +231,7 @@ export function TypeEditor(props: TypeEditorProps) { {field.documentation && {field.documentation}}
- {field.valueTypeConstraint && + {field.valueTypeConstraint && {sanitizeType(field.valueTypeConstraint as string)}}
type.label === updatedValue); + handleNewTypeSelected && handleNewTypeSelected(typeExists) const validTypeForCreation = updatedValue.match(/^[a-zA-Z_'][a-zA-Z0-9_]*$/); if (updatedValue && !typeExists && validTypeForCreation) { setShowDefaultCompletion(true); @@ -295,6 +299,7 @@ export function TypeEditor(props: TypeEditorProps) { placeholder={field.placeholder} autoFocus={autoFocus} sx={{ paddingInline: '0' }} + helperPaneZIndex={40001} /> {error?.message && } diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/index.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/index.ts index 2000e614562..06bdf31ca27 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/index.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/index.ts @@ -25,3 +25,5 @@ export * from "./ArrayEditor"; export * from "./MapEditor"; export * from "./FileSelect"; export * from "./FormMapEditor"; +export * from "./FieldContext"; +export { getPropertyFromFormField } from "./utils"; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-side-panel/src/utils/localStorage.ts b/workspaces/ballerina/ballerina-side-panel/src/utils/localStorage.ts new file mode 100644 index 00000000000..1eb35ff3f9f --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/utils/localStorage.ts @@ -0,0 +1,66 @@ +/** + * 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. + */ + +const EXPANDED_CATEGORIES_KEY = "bi-extension-side-panel"; + +export interface ExpandedCategoriesState { + [categoryTitle: string]: boolean; +} + +export const getExpandedCategories = (): ExpandedCategoriesState => { + try { + const stored = localStorage.getItem(EXPANDED_CATEGORIES_KEY); + return stored ? JSON.parse(stored) : {}; + } catch (error) { + console.warn("Failed to load expanded categories from localStorage:", error); + return {}; + } +}; + +export const setExpandedCategories = (expandedState: ExpandedCategoriesState): void => { + try { + localStorage.setItem(EXPANDED_CATEGORIES_KEY, JSON.stringify(expandedState)); + } catch (error) { + console.warn("Failed to save expanded categories to localStorage:", error); + } +}; + +export const getDefaultExpandedState = (categories: string[]): ExpandedCategoriesState => { + const defaultExpanded: ExpandedCategoriesState = {}; + + // Set default expanded categories + const defaultExpandedCategories = [ + "Statement", + "Control", + "Connections", + "Standard Library", + "Current Integration", + "Model Providers", + "Vector Stores", + "Embedding Providers", + "Vector Knowledge Bases", + "Data Loaders", + "Chunkers", + ]; + + categories.forEach((category) => { + defaultExpanded[category] = defaultExpandedCategories.includes(category); + }); + + return defaultExpanded; +}; diff --git a/workspaces/ballerina/ballerina-visualizer/package.json b/workspaces/ballerina/ballerina-visualizer/package.json index c94e793d7d9..6faadfdf324 100644 --- a/workspaces/ballerina/ballerina-visualizer/package.json +++ b/workspaces/ballerina/ballerina-visualizer/package.json @@ -29,7 +29,7 @@ "@wso2/ballerina-low-code-diagram": "workspace:*", "@wso2/ballerina-rpc-client": "workspace:*", "@wso2/ballerina-side-panel": "workspace:*", - "@wso2/data-mapper-view": "workspace:*", + "@wso2/ballerina-data-mapper": "workspace:*", "@wso2/bi-diagram": "workspace:*", "@wso2/sequence-diagram": "workspace:*", "@wso2/component-diagram": "workspace:*", @@ -39,7 +39,6 @@ "@wso2/record-creator": "workspace:*", "@wso2/syntax-tree": "workspace:*", "@wso2/ui-toolkit": "workspace:*", - "@wso2/ballerina-inline-data-mapper": "workspace:*", "@wso2/type-editor": "workspace:*", "@wso2/wso2-platform-core": "workspace:*", "react": "18.2.0", diff --git a/workspaces/ballerina/ballerina-visualizer/src/Context.tsx b/workspaces/ballerina/ballerina-visualizer/src/Context.tsx index 7557476e0c9..2bc0f1e6d44 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/Context.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/Context.tsx @@ -76,6 +76,8 @@ interface VisualizerContext { setComponentInfo?: (componentInfo: ComponentInfo) => void; cacheTriggers: TriggerModelsResponse, setCacheTriggers: (componentInfo: TriggerModelsResponse) => void; + showOverlay: boolean; + setShowOverlay: (value: boolean) => void; } export const VisualizerContext = createContext({ @@ -89,6 +91,7 @@ export const VisualizerContext = createContext({ setComponentInfo: (componentInfo: ComponentInfo) => { }, cacheTriggers: undefined, setCacheTriggers: (triggers: TriggerModelsResponse) => { }, + setShowOverlay: (value: boolean) => { }, } as VisualizerContext); @@ -101,6 +104,7 @@ export function VisualizerContextProvider({ children }: { children: ReactNode }) const [componentInfo, setComponentInfo] = useState(); const [activeFileInfo, setActiveFileInfo] = useState(); const [cacheTriggers, setCacheTriggers] = useState({ local: [] }); + const [showOverlay, setShowOverlay] = useState(false); const contextValue: VisualizerContext = { @@ -119,7 +123,9 @@ export function VisualizerContextProvider({ children }: { children: ReactNode }) componentInfo: componentInfo, setComponentInfo: setComponentInfo, cacheTriggers: cacheTriggers, - setCacheTriggers: setCacheTriggers + setCacheTriggers: setCacheTriggers, + showOverlay: showOverlay, + setShowOverlay: setShowOverlay }; return {children}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx b/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx index 7d94e3e111a..49933195e92 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx @@ -17,32 +17,33 @@ */ import { useQuery } from '@tanstack/react-query'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; -import { IDMViewState } from '@wso2/ballerina-core'; +import { DMViewState, LinePosition } from '@wso2/ballerina-core'; -export const useInlineDataMapperModel = ( +export const useDataMapperModel = ( filePath: string, - viewState: IDMViewState + viewState: DMViewState, + position?: LinePosition ) => { const { rpcClient } = useRpcContext(); const viewId = viewState?.viewId; const codedata = viewState?.codedata; - const getIDMModel = async () => { + const getDMModel = async () => { try { const modelParams = { filePath, codedata, targetField: viewId, - position: { + position: position ?? { line: codedata.lineRange.startLine.line, offset: codedata.lineRange.startLine.offset } }; const res = await rpcClient - .getInlineDataMapperRpcClient() + .getDataMapperRpcClient() .getDataMapperModel(modelParams); - console.log('>>> [Inline Data Mapper] Model:', res); + console.log('>>> [Data Mapper] Model:', res); return res.mappingsModel; } catch (error) { console.error(error); @@ -56,8 +57,8 @@ export const useInlineDataMapperModel = ( isError, refetch } = useQuery({ - queryKey: ['getIDMModel', { filePath, codedata, viewId }], - queryFn: () => getIDMModel(), + queryKey: ['getDMModel', { codedata, viewId }], + queryFn: () => getDMModel(), networkMode: 'always' }); diff --git a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx index d3480079063..1d5b8974709 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx @@ -24,13 +24,17 @@ import { MACHINE_VIEW, PopupMachineStateValue, EVENT_TYPE, + ParentPopupData, + ProjectStructureArtifactResponse, + DIRECTORY_MAP, + CodeData, + LinePosition, } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { Global, css } from "@emotion/react"; import { debounce } from "lodash"; import styled from "@emotion/styled"; import { LoadingRing } from "./components/Loader"; -import { DataMapper } from "./views/DataMapper"; import { ERDiagram } from "./views/ERDiagram"; import { GraphQLDiagram } from "./views/GraphQLDiagram"; import { ServiceDesigner } from "./views/BI/ServiceDesigner"; @@ -44,9 +48,9 @@ import { TestFunctionForm } from "./views/BI"; import { handleRedo, handleUndo } from "./utils/utils"; -import { FunctionDefinition } from "@wso2/syntax-tree"; +import { STKindChecker } from "@wso2/syntax-tree"; import { URI, Utils } from "vscode-uri"; -import { Typography } from "@wso2/ui-toolkit"; +import { ThemeColors, Typography } from "@wso2/ui-toolkit"; import { PanelType, useVisualizerContext } from "./Context"; import { ConstructPanel } from "./views/ConstructPanel"; import { EditPanel } from "./views/EditPanel"; @@ -70,7 +74,8 @@ import { AIAgentDesigner } from "./views/BI/AIChatAgent"; import { AIChatAgentWizard } from "./views/BI/AIChatAgent/AIChatAgentWizard"; import { BallerinaUpdateView } from "./views/BI/BallerinaUpdateView"; import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"; -import { InlineDataMapper } from "./views/InlineDataMapper"; +import { DataMapper } from "./views/DataMapper"; +import { ImportIntegration } from "./views/BI/ImportIntegration"; const globalStyles = css` *, @@ -147,6 +152,18 @@ const LoadingContent = styled.div` animation: fadeIn 1s ease-in-out; `; +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: ${ThemeColors.SURFACE_CONTAINER}; + opacity: 0.4; + z-index: 2001; + pointer-events: auto; +`; + const LoadingTitle = styled.h1` color: var(--vscode-foreground); font-size: 1.5em; @@ -171,7 +188,7 @@ const LoadingText = styled.div` const MainPanel = () => { const { rpcClient } = useRpcContext(); - const { sidePanel, setSidePanel, popupMessage, setPopupMessage, activePanel } = useVisualizerContext(); + const { sidePanel, setSidePanel, popupMessage, setPopupMessage, activePanel, showOverlay, setShowOverlay } = useVisualizerContext(); const [viewComponent, setViewComponent] = useState(); const [navActive, setNavActive] = useState(true); const [showHome, setShowHome] = useState(true); @@ -322,25 +339,55 @@ const MainPanel = () => { setViewComponent(); break; case MACHINE_VIEW.TypeDiagram: - setViewComponent(); + if (value?.identifier) { + setViewComponent( + + ); + } else { + // To support rerendering when user click on view all btn from left side panel + setViewComponent( + + ); + } break; case MACHINE_VIEW.DataMapper: + let position: LinePosition = { + line: value?.position?.startLine, + offset: value?.position?.startColumn + }; + if (STKindChecker.isFunctionDefinition(value?.syntaxTree) && + STKindChecker.isExpressionFunctionBody(value?.syntaxTree.functionBody) + ) { + position = { + line: value?.syntaxTree.functionBody.expression.position.startLine, + offset: value?.syntaxTree.functionBody.expression.position.startColumn + }; + } setViewComponent( ); break; case MACHINE_VIEW.InlineDataMapper: setViewComponent( - ); break; @@ -366,7 +413,10 @@ const MainPanel = () => { ); break; case MACHINE_VIEW.GraphQLDiagram: - setViewComponent(); + const getProjectStructure = await rpcClient.getBIDiagramRpcClient().getProjectStructure(); + const entryPoint = getProjectStructure.directoryMap[DIRECTORY_MAP.SERVICE].find((service: ProjectStructureArtifactResponse) => service.name === value?.identifier); + await rpcClient.getVisualizerRpcClient().openView({ type: EVENT_TYPE.UPDATE_PROJECT_LOCATION, location: { documentUri: entryPoint?.path, position: entryPoint?.position } }); + setViewComponent(); break; case MACHINE_VIEW.BallerinaUpdateView: setNavActive(false); @@ -384,6 +434,11 @@ const MainPanel = () => { setShowHome(false); setViewComponent(); break; + case MACHINE_VIEW.BIImportIntegration: + setShowHome(false); + setViewComponent(); + break; + case MACHINE_VIEW.BIComponentView: setViewComponent(); break; @@ -431,6 +486,14 @@ const MainPanel = () => { /> ); break; + case MACHINE_VIEW.AddCustomConnector: + setViewComponent( + + ); + break; case MACHINE_VIEW.BIMainFunctionForm: setViewComponent(); break; @@ -490,10 +553,10 @@ const MainPanel = () => { setPopupMessage(false); }; - const handleOnClose = () => { + const handleOnClose = (parent?: ParentPopupData) => { rpcClient .getVisualizerRpcClient() - .openView({ type: EVENT_TYPE.CLOSE_VIEW, location: { view: null }, isPopup: true }); + .openView({ type: EVENT_TYPE.CLOSE_VIEW, location: { view: null, recentIdentifier: parent?.recentIdentifier, artifactType: parent?.artifactType }, isPopup: true }); }; return ( @@ -501,6 +564,7 @@ const MainPanel = () => { {/* {navActive && } */} + {showOverlay && setShowOverlay(false)} />} {viewComponent && {viewComponent}} {!viewComponent && ( diff --git a/workspaces/ballerina/ballerina-visualizer/src/PopupPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/PopupPanel.tsx index 3437e26fb70..152e0d86ef9 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/PopupPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/PopupPanel.tsx @@ -19,7 +19,7 @@ import styled from "@emotion/styled"; import React, { useEffect, useState } from "react"; import { URI, Utils } from "vscode-uri"; -import { MACHINE_VIEW, PopupMachineStateValue, PopupVisualizerLocation } from "@wso2/ballerina-core"; +import { MACHINE_VIEW, ParentPopupData, PopupMachineStateValue, PopupVisualizerLocation } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import AddConnectionWizard from "./views/BI/Connection/AddConnectionWizard"; import { ThemeColors, Overlay } from "@wso2/ui-toolkit"; @@ -45,7 +45,7 @@ const TopBar = styled.div` interface PopupPanelProps { formState: PopupMachineStateValue; - onClose: () => void; + onClose: (parent?: ParentPopupData) => void; } const PopupPanel = (props: PopupPanelProps) => { @@ -74,6 +74,7 @@ const PopupPanel = (props: PopupPanelProps) => { fileName={location.documentUri || location.projectUri} target={machineState.metadata?.target || undefined} onClose={onClose} + isPopupScreen={true} /> ); }); diff --git a/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx b/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx index 22df08b7a63..bac5d61914b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx @@ -136,6 +136,8 @@ const VisualizerComponent = React.memo(({ state }: { state: MachineStateValue }) switch (true) { case typeof state === 'object' && 'viewActive' in state && state.viewActive === "viewReady": return ; + case typeof state === 'object' && 'viewActive' in state && state.viewActive === "resolveMissingDependencies": + return ; default: return ; } @@ -166,3 +168,30 @@ const LanguageServerLoadingView = () => { ); }; + +const PullingDependenciesView = () => { + return ( +
+ + + + + Pulling Dependencies + + + Fetching required modules for your project.
+ Please wait, this might take some time. +
+ + Pulling + +
+
+ ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/ButtonCard/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/ButtonCard/index.tsx index 84d9b0fe191..4531d5af02f 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/components/ButtonCard/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ButtonCard/index.tsx @@ -21,7 +21,7 @@ import { ThemeColors, Tooltip } from "@wso2/ui-toolkit"; import styled from "@emotion/styled"; import { BetaSVG } from "../../views/Connectors/Marketplace/BetaSVG"; -const Card = styled.div<{ active?: boolean; appearance?: ButtonCardAppearance, disabled?: boolean }>` +const Card = styled.div<{ active?: boolean; appearance?: ButtonCardAppearance, disabled?: boolean, sx?: any }>` gap: 16px; max-width: 42rem; padding: 12px; diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionConfig.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionConfig.tsx new file mode 100644 index 00000000000..d64a26afaed --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionConfig.tsx @@ -0,0 +1,102 @@ +/** + * 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 { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { FlowNode } from "@wso2/ballerina-core"; +import { FormField, FormValues } from "@wso2/ballerina-side-panel"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { FormGeneratorNew } from "../../views/BI/Forms/FormGeneratorNew"; +import { RelativeLoader } from "../RelativeLoader"; +import { ConnectionConfigProps } from "./types"; +import { getConnectionKindConfig } from "./config"; +import { createConnectionSelectField, fetchConnectionForNode, updateNodeWithConnectionVariable } from "./utils"; +import { LoaderContainer } from "./styles"; + +export function ConnectionConfig(props: ConnectionConfigProps): JSX.Element { + const { connectionKind, selectedNode, onSave, onNavigateToSelectionList } = props; + const config = useMemo(() => getConnectionKindConfig(connectionKind), [connectionKind]); + const { rpcClient } = useRpcContext(); + + const [selectedConnection, setSelectedConnection] = useState(); + const [selectedConnectionFields, setSelectedConnectionFields] = useState([]); + const [loading, setLoading] = useState(false); + const [savingForm, setSavingForm] = useState(false); + + const projectPath = useRef(""); + + useEffect(() => { + initPanel(); + }, []); + + useEffect(() => { + if (selectedConnection) { + renderFormField(); + } + }, [selectedConnection]); + + const initPanel = async () => { + setLoading(true); + projectPath.current = await rpcClient.getVisualizerLocation().then((location) => location.projectUri); + await fetchSelectedConnection(); + setLoading(false); + }; + + const fetchSelectedConnection = async () => { + const connection = await fetchConnectionForNode(rpcClient, connectionKind, selectedNode); + setSelectedConnection(connection); + }; + + const renderFormField = () => { + const connectionSelectField = createConnectionSelectField(selectedConnection, config, onCreateNewConnection); + setSelectedConnectionFields([connectionSelectField]); + }; + + const handleOnSave = useCallback(async (data: FormValues) => { + setSavingForm(true); + updateNodeWithConnectionVariable(connectionKind, selectedNode, data["connection"]); + onSave?.(selectedNode); + }, [onSave, rpcClient]); + + const onCreateNewConnection = useCallback(() => { + onNavigateToSelectionList?.(); + }, [onNavigateToSelectionList]); + + return ( + <> + {loading && ( + + + + )} + {!loading && selectedConnectionFields?.length > 0 && ( + <> + + + ) + } + + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionCreator.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionCreator.tsx new file mode 100644 index 00000000000..44ad8e265f0 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionCreator.tsx @@ -0,0 +1,112 @@ +/** + * 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 { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { FormField, FormValues, FormImports } from "@wso2/ballerina-side-panel"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { convertConfig } from "../../utils/bi"; +import { FormGeneratorNew } from "../../views/BI/Forms/FormGeneratorNew"; +import { RelativeLoader } from "../RelativeLoader"; +import { InfoBox } from "../InfoBox"; +import { ConnectionCreatorProps } from "./types"; +import { getConnectionSpecialConfig } from "./config"; +import { updateFormFieldsWithData, updateNodeTemplateProperties, updateNodeWithConnectionVariable, updateNodeLineRange } from "./utils"; +import { LoaderContainer } from "./styles"; +import { cloneDeep } from "lodash"; + +export function ConnectionCreator(props: ConnectionCreatorProps): JSX.Element { + const { connectionKind, selectedNode, nodeFormTemplate, onSave } = props; + + const connectionSymbol = useMemo(() => nodeFormTemplate?.codedata?.symbol || '', [nodeFormTemplate?.codedata?.symbol]); + const specialConfig = useMemo(() => getConnectionSpecialConfig(connectionSymbol) || {}, [connectionSymbol]); + const shouldShowInfo = useMemo(() => specialConfig.shouldShowInfo?.(connectionSymbol) ?? false, [specialConfig, connectionSymbol]); + + const { rpcClient } = useRpcContext(); + const [connectionFields, setConnectionFields] = useState([]); + const [loading, setLoading] = useState(false); + const [savingForm, setSavingForm] = useState(false); + + const projectPath = useRef(""); + + useEffect(() => { + initPanel(); + }, [nodeFormTemplate]); + + const initPanel = async () => { + setLoading(true); + projectPath.current = await rpcClient.getVisualizerLocation().then((location) => location.projectUri); + if (nodeFormTemplate && nodeFormTemplate.properties) { + const fields = convertConfig(nodeFormTemplate.properties); + setConnectionFields(fields); + } + setLoading(false); + }; + + const handleOnSave = useCallback(async (data: FormValues, formImports?: FormImports) => { + setSavingForm(true); + const nodeTemplate = cloneDeep(nodeFormTemplate); + updateFormFieldsWithData(connectionFields, data, formImports); + updateNodeTemplateProperties(nodeTemplate, connectionFields); + try { + const response = await rpcClient + .getBIDiagramRpcClient() + .getSourceCode({ filePath: projectPath.current, flowNode: nodeTemplate }); + // Update the selected node with the new connection variable + updateNodeWithConnectionVariable(connectionKind, selectedNode, nodeTemplate?.properties?.variable?.value as string); + // Update the line range for the selected node if it was updated + updateNodeLineRange(selectedNode, response.artifacts); + onSave?.(selectedNode); + } catch (error) { + console.error(`>>> Error creating ${connectionKind}`, error); + } + }, [onSave, rpcClient, connectionKind, connectionFields]); + + return ( + <> + {loading && ( + + + + )} + {!loading && connectionFields?.length > 0 && ( + <> + , + index: 0 + } + ] : []} + /> + + )} + + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionSelectionList.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionSelectionList.tsx new file mode 100644 index 00000000000..55191516b65 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/ConnectionSelectionList.tsx @@ -0,0 +1,79 @@ +/** + * 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 { useEffect, useRef, useState } from "react"; +import { Category, CardList } from "@wso2/ballerina-side-panel"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { RelativeLoader } from "../RelativeLoader"; +import { ConnectionSearchConfig, ConnectionSelectionListProps } from "./types"; +import { LoaderContainer } from "./styles"; +import { convertConnectionCategories, getSearchConfig } from "./utils"; +import { getAiModuleOrg } from "../../views/BI/AIChatAgent/utils"; + +export function ConnectionSelectionList(props: ConnectionSelectionListProps): JSX.Element { + const { connectionKind, selectedNode, onSelect } = props; + + const { rpcClient } = useRpcContext(); + const [connectionCategories, setConnectionCategories] = useState([]); + const [loading, setLoading] = useState(false); + + const projectPath = useRef(""); + const aiModuleOrg = useRef(""); + const searchConfig = useRef(); + + useEffect(() => { + initPanel(); + }, []); + + const initPanel = async () => { + setLoading(true); + projectPath.current = await rpcClient.getVisualizerLocation().then((location) => location.projectUri); + aiModuleOrg.current = await getAiModuleOrg(rpcClient, selectedNode?.codedata?.node); + searchConfig.current = getSearchConfig(connectionKind, aiModuleOrg.current); + await fetchConnections(); + setLoading(false); + }; + + const fetchConnections = async () => { + const connectionSearchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: projectPath.current, + queryMap: { + q: searchConfig.current.query + }, + searchKind: searchConfig.current.searchKind + }); + + setConnectionCategories(convertConnectionCategories(connectionKind, connectionSearchResponse.categories)); + }; + + return ( + <> + {loading && ( + + + + )} + {!loading && connectionCategories.length > 0 && ( + + )} + + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/config.ts b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/config.ts new file mode 100644 index 00000000000..10b585220f3 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/config.ts @@ -0,0 +1,76 @@ +/** + * 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 { ConnectionKind, ConnectionKindConfig, ConnectionSpecialConfig, ConnectionSearchConfig } from "./types"; +import { GET_DEFAULT_MODEL_PROVIDER, BALLERINAX } from "../../constants"; +import { + convertChunkerCategoriesToSidePanelCategories, + convertEmbeddingProviderCategoriesToSidePanelCategories, + convertModelProviderCategoriesToSidePanelCategories, + convertVectorStoreCategoriesToSidePanelCategories +} from "../../utils/bi"; + +export const CONNECTION_TYPE_CONFIGS: Record = { + MODEL_PROVIDER: { + displayName: "Model Provider", + valueTypeConstraint: "ai:ModelProvider", + nodePropertyKey: ["model", "modelProvider"], + categoryConverter: convertModelProviderCategoriesToSidePanelCategories, + searchConfig: (aiModuleOrg?: string): ConnectionSearchConfig => ({ + query: "", + searchKind: aiModuleOrg && aiModuleOrg === BALLERINAX ? "CLASS_INIT" : "MODEL_PROVIDER" + }) + }, + VECTOR_STORE: { + displayName: "Vector Store", + valueTypeConstraint: "ai:VectorStore", + nodePropertyKey: "vectorStore", + categoryConverter: convertVectorStoreCategoriesToSidePanelCategories, + }, + EMBEDDING_PROVIDER: { + displayName: "Embedding Provider", + valueTypeConstraint: "ai:EmbeddingProvider", + nodePropertyKey: "embeddingModel", + categoryConverter: convertEmbeddingProviderCategoriesToSidePanelCategories, + }, + CHUNKER: { + displayName: "Chunker", + valueTypeConstraint: "ai:Chunker", + nodePropertyKey: "chunker", + categoryConverter: convertChunkerCategoriesToSidePanelCategories, + } +}; + +export const CONNECTION_SPECIAL_CONFIGS: Record = { + [GET_DEFAULT_MODEL_PROVIDER]: { + infoMessage: { + text: "Using the default WSO2 Model Provider will automatically add the necessary configuration values to Config.toml.", + description: "This can also be done using the VSCode command palette command:", + codeCommand: "> Ballerina: Configure default WSO2 model provider" + }, + shouldShowInfo: (symbol: string) => symbol === GET_DEFAULT_MODEL_PROVIDER + } +}; + +export const getConnectionKindConfig = (connectionType: ConnectionKind): ConnectionKindConfig => { + return CONNECTION_TYPE_CONFIGS[connectionType]; +}; + +export const getConnectionSpecialConfig = (symbol: string): ConnectionSpecialConfig | undefined => { + return CONNECTION_SPECIAL_CONFIGS[symbol]; +}; diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/index.ts similarity index 55% rename from workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/index.tsx rename to workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/index.ts index 937b371087e..d4918d19deb 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/commons/Search/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/index.ts @@ -15,12 +15,24 @@ * specific language governing permissions and limitations * under the License. */ + +export { ConnectionConfig } from "./ConnectionConfig"; +export { ConnectionSelectionList } from "./ConnectionSelectionList"; +export { ConnectionCreator } from "./ConnectionCreator"; + +export type { + ConnectionKind, + ConnectionKindConfig, + ConnectionInfo, + ConnectionSpecialConfig, + ConnectionConfigProps, + ConnectionSelectionListProps, + ConnectionCreatorProps, +} from "./types"; + export { - InputSearchHighlight, - OutputSearchHighlight -} from './SearchHighlight'; -export { - InputSearchNoResultFound, - OutputSearchNoResultFound, - SearchNoResultFoundKind -} from './SearchNoResultFound'; + CONNECTION_TYPE_CONFIGS, + CONNECTION_SPECIAL_CONFIGS, + getConnectionKindConfig, + getConnectionSpecialConfig +} from "./config"; diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/styles.ts b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/styles.ts new file mode 100644 index 00000000000..b338102cb24 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/styles.ts @@ -0,0 +1,8 @@ +import styled from "@emotion/styled"; + +export const LoaderContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; +`; diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/types.ts b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/types.ts new file mode 100644 index 00000000000..29b773a5835 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/types.ts @@ -0,0 +1,65 @@ +/** + * 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 { FlowNode, SearchKind } from "@wso2/ballerina-core"; + +export type ConnectionKind = 'MODEL_PROVIDER' | 'VECTOR_STORE' | 'EMBEDDING_PROVIDER' | 'CHUNKER'; + +export interface ConnectionKindConfig { + displayName: string; + valueTypeConstraint: string; + nodePropertyKey: string | string[]; + categoryConverter: (categories: any[]) => any[]; + searchConfig?: (aiModuleOrg?: string) => ConnectionSearchConfig; +} + +export interface ConnectionInfo { + text: string; + description?: string; + codeCommand?: string; +} + +export interface ConnectionSpecialConfig { + infoMessage?: ConnectionInfo; + shouldShowInfo?: (symbol: string) => boolean; +} + +export interface ConnectionConfigProps { + connectionKind: ConnectionKind; + selectedNode?: FlowNode; + onSave?: (selectedCallNode: FlowNode) => void; + onNavigateToSelectionList?: () => void; +} + +export interface ConnectionSearchConfig { + query: string; + searchKind: SearchKind; +} + +export interface ConnectionSelectionListProps { + connectionKind: ConnectionKind; + selectedNode?: FlowNode; + onSelect?: (id: string, metadata?: any) => void; +} + +export interface ConnectionCreatorProps { + connectionKind: ConnectionKind; + selectedNode?: FlowNode; + nodeFormTemplate?: FlowNode; + onSave?: (connectionNode: FlowNode) => void; +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/utils.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/utils.tsx new file mode 100644 index 00000000000..2be904383e0 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/ConnectionSelector/utils.tsx @@ -0,0 +1,156 @@ +/** + * 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 { FlowNode, Category, Property, ProjectStructureArtifactResponse } from "@wso2/ballerina-core"; +import { FormField, Category as PanelCategory, FormValues, FormImports } from "@wso2/ballerina-side-panel"; +import { ConnectionKindConfig, ConnectionKind, ConnectionSearchConfig } from "./types"; +import { getImportsForProperty } from "../../utils/bi"; +import { getConnectionKindConfig } from "./config"; +import { Codicon } from "@wso2/ui-toolkit"; +import { BallerinaRpcClient } from "@wso2/ballerina-rpc-client"; + +export const createConnectionSelectField = ( + selectedConnection: FlowNode, + config: ConnectionKindConfig, + handleActionBtnClick: () => void +): FormField => { + const selectLabel = `Select ${config.displayName}`; + const description = `Choose an existing ${config.displayName} or create a new one.`; + const createLabel = `Create New ${config.displayName}`; + return { + "key": "connection", + "label": selectLabel, + "type": "ACTION_EXPRESSION", + "optional": false, + "advanced": false, + "placeholder": "\"\"", + "editable": true, + "enabled": true, + "hidden": false, + "documentation": description, + "advanceProps": [], + "valueType": "EXPRESSION", + "diagnostics": [], + "valueTypeConstraint": config.valueTypeConstraint, + "metadata": { + "label": selectLabel, + "description": description + }, + "codedata": { + "kind": "REQUIRED", + "originalName": "connection" + }, + "actionCallback": handleActionBtnClick, + "actionLabel": <>{createLabel}, + "value": (selectedConnection.properties.variable?.value as string) || "" + }; +}; + +export const updateFormFieldsWithData = ( + connectionFields: FormField[], + data: FormValues, + formImports?: FormImports +): void => { + connectionFields.forEach((field) => { + if (field.type === "DROPDOWN_CHOICE") { + field.dynamicFormFields[data[field.key]].forEach((dynamicField) => { + if (data[dynamicField.key]) { + dynamicField.value = data[dynamicField.key]; + } + }); + field.value = data[field.key]; + } else if (data[field.key]) { + field.value = data[field.key]; + } + if (formImports) { + field.imports = getImportsForProperty(field.key, formImports); + } + }); +}; + +export const updateNodeTemplateProperties = ( + nodeTemplate: FlowNode, + connectionFields: FormField[] +): void => { + connectionFields.forEach((field) => { + if (field.editable) { + nodeTemplate.properties[field.key as keyof typeof nodeTemplate.properties].value = field.value; + } + }); +}; + +export const convertConnectionCategories = (connectionKind: ConnectionKind, categories: Category[]): PanelCategory[] => { + const config = getConnectionKindConfig(connectionKind); + return config.categoryConverter(categories); +}; + +const getValidPropertyKey = (node: FlowNode, nodePropertyKeys: string | string[]): string | undefined => { + const keys = Array.isArray(nodePropertyKeys) ? nodePropertyKeys : [nodePropertyKeys]; + return keys.find(key => node.properties[key as keyof typeof node.properties]?.value); +}; + +export const fetchConnectionForNode = async ( + rpcClient: BallerinaRpcClient, + connectionKind: ConnectionKind, + targetNode: FlowNode, +): Promise => { + const moduleNodes = await rpcClient.getBIDiagramRpcClient().getModuleNodes(); + const connections = moduleNodes.flowModel.connections; + const config = getConnectionKindConfig(connectionKind); + const propertyKey = getValidPropertyKey(targetNode, config.nodePropertyKey); + const targetPropertyValue = propertyKey ? targetNode.properties?.[propertyKey as keyof typeof targetNode.properties]?.value : undefined; + + const connection = connections.find((node: FlowNode) => + node.properties.variable?.value === targetPropertyValue + ); + + if (!connection) + throw new Error(`Could not find a connection for the target node.`); + + return connection; +}; + +export const updateNodeWithConnectionVariable = (connectionKind: ConnectionKind, selectedNode: FlowNode, connectionVariable: string): void => { + const config = getConnectionKindConfig(connectionKind); + const propertyKey = getValidPropertyKey(selectedNode, config.nodePropertyKey) || (Array.isArray(config.nodePropertyKey) ? config.nodePropertyKey[0] : config.nodePropertyKey); + const property = selectedNode.properties[propertyKey as keyof typeof selectedNode.properties]; + + if (property && typeof property === 'object') { + (property as Property).value = connectionVariable; + } +}; + +export const getSearchConfig = (connectionKind: ConnectionKind, aiModuleOrg?: string): ConnectionSearchConfig => { + const config = getConnectionKindConfig(connectionKind); + if (config.searchConfig) + return config.searchConfig(aiModuleOrg); + return { query: "", searchKind: connectionKind }; +}; + +export const updateNodeLineRange = (selectedNode: FlowNode, artifacts: ProjectStructureArtifactResponse[]): void => { + const selectedNodeArtifact = artifacts.find((artifact) => { + return artifact.name === selectedNode.properties.variable.value; + }); + if (selectedNodeArtifact && selectedNodeArtifact.position && !selectedNode?.codedata?.isNew) { + selectedNode.codedata.lineRange = { + fileName: selectedNodeArtifact?.path, + startLine: { line: selectedNodeArtifact.position.startLine, offset: selectedNodeArtifact.position.startColumn }, + endLine: { line: selectedNodeArtifact.position.endLine, offset: selectedNodeArtifact.position.endColumn } + }; + } +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/FormHeader/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/FormHeader/index.tsx index b311fe6d39b..c1cbbd15572 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/components/FormHeader/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/components/FormHeader/index.tsx @@ -38,14 +38,14 @@ const Description = styled(Typography)` `; interface FormHeaderProps { - title: string; + title?: string; subtitle?: string; } export function FormHeader({ title, subtitle }: FormHeaderProps) { return ( - {title} + {title && {title}} {subtitle && ( {subtitle} diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/InfoBox/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/InfoBox/index.tsx new file mode 100644 index 00000000000..be992280a35 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/InfoBox/index.tsx @@ -0,0 +1,89 @@ +/** + * 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 styled from "@emotion/styled"; + +interface InfoBoxProps { + text: string; + description?: string; + codeCommand?: string; +} + +const Container = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +`; + +const InfoBoxContainer = styled.div` + font-size: 13px; + color: var(--vscode-foreground); + padding: 12px; + background-color: var(--vscode-editor-inactiveSelectionBackground); + border-radius: 4px; + + strong { + font-weight: 600; + } + + .description { + font-size: 12px; + color: var(--vscode-descriptionForeground); + line-height: 1.4; + } + + .command-wrapper { + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 8px; + } +`; + +const CodeBlock = styled.code` + font-size: 11px; + color: var(--vscode-foreground); + font-family: var(--vscode-editor-font-family); + background-color: var(--vscode-textCodeBlock-background); + padding: 2px 4px; + border-radius: 3px; + border: 1px solid var(--vscode-widget-border); + margin-top: 6px; + display: inline-block; +`; + +export function InfoBox({ text, description, codeCommand }: InfoBoxProps): JSX.Element { + return ( + + +
+ {text} +
+ {description && codeCommand && ( +
+ {description}
+ + {codeCommand} + +
+ )} +
+
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/Modal/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/Modal/index.tsx new file mode 100644 index 00000000000..cf83e532507 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/components/Modal/index.tsx @@ -0,0 +1,153 @@ +/** + * 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 React, { cloneElement, isValidElement, ReactNode, ReactElement, useEffect } from "react"; +import { createPortal } from "react-dom"; +import styled from "@emotion/styled"; +import { Codicon, Divider, ThemeColors, Typography } from "@wso2/ui-toolkit"; +import { useVisualizerContext } from "../../Context"; + +export type DynamicModalProps = { + children: ReactNode; + onClose?: () => void; + title: string; + anchorRef: React.RefObject; + width?: number; + height?: number; + openState: boolean; + setOpenState: (state: boolean) => void; +}; + +const ModalContainer = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 30000; + display: flex; + justify-content: center; + align-items: center; +`; + +const ModalBox = styled.div<{ width?: number; height?: number }>` + width: ${({ width }: { width?: number }) => (width ? `${width}px` : 'auto')}; + height: ${({ height }: { height?: number }) => (height ? `${height}px` : 'auto')}; + position: relative; + display: flex; + flex-direction: column; + overflow-y: hidden; + padding: 16px; + border-radius: 3px; + background-color: ${ThemeColors.SURFACE_DIM}; + box-shadow: 0 3px 8px rgb(0 0 0 / 0.2); + z-index: 30001; +`; + +const InvisibleButton = styled.button` + background: none; + border: none; + padding: 0; + margin: 0; + text-align: inherit; + color: inherit; + font: inherit; + cursor: pointer; + outline: none; + box-shadow: none; + appearance: none; + display: inline-flex; + align-items: center; +`; + + +const ModalHeaderSection = styled.header` + display: flex; + align-items: center; + justify-content: space-between; + padding-inline: 16px; +`; + +type TriggerProps = React.ButtonHTMLAttributes & { children: ReactNode }; +const Trigger: React.FC = (props) => {props.children}; + +const DynamicModal: React.FC & { Trigger: typeof Trigger } = ({ + children, + onClose, + title, + anchorRef, + width, + height, + openState, + setOpenState, +}) => { + const { setShowOverlay } = useVisualizerContext(); + let trigger: ReactElement | null = null; + const content: ReactNode[] = []; + + React.Children.forEach(children, child => { + if (isValidElement(child) && child.type === DynamicModal.Trigger) { + trigger = cloneElement(child as React.ReactElement, { + onClick: () => setOpenState(true) + }); + } else { + content.push(child); + } + }); + + const handleClose = () => { + setOpenState(false); + setShowOverlay(false); + onClose && onClose(); + }; + + if (openState) { + setShowOverlay(true); + } + + useEffect(() => { + return () => { + setShowOverlay(false); + }; + }, []); + + return ( + <> + {trigger} + {openState && createPortal( + + + + + {title} + + + + +
{content}
+
+
, + document.body + )} + + ); +}; + +DynamicModal.Trigger = Trigger; + +export default DynamicModal; diff --git a/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx b/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx index 5f2ba54a7c7..d841bf95879 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx @@ -75,6 +75,7 @@ import { DocSection } from "../components/ExpressionEditor"; // @ts-ignore import ballerina from "../languages/ballerina.js"; import { FUNCTION_REGEX } from "../resources/constants"; +import { ConnectionKind, getConnectionKindConfig } from "../components/ConnectionSelector"; hljs.registerLanguage("ballerina", ballerina); export const BALLERINA_INTEGRATOR_ISSUES_URL = "https://github.com/wso2/product-ballerina-integrator/issues"; @@ -160,10 +161,12 @@ export function convertModelProviderCategoriesToSidePanelCategories(categories: category.items?.forEach((item) => { if ((item as PanelNode).metadata?.codedata) { const codedata = (item as PanelNode).metadata.codedata; - item.icon = ; + const iconType = codedata?.module == "ai" ? codedata.object : codedata?.module; + item.icon = ; } else if (((item as PanelCategory).items.at(0) as PanelNode)?.metadata?.codedata) { const codedata = ((item as PanelCategory).items.at(0) as PanelNode)?.metadata.codedata; - item.icon = ; + const iconType = codedata?.module == "ai" ? codedata.object : codedata?.module; + item.icon = ; } }); }); @@ -171,27 +174,48 @@ export function convertModelProviderCategoriesToSidePanelCategories(categories: } export function convertVectorStoreCategoriesToSidePanelCategories(categories: Category[]): PanelCategory[] { + return convertCategoriesToSidePanelCategoriesWithIcon(categories, (codedata) => ( + + )); +} + +export function convertEmbeddingProviderCategoriesToSidePanelCategories(categories: Category[]): PanelCategory[] { + return convertModelProviderCategoriesToSidePanelCategories(categories); +} + +export function convertVectorKnowledgeBaseCategoriesToSidePanelCategories(categories: Category[]): PanelCategory[] { + return convertModelProviderCategoriesToSidePanelCategories(categories); +} + +export function convertCategoriesToSidePanelCategoriesWithIcon( + categories: Category[], + iconFactory: (codedata: any) => React.ReactElement +): PanelCategory[] { const panelCategories = categories.map((category) => convertDiagramCategoryToSidePanelCategory(category)); panelCategories.forEach((category) => { category.items?.forEach((item) => { if ((item as PanelNode).metadata?.codedata) { const codedata = (item as PanelNode).metadata.codedata; - item.icon = ; + item.icon = iconFactory(codedata); } else if (((item as PanelCategory).items.at(0) as PanelNode)?.metadata?.codedata) { const codedata = ((item as PanelCategory).items.at(0) as PanelNode)?.metadata.codedata; - item.icon = ; + item.icon = iconFactory(codedata); } }); }); return panelCategories; } -export function convertEmbeddingProviderCategoriesToSidePanelCategories(categories: Category[]): PanelCategory[] { - return convertModelProviderCategoriesToSidePanelCategories(categories); +export function convertDataLoaderCategoriesToSidePanelCategories(categories: Category[]): PanelCategory[] { + return convertCategoriesToSidePanelCategoriesWithIcon(categories, (codedata) => ( + + )); } -export function convertVectorKnowledgeBaseCategoriesToSidePanelCategories(categories: Category[]): PanelCategory[] { - return convertModelProviderCategoriesToSidePanelCategories(categories); +export function convertChunkerCategoriesToSidePanelCategories(categories: Category[]): PanelCategory[] { + return convertCategoriesToSidePanelCategoriesWithIcon(categories, (codedata) => ( + + )); } export function convertNodePropertiesToFormFields( @@ -333,14 +357,28 @@ export function updateNodeProperties( return updatedNodeProperties; } -export function getContainerTitle(view: SidePanelView, activeNode: FlowNode, clientName?: string): string { +function getConnectionDisplayName(connectionKind?: ConnectionKind): string { + if (!connectionKind) return 'Connection'; + try { + const config = getConnectionKindConfig(connectionKind); + return config.displayName; + } catch { + return 'Connection'; + } +} + +export function getContainerTitle(view: SidePanelView, activeNode: FlowNode, clientName?: string, connectionKind?: ConnectionKind): string { switch (view) { case SidePanelView.NODE_LIST: return ""; // Show switch instead of title case SidePanelView.NEW_AGENT: return "AI Agent"; - case SidePanelView.AGENT_MODEL: - return "Configure LLM Model"; + case SidePanelView.CONNECTION_CONFIG: + return `Configure ${getConnectionDisplayName(connectionKind)}`; + case SidePanelView.CONNECTION_SELECT: + return `Select ${getConnectionDisplayName(connectionKind)}`; + case SidePanelView.CONNECTION_CREATE: + return `Create ${getConnectionDisplayName(connectionKind)}`; case SidePanelView.AGENT_MEMORY_MANAGER: return "Configure Memory"; case SidePanelView.AGENT_TOOL: @@ -369,13 +407,11 @@ export function getContainerTitle(view: SidePanelView, activeNode: FlowNode, cli ) { return `${clientName || activeNode.properties.connection.value} → ${activeNode.metadata.label}`; } else if (activeNode.codedata?.node === "DATA_MAPPER_CALL") { - return `${activeNode.codedata?.module ? activeNode.codedata?.module + " :" : ""} ${ - activeNode.codedata.symbol - }`; + return `${activeNode.codedata?.module ? activeNode.codedata?.module + " :" : ""} ${activeNode.codedata.symbol + }`; } - return `${activeNode.codedata?.module ? activeNode.codedata?.module + " :" : ""} ${ - activeNode.metadata.label - }`; + return `${activeNode.codedata?.module ? activeNode.codedata?.module + " :" : ""} ${activeNode.metadata.label + }`; default: return ""; } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/AIPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/AIPanel.tsx index 5bf1f5a2383..dda9ebfc2ed 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/AIPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/AIPanel.tsx @@ -78,11 +78,11 @@ const AIPanel = (props: { state: AIMachineStateValue }) => { if (subState === "determineFlow") { component = ; - } else if (["ssoFlow", "apiKeyFlow", "validatingApiKey"].includes(subState)) { + } else if (["ssoFlow", "apiKeyFlow", "validatingApiKey", "awsBedrockFlow", "validatingAwsCredentials"].includes(subState)) { component = ( ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/LoginPanel/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/LoginPanel/index.tsx index 93c958d653e..a17d9a1523c 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/LoginPanel/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/LoginPanel/index.tsx @@ -139,6 +139,10 @@ const LoginPanel: React.FC = () => { rpcClient.sendAIStateEvent(AIMachineEventType.AUTH_WITH_API_KEY); }; + const handleAwsBedrockClick = () => { + rpcClient.sendAIStateEvent(AIMachineEventType.AUTH_WITH_AWS_BEDROCK); + }; + return ( @@ -166,7 +170,8 @@ const LoginPanel: React.FC = () => { Login to BI Copilot or - Enter your Anthropic API Key + Enter your Anthropic API key + Enter your AWS Bedrock credentials ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/WaitingForLoginSection/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/WaitingForLoginSection/index.tsx index c0facb8b48d..2c031f41252 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/WaitingForLoginSection/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/WaitingForLoginSection/index.tsx @@ -180,6 +180,15 @@ const WaitingForLogin = ({ loginMethod, isValidating = false, errorMessage }: Wa const { rpcClient } = useRpcContext(); const [apiKey, setApiKey] = useState(""); const [showApiKey, setShowApiKey] = useState(false); + const [awsCredentials, setAwsCredentials] = useState({ + accessKeyId: "", + secretAccessKey: "", + region: "", + sessionToken: "" + }); + const [showAccessKey, setShowAccessKey] = useState(false); + const [showSecretKey, setShowSecretKey] = useState(false); + const [showSessionToken, setShowSessionToken] = useState(false); const cancelLogin = () => { rpcClient.sendAIStateEvent(AIMachineEventType.CANCEL_LOGIN); @@ -195,14 +204,47 @@ const WaitingForLogin = ({ loginMethod, isValidating = false, errorMessage }: Wa } }; + const connectWithAwsCredentials = () => { + if (awsCredentials.accessKeyId.trim() && awsCredentials.secretAccessKey.trim() && awsCredentials.region.trim()) { + rpcClient.sendAIStateEvent({ + type: AIMachineEventType.SUBMIT_AWS_CREDENTIALS, + payload: { + accessKeyId: awsCredentials.accessKeyId.trim(), + secretAccessKey: awsCredentials.secretAccessKey.trim(), + region: awsCredentials.region.trim(), + sessionToken: awsCredentials.sessionToken.trim() || undefined + }, + }); + } + }; + const handleApiKeyChange = (e: any) => { setApiKey(e.target.value); }; + const handleAwsCredentialChange = (field: keyof typeof awsCredentials) => (e: any) => { + setAwsCredentials(prev => ({ + ...prev, + [field]: e.target.value + })); + }; + const toggleApiKeyVisibility = () => { setShowApiKey(!showApiKey); }; + const toggleAccessKeyVisibility = () => { + setShowAccessKey(!showAccessKey); + }; + + const toggleSecretKeyVisibility = () => { + setShowSecretKey(!showSecretKey); + }; + + const toggleSessionTokenVisibility = () => { + setShowSessionToken(!showSessionToken); + }; + if (loginMethod === LoginMethod.ANTHROPIC_KEY) { return ( @@ -261,6 +303,120 @@ const WaitingForLogin = ({ loginMethod, isValidating = false, errorMessage }: Wa ); } + if (loginMethod === LoginMethod.AWS_BEDROCK) { + const isFormValid = awsCredentials.accessKeyId.trim() && + awsCredentials.secretAccessKey.trim() && + awsCredentials.region.trim(); + + return ( + + + Connect with AWS Bedrock + + Enter your AWS credentials to connect to BI Copilot via AWS Bedrock. Your credentials will be securely stored + and used for authentication. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {errorMessage && ( + + + {errorMessage} + + )} + + + + {isValidating ? "Validating..." : "Connect with AWS Bedrock"} + + + Cancel + + + + + ); + } + // Default: BI_INTEL login method return ( diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/commandTemplates.const.ts b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/commandTemplates.const.ts index 8443e62e0b7..f6ba0905cbe 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/commandTemplates.const.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/commandTemplates.const.ts @@ -139,6 +139,19 @@ export const commandTemplates = { placeholders: [], }, ], + [Command.Doc]: [ + { + id: TemplateId.GenerateUserDoc, + text: 'generate user documentation for service', + placeholders: [ + { + id: 'servicename', + text: '', + multiline: false, + } + ], + } + ] } as const; export type CommandTemplates = typeof commandTemplates; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/placeholderTags.const.ts b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/placeholderTags.const.ts index 33db2eae01e..8b7dad97cfa 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/placeholderTags.const.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/placeholderTags.const.ts @@ -54,7 +54,7 @@ export const placeholderTags: PlaceholderTagMap = { functionName: [], }, 'mappings-for-function': { - functionName: [] + functionName: [], }, 'inline-mappings': {} }, @@ -72,4 +72,9 @@ export const placeholderTags: PlaceholderTagMap = { [Command.OpenAPI]: { 'wildcard': {}, }, + [Command.Doc]: { + 'generate-user-doc': { + servicename: [], + } + } }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/useFooterLogic.ts b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/useFooterLogic.ts index b32d1e2a99f..8c9487bf746 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/useFooterLogic.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/useFooterLogic.ts @@ -95,6 +95,19 @@ export const useFooterLogic = ({ kind: "placeholder-specific", })) ); + + // === Command.Doc === + injectTags( + Command.Doc, + "generate-user-doc", + "servicename", + serviceNames.map((serviceName) => ({ + display: `@${serviceName}`, + value: serviceName, + injected: true, + kind: "placeholder-specific", + })) + ); }; return { diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx index 149762ccb22..a3448ee92d8 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx @@ -23,7 +23,6 @@ import { SourceFile, MappingParameters, DataMappingRecord, - PostProcessResponse, TestGenerationTarget, LLMDiagnostics, ImportStatement, @@ -41,6 +40,13 @@ import { OperationType, GENERATE_TEST_AGAINST_THE_REQUIREMENT, GENERATE_CODE_AGAINST_THE_REQUIREMENT, + ComponentInfo, + ImportInfo, + MetadataWithAttachments, + ExtendedDataMapperMetadata, + DocGenerationRequest, + DocGenerationType, + FileChanges, } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; @@ -50,29 +56,21 @@ import { AIChatInputRef } from "../AIChatInput"; import ProgressTextSegment from "../ProgressTextSegment"; import RoleContainer from "../RoleContainter"; import { Attachment, AttachmentStatus } from "@wso2/ballerina-core"; -import { findRegexMatches, formatWithProperIndentation } from "../../../../utils/utils"; +import { formatWithProperIndentation } from "../../../../utils/utils"; import { AIChatView, Header, HeaderButtons, ChatMessage, Badge } from "../../styles"; import ReferenceDropdown from "../ReferenceDropdown"; import AccordionItem from "../TestScenarioSegment"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; -import { - CopilotContentBlockContent, - CopilotErrorContent, - CopilotEvent, - hasCodeBlocks, - parseCopilotSSEEvent, -} from "../../utils/sseUtils"; import MarkdownRenderer from "../MarkdownRenderer"; import { CodeSection } from "../CodeSection"; import ErrorBox from "../ErrorBox"; -import { Input, parseBadgeString, parseInput, stringifyInputArrayWithBadges } from "../AIChatInput/utils/inputUtils"; +import { Input, parseInput, stringifyInputArrayWithBadges } from "../AIChatInput/utils/inputUtils"; import { commandTemplates, NATURAL_PROGRAMMING_TEMPLATES } from "../../commandTemplates/data/commandTemplates.const"; import { placeholderTags } from "../../commandTemplates/data/placeholderTags.const"; import { getTemplateById, getTemplateTextById, - injectTags, removeTemplate, upsertTemplate, } from "../../commandTemplates/utils/utils"; @@ -90,17 +88,6 @@ import { getOnboardingOpens, incrementOnboardingOpens } from "./utils/utils"; import FeedbackBar from "./../FeedbackBar"; import { useFeedback } from "./utils/useFeedback"; -interface CodeBlock { - filePath: string; - content: string; -} - -interface ApiResponse { - event: string; - error: string | null; - questions: string[]; -} - interface ChatIndexes { integratedChatIndex: number; previouslyIntegratedChatIndex: number; @@ -155,6 +142,7 @@ const AIChat: React.FC = () => { const [testGenIntermediaryState, setTestGenIntermediaryState] = useState( null ); + const [isAddingToWorkspace, setIsAddingToWorkspace] = useState(false); const [showSettings, setShowSettings] = useState(false); @@ -203,9 +191,9 @@ const AIChat: React.FC = () => { /* REFACTORED CODE END [2] */ let codeSegmentRendered = false; - let tempStorage: { [filePath: string]: string } = {}; - const initialFiles = new Set(); - const emptyFiles = new Set(); + const [tempStorage, setTempStorage] = useState<{ [filePath: string]: string }>({}); + const [initialFiles, setInitialFiles] = useState>(new Set()); + const [emptyFiles, setEmptyFiles] = useState>(new Set()); async function fetchBackendUrl() { try { @@ -431,7 +419,7 @@ const AIChat: React.FC = () => { } }, [messages]); - async function handleSendQuery(content: { input: Input[]; attachments: Attachment[] }) { + async function handleSendQuery(content: { input: Input[]; attachments: Attachment[], metadata?: Record }) { // Clear previous generation refs currentDiagnosticsRef.current = []; functionsRef.current = []; @@ -624,6 +612,7 @@ const AIChat: React.FC = () => { outputRecord: parsedInput.placeholderValues.outputRecord, functionName: parsedInput.placeholderValues.functionName, }, + metadata as ExtendedDataMapperMetadata, attachments ); break; @@ -635,11 +624,16 @@ const AIChat: React.FC = () => { outputRecord: "", functionName: parsedInput.placeholderValues.functionName, }, + metadata as ExtendedDataMapperMetadata, attachments ); break; case "inline-mappings": - await processInlineMappingParameters(inputText, metadata, attachments); + await processInlineMappingParameters( + inputText, + metadata as ExtendedDataMapperMetadata, + attachments + ); break; } break; @@ -680,6 +674,16 @@ const AIChat: React.FC = () => { } break; } + case Command.Doc: { + switch (parsedInput.templateId) { + case "generate-user-doc": + await processUserDocGeneration( + parsedInput.placeholderValues.servicename + ); + break; + } + break; + } } } } @@ -799,19 +803,23 @@ const AIChat: React.FC = () => { command: string ) => { console.log("Add to integration called. Command: ", command); + setIsAddingToWorkspace(true); + + try { + const fileChanges: FileChanges[] = []; for (let { segmentText, filePath } of codeSegments) { let originalContent = ""; if (!tempStorage[filePath]) { try { originalContent = await rpcClient.getAiPanelRpcClient().getFromFile({ filePath: filePath }); - tempStorage[filePath] = originalContent; + setTempStorage(prev => ({ ...prev, [filePath]: originalContent })); if (originalContent === "") { - emptyFiles.add(filePath); + setEmptyFiles(prev => new Set([...prev, filePath])); } else { - initialFiles.add(filePath); + setInitialFiles(prev => new Set([...prev, filePath])); } } catch (error) { - tempStorage[filePath] = ""; + setTempStorage(prev => ({ ...prev, [filePath]: "" })); } } @@ -906,7 +914,12 @@ const AIChat: React.FC = () => { segmentText = updatedContent.trim(); } else if (command === "ai_map_inline") { - rpcClient.getAiPanelRpcClient().addInlineCodeSegmentToWorkspace({ segmentText, filePath }); + rpcClient.getAiPanelRpcClient().addInlineCodeSegmentToWorkspace( + { + segmentText, + filePath + } + ); continue; } else if (command === "test") { segmentText = `${originalContent}\n\n${segmentText}`; @@ -919,14 +932,17 @@ const AIChat: React.FC = () => { isTestCode = true; } - await rpcClient - .getAiPanelRpcClient() - .addToProject({ filePath: filePath, content: segmentText, isTestCode: isTestCode }); + fileChanges.push({ filePath, content: segmentText }); + } + + if (fileChanges.length > 0) { + await rpcClient.getAiPanelRpcClient().addFilesToProject({ fileChanges }); } const developerMdContent = await rpcClient.getAiPanelRpcClient().readDeveloperMdFile(chatLocation); const updatedChatHistory = generateChatHistoryForSummarize(chatArray); setIsCodeAdded(true); + setIsAddingToWorkspace(false); if (await rpcClient.getAiPanelRpcClient().isNaturalProgrammingDirectoryExists(chatLocation)) { fetchWithAuth({ @@ -957,6 +973,11 @@ const AIChat: React.FC = () => { rpcClient.getAiPanelRpcClient().handleChatSummaryError(UPDATE_CHAT_SUMMARY_FAILED); }); } + } catch (error) { + console.error("Error in handleAddAllCodeSegmentsToWorkspace:", error); + setIsAddingToWorkspace(false); + throw error; + } }; async function streamToString(stream: ReadableStream): Promise { @@ -981,7 +1002,10 @@ const AIChat: React.FC = () => { command: string ) => { console.log("Revert gration called. Command: ", command); - + setIsAddingToWorkspace(true); + + try { + const fileChanges: FileChanges[] = []; for (const { filePath } of codeSegments) { let originalContent = tempStorage[filePath]; if (originalContent === "" && !initialFiles.has(filePath) && !emptyFiles.has(filePath)) { @@ -997,11 +1021,12 @@ const AIChat: React.FC = () => { isTestCode = true; } const revertContent = emptyFiles.has(filePath) ? "" : originalContent; - await rpcClient - .getAiPanelRpcClient() - .addToProject({ filePath: filePath, content: revertContent, isTestCode: isTestCode }); + fileChanges.push({ filePath, content: revertContent }); } } + if (fileChanges.length > 0) { + await rpcClient.getAiPanelRpcClient().addFilesToProject({ fileChanges }); + } rpcClient.getAiPanelRpcClient().updateDevelopmentDocument({ content: previousDevelopmentDocumentContent, filepath: chatLocation, @@ -1011,8 +1036,16 @@ const AIChat: React.FC = () => { `chatArray-AIGenerationChat-${projectUuid}-developer-index`, JSON.stringify({ integratedChatIndex, previouslyIntegratedChatIndex }) ); - tempStorage = {}; + setTempStorage({}); + setInitialFiles(new Set()); + setEmptyFiles(new Set()); setIsCodeAdded(false); + setIsAddingToWorkspace(false); + } catch (error) { + console.error("Error in handleRevertChanges:", error); + setIsAddingToWorkspace(false); + throw error; + } }; async function processTestGeneration( @@ -1046,12 +1079,33 @@ const AIChat: React.FC = () => { } } + async function processUserDocGeneration(serviceName: string) { + try { + const requestBody: DocGenerationRequest = { + type: DocGenerationType.User, + serviceName: serviceName + }; + + await rpcClient.getAiPanelRpcClient().getGeneratedDocumentation(requestBody); + } catch (error: any) { + setIsLoading(false); + const errorName = error instanceof Error ? error.name : "Unknown error"; + const errorMessage = "message" in error ? error.message : "Unknown error"; + + if (errorName === "AbortError") { + throw new Error("Failed: The user cancelled the request."); + } else { + throw new Error(errorMessage); + } + } + } + // Process records from another package function processRecordReference( recordName: string, recordMap: Map, - allImports: Array<{ moduleName: string; alias?: string }>, - importsMap: Map + allImports: Array, + importsMap: Map ): DataMappingRecord | Error { const isArray = recordName.endsWith("[]"); const cleanedRecordName = recordName.replace(/\[\]$/, ""); @@ -1170,7 +1224,7 @@ const AIChat: React.FC = () => { function processOutput( outputParam: string, recordMap: Map, - allImports: { moduleName: string; alias?: string }[], + allImports: ImportInfo[], importsMap: Map ) { const parts = outputParam.split("|"); @@ -1191,6 +1245,7 @@ const AIChat: React.FC = () => { async function processMappingParameters( message: string, parameters: MappingParameters, + metadata?: ExtendedDataMapperMetadata, attachments?: Attachment[] ) { let assistant_response = ""; @@ -1203,6 +1258,7 @@ const AIChat: React.FC = () => { let outputParam; let inputNames: string[] = []; let result; + let finalContent: string = ""; setIsLoading(true); const functionName = parameters.functionName; @@ -1220,7 +1276,7 @@ const AIChat: React.FC = () => { } }); - const existingFunctions: { name: string; filePath: string; startLine: number; endLine: number }[] = []; + const existingFunctions: ComponentInfo[] = []; for (const pkg of projectComponents.components.packages || []) { for (const mod of pkg.modules || []) { @@ -1243,7 +1299,9 @@ const AIChat: React.FC = () => { name: func.name, filePath: filepath + func.filePath, startLine: func.startLine, + startColumn: func.startColumn, endLine: func.endLine, + endColumn: func.endColumn }); }); } @@ -1280,32 +1338,54 @@ const AIChat: React.FC = () => { output = processOutput(outputParam, recordMap, allImports, importsMap); const requestPayload: any = { - backendUri: "", - token: "", inputRecordTypes: inputs, outputRecordType: output, functionName, imports: Array.from(importsMap.values()), inputNames: inputNames, + model: metadata }; if (attachments && attachments.length > 0) { requestPayload.attachment = attachments; } - const response = await rpcClient.getAiPanelRpcClient().getMappingsFromRecord(requestPayload); - setIsLoading(false); - assistant_response = `Mappings consist of the following:\n`; - if (inputParams.length === 1) { - assistant_response += `- **Input Record**: ${inputParams[0]}\n`; - } else { - assistant_response += `- **Input Records**: ${inputParams.join(", ")}\n`; - } - assistant_response += `- **Output Record**: ${outputParam}\n`; - assistant_response += `- **Function Name**: ${functionName}\n`; + let allMappingsRequest; + const tempFileMetadata = await rpcClient.getAiPanelRpcClient().createTempFileAndGenerateMetadata( + { + inputs, + output, + functionName, + inputNames, + imports: Array.from(importsMap.values()) + } + ); + allMappingsRequest = await rpcClient.getAiPanelRpcClient().generateMappings( + { + metadata: tempFileMetadata, + attachments, + useTemporaryFile: true + } + ); - if (result.functionNameMatch) { - assistant_response += `\n**Note**: When you click **Add to Integration**, it will override your existing mappings.\n`; - } + const response = await rpcClient.getDataMapperRpcClient().getAllDataMapperSource(allMappingsRequest); + finalContent = response.textEdits[allMappingsRequest.filePath]?.[0]?.newText; + + await rpcClient.getAiPanelRpcClient().addCodeSegmentToWorkspace( + { + segmentText: finalContent, + filePath: tempFileMetadata.codeData.lineRange.fileName, + metadata: tempFileMetadata, + textEdit: response + } + ); + await new Promise(resolve => setTimeout(resolve, 100)); + finalContent = await rpcClient.getAiPanelRpcClient().getContentFromFile( + { + filePath: tempFileMetadata.codeData.lineRange.fileName + } + ); + + setIsLoading(false); let filePath; if (result.functionNameMatch) { @@ -1315,7 +1395,6 @@ const AIChat: React.FC = () => { } else { filePath = "data_mappings.bal"; } - let finalContent = response.mappingCode; const needsImports = Array.from(importsMap.values()).length > 0; if (needsImports) { @@ -1332,7 +1411,20 @@ const AIChat: React.FC = () => { }) .join("\n"); - finalContent = `${newImports}\n${response.mappingCode}`; + finalContent = `${newImports}\n${finalContent}`; + } + + assistant_response = `Mappings consist of the following:\n`; + if (inputParams.length === 1) { + assistant_response += `- **Input Record**: ${inputParams[0]}\n`; + } else { + assistant_response += `- **Input Records**: ${inputParams.join(", ")}\n`; + } + assistant_response += `- **Output Record**: ${outputParam}\n`; + assistant_response += `- **Function Name**: ${functionName}\n`; + + if (result.functionNameMatch) { + assistant_response += `\n**Note**: When you click **Add to Integration**, it will override your existing mappings.\n`; } assistant_response += `\n\`\`\`ballerina\n${finalContent}\n\`\`\`\n`; @@ -1347,7 +1439,7 @@ const AIChat: React.FC = () => { async function processInlineMappingParameters( message: string, - metadata?: Record, + metadata?: ExtendedDataMapperMetadata, attachments?: Attachment[] ) { let assistant_response = ""; @@ -1364,15 +1456,16 @@ const AIChat: React.FC = () => { setIsLoading(true); try { - const requestPayload: any = { + const requestPayload: MetadataWithAttachments = { metadata, + useTemporaryFile: false }; if (attachments && attachments.length > 0) { - requestPayload.attachment = attachments; + requestPayload.attachments = attachments; } - const allMappingsRequest = await rpcClient.getAiPanelRpcClient().getMappingsFromModel(requestPayload); + const allMappingsRequest = await rpcClient.getAiPanelRpcClient().generateMappings(requestPayload); const sourceResponse = await rpcClient - .getInlineDataMapperRpcClient() + .getDataMapperRpcClient() .getAllDataMapperSource(allMappingsRequest); setIsLoading(false); @@ -1816,6 +1909,7 @@ const AIChat: React.FC = () => { isPromptExecutedInCurrentWindow } isErrorChunkReceived={isErrorChunkReceivedRef.current} + isAddingToWorkspace={isAddingToWorkspace} /> ); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx index b47549b65d6..3df2392f818 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx @@ -19,7 +19,7 @@ import { useState, useRef, KeyboardEvent, useEffect, useLayoutEffect, useImperativeHandle, forwardRef } from "react"; import styled from "@emotion/styled"; import { Codicon } from "@wso2/ui-toolkit"; -import { AIPanelPrompt, Attachment, AttachmentStatus, Command, ExpandedDMModel, TemplateId } from "@wso2/ballerina-core"; +import { AIPanelPrompt, Attachment, AttachmentStatus, Command, ExtendedDataMapperMetadata, TemplateId } from "@wso2/ballerina-core"; import AttachmentBox, { AttachmentsContainer } from "../AttachmentBox"; import { StyledInputComponent, StyledInputRef } from "./StyledInput"; import { AttachmentOptions, useAttachments } from "./hooks/useAttachments"; @@ -473,9 +473,9 @@ const AIChatInput = forwardRef( // Extract and store metadata safely if (content?.type === 'command-template') { - setCurrentMetadata(content.metadata); + setCurrentMetadata(content.metadata as ExtendedDataMapperMetadata); } else { - setCurrentMetadata({}); + setCurrentMetadata(null); } }; @@ -485,7 +485,7 @@ const AIChatInput = forwardRef( const cleanChatInput = () => { setInputValue({ text: "" }); removeAllAttachments(); - setCurrentMetadata({}); + setCurrentMetadata(null); }; /** diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/CodeSection.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/CodeSection.tsx index 649fbb03a20..234478751a7 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/CodeSection.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/CodeSection.tsx @@ -46,6 +46,7 @@ interface CodeSectionProps { onRetryRepair: () => void; isPromptExecutedInCurrentWindow: boolean; isErrorChunkReceived: boolean; + isAddingToWorkspace: boolean; } const EntryContainer = styled.div<{ hasErrors: boolean; isOpen: boolean; isHovered: boolean }>( @@ -81,7 +82,8 @@ export const CodeSection: React.FC = ({ diagnostics = [], onRetryRepair = () => {}, isPromptExecutedInCurrentWindow, - isErrorChunkReceived + isErrorChunkReceived, + isAddingToWorkspace }) => { const [isOpen, setIsOpen] = useState(false); const [isCodeAdded, setIsCodeAdded] = useState(false); @@ -155,7 +157,7 @@ export const CodeSection: React.FC = ({ ? "Code was generated for different session, please regenerate again" : "" } - disabled={!buttonsActive || isSyntaxError || !isPromptExecutedInCurrentWindow || isErrorChunkReceived} + disabled={!buttonsActive || isSyntaxError || !isPromptExecutedInCurrentWindow || isErrorChunkReceived || isAddingToWorkspace} >   Add to Integration @@ -172,7 +174,7 @@ export const CodeSection: React.FC = ({ ? "Code was generated for different session, please regenerate again" : "" } - disabled={!buttonsActive || !isPromptExecutedInCurrentWindow} + disabled={!buttonsActive || !isPromptExecutedInCurrentWindow || isAddingToWorkspace} >   Revert to Checkpoint diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AIChatAgentWizard.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AIChatAgentWizard.tsx index 4952ce5a22e..a4f3a1b9a45 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AIChatAgentWizard.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AIChatAgentWizard.tsx @@ -16,23 +16,19 @@ * under the License. */ -import { useEffect, useRef, useState } from 'react'; -import { EVENT_TYPE, FlowNode, LinePosition, ListenerModel } from '@wso2/ballerina-core'; +import { useRef, useState } from 'react'; +import { AvailableNode, EVENT_TYPE, FlowNode, LinePosition, ListenerModel } from '@wso2/ballerina-core'; import { View, ViewContent, TextField, Button, Typography } from '@wso2/ui-toolkit'; import styled from '@emotion/styled'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; import { URI, Utils } from "vscode-uri"; -import { LoadingContainer } from '../../styles'; import { TitleBar } from '../../../components/TitleBar'; import { TopNavigationBar } from '../../../components/TopNavigationBar'; import { RelativeLoader } from '../../../components/RelativeLoader'; import { FormHeader } from '../../../components/FormHeader'; import { getAiModuleOrg, getNodeTemplate } from './utils'; -import { cloneDeep } from 'lodash'; import { AI, BALLERINA, GET_DEFAULT_MODEL_PROVIDER } from '../../../constants'; -const FORM_WIDTH = 600; - const FormContainer = styled.div` display: flex; flex-direction: column; @@ -81,11 +77,8 @@ export function AIChatAgentWizard(props: AIChatAgentWizardProps) { { label: "Completing", description: "Finalizing the agent setup" } ]; - const agentFilePath = useRef(""); + const projectPath = useRef(""); const aiModuleOrg = useRef(""); - const agentCallEndOfFile = useRef(null); - const agentEndOfFile = useRef(null); - const mainFilePath = useRef(""); const validateName = (name: string): boolean => { if (!name) { @@ -113,70 +106,55 @@ export function AIChatAgentWizard(props: AIChatAgentWizardProps) { // Initialize wizard data when user clicks create setCurrentStep(0); - // get agent file path - const filePath = await rpcClient.getVisualizerLocation(); - agentFilePath.current = Utils.joinPath(URI.file(filePath.projectUri), "agents.bal").fsPath; - // get agent org aiModuleOrg.current = await getAiModuleOrg(rpcClient); - // fetch agent node - get the agent node - const allAgents = await rpcClient.getAIAgentRpcClient().getAllAgents({ - filePath: agentFilePath.current, - orgName: aiModuleOrg.current + // get project path + const filePath = await rpcClient.getVisualizerLocation(); + projectPath.current = filePath.projectUri; + + // Search for agent node in the current file + const agentSearchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: projectPath.current, + queryMap: { orgName: aiModuleOrg.current }, + searchKind: "AGENT" }); - console.log(">>> allAgents", allAgents); - if (!allAgents.agents.length) { - console.log(">>> no agents found"); - throw new Error("No agents found"); + // Validate search response structure + if (!agentSearchResponse?.categories?.[0]?.items?.[0]) { + throw new Error('No agent node found in search response'); } - const agentCodeData = allAgents.agents.at(0); - // get agent node template - const agentNodeTemplate = await getNodeTemplate(rpcClient, agentCodeData, agentFilePath.current); - const agentNode = cloneDeep(agentNodeTemplate); - - // get all llm models - const allModels = await rpcClient - .getAIAgentRpcClient() - .getAllModels({ - agent: agentCodeData.object, - filePath: agentFilePath.current, - orgName: aiModuleOrg.current - }); - console.log(">>> allModels", allModels); + const agentNode = agentSearchResponse.categories[0].items[0] as AvailableNode; + console.log(">>> agentNode", agentNode); - // get openai model - const defaultModel = allModels.models.find((model) => - model.object === "OpenAiProvider" || (model.org === BALLERINA && model.module === AI) + // Generate template from agent node + const agentNodeTemplate = await getNodeTemplate(rpcClient, agentNode.codedata, projectPath.current); + + // Search for model providers + const modelProviderSearchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: projectPath.current, + queryMap: { q: aiModuleOrg.current === BALLERINA ? "ai" : "OpenAiProvider" }, + searchKind: aiModuleOrg.current === BALLERINA ? "MODEL_PROVIDER" : "CLASS_INIT" + }); + + const modelNodes = modelProviderSearchResponse.categories[0].items as AvailableNode[]; + console.log(">>> modelNodes", modelNodes); + + // get default model + const defaultModelNode = modelNodes.find((model) => + model.codedata.object === "OpenAiProvider" || (model.codedata.org === BALLERINA && model.codedata.module === AI) ); - if (!defaultModel) { + if (!defaultModelNode) { console.log(">>> no default model found"); throw new Error("No default model found"); } // get model node template - const modelNodeTemplate = await getNodeTemplate(rpcClient, defaultModel, agentFilePath.current); - const defaultModelNode = cloneDeep(modelNodeTemplate); - - // get end of files - // main.bal last line - mainFilePath.current = Utils.joinPath(URI.file(filePath.projectUri), "main.bal").fsPath; - const endOfFile = await rpcClient.getBIDiagramRpcClient().getEndOfFile({ filePath: mainFilePath.current }); - console.log(">>> endOfFile", endOfFile); - agentCallEndOfFile.current = endOfFile; - - // agent.bal file last line - const endOfAgentFile = await rpcClient - .getBIDiagramRpcClient() - .getEndOfFile({ filePath: agentFilePath.current }); - console.log(">>> endOfAgentFile", endOfAgentFile); - agentEndOfFile.current = endOfAgentFile; - - const listenerName = agentName + "Listener"; + const modelNodeTemplate = await getNodeTemplate(rpcClient, defaultModelNode.codedata, projectPath.current); // Get listener model + const listenerName = agentName + "Listener"; const listenerModelResponse = await rpcClient.getServiceDesignerRpcClient().getListenerModel({ moduleName: type, orgName: aiModuleOrg.current @@ -220,39 +198,30 @@ export function AIChatAgentWizard(props: AIChatAgentWizardProps) { // save model node const modelVarName = `_${agentName}Model`; - defaultModelNode.properties.variable.value = modelVarName; + modelNodeTemplate.properties.variable.value = modelVarName; const modelResponse = await rpcClient .getBIDiagramRpcClient() - .getSourceCode({ filePath: agentFilePath.current, flowNode: defaultModelNode }); + .getSourceCode({ filePath: projectPath.current, flowNode: modelNodeTemplate }); console.log(">>> modelResponse getSourceCode", { modelResponse }); - // wait 2 seconds (wait until LS is updated) - console.log(">>> wait 2 seconds"); - await new Promise((resolve) => setTimeout(resolve, 2000)); - // save the agent node - const updatedAgentNode = cloneDeep(agentNode); const systemPromptValue = `{role: "", instructions: string \`\`}`; const agentVarName = `_${agentName}Agent`; - updatedAgentNode.properties.systemPrompt.value = systemPromptValue; - updatedAgentNode.properties.model.value = modelVarName; - updatedAgentNode.properties.tools.value = []; - updatedAgentNode.properties.variable.value = agentVarName; + agentNodeTemplate.properties.systemPrompt.value = systemPromptValue; + agentNodeTemplate.properties.model.value = modelVarName; + agentNodeTemplate.properties.tools.value = []; + agentNodeTemplate.properties.variable.value = agentVarName; const agentResponse = await rpcClient .getBIDiagramRpcClient() - .getSourceCode({ filePath: agentFilePath.current, flowNode: updatedAgentNode }); + .getSourceCode({ filePath: projectPath.current, flowNode: agentNodeTemplate }); console.log(">>> agentResponse getSourceCode", { agentResponse }); - // If the selected model is the default WSO2 model provider, configure it + // If the selected model is the default WSO2 model provider, configure it if (defaultModelNode?.codedata?.symbol === GET_DEFAULT_MODEL_PROVIDER) { await rpcClient.getAIAgentRpcClient().configureDefaultModelProvider(); } - // wait 2 seconds (wait until LS is updated) - console.log(">>> wait 2 seconds"); - await new Promise((resolve) => setTimeout(resolve, 2000)); - if (newArtifact) { setCurrentStep(5); rpcClient.getVisualizerRpcClient().openView({ diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AddMcpServer.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AddMcpServer.tsx index e25700dc2a2..c7f4c9254e5 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AddMcpServer.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AddMcpServer.tsx @@ -9,7 +9,7 @@ import { useEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; -import { FlowNode } from "@wso2/ballerina-core"; +import { AvailableNode, FlowNode } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { Button, ThemeColors } from "@wso2/ui-toolkit"; import { RelativeLoader } from "../../../components/RelativeLoader"; @@ -139,6 +139,7 @@ export function AddMcpServer(props: AddToolProps): JSX.Element { const [loading, setLoading] = useState(false); const [savingForm, setSavingForm] = useState(false); + const projectPath = useRef(""); const agentFilePath = useRef(""); const hasUpdatedToolsField = useRef(false); const formRef = useRef(null); @@ -284,6 +285,9 @@ export function AddMcpServer(props: AddToolProps): JSX.Element { const initPanel = async () => { hasUpdatedToolsField.current = false; // Reset on panel init setLoading(true); + // get project path + const filePath = await rpcClient.getVisualizerLocation(); + projectPath.current = filePath.projectUri; // get agent file path agentFilePath.current = await getAgentFilePath(rpcClient); // fetch tools and agent node @@ -305,9 +309,16 @@ export function AddMcpServer(props: AddToolProps): JSX.Element { }; const fetchExistingTools = async () => { - const existingTools = await rpcClient.getAIAgentRpcClient().getTools({ filePath: agentFilePath.current }); - console.log(">>> existing tools", existingTools); - setExistingTools(existingTools.tools); + const agentToolsSearchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: projectPath.current, + position: { startLine: { line: 0, offset: 0 }, endLine: { line: 0, offset: 0 } }, + searchKind: "AGENT_TOOL" + }); + const existingToolsList = agentToolsSearchResponse.categories?.[0]?.items + ? (agentToolsSearchResponse.categories[0].items as AvailableNode[]).map((item) => item.codedata.symbol) + : []; + console.log(">>> existing tools", existingToolsList); + setExistingTools(existingToolsList); }; const fetchMcpTools = async (url: string) => { diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/MemoryManagerConfig.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/MemoryManagerConfig.tsx index 572b191b786..eb5866a8192 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/MemoryManagerConfig.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/MemoryManagerConfig.tsx @@ -101,16 +101,21 @@ export function MemoryManagerConfig(props: MemoryManagerConfigProps): JSX.Elemen return; } try { - const memoryManagers = await rpcClient - .getAIAgentRpcClient() - .getAllMemoryManagers({ filePath: agentFilePath.current, orgName: aiModuleOrg.current }); - console.log(">>> all memory managers", memoryManagers); - if (memoryManagers.memoryManagers) { - setMemoryManagersCodeData(memoryManagers.memoryManagers); - return memoryManagers.memoryManagers; - } else { - console.error("Memory managers not found", memoryManagers); + const memoryManagerSearchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: agentFilePath.current, + queryMap: { orgName: aiModuleOrg.current }, + searchKind: "MEMORY_MANAGER" + }); + + if (!memoryManagerSearchResponse?.categories?.[0]?.items) { + console.error("No memory managers found in search response"); + return []; } + + const memoryManagers = memoryManagerSearchResponse.categories[0].items.map((item: any) => item.codedata); + console.log(">>> all memory managers", memoryManagers); + setMemoryManagersCodeData(memoryManagers); + return memoryManagers; } catch (error) { console.error("Error fetching memory managers", error); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/ModelConfig.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/ModelConfig.tsx deleted file mode 100644 index 63a2a3ece76..00000000000 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/ModelConfig.tsx +++ /dev/null @@ -1,364 +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 React, { useEffect, useRef, useState } from "react"; -import styled from "@emotion/styled"; -import { CodeData, FlowNode, NodeMetadata, NodeProperties, UpdatedArtifactsResponse } from "@wso2/ballerina-core"; -import { FormField, FormValues } from "@wso2/ballerina-side-panel"; -import { useRpcContext } from "@wso2/ballerina-rpc-client"; -import { convertConfig } from "../../../utils/bi"; -import ConfigForm from "./ConfigForm"; -import { Dropdown } from "@wso2/ui-toolkit"; -import { cloneDeep } from "lodash"; -import { RelativeLoader } from "../../../components/RelativeLoader"; -import { getAgentFilePath, getAiModuleOrg, getNodeTemplate, getNPFilePath } from "./utils"; -import { BALLERINA, BALLERINAX, GET_DEFAULT_MODEL_PROVIDER, WSO2_MODEL_PROVIDER, PROVIDER_NAME_MAP } from "../../../constants"; - -const Container = styled.div` - padding: 16px; - height: 100%; -`; - -const LoaderContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - height: 100%; -`; - -const Row = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - margin-bottom: 16px; -`; - -const InfoBox = styled.div` - font-size: 13px; - color: var(--vscode-foreground); - padding: 12px; - margin-bottom: 16px; - background-color: var(--vscode-editor-inactiveSelectionBackground); - border-radius: 4px; - - strong { - font-weight: 600; - } - - .description { - font-size: 12px; - color: var(--vscode-descriptionForeground); - line-height: 1.4; - } - - .command-wrapper { - display: flex; - align-items: flex-start; - gap: 8px; - margin-bottom: 8px; - } -`; - -const CodeBlock = styled.code` - font-size: 11px; - color: var(--vscode-foreground); - font-family: var(--vscode-editor-font-family); - background-color: var(--vscode-textCodeBlock-background); - padding: 2px 4px; - border-radius: 3px; - border: 1px solid var(--vscode-widget-border); - margin-top: 6px; - display: inline-block; -`; - -interface ModelConfigProps { - agentCallNode: FlowNode; - onSave?: (response?: UpdatedArtifactsResponse) => void; -} - -export function ModelConfig(props: ModelConfigProps): JSX.Element { - const { agentCallNode, onSave } = props; - - const { rpcClient } = useRpcContext(); - // use selected model - const [modelsCodeData, setModelsCodeData] = useState([]); - const [selectedModelCodeData, setSelectedModelCodeData] = useState(); - // already assigned model - const [selectedModel, setSelectedModel] = useState(); - const [selectedModelFields, setSelectedModelFields] = useState([]); - - const [loading, setLoading] = useState(false); - const [savingForm, setSavingForm] = useState(false); - - const agentFilePath = useRef(""); - const aiModuleOrg = useRef(""); - const moduleConnectionNodes = useRef([]); - const selectedModelFlowNode = useRef(); - - useEffect(() => { - initPanel(); - }, []); - - useEffect(() => { - if (modelsCodeData.length > 0 && selectedModel && !selectedModelCodeData) { - // Find the initial modelCodeData that matches selectedModel - let initialModelCodeData: CodeData | undefined; - if (aiModuleOrg.current === BALLERINA) { - initialModelCodeData = modelsCodeData.find((model) => model.module === selectedModel.codedata.module); - } else if (aiModuleOrg.current === BALLERINAX) { - initialModelCodeData = modelsCodeData.find((model) => model.object === selectedModel.codedata.object); - } else { - initialModelCodeData = modelsCodeData.find((model) => model.module === selectedModel.codedata.module); - } - if (initialModelCodeData) { - setSelectedModelCodeData(initialModelCodeData); - fetchModelNodeTemplate(initialModelCodeData); - } else { - fetchModelNodeTemplate(selectedModel.codedata); - } - } - }, [modelsCodeData, selectedModel]); - - const initPanel = async () => { - setLoading(true); - if (agentCallNode?.codedata?.node === "NP_FUNCTION") { - agentFilePath.current = await getNPFilePath(rpcClient); - } else { - agentFilePath.current = await getAgentFilePath(rpcClient); - } - - if (agentCallNode?.codedata?.node === "NP_FUNCTION") { - aiModuleOrg.current = "ballerina" - } else { - aiModuleOrg.current = await getAiModuleOrg(rpcClient); - } - - // fetch all models - await fetchModels(); - // fetch selected agent model - await fetchSelectedAgentModel(); - setLoading(false); - }; - - // Helper to get provider name from modelCodeData - const getProviderName = (model: CodeData, useMappedName: boolean = false) => { - if (!model) return ""; - if (aiModuleOrg.current === BALLERINA) { - if (useMappedName) { - return PROVIDER_NAME_MAP[model.module] || model.module; - } - return model.module; - } else if (aiModuleOrg.current === BALLERINAX) { - return model.object; - } - // fallback - return model.module || model.object; - }; - - const fetchModels = async () => { - console.log(">>> agent call node", agentCallNode); - let agentName; - if (agentCallNode?.codedata?.node === "NP_FUNCTION") { - agentName = agentCallNode.properties?.modelProvider?.value; - } else { - agentName = agentCallNode?.properties.connection.value; - } - if (!agentName) { - console.error("Agent name not found", agentCallNode); - return; - } - const models = await rpcClient - .getAIAgentRpcClient() - .getAllModels({ agent: agentName, filePath: agentFilePath.current, orgName: aiModuleOrg.current }); - console.log(">>> all models", models); - setModelsCodeData(models.models); - }; - - const fetchSelectedAgentModel = async () => { - // get module nodes - const moduleNodes = await rpcClient.getBIDiagramRpcClient().getModuleNodes(); - console.log(">>> module nodes", moduleNodes); - if (moduleNodes.flowModel.connections.length > 0) { - moduleConnectionNodes.current = moduleNodes.flowModel.connections; - } - // get agent name - let agentName; - if (agentCallNode?.codedata?.node === "NP_FUNCTION") { - agentName = agentCallNode.properties?.modelProvider?.value; - } else { - agentName = agentCallNode.properties.connection.value; - } - // get agent node - const agentNode = moduleConnectionNodes.current.find((node) => node.properties.variable.value === agentName); - console.log(">>> agent node", agentNode); - if (!agentNode) { - console.error("Agent node not found", agentCallNode); - return; - } - // get model name - const modelName = agentNode?.properties?.model?.value || agentNode?.properties?.variable?.value; - console.log(">>> model name", modelName); - // get model node - const modelNode = moduleConnectionNodes.current.find((node) => node.properties.variable.value === modelName); - setSelectedModel(modelNode); - console.log(">>> selected model node", modelNode); - }; - - // fetch selected model code data - node template - const fetchModelNodeTemplate = async (modelCodeData: CodeData) => { - setLoading(true); - let nodeProperties: NodeProperties = {}; - // Determine provider match for both aiModuleOrg cases - let isProviderMatch = false; - if (aiModuleOrg.current === BALLERINA) { - isProviderMatch = selectedModel?.codedata.module === modelCodeData.module; - } else if (aiModuleOrg.current === BALLERINAX) { - isProviderMatch = selectedModel?.codedata.object === modelCodeData.object; - } else { - isProviderMatch = selectedModel?.codedata.module === modelCodeData.module; - } - if (isProviderMatch) { - // use selected model properties - selectedModelFlowNode.current = cloneDeep(selectedModel); - nodeProperties = selectedModel?.properties; - } else { - const modelNodeTemplate = await getNodeTemplate(rpcClient, modelCodeData, agentFilePath.current); - console.log(">>> selected model node template", { modelNodeTemplate, modelCodeData }); - selectedModelFlowNode.current = cloneDeep(modelNodeTemplate); - nodeProperties = modelNodeTemplate.properties; - } - console.log(">>> node properties", nodeProperties); - // use same variable name for model fields - if (selectedModel?.properties.variable) { - nodeProperties.variable.value = selectedModel?.properties.variable.value; - nodeProperties.variable.hidden = true; - } else { - console.error("Already assigned model node variable not found", selectedModel); - } - - const modelFields = convertConfig(nodeProperties); - setSelectedModelFields(modelFields); - setLoading(false); - }; - - const handleOnSave = async (data: FormField[], rawData: FormValues) => { - console.log(">>> save value", { data, rawData }); - setSavingForm(true); - const nodeTemplate = selectedModelFlowNode.current; - data.forEach((field) => { - if (field.editable) { - nodeTemplate.properties[field.key as keyof typeof nodeTemplate.properties].value = field.value; - } - }); - // update codedata range with already assigned model range (override) - nodeTemplate.codedata.lineRange = selectedModel?.codedata.lineRange; - // update isNew to false - nodeTemplate.codedata.isNew = false; - // update model name - nodeTemplate.properties.variable.value = selectedModel?.properties.variable.value; - console.log(">>> request getSourceCode with template ", { nodeTemplate }); - // update source - const response = await rpcClient - .getBIDiagramRpcClient() - .getSourceCode({ filePath: agentFilePath.current, flowNode: nodeTemplate }); - console.log(">>> response getSourceCode with template ", { response }); - - // If the selected model is the default WSO2 model provider, configure it - if (selectedModelCodeData?.symbol === GET_DEFAULT_MODEL_PROVIDER) { - await rpcClient.getAIAgentRpcClient().configureDefaultModelProvider(); - } - - onSave?.(response); - setSavingForm(false); - }; - - return ( - - {modelsCodeData.length > 0 && ( - <> - - (b.symbol === GET_DEFAULT_MODEL_PROVIDER ? 1 : 0) - (a.symbol === GET_DEFAULT_MODEL_PROVIDER ? 1 : 0)) - .map((model) => ({ - value: getProviderName(model), - content: model.symbol === GET_DEFAULT_MODEL_PROVIDER ? WSO2_MODEL_PROVIDER : getProviderName(model, true) - })), - ]} - label="Select Model Provider" - description={"Available Providers"} - onValueChange={(value) => { - if (value === "Select a provider...") { - return; // Skip the init option - } - const selectedModelCodeData = modelsCodeData.find((model) => getProviderName(model) === value); - console.log("Selected Model Code Data: ", selectedModelCodeData); - setSelectedModelCodeData(selectedModelCodeData); - fetchModelNodeTemplate(selectedModelCodeData); - }} - value={ - selectedModelCodeData - ? getProviderName(selectedModelCodeData) - : ((agentCallNode?.metadata.data as NodeMetadata)?.model?.type as string) - } - containerSx={{ width: "100%" }} - /> - - {selectedModelCodeData?.symbol === GET_DEFAULT_MODEL_PROVIDER && ( - - -
- - Using the default WSO2 Model Provider will automatically add the necessary configuration values to Config.toml. - -
-
- This can also be done using the VSCode command palette command:
- - {">"} Ballerina: Configure default WSO2 model provider - -
-
-
- )} - - )} - {loading && ( - - - - )} - {!loading && selectedModelFields?.length > 0 && selectedModel?.codedata?.lineRange && ( - - )} -
- ); -} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/NewAgent.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/NewAgent.tsx index e0c11fd5d7c..b6a4c21f41f 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/NewAgent.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/NewAgent.tsx @@ -18,11 +18,10 @@ import { useEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; -import { FlowNode, LinePosition, LineRange, NodeProperties } from "@wso2/ballerina-core"; +import { AvailableNode, FlowNode, LineRange, NodeProperties } from "@wso2/ballerina-core"; import { FormField, FormValues } from "@wso2/ballerina-side-panel"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { convertConfig } from "../../../utils/bi"; -import { URI, Utils } from "vscode-uri"; import ConfigForm from "./ConfigForm"; import { cloneDeep } from "lodash"; import { RelativeLoader } from "../../../components/RelativeLoader"; @@ -54,17 +53,13 @@ export function NewAgent(props: NewAgentProps): JSX.Element { const { rpcClient } = useRpcContext(); const [agentNode, setAgentNode] = useState(null); - const [defaultModelNode, setDefaultModelNode] = useState(null); const [formFields, setFormFields] = useState([]); const [loading, setLoading] = useState(false); const [savingForm, setSavingForm] = useState(false); - const agentFilePath = useRef(""); - const mainFilePath = useRef(""); + const projectPath = useRef(""); const aiModuleOrg = useRef(""); - const agentCallEndOfFile = useRef(null); - const agentEndOfFile = useRef(null); useEffect(() => { initPanel(); @@ -72,64 +67,40 @@ export function NewAgent(props: NewAgentProps): JSX.Element { const initPanel = async () => { setLoading(true); - // get agent file path + // get project path const filePath = await rpcClient.getVisualizerLocation(); - agentFilePath.current = await rpcClient.getVisualizerRpcClient().joinProjectPath("agents.bal"); - // get main file path - mainFilePath.current = Utils.joinPath(URI.file(filePath.projectUri), "main.bal").fsPath; + projectPath.current = filePath.projectUri; // get agent org aiModuleOrg.current = await getAiModuleOrg(rpcClient); // fetch agent node await fetchAgentNode(); - // get end of files - // main.bal last line - const endOfFile = await rpcClient.getBIDiagramRpcClient().getEndOfFile({ filePath: fileName }); - console.log(">>> endOfFile", endOfFile); - agentCallEndOfFile.current = endOfFile; - // agent.bal file last line - const endOfAgentFile = await rpcClient - .getBIDiagramRpcClient() - .getEndOfFile({ filePath: agentFilePath.current }); - console.log(">>> endOfAgentFile", endOfAgentFile); - agentEndOfFile.current = endOfAgentFile; }; useEffect(() => { - if (agentNode && defaultModelNode) { + if (agentNode) { configureFormFields(); setLoading(false); } - }, [agentNode, defaultModelNode]); + }, [agentNode]); const fetchAgentNode = async () => { - // get the agent node - const allAgents = await rpcClient.getAIAgentRpcClient().getAllAgents({ filePath: agentFilePath.current, orgName: aiModuleOrg.current }); - console.log(">>> allAgents", allAgents); - if (!allAgents.agents.length) { + // Search for agent node using search API + const agentSearchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: projectPath.current, + queryMap: { orgName: aiModuleOrg.current }, + searchKind: "AGENT" + }); + console.log(">>> agentSearchResponse", agentSearchResponse); + + // Validate search response structure + if (!agentSearchResponse?.categories?.[0]?.items?.length) { console.log(">>> no agents found"); return; } - const agentCodeData = allAgents.agents.at(0); + const agentCodeData = (agentSearchResponse.categories[0].items[0] as AvailableNode).codedata; // get agent node template - const agentNodeTemplate = await getNodeTemplate(rpcClient, agentCodeData, agentFilePath.current); + const agentNodeTemplate = await getNodeTemplate(rpcClient, agentCodeData, projectPath.current); setAgentNode(agentNodeTemplate); - - // get all llm models - const allModels = await rpcClient - .getAIAgentRpcClient() - .getAllModels({ agent: agentCodeData.object, filePath: agentFilePath.current, orgName: aiModuleOrg.current }); - console.log(">>> allModels", allModels); - // get openai model - const defaultModel = allModels.models.find((model) => model.object === "OpenAiProvider" || (model.org === BALLERINA && model.module === AI)); - if (!defaultModel) { - console.log(">>> no default model found"); - return; - } - // get model node template - const modelNodeTemplate = await getNodeTemplate(rpcClient, defaultModel, agentFilePath.current); - setDefaultModelNode(modelNodeTemplate); - - // get agent call node template }; const configureFormFields = () => { @@ -231,16 +202,33 @@ export function NewAgent(props: NewAgentProps): JSX.Element { console.log(">>> save value", { data, rawData }); setSavingForm(true); + // Search for model providers using search API + const modelProviderSearchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: projectPath.current, + queryMap: { q: aiModuleOrg.current === BALLERINA ? "ai" : "OpenAiProvider" }, + searchKind: aiModuleOrg.current === BALLERINA ? "MODEL_PROVIDER" : "CLASS_INIT" + }); + console.log(">>> modelProviderSearchResponse", modelProviderSearchResponse); + + const modelNodes = modelProviderSearchResponse.categories[0].items as AvailableNode[]; + // get openai model + const defaultModelNode = modelNodes.find((model) => + model.codedata.object === "OpenAiProvider" || (model.codedata.org === BALLERINA && model.codedata.module === AI) + ); + const defaultModel = defaultModelNode?.codedata; + if (!defaultModel) { + console.log(">>> no default model found"); + return; + } + // get model node template + const defaultModelNodeTemplate = await getNodeTemplate(rpcClient, defaultModel, projectPath.current); + // save model node const modelResponse = await rpcClient .getBIDiagramRpcClient() - .getSourceCode({ filePath: agentFilePath.current, flowNode: defaultModelNode }); + .getSourceCode({ filePath: projectPath.current, flowNode: defaultModelNodeTemplate }); console.log(">>> modelResponse getSourceCode", { modelResponse }); - const modelVarName = defaultModelNode.properties.variable.value as string; - - // wait 2 seconds (wait until LS is updated) - console.log(">>> wait 2 seconds"); - await new Promise((resolve) => setTimeout(resolve, 2000)); + const modelVarName = defaultModelNodeTemplate.properties.variable.value as string; // save the agent node const updatedAgentNode = cloneDeep(agentNode); @@ -258,14 +246,10 @@ export function NewAgent(props: NewAgentProps): JSX.Element { const agentResponse = await rpcClient .getBIDiagramRpcClient() - .getSourceCode({ filePath: agentFilePath.current, flowNode: updatedAgentNode }); + .getSourceCode({ filePath: projectPath.current, flowNode: updatedAgentNode }); console.log(">>> agentResponse getSourceCode", { agentResponse }); const agentVarName = agentNode.properties.variable.value as string; - // wait 2 seconds (wait until LS is updated) - console.log(">>> wait 2 seconds"); - await new Promise((resolve) => setTimeout(resolve, 2000)); - // update the agent call node const updatedAgentCallNode = cloneDeep(agentCallNode); updatedAgentCallNode.properties.variable.value = rawData["variable"]; @@ -303,7 +287,7 @@ export function NewAgent(props: NewAgentProps): JSX.Element { )} {!loading && ( { +export const getAiModuleOrg = async (rpcClient: BallerinaRpcClient, nodeKind?: NodeKind) => { + if (nodeKind && (nodeKind === "NP_FUNCTION" || nodeKind === "NP_FUNCTION_DEFINITION")) return BALLERINA; const filePath = await rpcClient.getVisualizerLocation(); const aiModuleOrgResponse = await rpcClient .getAIAgentRpcClient() @@ -46,8 +48,6 @@ export const getAiModuleOrg = async (rpcClient: BallerinaRpcClient) => { } export const getAgentFilePath = async (rpcClient: BallerinaRpcClient) => { - // Get the agent file path and update the node - const filePath = await rpcClient.getVisualizerLocation(); // Create the agent file path const agentFilePath = await rpcClient.getVisualizerRpcClient().joinProjectPath("agents.bal"); return agentFilePath; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Configurables/ViewConfigurableVariables/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Configurables/ViewConfigurableVariables/index.tsx index 2ed07dace3b..804863e6ace 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Configurables/ViewConfigurableVariables/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Configurables/ViewConfigurableVariables/index.tsx @@ -55,20 +55,6 @@ const Description = styled(Typography)` color: var(--vscode-descriptionForeground); `; -const ButtonWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - font-size: 10px; - width: auto; - margin-left: 15px; -`; - -const ConfigValueField = styled.div` - display: flex; -`; - const TitleBoxShadow = styled.div` box-shadow: var(--vscode-scrollbar-shadow) 0 6px 6px -6px inset; height: 3px; @@ -88,17 +74,6 @@ const SearchContainer = styled.div` gap: 40px; `; -const ConfigNameTitle = styled.div` - font-size: 13px; - font-weight: 700; - height: 20px; - color: var(--vscode-settings-headerForeground); - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 5px; -`; - const searchIcon = (); export interface ConfigProps { @@ -297,7 +272,10 @@ export function ViewConfigurableVariables(props?: ConfigProps) { await rpcClient .getBIDiagramRpcClient() - .getConfigVariablesV2() + .getConfigVariablesV2({ + includeLibraries: false, + projectPath: '' + }) .then((variables) => { data = (variables as any).configVariables; errorMsg = (variables as any).errorMsg; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx index 4f0b8dbd0de..e04f50bdd1e 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx @@ -20,11 +20,13 @@ import { useEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; import { AvailableNode, + DIRECTORY_MAP, EVENT_TYPE, FlowNode, LinePosition, LineRange, MACHINE_VIEW, + ParentPopupData, RunExternalCommandResponse, SubPanel, SubPanelView, @@ -35,6 +37,7 @@ import ConnectionConfigView from "../ConnectionConfigView"; import { getFormProperties } from "../../../../utils/bi"; import { ExpressionFormField, FormField, FormValues, PanelContainer } from "@wso2/ballerina-side-panel"; import { Icon, Overlay, ThemeColors, Typography } from "@wso2/ui-toolkit"; +import { RelativeLoader } from "../../../../components/RelativeLoader"; import { HelperView } from "../../HelperView"; import { BodyText } from "../../../styles"; import { DownloadIcon } from "../../../../components/DownloadIcon"; @@ -46,11 +49,23 @@ const Container = styled.div` justify-content: center; `; +const PopupContainer = styled.div` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80%; + height: 80%; + z-index: 2000; + background-color: ${ThemeColors.SURFACE_BRIGHT}; + border-radius: 20px; + overflow: hidden; +`; + const StatusCard = styled.div` margin: 16px 16px 0 16px; padding: 16px; border-radius: 8px; - background: ${ThemeColors.SURFACE_DIM_2}; display: flex; flex-direction: row; align-items: center; @@ -62,6 +77,13 @@ const StatusCard = styled.div` } `; +const StatusContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; +`; + const StatusText = styled(Typography)` color: ${ThemeColors.ON_SURFACE}; `; @@ -73,9 +95,9 @@ enum WizardStep { } enum PullingStatus { + FETCHING = "fetching", PULLING = "pulling", SUCCESS = "success", - EXISTS = "exists", ERROR = "error", } @@ -88,15 +110,17 @@ enum SavingFormStatus { interface AddConnectionWizardProps { fileName: string; // file path of `connection.bal` target?: LinePosition; - onClose?: () => void; + onClose?: (parent?: ParentPopupData) => void; + isPopupScreen?: boolean; + openCustomConnectorView?: boolean; } export function AddConnectionWizard(props: AddConnectionWizardProps) { - const { fileName, target, onClose } = props; + const { fileName, target, onClose, isPopupScreen, openCustomConnectorView } = props; const { rpcClient } = useRpcContext(); const [currentStep, setCurrentStep] = useState(WizardStep.CONNECTOR_LIST); - const [pullingStatus, setPullingStatus] = useState(undefined); + const [pullingStatus, setPullingStatus] = useState(PullingStatus.FETCHING); const [savingFormStatus, setSavingFormStatus] = useState(undefined); const selectedConnectorRef = useRef(); const selectedNodeRef = useRef(); @@ -137,15 +161,49 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { return; } selectedConnectorRef.current = connector; - setFetchingInfo(true); + setCurrentStep(WizardStep.CONNECTION_CONFIG); try { - const response = await rpcClient.getBIDiagramRpcClient().getNodeTemplate({ + let timer: ReturnType | null = null; + let didTimeout = false; + + // Set status to FETCHING before starting + setPullingStatus(PullingStatus.FETCHING); + selectedNodeRef.current = undefined; + + // Start a timer for 3 seconds + const timeoutPromise = new Promise((resolve) => { + timer = setTimeout(() => { + didTimeout = true; + setPullingStatus(PullingStatus.PULLING); + resolve(); + }, 3000); + }); + + // Start the request + const nodeTemplatePromise = rpcClient.getBIDiagramRpcClient().getNodeTemplate({ position: target || null, filePath: fileName, id: connector.codedata, }); + // Wait for either the timer or the request to finish + const response = await Promise.race([ + nodeTemplatePromise.then((res) => { + if (timer) { + clearTimeout(timer); + timer = null; + } + return res; + }), + timeoutPromise.then(() => nodeTemplatePromise) + ]); + + if (didTimeout) { + // If it timed out, set status to SUCCESS + setPullingStatus(PullingStatus.SUCCESS); + } + console.log(">>> FlowNode template", response); selectedNodeRef.current = response.flowNode; const formProperties = getFormProperties(response.flowNode); @@ -156,12 +214,14 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { handleOnFormSubmit(response.flowNode); return; } - - // get node properties - setCurrentStep(WizardStep.CONNECTION_CONFIG); - // Start pulling connector after transitioning to config step + } catch (error) { + console.error(">>> Error selecting connector", error); + setPullingStatus(PullingStatus.ERROR); } finally { - setFetchingInfo(false); + // After few seconds, set status to undefined + setTimeout(() => { + setPullingStatus(undefined); + }, 2000); } }; @@ -208,7 +268,8 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { // clear memory selectedNodeRef.current = undefined; setSavingFormStatus(SavingFormStatus.SUCCESS); - onClose ? onClose() : gotoHome(); + const newConnection = response.artifacts.find((artifact) => artifact.isNew); + onClose ? onClose({ recentIdentifier: newConnection.name, artifactType: DIRECTORY_MAP.CONNECTION }) : gotoHome(); } else { console.error(">>> Error updating source code", response); setSavingFormStatus(SavingFormStatus.ERROR); @@ -243,7 +304,7 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { }; const handleOnBack = () => { - setCurrentStep(WizardStep.CONNECTOR_LIST); + isPopupScreen ? onClose() : setCurrentStep(WizardStep.CONNECTOR_LIST); }; const handleSubPanel = (subPanel: SubPanel) => { @@ -287,87 +348,92 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { return ( - <> + {!isPopupScreen ? ( - {(currentStep === WizardStep.CONNECTION_CONFIG || currentStep === WizardStep.GENERATE_CONNECTOR) && ( - - )} - - {!fetchingInfo && currentStep === WizardStep.CONNECTION_CONFIG && ( + ) : (currentStep === WizardStep.CONNECTOR_LIST) && ( + <> + + + + + + )} + {(currentStep === WizardStep.CONNECTION_CONFIG || currentStep === WizardStep.GENERATE_CONNECTOR) && ( + + )} + {currentStep === WizardStep.CONNECTION_CONFIG && ( <> - {pullingStatus === PullingStatus.PULLING && ( - - - - Please wait while the connector package is being pulled... - - - )} - {pullingStatus === PullingStatus.EXISTS && ( - - - - Connector module already pulled. Please continue with the configuration. - - - )} - {pullingStatus === PullingStatus.SUCCESS && ( - - - Connector module pulled successfully. - - )} - {pullingStatus === PullingStatus.ERROR && ( - - - - Failed to pull the connector module. Please try again. - - + {pullingStatus && ( + + {pullingStatus === PullingStatus.FETCHING && ( + + )} + {pullingStatus === PullingStatus.PULLING && ( + + + + Please wait while the connector package is being pulled... + + + )} + {pullingStatus === PullingStatus.SUCCESS && ( + + + Connector package pulled successfully. + + )} + {pullingStatus === PullingStatus.ERROR && ( + + + + Failed to pull the connector package. Please try again. + + + )} + )} - - - Provide the necessary configuration details for the selected connector to complete the - setup. - - - {savingFormStatus === SavingFormStatus.SAVING && ( - Saving connection ... - )} - {savingFormStatus === SavingFormStatus.SUCCESS && ( - Connection saved successfully. - )} - {savingFormStatus === SavingFormStatus.ERROR && ( - Error saving connection. + {!pullingStatus && selectedNodeRef.current && ( + <> + + Provide the necessary configuration details for the selected connector to complete the + setup. + + + )} @@ -405,31 +471,8 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { )} - +
); } export default AddConnectionWizard; - -// TODO: remove this logic once module pull supported from LS -export function isConnectorDependOnDriver(connectorModule: string): boolean { - const dbConnectors = ["mysql", "mssql", "postgresql", "oracledb", "cdata.connect", "snowflake"]; - if (dbConnectors.includes(connectorModule)) { - return true; - } - return false; -} - -// run command message handler -export function handleRunCommandResponse(response: RunExternalCommandResponse): PullingStatus { - if (response.message.includes("Package already exists")) { - return PullingStatus.EXISTS; - } - if (response.message.includes("pulled from central successfully")) { - return PullingStatus.SUCCESS; - } - if (!response.error) { - return PullingStatus.SUCCESS; - } - return PullingStatus.ERROR; -} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigView/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigView/index.tsx index e4b988052ae..9b68166967b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigView/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigView/index.tsx @@ -22,6 +22,8 @@ import { ExpressionFormField } from "@wso2/ballerina-side-panel"; import { FlowNode, LineRange, SubPanel } from "@wso2/ballerina-core"; import FormGenerator from "../../Forms/FormGenerator"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { SidePanelView } from "../../FlowDiagram/PanelManager"; +import { ConnectionKind } from "../../../../components/ConnectionSelector"; const Container = styled.div` max-width: 600px; @@ -54,6 +56,7 @@ interface ConnectionConfigViewProps { resetUpdatedExpressionField?: () => void; isActiveSubPanel?: boolean; isPullingConnector?: boolean; + navigateToPanel?: (targetPanel: SidePanelView, connectionKind?: ConnectionKind) => void; } export function ConnectionConfigView(props: ConnectionConfigViewProps) { @@ -67,6 +70,7 @@ export function ConnectionConfigView(props: ConnectionConfigViewProps) { isPullingConnector, submitText, isSaving, + navigateToPanel, } = props; const { rpcClient } = useRpcContext(); const [targetLineRange, setTargetLineRange] = useState(); @@ -109,6 +113,7 @@ export function ConnectionConfigView(props: ConnectionConfigViewProps) { updatedExpressionField={updatedExpressionField} resetUpdatedExpressionField={resetUpdatedExpressionField} disableSaveButton={isPullingConnector} + navigateToPanel={navigateToPanel} /> )} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectorView/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectorView/index.tsx index 37d4c6e2af8..b7e04db9d39 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectorView/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectorView/index.tsx @@ -20,7 +20,7 @@ import React, { useEffect, useState } from "react"; import styled from "@emotion/styled"; import { AvailableNode, Category, FlowNode, Item, LinePosition } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; -import { Button, Codicon, ProgressRing, SearchBox, Typography, View } from "@wso2/ui-toolkit"; +import { Button, Codicon, ProgressRing, SearchBox, SplitView, ThemeColors, TreeViewItem, Typography, View } from "@wso2/ui-toolkit"; import { cloneDeep, debounce } from "lodash"; import ButtonCard from "../../../../components/ButtonCard"; import { BodyText, BodyTinyInfo, TopBar } from "../../../styles"; @@ -31,13 +31,17 @@ import { TopNavigationBar } from "../../../../components/TopNavigationBar"; const ViewWrapper = styled.div<{ isHalfView?: boolean }>` display: flex; flex-direction: column; - height: ${(props: { isHalfView: boolean }) => (props.isHalfView ? "40vh" : "100vh")}; + height: ${(props: { isHalfView: boolean }) => (props.isHalfView ? "40vh" : "100%")}; width: 100%; `; const Container = styled.div` padding: 0 20px; width: 100%; + height: 100%; + padding-bottom: 20px; + display: flex; + flex-direction: column; `; const ListContainer = styled.div<{ isHalfView?: boolean }>` @@ -45,6 +49,7 @@ const ListContainer = styled.div<{ isHalfView?: boolean }>` flex-direction: column; gap: 8px; margin-top: 16px; + margin-left: 20px; height: ${(props: { isHalfView: boolean }) => (props.isHalfView ? "30vh" : "calc(100vh - 200px)")}; overflow-y: scroll; `; @@ -73,6 +78,15 @@ const LabelRow = styled.div` width: 100%; `; +const TreeViewItemContent = styled.div<{ isLoading: boolean }>` + display: flex; + align-items: center; + height: 20px; + opacity: ${(props: { isLoading: boolean }) => props.isLoading ? 0.5 : 1}; + cursor: ${(props: { isLoading: boolean }) => props.isLoading ? 'not-allowed' : 'pointer'}; + pointer-events: ${(props: { isLoading: boolean }) => props.isLoading ? 'none' : 'auto'}; +`; + const StyledSearchInput = styled(SearchBox)` height: 30px; `; @@ -82,9 +96,9 @@ interface ConnectorViewProps { targetLinePosition: LinePosition; onSelectConnector: (connector: AvailableNode) => void; onAddGeneratedConnector: () => void; - fetchingInfo: boolean; onClose?: () => void; hideTitle?: boolean; + openCustomConnectorView?: boolean; } export function ConnectorView(props: ConnectorViewProps) { @@ -94,14 +108,18 @@ export function ConnectorView(props: ConnectorViewProps) { onSelectConnector, onAddGeneratedConnector, onClose, - fetchingInfo, hideTitle, + openCustomConnectorView, } = props; const { rpcClient } = useRpcContext(); const [connectors, setConnectors] = useState([]); const [searchText, setSearchText] = useState(""); const [isSearching, setIsSearching] = useState(false); + const [fetchingInfo, setFetchingInfo] = useState(false); + const [selectedConnectorCategory, setSelectedConnectorCategory] = useState( + openCustomConnectorView? "LocalConnectors" : "StandardLibrary" + ); useEffect(() => { setIsSearching(true); @@ -114,7 +132,8 @@ export function ConnectorView(props: ConnectorViewProps) { } }); - const getConnectors = () => { + const getConnectors = (filter?: boolean) => { + setFetchingInfo(true); rpcClient .getBIDiagramRpcClient() .search({ @@ -123,8 +142,12 @@ export function ConnectorView(props: ConnectorViewProps) { endLine: targetLinePosition, }, filePath: fileName, - queryMap: { limit: 60 }, - searchKind: "CONNECTOR", + queryMap: { + limit: 60, + filterByCurrentOrg: + filter ?? false, + }, + searchKind: "CONNECTOR" }) .then(async (model) => { console.log(">>> bi connectors", model); @@ -134,6 +157,7 @@ export function ConnectorView(props: ConnectorViewProps) { }) .finally(() => { setIsSearching(false); + setFetchingInfo(false); }); }; @@ -152,8 +176,14 @@ export function ConnectorView(props: ConnectorViewProps) { endLine: targetLinePosition, }, filePath: fileName, - queryMap: { q: text, limit: 60 }, - searchKind: "CONNECTOR", + queryMap: { + q: text, + limit: 60, + filterByCurrentOrg: + selectedConnectorCategory === "CurrentOrg" ? + true : false, + }, + searchKind: "CONNECTOR" }) .then(async (model) => { console.log(">>> bi searched connectors", model); @@ -171,6 +201,13 @@ export function ConnectorView(props: ConnectorViewProps) { setSearchText(text); }; + const handleCategoryChange = (category: string) => { + if (category !== selectedConnectorCategory) { + setSelectedConnectorCategory(category); + getConnectors(category === "CurrentOrg" ? true : false); + } + }; + const filterItems = (items: Item[]): Item[] => { return items .map((item) => { @@ -201,6 +238,12 @@ export function ConnectorView(props: ConnectorViewProps) { } category.items = filterItems(category.items); return category; + }).filter((category) => { + if (selectedConnectorCategory === "LocalConnectors") { + return category.metadata.label === "Local"; + } else { + return category.metadata.label !== "Local"; + } }); async function filterCategories(categories: Category[]): Promise { @@ -221,6 +264,13 @@ export function ConnectorView(props: ConnectorViewProps) { }); } + const openLearnMoreURL = () => { + rpcClient.getCommonRpcClient().openExternalUrl({ + url: 'https://ballerina.io/learn/publish-packages-to-ballerina-central/' + }) + }; + + const isFullView = onClose === undefined; const isLoading = isSearching || fetchingInfo; @@ -257,116 +307,252 @@ export function ConnectorView(props: ConnectorViewProps) { autoFocus={true} onChange={handleOnSearch} size={60} - sx={{ width: "100%" }} + sx={{ width: "100%", marginBottom: "10px" }} /> - {isLoading && ( - -
+
+ - -
- - )} - {!isLoading && filteredCategories && filteredCategories.length > 0 && ( + { + handleCategoryChange("StandardLibrary"); + }} + > + + {'Standard Libraries'} + + + + + { + handleCategoryChange("CurrentOrg"); + }} + > + + {'Organization\'s Connectors'} + + + + + { + handleCategoryChange("LocalConnectors"); + }} + > + + {'Custom Connectors'} + + + +
+ {selectedConnectorCategory === "CurrentOrg" && ( + + {'Organization\'s Connectors'} + + )} + {isLoading && ( + +
+ +
+
+ )} + {selectedConnectorCategory === "CurrentOrg" && filteredCategories.length === 0 && ( +
+ + No connectors found in your organization. You can create and publish connectors to Ballerina Central. + + + Learn how to{' '} + { + openLearnMoreURL(); + }} + > + publish packages to Ballerina Central + + +
+ )} {/* Default connectors of LS is hardcoded and is sent with categories with item field */} - {filteredCategories[0]?.items ? ( - filteredCategories.map((category, index) => { - const isLocalConnectorCategory = category.metadata.label === "Local"; - const itemCount = category.items?.length || 0; - const isLocalConnectorsEmpty = isLocalConnectorCategory && itemCount === 0; - - if (!isLocalConnectorCategory && (!category.items || category.items.length === 0)) { - return null; - } - - return ( -
- - {category.metadata.label} - {isLocalConnectorCategory && ( - - )} - - {isLocalConnectorsEmpty ? ( - - No local connectors found. You can create one by importing an OpenAPI - spec. - - ) : ( - - {category.items?.map((connector, index) => { - return ( - - ) : ( - - ) - } - onClick={() => { - onSelectConnector(connector as AvailableNode); - }} - /> - ); - })} - - )} -
- ); - }) - ) : ( - - {connectors.map((item, index) => { - const connector = item as Item; - return ( - + {!isLoading && filteredCategories && filteredCategories.length > 0 && ( +
+ {filteredCategories[0]?.items ? ( + filteredCategories.map((category, index) => { + const isLocalConnectorCategory = category.metadata.label === "Local"; + const itemCount = category.items?.length || 0; + const isLocalConnectorsEmpty = isLocalConnectorCategory && itemCount === 0; + const label = category.metadata.label === "Local" ? "Custom Connectors" : category.metadata.label; + + if (!isLocalConnectorCategory && (!category.items || category.items.length === 0)) { + return null; + } + + return ( +
+ + {label} + {isLocalConnectorCategory && ( + + )} + + {isLocalConnectorsEmpty ? ( + <> + + Generate connector using OpenAPI spec + + + Can’t find what you need in the standard connectors? Create a custom connector using an OpenAPI specification. + +
+ + +
+ ) : ( - - ) - } - onClick={() => { - onSelectConnector(connector as AvailableNode); - }} - /> - ); - })} - + + {category.items?.map((connector, index) => { + return ( + + ) : ( + + ) + } + onClick={() => { + onSelectConnector(connector as AvailableNode); + }} + /> + ); + })} + + )} +
+ ); + }) + ) : ( + + {connectors.map((item, index) => { + const connector = item as Item; + return ( + + ) : ( + + ) + } + onClick={() => { + onSelectConnector(connector as AvailableNode); + }} + /> + ); + })} + + )} +
)}
- )} - {!isSearching && connectors.length === 0 &&

No connectors found

} + ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/EditConnectionWizard/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/EditConnectionWizard/index.tsx index c5f99a4868b..4eb8afe443c 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/EditConnectionWizard/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/EditConnectionWizard/index.tsx @@ -25,7 +25,9 @@ import { getFormProperties } from "../../../../utils/bi"; import { ExpressionFormField, PanelContainer } from "@wso2/ballerina-side-panel"; import { ProgressRing, ThemeColors } from "@wso2/ui-toolkit"; import { HelperView } from "../../HelperView"; -import { URI, Utils } from "vscode-uri"; +import { ConnectionKind, ConnectionSelectionList, ConnectionCreator } from "../../../../components/ConnectionSelector"; +import { SidePanelView } from "../../FlowDiagram/PanelManager"; +import { getNodeTemplateForConnection } from "../../FlowDiagram/utils"; const Container = styled.div` width: 100%; @@ -39,6 +41,13 @@ const SpinnerContainer = styled.div` height: 100%; `; +// Navigation views for the wizard +export enum WizardView { + CONNECTION_CONFIG = "CONNECTION_CONFIG", + CONNECTION_SELECT = "CONNECTION_SELECT", + CONNECTION_CREATE = "CONNECTION_CREATE" +} + interface EditConnectionWizardProps { projectUri: string; connectionName: string; @@ -46,16 +55,20 @@ interface EditConnectionWizardProps { } export function EditConnectionWizard(props: EditConnectionWizardProps) { - const { projectUri, connectionName, onClose } = props; + const { connectionName, onClose } = props; const { rpcClient } = useRpcContext(); const [connection, setConnection] = useState(); const [subPanel, setSubPanel] = useState({ view: SubPanelView.UNDEFINED }); - const [showSubPanel, setShowSubPanel] = useState(false); const [updatingContent, setUpdatingContent] = useState(false); const [filePath, setFilePath] = useState(""); const [updatedExpressionField, setUpdatedExpressionField] = useState(undefined); + // Navigation state + const [currentView, setCurrentView] = useState(WizardView.CONNECTION_CONFIG); + const [selectedConnectionKind, setSelectedConnectionKind] = useState(); + const [nodeFormTemplate, setNodeFormTemplate] = useState(); + useEffect(() => { rpcClient .getBIDiagramRpcClient() @@ -131,7 +144,6 @@ export function EditConnectionWizard(props: EditConnectionWizardProps) { }; const handleSubPanel = (subPanel: SubPanel) => { - setShowSubPanel(subPanel.view !== SubPanelView.UNDEFINED); setSubPanel(subPanel); }; @@ -161,6 +173,110 @@ export function EditConnectionWizard(props: EditConnectionWizardProps) { setUpdatedExpressionField(undefined); }; + // Navigation handlers + const handleNavigateToPanel = async (targetPanel: SidePanelView, connectionKind?: ConnectionKind) => { + if (connectionKind) { + setSelectedConnectionKind(connectionKind); + } + + switch (targetPanel) { + case SidePanelView.CONNECTION_SELECT: + setCurrentView(WizardView.CONNECTION_SELECT); + break; + case SidePanelView.CONNECTION_CREATE: + setCurrentView(WizardView.CONNECTION_CREATE); + break; + default: + setCurrentView(WizardView.CONNECTION_CONFIG); + break; + } + }; + + const handleSelectNewConnection = async (nodeId: string, metadata?: any) => { + try { + const { flowNode, connectionKind } = await getNodeTemplateForConnection( + nodeId, + metadata, + connection?.codedata?.lineRange, + filePath, + rpcClient + ); + + setNodeFormTemplate(flowNode); + setSelectedConnectionKind(connectionKind as ConnectionKind); + setCurrentView(WizardView.CONNECTION_CREATE); + } catch (error) { + console.error('Error getting node template for connection:', error); + } + }; + + const handleConnectionCreated = (connectionNode: FlowNode) => { + setCurrentView(WizardView.CONNECTION_CONFIG); + }; + + const handleBack = () => { + switch (currentView) { + case WizardView.CONNECTION_SELECT: + setCurrentView(WizardView.CONNECTION_CONFIG); + break; + case WizardView.CONNECTION_CREATE: + setCurrentView(WizardView.CONNECTION_SELECT); + break; + default: + onClose ? onClose() : gotoHome(); + break; + } + }; + + const getViewTitle = () => { + switch (currentView) { + case WizardView.CONNECTION_SELECT: + return `Select ${connection?.codedata?.module || ''} Connection`; + case WizardView.CONNECTION_CREATE: + return `Create ${connection?.codedata?.module || ''} Connection`; + default: + return `Configure ${connection?.codedata?.module || ''} Connector`; + } + }; + + const renderCurrentView = () => { + switch (currentView) { + case WizardView.CONNECTION_SELECT: + return ( + + ); + + case WizardView.CONNECTION_CREATE: + return ( + + ); + + default: + return ( + + ); + } + }; + return ( {!connection && ( @@ -171,23 +287,14 @@ export function EditConnectionWizard(props: EditConnectionWizardProps) { {connection && ( - + {renderCurrentView()} )} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/PanelManager.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/PanelManager.tsx index 80fb85a018c..84e77cc91b3 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/PanelManager.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/PanelManager.tsx @@ -29,16 +29,16 @@ import { import { HelperView } from "../HelperView"; import FormGenerator from "../Forms/FormGenerator"; import { getContainerTitle, getSubPanelWidth } from "../../../utils/bi"; -import { ModelConfig } from "../AIChatAgent/ModelConfig"; import { ToolConfig } from "../AIChatAgent/ToolConfig"; import { AgentConfig } from "../AIChatAgent/AgentConfig"; import { NewAgent } from "../AIChatAgent/NewAgent"; import { AddTool } from "../AIChatAgent/AddTool"; import { AddMcpServer } from "../AIChatAgent/AddMcpServer"; -import { useEffect, useState } from "react"; import { NewTool, NewToolSelectionMode } from "../AIChatAgent/NewTool"; import styled from "@emotion/styled"; import { MemoryManagerConfig } from "../AIChatAgent/MemoryManagerConfig"; +import { FormSubmitOptions } from "."; +import { ConnectionConfig, ConnectionCreator, ConnectionSelectionList, ConnectionKind } from "../../../components/ConnectionSelector"; const Container = styled.div` display: flex; @@ -58,6 +58,10 @@ export enum SidePanelView { VECTOR_STORE_LIST = "VECTOR_STORE_LIST", EMBEDDING_PROVIDERS = "EMBEDDING_PROVIDERS", EMBEDDING_PROVIDER_LIST = "EMBEDDING_PROVIDER_LIST", + DATA_LOADERS = "DATA_LOADERS", + DATA_LOADER_LIST = "DATA_LOADER_LIST", + CHUNKERS = "CHUNKERS", + CHUNKER_LIST = "CHUNKER_LIST", VECTOR_KNOWLEDGE_BASE_LIST = "VECTOR_KNOWLEDGE_BASE_LIST", NEW_AGENT = "NEW_AGENT", ADD_TOOL = "ADD_TOOL", @@ -67,7 +71,9 @@ export enum SidePanelView { ADD_MCP_SERVER = "ADD_MCP_SERVER", EDIT_MCP_SERVER = "EDIT_MCP_SERVER", AGENT_TOOL = "AGENT_TOOL", - AGENT_MODEL = "AGENT_MODEL", + CONNECTION_CONFIG = "CONNECTION_CONFIG", + CONNECTION_SELECT = "CONNECTION_SELECT", + CONNECTION_CREATE = "CONNECTION_CREATE", AGENT_MEMORY_MANAGER = "AGENT_MEMORY_MANAGER", AGENT_CONFIG = "AGENT_CONFIG", } @@ -90,6 +96,7 @@ interface PanelManagerProps { showProgressIndicator?: boolean; canGoBack?: boolean; selectedMcpToolkitName?: string; + selectedConnectionKind?: ConnectionKind; // Action handlers onClose: () => void; @@ -103,7 +110,9 @@ interface PanelManagerProps { onAddVectorStore?: () => void; onAddEmbeddingProvider?: () => void; onAddVectorKnowledgeBase?: () => void; - onSubmitForm: (updatedNode?: FlowNode, openInDataMapper?: boolean) => void; + onAddDataLoader?: () => void; + onAddChunker?: () => void; + onSubmitForm: (updatedNode?: FlowNode, openInDataMapper?: boolean, options?: FormSubmitOptions) => void; onDiscardSuggestions: () => void; onSubPanel: (subPanel: SubPanel) => void; onUpdateExpressionField: (updatedExpressionField: ExpressionFormField) => void; @@ -114,7 +123,11 @@ interface PanelManagerProps { onSearchVectorStore?: (searchText: string, functionType: FUNCTION_TYPE) => void; onSearchEmbeddingProvider?: (searchText: string, functionType: FUNCTION_TYPE) => void; onSearchVectorKnowledgeBase?: (searchText: string, functionType: FUNCTION_TYPE) => void; + onSearchDataLoader?: (searchText: string, functionType: FUNCTION_TYPE) => void; + onSearchChunker?: (searchText: string, functionType: FUNCTION_TYPE) => void; onEditAgent?: () => void; + onNavigateToPanel?: (targetPanel: SidePanelView, connectionKind?: ConnectionKind) => void; + setSidePanelView: (view: SidePanelView) => void; // AI Agent handlers onSelectTool?: (tool: ToolData, node: FlowNode) => void; @@ -122,6 +135,8 @@ interface PanelManagerProps { onDeleteTool?: (tool: ToolData, node: FlowNode) => void; onAddTool?: (node: FlowNode) => void; onAddMcpServer?: (node: FlowNode) => void; + onSelectNewConnection?: (nodeId: string, metadata?: any) => void; + onUpdateNodeWithConnection?: (selectedNode: FlowNode) => void; } export function PanelManager(props: PanelManagerProps) { @@ -143,6 +158,8 @@ export function PanelManager(props: PanelManagerProps) { showProgressIndicator, canGoBack, selectedMcpToolkitName, + selectedConnectionKind, + setSidePanelView, onClose, onBack, onSelectNode, @@ -154,6 +171,8 @@ export function PanelManager(props: PanelManagerProps) { onAddVectorStore, onAddEmbeddingProvider, onAddVectorKnowledgeBase, + onAddDataLoader, + onAddChunker, onSubmitForm, onDiscardSuggestions, onSubPanel, @@ -164,40 +183,39 @@ export function PanelManager(props: PanelManagerProps) { onSearchVectorStore, onSearchEmbeddingProvider, onSearchVectorKnowledgeBase, + onSearchDataLoader, + onSearchChunker, + onSelectNewConnection, + onUpdateNodeWithConnection, + onNavigateToPanel, } = props; - const [panelView, setPanelView] = useState(sidePanelView); - - useEffect(() => { - setPanelView(sidePanelView); - }, [sidePanelView]); - const handleOnAddTool = () => { - setPanelView(SidePanelView.NEW_TOOL); + setSidePanelView(SidePanelView.NEW_TOOL); }; const handleOnAddMcpServer = () => { - setPanelView(SidePanelView.ADD_MCP_SERVER); + setSidePanelView(SidePanelView.ADD_MCP_SERVER); }; const handleOnEditMcpServer = () => { - setPanelView(SidePanelView.EDIT_MCP_SERVER); + setSidePanelView(SidePanelView.EDIT_MCP_SERVER); }; const handleOnBackToAddTool = () => { - setPanelView(SidePanelView.ADD_TOOL); + setSidePanelView(SidePanelView.ADD_TOOL); }; const handleOnUseConnection = () => { - setPanelView(SidePanelView.NEW_TOOL_FROM_CONNECTION); + setSidePanelView(SidePanelView.NEW_TOOL_FROM_CONNECTION); }; const handleOnUseFunction = () => { - setPanelView(SidePanelView.NEW_TOOL_FROM_FUNCTION); + setSidePanelView(SidePanelView.NEW_TOOL_FROM_FUNCTION); }; const handleOnUseMcpServer = () => { - setPanelView(SidePanelView.ADD_MCP_SERVER); + setSidePanelView(SidePanelView.ADD_MCP_SERVER); }; const handleSubmitAndClose = () => { @@ -224,7 +242,7 @@ export function PanelManager(props: PanelManagerProps) { }; const renderPanelContent = () => { - switch (panelView) { + switch (sidePanelView) { case SidePanelView.NODE_LIST: return ( ; - case SidePanelView.AGENT_MODEL: - return ; - case SidePanelView.AGENT_CONFIG: return ; @@ -464,6 +479,87 @@ export function PanelManager(props: PanelManagerProps) { /> ); + case SidePanelView.DATA_LOADER_LIST: + return ( + + onSearchDataLoader?.(searchText, FUNCTION_TYPE.REGULAR) + } + onBack={canGoBack ? onBack : undefined} + /> + ); + + case SidePanelView.DATA_LOADERS: + return ( + + ); + + case SidePanelView.CHUNKER_LIST: + return ( + + onSearchChunker?.(searchText, FUNCTION_TYPE.REGULAR) + } + onBack={canGoBack ? onBack : undefined} + /> + ); + + case SidePanelView.CHUNKERS: + return ( + + ); + + case SidePanelView.CONNECTION_CONFIG: + return ( + onNavigateToPanel?.(SidePanelView.CONNECTION_SELECT)} + /> + ); + + case SidePanelView.CONNECTION_SELECT: + return ; + + case SidePanelView.CONNECTION_CREATE: + return ( + + ); + case SidePanelView.FORM: return ( ); @@ -489,21 +588,27 @@ export function PanelManager(props: PanelManagerProps) { } }; - const onBackCallback = - panelView === SidePanelView.NEW_TOOL || - panelView === SidePanelView.NEW_TOOL_FROM_CONNECTION || - panelView === SidePanelView.NEW_TOOL_FROM_FUNCTION || - panelView === SidePanelView.ADD_MCP_SERVER - ? handleOnBackToAddTool - : panelView === SidePanelView.NEW_AGENT - ? onBack - : panelView === SidePanelView.FORM && !showEditForm - ? onBack - : undefined; + const onBackCallback = (() => { + switch (sidePanelView) { + case SidePanelView.NEW_TOOL: + case SidePanelView.NEW_TOOL_FROM_CONNECTION: + case SidePanelView.NEW_TOOL_FROM_FUNCTION: + case SidePanelView.ADD_MCP_SERVER: + return handleOnBackToAddTool; + case SidePanelView.CONNECTION_SELECT: + case SidePanelView.CONNECTION_CREATE: + case SidePanelView.NEW_AGENT: + return onBack; + case SidePanelView.FORM: + return !showEditForm ? onBack : undefined; + default: + return undefined; + } + })(); return ( void; + shouldUpdateTargetLine?: boolean; + +}; + export function BIFlowDiagram(props: BIFlowDiagramProps) { const { projectPath, breakpointState, syntaxTree, onUpdate, onReady, onSave } = props; const { rpcClient } = useRpcContext(); @@ -118,6 +126,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { const [breakpointInfo, setBreakpointInfo] = useState(); const [selectedMcpToolkitName, setSelectedMcpToolkitName] = useState(undefined); const [forceUpdate, setForceUpdate] = useState(0); + const [selectedConnectionKind, setSelectedConnectionKind] = useState(); // Navigation stack for back navigation const [navigationStack, setNavigationStack] = useState([]); @@ -132,10 +141,17 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { const initialCategoriesRef = useRef([]); const showEditForm = useRef(false); const selectedNodeMetadata = useRef<{ nodeId: string; metadata: any; fileName: string }>(); + const onRerenderRef = useRef<((nodes: FlowNode[]) => void) | null>(null); + const shouldUpdateLineRangeRef = useRef(false); + const updatedNodeRef = useRef(undefined); + const [targetLineRange, setTargetLineRange] = useState(targetRef?.current); + const isCreatingNewModelProvider = useRef(false); const isCreatingNewVectorStore = useRef(false); const isCreatingNewEmbeddingProvider = useRef(false); const isCreatingNewVectorKnowledgeBase = useRef(false); + const isCreatingNewDataLoader = useRef(false); + const isCreatingNewChunker = useRef(false); useEffect(() => { debouncedGetFlowModel(); @@ -160,11 +176,47 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { return; } setShowProgressIndicator(true); + if (parent.artifactType === DIRECTORY_MAP.CONNECTION) { + updateConnectionWithNewItem(parent.recentIdentifier); + } fetchNodesAndAISuggestions(topNodeRef.current, targetRef.current, false, false); } }); }, [rpcClient]); + const updateConnectionWithNewItem = (recentIdentifier: string) => { + // Add a new item as loading into the "Connections" category + setCategories((prev: PanelCategory[]) => { + // Find the "Connections" category + const updated = prev.map((cat) => { + if (cat.title === "Connections") { + // Add new item to the items array and sort the items by title + return { + ...cat, + items: [ + ...(cat.items || []), + { title: recentIdentifier, isLoading: true, items: [] } + ].sort((a, b) => (a as PanelCategory).title.localeCompare((b as PanelCategory).title)) + }; + } + return cat; + }); + return updated as PanelCategory[]; + }); + }; + + + const changeTargetRange = (range: LineRange) => { + targetRef.current = range; + setTargetLineRange(range); + } + + useEffect(() => { + console.log(targetLineRange) + }, [targetLineRange]); + + + const debouncedGetFlowModel = useCallback( debounce(() => { getFlowModel(); @@ -258,7 +310,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } } else { console.log(">>> MODEL_PROVIDER_LIST not found in navigation stack, closing panel"); - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); } }; @@ -288,7 +340,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } } else { console.log(">>> VECTOR_STORE_LIST not found in navigation stack, closing panel"); - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); } }; @@ -318,7 +370,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } } else { console.log(">>> EMBEDDING_PROVIDER_LIST not found in navigation stack, closing panel"); - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); } }; @@ -351,7 +403,63 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } } else { console.log(">>> VECTOR_KNOWLEDGE_BASE_LIST not found in navigation stack, closing panel"); - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); + } + }; + + const handleDataLoaderAdded = async () => { + console.log(">>> Data loader added, navigating back to data loader list"); + + // Try to navigate back to DATA_LOADER_LIST in the stack + const foundInStack = popNavigationStackUntilView(SidePanelView.DATA_LOADER_LIST); + + if (foundInStack) { + setShowProgressIndicator(true); + try { + const response = await rpcClient.getBIDiagramRpcClient().getAvailableDataLoaders({ + position: targetRef.current.startLine, + filePath: model?.fileName, + }); + console.log(">>> Refreshed data loader list", response); + setCategories(convertDataLoaderCategoriesToSidePanelCategories(response.categories as Category[])); + setSidePanelView(SidePanelView.DATA_LOADER_LIST); + setShowSidePanel(true); + } catch (error) { + console.error(">>> Error refreshing data loaders", error); + } finally { + setShowProgressIndicator(false); + } + } else { + console.log(">>> DATA_LOADER_LIST not found in navigation stack, closing panel"); + closeSidePanelAndFetchUpdatedFlowModel(); + } + }; + + const handleChunkerAdded = async () => { + console.log(">>> Chunker added, navigating back to chunker list"); + + // Try to navigate back to CHUNKER_LIST in the stack + const foundInStack = popNavigationStackUntilView(SidePanelView.CHUNKER_LIST); + + if (foundInStack) { + setShowProgressIndicator(true); + try { + const response = await rpcClient.getBIDiagramRpcClient().getAvailableChunkers({ + position: targetRef.current.startLine, + filePath: model?.fileName, + }); + console.log(">>> Refreshed chunker list", response); + setCategories(convertChunkerCategoriesToSidePanelCategories(response.categories as Category[])); + setSidePanelView(SidePanelView.CHUNKER_LIST); + setShowSidePanel(true); + } catch (error) { + console.error(">>> Error refreshing chunkers", error); + } finally { + setShowProgressIndicator(false); + } + } else { + console.log(">>> CHUNKER_LIST not found in navigation stack, closing panel"); + closeSidePanelAndFetchUpdatedFlowModel(); } }; @@ -375,6 +483,19 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { (node) => node.codedata.node === "EVENT_START" )?.metadata.data as ParentMetadata | undefined; onReady(model.flowModel.fileName, parentMetadata); + if (onRerenderRef.current) { + onRerenderRef.current(model.flowModel.nodes); + } + if (shouldUpdateLineRangeRef.current) { + const varName = typeof updatedNodeRef.current?.properties?.variable?.value === "string" + ? updatedNodeRef.current.properties.variable.value + : ""; + const newNode = searchNodesByName(model.flowModel.nodes, varName) + changeTargetRange({ + startLine: newNode.codedata.lineRange.endLine, + endLine: newNode.codedata.lineRange.endLine + }) + } } }) .finally(() => { @@ -435,8 +556,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { // Only update refs if we found a matching node and it's different from the current one if (matchingNode && matchingNode.id !== selectedNodeRef.current.id) { selectedNodeRef.current = matchingNode; - targetRef.current = matchingNode.codedata.lineRange; - setForceUpdate(prev => prev + 1); + changeTargetRange(matchingNode.codedata.lineRange) } } }, [model]); @@ -483,7 +603,31 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { return searchNodes(flowModel.nodes); }; - const handleOnCloseSidePanel = () => { + + const findNodeWithName = (node: FlowNode, name: string) => { + return node?.properties?.variable?.value === name; + } + + const searchNodesByName = (nodes: FlowNode[], name: string): FlowNode | undefined => { + for (const node of nodes) { + if (findNodeWithName(node, name)) { + return node; + } + if (node.branches && node.branches.length > 0) { + for (const branch of node.branches) { + if (branch.children && branch.children.length > 0) { + const foundNode = searchNodesByName(branch.children, name); + if (foundNode) { + return foundNode; + } + } + } + } + } + return undefined; + }; + + const resetNodeSelectionStates = () => { setShowSidePanel(false); setSidePanelView(SidePanelView.NODE_LIST); setSubPanel({ view: SubPanelView.UNDEFINED }); @@ -491,12 +635,21 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { nodeTemplateRef.current = undefined; topNodeRef.current = undefined; targetRef.current = undefined; + changeTargetRange(undefined); selectedClientName.current = undefined; showEditForm.current = false; isCreatingNewModelProvider.current = false; + isCreatingNewVectorStore.current = false; + isCreatingNewEmbeddingProvider.current = false; + isCreatingNewVectorKnowledgeBase.current = false; + isCreatingNewDataLoader.current = false; + isCreatingNewChunker.current = false; clearNavigationStack(); + }; - // restore original model + const closeSidePanelAndFetchUpdatedFlowModel = () => { + resetNodeSelectionStates(); + // fetch new flow model if (originalFlowModel.current) { debouncedGetFlowModel(); originalFlowModel.current = undefined; @@ -505,6 +658,17 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } }; + const handleOnCloseSidePanel = () => { + resetNodeSelectionStates(); + // return to previous flow model + if (originalFlowModel.current) { + setModel(originalFlowModel.current); + originalFlowModel.current = undefined; + setSuggestedModel(undefined); + suggestedText.current = undefined; + } + }; + const fetchNodesAndAISuggestions = ( parent: FlowNode | Branch, target: LineRange, @@ -585,22 +749,22 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { topNodeRef: topNodeRef.current, targetRef: targetRef.current, }); - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); return; } // handle add new node topNodeRef.current = parent; - targetRef.current = target; + changeTargetRange(target) fetchNodesAndAISuggestions(parent, target); }; const handleOnAddNodePrompt = (parent: FlowNode | Branch, target: LineRange, prompt: string) => { if (topNodeRef.current || targetRef.current) { - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); return; } topNodeRef.current = parent; - targetRef.current = target; + changeTargetRange(target) originalFlowModel.current = model; setFetchingAiSuggestions(true); rpcClient @@ -669,6 +833,12 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { case "VECTOR_KNOWLEDGE_BASE": panelView = SidePanelView.VECTOR_KNOWLEDGE_BASE_LIST; break; + case "DATA_LOADER": + panelView = SidePanelView.DATA_LOADER_LIST; + break; + case "CHUNKER": + panelView = SidePanelView.CHUNKER_LIST; + break; default: panelView = SidePanelView.NODE_LIST; } @@ -705,6 +875,14 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { // await handleSearch(searchText, functionType, "VECTOR_KNOWLEDGE_BASE"); }; + const handleSearchDataLoader = async (searchText: string, functionType: FUNCTION_TYPE) => { + // await handleSearch(searchText, functionType, "DATA_LOADER"); + }; + + const handleSearchChunker = async (searchText: string, functionType: FUNCTION_TYPE) => { + // await handleSearch(searchText, functionType, "CHUNKER"); + }; + const updateCurrentArtifactLocation = async (artifacts: UpdatedArtifactsResponse) => { console.log(">>> Updating current artifact location", { artifacts }); // Get the updated component and update the location @@ -747,6 +925,16 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { await handleVectorKnowledgeBaseAdded(); return; } + if (isCreatingNewDataLoader.current) { + isCreatingNewDataLoader.current = false; + await handleDataLoaderAdded(); + return; + } + if (isCreatingNewChunker.current) { + isCreatingNewChunker.current = false; + await handleChunkerAdded(); + return; + } } await rpcClient.getVisualizerRpcClient().openView({ type: EVENT_TYPE.UPDATE_PROJECT_LOCATION, @@ -756,7 +944,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { identifier: currentIdentifier, }, }); - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); debouncedGetFlowModel(); }; @@ -942,6 +1130,44 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { }); break; + case "DATA_LOADERS": + setShowProgressIndicator(true); + rpcClient + .getBIDiagramRpcClient() + .getAvailableDataLoaders({ + position: targetRef.current.startLine, + filePath: model?.fileName || fileName, + }) + .then((response) => { + console.log(">>> List of data loaders", response); + setCategories(convertDataLoaderCategoriesToSidePanelCategories(response.categories as Category[])); + setSidePanelView(SidePanelView.DATA_LOADER_LIST); + setShowSidePanel(true); + }) + .finally(() => { + setShowProgressIndicator(false); + }); + break; + + case "CHUNKERS": + setShowProgressIndicator(true); + rpcClient + .getBIDiagramRpcClient() + .getAvailableChunkers({ + position: targetRef.current.startLine, + filePath: model?.fileName || fileName, + }) + .then((response) => { + console.log(">>> List of chunkers", response); + setCategories(convertChunkerCategoriesToSidePanelCategories(response.categories as Category[])); + setSidePanelView(SidePanelView.CHUNKER_LIST); + setShowSidePanel(true); + }) + .finally(() => { + setShowProgressIndicator(false); + }); + break; + default: // default node console.log(">>> on select panel node", { nodeId, metadata }); @@ -974,17 +1200,41 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } }; - const handleOnFormSubmit = (updatedNode?: FlowNode, openInDataMapper?: boolean) => { + const handleOnSelectNewConnection = async (nodeId: string, metadata?: any) => { + // Push current state to navigation stack before navigating + pushToNavigationStack(sidePanelView, categories, selectedNodeRef.current, selectedClientName.current); + setShowProgressIndicator(true); + + try { + const { flowNode, connectionKind } = await getNodeTemplateForConnection( + nodeId, + metadata, + targetRef.current, + model?.fileName, + rpcClient + ); + + nodeTemplateRef.current = flowNode; + setSelectedConnectionKind(connectionKind as ConnectionKind); + setSidePanelView(SidePanelView.CONNECTION_CREATE); + setShowSidePanel(true); + } finally { + setShowProgressIndicator(false); + } + }; + + const handleOnFormSubmit = (updatedNode?: FlowNode, openInDataMapper?: boolean, options?: FormSubmitOptions) => { if (!updatedNode) { console.log(">>> No updated node found"); updatedNode = selectedNodeRef.current; debouncedGetFlowModel(); } setShowProgressIndicator(true); + const noFormSubmitOptions = !options || !(options?.shouldCloseSidePanel || options?.updateLineRangeForRecursiveInserts); if (openInDataMapper) { rpcClient - .getInlineDataMapperRpcClient() + .getDataMapperRpcClient() .getInitialIDMSource({ filePath: model.fileName, flowNode: updatedNode, @@ -1027,18 +1277,31 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { .then(async (response) => { console.log(">>> Updated source code", response); if (response.artifacts.length > 0) { - // If the selected model is the default WSO2 model provider, configure it if (updatedNode?.codedata?.symbol === GET_DEFAULT_MODEL_PROVIDER) { await rpcClient.getAIAgentRpcClient().configureDefaultModelProvider(); } - selectedNodeRef.current = undefined; - await updateCurrentArtifactLocation(response); + if (noFormSubmitOptions) { + selectedNodeRef.current = undefined; + await updateCurrentArtifactLocation(response); + } + if (options?.shouldCloseSidePanel) { + selectedNodeRef.current = undefined; + closeSidePanelAndFetchUpdatedFlowModel(); + } + if (options?.updateLineRangeForRecursiveInserts) { + onRerenderRef.current = options.updateLineRangeForRecursiveInserts; + } + shouldUpdateLineRangeRef.current = options?.shouldUpdateTargetLine; + updatedNodeRef.current = updatedNode; } else { console.error(">>> Error updating source code", response); } }) .finally(() => { setShowProgressIndicator(false); + if (options?.shouldCloseSidePanel === true) { + setShowSidePanel(false); + } }); }; @@ -1068,7 +1331,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { await updateCurrentArtifactLocation(deleteNodeResponse); selectedNodeRef.current = undefined; - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); setShowProgressIndicator(false); debouncedGetFlowModel(); }; @@ -1116,7 +1379,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { if (response.artifacts.length > 0) { selectedNodeRef.current = undefined; await updateCurrentArtifactLocation(response); - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); } else { console.error(">>> Error updating source code", response); } @@ -1131,6 +1394,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } else { topNodeRef.current = undefined; targetRef.current = node.codedata.lineRange; + setTargetLineRange(node.codedata.lineRange) } if (!targetRef.current) { return; @@ -1193,6 +1457,22 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { ); setCategories([]); setSidePanelView(SidePanelView.EMBEDDING_PROVIDER_LIST); + } else if (sidePanelView === SidePanelView.DATA_LOADERS) { + handleOnSelectNode( + selectedNodeMetadata.current.nodeId, + selectedNodeMetadata.current.metadata, + selectedNodeMetadata.current.fileName + ); + setCategories([]); + setSidePanelView(SidePanelView.DATA_LOADER_LIST); + } else if (sidePanelView === SidePanelView.CHUNKERS) { + handleOnSelectNode( + selectedNodeMetadata.current.nodeId, + selectedNodeMetadata.current.metadata, + selectedNodeMetadata.current.fileName + ); + setCategories([]); + setSidePanelView(SidePanelView.CHUNKER_LIST); } else if ( sidePanelView === SidePanelView.FORM && selectedNodeMetadata.current.metadata.node.codedata.node === "VECTOR_KNOWLEDGE_BASE" @@ -1211,7 +1491,9 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { sidePanelView === SidePanelView.MODEL_PROVIDER_LIST || sidePanelView === SidePanelView.VECTOR_STORE_LIST || sidePanelView === SidePanelView.EMBEDDING_PROVIDER_LIST || - sidePanelView === SidePanelView.VECTOR_KNOWLEDGE_BASE_LIST + sidePanelView === SidePanelView.VECTOR_KNOWLEDGE_BASE_LIST || + sidePanelView === SidePanelView.DATA_LOADER_LIST || + sidePanelView === SidePanelView.CHUNKER_LIST ) { setCategories(initialCategoriesRef.current); setSidePanelView(SidePanelView.NODE_LIST); @@ -1401,6 +1683,62 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { }); }; + const handleOnAddNewDataLoader = () => { + console.log(">>> Adding new data loader"); + isCreatingNewDataLoader.current = true; + setShowProgressIndicator(true); + + // Push current state to navigation stack + pushToNavigationStack(sidePanelView, categories, selectedNodeRef.current, selectedClientName.current); + + // Use search to get available data loader types + rpcClient + .getBIDiagramRpcClient() + .search({ + position: { startLine: targetRef.current.startLine, endLine: targetRef.current.endLine }, + filePath: model?.fileName, + queryMap: undefined, + searchKind: "DATA_LOADER", + }) + .then((response) => { + console.log(">>> Available data loader types", response); + setCategories(convertDataLoaderCategoriesToSidePanelCategories(response.categories as Category[])); + setSidePanelView(SidePanelView.DATA_LOADERS); + setShowSidePanel(true); + }) + .finally(() => { + setShowProgressIndicator(false); + }); + }; + + const handleOnAddNewChunker = () => { + console.log(">>> Adding new chunker"); + isCreatingNewChunker.current = true; + setShowProgressIndicator(true); + + // Push current state to navigation stack + pushToNavigationStack(sidePanelView, categories, selectedNodeRef.current, selectedClientName.current); + + // Use search to get available chunker types + rpcClient + .getBIDiagramRpcClient() + .search({ + position: { startLine: targetRef.current.startLine, endLine: targetRef.current.endLine }, + filePath: model?.fileName, + queryMap: undefined, + searchKind: "CHUNKER", + }) + .then((response) => { + console.log(">>> Available chunker types", response); + setCategories(convertChunkerCategoriesToSidePanelCategories(response.categories as Category[])); + setSidePanelView(SidePanelView.CHUNKERS); + setShowSidePanel(true); + }) + .finally(() => { + setShowProgressIndicator(false); + }); + }; + const handleOnGoToSource = (node: FlowNode) => { const targetPosition: NodePosition = { startLine: node.codedata.lineRange.startLine.line, @@ -1450,7 +1788,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { applyModifications(rpcClient, modifications); // clear diagram - handleOnCloseSidePanel(); + closeSidePanelAndFetchUpdatedFlowModel(); onDiscardSuggestions(); }; @@ -1490,11 +1828,19 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { }; // AI Agent callback handlers - const handleOnEditAgentModel = (node: FlowNode) => { - console.log(">>> Edit agent model called", node); - selectedNodeRef.current = node; + const handleOnEditAgentModel = async (agentCallNode: FlowNode) => { + console.log(">>> Edit agent model called", agentCallNode); + + const moduleNodes = await rpcClient.getBIDiagramRpcClient().getModuleNodes(); + const agentNode = moduleNodes.flowModel.connections.find((node) => node.properties.variable.value === agentCallNode.properties.connection.value); + if (!agentNode) { + console.error(`Agent node not found`, agentCallNode); + } + + selectedNodeRef.current = agentNode; showEditForm.current = true; - setSidePanelView(SidePanelView.AGENT_MODEL); + setSelectedConnectionKind('MODEL_PROVIDER'); + setSidePanelView(SidePanelView.CONNECTION_CONFIG); setShowSidePanel(true); }; @@ -1680,6 +2026,17 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { }, 500); }; + const updateNodeWithConnection = async (selectedNode: FlowNode) => { + if (selectedNode.codedata.node === "VECTOR_KNOWLEDGE_BASE") { + setSidePanelView(SidePanelView.FORM); + return; + } + await rpcClient + .getBIDiagramRpcClient() + .getSourceCode({ filePath: projectPath, flowNode: selectedNode }); + closeSidePanelAndFetchUpdatedFlowModel(); + }; + const handleOnDeleteTool = async (tool: ToolData, node: FlowNode) => { console.log(">>> Delete tool called", tool, node); @@ -1737,6 +2094,14 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { }); }; + const handleOnNavigateToPanel = (targetPanel: SidePanelView, connectionKind?: ConnectionKind) => { + if (connectionKind) { + setSelectedConnectionKind(connectionKind); + } + pushToNavigationStack(sidePanelView, categories, selectedNodeRef.current, selectedClientName.current); + setSidePanelView(targetPanel); + }; + const flowModel = originalFlowModel.current && suggestedModel ? suggestedModel : model; const memoizedDiagramProps = useMemo( @@ -1795,13 +2160,15 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { nodeFormTemplate={nodeTemplateRef.current} selectedClientName={selectedClientName.current} showEditForm={showEditForm.current} - targetLineRange={targetRef.current} + targetLineRange={targetLineRange} connections={model?.connections} fileName={model?.fileName} projectPath={projectPath} editForm={showEditForm.current} updatedExpressionField={updatedExpressionField} canGoBack={navigationStack.length > 0} + selectedConnectionKind={selectedConnectionKind} + setSidePanelView={setSidePanelView} // Regular callbacks onClose={handleOnCloseSidePanel} onBack={handleOnFormBack} @@ -1815,6 +2182,8 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { onAddVectorStore={handleOnAddNewVectorStore} onAddEmbeddingProvider={handleOnAddNewEmbeddingProvider} onAddVectorKnowledgeBase={handleOnAddNewVectorKnowledgeBase} + onAddDataLoader={handleOnAddNewDataLoader} + onAddChunker={handleOnAddNewChunker} onSubmitForm={handleOnFormSubmit} showProgressIndicator={showProgressIndicator} onDiscardSuggestions={onDiscardSuggestions} @@ -1827,13 +2196,18 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { onSearchVectorStore={handleSearchVectorStore} onSearchEmbeddingProvider={handleSearchEmbeddingProvider} onSearchVectorKnowledgeBase={handleSearchVectorKnowledgeBase} + onSearchDataLoader={handleSearchDataLoader} + onSearchChunker={handleSearchChunker} + onUpdateNodeWithConnection={updateNodeWithConnection} // AI Agent specific callbacks onEditAgent={handleEditAgent} onSelectTool={handleOnSelectTool} onDeleteTool={handleOnDeleteTool} onAddTool={handleOnAddTool} onAddMcpServer={handleOnAddMcpServer} + onSelectNewConnection={handleOnSelectNewConnection} selectedMcpToolkitName={selectedMcpToolkitName} + onNavigateToPanel={handleOnNavigateToPanel} /> ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/utils.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/utils.ts index deaab7daf45..f886145ed02 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/utils.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/utils.ts @@ -85,3 +85,36 @@ export const findFunctionByName = (components: BallerinaProjectComponents, funct } return null; }; + +export const getNodeTemplateForConnection = async ( + nodeId: string, + metadata: any, + targetRef: any, + modelFileName: string | undefined, + rpcClient: any +) => { + const { node } = metadata as { node: AvailableNode }; + + const response = await rpcClient + .getBIDiagramRpcClient() + .getNodeTemplate({ + position: targetRef?.startLine || { line: 0, offset: 0 }, + filePath: modelFileName, + id: node.codedata, + }); + + const flowNode = response.flowNode; + flowNode.metadata = node.metadata; + + let connectionKind: string; + switch (nodeId) { + case "MODEL_PROVIDER": + case "CLASS_INIT": + connectionKind = 'MODEL_PROVIDER'; + break; + default: + connectionKind = nodeId; + } + + return { flowNode, connectionKind }; +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FocusFlowDiagram/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FocusFlowDiagram/index.tsx index a1d3d6fa42d..c050f301dd2 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FocusFlowDiagram/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FocusFlowDiagram/index.tsx @@ -37,7 +37,6 @@ import { VisualizerLocation, CurrentBreakpointsResponse as BreakpointInfo, ParentPopupData, - FocusFlowDiagramView, ExpressionProperty, TRIGGER_CHARACTERS, TriggerCharacter, @@ -46,7 +45,7 @@ import { UpdatedArtifactsResponse } from "@wso2/ballerina-core"; import { PanelContainer } from "@wso2/ballerina-side-panel"; -import { ModelConfig } from "../AIChatAgent/ModelConfig"; +import { ConnectionConfig, ConnectionCreator, ConnectionSelectionList } from "../../../components/ConnectionSelector"; import { addDraftNodeToDiagram, @@ -54,12 +53,14 @@ import { convertBICategoriesToSidePanelCategories, getFlowNodeForNaturalFunction, calculateExpressionOffsets, - isNaturalFunction, updateLineRange, } from "../../../utils/bi"; -import { NodePosition, STNode } from "@wso2/syntax-tree"; +import { getNodeTemplateForConnection } from "../FlowDiagram/utils"; +import { NodePosition } from "@wso2/syntax-tree"; import { View, ProgressRing, ProgressIndicator, ThemeColors, CompletionItem } from "@wso2/ui-toolkit"; import { EXPRESSION_EXTRACTION_REGEX } from "../../../constants"; +import { ConnectionKind } from "../../../components/ConnectionSelector"; +import { SidePanelView } from "../FlowDiagram/PanelManager"; const Container = styled.div` width: 100%; @@ -88,8 +89,10 @@ export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { const [suggestedModel, setSuggestedModel] = useState(); const [showProgressIndicator, setShowProgressIndicator] = useState(false); const [breakpointInfo, setBreakpointInfo] = useState(); - const [showModelConfigPanel, setShowModelConfigPanel] = useState(false); - const [selectedNodeForModelConfig, setSelectedNodeForModelConfig] = useState(); + const [showConnectionPanel, setShowConnectionPanel] = useState(false); + const [selectedNodeForConnection, setSelectedNodeForConnection] = useState(); + const [selectedConnectionKind, setSelectedConnectionKind] = useState(); + const [connectionView, setConnectionView] = useState(); const selectedNodeRef = useRef(); const nodeTemplateRef = useRef(); @@ -567,23 +570,58 @@ export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { handleExpressionEditorCancel(); }; - const handleOnEditAgentModel = (node: FlowNode) => { - console.log(">>> on edit agent model", node); - setSelectedNodeForModelConfig(node); - setShowModelConfigPanel(true); + const handleOnEditNPFunctionModel = (node: FlowNode) => { + console.log(">>> on edit np function model provider", node); + setSelectedNodeForConnection(node); + setSelectedConnectionKind('MODEL_PROVIDER'); + setConnectionView(SidePanelView.CONNECTION_CONFIG); + setShowConnectionPanel(true); }; - const handleSaveModelConfigPanel = (response: UpdatedArtifactsResponse) => { - setShowModelConfigPanel(false); - setSelectedNodeForModelConfig(undefined); - if (response) { - updateCurrentArtifactLocation(response); + const handleCloseConnectionPanel = () => { + setShowConnectionPanel(false); + setSelectedNodeForConnection(undefined); + getFlowModel(); + }; + + const handleCreateNewConnection = () => { + setConnectionView(SidePanelView.CONNECTION_SELECT); + }; + + const handleSelectConnection = async (nodeId: string, metadata?: any) => { + setShowProgressIndicator(true); + + try { + const { flowNode, connectionKind } = await getNodeTemplateForConnection( + nodeId, + metadata, + targetRef.current, + model?.fileName, + rpcClient + ); + + nodeTemplateRef.current = flowNode; + setSelectedConnectionKind(connectionKind as ConnectionKind); + setConnectionView(SidePanelView.CONNECTION_CREATE); + } finally { + setShowProgressIndicator(false); } }; - const handleCloseModelConfigPanel = () => { - setShowModelConfigPanel(false); - setSelectedNodeForModelConfig(undefined); + const handleUpdateNodeWithConnection = async (selectedNode: FlowNode) => { + setShowProgressIndicator(true); + + const clonedNode = structuredClone(selectedNode); + + const isNpFunction = clonedNode.codedata.node === "NP_FUNCTION"; + if (isNpFunction) + clonedNode.codedata.node = "NP_FUNCTION_DEFINITION"; + + await rpcClient + .getBIDiagramRpcClient() + .getSourceCode({ filePath: model?.fileName, flowNode: clonedNode, isFunctionNodeUpdate: isNpFunction }); + handleCloseConnectionPanel(); + setShowProgressIndicator(false); }; const memoizedDiagramProps = useMemo( @@ -607,7 +645,7 @@ export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { onCancel: handleExpressionEditorCancel }, aiNodes: { - onModelSelect: handleOnEditAgentModel, + onModelSelect: handleOnEditNPFunctionModel, }, }), [flowModel, projectPath, breakpointInfo, filteredCompletions] @@ -629,16 +667,38 @@ export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { - {showModelConfigPanel && selectedNodeForModelConfig && ( + {showConnectionPanel && selectedNodeForConnection && ( setConnectionView(SidePanelView.CONNECTION_CONFIG) : + connectionView === SidePanelView.CONNECTION_CREATE ? () => setConnectionView(SidePanelView.CONNECTION_SELECT) : + undefined} > - + {connectionView === SidePanelView.CONNECTION_CONFIG && ( + + )} + {connectionView === SidePanelView.CONNECTION_SELECT && ( + + )} + {connectionView === SidePanelView.CONNECTION_CREATE && ( + + )} )} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/DeclareVariableForm/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/DeclareVariableForm/index.tsx new file mode 100644 index 00000000000..cdfaedf47de --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/DeclareVariableForm/index.tsx @@ -0,0 +1,57 @@ +/** + * 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 { + Form, + FormProps, +} from "@wso2/ballerina-side-panel"; +import { CompletionItem } from "@wso2/ui-toolkit"; +import { useEffect, useState } from "react"; + + +export const VariableForm = (props: FormProps) => { + const { handleSelectedTypeChange } = props; + const [formFields, setFormFields] = useState(props.formFields); + + useEffect(() => { + setFormFields(props.formFields); + }, [props.formFields]); + + const handleOnTypeChange = (type: CompletionItem) => { + updateExpressionValueTypeConstraint(type?.value || ''); + handleSelectedTypeChange(type); + }; + + const updateExpressionValueTypeConstraint = (valueTypeConstraint: string) => { + const fieldsWithoutExpression = props.formFields.filter((field) => { + return field.type !== "ACTION_OR_EXPRESSION"; + }); + const expressionField = props.formFields.find((field) => field.type === "ACTION_OR_EXPRESSION"); + if (expressionField) { + expressionField.valueTypeConstraint = valueTypeConstraint; + } + const updatedFields = [...fieldsWithoutExpression, expressionField]; + setFormFields(updatedFields); + } + return ( + <> +
+ + + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx index 6af595ddbaa..87f8f991c06 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx @@ -19,7 +19,6 @@ import { RefObject, useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "react"; import { EVENT_TYPE, - ColorThemeKind, FlowNode, LineRange, NodePosition, @@ -36,7 +35,9 @@ import { RecordTypeField, Imports, CodeData, - VisualizableField + VisualizableField, + Member, + TypeNodeKind } from "@wso2/ballerina-core"; import { FormField, @@ -80,12 +81,19 @@ import { updateNodeWithProperties, } from "../form-utils"; import ForkForm from "../ForkForm"; -import { getHelperPane } from "../../HelperPane"; import { FormTypeEditor } from "../../TypeEditor"; import { getTypeHelper } from "../../TypeHelper"; import { EXPRESSION_EXTRACTION_REGEX } from "../../../../constants"; import MatchForm from "../MatchForm"; +import { FormSubmitOptions } from "../../FlowDiagram"; +import { getHelperPaneNew } from "../../HelperPaneNew"; +import { VariableForm } from "../DeclareVariableForm"; import VectorKnowledgeBaseForm from "../VectorKnowledgeBaseForm"; +import { EditorContext, StackItem } from "@wso2/type-editor"; +import DynamicModal from "../../../../components/Modal"; +import React from "react"; +import { SidePanelView } from "../../FlowDiagram/PanelManager"; +import { ConnectionKind } from "../../../../components/ConnectionSelector"; interface TypeEditorState { isOpen: boolean; @@ -116,11 +124,14 @@ interface FormProps { description?: string; // Optional description explaining what the action button does callback: () => void; }; + handleOnFormSubmit?: (updatedNode?: FlowNode, openInDataMapper?: boolean, options?: FormSubmitOptions) => void; + isInModal?: boolean; scopeFieldAddon?: React.ReactNode; newServerUrl?: string; onChange?: (fieldKey: string, value: any, allValues: FormValues) => void; mcpTools?: { name: string; description?: string }[]; onToolsChange?: (selectedTools: string[]) => void; + navigateToPanel?: (panel: SidePanelView, connectionKind?: ConnectionKind) => void; } // Styled component for the action button description @@ -146,6 +157,30 @@ const StyledActionButton = styled(Button)` } `; +export const BreadcrumbContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; + padding: 8px 20px; + background: ${ThemeColors.SURFACE_CONTAINER}; + border-bottom: 1px solid ${ThemeColors.OUTLINE_VARIANT}; +`; + +export const BreadcrumbItem = styled.span` + color: ${ThemeColors.ON_SURFACE_VARIANT}; + font-size: var(--vscode-font-size); + + &:last-child { + color: ${ThemeColors.ON_SURFACE}; + font-weight: 500; + } +`; + +export const BreadcrumbSeparator = styled.span` + color: ${ThemeColors.ON_SURFACE_VARIANT}; + font-size: var(--vscode-font-size); +`; + export const FormGenerator = forwardRef(function FormGenerator(props: FormProps, ref: React.ForwardedRef) { const { fileName, @@ -165,6 +200,8 @@ export const FormGenerator = forwardRef(func disableSaveButton, actionButtonConfig, submitText, + handleOnFormSubmit, + isInModal, scopeFieldAddon, newServerUrl, onChange, @@ -186,8 +223,58 @@ export const FormGenerator = forwardRef(func const [types, setTypes] = useState([]); const [filteredTypes, setFilteredTypes] = useState([]); const expressionOffsetRef = useRef(0); // To track the expression offset on adding import statements + + const [selectedType, setSelectedType] = useState(null); + const importsCodedataRef = useRef(null); // To store codeData for getVisualizableFields + //stack for recursive type creation + const [stack, setStack] = useState([{ + isDirty: false, + type: undefined + }]); + const [refetchStates, setRefetchStates] = useState([false]); + + const pushTypeStack = (item: StackItem) => { + setStack((prev) => [...prev, item]); + setRefetchStates((prev) => [...prev, false]); + }; + + const popTypeStack = () => { + setStack((prev) => prev.slice(0, -1)); + setRefetchStates((prev) => { + const newStates = [...prev]; + const currentState = newStates.pop(); + if (currentState && newStates.length > 0) { + newStates[newStates.length - 1] = true; + } + return newStates; + }); + }; + + const peekTypeStack = (): StackItem | null => { + return stack.length > 0 ? stack[stack.length - 1] : null; + }; + + const replaceTop = (item: StackItem) => { + if (stack.length === 0) return; + setStack((prev) => { + const newStack = [...prev]; + newStack[newStack.length - 1] = item; + return newStack; + }); + } + + const setRefetchForCurrentModal = (shouldRefetch: boolean) => { + setRefetchStates((prev) => { + const newStates = [...prev]; + if (newStates.length > 0) { + newStates[newStates.length - 1] = shouldRefetch; + } + return newStates; + }); + }; + useEffect(() => { if (rpcClient) { // Set current theme @@ -269,7 +356,7 @@ export const FormGenerator = forwardRef(func if (node.codedata.node === "VARIABLE") { const codedata = importsCodedataRef.current || { symbol: formProperties?.type.value }; rpcClient - .getInlineDataMapperRpcClient() + .getDataMapperRpcClient() .getVisualizableFields({ filePath: fileName, codedata }) .then((res) => { setVisualizableField(res.visualizableProperties); @@ -345,6 +432,17 @@ export const FormGenerator = forwardRef(func setTypeEditorState({ isOpen, fieldKey: editingField?.key, newTypeValue: f[editingField?.key] }); }; + const handleTypeEditorStateChange = (state: boolean) => { + if (!state) { + if (stack.length > 1) { + popTypeStack(); + return; + } + stack[0].type = undefined + } + setTypeEditorState({ isOpen: state }); + } + const handleUpdateImports = (key: string, imports: Imports, codedata?: CodeData) => { importsCodedataRef.current = codedata; const importKey = Object.keys(imports)?.[0]; @@ -605,7 +703,7 @@ export const FormGenerator = forwardRef(func }; const onTypeEditorClosed = () => { - setTypeEditorState({ isOpen: false }); + setTypeEditorState({ isOpen: stack.length !== 0 }); }; const onTypeChange = async (type: Type) => { @@ -616,7 +714,7 @@ export const FormGenerator = forwardRef(func return field; }); setFields(updatedFields); - setTypeEditorState({ isOpen: false }); + setTypeEditorState({ isOpen: true }); }; const handleGetHelperPane = ( @@ -629,7 +727,8 @@ export const FormGenerator = forwardRef(func changeHelperPaneState: (isOpen: boolean) => void, helperPaneHeight: HelperPaneHeight, recordTypeField?: RecordTypeField, - isAssignIdentifier?: boolean + isAssignIdentifier?: boolean, + valueTypeConstraint?: string, ) => { const handleHelperPaneClose = () => { debouncedRetrieveCompletions.cancel(); @@ -637,7 +736,7 @@ export const FormGenerator = forwardRef(func handleExpressionEditorCancel(); } - return getHelperPane({ + return getHelperPaneNew({ fieldKey: fieldKey, fileName: fileName, targetLineRange: updateLineRange(targetLineRange, expressionOffsetRef.current), @@ -650,7 +749,15 @@ export const FormGenerator = forwardRef(func helperPaneHeight: helperPaneHeight, recordTypeField: recordTypeField, isAssignIdentifier: isAssignIdentifier, - updateImports: handleUpdateImports + updateImports: handleUpdateImports, + completions: completions, + projectPath: projectPath, + handleOnFormSubmit: handleOnFormSubmit, + selectedType: selectedType, + filteredCompletions: filteredCompletions, + isInModal: isInModal, + valueTypeConstraint: valueTypeConstraint, + handleRetrieveCompletions: handleRetrieveCompletions, }); }; @@ -665,6 +772,7 @@ export const FormGenerator = forwardRef(func changeHelperPaneState: (isOpen: boolean) => void, typeHelperHeight: HelperPaneHeight, onTypeCreate: () => void, + exprRef?: RefObject, ) => { const handleCreateNewType = (typeName: string) => { onTypeCreate(); @@ -690,7 +798,8 @@ export const FormGenerator = forwardRef(func changeTypeHelperState: changeHelperPaneState, updateImports: handleUpdateImports, onTypeCreate: handleCreateNewType, - onCloseCompletions: handleCloseCompletions + onCloseCompletions: handleCloseCompletions, + exprRef: exprRef, }); } @@ -709,8 +818,9 @@ export const FormGenerator = forwardRef(func onCompletionItemSelect: handleCompletionItemSelect, onBlur: handleExpressionEditorBlur, onCancel: handleExpressionEditorCancel, - helperPaneOrigin: "left", - helperPaneHeight: "full", + helperPaneOrigin: "vertical", + helperPaneHeight: "default", + helperPaneZIndex: isInModal ? 40001 : undefined, } as FormExpressionEditorProps; }, [ filteredCompletions, @@ -729,16 +839,59 @@ export const FormGenerator = forwardRef(func const fetchVisualizableFields = async (filePath: string, typeName?: string) => { const codedata = importsCodedataRef.current || { symbol: typeName }; const res = await rpcClient - .getInlineDataMapperRpcClient() - .getVisualizableFields({ filePath, codedata}); + .getDataMapperRpcClient() + .getVisualizableFields({ filePath, codedata }); setVisualizableField(res.visualizableProperties); importsCodedataRef.current = {}; }; const handleTypeCreate = (typeName?: string) => { - setTypeEditorState({ isOpen: true, newTypeValue: typeName, fieldKey: typeEditorState.fieldKey }); + try { + setTypeEditorState({ isOpen: stack.length !== 0, newTypeValue: typeName, fieldKey: typeEditorState.fieldKey }); + popTypeStack() + } catch (e) { + console.error(e) + } }; + const onSaveType = (type: Type) => { + if (stack.length > 0) { + setRefetchForCurrentModal(true); + popTypeStack(); + } else { + setTypeEditorState({ isOpen: false }); + } + } + + const handleSelectedTypeChange = (type: CompletionItem) => { + setSelectedType(type); + } + + const getNewTypeCreateForm = () => { + pushTypeStack({ + type: { + name: "", + members: [] as Member[], + editable: true, + metadata: { + description: "", + label: "" + }, + properties: {}, + codedata: { + node: "RECORD" as TypeNodeKind + }, + includes: [] as string[], + allowAdditionalFields: false + }, + isDirty: false + }) + setTypeEditorState({ + isOpen: true, + newTypeValue: "" + }) + } + // handle if node form if (node?.codedata.node === "IF") { return ( @@ -808,6 +961,7 @@ export const FormGenerator = forwardRef(func resetUpdatedExpressionField={resetUpdatedExpressionField} subPanelView={subPanelView} disableSaveButton={disableSaveButton} + navigateToPanel={props.navigateToPanel} /> ); } @@ -834,9 +988,79 @@ export const FormGenerator = forwardRef(func ) : undefined; + // handle declare variable node form + if (node?.codedata.node === "VARIABLE") { + return ( + + + { + stack.map((item, i) => +
+ + {stack.slice(0, i + 1).map((stackItem, index) => ( + + {index > 0 && /} + + {stackItem?.type?.name || "New Type"} + + + ))} + + +
+
) + } +
+ ); + } + // default form return ( - <> + {fields && fields.length > 0 && ( (func recordTypeFields={recordTypeFields} isInferredReturnType={!!node.codedata?.inferredReturnType} formImports={formImports} + handleSelectedTypeChange={handleSelectedTypeChange} preserveOrder={node.codedata.node === "VARIABLE" || node.codedata.node === "CONFIG_VARIABLE"} scopeFieldAddon={scopeFieldAddon} newServerUrl={newServerUrl} @@ -872,18 +1097,41 @@ export const FormGenerator = forwardRef(func onToolsChange={props.onToolsChange} /> )} - {typeEditorState.isOpen && ( - - - - )} - + { + stack.map((item, i) => +
+ + {stack.slice(0, i + 1).map((stackItem, index) => ( + + {index > 0 && /} + + {stackItem?.type?.name || "New Type"} + + + ))} + + +
+
) + } +
); }); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGeneratorNew/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGeneratorNew/index.tsx index 9751e11b995..2cfd62adf27 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGeneratorNew/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGeneratorNew/index.tsx @@ -32,7 +32,10 @@ import { RecordTypeField, FormDiagnostics, Imports, - CodeData + CodeData, + LinePosition, + TypeNodeKind, + Member } from "@wso2/ballerina-core"; import { FormField, @@ -57,11 +60,15 @@ import { removeDuplicateDiagnostics, updateLineRange } from "../../../../utils/bi"; -import { debounce, set } from "lodash"; -import { getHelperPane } from "../../HelperPane"; +import { debounce } from "lodash"; import { FormTypeEditor } from "../../TypeEditor"; import { getTypeHelper } from "../../TypeHelper"; import { EXPRESSION_EXTRACTION_REGEX } from "../../../../constants"; +import { getHelperPaneNew } from "../../HelperPaneNew"; +import React from "react"; +import { BreadcrumbContainer, BreadcrumbItem, BreadcrumbSeparator } from "../FormGenerator"; +import { EditorContext, StackItem } from "@wso2/type-editor"; +import DynamicModal from "../../../../components/Modal"; interface TypeEditorState { isOpen: boolean; @@ -72,7 +79,7 @@ interface TypeEditorState { interface FormProps { fileName: string; fields: FormField[]; - targetLineRange: LineRange; + targetLineRange?: LineRange; projectPath?: string; submitText?: string; cancelText?: string; @@ -128,11 +135,15 @@ export function FormGeneratorNew(props: FormProps) { concertRequired, description, preserveFieldOrder, - injectedComponents + injectedComponents, } = props; const { rpcClient } = useRpcContext(); + const getAdjustedStartLine = (targetLineRange: LineRange | undefined, expressionOffset: number): LinePosition | undefined => { + return targetLineRange ? updateLineRange(targetLineRange, expressionOffset).startLine : undefined; + }; + const [typeEditorState, setTypeEditorState] = useState({ isOpen: false, newTypeValue: "" }); /* Expression editor related state and ref variables */ @@ -146,6 +157,88 @@ export function FormGeneratorNew(props: FormProps) { const [fieldsValues, setFields] = useState(fields); const [formImports, setFormImports] = useState({}); + const [selectedType, setSelectedType] = useState(null); + const [refetchStates, setRefetchStates] = useState([false]); + //stack for recursive type creation + const [stack, setStack] = useState([{ + isDirty: false, + type: undefined + }]); + + const pushTypeStack = (item: StackItem) => { + setStack((prev) => [...prev, item]); + setRefetchStates((prev) => [...prev, false]); + }; + + const popTypeStack = () => { + setStack((prev) => prev.slice(0, -1)); + setRefetchStates((prev) => { + const newStates = [...prev]; + const currentState = newStates.pop(); + if (currentState && newStates.length > 0) { + newStates[newStates.length - 1] = true; + } + return newStates; + }); + }; + + const peekTypeStack = (): StackItem | null => { + return stack.length > 0 ? stack[stack.length - 1] : null; + }; + + const replaceTop = (item: StackItem) => { + if (stack.length === 0) return; + setStack((prev) => { + const newStack = [...prev]; + newStack[newStack.length - 1] = item; + return newStack; + }); + } + + const setRefetchForCurrentModal = (shouldRefetch: boolean) => { + setRefetchStates((prev) => { + const newStates = [...prev]; + if (newStates.length > 0) { + newStates[newStates.length - 1] = shouldRefetch; + } + return newStates; + }); + }; + + const defaultType = (): Type => { + if (typeEditorState.field?.type === 'PARAM_MANAGER') { + return { + name: typeEditorState.newTypeValue || "MyType", + editable: true, + metadata: { + label: "", + description: "", + }, + codedata: { + node: "RECORD", + }, + properties: {}, + members: [], + includes: [] as string[], + allowAdditionalFields: false + }; + } return { + name: typeEditorState.newTypeValue || "MyType", + editable: true, + metadata: { + label: "", + description: "" + }, + codedata: { + node: "CLASS" + }, + properties: {}, + members: [], + includes: [] as string[], + functions: [] + }; + } + useEffect(() => { if (rpcClient) { @@ -244,7 +337,7 @@ export function FormGeneratorNew(props: FormProps) { filePath: fileName, context: { expression: value, - startLine: updateLineRange(targetLineRange, expressionOffsetRef.current).startLine, + startLine: getAdjustedStartLine(targetLineRange, expressionOffsetRef.current), lineOffset: lineOffset, offset: charOffset, codedata: undefined, @@ -314,7 +407,7 @@ export function FormGeneratorNew(props: FormProps) { if (!types.length) { const types = await rpcClient.getBIDiagramRpcClient().getVisibleTypes({ filePath: fileName, - position: updateLineRange(targetLineRange, expressionOffsetRef.current).startLine, + position: getAdjustedStartLine(targetLineRange, expressionOffsetRef.current), ...(valueTypeConstraint && { typeConstraint: valueTypeConstraint }) }); @@ -397,7 +490,7 @@ export function FormGeneratorNew(props: FormProps) { filePath: fileName, context: { expression: expression, - startLine: updateLineRange(targetLineRange, expressionOffsetRef.current).startLine, + startLine: getAdjustedStartLine(targetLineRange, expressionOffsetRef.current), lineOffset: 0, offset: 0, codedata: field.codedata, @@ -408,7 +501,7 @@ export function FormGeneratorNew(props: FormProps) { let uniqueDiagnostics = removeDuplicateDiagnostics(response.diagnostics); // HACK: filter unknown module and undefined type diagnostics for local connections uniqueDiagnostics = filterUnsupportedDiagnostics(uniqueDiagnostics); - + setDiagnosticsInfo({ key, diagnostics: uniqueDiagnostics }); } } catch (error) { @@ -433,17 +526,19 @@ export function FormGeneratorNew(props: FormProps) { changeHelperPaneState: (isOpen: boolean) => void, helperPaneHeight: HelperPaneHeight, recordTypeField?: RecordTypeField, - isAssignIdentifier?: boolean + isAssignIdentifier?: boolean, + valueTypeConstraint?: string, ) => { const handleHelperPaneClose = () => { + debouncedRetrieveCompletions.cancel(); changeHelperPaneState(false); handleExpressionEditorCancel(); } - return getHelperPane({ + return getHelperPaneNew({ fieldKey: fieldKey, fileName: fileName, - targetLineRange: updateLineRange(targetLineRange, expressionOffsetRef.current), + targetLineRange: targetLineRange ? updateLineRange(targetLineRange, expressionOffsetRef.current) : undefined, exprRef: exprRef, anchorRef: anchorRef, onClose: handleHelperPaneClose, @@ -453,7 +548,14 @@ export function FormGeneratorNew(props: FormProps) { helperPaneHeight: helperPaneHeight, recordTypeField: recordTypeField, isAssignIdentifier: isAssignIdentifier, - updateImports: handleUpdateImports + updateImports: handleUpdateImports, + completions: completions, + projectPath: projectPath, + selectedType: selectedType, + filteredCompletions: filteredCompletions, + isInModal: false, + valueTypeConstraint: valueTypeConstraint, + handleRetrieveCompletions: handleRetrieveCompletions, }); }; @@ -468,6 +570,7 @@ export function FormGeneratorNew(props: FormProps) { changeHelperPaneState: (isOpen: boolean) => void, typeHelperHeight: HelperPaneHeight, onTypeCreate: () => void, + exprRef: RefObject, ) => { const formField = fieldsValues.find(f => f.key === fieldKey); const handleCreateNewType = (typeName: string) => { @@ -485,7 +588,7 @@ export function FormGeneratorNew(props: FormProps) { valueTypeConstraint: valueTypeConstraint, typeBrowserRef: typeBrowserRef, filePath: fileName, - targetLineRange: updateLineRange(targetLineRange, expressionOffsetRef.current), + targetLineRange: targetLineRange ? updateLineRange(targetLineRange, expressionOffsetRef.current) : undefined, currentType: currentType, currentCursorPosition: currentCursorPosition, helperPaneHeight: typeHelperHeight, @@ -494,12 +597,13 @@ export function FormGeneratorNew(props: FormProps) { changeTypeHelperState: changeHelperPaneState, updateImports: handleUpdateImports, onTypeCreate: handleCreateNewType, - onCloseCompletions: handleCloseCompletions + onCloseCompletions: handleCloseCompletions, + exprRef: exprRef, }); } const handleTypeChange = async (type: Type) => { - setTypeEditorState({ isOpen: false }); + setTypeEditorState({ isOpen: true }); if (typeEditorState.field) { const updatedFields = fieldsValues.map(field => { @@ -559,52 +663,48 @@ export function FormGeneratorNew(props: FormProps) { } } - const defaultType = (): Type => { - if (typeEditorState.field.type === 'PARAM_MANAGER') { - return { - name: typeEditorState.newTypeValue || "MyType", - editable: true, - metadata: { - label: "", - description: "", - }, - codedata: { - node: "RECORD", - }, - properties: {}, - members: [], - includes: [] as string[], - allowAdditionalFields: false - }; - } - return { - name: typeEditorState.newTypeValue || "MyType", - editable: true, - metadata: { - label: "", - description: "" - }, - codedata: { - node: "CLASS" - }, - properties: {}, - members: [], - includes: [] as string[], - functions: [] - }; + const handleSelectedTypeChange = (type: CompletionItem) => { + setSelectedType(type); } const onCloseTypeEditor = () => { setTypeEditorState({ isOpen: false }); }; + const handleTypeEditorStateChange = (state: boolean) => { + if (!state) { + if (stack.length > 1) { + popTypeStack(); + return; + } + stack[0].type = undefined + } + setTypeEditorState({ isOpen: state }); + } + + const getNewTypeCreateForm = () => { + pushTypeStack({ + type: defaultType(), + isDirty: false + }) + } + + const onSaveType = (type: Type) => { + if (stack.length > 0) { + setRefetchForCurrentModal(true); + popTypeStack(); + } else { + setTypeEditorState({ isOpen: false }); + } + } + const extractArgsFromFunction = async (value: string, property: ExpressionProperty, cursorPosition: number) => { const { lineOffset, charOffset } = calculateExpressionOffsets(value, cursorPosition); const signatureHelp = await rpcClient.getBIDiagramRpcClient().getSignatureHelp({ filePath: fileName, context: { expression: value, - startLine: updateLineRange(targetLineRange, expressionOffsetRef.current).startLine, + startLine: getAdjustedStartLine(targetLineRange, expressionOffsetRef.current), lineOffset: lineOffset, offset: charOffset, codedata: undefined, @@ -634,8 +734,8 @@ export function FormGeneratorNew(props: FormProps) { onCompletionItemSelect: handleCompletionItemSelect, onBlur: handleExpressionEditorBlur, onCancel: handleExpressionEditorCancel, - helperPaneOrigin: helperPaneSide || "right", - helperPaneHeight: "3/4" + helperPaneOrigin: "vertical", + helperPaneHeight: "default", } as FormExpressionEditorProps; }, [ filteredCompletions, @@ -658,28 +758,9 @@ export function FormGeneratorNew(props: FormProps) { setTypeEditorState({ isOpen: true, newTypeValue: typeName, field: typeEditorState.field }); }; - const renderTypeEditor = (isGraphql: boolean) => ( - <> - - - - - - ); - // default form return ( - <> + {fields && fields.length > 0 && ( )} - {typeEditorState.isOpen && ( - renderTypeEditor(isGraphqlEditor) - )} - + { + stack.map((item, i) => +
+ + {stack.slice(0, i + 1).map((stackItem, index) => ( + + {index > 0 && /} + + {stackItem?.type?.name || "New Type"} + + + ))} + + +
+
) + } +
); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/MatchForm/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/MatchForm/index.tsx index 4bc82255f89..055fea307f8 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/MatchForm/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/MatchForm/index.tsx @@ -166,8 +166,8 @@ export function MatchForm(props: MatchFormProps) { useEffect(() => { handleFormOpen(); // set target value - if (node.properties.condition) { - setValue(`branch-${TARGET_FIELD_INDEX}`, node.properties.condition.value); + if (node.properties.matchTarget) { + setValue(`branch-${TARGET_FIELD_INDEX}`, node.properties.matchTarget.value); } // set branch values branches.forEach((branch, index) => { @@ -203,7 +203,7 @@ export function MatchForm(props: MatchFormProps) { } // update target value - updatedNode.properties.condition.value = data[`branch-${TARGET_FIELD_INDEX}`]?.trim(); + updatedNode.properties.matchTarget.value = data[`branch-${TARGET_FIELD_INDEX}`]?.trim(); // Update branches with form values const updatedBranches = branches.map((branch, index) => { @@ -512,7 +512,7 @@ export function MatchForm(props: MatchFormProps) { console.log(">>> Match node", node); const disableSaveButton = !isValid || isValidating || showProgressIndicator; - const targetField = convertNodePropertyToFormField(`branch-${TARGET_FIELD_INDEX}`, node.properties.condition); + const targetField = convertNodePropertyToFormField(`branch-${TARGET_FIELD_INDEX}`, node.properties.matchTarget); targetField.label = "Target"; return ( @@ -520,7 +520,6 @@ export function MatchForm(props: MatchFormProps) { div:first-child { padding: 0px; } `; - export const Divider = styled.div` - height: 1px; - background: ${ThemeColors.OUTLINE_VARIANT}; - margin-bottom: 20px; - `; - export const SpinnerContainer = styled.div` display: flex; justify-content: center; @@ -126,6 +108,7 @@ interface VectorKnowledgeBaseFormProps { resetUpdatedExpressionField?: () => void; subPanelView?: SubPanelView; disableSaveButton?: boolean; + navigateToPanel?: (panel: SidePanelView, connectionKind?: ConnectionKind) => void; } interface ComponentData { @@ -148,61 +131,21 @@ export function VectorKnowledgeBaseForm(props: VectorKnowledgeBaseFormProps) { } = props; const { rpcClient } = useRpcContext(); - const [vectorStoreOptions, setVectorStoreOptions] = useState([]); - const [embeddingProviderOptions, setEmbeddingProviderOptions] = useState([]); - - const [vectorStoreFields, setVectorStoreFields] = useState([]); - const [embeddingProviderFields, setEmbeddingProviderFields] = useState([]); const [knowledgeBaseFields, setKnowledgeBaseFields] = useState([]); const [formImports, setFormImports] = useState({}); - const [isVectorStoreFormValid, setIsVectorStoreFormValid] = useState(false); - const [isEmbeddingProviderFormValid, setIsEmbeddingProviderFormValid] = useState(false); - const [isKnowledgeBaseFormValid, setIsKnowledgeBaseFormValid] = useState(false); - const [componentDataMap, setComponentDataMap] = useState({}); const [isInitialized, setIsInitialized] = useState(false); - const [vectorStoreVariableName, setVectorStoreVariableName] = useState(""); - const [embeddingProviderVariableName, setEmbeddingProviderVariableName] = useState(""); - const [vectorStoreFormValues, setVectorStoreFormValues] = useState({}); - const [embeddingProviderFormValues, setEmbeddingProviderFormValues] = useState({}); + const [isFormValid, setIsFormValid] = useState(true); const [knowledgeBaseFormValues, setKnowledgeBaseFormValues] = useState({}); - const [isEditForm, setIsEditForm] = useState(false); - const [selectedVectorStoreOption, setSelectedVectorStoreOption] = useState(""); - const [selectedEmbeddingProviderOption, setSelectedEmbeddingProviderOption] = useState(""); const [saving, setSaving] = useState(false); - const vectorStoreTemplateRef = useRef(null); - const embeddingProviderTemplateRef = useRef(null); - const vectorStoreLineRange = useRef(null); - const embeddingProviderLineRange = useRef(null); - useEffect(() => { - // Check if this is an edit form - const formProperties = getFormProperties(node); - const vectorStoreValue = formProperties.vectorStore?.value as string; - const embeddingModelValue = formProperties.embeddingModel?.value as string; - const isEdit = !!(vectorStoreValue && embeddingModelValue); - setIsEditForm(isEdit); - - if (isEdit) { - editFormInit(); - } else { - initializeForm(); - fetchAvailableComponents(); - } + initializeForm(); handleFormOpen(); - return () => { handleFormClose(); }; }, []); - // Update knowledge base fields when variable names change - useEffect(() => { - if (vectorStoreVariableName && embeddingProviderVariableName && knowledgeBaseFields.length > 0) { - updateKnowledgeBaseFields(); - } - }, [vectorStoreVariableName, embeddingProviderVariableName]); - const handleFormOpen = () => { rpcClient .getBIDiagramRpcClient() @@ -221,406 +164,43 @@ export function VectorKnowledgeBaseForm(props: VectorKnowledgeBaseFormProps) { }); }; - const initializeForm = () => { - // Initialize knowledge base form fields from the node - const formProperties = getFormProperties(node); - formProperties.vectorStore.hidden = true; - formProperties.embeddingModel.hidden = true; - const fields = convertNodePropertiesToFormFields(formProperties); - setKnowledgeBaseFields(fields); - setFormImports(getImportsForFormFields(fields)); - }; - - const editFormInit = async () => { - try { - // Get the current vector store and embedding model values - const formProperties = getFormProperties(node); - const vectorStoreValue = formProperties.vectorStore?.value; - const embeddingModelValue = formProperties.embeddingModel?.value; - - // Fetch module nodes to find existing components - const moduleNodesResponse = await rpcClient.getBIDiagramRpcClient().getModuleNodes(); - // Find the vector store and embedding provider nodes - let vectorStoreNode: FlowNode | null = null; - let embeddingProviderNode: FlowNode | null = null; - - // Search through module nodes to find matching variable names - moduleNodesResponse.flowModel.connections.forEach((moduleNode) => { - const moduleNodeVariableValue = moduleNode.properties?.variable?.value; - const isStringValue = typeof moduleNodeVariableValue === "string"; - - if (isStringValue && moduleNodeVariableValue === vectorStoreValue) { - if ( - moduleNode.codedata.node === "VECTOR_STORE" || - moduleNode.codedata.object?.includes("VectorStore") - ) { - vectorStoreNode = moduleNode; - vectorStoreTemplateRef.current = moduleNode; - vectorStoreLineRange.current = moduleNode.codedata.lineRange; - setVectorStoreVariableName(vectorStoreValue as string); - } - } - - if (isStringValue && moduleNodeVariableValue === embeddingModelValue) { - if ( - moduleNode.codedata.node === "EMBEDDING_PROVIDER" || - moduleNode.codedata.object?.includes("EmbeddingProvider") || - moduleNode.codedata.symbol?.includes("EmbeddingProvider") - ) { - embeddingProviderNode = moduleNode; - embeddingProviderTemplateRef.current = moduleNode; - embeddingProviderLineRange.current = moduleNode.codedata.lineRange; - setEmbeddingProviderVariableName(embeddingModelValue as string); - } - } - }); - - if (vectorStoreNode && embeddingProviderNode) { - // Process vector store fields - const vectorStoreFormProperties = getFormProperties(vectorStoreNode); - const vectorStoreProps = vectorStoreFormProperties as any; - if (vectorStoreProps.variable) { - vectorStoreProps.variable.hidden = true; - } - if (vectorStoreProps.type) { - vectorStoreProps.type.hidden = true; - } - const vectorStoreFields = convertNodePropertiesToFormFields(vectorStoreFormProperties); - setVectorStoreFields(vectorStoreFields); - - // Set existing vector store form values - const vectorStoreExistingValues: FormValues = {}; - Object.keys(vectorStoreNode.properties || {}).forEach((key) => { - if (key !== "variable" && key !== "type") { - vectorStoreExistingValues[key] = (vectorStoreNode.properties as any)[key].value; - } - }); - setVectorStoreFormValues(vectorStoreExistingValues); - - // Process embedding provider fields - const embeddingProviderFormProperties = getFormProperties(embeddingProviderNode); - const embeddingProviderProps = embeddingProviderFormProperties as any; - if (embeddingProviderProps.variable) { - embeddingProviderProps.variable.hidden = true; - } - if (embeddingProviderProps.type) { - embeddingProviderProps.type.hidden = true; - } - const embeddingProviderFields = convertNodePropertiesToFormFields(embeddingProviderFormProperties); - setEmbeddingProviderFields(embeddingProviderFields); - - // Set existing embedding provider form values - const embeddingProviderExistingValues: FormValues = {}; - Object.keys(embeddingProviderNode.properties || {}).forEach((key) => { - if (key !== "variable" && key !== "type") { - embeddingProviderExistingValues[key] = (embeddingProviderNode.properties as any)[key].value; - } - }); - setEmbeddingProviderFormValues(embeddingProviderExistingValues); - } - - // Initialize knowledge base fields (same as create mode) - const knowledgeBaseProps = formProperties as any; - if (knowledgeBaseProps.vectorStore) { - knowledgeBaseProps.vectorStore.hidden = true; - } - if (knowledgeBaseProps.embeddingModel) { - knowledgeBaseProps.embeddingModel.hidden = true; - } - const knowledgeBaseFields = convertNodePropertiesToFormFields(formProperties); - setKnowledgeBaseFields(knowledgeBaseFields); - setFormImports(getImportsForFormFields(knowledgeBaseFields)); - - // Set existing knowledge base form values - const knowledgeBaseExistingValues: FormValues = {}; - Object.keys(node.properties || {}).forEach((key) => { - if (key !== "vectorStore" && key !== "embeddingModel") { - knowledgeBaseExistingValues[key] = (node.properties as any)[key].value; - } - }); - setKnowledgeBaseFormValues(knowledgeBaseExistingValues); - - // load dropdown options and default selections - fetchAvailableComponentsInEditMode(vectorStoreNode, embeddingProviderNode); - - setIsInitialized(true); - } catch (error) { - console.error("Error initializing edit form:", error); - setIsInitialized(true); // Still set to prevent infinite loading - } - }; - - const fetchAvailableComponents = async () => { - try { - // Fetch available vector stores - const vectorStoreResponse = await rpcClient.getBIDiagramRpcClient().search({ - position: { startLine: targetLineRange.startLine, endLine: targetLineRange.endLine }, - filePath: fileName, - queryMap: undefined, - searchKind: "VECTOR_STORE", - }); - - const vectorStoreOptions: OptionProps[] = []; - const vectorStoreDataMap: ComponentData = {}; - - vectorStoreResponse.categories?.forEach((category) => { - category.items?.forEach((item) => { - // Check if it's an AvailableNode (not a nested Category) - if ("codedata" in item && "enabled" in item) { - const flowNode = item as any; // AvailableNode extends FlowNode - vectorStoreOptions.push({ - id: item.metadata.label, - content: item.metadata.label, - value: item.metadata.label, - }); - vectorStoreDataMap[item.metadata.label] = flowNode; - } - }); - }); - - setVectorStoreOptions(vectorStoreOptions); - - // Fetch available embedding providers - const embeddingProviderResponse = await rpcClient.getBIDiagramRpcClient().search({ - position: { startLine: targetLineRange.startLine, endLine: targetLineRange.endLine }, - filePath: fileName, - queryMap: undefined, - searchKind: "EMBEDDING_PROVIDER", - }); - - const embeddingProviderOptions: OptionProps[] = []; - const embeddingProviderDataMap: ComponentData = {}; - - embeddingProviderResponse.categories?.forEach((category) => { - category.items?.forEach((item) => { - // Check if it's an AvailableNode (not a nested Category) - if ("codedata" in item && "enabled" in item) { - const flowNode = item as any; // AvailableNode extends FlowNode - embeddingProviderOptions.push({ - id: item.metadata.label, - content: item.metadata.label, - value: item.metadata.label, - }); - embeddingProviderDataMap[item.metadata.label] = flowNode; - } - }); - }); - - setEmbeddingProviderOptions(embeddingProviderOptions); - const fullComponentDataMap = { ...vectorStoreDataMap, ...embeddingProviderDataMap }; - setComponentDataMap(fullComponentDataMap); - - // Set default selections and load their forms - const defaultVectorStore = vectorStoreOptions.find((option) => { - const content = option.content?.toString().toLowerCase() || ""; - return content.includes("memory") || content.includes("in-memory"); - }); - - const defaultEmbeddingProvider = embeddingProviderOptions.find((option) => { - const content = option.content?.toString().toLowerCase() || ""; - return content.includes("wso2") || content.includes("default"); - }); - - // Load default vector store - if (defaultVectorStore) { - await handleVectorStoreSelect(defaultVectorStore.value, fullComponentDataMap); - } else if (vectorStoreOptions.length > 0) { - await handleVectorStoreSelect(vectorStoreOptions[0].value, fullComponentDataMap); - } - - // Load default embedding provider - if (defaultEmbeddingProvider) { - await handleEmbeddingProviderSelect(defaultEmbeddingProvider.value, fullComponentDataMap); - } else if (embeddingProviderOptions.length > 0) { - await handleEmbeddingProviderSelect(embeddingProviderOptions[0].value, fullComponentDataMap); - } - - setIsInitialized(true); - } catch (error) { - console.error("Error fetching available components:", error); - } - }; - - const fetchAvailableComponentsInEditMode = async (vectorStoreNode: FlowNode, embeddingProviderNode: FlowNode) => { - try { - // Fetch available vector stores - const vectorStoreResponse = await rpcClient.getBIDiagramRpcClient().search({ - position: { startLine: targetLineRange.startLine, endLine: targetLineRange.endLine }, - filePath: fileName, - queryMap: undefined, - searchKind: "VECTOR_STORE", - }); - - const vectorStoreOptions: OptionProps[] = []; - const vectorStoreDataMap: ComponentData = {}; - - vectorStoreResponse.categories?.forEach((category) => { - category.items?.forEach((item) => { - // Check if it's an AvailableNode (not a nested Category) - if ("codedata" in item && "enabled" in item) { - const flowNode = item as any; // AvailableNode extends FlowNode - vectorStoreOptions.push({ - id: item.metadata.label, - content: item.metadata.label, - value: item.metadata.label, - }); - vectorStoreDataMap[item.metadata.label] = flowNode; - if ( - flowNode.codedata.module === vectorStoreNode.codedata.module && - flowNode.codedata.org === vectorStoreNode.codedata.org - ) { - setSelectedVectorStoreOption(item.metadata.label); - } - } - }); - }); - - setVectorStoreOptions(vectorStoreOptions); - - // Fetch available embedding providers - const embeddingProviderResponse = await rpcClient.getBIDiagramRpcClient().search({ - position: { startLine: targetLineRange.startLine, endLine: targetLineRange.endLine }, - filePath: fileName, - queryMap: undefined, - searchKind: "EMBEDDING_PROVIDER", - }); - - const embeddingProviderOptions: OptionProps[] = []; - const embeddingProviderDataMap: ComponentData = {}; - - embeddingProviderResponse.categories?.forEach((category) => { - category.items?.forEach((item) => { - // Check if it's an AvailableNode (not a nested Category) - if ("codedata" in item && "enabled" in item) { - const flowNode = item as any; // AvailableNode extends FlowNode - embeddingProviderOptions.push({ - id: item.metadata.label, - content: item.metadata.label, - value: item.metadata.label, - }); - embeddingProviderDataMap[item.metadata.label] = flowNode; - if ( - flowNode.codedata.module === embeddingProviderNode.codedata.module && - flowNode.codedata.org === embeddingProviderNode.codedata.org - ) { - setSelectedEmbeddingProviderOption(item.metadata.label); - } - } - }); - }); - - setEmbeddingProviderOptions(embeddingProviderOptions); - const fullComponentDataMap = { ...vectorStoreDataMap, ...embeddingProviderDataMap }; - setComponentDataMap(fullComponentDataMap); - } catch (error) { - console.error("Error fetching available components:", error); - } - }; - - const handleVectorStoreSelect = async (value: string, dataMap?: ComponentData) => { - const currentDataMap = dataMap || componentDataMap; - const vectorStoreNode = currentDataMap[value]; - if (vectorStoreNode) { - vectorStoreTemplateRef.current = vectorStoreNode; - setSelectedVectorStoreOption(value); - - // Get node template for the selected vector store to show its form - try { - const template = await rpcClient.getBIDiagramRpcClient().getNodeTemplate({ - filePath: fileName, - position: { line: targetLineRange.startLine.line, offset: 0 }, - id: vectorStoreNode.codedata, - }); - - const variableName = `${template.flowNode.properties.variable.value || "vectorStore"}`; - setVectorStoreVariableName(variableName); - - // Store template for later use in submission - vectorStoreTemplateRef.current = template.flowNode; - - const formProperties = getFormProperties(template.flowNode); - const props = formProperties as any; - if (props.variable) { - props.variable.hidden = true; - } - if (props.type) { - props.type.hidden = true; - } - const fields = convertNodePropertiesToFormFields(formProperties); - setVectorStoreFields(fields); - - // Update knowledge base fields with the vector store variable name - updateKnowledgeBaseFields(); - } catch (error) { - console.error("Error fetching vector store template:", error); - } + const getConnectionKind = (fieldName: string): ConnectionKind | undefined => { + switch (fieldName) { + case "vectorStore": + return "VECTOR_STORE"; + case "embeddingModel": + return "EMBEDDING_PROVIDER"; + case "chunker": + return "CHUNKER"; + default: + return undefined; } }; - const handleEmbeddingProviderSelect = async (value: string, dataMap?: ComponentData) => { - const currentDataMap = dataMap || componentDataMap; - const embeddingProviderNode = currentDataMap[value]; - if (embeddingProviderNode) { - embeddingProviderTemplateRef.current = embeddingProviderNode; - setSelectedEmbeddingProviderOption(value); - - // Get node template for the selected embedding provider to show its form - try { - const template = await rpcClient.getBIDiagramRpcClient().getNodeTemplate({ - filePath: fileName, - position: { line: targetLineRange.startLine.line, offset: 0 }, - id: embeddingProviderNode.codedata, - }); - - const variableName = `${template.flowNode.properties.variable.value || "embeddingProvider"}`; - setEmbeddingProviderVariableName(variableName); - - // Store template for later use in submission - embeddingProviderTemplateRef.current = template.flowNode; - - const formProperties = getFormProperties(template.flowNode); - const props = formProperties as any; - if (props.variable) { - props.variable.hidden = true; - } - if (props.type) { - props.type.hidden = true; - } - const fields = convertNodePropertiesToFormFields(formProperties); - setEmbeddingProviderFields(fields); - - // Update knowledge base fields with the embedding provider variable name - updateKnowledgeBaseFields(); - } catch (error) { - console.error("Error fetching embedding provider template:", error); + const initializeForm = () => { + const formProperties = getFormProperties(node); + const fields = convertNodePropertiesToFormFields(formProperties); + const actionFields = ["vectorStore", "embeddingModel", "chunker"]; + fields.forEach((field) => { + const originalName = field?.codedata?.originalName; + if (actionFields.includes(originalName)) { + field.type = "ACTION_EXPRESSION"; + field.advanced = false; + field.actionCallback = () => { + props.navigateToPanel?.(SidePanelView.CONNECTION_SELECT, getConnectionKind(originalName)); + }; + field.actionLabel = <>{`Create New ${field?.label}`}; + field.imports = node?.properties?.type?.imports; } - } - }; - - const updateKnowledgeBaseFields = () => { - // Update knowledge base fields with variable names from selected components - if (knowledgeBaseFields.length > 0) { - let hasChanges = false; - const updatedFields = knowledgeBaseFields.map((field) => { - if (field.key === "vectorStore" && vectorStoreVariableName && field.value !== vectorStoreVariableName) { - hasChanges = true; - return { ...field, value: vectorStoreVariableName }; - } - if ( - field.key === "embeddingModel" && - embeddingProviderVariableName && - field.value !== embeddingProviderVariableName - ) { - hasChanges = true; - return { ...field, value: embeddingProviderVariableName }; - } - return field; - }); - - if (hasChanges) { - setKnowledgeBaseFields(updatedFields); + if (originalName === "chunker") { + // hack: set default value for chunker field + field.defaultValue = DEFAULT_CHUNKER_VALUE; + field.value ??= field.defaultValue; } - } + }); + setKnowledgeBaseFields(fields); + setFormImports(getImportsForFormFields(fields)); + setIsInitialized(true); }; const mergeFormDataWithFlowNode = (data: FormValues, targetLineRange: LineRange): FlowNode => { @@ -632,105 +212,9 @@ export function VectorKnowledgeBaseForm(props: VectorKnowledgeBaseFormProps) { }; const handleSubmit = async () => { - if (!vectorStoreTemplateRef.current || !embeddingProviderTemplateRef.current) { - console.error("Vector store and embedding provider must be selected"); - return; - } - - if (!vectorStoreTemplateRef.current || !embeddingProviderTemplateRef.current) { - console.error("Templates not loaded yet"); - return; - } - try { setSaving(true); - // Use stored templates instead of fetching again - const vectorStoreTemplate = vectorStoreTemplateRef.current; - const embeddingProviderTemplate = embeddingProviderTemplateRef.current; - - // Merge vector store template with form values - const vectorStoreNode = cloneDeep(vectorStoreTemplate); - const vectorStoreUpdatedNode = updateNodeWithProperties( - vectorStoreNode, - vectorStoreNode, - vectorStoreFormValues, - formImports - ); - // Set the variable name - if (vectorStoreUpdatedNode.properties?.variable) { - vectorStoreUpdatedNode.properties.variable.value = vectorStoreVariableName; - } - - // Merge embedding provider template with form values - const embeddingProviderNode = cloneDeep(embeddingProviderTemplate); - const embeddingProviderUpdatedNode = updateNodeWithProperties( - embeddingProviderNode, - embeddingProviderNode, - embeddingProviderFormValues, - formImports - ); - // Set the variable name - if (embeddingProviderUpdatedNode.properties?.variable) { - embeddingProviderUpdatedNode.properties.variable.value = embeddingProviderVariableName; - } - - if (isEditForm) { - if (!vectorStoreLineRange.current || !embeddingProviderLineRange.current) { - console.error("Vector store and embedding provider line range not found"); - return; - } - // new vector store node - const newVectorStoreNode = cloneDeep(vectorStoreUpdatedNode); - newVectorStoreNode.codedata.lineRange = { - fileName: vectorStoreLineRange.current.fileName, - startLine: vectorStoreLineRange.current.startLine, - endLine: vectorStoreLineRange.current.endLine, - }; - newVectorStoreNode.codedata.isNew = false; - const newEmbeddingProviderNode = cloneDeep(embeddingProviderUpdatedNode); - newEmbeddingProviderNode.codedata.lineRange = { - fileName: embeddingProviderLineRange.current.fileName, - startLine: embeddingProviderLineRange.current.startLine, - endLine: embeddingProviderLineRange.current.endLine, - }; - newEmbeddingProviderNode.codedata.isNew = false; - - // save the vector store and embedding provider nodes - const vectorStoreSourceCode = await rpcClient.getBIDiagramRpcClient().getSourceCode({ - filePath: fileName, - flowNode: newVectorStoreNode, - }); - console.log(">>> vector store source code updated", { newVectorStoreNode, vectorStoreSourceCode }); - const embeddingProviderSourceCode = await rpcClient.getBIDiagramRpcClient().getSourceCode({ - filePath: fileName, - flowNode: newEmbeddingProviderNode, - }); - console.log(">>> embedding provider source code updated", { - newEmbeddingProviderNode, - embeddingProviderSourceCode, - }); - } else { - // save the vector store and embedding provider nodes - const vectorStoreSourceCode = await rpcClient.getBIDiagramRpcClient().getSourceCode({ - filePath: fileName, - flowNode: vectorStoreUpdatedNode, - }); - console.log(">>> vector store source code", { vectorStoreSourceCode }); - const embeddingProviderSourceCode = await rpcClient.getBIDiagramRpcClient().getSourceCode({ - filePath: fileName, - flowNode: embeddingProviderUpdatedNode, - }); - console.log(">>> embedding provider source code", { embeddingProviderSourceCode }); - } - // Create knowledge base node with form values and references - const combinedKnowledgeBaseData = { - ...knowledgeBaseFormValues, - vectorStore: vectorStoreVariableName, - embeddingModel: embeddingProviderVariableName, - }; - - const knowledgeBaseNode = mergeFormDataWithFlowNode(combinedKnowledgeBaseData, targetLineRange); - + const knowledgeBaseNode = mergeFormDataWithFlowNode(knowledgeBaseFormValues, targetLineRange); onSubmit(knowledgeBaseNode, false, formImports); } catch (error) { console.error("Error creating vector knowledge base:", error); @@ -739,24 +223,6 @@ export function VectorKnowledgeBaseForm(props: VectorKnowledgeBaseFormProps) { } }; - const isFormValid = useMemo(() => { - return ( - vectorStoreTemplateRef.current && - embeddingProviderTemplateRef.current && - isVectorStoreFormValid && - isEmbeddingProviderFormValid && - isKnowledgeBaseFormValid && - vectorStoreVariableName && - embeddingProviderVariableName - ); - }, [ - isVectorStoreFormValid, - isEmbeddingProviderFormValid, - isKnowledgeBaseFormValid, - vectorStoreVariableName, - embeddingProviderVariableName, - ]); - if (!isInitialized) { return ( @@ -773,77 +239,6 @@ export function VectorKnowledgeBaseForm(props: VectorKnowledgeBaseFormProps) { - - handleVectorStoreSelect(e.target.value)} - placeholder="Choose a vector store..." - value={selectedVectorStoreOption} - /> - - - {vectorStoreFields.length > 0 ? ( - { - setVectorStoreFormValues(allValues); - }} - /> - ) : ( - Loading vector store configuration... - )} - - - - handleEmbeddingProviderSelect(e.target.value)} - placeholder="Choose an embedding provider..." - value={selectedEmbeddingProviderOption} - /> - - - - {embeddingProviderFields.length > 0 ? ( - { - setEmbeddingProviderFormValues(allValues); - }} - /> - ) : ( - Loading embedding provider configuration... - )} - - {knowledgeBaseFields.length > 0 && ( { + const isValid = allValues["vectorStore"] !== "" && allValues["embeddingModel"] !== ""; + setIsFormValid(isValid); setKnowledgeBaseFormValues(allValues); }} /> @@ -868,16 +267,6 @@ export function VectorKnowledgeBaseForm(props: VectorKnowledgeBaseFormProps) { )} - - - - ); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FunctionFormStatic/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FunctionFormStatic/index.tsx new file mode 100644 index 00000000000..9924b79ecbc --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FunctionFormStatic/index.tsx @@ -0,0 +1,393 @@ +/** + * 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 { useEffect, useRef, useState } from "react"; +import { FunctionNode, LineRange, NodeKind, NodeProperties as OriginalNodeProperties, NodePropertyKey, DIRECTORY_MAP, EVENT_TYPE } from "@wso2/ballerina-core"; +import styled from "@emotion/styled"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { FormField, FormImports, FormValues } from "@wso2/ballerina-side-panel"; +import { URI, Utils } from "vscode-uri"; +import FormGeneratorNew from "../Forms/FormGeneratorNew"; +import { FormHeader } from "../../../components/FormHeader"; +import { convertConfig, getImportsForProperty } from "../../../utils/bi"; +import { LoadingContainer } from "../../styles"; +import { LoadingRing } from "../../../components/Loader"; + +type NodeProperties = OriginalNodeProperties & { + [key: string]: any; +}; + +const FormContainer = styled.div` + display: flex; + flex-direction: column; + max-width: 600px; + height: 400px; + gap: 20px; + overflow-y: auto; + padding-right: 16px; + + /* Add smooth scrolling */ + scroll-behavior: smooth; + + /* Style the scrollbar */ + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 4px; + } +`; + +interface FunctionFormProps { + filePath: string; + projectPath: string; + functionName: string; + isDataMapper?: boolean; + isNpFunction?: boolean; + isAutomation?: boolean; + handleSubmit?: (value: string) => void; + defaultType?: string; +} + +export function FunctionFormStatic(props: FunctionFormProps) { + const { rpcClient } = useRpcContext(); + const { projectPath, functionName, filePath, isDataMapper, isNpFunction, isAutomation, defaultType, handleSubmit } = props; + + const [functionFields, setFunctionFields] = useState([]); + const [functionNode, setFunctionNode] = useState(undefined); + const [targetLineRange, setTargetLineRange] = useState(); + const [saving, setSaving] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const fileName = filePath.split(/[\\/]/).pop(); + const formType = useRef("Function"); + + useEffect(() => { + let nodeKind: NodeKind; + if (isAutomation || functionName === "main") { + nodeKind = 'AUTOMATION'; + formType.current = "Automation"; + } else if (isDataMapper) { + nodeKind = 'DATA_MAPPER_DEFINITION'; + formType.current = 'Data Mapper'; + } else if (isNpFunction) { + nodeKind = 'NP_FUNCTION_DEFINITION'; + formType.current = 'Natural Function'; + } else { + nodeKind = 'FUNCTION_DEFINITION'; + formType.current = 'Function'; + } + + if (functionName) { + getExistingFunctionNode(); + } else { + getFunctionNode(nodeKind); + } + }, [isDataMapper, isNpFunction, isAutomation, functionName]); + + useEffect(() => { + let fields = functionNode ? convertConfig(functionNode.properties) : []; + + // TODO: Remove this once the hidden flag is implemented + if (isAutomation || functionName === "main") { + formType.current = "Automation"; + const automationFields = fields.filter(field => field.key !== "functionName" && field.key !== "type"); + fields = automationFields; + } + + // update description fields as "TEXTAREA" + fields.forEach((field) => { + if (field.key === "functionNameDescription" || field.key === "typeDescription") { + field.type = "TEXTAREA"; + } + if (field.key === "parameters") { + if ((field.valueTypeConstraint as any).value.parameterDescription) { + (field.valueTypeConstraint as any).value.parameterDescription.type = "TEXTAREA"; + } + } + }); + + setFunctionFields(fields); + }, [functionNode]); + + const getFunctionNode = async (kind: NodeKind) => { + setIsLoading(true); + const res = await rpcClient + .getBIDiagramRpcClient() + .getNodeTemplate({ + position: { line: 0, offset: 0 }, + filePath: Utils.joinPath(URI.file(projectPath), fileName).fsPath, + id: { node: kind }, + }); + let flowNode = res.flowNode; + + let properties = flowNode.properties as NodeProperties; + + console.log("FLOWNODE", flowNode) + + // Remove the description fields from properties + properties = Object.keys(properties).reduce((acc, key) => { + if (!key.toLowerCase().includes('functionnamedescription') && !key.toLowerCase().includes('typedescription')) { + acc[key] = properties[key]; + } + return acc; + }, {} as NodeProperties); + flowNode.properties = properties; + + if (defaultType && flowNode.properties && flowNode.properties.type) { + flowNode.properties.type.value = defaultType; + } + if (isNpFunction) { + /* + * TODO: Remove this once the LS is updated + * HACK: Add the advanced fields under parameters.advanceProperties + */ + // Get all the advanced fields + const advancedProperties = Object.fromEntries( + Object.entries(properties).filter(([_, property]) => property.advanced) + ); + // Remove the advanced fields from properties + properties = Object.fromEntries( + Object.entries(properties).filter(([_, property]) => !property.advanced) + ); + flowNode.properties = properties; + + // Add the all the advanced fields to advanceProperties + flowNode.properties.parameters = { + ...flowNode.properties.parameters, + advanceProperties: advancedProperties + } + } + + setFunctionNode(flowNode); + setIsLoading(false); + console.log("Function Node: ", flowNode); + } + + const getExistingFunctionNode = async () => { + setIsLoading(true); + const res = await rpcClient + .getBIDiagramRpcClient() + .getFunctionNode({ + functionName, + fileName, + projectPath + }); + let flowNode = res.functionDefinition; + if (isNpFunction) { + /* + * TODO: Remove this once the LS is updated + * HACK: Add the advanced fields under parameters.advanceProperties + */ + // Get all the advanced fields + let properties = flowNode.properties as NodeProperties; + const advancedProperties = Object.fromEntries( + Object.entries(properties).filter(([_, property]) => property.advanced) + ); + // Remove the advanced fields from properties + properties = Object.fromEntries( + Object.entries(properties).filter(([_, property]) => !property.advanced) + ); + flowNode.properties = properties; + + // Add the all the advanced fields to advanceProperties + flowNode.properties.parameters = { + ...flowNode.properties.parameters, + advanceProperties: advancedProperties + } + } + + setFunctionNode(flowNode); + setIsLoading(false); + console.log("Existing Function Node: ", flowNode); + } + + const onSubmit = async (data: FormValues, formImports?: FormImports) => { + console.log("Function Form Data: ", data); + const functionNodeCopy = { ...functionNode }; + + /** + * TODO: Remove this once the LS is updated + * HACK: Add the advanced fields under parameters.advanceProperties back to properties + */ + if (isNpFunction) { + // Add values back to properties + const properties = functionNodeCopy.properties; + functionNodeCopy.properties = { + ...properties, + ...properties.parameters.advanceProperties, + } + + // Remove the advanceProperties from parameters + delete properties.parameters.advanceProperties; + } + + if (isNpFunction) { + // Handle advance properties + const enrichFlowNodeForAdvanceProperties = (data: FormValues) => { + for (const value of Object.values(data)) { + const nestedData = value.advanceProperties; + if (nestedData) { + for (const [advanceKey, advanceValue] of Object.entries(nestedData)) { + functionNodeCopy.properties[advanceKey as NodePropertyKey].value = advanceValue; + } + + delete value.advanceProperties; + } + } + } + + enrichFlowNodeForAdvanceProperties(data); + } + + for (const [dataKey, dataValue] of Object.entries(data)) { + const properties = functionNodeCopy.properties as NodeProperties; + for (const [key, property] of Object.entries(properties)) { + if (dataKey === key) { + if (property.valueType === "REPEATABLE_PROPERTY") { + const baseConstraint = property.valueTypeConstraint; + property.value = {}; + // Go through the parameters array + for (const [repeatKey, repeatValue] of Object.entries(dataValue)) { + // Create a deep copy for each iteration + const valueConstraint = JSON.parse(JSON.stringify(baseConstraint)); + // Fill the values of the parameter constraint + for (const [paramKey, param] of Object.entries((valueConstraint as any).value as NodeProperties)) { + param.value = (repeatValue as any).formValues[paramKey] || ""; + } + (property.value as any)[(repeatValue as any).key] = valueConstraint; + } + } else { + property.value = dataValue; + } + const imports = getImportsForProperty(key, formImports); + property.imports = imports; + } + } + } + console.log("Updated function node: ", functionNodeCopy); + const sourceCode = await rpcClient + .getBIDiagramRpcClient() + .getSourceCode({ filePath, flowNode: functionNodeCopy, isFunctionNodeUpdate: true }); + + if (sourceCode.artifacts.length === 0) { + showErrorNotification(); + } + else { + handleSubmit(`${sourceCode.artifacts[0].name}()`); + } + setSaving(false) + }; + + const handleFormSubmit = async (data: FormValues, formImports?: FormImports) => { + setSaving(true); + // HACK: Remove new lines from function description fields + const descriptionFields = ["functionNameDescription", "typeDescription"]; + for (const field of descriptionFields) { + if (data[field]) { + data[field] = data[field]?.replace(/\n/g, " "); + } + } + // HACK: Remove new lines from parameter description + if (data.parameters) { + for (const parameter of data.parameters) { + if (parameter && parameter.formValues?.parameterDescription) { + parameter.formValues.parameterDescription = parameter.formValues.parameterDescription.replace(/\n/g, " "); + } + } + } + + try { + await onSubmit(data, formImports); + } catch (error) { + console.error("Error submitting form: ", error); + showErrorNotification(); + } + }; + + const showErrorNotification = async () => { + const functionType = getFunctionType(); + await rpcClient + .getCommonRpcClient() + .showErrorMessage({ + message: `${functionName ? `Failed to update the ${functionType}` : `Failed to create the ${functionType}`}. ` + }); + } + + const getFunctionType = () => { + if (isDataMapper) { + return "Data Mapper"; + } else if (isNpFunction) { + return "Natural Function"; + } else if (isAutomation || functionName === "main") { + return "Automation"; + } + return "Function"; + }; + + useEffect(() => { + if (filePath && rpcClient) { + rpcClient + .getBIDiagramRpcClient() + .getEndOfFile({ filePath }) + .then((res) => { + setTargetLineRange({ + startLine: res, + endLine: res, + }); + }); + } + }, [filePath, rpcClient]); + + //HACK: Hide is isolated field form function form + functionFields.forEach((field) => { + if (field.key === "isIsolated") { + field.hidden = true; + } + }); + + return ( +
+ + {isLoading && ( + + + + )} + + {filePath && targetLineRange && functionFields.length > 0 && + + } + +
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/ConfigureRecordPage.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/ConfigureRecordPage.tsx index 1bf853b308d..c85f0880061 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/ConfigureRecordPage.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/ConfigureRecordPage.tsx @@ -26,7 +26,7 @@ import { debounce } from "lodash"; type ConfigureRecordPageProps = { fileName: string; - targetLineRange: LineRange; + targetLineRange?: LineRange; onChange: (value: string, isRecordConfigureChange: boolean) => void; currentValue: string; recordTypeField: RecordTypeField; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/FunctionsPage.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/FunctionsPage.tsx index 3138cb63a51..4ef98c8b8ac 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/FunctionsPage.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/FunctionsPage.tsx @@ -42,7 +42,7 @@ type FunctionsPageProps = { fieldKey: string; anchorRef: RefObject; fileName: string; - targetLineRange: LineRange; + targetLineRange?: LineRange; onClose: () => void; onChange: (insertText: CompletionInsertText) => void; updateImports: (key: string, imports: {[key: string]: string}) => void; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/ParameterBranch.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/ParameterBranch.tsx index e4886b265e4..a2fddd7311b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/ParameterBranch.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/ParameterBranch.tsx @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useState } from "react"; import { TypeField } from "@wso2/ballerina-core"; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/CustomType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/CustomType/index.tsx index 0ef73447d39..c45afd16c47 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/CustomType/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/CustomType/index.tsx @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useState } from "react"; import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/InclusionType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/InclusionType/index.tsx index 37ec0427e5e..3141405e95c 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/InclusionType/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/InclusionType/index.tsx @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useState } from "react"; import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/RecordType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/RecordType/index.tsx index 750c7dcea01..7841775c8ac 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/RecordType/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/RecordType/index.tsx @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useState } from "react"; import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/UnionType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/UnionType/index.tsx index dfd1ceaf27c..edd70167fa2 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/UnionType/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/Types/UnionType/index.tsx @@ -15,7 +15,8 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useRef, useState, useEffect } from "react"; + +import { useState, useEffect } from "react"; import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; import { TypeField } from "@wso2/ballerina-core"; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/styles.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/styles.ts index eabf726c434..a2e8a00ab71 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/styles.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/RecordConstructView/styles.ts @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import { css } from "@emotion/css"; @@ -313,5 +314,3 @@ export const useHelperPaneStyles = () => ({ paddingLeft: '0px', }), }); - - diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/SuggestionsPage.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/SuggestionsPage.tsx index c95f8a81d0a..8cd25af4bcb 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/SuggestionsPage.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/SuggestionsPage.tsx @@ -26,7 +26,7 @@ import { convertToHelperPaneVariable, filterHelperPaneVariables } from "../../.. type SuggestionsPageProps = { fileName: string; - targetLineRange: LineRange; + targetLineRange?: LineRange; defaultValue: string; onChange: (value: string) => void; }; @@ -42,14 +42,14 @@ export const SuggestionsPage = ({ fileName, targetLineRange, defaultValue, onCha const getVariableInfo = useCallback(() => { setIsLoading(true); setTimeout(() => { + const position = targetLineRange + ? { line: targetLineRange.startLine.line, offset: targetLineRange.startLine.offset } + : undefined; rpcClient .getBIDiagramRpcClient() .getVisibleVariableTypes({ filePath: fileName, - position: { - line: targetLineRange.startLine.line, - offset: targetLineRange.startLine.offset - } + position }) .then((response) => { if (response.categories?.length) { diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/index.tsx index bb8ae3cfc8d..4b8f16db397 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPane/index.tsx @@ -28,7 +28,7 @@ import { RecordTypeField } from '@wso2/ballerina-core'; export type HelperPaneProps = { fieldKey: string; fileName: string; - targetLineRange: LineRange; + targetLineRange?: LineRange; exprRef: RefObject; anchorRef: RefObject; onClose: () => void; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/interfaces.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/EmptyItemsPlaceHolder.tsx similarity index 68% rename from workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/interfaces.ts rename to workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/EmptyItemsPlaceHolder.tsx index ed626f1609c..e312567e6db 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/inline-data-mapper/interfaces.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/EmptyItemsPlaceHolder.tsx @@ -16,15 +16,16 @@ * under the License. */ -import { TypeKind } from "../../interfaces/inline-data-mapper"; - -export interface IDMType { - category: string; - kind: TypeKind; - typeName?: string; - fieldName?: string; - memberType?: IDMType; - defaultValue?: unknown; - optional?: boolean; - fields?: IDMType[]; +export const EmptyItemsPlaceHolder = () => { + return ( +
+ No items found +
+ ) } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ExpandableList.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ExpandableList.tsx new file mode 100644 index 00000000000..354fdef072b --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ExpandableList.tsx @@ -0,0 +1,84 @@ +/** + * 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 { HorizontalListContainer, HorizontalListItem, HorizontalListItemLeftContent } from "../styles/HorizontalList" +import React from "react"; +import styled from "@emotion/styled"; + + +type ExpandableListProps = { + children: React.ReactNode; + sx?: React.CSSProperties; +}; + +export const ExpandableList = ({ children, sx }: ExpandableListProps) => { + return ( + + {children} + + ); +}; + +interface ExpandableListItemProps { + children: React.ReactNode; + onClick?: (event: React.MouseEvent) => void; + sx?: React.CSSProperties; +} + +const Item = ({ children, onClick, sx }: ExpandableListItemProps) => { + return ( + + + {children} + + + ); +}; + +interface ExpandableListSectionProps { + children: React.ReactNode; + title: string; + level?: number; + sx?: React.CSSProperties; +} + + +const Section = ({ children, title, level = 0, sx }: ExpandableListSectionProps) => { + return ( + + {title} + {children} + + ); +}; + + +ExpandableList.Item = Item; +ExpandableList.Section = Section; + +export default ExpandableList; + + +const ExpandableListSection = styled.div<{ level?: number }>` + +`; + + +const ExpandableListSectionTitle = styled.span` + font-weight: 600; +`; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/FooterButtons.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/FooterButtons.tsx new file mode 100644 index 00000000000..eee7dddce3f --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/FooterButtons.tsx @@ -0,0 +1,60 @@ +/** + * 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 { Button, Codicon, ThemeColors } from "@wso2/ui-toolkit"; +import styled from '@emotion/styled'; + +const InvisibleButton = styled.button` + background: none; + border: none; + padding: 0; + margin: 0; + text-align: inherit; + color: inherit; + font: inherit; + cursor: pointer; + outline: none; + box-shadow: none; + appearance: none; + display: inline-flex; + align-items: center; +`; + +type FooterButtonProps = { + onClick?: () => void; + startIcon: string; + title: string; + sx?: React.CSSProperties; + disabled?:boolean; +} + +const FooterButtons = (props: FooterButtonProps) => { + const { onClick, startIcon, title, sx } = props; + return ( +
+ + + {title} + +
+ ) +} + +export default FooterButtons; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/ParameterBranch.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/ParameterBranch.tsx new file mode 100644 index 00000000000..6ff47a25c4c --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/ParameterBranch.tsx @@ -0,0 +1,98 @@ +/** + * 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 React, { useState } from "react"; + +import { TypeField } from "@wso2/ballerina-core"; +import { Button } from "@wso2/ui-toolkit"; + + +import { useHelperPaneStyles } from "./styles"; +import { isAnyFieldSelected, isRequiredParam } from "./utils"; + +import * as Types from "./Types"; + +export interface ParameterBranchProps { + parameters: TypeField[]; + depth: number; + onChange: () => void; +} + +export interface TypeProps { + param: TypeField; + depth: number; + onChange: () => void; +} + +export function ParameterBranch(props: ParameterBranchProps) { + const { parameters, depth, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + + const [showOptionalParams, setShowOptionalParams] = useState(isAnyFieldSelected(parameters)); + + const requiredParams: JSX.Element[] = []; + const optionalParams: JSX.Element[] = []; + + parameters?.forEach((param: TypeField, index: number) => { + let TypeComponent = (Types as any)[param.typeName]; + const typeProps: TypeProps = { + param, + depth, + onChange, + }; + if (!TypeComponent) { + TypeComponent = (Types as any).custom; + } + if (isRequiredParam(param)) { + requiredParams.push(); + } else { + optionalParams.push(); + } + }); + + function toggleOptionalParams(e: any) { + setShowOptionalParams(!showOptionalParams); + } + + return ( +
+ {requiredParams} + {(optionalParams.length > 0 && depth === 1) ? ( + optionalParams + ) : ( + <> + {optionalParams.length > 0 && ( +
+ {/*
Optional fields
*/} + +
+ )} + {showOptionalParams && optionalParams.length > 0 && optionalParams} + + )} +
+ ); +} + +export const MemoizedParameterBranch = React.memo(ParameterBranch); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/CustomType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/CustomType/index.tsx new file mode 100644 index 00000000000..0ef73447d39 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/CustomType/index.tsx @@ -0,0 +1,91 @@ +/** + * 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 React, { useState } from "react"; + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { Codicon, Tooltip, Typography } from "@wso2/ui-toolkit"; + +import { TypeProps } from "../../ParameterBranch"; +import { useHelperPaneStyles } from "../../styles"; +import { isRequiredParam } from "../../utils"; + +export default function CustomType(props: TypeProps) { + const { param, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + const requiredParam = isRequiredParam(param); + if (requiredParam) { + param.selected = true; + } + + const [paramSelected, setParamSelected] = useState(param.selected || requiredParam); + + const toggleParamCheck = () => { + if (!requiredParam) { + const newSelectedState = !paramSelected; + param.selected = newSelectedState; + setParamSelected(newSelectedState); + onChange(); + } + }; + + return ( +
+
+
+ + + {param.name} + + + {param.optional || param.defaultable ? param.typeName + " (Optional)" : param.typeName} + + {param.documentation && ( + + {param.documentation} + + } + position="right" + sx={{ maxWidth: '300px', whiteSpace: 'normal', pointerEvents: 'none' }} + > + + + )} +
+
+
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/InclusionType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/InclusionType/index.tsx new file mode 100644 index 00000000000..37ec0427e5e --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/InclusionType/index.tsx @@ -0,0 +1,117 @@ +/** + * 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 React, { useState } from "react"; + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { Codicon, Tooltip, Typography } from "@wso2/ui-toolkit"; + +import { TypeProps } from "../../ParameterBranch"; +import { useHelperPaneStyles } from "../../styles"; +import { ParameterBranch } from "../../ParameterBranch"; +import { isAllDefaultableFields, isRequiredParam, updateFieldsSelection } from "../../utils"; + +export default function InclusionType(props: TypeProps) { + const { param, depth, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + const requiredParam = isRequiredParam(param) && depth > 1; // Only apply required param logic after depth 1 + const isAllIncludedParamDefaultable = isAllDefaultableFields(param.inclusionType?.fields); + if (requiredParam && !isAllIncludedParamDefaultable) { + param.selected = true; + } + + const [paramSelected, setParamSelected] = useState( + param.selected || (requiredParam && !isAllIncludedParamDefaultable) + ); + + const toggleParamCheck = () => { + const newSelectedState = !paramSelected; + param.selected = newSelectedState; + param.inclusionType.selected = newSelectedState; + + // If the inclusion type has fields, update their selection state + if (param.inclusionType?.fields && param.inclusionType.fields.length > 0) { + updateFieldsSelection(param.inclusionType.fields, newSelectedState); + } + + setParamSelected(newSelectedState); + onChange(); + }; + + const handleOnChange = () => { + param.selected = param.inclusionType.selected; + onChange(); + }; + + return ( +
+
+
+ + + {param.name} + + {param.inclusionType?.typeInfo && ( + + {(param.optional || param.defaultable) && " (Optional)"} * + {param.inclusionType.typeInfo.name} + + )} + {param.documentation && ( + + {param.documentation} + + } + position="right" + sx={{ maxWidth: '300px', whiteSpace: 'normal', pointerEvents: 'none' }} + > + + + )} +
+ {paramSelected && param.inclusionType?.fields?.length > 0 && ( +
+ +
+ )} +
+
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/RecordType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/RecordType/index.tsx new file mode 100644 index 00000000000..750c7dcea01 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/RecordType/index.tsx @@ -0,0 +1,105 @@ +/** + * 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 React, { useState } from "react"; + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { Codicon, Tooltip, Typography } from "@wso2/ui-toolkit"; + +import { TypeProps } from "../../ParameterBranch"; +import { useHelperPaneStyles } from "../../styles"; +import { MemoizedParameterBranch } from "../../ParameterBranch"; +import { isRequiredParam, updateFieldsSelection } from "../../utils"; + +export default function RecordType(props: TypeProps) { + const { param, depth, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + const requiredParam = isRequiredParam(param) && depth > 1; // Only apply required param logic after depth 1 + if (requiredParam) { + param.selected = true; + } + + const [paramSelected, setParamSelected] = useState(param.selected || requiredParam); + + const toggleParamCheck = () => { + if (!requiredParam) { + const newSelectedState = !paramSelected; + param.selected = newSelectedState; + + // If the record has fields, update their selection state + if (param.fields && param.fields.length > 0) { + updateFieldsSelection(param.fields, newSelectedState); + } + + setParamSelected(newSelectedState); + onChange(); + } + }; + + return ( +
+
+
+ + + {param.name} + + {param.typeInfo && ( + + {(param.optional || param.defaultable) && " (Optional)"} {param.typeInfo.name} + + )} + {param.documentation && ( + + {param.documentation} + + } + position="right" + sx={{ maxWidth: '300px', whiteSpace: 'normal', pointerEvents: 'none' }} + > + + + )} +
+ {paramSelected && param.fields?.length > 0 && ( +
+ +
+ )} +
+
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/UnionType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/UnionType/index.tsx new file mode 100644 index 00000000000..dfd1ceaf27c --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/UnionType/index.tsx @@ -0,0 +1,184 @@ +/** + * 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 React, { useRef, useState, useEffect } from "react"; + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { TypeField } from "@wso2/ballerina-core"; +import { Codicon, Dropdown, Tooltip, Typography } from "@wso2/ui-toolkit"; + +import { TypeProps } from "../../ParameterBranch"; +import { useHelperPaneStyles } from "../../styles"; +import { ParameterBranch } from "../../ParameterBranch"; +import { getSelectedUnionMember, isRequiredParam, updateFieldsSelection } from "../../utils"; + +export default function UnionType(props: TypeProps) { + const { param, depth, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + + const requiredParam = isRequiredParam(param) && depth > 1; // Only apply required param logic after depth 1 + if (requiredParam) { + param.selected = true; + } + const memberTypes = param.members?.map((field, index) => ({ id: index.toString(), value: getUnionParamName(field) })); + const initSelectedMember = getSelectedUnionMember(param); + + const [paramSelected, setParamSelected] = useState(param.selected || requiredParam); + const [selectedMemberType, setSelectedMemberType] = useState(getUnionParamName(initSelectedMember)); + const [parameter, setParameter] = useState(initSelectedMember); + + // Initialize: If the union is selected, ensure the selected member and its required fields are also selected + useEffect(() => { + if (paramSelected && initSelectedMember) { + handleMemberType(paramSelected ? selectedMemberType : ""); + } + }, []); + + if (!(param.members && param.members.length > 0)) { + return <>; + } + + const updateFormFieldMemberSelection = (unionField: TypeField) => { + const unionFieldName = getUnionParamName(unionField); + param.members.forEach((field) => { + field.selected = getUnionParamName(field) === unionFieldName; + + // If this is the selected field and it has nested fields, update them + if (field.selected && field.fields && field.fields.length > 0) { + // Set required fields to selected + updateFieldsSelection(field.fields, true); + } else if (!field.selected && field.fields && field.fields.length > 0) { + // Deselect all fields of non-selected members + updateFieldsSelection(field.fields, false); + } + }); + }; + + const handleMemberType = (type: string) => { + const selectedMember = param.members.find((field) => getUnionParamName(field) === type); + updateFormFieldMemberSelection(selectedMember); + setSelectedMemberType(type); + setParameter(selectedMember); + + // If the parent is selected and the selected member has fields, ensure required fields are selected + if (param.selected && selectedMember && selectedMember.fields && selectedMember.fields.length > 0) { + updateFieldsSelection(selectedMember.fields, true); + } + + onChange(); + }; + + const toggleParamCheck = () => { + const newSelectedState = !paramSelected; + param.selected = newSelectedState; + + // When checkbox is checked, ensure the currently selected member is also marked as selected + if (newSelectedState) { + const selectedMember = param.members.find((field) => getUnionParamName(field) === selectedMemberType); + if (selectedMember) { + updateFormFieldMemberSelection(selectedMember); + + // If the selected member has fields, recursively set required fields to selected + if (selectedMember.fields && selectedMember.fields.length > 0) { + updateFieldsSelection(selectedMember.fields, true); + } + } + } else { + // When unchecking, clear all member selections + param.members.forEach((field) => { + field.selected = false; + + // If the member has fields, recursively deselect all fields + if (field.fields && field.fields.length > 0) { + updateFieldsSelection(field.fields, false); + } + }); + } + + setParamSelected(newSelectedState); + onChange(); + }; + + return ( +
+
+
+ + + {param.name} + + {(param.optional || param.defaultable) && ( + + {"(Optional)"} + + )} + {param.documentation && ( + + {param.documentation} + + } + position="right" + sx={{ maxWidth: '300px', whiteSpace: 'normal', pointerEvents: 'none' }} + > + + + )} +
+ +
+
+ {paramSelected && parameter && ( +
+ +
+ )} +
+
+ ); +} + +export function getUnionParamName(param: TypeField) { + return param ? param.name || param.typeName : ""; +} diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/ExpandedMappingHeader/index.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/index.ts similarity index 68% rename from workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/ExpandedMappingHeader/index.ts rename to workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/index.ts index f265dfdfdd6..74a8994a46e 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/ExpandedMappingHeader/index.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/index.ts @@ -16,5 +16,13 @@ * under the License. */ -export * from "./ExpandedMappingHeaderNode"; -export * from "./ExpandedMappingHeaderNodeFactory"; +import CustomType from './CustomType'; +import InclusionType from "./InclusionType"; +import RecordType from "./RecordType"; +import UnionType from "./UnionType"; + +export {RecordType as record}; +export {UnionType as union}; +export {UnionType as enum}; +export {InclusionType as inclusion}; +export {CustomType as custom}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/styles.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/styles.ts new file mode 100644 index 00000000000..eabf726c434 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/styles.ts @@ -0,0 +1,317 @@ +/** + * 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 { css } from "@emotion/css"; + + +const removePadding = { + padding: '0px' +} + +const statementFontStyles = { + fontSize: "15px", + 'user-select': 'none', + fontFamily: 'monospace' +} + +const truncateText = { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' +} + +export const useHelperPaneStyles = () => ({ + suggestionsInner: css({ + overflowY: 'hidden', + height: '100%', + width: '100%' + }), + suggestionListContainer: css({ + overflowY: 'scroll', + marginTop: '5px', + }), + suggestionListItem: css({ + display: 'flex' + }), + suggestionDataType: css({ + color: 'var(--vscode-terminal-ansiGreen)', + ...truncateText, + }), + suggestionValue: css({ + ...truncateText, + }), + listItem: css({ + display: 'flex', + maxWidth: '155px', + }), + suggestionListInner: css({ + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column' + }), + expressionExample: css({ + fontSize: '13px', + }), + searchBox: css({ + width: '100%' + }), + librarySearchBox: css({ + position: 'relative', + height: '32px', + width: 'inherit', + border: '1px var(--custom-input-border-color)', + color: '#8D91A3', + textIndent: '12px', + textAlign: 'left', + marginBottom: '16px', + paddingLeft: '10px' + }), + helperPaneSubHeader: css({ + color: 'var(--vscode-editor-foreground)', + marginBottom: '4px', + fontWeight: 500 + }), + groupHeaderWrapper: css({ + display: 'flex', + flexDirection: 'row', + marginBottom: '14px' + }), + selectionWrapper: css({ + display: 'flex', + flexDirection: 'row', + marginTop: '5px' + }), + suggestionDividerWrapper: css({ + marginTop: '5px', + }), + groupHeader: css({ + color: 'var(--vscode-editor-foreground)', + fontWeight: 500 + }), + selectionSubHeader: css({ + color: 'var(--vscode-settings-textInputForeground)', + borderRadius: '5px', + backgroundColor: 'var(--vscode-editor-selectionBackground)', + marginRight: '5px', + ...statementFontStyles + }), + selectionSeparator: css({ + height: '1px', + width: '100%', + flex: '1 0', + backgroundColor: 'var(--vscode-panel-border)', + alignSelf: 'flex-end' + }), + loadingContainer: css({ + height: '60vh', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }), + libraryWrapper: css({ + marginTop: '5px', + overflowY: 'scroll', + }), + libraryBrowser: css({ + height: '100%' + }), + libraryBrowserHeader: css({ + display: 'flex', + flexDirection: 'row', + width: '100%', + alignItems: 'center', + zIndex: 1, + color: 'var(--vscode-sideBar-foreground)' + }), + searchResult: css({ + paddingTop: '15px', + }), + + moduleTitle: css({ + fontSize: '14px', + margin: '0 10px' + }), + libraryReturnIcon: css({ + marginRight: '8px' + }), + libraryElementBlock: css({ + top: '5%', + display: 'flex', + flexDirection: 'column' + }), + libraryElementBlockLabel: css({ + height: '10%', + padding: '0 10px' + }), + parameterCheckbox: css({ + margin: '0px' + }), + checked: css({}), + docParamSuggestions: css({ + height: '100%', + overflowY: 'scroll' + }), + returnSeparator: css({ + height: '1px', + opacity: '0.52', + backgroundColor: 'var(--vscode-panel-border)', + marginBottom: '15px' + }), + docParamDescriptionText: css({ + flex: "inherit", + ...removePadding + }), + includedRecordPlusBtn: css({ + display: 'block', + alignSelf: 'center', + padding: '0px', + marginLeft: '10px' + }), + paramHeader: css({ + marginTop: '0px', + color: 'var(--vscode-editor-foreground)' + }), + paramDataType: css({ + marginLeft: '8px', + marginRight: '8px', + flex: 'inherit', + ...removePadding + }), + requiredArgList: css({ + display: 'flex', + alignItems: 'flex-start', + overflowX: 'hidden', + width: 'fit-content', + ...removePadding + }), + docDescription: css({ + maxHeight: '50%', + overflowY: 'scroll', + whiteSpace: 'break-spaces', + display: 'block', + margin: '10px 0px', + ...removePadding + }), + returnDescription: css({ + maxHeight: '15%', + overflowY: 'scroll', + whiteSpace: 'break-spaces', + "& .MuiListItem-root": { + paddingLeft: '0px' + }, + ...removePadding + }), + paramList: css({ + overflowY: 'auto', + margin: '10px 0px', + }), + documentationWrapper: css({ + marginLeft: '28px', + }), + includedRecordHeaderList: css({ + "& .MuiListItem-root": { + padding: '0px', + alignItems: 'flex-start' + }, + "& .MuiListItemText-root": { + flex: "inherit" + }, + ...removePadding + }), + docListDefault: css({ + "& .MuiListItem-root": { + padding: '0px' + }, + "& .MuiListItemText-root": { + flex: 'inherit', + minWidth: 'auto', + margin: '0 6px 0 0' + }, + alignItems: 'flex-start', + width: 'fit-content', + ...removePadding + }), + docListCustom: css({ + marginBottom: '12px', + "& .MuiListItem-root": { + padding: '0px' + }, + "& .MuiListItemText-root": { + flex: 'inherit', + minWidth: 'auto', + margin: '0 6px 0 0' + }, + alignItems: 'flex-start', + width: 'fit-content', + ...removePadding + }), + exampleCode: css({ + display: 'flex', + padding: '5px', + fontFamily: 'monospace', + borderRadius: '0px' + }), + paramTreeDescriptionText: css({ + flex: "inherit", + whiteSpace: 'pre-wrap', + marginLeft: '24px', + ...removePadding + }), + listItemMultiLine: css({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-start', + minHeight: '32px' + }), + listItemHeader: css({ + display: 'flex', + alignItems: 'center', + height: '28px' + }), + listItemBody: css({ + marginLeft: '12px', + marginBottom: '8px', + paddingLeft: '16px', + borderLeft: "1px solid #d8d8d8", + }), + listDropdownWrapper: css({ + width: '200px', + }), + listOptionalWrapper: css({ + display: 'flex', + alignItems: 'center', + height: '32px', + marginBottom: '12px' + }), + listOptionalBtn: css({ + textTransform: 'none', + minWidth: '32px', + marginLeft: '8px' + }), + listOptionalHeader: css({ + fontSize: '13px', + color: "gray", + fontWeight: 500, + letterSpacing: '0', + lineHeight: '14px', + paddingLeft: '0px', + }), +}); + + diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/utils/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/utils/index.tsx new file mode 100644 index 00000000000..2027af17715 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/utils/index.tsx @@ -0,0 +1,82 @@ +/** + * 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 { + FormField, + keywords, +} from "@wso2/ballerina-core"; + + +export function isRequiredParam(param: FormField): boolean { + return !(param.optional || param.defaultable); +} + +export function isAllDefaultableFields(recordFields: FormField[]): boolean { + return recordFields?.every((field) => field.defaultable || (field.fields && isAllDefaultableFields(field.fields))); +} + +export function isAnyFieldSelected(recordFields: FormField[]): boolean { + return recordFields?.some((field) => field.selected || (field.fields && isAnyFieldSelected(field.fields))); +} + +export function getSelectedUnionMember(unionFields: FormField): FormField { + let selectedMember = unionFields.members?.find((member) => member.selected === true); + if (!selectedMember) { + selectedMember = unionFields.members?.find( + (member) => getUnionFormFieldName(member) === unionFields.selectedDataType + ); + } + if (!selectedMember) { + selectedMember = unionFields.members?.find( + (member) => member.typeName === unionFields.value?.replace(/['"]+/g, "") + ); + } + if (!selectedMember && unionFields.members && unionFields.members.length > 0) { + selectedMember = unionFields.members[0]; + } + return selectedMember; +} + +export function getFieldName(fieldName: string): string { + return keywords.includes(fieldName) ? "'" + fieldName : fieldName; +} + +export function getUnionFormFieldName(field: FormField): string { + return field.name || field.typeInfo?.name || field.typeName; +} + +export function checkFormFieldValue(field: FormField): boolean { + return field.value !== undefined && field.value !== null; +} + +export function updateFieldsSelection(fields: FormField[], selected: boolean): void { + if (!fields || !fields.length) return; + + fields.forEach(field => { + // When selecting: only select required fields + // When deselecting: deselect all fields (both required and optional) + if (!selected || isRequiredParam(field)) { + field.selected = selected; + } + + // Recursively process nested fields + if (field.fields && field.fields.length > 0) { + updateFieldsSelection(field.fields, selected); + } + }); +} diff --git a/workspaces/ballerina/data-mapper-view/src/resources/test-samples/main.bal b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ScrollableContainer.tsx similarity index 82% rename from workspaces/ballerina/data-mapper-view/src/resources/test-samples/main.bal rename to workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ScrollableContainer.tsx index 9175dc317b0..254043e4e1b 100644 --- a/workspaces/ballerina/data-mapper-view/src/resources/test-samples/main.bal +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ScrollableContainer.tsx @@ -15,8 +15,11 @@ * specific language governing permissions and limitations * under the License. */ -import ballerina/io; -public function main() { - io:println("Hello, World!"); -} +import styled from "@emotion/styled"; + +export const ScrollableContainer = styled.div` + flex: 1; + overflow: auto; + min-height: 0; +`; diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/utils/modifications.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableItem.tsx similarity index 64% rename from workspaces/ballerina/data-mapper-view/src/components/Diagram/utils/modifications.ts rename to workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableItem.tsx index 4d5fb714826..e19317b7f3e 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/utils/modifications.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableItem.tsx @@ -15,16 +15,20 @@ * specific language governing permissions and limitations * under the License. */ -import { STModification } from "@wso2/ballerina-core"; -import { NodePosition } from "@wso2/syntax-tree"; -export function getModification(statement: string, targetPosition: NodePosition): STModification { - return { - type: "INSERT", - isImport: false, - config: { - "STATEMENT": statement - }, - ...targetPosition - }; -} +import { ThemeColors } from "@wso2/ui-toolkit"; +import styled from "@emotion/styled"; + +const SelectableItem = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 8px; + &:hover { + background-color: ${ThemeColors.SURFACE_DIM}; + cursor: pointer; + } +` + +export default SelectableItem; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/VariableTypeIndicator.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/VariableTypeIndicator.tsx new file mode 100644 index 00000000000..4364333639a --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/VariableTypeIndicator.tsx @@ -0,0 +1,31 @@ +/** + * 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 styled from "@emotion/styled"; +import { VSCodeTag } from "@vscode/webview-ui-toolkit/react"; + +export const VariableTypeIndicator = styled(VSCodeTag)` + ::part(control) { + text-transform: none; + font-size: 10px; + height: 11px; + display: flex; + align-items: center; + justify-content: center; + } +`; diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Mappings/EditableRecordField.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Utils/types.ts similarity index 57% rename from workspaces/ballerina/data-mapper-view/src/components/Diagram/Mappings/EditableRecordField.ts rename to workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Utils/types.ts index 28944b81d6b..cab38d07515 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Mappings/EditableRecordField.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Utils/types.ts @@ -15,25 +15,26 @@ * specific language governing permissions and limitations * under the License. */ -import { TypeField } from "@wso2/ballerina-core"; -import { STNode } from "@wso2/syntax-tree"; -export interface ArrayElement { - member: EditableRecordField; - elementNode: STNode; +export const DEFAULT_VALUE_MAP: Record = { + "struct": "{}", + "array": "[]", + "map": "{}", + "int": "0", + "float": "0.0", + "boolean": "false", + "any": "null", } -export class EditableRecordField { - constructor( - public type: TypeField, - public value?: STNode, - public parentType?: EditableRecordField, - public originalType?: TypeField, - public childrenTypes?: EditableRecordField[], - public elements?: ArrayElement[] - ){} +export const isRowType = (type: string | string[]) => { + return type && type === "struct"; +} + +export const isUnionType = (type: string) => { + return type && type === "enum"; +} - public hasValue() { - return !!this.value; - } +export const getDefaultValue = (type: string) => { + //TODO: handle this using API + return DEFAULT_VALUE_MAP[type] || ""; } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx new file mode 100644 index 00000000000..1d4697a24de --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx @@ -0,0 +1,259 @@ +/** + * 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 { CompletionInsertText, ConfigVariable, FlowNode, LineRange, TomlPackage } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { ReactNode, useEffect, useState } from "react"; +import ExpandableList from "../Components/ExpandableList"; +import { SlidingPaneNavContainer } from "@wso2/ui-toolkit/lib/components/ExpressionEditor/components/Common/SlidingPane"; +import { COMPLETION_ITEM_KIND, Divider, getIcon, ThemeColors } from "@wso2/ui-toolkit"; +import { ScrollableContainer } from "../Components/ScrollableContainer"; +import DynamicModal from "../../../../components/Modal"; +import FooterButtons from "../Components/FooterButtons"; +import FormGenerator from "../../Forms/FormGenerator"; +import { URI, Utils } from "vscode-uri"; + +type ConfigVariablesState = { + [category: string]: { + [module: string]: ConfigVariable[]; + }; +}; + +type ListItem = { + name: string; + items: any[] +} + +type ConfigurablesPageProps = { + onChange: (insertText: string | CompletionInsertText, isRecordConfigureChange?: boolean) => void; + isInModal?: boolean; + anchorRef: React.RefObject; + fileName: string; + targetLineRange: LineRange; +} + +type AddNewConfigFormProps = { + isImportEnv: boolean; + title: string; +} + +export const Configurables = (props: ConfigurablesPageProps) => { + const { onChange, anchorRef, fileName, targetLineRange } = props; + + const { rpcClient } = useRpcContext(); + const [configVariables, setConfigVariables] = useState({}); + const [errorMessage, setErrorMessage] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + const [configVarNode, setCofigVarNode] = useState(); + const [isSaving, setIsSaving] = useState(false); + const [packageInfo, setPackageInfo] = useState(); + const [isImportEnv, setIsImportEnv] = useState(false); + const [projectPathUri, setProjectPathUri] = useState(); + + useEffect(() => { + const fetchNode = async () => { + const node = await rpcClient.getBIDiagramRpcClient().getConfigVariableNodeTemplate({ + isNew: true, + isEnvVariable: isImportEnv + }); + setCofigVarNode(node.flowNode); + }; + + fetchNode(); + }, [isImportEnv]); + + useEffect(() => { + getConfigVariables() + getProjectInfo() + const fetchTomlValues = async () => { + try { + const tomValues = await rpcClient.getCommonRpcClient().getCurrentProjectTomlValues(); + setPackageInfo(tomValues?.package); + } catch (error) { + console.error("Failed to fetch TOML values:", error); + setPackageInfo({ + org: "", + name: "", + version: "" + }); + } + }; + + fetchTomlValues(); + }, []) + + const getProjectInfo = async () => { + const projectPath = await rpcClient.getVisualizerLocation(); + setProjectPathUri(URI.file(projectPath.projectUri).fsPath); + } + + const getConfigVariables = async () => { + let data: ConfigVariablesState = {}; + let errorMsg: string = ''; + + await rpcClient + .getBIDiagramRpcClient() + .getConfigVariablesV2({ + includeLibraries: false, + projectPath: projectPathUri + }) + .then((variables) => { + data = (variables as any).configVariables; + errorMsg = (variables as any).errorMsg; + }); + + setConfigVariables(data); + setErrorMessage(errorMsg); + }; + + const handleFormClose = () => { + setIsModalOpen(false) + } + + const handleSave = async (node: FlowNode) => { + setIsModalOpen(false); + //TODO: Need to disable the form before saving and move form close to finally block + setIsSaving(true); + await rpcClient.getBIDiagramRpcClient().updateConfigVariablesV2({ + configFilePath: Utils.joinPath(URI.file(projectPathUri), 'config.bal').fsPath, + configVariable: node, + packageName: `${packageInfo.org}/${packageInfo.name}`, + moduleName: "", + }).finally(() => { + setIsSaving(false); + getConfigVariables(); + }); + }; + + const translateToArrayFormat = (object: object): ListItem[] => { + if (Array.isArray(object)) return object; + const keys = Object.keys(object); + return keys.map((key): { name: string; items: object[] } => { + return { + name: key, + items: translateToArrayFormat((object as Record)[key]) + } + }); + } + + const handleItemClicked = (name: string) => { + onChange(name, true) + } + + const AddNewForms = (props: AddNewConfigFormProps) => { + return ( + + + { + setIsImportEnv(props.isImportEnv) + }} + /> + + { }} + isInModal={true} + /> + + ) + } + + return ( +
+ + {translateToArrayFormat(configVariables) + .filter(category => + Array.isArray(category.items) && + category.items.some(sub => Array.isArray(sub.items) && sub.items.length > 0) + ) + .map(category => ( +
+ {category.items + .filter(subCategory => subCategory.items && subCategory.items.length > 0) + .map(subCategory => ( +
+ {subCategory.name !== '' ? ( + +
+ {subCategory.items.map((item: ConfigVariable) => ( + { handleItemClicked(item?.properties?.variable?.value as string) }} + > + + {getIcon(COMPLETION_ITEM_KIND.Constant)} + {item?.properties?.variable?.value as ReactNode} + + + ))} +
+
+ ) : ( +
+ {subCategory.items.map((item: ConfigVariable) => ( + { handleItemClicked(item?.properties?.variable?.value as string) }}> + + {getIcon(COMPLETION_ITEM_KIND.Constant)} + {item?.properties?.variable?.value as ReactNode} + + + ))} +
+ )} +
+ ))} +
+ ))} +
+ + +
+ + {/* */} +
+
+ ) +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/CreateValue.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/CreateValue.tsx new file mode 100644 index 00000000000..b0288750132 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/CreateValue.tsx @@ -0,0 +1,187 @@ +/** + * 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 { GetRecordConfigResponse, PropertyTypeMemberInfo, RecordTypeField, TypeField } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { RefObject, useEffect, useRef, useState } from "react"; +import { getDefaultValue, isRowType } from "../Utils/types"; +import ExpandableList from "../Components/ExpandableList"; +import { SlidingPaneNavContainer } from "@wso2/ui-toolkit/lib/components/ExpressionEditor/components/Common/SlidingPane"; + +type CreateValuePageProps = { + fileName: string; + currentValue: string; + onChange: (value: string, isRecordConfigureChange: boolean) => void; + selectedType?: string | string[]; + recordTypeField?: RecordTypeField; + anchorRef: RefObject; +} + +const passPackageInfoIfExists = (recordTypeMember: PropertyTypeMemberInfo) => { + let org = ""; + let module = ""; + let version = ""; + if (recordTypeMember?.packageInfo) { + const parts = recordTypeMember?.packageInfo.split(':'); + if (parts.length === 3) { + [org, module, version] = parts; + } + } + return { org, module, version } +} + +const getPropertyMember = (field: RecordTypeField) => { + return field?.recordTypeMembers.at(0); +} + +export const CreateValue = (props: CreateValuePageProps) => { + const { fileName, currentValue, onChange, selectedType, recordTypeField, anchorRef } = props; + const [recordModel, setRecordModel] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + + const { rpcClient } = useRpcContext(); + const propertyMember = getPropertyMember(recordTypeField) + + const sourceCode = useRef(currentValue); + + const getRecordConfigRequest = async () => { + if (recordTypeField) { + const packageInfo = passPackageInfoIfExists(recordTypeField?.recordTypeMembers.at(0)) + return { + filePath: fileName, + codedata: { + org: packageInfo.org, + module: packageInfo.module, + version: packageInfo.version, + packageName: propertyMember?.packageName, + }, + typeConstraint: propertyMember?.type, + } + } + else { + const tomValues = await rpcClient.getCommonRpcClient().getCurrentProjectTomlValues(); + return { + filePath: fileName, + codedata: { + org: tomValues.package.org, + module: tomValues.package.name, + version: tomValues.package.version, + packageName: propertyMember?.packageName, + }, + typeConstraint: propertyMember?.type || Array.isArray(selectedType) ? selectedType[0] : selectedType, + } + } + } + + const fetchRecordModel = async () => { + const request = await getRecordConfigRequest(); + const typeFieldResponse: GetRecordConfigResponse = await rpcClient.getBIDiagramRpcClient().getRecordConfig(request); + if (typeFieldResponse.recordConfig) { + const recordConfig: TypeField = { + name: propertyMember?.type, + ...typeFieldResponse.recordConfig + } + + setRecordModel([recordConfig]); + } + } + + useEffect(() => { + fetchRecordModel() + }, []); + + return ( + (recordTypeField) ? + <> + + : + ) +} + +const isSelectedTypeContainsType = (selectedType: string | string[], searchType: string) => { + if (Array.isArray(selectedType)) { + return selectedType.some(type => type.includes(searchType)); + } + const unionTypes = selectedType.split("|").map(type => type.trim()); + return unionTypes.includes(searchType); +} + +const NonRecordCreateValue = (props: CreateValuePageProps) => { + const { selectedType, onChange } = props; + + const handleValueSelect = (value: string) => { + onChange(value, false); + } + + const defaultValue = getDefaultValue(Array.isArray(selectedType) ? selectedType[0] : selectedType); + return ( +
+ {defaultValue && ( + + { handleValueSelect(defaultValue) }}> + + Initialize to {defaultValue} + + + + )} + {isSelectedTypeContainsType(selectedType, "string") && ( + + { handleValueSelect("\"TEXT_HERE\"") }}> + + Create a string value + + + + )} + {isSelectedTypeContainsType(selectedType, "string[]") && ( + + { handleValueSelect("[\"\"]") }}> + + Create a string array + + + + )} + {isSelectedTypeContainsType(selectedType, "log:PrintableRawTemplate") && ( + + { handleValueSelect("string `TEXT_HERE`") }}> + + Create a printable template + + + + )} + {isSelectedTypeContainsType(selectedType, "error") && ( + + { handleValueSelect("error(\"ERROR_MESSAGE_HERE\")") }}> + + Create an error + + + + )} +
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Functions.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Functions.tsx new file mode 100644 index 00000000000..0e26d822ca5 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Functions.tsx @@ -0,0 +1,287 @@ +/** + * 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 { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { HelperPaneCompletionItem, HelperPaneFunctionInfo } from "@wso2/ballerina-side-panel"; +import { debounce } from "lodash"; +import { useRef, useState, useCallback, RefObject, useEffect } from "react"; +import { convertToHelperPaneFunction, extractFunctionInsertText } from "../../../../utils/bi"; +import { CompletionInsertText, FunctionKind, LineRange } from "@wso2/ballerina-core"; +import { useMutation } from "@tanstack/react-query"; +import { ExpandableList } from "../Components/ExpandableList"; +import { SlidingPaneNavContainer } from "@wso2/ui-toolkit/lib/components/ExpressionEditor/components/Common/SlidingPane"; +import { COMPLETION_ITEM_KIND, CompletionItem, getIcon, HelperPaneCustom } from "@wso2/ui-toolkit/lib/components/ExpressionEditor"; +import { EmptyItemsPlaceHolder } from "../Components/EmptyItemsPlaceHolder"; +import styled from "@emotion/styled"; +import { Divider, SearchBox } from "@wso2/ui-toolkit"; +import { LibraryBrowser } from "../../HelperPane/LibraryBrowser"; +import { ScrollableContainer } from "../Components/ScrollableContainer"; +import FooterButtons from "../Components/FooterButtons"; +import DynamicModal from "../../../../components/Modal"; +import { URI, Utils } from "vscode-uri"; +import { FunctionFormStatic } from "../../FunctionFormStatic"; + +type FunctionsPageProps = { + fieldKey: string; + anchorRef: RefObject; + fileName: string; + targetLineRange: LineRange; + onClose: () => void; + onChange: (insertText: CompletionInsertText | string) => void; + updateImports: (key: string, imports: { [key: string]: string }) => void; + selectedType?: CompletionItem +}; + +export const FunctionsPage = ({ + fieldKey, + anchorRef, + fileName, + targetLineRange, + onClose, + onChange, + updateImports, + selectedType +}: FunctionsPageProps) => { + + const { rpcClient } = useRpcContext(); + const firstRender = useRef(true); + const [searchValue, setSearchValue] = useState(''); + const [isLibraryBrowserOpen, setIsLibraryBrowserOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [functionInfo, setFunctionInfo] = useState(undefined); + const [libraryBrowserInfo, setLibraryBrowserInfo] = useState(undefined); + const [projectUri, setProjectUri] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + + + + //TODO: get the correct filepath + let defaultFunctionsFile = Utils.joinPath(URI.file(projectUri), 'functions.bal').fsPath; + + const debounceFetchFunctionInfo = useCallback( + debounce((searchText: string, includeAvailableFunctions?: string) => { + setIsLoading(true); + rpcClient + .getBIDiagramRpcClient() + .search({ + position: targetLineRange, + filePath: fileName, + queryMap: { + q: searchText.trim(), + limit: 12, + offset: 0, + ...(!!includeAvailableFunctions && { includeAvailableFunctions }) + }, + searchKind: "FUNCTION" + }) + .then((response) => { + if (response.categories?.length) { + if (!!includeAvailableFunctions) { + setLibraryBrowserInfo(convertToHelperPaneFunction(response.categories)); + } else { + setFunctionInfo(convertToHelperPaneFunction(response.categories)); + } + } + console.log(response); + }) + .then(() => setIsLoading(false)); + }, 150), + [rpcClient, fileName, targetLineRange] + ); + + const fetchFunctionInfo = useCallback( + (searchText: string, includeAvailableFunctions?: string) => { + debounceFetchFunctionInfo(searchText, includeAvailableFunctions); + }, + [debounceFetchFunctionInfo, searchValue] + ); + + const { mutateAsync: addFunction, isPending: isAddingFunction } = useMutation({ + mutationFn: (item: HelperPaneCompletionItem) => + rpcClient.getBIDiagramRpcClient().addFunction({ + filePath: fileName, + codedata: item.codedata, + kind: item.kind as FunctionKind, + searchKind: 'FUNCTION' + }) + }); + + const onFunctionItemSelect = async (item: HelperPaneCompletionItem) => { + setIsLoading(true); + const response = await addFunction(item); + + setIsLoading(false) + if (response) { + const importStatement = { + [response.prefix]: response.moduleId + }; + updateImports(fieldKey, importStatement); + return extractFunctionInsertText(response.template); + } + + return { value: '' }; + }; + + useEffect(() => { + if (firstRender.current) { + firstRender.current = false; + fetchFunctionInfo(''); + } + + setDefaultFunctionsPath() + }, []); + + const setDefaultFunctionsPath = () => { + rpcClient.getVisualizerLocation().then((location) => { + setProjectUri(location?.projectUri || '') + }) + } + + const handleFunctionSearch = (searchText: string) => { + setSearchValue(searchText); + + // Search functions + if (isLibraryBrowserOpen) { + fetchFunctionInfo(searchText, 'true'); + } else { + fetchFunctionInfo(searchText); + } + }; + + const handleFunctionItemSelect = async (item: HelperPaneCompletionItem) => { + const { value, cursorOffset } = await onFunctionItemSelect(item); + onChange({ value, cursorOffset }); + onClose(); + }; + + return ( +
+
+ +
+ + { + + isLoading ? ( + + ) : ( + <> + { + !functionInfo || !functionInfo.category || functionInfo.category.length === 0 ? ( + + ) : ( + functionInfo.category.map((category) => { + if (!category.subCategory) { + if (!category.items || category.items.length === 0) { + return null; + } + + return ( + + +
+ {category.items.map((item) => ( + await handleFunctionItemSelect(item)}> + + {getIcon(COMPLETION_ITEM_KIND.Function)} + {`${item.label}()`} + + + ))} +
+
+
+ ) + } + + //if sub category is empty + if (category.subCategory.length === 0) { + return null; + } + + return ( + + {category.subCategory.map((subCategory) => ( + +
+ {subCategory.items.map((item) => ( + await handleFunctionItemSelect(item)}> + + {getIcon(COMPLETION_ITEM_KIND.Function)} + {`${item.label}()`} + + + ))} +
+
+ ))} +
+ ) + }) + ) + } + + ) + } +
+ +
+ + + + + + + setIsLibraryBrowserOpen(true)} /> + +
+ {isLibraryBrowserOpen && ( + setIsLibraryBrowserOpen(false)} + onClose={onClose} + onChange={onChange} + onFunctionItemSelect={onFunctionItemSelect} + /> + )} +
+ ) +} + +const FunctionItemLabel = styled.span` + font-size: 13px; +`; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/GenerateBICopilot.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/GenerateBICopilot.tsx new file mode 100644 index 00000000000..e9f7df88ef0 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/GenerateBICopilot.tsx @@ -0,0 +1,121 @@ +/** + * 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 { AutoResizeTextArea, Codicon, TextArea, ThemeColors } from "@wso2/ui-toolkit" +import styled from "@emotion/styled"; +import { useState } from "react"; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +` + +const PromptBox = styled.div` + display: flex; + flex-direction: row; + gap: 10px; + justify-content: space-between; + align-items: center; + position: relative; +` + +const StyledTextArea = styled(AutoResizeTextArea)` + ::part(control) { + font-family: monospace; + font-size: 12px; + min-height: 20px; + padding: 5px 8px; + } +`; + +const GenerateButton = styled.button` + width: 30px; + height: 30px; + border: none; + border-radius: 100%; + background-color: ${ThemeColors.PRIMARY}; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +`; + +const ButtonContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + position: absolute; + right: 10px; + top: 0; + height: 100%; +` + +export const GenerateBICopilot = () => { + + const [prompt, setPrompt] = useState(''); + const [generatedText, setGeneratedText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const getGeneratedText = async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve("This is the generated text you got!"); + }, 1000); + }); + } + + const handlePromptChange = async (event: React.ChangeEvent) => { + setPrompt(event?.target?.value || ''); + } + + const handleGenerate = async () => { + setGeneratedText(''); + setIsLoading(true); + const generatedText = await getGeneratedText(); + + let i = 0; + function animate() { + setGeneratedText(generatedText.slice(0, i)); + i++; + if (i <= generatedText.length) { + setTimeout(animate, 10); + } else { + setIsLoading(false); + setPrompt(''); + } + } + animate(); + }; + + return ( + + +