diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 031a86fe849..d599a6029c2 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -43,34 +43,34 @@ inputs: required: false BALLERINA_AUTH_ORG: default: true - type: string + type: string BALLERINA_AUTH_CLIENT_ID: default: true - type: string + type: string MI_AUTH_ORG: default: true - type: string + type: string MI_AUTH_CLIENT_ID: default: true - type: string + type: string PLATFORM_DEFAULT_GHAPP_CLIENT_ID: default: true - type: string + type: string PLATFORM_DEFAULT_DEVANT_ASGARDEO_CLIENT_ID: default: true - type: string + type: string PLATFORM_STAGE_GHAPP_CLIENT_ID: default: true - type: string + type: string PLATFORM_STAGE_DEVANT_ASGARDEO_CLIENT_ID: default: true - type: string + type: string PLATFORM_DEV_GHAPP_CLIENT_ID: default: true - type: string + type: string PLATFORM_DEV_DEVANT_ASGARDEO_CLIENT_ID: default: true - type: string + type: string runs: using: "composite" @@ -137,6 +137,24 @@ runs: cp "$example" "$envfile" done + - name: Restore MI LS from cache + id: mi-ls-cache + if: ${{ inputs.isPreRelease == 'true' }} + uses: actions/cache@v4 + with: + path: workspaces/mi/mi-extension/ls + key: mi-ls + retention-days: 1 + + - name: Restore Ballerina LS from cache + id: ballerina-ls-cache + if: ${{ inputs.isPreRelease == 'true' }} + uses: actions/cache@v4 + with: + path: workspaces/ballerina/ballerina-extension/ls + key: ballerina-ls + retention-days: 1 + - name: Build repo shell: bash id: build @@ -158,7 +176,7 @@ runs: - name: Compress build shell: bash run: | - zip -r build.zip ./ -x 'common/temp*' -x '**/node_modules/*' -y + zip -rq build.zip ./ -x 'common/temp*' -x '**/node_modules/*' zip VSIX.zip *.vsix - name: Save build diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 5619cf5ec11..3459b053b9c 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -19,9 +19,9 @@ jobs: - name: Cleanup run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - PR_NUMBER="${{ github.event.inputs.pull_request_number }}" + PR_NUMBER="${{ github.event.inputs.pull_request_number }}" else - PR_NUMBER="${{ github.event.pull_request.number }}" + PR_NUMBER="${{ github.event.pull_request.number }}" fi REPO=${{ github.repository }} diff --git a/.github/workflows/daily-build.yml b/.github/workflows/daily-build.yml index 7172d2447ae..5d257c7cabf 100644 --- a/.github/workflows/daily-build.yml +++ b/.github/workflows/daily-build.yml @@ -39,7 +39,7 @@ jobs: - name: Notification - MI uses: ./.github/actions/dailyBuildNotification with: - title: Micro Integrator + title: "WSO2 Integrator: MI" fileName: micro-integrator chatAPI: ${{ secrets.MI_TEAM_CHAT_API }} diff --git a/README.md b/README.md index 347df4b92a1..cbef0978cf2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository contains multiple Visual Studio Code extensions developed by WSO - [Choreo](https://marketplace.visualstudio.com/items?itemName=WSO2.choreo) - [WSO2 Integrator: BI](https://marketplace.visualstudio.com/items?itemName=WSO2.ballerina-integrator) - [APK Config Language Support](https://marketplace.visualstudio.com/items?itemName=WSO2.apk-config-language-support) -- [Micro Integrator](https://marketplace.visualstudio.com/items?itemName=WSO2.micro-integrator) +- [WSO2 Integrator: MI](https://marketplace.visualstudio.com/items?itemName=WSO2.micro-integrator) ## Prerequisites diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 2f890744c80..be560422d62 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -186,7 +186,7 @@ importers: version: 2.1.1 portfinder: specifier: ^1.0.32 - version: 1.0.37 + version: 1.0.37(supports-color@5.5.0) source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -2124,8 +2124,8 @@ importers: specifier: ^6.2.0 version: 6.2.0(webpack@5.99.9) html-to-image: - specifier: ^1.11.13 - version: 1.11.13 + specifier: ^1.11.11 + version: 1.11.11 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -3236,8 +3236,8 @@ importers: specifier: ~4.1.4 version: 4.1.4(@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.33.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2)) html-to-image: - specifier: 1.11.13 - version: 1.11.13 + specifier: 1.11.11 + version: 1.11.11 lodash: specifier: ~4.17.21 version: 4.17.21 @@ -3487,7 +3487,7 @@ importers: version: 2.1.0(webpack@5.99.9) portfinder: specifier: ^1.0.37 - version: 1.0.37 + version: 1.0.37(supports-color@5.5.0) recast: specifier: ^0.23.11 version: 0.23.11 @@ -14597,6 +14597,9 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html-to-image@1.11.11: + resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==} + html-to-image@1.11.13: resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} @@ -23508,7 +23511,7 @@ snapshots: '@babel/traverse': 7.27.4 '@babel/types': 7.27.6 convert-source-map: 1.9.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 lodash: 4.17.21 @@ -23531,7 +23534,7 @@ snapshots: '@babel/traverse': 7.27.4 '@babel/types': 7.27.6 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -23585,7 +23588,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/traverse': 7.27.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 semver: 6.3.1 @@ -23599,7 +23602,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/traverse': 7.27.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 semver: 6.3.1 @@ -23611,7 +23614,7 @@ snapshots: '@babel/core': 7.27.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -24456,7 +24459,7 @@ snapshots: '@babel/parser': 7.27.5 '@babel/template': 7.27.2 '@babel/types': 7.27.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -24959,7 +24962,7 @@ snapshots: '@eslint/config-array@0.20.1': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -24981,7 +24984,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -24995,7 +24998,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -25192,7 +25195,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -27150,7 +27153,7 @@ snapshots: '@secretlint/resolver': 9.3.4 '@secretlint/types': 9.3.4 ajv: 8.17.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) rc-config-loader: 4.1.3 transitivePeerDependencies: - supports-color @@ -27159,7 +27162,7 @@ snapshots: dependencies: '@secretlint/profiler': 9.3.4 '@secretlint/types': 9.3.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) structured-source: 4.0.0 transitivePeerDependencies: - supports-color @@ -27172,7 +27175,7 @@ snapshots: '@textlint/module-interop': 14.8.4 '@textlint/types': 14.8.4 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) pluralize: 8.0.0 strip-ansi: 6.0.1 table: 6.9.0 @@ -27188,7 +27191,7 @@ snapshots: '@secretlint/profiler': 9.3.4 '@secretlint/source-creator': 9.3.4 '@secretlint/types': 9.3.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) p-map: 4.0.0 transitivePeerDependencies: - supports-color @@ -29077,7 +29080,7 @@ snapshots: semver: 7.7.2 storybook: 9.0.12(@testing-library/dom@10.4.0)(prettier@3.5.3) style-loader: 3.3.4(webpack@5.99.9(webpack-cli@5.1.4)) - terser-webpack-plugin: 5.3.14(webpack@5.99.9(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.14(webpack@5.99.9) ts-dedent: 2.2.0 url: 0.11.4 util: 0.12.5 @@ -31170,7 +31173,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@4.9.5)(webpack@5.99.9)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -31184,7 +31187,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.99.9)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -31198,7 +31201,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(webpack-cli@5.1.4))': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -31212,7 +31215,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -32640,7 +32643,7 @@ snapshots: '@textlint/resolver': 14.8.4 '@textlint/types': 14.8.4 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) js-yaml: 3.14.1 lodash: 4.17.21 pluralize: 2.0.0 @@ -33246,7 +33249,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.48.2 '@typescript-eslint/type-utils': 5.48.2(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/utils': 5.48.2(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 ignore: 5.3.2 natural-compare-lite: 1.4.0 @@ -33266,7 +33269,7 @@ snapshots: '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -33286,7 +33289,7 @@ snapshots: '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -33412,7 +33415,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.48.2 '@typescript-eslint/types': 5.48.2 '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.8.3 @@ -33425,7 +33428,7 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.8.3 @@ -33438,7 +33441,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.8.3 @@ -33451,7 +33454,7 @@ snapshots: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.27.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -33463,7 +33466,7 @@ snapshots: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 typescript: 5.8.3 transitivePeerDependencies: @@ -33475,7 +33478,7 @@ snapshots: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.26.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -33487,7 +33490,7 @@ snapshots: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.27.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -33497,7 +33500,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) '@typescript-eslint/types': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -33535,7 +33538,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.8.3) '@typescript-eslint/utils': 5.48.2(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 tsutils: 3.21.0(typescript@5.8.3) optionalDependencies: @@ -33547,7 +33550,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.8.3) optionalDependencies: @@ -33559,7 +33562,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.8.3) optionalDependencies: @@ -33571,7 +33574,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.26.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -33582,7 +33585,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.27.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -33593,7 +33596,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/utils': 8.33.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.27.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -33612,7 +33615,7 @@ snapshots: '@typescript-eslint/typescript-estree@2.34.0(typescript@3.9.10)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint-visitor-keys: 1.3.0 glob: 7.2.3 is-glob: 4.0.3 @@ -33628,7 +33631,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.48.2 '@typescript-eslint/visitor-keys': 5.48.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -33642,7 +33645,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -33657,7 +33660,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -33672,7 +33675,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -33688,7 +33691,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) '@typescript-eslint/types': 8.33.1 '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -34417,7 +34420,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -35765,7 +35768,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -37657,7 +37660,7 @@ snapshots: detect-port@1.6.1: dependencies: address: 1.2.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -38146,14 +38149,14 @@ snapshots: esbuild-register@3.6.0(esbuild@0.18.20): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color esbuild-register@3.6.0(esbuild@0.25.5): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) esbuild: 0.25.5 transitivePeerDependencies: - supports-color @@ -38481,7 +38484,7 @@ snapshots: ajv: 6.12.6 chalk: 2.4.2 cross-spawn: 6.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 eslint-scope: 5.1.1 eslint-utils: 1.4.3 @@ -38530,7 +38533,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -38579,7 +38582,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -38622,7 +38625,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -38937,7 +38940,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -39291,7 +39294,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -40607,6 +40610,8 @@ snapshots: html-tags@3.3.1: {} + html-to-image@1.11.11: {} + html-to-image@1.11.13: {} html-url-attributes@3.0.1: {} @@ -40725,7 +40730,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -40733,14 +40738,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -40789,7 +40794,7 @@ snapshots: mime: 1.6.0 minimist: 1.2.8 opener: 1.5.2 - portfinder: 1.0.37 + portfinder: 1.0.37(supports-color@5.5.0) secure-compare: 3.0.1 union: 0.5.0 url-join: 4.0.1 @@ -40813,21 +40818,21 @@ snapshots: https-proxy-agent@4.0.0: dependencies: agent-base: 5.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -41530,7 +41535,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -43006,7 +43011,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 14.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lilconfig: 3.1.3 listr2: 8.3.3 micromatch: 4.0.8 @@ -44016,7 +44021,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) decode-named-character-reference: 1.2.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -44038,7 +44043,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -45428,13 +45433,6 @@ snapshots: style-value-types: 5.0.0 tslib: 2.8.1 - portfinder@1.0.37: - dependencies: - async: 3.2.6 - debug: 4.4.1(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - portfinder@1.0.37(supports-color@5.5.0): dependencies: async: 3.2.6 @@ -46185,7 +46183,7 @@ snapshots: puppeteer-core@2.1.1: dependencies: '@types/mime-types': 2.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -46298,7 +46296,7 @@ snapshots: rc-config-loader@4.1.3: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) js-yaml: 4.1.0 json5: 2.2.3 require-from-string: 2.0.2 @@ -47519,7 +47517,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -47714,7 +47712,7 @@ snapshots: '@secretlint/formatter': 9.3.4 '@secretlint/node': 9.3.4 '@secretlint/profiler': 9.3.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 14.1.0 read-pkg: 8.1.0 transitivePeerDependencies: @@ -47773,7 +47771,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -48065,7 +48063,7 @@ snapshots: socks-proxy-agent@6.2.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) socks: 2.8.5 transitivePeerDependencies: - supports-color @@ -48073,7 +48071,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) socks: 2.8.5 transitivePeerDependencies: - supports-color @@ -48184,7 +48182,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -48204,7 +48202,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -48363,7 +48361,7 @@ snapshots: streamroller@3.1.5: dependencies: date-format: 4.0.14 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -48669,7 +48667,7 @@ snapshots: cosmiconfig: 9.0.0(typescript@5.8.3) css-functions-list: 3.2.3 css-tree: 3.1.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 10.1.1 @@ -49225,15 +49223,6 @@ snapshots: terser: 5.43.0 webpack: 5.88.2(webpack-cli@5.1.4) - terser-webpack-plugin@5.3.14(webpack@5.99.9(webpack-cli@5.1.4)): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - jest-worker: 27.5.1 - schema-utils: 4.3.2 - serialize-javascript: 6.0.2 - terser: 5.43.0 - webpack: 5.99.9(webpack-cli@5.1.4) - terser-webpack-plugin@5.3.14(webpack@5.99.9): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -49241,7 +49230,7 @@ snapshots: schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.43.0 - webpack: 5.99.9(webpack-cli@6.0.1) + webpack: 5.99.9(webpack-cli@5.1.4) terser@4.8.1: dependencies: @@ -49591,7 +49580,7 @@ snapshots: semver: 7.7.2 source-map: 0.7.4 typescript: 5.8.3 - webpack: 5.99.9(webpack-cli@6.0.1) + webpack: 5.99.9(webpack-cli@5.1.4) ts-mixer@6.0.4: {} diff --git a/common/scripts/env-webpack-helper.js b/common/scripts/env-webpack-helper.js new file mode 100644 index 00000000000..ac672ce5a54 --- /dev/null +++ b/common/scripts/env-webpack-helper.js @@ -0,0 +1,53 @@ +/** + * 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. + */ + +/** + * Creates environment variables object for webpack DefinePlugin with fallback logic: + * 1. Check .env file first for variable values + * 2. If .env declares a variable but has no value, fallback to process.env + * 3. Only define variables that are explicitly declared in .env file + * + * @param {Object} env - Parsed environment variables from .env file (from dotenv.config().parsed) + * @returns {Object} Object containing envKeys for webpack.DefinePlugin and missingVars array + */ +function createEnvDefinePlugin(env) { + + const envKeys = Object.create(null); + const missingVars = []; + + if (env) { + Object.entries(env).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + envKeys[`process.env.${key}`] = JSON.stringify(value); + } + else if (process.env[key] !== undefined && process.env[key] !== '') { + envKeys[`process.env.${key}`] = JSON.stringify(process.env[key]); + } + else { + missingVars.push(key); + } + }); + } + + return { envKeys, missingVars }; + } + + module.exports = { + createEnvDefinePlugin + }; + \ No newline at end of file diff --git a/rush.json b/rush.json index bfd39805c73..f600b8dc962 100644 --- a/rush.json +++ b/rush.json @@ -38,7 +38,7 @@ * LTS schedule: https://nodejs.org/en/about/releases/ * LTS versions: https://nodejs.org/en/download/releases/ */ - "nodeSupportedVersionRange": ">=20.0.0 <=22.16.0", + "nodeSupportedVersionRange": ">=20.0.0 <23.0.0", /** * If the version check above fails, Rush will display a message showing the current * node version and the supported version range. You can use this setting to provide diff --git a/workspaces/api-designer/api-designer-extension/LICENSE b/workspaces/api-designer/api-designer-extension/LICENSE index 8b717b0ab43..836028b89bd 100644 --- a/workspaces/api-designer/api-designer-extension/LICENSE +++ b/workspaces/api-designer/api-designer-extension/LICENSE @@ -1,7 +1,7 @@ - MICRO INTEGRATOR VSCODE PLUGIN LICENSE TERMS + WSO2 INTEGRATOR: MI VSCODE PLUGIN LICENSE TERMS These license terms are an agreement between you and WSO2, Inc. These terms apply to the - Micro Integrator VSCode Plugin ("the software") and any revisions to that software. By installing the + WSO2 Integrator: MI VSCode Plugin ("the software") and any revisions to that software. By installing the software, you agree to comply with these terms. If you are an individual accepting this agreement on behalf of a company or other legal entity, you represent that you are authorized to bind the entity to the terms of this agreement and "you" or "your" will refer to the entity diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts b/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts index 864ed51839c..84e3500564c 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts @@ -73,19 +73,29 @@ export type Metadata = { icon?: string; keywords?: string[]; draft?: boolean; // for diagram draft nodes - data?: { - isDataMappedFunction?: boolean; - isAgentTool?: boolean; - isIsolatedFunction?: boolean; - tools?: ToolData[]; - model?: ToolData; - memory?: MemoryData; - agent?: AgentData; - paramsToHide?: string[]; // List of properties keys to to hide from forms - }; + data?: NodeMetadata | ParentMetadata; functionKind?: string; }; +export type NodeMetadata = { + isDataMappedFunction?: boolean; + isAgentTool?: boolean; + isIsolatedFunction?: boolean; + tools?: ToolData[]; + model?: ToolData; + memory?: MemoryData; + agent?: AgentData; + paramsToHide?: string[]; // List of properties keys to to hide from forms +}; + +export type ParentMetadata = { + kind: string; + label: string; + accessor?: string; + parameters?: string[]; + return?: string; +}; + export type ToolData = { name: string; description?: string; diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts b/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts index c8d90f609a7..c75642b9b13 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/constants.ts @@ -16,16 +16,16 @@ * under the License. */ -export enum SHARED_COMMANDS { - FORCE_UPDATE_PROJECT_ARTIFACTS = 'ballerina.force.update.artifacts', - SHOW_VISUALIZER = 'ballerina.show.visualizer', - GET_STATE_CONTEXT = 'ballerina.get.stateContext', - OPEN_BI_WELCOME = 'ballerina.open.bi.welcome', - OPEN_BI_NEW_PROJECT = 'ballerina.open.bi.new', - OPEN_SERVICE_FORM = 'ballerina.open.service.form', - OPEN_AI_PANEL = 'ballerina.open.ai.panel', - CLOSE_AI_PANEL = 'ballerina.close.ai.panel', - OPEN_AGENT_CHAT = 'ballerina.open.agent.chat' +export const SHARED_COMMANDS = { + FORCE_UPDATE_PROJECT_ARTIFACTS: 'ballerina.force.update.artifacts', + SHOW_VISUALIZER: 'ballerina.showVisualizer', + GET_STATE_CONTEXT: 'ballerina.get.stateContext', + OPEN_BI_WELCOME: 'ballerina.open.bi.welcome', + OPEN_BI_NEW_PROJECT: 'ballerina.open.bi.new', + OPEN_SERVICE_FORM: 'ballerina.open.service.form', + OPEN_AI_PANEL: 'ballerina.open.ai.panel', + CLOSE_AI_PANEL: 'ballerina.close.ai.panel', + OPEN_AGENT_CHAT: 'ballerina.open.agent.chat' } export const BI_COMMANDS = { @@ -42,6 +42,7 @@ export const BI_COMMANDS = { ADD_FUNCTION: 'BI.project-explorer.add-function', OPEN_TYPE_DIAGRAM: 'BI.view.typeDiagram', ADD_CONFIGURATION: 'BI.project-explorer.add-configuration', + VIEW_CONFIGURATION: 'BI.project-explorer.view-configuration', ADD_PROJECT: 'BI.project-explorer.add', SHOW_OVERVIEW: 'BI.project-explorer.overview', SWITCH_PROJECT: 'BI.project-explorer.switch-project', 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 1e0046f1c78..78128fca88c 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts @@ -1380,8 +1380,36 @@ export interface ResourceSourceCodeResponse { [key: string]: TextEdit[]; }; } + +export interface ResourceReturnTypesRequest { + filePath?: string; + context?: string; +} + +export interface ResponseCode { + category: string; + label: string; + type: string; + statusCode: string; + hasBody?: boolean; +} +export interface ResourceReturnTypesResponse { + completions: ResponseCode[]; +} + + // <-------- Service Designer Related -------> +export interface FunctionFromSourceRequest { + filePath: string; + codedata: CodeData; +} + +export interface FunctionFromSourceResponse { + function: FunctionModel; + errorMsg?: string; + stacktrace?: string; +} export interface FunctionNodeRequest { projectPath?: string; @@ -1594,9 +1622,11 @@ export interface BIInterface extends BaseLangClientInterface { getHttpResourceModel: (params: HttpResourceModelRequest) => Promise; addResourceSourceCode: (params: FunctionSourceCodeRequest) => Promise; addFunctionSourceCode: (params: FunctionSourceCodeRequest) => Promise; + getResourceReturnTypes: (params: ResourceReturnTypesRequest) => Promise; // Function APIs getFunctionNode: (params: FunctionNodeRequest) => Promise; + getFunctionFromSource: (params: FunctionFromSourceRequest) => Promise; getDesignModel: (params: BIDesignModelRequest) => Promise; getType: (params: GetTypeRequest) => Promise; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/index.ts index 4ddf2ee0735..1b0d5d6a99f 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/index.ts @@ -17,7 +17,7 @@ */ import { UpdatedArtifactsResponse } from "../../interfaces/bi"; -import { ListenerModelRequest, ListenerModelResponse, ServiceModelRequest, ServiceModelResponse, ServiceModelFromCodeRequest, ServiceModelFromCodeResponse, HttpResourceModelRequest, HttpResourceModelResponse, FunctionSourceCodeRequest, ListenerSourceCodeRequest, ListenersRequest, ListenersResponse, ServiceSourceCodeRequest, ListenerModelFromCodeRequest, ListenerModelFromCodeResponse, TriggerModelsRequest, TriggerModelsResponse, FunctionModelRequest, FunctionModelResponse } from "../../interfaces/extended-lang-client"; +import { ListenerModelRequest, ListenerModelResponse, ServiceModelRequest, ServiceModelResponse, ServiceModelFromCodeRequest, ServiceModelFromCodeResponse, HttpResourceModelRequest, HttpResourceModelResponse, FunctionSourceCodeRequest, ListenerSourceCodeRequest, ListenersRequest, ListenersResponse, ServiceSourceCodeRequest, ListenerModelFromCodeRequest, ListenerModelFromCodeResponse, TriggerModelsRequest, TriggerModelsResponse, FunctionModelRequest, FunctionModelResponse, ResourceReturnTypesRequest, ResourceReturnTypesResponse, FunctionFromSourceRequest, FunctionFromSourceResponse } from "../../interfaces/extended-lang-client"; import { ExportOASRequest, ExportOASResponse, @@ -33,10 +33,12 @@ export interface ServiceDesignerAPI { getListenerModelFromCode: (params: ListenerModelFromCodeRequest) => Promise; getServiceModel: (params: ServiceModelRequest) => Promise; getFunctionModel: (params: FunctionModelRequest) => Promise; + getFunctionFromSource: (params: FunctionFromSourceRequest) => Promise; addServiceSourceCode: (params: ServiceSourceCodeRequest) => Promise; updateServiceSourceCode: (params: ServiceSourceCodeRequest) => Promise; getServiceModelFromCode: (params: ServiceModelFromCodeRequest) => Promise; getHttpResourceModel: (params: HttpResourceModelRequest) => Promise; + getResourceReturnTypes: (params: ResourceReturnTypesRequest) => Promise; addResourceSourceCode: (params: FunctionSourceCodeRequest) => Promise; addFunctionSourceCode: (params: FunctionSourceCodeRequest) => Promise; updateResourceSourceCode: (params: FunctionSourceCodeRequest) => Promise; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/interfaces.ts index 20bc9aa8262..2326cc8f5de 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/interfaces.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/interfaces.ts @@ -24,73 +24,6 @@ export interface ExportOASRequest { export interface ExportOASResponse { openSpecFile: string; } -export interface ResponseCode { - code: number; - title: string; - source: string; -} - -export const responseCodes: ResponseCode[] = [ - { code: 200, title: "200 - OK", source: "http:Ok" }, - { code: 100, title: "100 - Continue", source: "http:Continue" }, - { code: 101, title: "101 - Switching Protocols", source: "http:SwitchingProtocols" }, - { code: 201, title: "201 - Created", source: "http:Created" }, - { code: 202, title: "202 - Accepted", source: "http:Accepted" }, - { code: 203, title: "203 - Non-Authoritative Information", source: "http:NonAuthoritativeInformation" }, - { code: 204, title: "204 - No Content", source: "http:NoContent" }, - { code: 205, title: "205 - Reset Content", source: "http:ResetContent" }, - { code: 206, title: "206 - Partial Content", source: "http:PartialContent" }, - { code: 207, title: "207 - Multi-Status", source: "http:MultiStatus" }, - { code: 208, title: "208 - Already Reported", source: "http:AlreadyReported" }, - { code: 226, title: "226 - IM Used", source: "http:IMUsed" }, - { code: 300, title: "300 - Multiple Choices", source: "http:MultipleChoices" }, - { code: 301, title: "301 - Moved Permanently", source: "http:MovedPermanently" }, - { code: 302, title: "302 - Found", source: "http:Found" }, - { code: 303, title: "303 - See Other", source: "http:SeeOther" }, - { code: 304, title: "304 - Not Modified", source: "http:NotModified" }, - { code: 305, title: "305 - Use Proxy", source: "http:UseProxy" }, - { code: 307, title: "307 - Temporary Redirect", source: "http:TemporaryRedirect" }, - { code: 308, title: "308 - Permanent Redirect", source: "http:PermanentRedirect" }, - { code: 400, title: "400 - Bad Request", source: "http:BadRequest" }, - { code: 401, title: "401 - Unauthorized", source: "http:Unauthorized" }, - { code: 402, title: "402 - Payment Required", source: "http:PaymentRequired" }, - { code: 403, title: "403 - Forbidden", source: "http:Forbidden" }, - { code: 404, title: "404 - Not Found", source: "http:NotFound" }, - { code: 405, title: "405 - Method Not Allowed", source: "http:MethodNotAllowed" }, - { code: 406, title: "406 - Not Acceptable", source: "http:NotAcceptable" }, - { code: 407, title: "407 - Proxy Authentication Required", source: "http:ProxyAuthenticationRequired" }, - { code: 408, title: "408 - Request Timeout", source: "http:RequestTimeout" }, - { code: 409, title: "409 - Conflict", source: "http:Conflict" }, - { code: 410, title: "410 - Gone", source: "http:Gone" }, - { code: 411, title: "411 - Length Required", source: "http:LengthRequired" }, - { code: 412, title: "412 - Precondition Failed", source: "http:PreconditionFailed" }, - { code: 413, title: "413 - Payload Too Large", source: "http:PayloadTooLarge" }, - { code: 414, title: "414 - URI Too Long", source: "http:UriTooLong" }, - { code: 415, title: "415 - Unsupported Media Type", source: "http:UnsupportedMediaType" }, - { code: 416, title: "416 - Range Not Satisfiable", source: "http:RangeNotSatisfiable" }, - { code: 417, title: "417 - Expectation Failed", source: "http:ExpectationFailed" }, - { code: 422, title: "422 - Unprocessable Entity", source: "http:UnprocessableEntity" }, - { code: 423, title: "423 - Locked", source: "http:Locked" }, - { code: 424, title: "424 - Failed Dependency", source: "http:FailedDependency" }, - { code: 425, title: "425 - Too Early", source: "http:TooEarly" }, - { code: 426, title: "426 - Upgrade Required", source: "http:UpgradeRequired" }, - { code: 428, title: "428 - Precondition Required", source: "http:PreconditionRequired" }, - { code: 429, title: "429 - Too Many Requests", source: "http:TooManyRequests" }, - { code: 431, title: "431 - Request Header Fields Too Large", source: "http:RequestHeaderFieldsTooLarge" }, - { code: 451, title: "451 - Unavailable Due To Legal Reasons", source: "http:UnavailableDueToLegalReasons" }, - { code: 500, title: "500 - Internal Server Error", source: "http:InternalServerError" }, - { code: 501, title: "501 - Not Implemented", source: "http:NotImplemented" }, - { code: 502, title: "502 - Bad Gateway", source: "http:BadGateway" }, - { code: 503, title: "503 - Service Unavailable", source: "http:ServiceUnavailable" }, - { code: 504, title: "504 - Gateway Timeout", source: "http:GatewayTimeout" }, - { code: 505, title: "505 - HTTP Version Not Supported", source: "http:HttpVersionNotSupported" }, - { code: 506, title: "506 - Variant Also Negotiates", source: "http:VariantAlsoNegotiates" }, - { code: 507, title: "507 - Insufficient Storage", source: "http:InsufficientStorage" }, - { code: 508, title: "508 - Loop Detected", source: "http:LoopDetected" }, - { code: 510, title: "510 - Not Extended", source: "http:NotExtended" }, - { code: 511, title: "511 - Network Authentication Required", source: "http:NetworkAuthorizationRequired" } -] - export interface SourceUpdateResponse { artifacts: ProjectStructureArtifactResponse[] } diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/rpc-type.ts index 14b4bc9b18e..dc1b7c7ebbc 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/service-designer/rpc-type.ts @@ -18,7 +18,7 @@ * THIS FILE INCLUDES AUTO GENERATED CODE */ import { UpdatedArtifactsResponse } from "../../interfaces/bi"; -import { ListenerModelRequest, ListenerModelResponse, ServiceModelRequest, ServiceModelResponse, ServiceModelFromCodeRequest, ServiceModelFromCodeResponse, HttpResourceModelRequest, HttpResourceModelResponse, FunctionSourceCodeRequest, ListenerSourceCodeRequest, ListenersRequest, ListenersResponse, ServiceSourceCodeRequest, ListenerModelFromCodeRequest, ListenerModelFromCodeResponse, TriggerModelsRequest, TriggerModelsResponse, FunctionModelRequest, FunctionModelResponse } from "../../interfaces/extended-lang-client"; +import { ListenerModelRequest, ListenerModelResponse, ServiceModelRequest, ServiceModelResponse, ServiceModelFromCodeRequest, ServiceModelFromCodeResponse, HttpResourceModelRequest, HttpResourceModelResponse, FunctionSourceCodeRequest, ListenerSourceCodeRequest, ListenersRequest, ListenersResponse, ServiceSourceCodeRequest, ListenerModelFromCodeRequest, ListenerModelFromCodeResponse, TriggerModelsRequest, TriggerModelsResponse, FunctionModelRequest, FunctionModelResponse, ResourceReturnTypesRequest, ResourceReturnTypesResponse, FunctionFromSourceRequest, FunctionFromSourceResponse } from "../../interfaces/extended-lang-client"; import { ExportOASRequest, ExportOASResponse, @@ -35,10 +35,12 @@ export const updateListenerSourceCode: RequestType = { method: `${_preFix}/getListenerModelFromCode` }; export const getServiceModel: RequestType = { method: `${_preFix}/getServiceModel` }; export const getFunctionModel: RequestType = { method: `${_preFix}/getFunctionModel` }; +export const getFunctionFromSource: RequestType = { method: `${_preFix}/getFunctionFromSource` }; export const addServiceSourceCode: RequestType = { method: `${_preFix}/addServiceSourceCode` }; export const updateServiceSourceCode: RequestType = { method: `${_preFix}/updateServiceSourceCode` }; export const getServiceModelFromCode: RequestType = { method: `${_preFix}/getServiceModelFromCode` }; export const getHttpResourceModel: RequestType = { method: `${_preFix}/getHttpResourceModel` }; +export const getResourceReturnTypes: RequestType = { method: `${_preFix}/getResourceReturnTypes` }; export const addResourceSourceCode: RequestType = { method: `${_preFix}/addResourceSourceCode` }; export const addFunctionSourceCode: RequestType = { method: `${_preFix}/addFunctionSourceCode` }; export const updateResourceSourceCode: RequestType = { method: `${_preFix}/updateResourceSourceCode` }; diff --git a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts index 74d743380eb..1c8b36698fa 100644 --- a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts +++ b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts @@ -76,6 +76,7 @@ export enum MACHINE_VIEW { AddConnectionWizard = "Add Connection Wizard", ViewConfigVariables = "View Config Variables", EditConfigVariables = "Edit Config Variables", + AddConfigVariables = "Add Config Variables", EditConnectionWizard = "Edit Connection Wizard", BIMainFunctionForm = "Add Automation SKIP", BIFunctionForm = "Add Function SKIP", diff --git a/workspaces/ballerina/ballerina-extension/.env.example b/workspaces/ballerina/ballerina-extension/.env.example index 44bc69d0787..425c38e65e9 100644 --- a/workspaces/ballerina/ballerina-extension/.env.example +++ b/workspaces/ballerina/ballerina-extension/.env.example @@ -1,4 +1,4 @@ BALLERINA_ROOT_URL=https://dev-tools.wso2.com/ballerina-copilot/v2.0 BALLERINA_AUTH_ORG= BALLERINA_AUTH_CLIENT_ID= -BALLERINA_AUTH_REDIRECT_URL=https://98c70105-822c-4359-8579-4da58f0ab4b7.e1-us-east-azure.choreoapps.dev \ No newline at end of file +BALLERINA_AUTH_REDIRECT_URL=https://eae690d5-80c3-4fb7-9bc5-e8d747cca11b.e1-us-east-azure.choreoapps.dev \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-extension/CHANGELOG.md b/workspaces/ballerina/ballerina-extension/CHANGELOG.md index 84c0d8068e8..d77992aac0d 100644 --- a/workspaces/ballerina/ballerina-extension/CHANGELOG.md +++ b/workspaces/ballerina/ballerina-extension/CHANGELOG.md @@ -2,6 +2,51 @@ All notable changes to the "Ballerina" extension will be documented in this file. +## **5.2.0** (2025-07-14) + +### Major Features + +- **Bundled Language Server**: Ballerina Language Server is now bundled with the extension, eliminating separate installation requirements and improving startup performance +- **Configurable Editor v2**: Complete redesign of the configuration editor with enhanced UI/UX and improved functionality +- **Type Editor Revamp**: A redesign of the type editor to improve feature discoverability and deliver a better user experience + +### Added + +- Enhanced AI file upload support with additional file types for improved analysis capabilities +- Documentation display in Signature Help for a better developer experience during code completion +- Enhanced service resource creation with comprehensive validation system for base paths, resource action calls, reserved keywords, and new UX for creating HTTP responses + +### Changed + +- **Integration Management**: Refactored artifacts management and navigation +- **UI Components**: + - Type Diagram and GraphQL designer with improved visual presentation +- **Developer Experience**: + - Enhanced renaming editor functionality + - Enhanced Form and Input Editor with Markdown support + - Updated imported types display as view-only nodes for clarity + +### Fixed + +- **Extension Stability**: + - Resolved extension startup and activation issues for reliable performance +- **Data Mapping & Visualization**: + - Fixed issues when working with complex data types from imported modules + - Improved visualization of array types and nested data structures + - Enhanced connection line display in design diagrams +- **Testing & Debugging**: + - Fixed GraphQL testing functionality for seamless API testing + - Improved service testing support across different Ballerina versions + - Enhanced test explorer compatibility with legacy projects +- **Configuration Management**: + - Resolved configuration file editing and creation issues + - Fixed form rendering problems that could cause UI freezing +- **Cross-Platform Support**: + - Enhanced Windows compatibility for Java development kit integration + - Improved file path handling across different operating systems +- **User Interface**: + - Fixed theme-related display issues in command interfaces + ## **5.1.3** (2025-05-28) ### Fixed diff --git a/workspaces/ballerina/ballerina-extension/package.json b/workspaces/ballerina/ballerina-extension/package.json index 76bb3fdd315..e14ac89b829 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.1.5", + "version": "5.2.1", "publisher": "wso2", "icon": "resources/images/ballerina.png", "homepage": "https://wso2.com/ballerina/vscode/docs", @@ -199,6 +199,19 @@ "type": "boolean", "default": false, "description": "Use Ballerina distribution language server instead of bundled language server. Note: This will be automatically enabled for Ballerina versions older than 2201.12.3." + }, + "ballerina.enableSequenceDiagramView": { + "type": "boolean", + "default": true, + "description": "Enable the experimental Sequence Diagram View.", + "tags": [ + "experimental" + ] + }, + "ballerina.enableAiSuggestions": { + "type": "boolean", + "default": true, + "description": "Enable AI suggestions in the Flow Diagram View." } } }, @@ -391,7 +404,7 @@ "icon": "$(distro-delete)" }, { - "command": "ballerina.show.visualizer", + "command": "ballerina.showVisualizer", "title": "Show Visualizer", "icon": "$(distro-design-view)", "category": "Ballerina" @@ -514,7 +527,7 @@ "category": "Ballerina" }, { - "command": "ballerina.tryit", + "command": "ballerina.tryIt", "title": "Open Try It", "category": "Ballerina", "icon": "$(server)" @@ -632,6 +645,13 @@ "group": "navigation", "category": "BI" }, + { + "command": "BI.project-explorer.view-configuration", + "title": "View Configurations", + "icon": "$(distro-design-view)", + "group": "navigation", + "category": "BI" + }, { "command": "BI.project-explorer.switch-project", "title": "Switch Project", @@ -731,7 +751,7 @@ "when": "resourceLangId == ballerina || isBallerinaDiagram" }, { - "command": "ballerina.tryit", + "command": "ballerina.tryIt", "group": "navigation@1", "title": "Open Try It", "when": "isBIProjectRunning" @@ -750,7 +770,7 @@ }, { "when": "resourceLangId == ballerina && !isPersistModelActive", - "command": "ballerina.show.visualizer", + "command": "ballerina.showVisualizer", "group": "navigation@3" }, { @@ -833,7 +853,7 @@ "when": "editorLangId != ballerina && !isBallerinaDiagram" }, { - "command": "ballerina.show.visualizer", + "command": "ballerina.showVisualizer", "when": "editorLangId == ballerina" }, { @@ -1075,7 +1095,7 @@ "copyFonts": "copyfiles -f ./node_modules/@wso2/font-wso2-vscode/dist/* ./resources/font-wso2-vscode/dist/", "copyVSIX": "copyfiles *.vsix ./vsix", "copyVSIXToRoot": "copyfiles -f ./vsix/*.vsix ../../..", - "download-ls": "node scripts/download-ls.js --prerelease", + "download-ls": "node scripts/download-ls.js", "build": "pnpm run compile && pnpm run lint && pnpm run postbuild", "rebuild": "pnpm run clean && pnpm run compile && pnpm run postbuild", "postbuild": "if [ \"$isPreRelease\" = \"true\" ]; then pnpm run download-ls --prerelease; else pnpm run download-ls; fi && pnpm run copyFonts && pnpm run copyJSLibs && pnpm run package && pnpm run copyVSIX", 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 f6d845902be..3d5af99072c 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts @@ -223,8 +223,12 @@ import { UpdateConfigVariableResponseV2, DeleteConfigVariableRequestV2, DeleteConfigVariableResponseV2, + ResourceReturnTypesRequest, + ResourceReturnTypesResponse, JsonToTypeRequest, - JsonToTypeResponse + JsonToTypeResponse, + FunctionFromSourceRequest, + FunctionFromSourceResponse } from "@wso2/ballerina-core"; import { BallerinaExtension } from "./index"; import { debug, handlePullModuleProgress } from "../utils"; @@ -344,10 +348,12 @@ enum EXTENDED_APIS { BI_SERVICE_GET_SERVICE_SOURCE = 'serviceDesign/getServiceFromSource', BI_SERVICE_UPDATE_SERVICE_CLASS = 'serviceDesign/updateServiceClass', BI_SERVICE_GET_RESOURCE = 'serviceDesign/getFunctionModel', + BI_SERVICE_GET_RESOURCE_RETURN_TYPES = 'serviceDesign/types', BI_SERVICE_ADD_RESOURCE = 'serviceDesign/addResource', BI_SERVICE_ADD_FUNCTION = 'serviceDesign/addFunction', BI_SERVICE_UPDATE_RESOURCE = 'serviceDesign/updateFunction', BI_SERVICE_SERVICE_CLASS_MODEL = 'serviceDesign/getServiceClassModelFromSource', + BI_GET_FUNCTION_FROM_SOURCE = 'serviceDesign/getFunctionFromSource', BI_UPDATE_CLASS_FIELD = 'serviceDesign/updateClassField', BI_ADD_CLASS_FIELD = 'serviceDesign/addField', BI_DESIGN_MODEL = 'designModelService/getDesignModel', @@ -1012,6 +1018,10 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.BI_SERVICE_SERVICE_CLASS_MODEL, params); } + async getFunctionFromSource(params: FunctionFromSourceRequest): Promise { + return this.sendRequest(EXTENDED_APIS.BI_GET_FUNCTION_FROM_SOURCE, params); + } + async updateClassField(params: ClassFieldModifierRequest): Promise { return this.sendRequest(EXTENDED_APIS.BI_UPDATE_CLASS_FIELD, params); } @@ -1024,6 +1034,10 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.BI_SERVICE_GET_RESOURCE, params); } + async getResourceReturnTypes(params: ResourceReturnTypesRequest): Promise { + return this.sendRequest(EXTENDED_APIS.BI_SERVICE_GET_RESOURCE_RETURN_TYPES, params); + } + async addResourceSourceCode(params: FunctionSourceCodeRequest): Promise { return this.sendRequest(EXTENDED_APIS.BI_SERVICE_ADD_RESOURCE, params); } diff --git a/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts b/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts index 4f5073260fb..b58a0af51db 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts @@ -34,7 +34,7 @@ import { BiDiagramRpcManager } from "../../rpc-managers/bi-diagram/rpc-manager"; import { readFileSync, readdirSync, statSync } from "fs"; import path from "path"; import { isPositionEqual, isPositionWithinDeletedComponent } from "../../utils/history/util"; -import { startDebugging } from "../editor-support/codelens-provider"; +import { startDebugging } from "../editor-support/activator"; const FOCUS_DEBUG_CONSOLE_COMMAND = 'workbench.debug.action.focusRepl'; @@ -69,10 +69,13 @@ export function activate(context: BallerinaExtension) { }); commands.registerCommand(BI_COMMANDS.ADD_CONFIGURATION, () => { - // Trigger to open the configuration setup view - openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.ViewConfigVariables }); + openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.AddConfigVariables }); }); + 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 }); }); diff --git a/workspaces/ballerina/ballerina-extension/src/features/config-generator/configGenerator.ts b/workspaces/ballerina/ballerina-extension/src/features/config-generator/configGenerator.ts index 1438f0fa8a5..e8731888b4c 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/config-generator/configGenerator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/config-generator/configGenerator.ts @@ -27,7 +27,7 @@ import { BallerinaProject, ConfigVariableResponse, EVENT_TYPE, MACHINE_VIEW, Pac import { TextDocumentEdit } from "vscode-languageserver-types"; import { modifyFileContent } from "../../utils/modification"; import { fileURLToPath } from "url"; -import { startDebugging } from "../editor-support/codelens-provider"; +import { startDebugging } from "../editor-support/activator"; import { openView } from "../../stateMachine"; import * as path from "path"; diff --git a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts index 34a6d0e3e4a..dee5b49091c 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts @@ -694,16 +694,7 @@ class BIRunAdapter extends LoggingDebugSession { task: 'run' }; - const ballerinaHome = ballerinaExtInstance.getConfiguredBallerinaHome(); - const pluginDevModeEnabled = ballerinaExtInstance.overrideBallerinaHome(); - - let runCommand: string; - if (pluginDevModeEnabled && ballerinaHome) { - runCommand = path.join(ballerinaHome, 'bin', ballerinaExtInstance.getBallerinaCmd()); - } else { - runCommand = ballerinaExtInstance.getBallerinaCmd(); - } - runCommand += ' run'; + let runCommand: string = `${ballerinaExtInstance.getBallerinaCmd()} run`; const programArgs = (args as any).programArgs; if (programArgs && programArgs.length > 0) { diff --git a/workspaces/ballerina/ballerina-extension/src/features/editor-support/activator.ts b/workspaces/ballerina/ballerina-extension/src/features/editor-support/activator.ts index 96e0fbb34dc..c9d997b64b1 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/editor-support/activator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/editor-support/activator.ts @@ -17,17 +17,20 @@ */ import { isSupportedVersion, VERSION } from "../../utils"; -import { languages, workspace } from "vscode"; -import { BallerinaExtension, LANGUAGE } from "../../core"; -import { ExecutorCodeLensProvider } from "./codelens-provider"; +import { commands, debug, DebugConfiguration, Uri, window, workspace, WorkspaceFolder } from "vscode"; +import { BallerinaExtension } from "../../core"; import { ReadOnlyContentProvider } from "./readonly-content-provider"; -import { StringSplitFeature, StringSplitter } from "./split-provider"; import * as gitStatus from "./git-status"; +import { INTERNAL_DEBUG_COMMAND, clearTerminal, FOCUS_DEBUG_CONSOLE_COMMAND, SOURCE_DEBUG_COMMAND, TEST_DEBUG_COMMAND } from "../project"; +import { sendTelemetryEvent, TM_EVENT_SOURCE_DEBUG_CODELENS, CMP_EXECUTOR_CODELENS, TM_EVENT_TEST_DEBUG_CODELENS } from "../telemetry"; +import { constructDebugConfig } from "../debugger"; +import { StringSplitFeature, StringSplitter } from "./split-provider"; export function activate(ballerinaExtInstance: BallerinaExtension) { if (!ballerinaExtInstance.context || !ballerinaExtInstance.langClient) { return; } + if (isSupportedVersion(ballerinaExtInstance, VERSION.ALPHA, 5)) { ballerinaExtInstance.context!.subscriptions.push(new StringSplitFeature(new StringSplitter(), ballerinaExtInstance)); @@ -43,7 +46,37 @@ export function activate(ballerinaExtInstance: BallerinaExtension) { return; } if (ballerinaExtInstance.isAllCodeLensEnabled() && isSupportedVersion(ballerinaExtInstance, VERSION.BETA, 1)) { - languages.registerCodeLensProvider([{ language: LANGUAGE.BALLERINA, scheme: 'file' }], - new ExecutorCodeLensProvider(ballerinaExtInstance)); + // TODO: Remove this once LS changes are merged + // languages.registerCodeLensProvider([{ language: LANGUAGE.BALLERINA, scheme: 'file' }], + // new ExecutorCodeLensProvider(ballerinaExtInstance)); + + commands.registerCommand(INTERNAL_DEBUG_COMMAND, async () => { + sendTelemetryEvent(ballerinaExtInstance, TM_EVENT_SOURCE_DEBUG_CODELENS, CMP_EXECUTOR_CODELENS); + clearTerminal(); + commands.executeCommand(FOCUS_DEBUG_CONSOLE_COMMAND); + startDebugging(window.activeTextEditor!.document.uri, false); + }); + + commands.registerCommand(SOURCE_DEBUG_COMMAND, async () => { + commands.executeCommand(INTERNAL_DEBUG_COMMAND); + return; + }); + + commands.registerCommand(TEST_DEBUG_COMMAND, async () => { + sendTelemetryEvent(ballerinaExtInstance, TM_EVENT_TEST_DEBUG_CODELENS, CMP_EXECUTOR_CODELENS); + clearTerminal(); + commands.executeCommand(FOCUS_DEBUG_CONSOLE_COMMAND); + startDebugging(window.activeTextEditor!.document.uri, true); + }); } } + + +export async function startDebugging(uri: Uri, testDebug: boolean = false, suggestTryit: boolean = false, noDebugMode: boolean = false): Promise { + const workspaceFolder: WorkspaceFolder | undefined = workspace.getWorkspaceFolder(uri); + const debugConfig: DebugConfiguration = await constructDebugConfig(uri, testDebug); + debugConfig.suggestTryit = suggestTryit; + debugConfig.noDebug = noDebugMode; + + return debug.startDebugging(workspaceFolder, debugConfig); +} diff --git a/workspaces/ballerina/ballerina-extension/src/features/editor-support/codelens-provider.ts b/workspaces/ballerina/ballerina-extension/src/features/editor-support/codelens-provider.ts deleted file mode 100644 index 3e22b7680aa..00000000000 --- a/workspaces/ballerina/ballerina-extension/src/features/editor-support/codelens-provider.ts +++ /dev/null @@ -1,183 +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 { BallerinaExtension, ExtendedLangClient, LANGUAGE } from '../../core'; -import { - CancellationToken, CodeLens, CodeLensProvider, commands, debug, DebugConfiguration, Event, EventEmitter, - ExtensionContext, - ProviderResult, Range, TextDocument, Uri, window, workspace, WorkspaceFolder -} from 'vscode'; -import { BAL_TOML, clearTerminal, PALETTE_COMMANDS } from '../project'; -import { - CMP_EXECUTOR_CODELENS, sendTelemetryEvent, TM_EVENT_SOURCE_DEBUG_CODELENS, TM_EVENT_TEST_DEBUG_CODELENS -} from '../telemetry'; -import { constructDebugConfig } from '../debugger'; -import { ExecutorPosition, ExecutorPositionsResponse, SyntaxTree } from '@wso2/ballerina-core'; -import { traversNode } from '@wso2/syntax-tree'; -import { CodeLensProviderVisitor } from './codelense-provider-visitor'; - -export enum EXEC_POSITION_TYPE { - SOURCE = 'source', - TEST = 'test' -} - -enum EXEC_TYPE { - RUN = 'Run', - DEBUG = 'Debug' -} - -enum EXEC_ARG { - TESTS = '--tests' -} - -export const INTERNAL_DEBUG_COMMAND = "ballerina.internal.debug"; - -const SOURCE_DEBUG_COMMAND = "ballerina.source.debug"; -const TEST_DEBUG_COMMAND = "ballerina.test.debug"; -const FOCUS_DEBUG_CONSOLE_COMMAND = 'workbench.debug.action.focusRepl'; - -export class ExecutorCodeLensProvider implements CodeLensProvider { - - private _onDidChangeCodeLenses: EventEmitter = new EventEmitter(); - public readonly onDidChangeCodeLenses: Event = this._onDidChangeCodeLenses.event; - private activeTextEditorUri: Uri | undefined; - - private ballerinaExtension: BallerinaExtension; - - constructor(extensionInstance: BallerinaExtension) { - this.ballerinaExtension = extensionInstance; - - workspace.onDidOpenTextDocument((document) => { - if (document.languageId === LANGUAGE.BALLERINA || document.fileName.endsWith(BAL_TOML)) { - this._onDidChangeCodeLenses.fire(); - } - }); - - workspace.onDidChangeTextDocument((activatedTextEditor) => { - if (activatedTextEditor && activatedTextEditor.document.languageId === LANGUAGE.BALLERINA || - activatedTextEditor.document.fileName.endsWith(BAL_TOML)) { - this._onDidChangeCodeLenses.fire(); - } - }); - - commands.registerCommand(INTERNAL_DEBUG_COMMAND, async () => { - sendTelemetryEvent(this.ballerinaExtension, TM_EVENT_SOURCE_DEBUG_CODELENS, CMP_EXECUTOR_CODELENS); - clearTerminal(); - commands.executeCommand(FOCUS_DEBUG_CONSOLE_COMMAND); - startDebugging(this.activeTextEditorUri!, false); - }); - - commands.registerCommand(SOURCE_DEBUG_COMMAND, async () => { - this.activeTextEditorUri = window.activeTextEditor!.document.uri; - commands.executeCommand(INTERNAL_DEBUG_COMMAND); - return; - }); - - commands.registerCommand(TEST_DEBUG_COMMAND, async () => { - sendTelemetryEvent(this.ballerinaExtension, TM_EVENT_TEST_DEBUG_CODELENS, CMP_EXECUTOR_CODELENS); - clearTerminal(); - commands.executeCommand(FOCUS_DEBUG_CONSOLE_COMMAND); - startDebugging(window.activeTextEditor!.document.uri, true); - }); - } - - provideCodeLenses(_document: TextDocument, _token: CancellationToken): ProviderResult { - if (this.ballerinaExtension.langClient && window.activeTextEditor) { - return this.getCodeLensList(); - } - return []; - } - - private async getCodeLensList(): Promise { - let codeLenses: CodeLens[] = []; - let langClient: ExtendedLangClient | undefined = this.ballerinaExtension.langClient; - - if (!langClient) { - return codeLenses; - } - - const activeEditorUri = window.activeTextEditor!.document.uri; - const fileUri = activeEditorUri.toString(); - - try { - const response = await langClient!.getExecutorPositions({ - documentIdentifier: { - uri: fileUri - } - }) as ExecutorPositionsResponse; - if (response.executorPositions) { - response.executorPositions.forEach(position => { - if (position.kind === EXEC_POSITION_TYPE.SOURCE) { - codeLenses.push(this.createCodeLens(position, EXEC_TYPE.RUN)); - codeLenses.push(this.createCodeLens(position, EXEC_TYPE.DEBUG)); - } - }); - } - } catch (error) { - } - - // Open in diagram code lenses - try { - const syntaxTreeResponse = await langClient!.getSyntaxTree({ - documentIdentifier: { - uri: fileUri - } - }); - const response = syntaxTreeResponse as SyntaxTree; - if (response.parseSuccess && response.syntaxTree) { - const syntaxTree = response.syntaxTree; - - const visitor = new CodeLensProviderVisitor(activeEditorUri); - traversNode(syntaxTree, visitor, undefined); - codeLenses.push(...visitor.getCodeLenses()); - } - } catch (error) { - } - - return codeLenses; - } - - private createCodeLens(execPosition: ExecutorPosition, execType: EXEC_TYPE): CodeLens { - const startLine = execPosition.range.startLine.line; - const startColumn = execPosition.range.startLine.offset; - const endLine = execPosition.range.endLine.line; - const endColumn = execPosition.range.endLine.offset; - const codeLens = new CodeLens(new Range(startLine, startColumn, endLine, endColumn)); - const textDocument = window.activeTextEditor!.document; - this.activeTextEditorUri = textDocument.uri; - codeLens.command = { - title: execType.toString(), - tooltip: `${execType.toString()} ${execPosition.name}`, - command: execPosition.kind === EXEC_POSITION_TYPE.SOURCE ? (execType === EXEC_TYPE.RUN ? - PALETTE_COMMANDS.RUN : SOURCE_DEBUG_COMMAND) : (execType === EXEC_TYPE.RUN ? PALETTE_COMMANDS.TEST : - TEST_DEBUG_COMMAND), - arguments: execPosition.kind === EXEC_POSITION_TYPE.SOURCE ? [textDocument.uri] : (execType === EXEC_TYPE.RUN ? - [EXEC_ARG.TESTS, execPosition.name] : [execPosition.name]) - }; - return codeLens; - } -} - -export async function startDebugging(uri: Uri, testDebug: boolean = false, suggestTryit: boolean = false, noDebugMode: boolean = false): Promise { - const workspaceFolder: WorkspaceFolder | undefined = workspace.getWorkspaceFolder(uri); - const debugConfig: DebugConfiguration = await constructDebugConfig(uri, testDebug); - debugConfig.suggestTryit = suggestTryit; - debugConfig.noDebug = noDebugMode; - - return debug.startDebugging(workspaceFolder, debugConfig); -} diff --git a/workspaces/ballerina/ballerina-extension/src/features/editor-support/codelense-provider-visitor.ts b/workspaces/ballerina/ballerina-extension/src/features/editor-support/codelense-provider-visitor.ts deleted file mode 100644 index 2df98e9b58b..00000000000 --- a/workspaces/ballerina/ballerina-extension/src/features/editor-support/codelense-provider-visitor.ts +++ /dev/null @@ -1,148 +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 { - STNode, - Visitor, - FunctionDefinition, - ServiceDeclaration, - ObjectMethodDefinition, - ResourceAccessorDefinition, - STKindChecker, - TypeDefinition -} from "@wso2/syntax-tree"; -import { PALETTE_COMMANDS } from "../project"; -import { CodeLens, Range, Uri } from "vscode"; -import { checkIsPersistModelFile } from "../../views/persist-layer-diagram/activator"; -import { SHARED_COMMANDS } from "@wso2/ballerina-core"; - -export class CodeLensProviderVisitor implements Visitor { - activeEditorUri: Uri; - codeLenses: CodeLens[] = []; - supportedServiceTypes: string[] = ["http", "ai", "graphql"]; - - constructor(activeEditorUri: Uri) { - this.activeEditorUri = activeEditorUri; - } - - public beginVisitFunctionDefinition(node: FunctionDefinition, parent?: STNode): void { - this.createVisulizeCodeLens(node.functionName.position, node.position); - } - - public beginVisitServiceDeclaration(node: ServiceDeclaration, parent?: STNode): void { - if (node.expressions.length > 0) { - const expr = node.expressions[0]; - if ((STKindChecker.isExplicitNewExpression(expr) && - expr.typeDescriptor && - STKindChecker.isQualifiedNameReference(expr.typeDescriptor) && - this.supportedServiceTypes.includes(expr.typeDescriptor.modulePrefix.value)) || - (STKindChecker.isSimpleNameReference(expr) && - this.supportedServiceTypes.includes(expr.typeData.typeSymbol.moduleID.moduleName))) { - this.createTryItCodeLens(node.position, node.serviceKeyword.position, node.absoluteResourcePath.map((path) => path.value).join(''), node.expressions.map((exp) => exp.source.trim()).join(',')); - if (expr?.typeData?.typeSymbol?.signature?.includes("graphql")) { - this.createVisulizeGraphqlCodeLens(node.serviceKeyword.position, node.position); - } else { - this.createVisulizeCodeLens(node.serviceKeyword.position, node.position); - } - } - } - } - - public beginVisitTypeDefinition(node: TypeDefinition, parent?: STNode): void { - if (STKindChecker.isRecordTypeDesc(node.typeDescriptor) && checkIsPersistModelFile(this.activeEditorUri)) { - this.createVisualizeERCodeLens(node.position, node.typeName.value); - } - } - - public beginVisitObjectMethodDefinition(node: ObjectMethodDefinition, parent?: STNode): void { - this.createVisulizeCodeLens(node.functionKeyword.position, node.position); - } - - public beginVisitResourceAccessorDefinition(node: ResourceAccessorDefinition, parent?: STNode): void { - this.createVisulizeCodeLens(node.qualifierList[0].position, node.position); - } - - private createVisulizeCodeLens(range: any, position: any) { - const codeLens = new CodeLens(new Range( - range.startLine, - range.startColumn, - range.endLine, - range.endColumn - )); - codeLens.command = { - title: "Visualize", - tooltip: "Visualize code block", - command: SHARED_COMMANDS.SHOW_VISUALIZER, - arguments: [this.activeEditorUri.fsPath, position] - }; - this.codeLenses.push(codeLens); - } - - private createVisulizeGraphqlCodeLens(range: any, position: any) { - const codeLens = new CodeLens(new Range( - range.startLine, - range.startColumn, - range.endLine, - range.endColumn - )); - codeLens.command = { - title: "Visualize", - tooltip: "Visualize code block", - command: SHARED_COMMANDS.SHOW_VISUALIZER, - arguments: [this.activeEditorUri.fsPath, position] - }; - this.codeLenses.push(codeLens); - } - - private createVisualizeERCodeLens(range: any, recordName: string) { - const codeLens = new CodeLens(new Range( - range.startLine, - range.startColumn, - range.endLine, - range.endColumn - )); - codeLens.command = { - title: "Visualize", - tooltip: "View this entity in the Entity Relationship diagram", - command: PALETTE_COMMANDS.SHOW_ENTITY_DIAGRAM, - arguments: [this.activeEditorUri.fsPath, recordName] - }; - this.codeLenses.push(codeLens); - } - - private createTryItCodeLens(range: any, position: any, basePath: string, listener: string) { - const codeLens = new CodeLens(new Range( - position.startLine, - position.startColumn, - position.endLine, - position.endColumn - )); - - codeLens.command = { - title: "Try it", - tooltip: "Try running this service", - command: PALETTE_COMMANDS.TRY_IT, - arguments: [false, undefined, { basePath, listener }, this.activeEditorUri.fsPath] - }; - this.codeLenses.push(codeLens); - } - - public getCodeLenses(): CodeLens[] { - return this.codeLenses; - } -} diff --git a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts index ab92d8ff176..43fed68700d 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts @@ -22,41 +22,46 @@ import { isSupportedSLVersion, isWindows } from "../../../utils"; import { ballerinaExtInstance } from "../../../core"; -export enum PALETTE_COMMANDS { - ADD = 'ballerina.project.add', - BUILD = 'ballerina.project.build', - PACK = 'ballerina.project.pack', - CLOUD = 'ballerina.create.cloud', - LOGIN_COPILOT = "ballerina.login.copilot", - RESET_BI = "ballerina.reset.bi", - DOC = 'ballerina.project.doc', - FOCUS_EXPLORER = 'ballerinaExplorerTreeView.focus', - RUN_CMD = 'ballerina.project.run.cmd', - RUN = 'ballerina.project.run', - SAVE_ALL = 'workbench.action.files.saveFiles', - TEST = 'ballerina.project.test', - PASTE_JSON_AS_RECORD = 'ballerina.pasteAsRecord', - PASTE_XML_AS_RECORD = 'ballerina.pasteXMLAsRecord', - CHOREO_SIGNIN = 'ballerina.choreo.signin', - CHOREO_ANON_SIGNIN = 'ballerina.choreo.anonymous.signin', - CHOREO_SIGNOUT = 'ballerina.choreo.signout', - FOCUS_SOURCE_CONTROL = 'workbench.view.scm', - CHOREO_SYNC_CHANGES = 'ballerina.choreo.sync', - PERFORMANCE_FORECAST_ENABLE = 'performance.forecasting.enable', - PERFORMANCE_FORECAST_DISABLE = 'performance.forecasting.disable', - TRY_IT = 'ballerina.tryit', - OPEN_IN_DIAGRAM = 'ballerina.openIn.diagram', - SHOW_DIAGRAM = 'ballerina.show.diagram', - SHOW_SOURCE = 'ballerina.show.source', - SHOW_ARCHITECTURE_VIEW = 'ballerina.view.architectureView', - SHOW_EXAMPLES = 'ballerina.showExamples', - REFRESH_SHOW_ARCHITECTURE_VIEW = "ballerina.view.architectureView.refresh", - RUN_CONFIG = 'ballerina.project.run.config', - CONFIG_CREATE_COMMAND = 'ballerina.project.config.create', - SHOW_ENTITY_DIAGRAM = 'ballerina.view.entityDiagram', - SHOW_SERVICE_DESIGNER_VIEW = 'ballerina.view.serviceDesigner', - SHOW_GRAPHQL_DESIGNER_VIEW = 'ballerina.view.graphqlDesigner' -} +export const PALETTE_COMMANDS = { + ADD: 'ballerina.project.add', + BUILD: 'ballerina.project.build', + PACK: 'ballerina.project.pack', + CLOUD: 'ballerina.create.cloud', + LOGIN_COPILOT: "ballerina.login.copilot", + RESET_BI: "ballerina.reset.bi", + DOC: 'ballerina.project.doc', + FOCUS_EXPLORER: 'ballerinaExplorerTreeView.focus', + RUN_CMD: 'ballerina.project.run.cmd', + RUN: 'ballerina.project.run', + SAVE_ALL: 'workbench.action.files.saveFiles', + TEST: 'ballerina.project.test', + PASTE_JSON_AS_RECORD: 'ballerina.pasteAsRecord', + PASTE_XML_AS_RECORD: 'ballerina.pasteXMLAsRecord', + CHOREO_SIGNIN: 'ballerina.choreo.signin', + CHOREO_ANON_SIGNIN: 'ballerina.choreo.anonymous.signin', + CHOREO_SIGNOUT: 'ballerina.choreo.signout', + FOCUS_SOURCE_CONTROL: 'workbench.view.scm', + CHOREO_SYNC_CHANGES: 'ballerina.choreo.sync', + PERFORMANCE_FORECAST_ENABLE: 'performance.forecasting.enable', + PERFORMANCE_FORECAST_DISABLE: 'performance.forecasting.disable', + TRY_IT: 'ballerina.tryIt', + OPEN_IN_DIAGRAM: 'ballerina.openIn.diagram', + SHOW_DIAGRAM: 'ballerina.show.diagram', + SHOW_SOURCE: 'ballerina.show.source', + SHOW_ARCHITECTURE_VIEW: 'ballerina.view.architectureView', + SHOW_EXAMPLES: 'ballerina.showExamples', + REFRESH_SHOW_ARCHITECTURE_VIEW: "ballerina.view.architectureView.refresh", + RUN_CONFIG: 'ballerina.project.run.config', + CONFIG_CREATE_COMMAND: 'ballerina.project.config.create', + SHOW_ENTITY_DIAGRAM: 'ballerina.view.entityDiagram', + SHOW_SERVICE_DESIGNER_VIEW: 'ballerina.view.serviceDesigner', + SHOW_GRAPHQL_DESIGNER_VIEW: 'ballerina.view.graphqlDesigner' +}; + +export const INTERNAL_DEBUG_COMMAND = "ballerina.internal.debug"; +export const SOURCE_DEBUG_COMMAND = "ballerina.source.debug"; +export const TEST_DEBUG_COMMAND = "ballerina.test.debug"; +export const FOCUS_DEBUG_CONSOLE_COMMAND = 'workbench.debug.action.focusRepl'; export enum BALLERINA_COMMANDS { TEST = "test", BUILD = "build", FORMAT = "format", RUN = "run", RUN_WITH_WATCH = "run --watch", DOC = "doc", @@ -168,7 +173,7 @@ export function runCommandWithConf(file: BallerinaProject | string, executor: st terminal.sendText(commandText, true); } -export function runTerminalCommand(executor: string, file?: BallerinaProject | string, env? : { [key: string]:string }) { +export function runTerminalCommand(executor: string, file?: BallerinaProject | string, env?: { [key: string]: string }) { let filePath = ''; typeof file === 'string' ? filePath = file : filePath = file?.path!; if (!terminal) { @@ -183,7 +188,7 @@ export function clearTerminal(): void { } } -export function createTerminal(path: string, env? : { [key: string]:string }): void { +export function createTerminal(path: string, env?: { [key: string]: string }): void { if (terminal) { terminal = window.createTerminal({ name: TERMINAL_NAME, cwd: path, env: env }); } diff --git a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/configRun.ts b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/configRun.ts index a36afc39675..1dec715093a 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/configRun.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/configRun.ts @@ -16,12 +16,11 @@ * under the License. */ -import { commands, languages, Uri, window } from "vscode"; +import { commands, languages, Uri, window, workspace } from "vscode"; import { BALLERINA_COMMANDS, getRunCommand, PALETTE_COMMANDS, runCommand } from "./cmd-runner"; import { ballerinaExtInstance } from "../../../core"; -import { prepareAndGenerateConfig } from "../../config-generator/configGenerator"; import { getConfigCompletions } from "../../config-generator/utils"; - +import { BiDiagramRpcManager } from "../../../rpc-managers/bi-diagram/rpc-manager"; function activateConfigRunCommand() { // register the config view run command @@ -37,10 +36,16 @@ function activateConfigRunCommand() { commands.registerCommand(PALETTE_COMMANDS.CONFIG_CREATE_COMMAND, async () => { try { - const currentProject = ballerinaExtInstance.getDocumentContext().getCurrentProject(); - const filePath = window.activeTextEditor.document; - const path = filePath.uri.fsPath; - prepareAndGenerateConfig(ballerinaExtInstance, currentProject ? currentProject.path! : path, true); + // Open current config.toml or create a new config.toml if it does not exist + let projectPath: string; + if (window.activeTextEditor) { + projectPath = window.activeTextEditor.document.uri.fsPath; + } else if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { + projectPath = workspace.workspaceFolders[0].uri.fsPath; + } + + const biDiagramRpcManager = new BiDiagramRpcManager(); + await biDiagramRpcManager.openConfigToml({ filePath: projectPath }); return; } catch (error) { throw new Error("Unable to create Config.toml file. Try again with a valid Ballerina file open in the editor."); diff --git a/workspaces/ballerina/ballerina-extension/src/features/tryit/activator.ts b/workspaces/ballerina/ballerina-extension/src/features/tryit/activator.ts index c1e2eefd8c3..36106b1ab85 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/tryit/activator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/tryit/activator.ts @@ -25,7 +25,7 @@ import { BallerinaExtension } from "src/core"; import Handlebars from "handlebars"; import { clientManager, findRunningBallerinaProcesses, handleError, HTTPYAC_CONFIG_TEMPLATE, TRYIT_TEMPLATE, waitForBallerinaService } from "./utils"; import { BIDesignModelResponse, OpenAPISpec } from "@wso2/ballerina-core"; -import { startDebugging } from "../editor-support/codelens-provider"; +import { startDebugging } from "../editor-support/activator"; import { v4 as uuidv4 } from "uuid"; import { createGraphqlView } from "../../views/graphql"; 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 a170a1315cc..0080b21e004 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 @@ -661,12 +661,12 @@ export class BiDiagramRpcManager implements BIDiagramAPI { } - // Function to open config toml + // Function to open Config.toml async openConfigToml(params: OpenConfigTomlRequest): Promise { return new Promise(async (resolve) => { const currentProject: BallerinaProject | undefined = await getCurrentBIProject(params.filePath); - const configFilePath = path.join(StateMachine.context().projectUri, "config.toml"); + const configFilePath = path.join(StateMachine.context().projectUri, "Config.toml"); const ignoreFile = path.join(StateMachine.context().projectUri, ".gitignore"); const docLink = "https://ballerina.io/learn/provide-values-to-configurable-variables/#provide-via-toml-syntax"; const uri = Uri.file(configFilePath); @@ -694,8 +694,8 @@ export class BiDiagramRpcManager implements BIDiagramAPI { if (fs.existsSync(ignoreFile)) { const ignoreUri = Uri.file(ignoreFile); let ignoreContent: string = fs.readFileSync(ignoreUri.fsPath, 'utf8'); - if (!ignoreContent.includes("config.toml")) { - ignoreContent += `\n${"config.toml"}\n`; + if (!ignoreContent.includes("Config.toml")) { + ignoreContent += `\n${"Config.toml"}\n`; fs.writeFile(ignoreUri.fsPath, ignoreContent, function (error) { if (error) { return window.showErrorMessage('Unable to update the .gitIgnore file: ' + error); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/service-designer/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/service-designer/rpc-handler.ts index 9fa559e6431..2dabb631e0f 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/service-designer/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/service-designer/rpc-handler.ts @@ -18,31 +18,35 @@ * THIS FILE INCLUDES AUTO GENERATED CODE */ import { - ExportOASRequest, - FunctionModelRequest, - FunctionSourceCodeRequest, - HttpResourceModelRequest, - ListenerModelFromCodeRequest, - ListenerModelRequest, - ListenerSourceCodeRequest, - ListenersRequest, - ServiceModelFromCodeRequest, - ServiceModelRequest, - ServiceSourceCodeRequest, - TriggerModelsRequest, addFunctionSourceCode, addListenerSourceCode, addResourceSourceCode, addServiceSourceCode, exportOASFile, + ExportOASRequest, + FunctionFromSourceRequest, + FunctionModelRequest, + FunctionSourceCodeRequest, + getFunctionFromSource, getFunctionModel, getHttpResourceModel, getListenerModel, getListenerModelFromCode, getListeners, + getResourceReturnTypes, getServiceModel, getServiceModelFromCode, getTriggerModels, + HttpResourceModelRequest, + ListenerModelFromCodeRequest, + ListenerModelRequest, + ListenerSourceCodeRequest, + ListenersRequest, + ResourceReturnTypesRequest, + ServiceModelFromCodeRequest, + ServiceModelRequest, + ServiceSourceCodeRequest, + TriggerModelsRequest, updateListenerSourceCode, updateResourceSourceCode, updateServiceSourceCode @@ -61,10 +65,12 @@ export function registerServiceDesignerRpcHandlers(messenger: Messenger) { messenger.onRequest(getListenerModelFromCode, (args: ListenerModelFromCodeRequest) => rpcManger.getListenerModelFromCode(args)); messenger.onRequest(getServiceModel, (args: ServiceModelRequest) => rpcManger.getServiceModel(args)); messenger.onRequest(getFunctionModel, (args: FunctionModelRequest) => rpcManger.getFunctionModel(args)); + messenger.onRequest(getFunctionFromSource, (args: FunctionFromSourceRequest) => rpcManger.getFunctionFromSource(args)); messenger.onRequest(addServiceSourceCode, (args: ServiceSourceCodeRequest) => rpcManger.addServiceSourceCode(args)); messenger.onRequest(updateServiceSourceCode, (args: ServiceSourceCodeRequest) => rpcManger.updateServiceSourceCode(args)); messenger.onRequest(getServiceModelFromCode, (args: ServiceModelFromCodeRequest) => rpcManger.getServiceModelFromCode(args)); messenger.onRequest(getHttpResourceModel, (args: HttpResourceModelRequest) => rpcManger.getHttpResourceModel(args)); + messenger.onRequest(getResourceReturnTypes, (args: ResourceReturnTypesRequest) => rpcManger.getResourceReturnTypes(args)); messenger.onRequest(addResourceSourceCode, (args: FunctionSourceCodeRequest) => rpcManger.addResourceSourceCode(args)); messenger.onRequest(addFunctionSourceCode, (args: FunctionSourceCodeRequest) => rpcManger.addFunctionSourceCode(args)); messenger.onRequest(updateResourceSourceCode, (args: FunctionSourceCodeRequest) => rpcManger.updateResourceSourceCode(args)); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/service-designer/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/service-designer/rpc-manager.ts index 4025d35ce0b..f5f9b94de7b 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/service-designer/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/service-designer/rpc-manager.ts @@ -18,8 +18,11 @@ * THIS FILE INCLUDES AUTO GENERATED CODE */ import { + DIRECTORY_MAP, ExportOASRequest, ExportOASResponse, + FunctionFromSourceRequest, + FunctionFromSourceResponse, FunctionModelRequest, FunctionModelResponse, FunctionSourceCodeRequest, @@ -34,29 +37,25 @@ import { ListenersRequest, ListenersResponse, OpenAPISpec, + ResourceReturnTypesRequest, + ResourceReturnTypesResponse, ResourceSourceCodeResponse, - STModification, ServiceDesignerAPI, ServiceModelFromCodeRequest, ServiceModelFromCodeResponse, ServiceModelRequest, ServiceModelResponse, ServiceSourceCodeRequest, - UpdatedArtifactsResponse, - SyntaxTree, TriggerModelsRequest, TriggerModelsResponse, - DIRECTORY_MAP + UpdatedArtifactsResponse } from "@wso2/ballerina-core"; -import { NodePosition } from "@wso2/syntax-tree"; import * as fs from 'fs'; -import { existsSync, writeFileSync } from "fs"; import * as yaml from 'js-yaml'; import * as path from 'path'; -import * as vscode from "vscode"; -import { Uri, window, workspace } from "vscode"; -import { StateMachine } from "../../stateMachine"; +import { window, workspace } from "vscode"; import { extension } from "../../BalExtensionContext"; +import { StateMachine } from "../../stateMachine"; import { updateSourceCode } from "../../utils/source-utils"; export class ServiceDesignerRpcManager implements ServiceDesignerAPI { @@ -388,4 +387,30 @@ export class ServiceDesignerRpcManager implements ServiceDesignerAPI { console.log(`>>> Created file at ${targetFile}`); } } + + async getResourceReturnTypes(params: ResourceReturnTypesRequest): Promise { + return new Promise(async (resolve) => { + const context = StateMachine.context(); + params.filePath = StateMachine.context().projectUri; + params.context = "HTTP_STATUS_CODE"; + try { + const res: ResourceReturnTypesResponse = await context.langClient.getResourceReturnTypes(params); + resolve(res); + } catch (error) { + console.log(">>> error fetching resource return types", error); + } + }); + } + + async getFunctionFromSource(params: FunctionFromSourceRequest): Promise { + return new Promise(async (resolve) => { + const context = StateMachine.context(); + try { + const res: FunctionFromSourceResponse = await context.langClient.getFunctionFromSource(params); + resolve(res); + } catch (error) { + console.log(">>> error fetching function model", error); + } + }); + } } diff --git a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts index ed49d2e4cbb..d5819faaa1c 100644 --- a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts @@ -1,5 +1,5 @@ -import { ExtendedLangClient } from './core'; +import { ballerinaExtInstance, 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 } from "@wso2/ballerina-core"; @@ -66,15 +66,40 @@ const stateMachine = createMachine( initialize: { invoke: { src: checkForProjects, + onDone: [ + { + target: "renderInitialView", + cond: (context, event) => event.data && event.data.isBI, + 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, + }) + }, + { + target: "activateLS", + cond: (context, event) => event.data && event.data.isBI === false, + 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, + }) + } + ], + onError: { + target: "renderInitialView" + } + } + }, + renderInitialView: { + invoke: { + src: 'openWebView', onDone: { - target: "activateLS", - 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, - }) + target: "activateLS" }, onError: { target: "activateLS" @@ -256,6 +281,7 @@ const stateMachine = createMachine( // Get context values from the project storage so that we can restore the earlier state when user reopens vscode return new Promise((resolve, reject) => { if (!VisualizerWebview.currentPanel) { + ballerinaExtInstance.setContext(extension.context); VisualizerWebview.currentPanel = new VisualizerWebview(); RPCLayer._messenger.onNotification(webviewReady, () => { history = new History(); @@ -314,7 +340,7 @@ const stateMachine = createMachine( return resolve({ ...selectedEntry.location, view: selectedEntry.location.view ? selectedEntry.location.view : MACHINE_VIEW.Overview }); } - if (selectedEntry && selectedEntry.location.view === MACHINE_VIEW.ERDiagram) { + if (selectedEntry && (selectedEntry.location.view === MACHINE_VIEW.ERDiagram || selectedEntry.location.view === MACHINE_VIEW.ServiceDesigner || selectedEntry.location.view === MACHINE_VIEW.BIDiagram)) { return resolve(selectedEntry.location); } @@ -329,6 +355,7 @@ const stateMachine = createMachine( const { documentUri, position } = location; + // TODO: Refactor this to remove the full ST request const node = documentUri && await StateMachine.langClient().getSyntaxTree({ documentIdentifier: { uri: Uri.file(documentUri).toString() @@ -535,7 +562,7 @@ async function handleSingleWorkspace(workspaceURI: any) { console.error("No BI enabled workspace found"); } - return { isBI, projectPath, scope, orgName, packageName }; + return { isBI, projectPath, scope, orgName, packageName }; } function setBIContext(isBI: boolean) { diff --git a/workspaces/ballerina/ballerina-extension/src/utils/runCommand.ts b/workspaces/ballerina/ballerina-extension/src/utils/runCommand.ts index 47da379d336..49dddf0f045 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/runCommand.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/runCommand.ts @@ -16,15 +16,10 @@ * under the License. */ -import { PALETTE_COMMANDS } from 'src/features/project'; import * as vscode from 'vscode'; import child_process from 'child_process'; import { CommandResponse } from '@wso2/ballerina-core'; -export function runCommand(command: PALETTE_COMMANDS, args: any[]) { - vscode.commands.executeCommand(command, ...args); -} - export async function runBackgroundTerminalCommand(command: string) { return new Promise(function (resolve) { child_process.exec(`${command}`, async (err, stdout, stderr) => { diff --git a/workspaces/ballerina/ballerina-extension/src/utils/source-utils.ts b/workspaces/ballerina/ballerina-extension/src/utils/source-utils.ts index f9c3ae010e7..c69414be8f5 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/source-utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/source-utils.ts @@ -93,29 +93,52 @@ export async function updateSourceCode(updateSourceCodeRequest: UpdateSourceCode // Iterate through modificationRequests and apply modifications try { + // <-------- Using simply the text edits to update the source code --------> const workspaceEdit = new vscode.WorkspaceEdit(); for (const [fileUriString, request] of Object.entries(modificationRequests)) { - const { parseSuccess, source, syntaxTree } = (await StateMachine.langClient().stModify({ - documentIdentifier: { uri: fileUriString }, - astModifications: request.modifications, - })) as SyntaxTree; - - if (parseSuccess) { + for (const modification of request.modifications) { const fileUri = Uri.file(request.filePath); + const source = modification.config.STATEMENT; workspaceEdit.replace( fileUri, new vscode.Range( - new vscode.Position(0, 0), - new vscode.Position(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) + new vscode.Position(modification.startLine, modification.startColumn), + new vscode.Position(modification.endLine, modification.endColumn) ), source ); } } - // Apply all changes at once await workspace.applyEdit(workspaceEdit); + // <-------- Format the document after applying all changes using the native formatting API--------> + const formattedWorkspaceEdit = new vscode.WorkspaceEdit(); + for (const [fileUriString, request] of Object.entries(modificationRequests)) { + const fileUri = Uri.file(request.filePath); + const formattedSources: { newText: string, range: { start: { line: number, character: number }, end: { line: number, character: number } } }[] = await StateMachine.langClient().sendRequest("textDocument/formatting", { + textDocument: { uri: fileUriString }, + options: { + tabSize: 4, + insertSpaces: true + } + }); + for (const formattedSource of formattedSources) { + // Replace the entire document content with the formatted text to avoid duplication + formattedWorkspaceEdit.replace( + fileUri, + new vscode.Range( + new vscode.Position(0, 0), + new vscode.Position(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) + ), + formattedSource.newText + ); + } + } + + // Apply all formatted changes at once + await workspace.applyEdit(formattedWorkspaceEdit); + // Handle missing dependencies after all changes are applied if (updateSourceCodeRequest.resolveMissingDependencies) { for (const [fileUriString] of Object.entries(modificationRequests)) { diff --git a/workspaces/ballerina/ballerina-extension/src/views/visualizer/activate.ts b/workspaces/ballerina/ballerina-extension/src/views/visualizer/activate.ts index 3c38dc1d6e1..557e5d48bd1 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/visualizer/activate.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/visualizer/activate.ts @@ -20,7 +20,7 @@ import * as vscode from 'vscode'; import { PALETTE_COMMANDS } from '../../features/project/cmds/cmd-runner'; import { StateMachine, openView } from '../../stateMachine'; import { extension } from '../../BalExtensionContext'; -import { BI_COMMANDS, EVENT_TYPE, MACHINE_VIEW, SHARED_COMMANDS } from '@wso2/ballerina-core'; +import { BI_COMMANDS, EVENT_TYPE, MACHINE_VIEW, NodePosition, SHARED_COMMANDS } from '@wso2/ballerina-core'; import { ViewColumn } from 'vscode'; import { buildProjectArtifactsStructure } from '../../utils/project-artifacts'; @@ -46,9 +46,20 @@ export function activateSubscriptions() { // <------------- Shared Commands ------------> context.subscriptions.push( vscode.commands.registerCommand(SHARED_COMMANDS.SHOW_VISUALIZER, (path: string | vscode.Uri, position, resetHistory = false) => { - const documentPath = path ? (typeof path === "string" ? path : path.fsPath) : ""; + // Check if position is a LineRange object (has 'start' and 'end' keys) + let nodePosition: NodePosition = position; + if (position && typeof position === "object" && "start" in position && "end" in position) { + // Convert LineRange to NodePosition + nodePosition = { + startLine: position.start.line, + startColumn: position.start.character, + endLine: position.end.line, + endColumn: position.end.character + }; + } + const documentPath = path ? (typeof path === "string" ? vscode.Uri.parse(path).fsPath : path.fsPath) : ""; if (StateMachine.langClient() && StateMachine.context().isBISupported) { // This is added since we can't fetch new diagram data without bi supported ballerina version - openView(EVENT_TYPE.OPEN_VIEW, { documentUri: documentPath || vscode.window.activeTextEditor?.document.uri.fsPath, position: position }, resetHistory); + openView(EVENT_TYPE.OPEN_VIEW, { documentUri: documentPath || vscode.window.activeTextEditor?.document.uri.fsPath, position: nodePosition }, resetHistory); } else { openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.BallerinaUpdateView }); // Redirect user to the ballerina update available page } diff --git a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts index 658523db3d6..b4f6bdd7dd4 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts @@ -48,13 +48,17 @@ export class VisualizerWebview { }, 500); vscode.workspace.onDidChangeTextDocument(async (document) => { - await document.document.save(); const state = StateMachine.state(); const machineReady = typeof state === 'object' && 'viewActive' in state && state.viewActive === "viewReady"; + // Save the document only if it is not already opened in a visible editor or the webview is active + const isOpened = vscode.window.visibleTextEditors.some(editor => editor.document.uri.toString() === document.document.uri.toString()); + if (!isOpened || this._panel?.active) { + await document.document.save(); + } if (this._panel?.active && machineReady && document && document.document.languageId === LANGUAGE.BALLERINA) { sendUpdateNotificationToWebview(); } else if (machineReady && document?.document && document.document.languageId === LANGUAGE.TOML && document.document.fileName.endsWith("Config.toml") && - vscode.window.visibleTextEditors.some(editor => editor.document.fileName === document.document.fileName)){ + vscode.window.visibleTextEditors.some(editor => editor.document.fileName === document.document.fileName)) { sendUpdateNotificationToWebview(true); } }, extension.context); @@ -109,7 +113,16 @@ export class VisualizerWebview { private getWebviewContent(webView: Webview) { const body = `
-
+
+
+
+
+

WSO2 Integrator: BI

+

Setting up your workspace and tools

+
+ Loading +
+
`; const bodyCss = ``; @@ -123,9 +136,10 @@ export class VisualizerWebview { .loader-wrapper { display: flex; justify-content: center; - align-items: center; + align-items: flex-start; height: 100%; width: 100%; + padding-top: 30vh; } .loader { width: 32px; @@ -151,6 +165,56 @@ export class VisualizerWebview { 50% {transform:scaleY(-1) rotate(0deg)} 100% {transform:scaleY(-1) rotate(-135deg)} } + /* New welcome view styles */ + .welcome-content { + text-align: center; + max-width: 500px; + padding: 2rem; + animation: fadeIn 1s ease-in-out; + font-family: var(--vscode-font-family); + } + .logo-container { + margin-bottom: 2rem; + display: flex; + justify-content: center; + } + .welcome-title { + color: var(--vscode-foreground); + margin: 0 0 0.5rem 0; + letter-spacing: -0.02em; + font-size: 1.5em; + font-weight: 400; + line-height: normal; + } + .welcome-subtitle { + color: var(--vscode-descriptionForeground); + font-size: 13px; + margin: 0 0 2rem 0; + opacity: 0.8; + } + .loading-text { + color: var(--vscode-foreground); + font-size: 13px; + font-weight: 500; + } + .loading-dots::after { + content: ''; + animation: dots 1.5s infinite; + } + @keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @keyframes dots { + 0%, 20% { content: ''; } + 40% { content: '.'; } + 60% { content: '..'; } + 80%, 100% { content: '...'; } + } `; const scripts = ` function loadedScript() { diff --git a/workspaces/ballerina/ballerina-extension/webpack.config.js b/workspaces/ballerina/ballerina-extension/webpack.config.js index 2db9c13e27f..1591d571bd3 100644 --- a/workspaces/ballerina/ballerina-extension/webpack.config.js +++ b/workspaces/ballerina/ballerina-extension/webpack.config.js @@ -6,30 +6,20 @@ const path = require('path'); const MergeIntoSingleFile = require('webpack-merge-and-include-globally'); const dotenv = require('dotenv'); const webpack = require('webpack'); +const { createEnvDefinePlugin } = require('../../../common/scripts/env-webpack-helper'); const envPath = path.resolve(__dirname, '.env'); const env = dotenv.config({ path: envPath }).parsed; - -function shouldSkipEnvVar(key) { - const pathVariables = ['PATH', 'Path']; - return pathVariables.includes(key); +console.log("Fetching values for environment variables..."); +const { envKeys, missingVars } = createEnvDefinePlugin(env); +if (missingVars.length > 0) { + console.warn( + '\n⚠️ Environment Variable Configuration Warning:\n' + + `Missing required environment variables: ${missingVars.join(', ')}\n` + + `Please provide values in either .env file or runtime environment.\n` + ); } -const filteredProcessEnv = Object.fromEntries( - Object.entries(process.env).filter(([key, value]) => !shouldSkipEnvVar(key)) -); - -const mergedEnv = { ...env, ...filteredProcessEnv }; - -const envKeys = Object.fromEntries( - Object.entries(mergedEnv) - .filter(([key, value]) => key && value !== undefined && value !== '') - .map(([key, value]) => [ - `process.env.${key}`, - JSON.stringify(value), - ]) -); - /** @type {import('webpack').Configuration} */ module.exports = { watch: false, diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/service-designer/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/service-designer/rpc-client.ts index a774cdfe7a1..0f4d33beae2 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/service-designer/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/service-designer/rpc-client.ts @@ -20,6 +20,8 @@ import { ExportOASRequest, ExportOASResponse, + FunctionFromSourceRequest, + FunctionFromSourceResponse, FunctionModelRequest, FunctionModelResponse, FunctionSourceCodeRequest, @@ -32,6 +34,8 @@ import { ListenerSourceCodeRequest, ListenersRequest, ListenersResponse, + ResourceReturnTypesRequest, + ResourceReturnTypesResponse, ServiceDesignerAPI, ServiceModelFromCodeRequest, ServiceModelFromCodeResponse, @@ -46,11 +50,13 @@ import { addResourceSourceCode, addServiceSourceCode, exportOASFile, + getFunctionFromSource, getFunctionModel, getHttpResourceModel, getListenerModel, getListenerModelFromCode, getListeners, + getResourceReturnTypes, getServiceModel, getServiceModelFromCode, getTriggerModels, @@ -104,6 +110,10 @@ export class ServiceDesignerRpcClient implements ServiceDesignerAPI { return this._messenger.sendRequest(getFunctionModel, HOST_EXTENSION, params); } + getFunctionFromSource(params: FunctionFromSourceRequest): Promise { + return this._messenger.sendRequest(getFunctionFromSource, HOST_EXTENSION, params); + } + addServiceSourceCode(params: ServiceSourceCodeRequest): Promise { return this._messenger.sendRequest(addServiceSourceCode, HOST_EXTENSION, params); } @@ -120,6 +130,10 @@ export class ServiceDesignerRpcClient implements ServiceDesignerAPI { return this._messenger.sendRequest(getHttpResourceModel, HOST_EXTENSION, params); } + getResourceReturnTypes(params: ResourceReturnTypesRequest): Promise { + return this._messenger.sendRequest(getResourceReturnTypes, HOST_EXTENSION, params); + } + addResourceSourceCode(params: FunctionSourceCodeRequest): Promise { return this._messenger.sendRequest(addResourceSourceCode, HOST_EXTENSION, params); } diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx index fbdd3bd3ede..cc18ba4506b 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx @@ -351,7 +351,6 @@ export const Form = forwardRef((props: FormProps, ref) => { const { infoLabel, formFields, - projectPath, selectedNode, submitText, cancelText, @@ -361,7 +360,6 @@ export const Form = forwardRef((props: FormProps, ref) => { onCancelForm, oneTimeForm, openRecordEditor, - openView, openSubPanel, subPanelView, expressionEditor, @@ -393,7 +391,7 @@ export const Form = forwardRef((props: FormProps, ref) => { setValue, setError, clearErrors, - formState: { isValidating, errors, isDirty, isValid: isFormValid, dirtyFields }, + formState: { isValidating, errors, dirtyFields }, } = useForm(); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); @@ -401,10 +399,13 @@ export const Form = forwardRef((props: FormProps, ref) => { const [diagnosticsInfo, setDiagnosticsInfo] = useState(undefined); const [isMarkdownExpanded, setIsMarkdownExpanded] = useState(false); const [isIdentifierEditing, setIsIdentifierEditing] = useState(false); + const [isSubComponentEnabled, setIsSubComponentEnabled] = useState(false); + const markdownRef = useRef(null); const [isUserConcert, setIsUserConcert] = useState(false); + useEffect(() => { // Check if the form is a onetime usage or not. This is checked due to reset issue with nested forms in param manager if (!oneTimeForm) { @@ -580,7 +581,6 @@ export const Form = forwardRef((props: FormProps, ref) => { const variableField = formFields.find((field) => field.key === "variable"); const typeField = formFields.find((field) => field.key === "type"); const targetTypeField = formFields.find((field) => field.codedata?.kind === "PARAM_FOR_TYPE_INFER"); - const dataMapperField = formFields.find((field) => field.label.includes("Data mapper")); const hasParameters = hasRequiredParameters(formFields, selectedNode) || hasOptionalParameters(formFields); const contextValue: FormContext = { @@ -654,7 +654,7 @@ export const Form = forwardRef((props: FormProps, ref) => { const disableSaveButton = !isValid || isValidating || props.disableSaveButton || (concertMessage && concertRequired && !isUserConcert) || - isIdentifierEditing || Object.keys(errors).length > 0; + isIdentifierEditing || isSubComponentEnabled || Object.keys(errors).length > 0; const handleShowMoreClick = () => { setIsMarkdownExpanded(!isMarkdownExpanded); @@ -731,6 +731,7 @@ export const Form = forwardRef((props: FormProps, ref) => { visualizableFields={visualizableFields} recordTypeFields={recordTypeFields} onIdentifierEditingStateChange={handleIdentifierEditingStateChange} + setSubComponentEnabled={setIsSubComponentEnabled} /> ); 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 707d4066841..4f8244af31c 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts @@ -17,7 +17,7 @@ */ import { RefObject } from "react"; -import { DiagnosticMessage, FormDiagnostics, TextEdit, PropertyModel, LinePosition, LineRange, ExpressionProperty, Metadata, RecordTypeField, Imports } from "@wso2/ballerina-core"; +import { DiagnosticMessage, FormDiagnostics, TextEdit, PropertyModel, LinePosition, LineRange, ExpressionProperty, Metadata, RecordTypeField, Imports } from "@wso2/ballerina-core"; import { ParamConfig } from "../ParamManager/ParamManager"; import { CompletionItem, FormExpressionEditorRef, HelperPaneHeight, HelperPaneOrigin, OptionProps } from "@wso2/ui-toolkit"; @@ -53,8 +53,9 @@ export type FormField = { enabled: boolean; lineRange?: LineRange; metadata?: Metadata; - codedata?: {[key: string]: any}; - imports?: {[key: string]: string}; + codedata?: { [key: string]: any }; + imports?: { [key: string]: string }; + onValueChange?: (value: string) => void; }; export type ParameterValue = { 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 bd9195d95ac..81a51ec9a85 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx @@ -201,6 +201,31 @@ namespace S { width: 100%; margin-top: 20px; `; + + export const ShowMoreContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + height: 35px; + cursor: pointer; + border: 1px dashed ${ThemeColors.OUTLINE_VARIANT}; + border-radius: 5px; + &:hover { + background-color: ${ThemeColors.PRIMARY_CONTAINER}; + border: 1px dashed ${ThemeColors.PRIMARY}; + border-radius: 5px; + } + `; + + export const ShowMoreTitle = styled.div` + white-space: nowrap; + justify-items: center; + align-items: center; + color: ${ThemeColors.ON_SURFACE_VARIANT}; + opacity: 0.7; + `; } interface NodeListProps { @@ -328,6 +353,9 @@ export function NodeList(props: NodeListProps) { ); const getCategoryContainer = (groups: Category[], isSubCategory = false) => { + const callFunctionNode = groups + .flatMap((group) => group?.items) + .find((item) => "id" in item && item.id === "FUNCTION"); const content = ( <> {groups.map((group, index) => { @@ -444,6 +472,13 @@ export function NodeList(props: NodeListProps) { ); })} + {callFunctionNode && ( + + handleAddNode(callFunctionNode as Node)}> + Show More Functions + + + )} ); 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 f9a40477e01..551901dbae5 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamManager.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamManager.tsx @@ -54,6 +54,7 @@ export interface ParamManagerProps { openRecordEditor?: (open: boolean) => void; readonly?: boolean; selectedNode?: NodeKind; + setSubComponentEnabled?: (isAdding: boolean) => void; } const AddButtonWrapper = styled.div` @@ -110,10 +111,11 @@ export interface ParamManagerEditorProps { handleOnFieldFocus?: (key: string) => void; openRecordEditor?: (open: boolean) => void; selectedNode?: NodeKind; + setSubComponentEnabled?: (isAdding: boolean) => void; } export function ParamManagerEditor(props: ParamManagerEditorProps) { - const { field, openRecordEditor, selectedNode } = props; + const { field, openRecordEditor, selectedNode, setSubComponentEnabled } = props; const { form } = useFormContext(); const { control, setValue, getValues } = form; @@ -156,6 +158,7 @@ export function ParamManagerEditor(props: ParamManagerEditorProps) { onChange(config.paramValues); }} selectedNode={selectedNode} + setSubComponentEnabled={setSubComponentEnabled} /> {error && } @@ -203,7 +206,7 @@ export function ParamManagerEditor(props: ParamManagerEditorProps) { } export function ParamManager(props: ParamManagerProps) { - const { propertyKey, paramConfigs, readonly, onChange, openRecordEditor, selectedNode } = props; + const { propertyKey, paramConfigs, readonly, onChange, openRecordEditor, selectedNode, setSubComponentEnabled } = props; const { rpcClient } = useRpcContext(); const [editingSegmentId, setEditingSegmentId] = useState(-1); @@ -214,6 +217,7 @@ export function ParamManager(props: ParamManagerProps) { const onEdit = (param: Parameter) => { setEditingSegmentId(param.id); + setSubComponentEnabled?.(true); }; const getNewParam = (fields: FormField[], index: number): Parameter => { @@ -239,6 +243,7 @@ export function ParamManager(props: ParamManagerProps) { updatedParameters.push(newParams); setParameters(updatedParameters); setIsNew(true); + setSubComponentEnabled?.(true); }; const onDelete = (param: Parameter) => { @@ -269,10 +274,12 @@ export function ParamManager(props: ParamManagerProps) { onChangeParam(paramConfig); setEditingSegmentId(-1); setIsNew(false); + setSubComponentEnabled?.(false); }; const onParamEditCancel = (param: Parameter) => { setEditingSegmentId(-1); + setSubComponentEnabled?.(false); if (isNew) { onDelete(param); } diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/DropdownEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/DropdownEditor.tsx index 9965b7c2e67..53141e52b04 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/DropdownEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/DropdownEditor.tsx @@ -16,7 +16,7 @@ * under the License. */ -import React from "react"; +import React, { useEffect } from "react"; import { Dropdown } from "@wso2/ui-toolkit"; @@ -35,6 +35,13 @@ export function DropdownEditor(props: DropdownEditorProps) { const { form } = useFormContext(); const { register, setValue } = form; + useEffect(() => { + // if field.key is "modelType" and value is not set and default value set. then setValue to default value initially + if (field.key === "modelType" && field.value === undefined && (field.defaultValue || field.placeholder)) { + setValue(field.key, field.defaultValue || field.placeholder); + } + }, [field.defaultValue, field.placeholder]); + // HACK: create values for Scope field if (field.key === "scope") { field.items = ["Global", "Local"]; @@ -43,12 +50,16 @@ export function DropdownEditor(props: DropdownEditorProps) { return ( ({ id: item, content: item, value: item }))} required={!field.optional} disabled={!field.editable} - onChange={(e) => setValue(field.key, e.target.value)} + onChange={(e) => { + setValue(field.key, e.target.value); + field.onValueChange?.(e.target.value); + }} sx={{ width: "100%" }} containerSx={{ width: "100%" }} addNewBtnClick={field.addNewButton ? () => openSubPanel({ view: SubPanelView.ADD_NEW_FORM }) : undefined} 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 7f07e97db5c..9763e2ad4ac 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx @@ -40,6 +40,7 @@ import { ReadonlyField } from "./ReadonlyField"; import { ContextAwareRawExpressionEditor } from "./RawExpressionEditor"; import { IdentifierField } from "./IdentifierField"; import { PathEditor } from "./PathEditor"; +import { HeaderSetEditor } from "./HeaderSetEditor"; interface FormFieldEditorProps { field: FormField; @@ -53,6 +54,7 @@ interface FormFieldEditorProps { visualizableFields?: string[]; recordTypeFields?: RecordTypeField[]; onIdentifierEditingStateChange?: (isEditing: boolean) => void; + setSubComponentEnabled?: (isAdding: boolean) => void; } export const EditorFactory = (props: FormFieldEditorProps) => { @@ -67,12 +69,15 @@ export const EditorFactory = (props: FormFieldEditorProps) => { handleOnTypeChange, visualizableFields, recordTypeFields, - onIdentifierEditingStateChange + onIdentifierEditingStateChange, + setSubComponentEnabled } = props; if (!field.enabled || field.hidden) { return <>; } else if (field.type === "MULTIPLE_SELECT") { return ; + } else if (field.type === "HEADER_SET") { + return ; } else if (field.type === "CHOICE") { return ; } else if (field.type === "DROPDOWN_CHOICE") { @@ -142,7 +147,7 @@ export const EditorFactory = (props: FormFieldEditorProps) => { // Skip this property return <>; } else if (field.type === "PARAM_MANAGER") { - return ; + return ; } else if (field.type === "REPEATABLE_PROPERTY") { return ; } else if (field.type === "IDENTIFIER" && !field.editable && field?.lineRange) { diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/HeaderSetEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/HeaderSetEditor.tsx new file mode 100644 index 00000000000..53849ad1a2c --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/HeaderSetEditor.tsx @@ -0,0 +1,290 @@ +/** + * 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, useState } from "react"; + +import { Button, Codicon, ThemeColors } from "@wso2/ui-toolkit"; +import styled from "@emotion/styled"; + +import { FormField, FormValues } from "../Form/types"; +import { useFormContext } from "../../context"; +import { ParamIcon } from "@wso2/ballerina-core"; +import Form from "../Form"; +import { ActionIconWrapper, DeleteIconWrapper, EditIconWrapper, HeaderLabel, IconTextWrapper, IconWrapper, OptionLabel } from "../ParamManager/styles"; + +namespace S { + export const Container = styled.div({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: '4px', + }); + + export const LabelContainer = styled.div({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' + }); + + export const Label = styled.label({ + color: 'var(--vscode-editor-foreground)', + textTransform: 'capitalize', + }); + + export const Description = styled.div({ + color: 'var(--vscode-list-deemphasizedForeground)', + }); + + export const DropdownContainer = styled.div({ + display: 'flex', + gap: '8px', + alignItems: 'center', + width: '100%', + }); + + export const AddNewButton = styled(Button)` + & > vscode-button { + color: var(--vscode-textLink-activeForeground); + border-radius: 0px; + padding: 3px 5px; + margin-top: 4px; + }; + & > vscode-button > * { + margin-right: 6px; + }; + `; + + export const AddNewButtonOption = styled.div({ + width: '100%', + display: 'flex', + padding: '5px', + gap: '8px', + }); + + export const DeleteButton = styled(Button)` + & > vscode-button { + color: ${ThemeColors.ERROR}; + } + `; + + export const FormSection = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '8px', + borderRadius: '5px', + padding: '10px', + border: '1px solid var(--vscode-dropdown-border)' + }); + + export const ContentSection = styled.div({ + display: 'flex', + flexDirection: 'row', + width: '300px', + justifyContent: 'space-between', + marginRight: '20px', + marginLeft: '5px' + }); +} + +interface HeaderSetEditorProps { + field: FormField; +} + +interface HeaderSet { + name: string; + type: string; + optional: boolean; +} + +export function HeaderSetEditor(props: HeaderSetEditorProps) { + const { field } = props; + const { form } = useFormContext(); + const { setValue } = form; + + const [headerSets, setHeaderSets] = useState(Array.isArray(field.value) ? field.value : []); + const [formOpen, setFormOpen] = useState(false); + + const [headerSetToEdit, setHeaderSetToEdit] = useState(null); + const [headerSetIndexToEdit, setHeaderSetIndexToEdit] = useState(null); + + const onAddAnother = () => { + setFormOpen(true); + }; + + const onDelete = (indexToDelete: number) => { + setHeaderSets(headerSets.filter((_, index) => index !== indexToDelete)); + setValue(field.key, headerSets); + }; + + const onSubmit = (data: FormValues) => { + console.log(data); + const newHeaderSet: HeaderSet = { + name: data.name, + type: data.type, + optional: Boolean(data.optional) + }; + if (headerSetIndexToEdit !== null) { + headerSets[headerSetIndexToEdit] = newHeaderSet; + } else { + headerSets.push(newHeaderSet); + } + setHeaderSets(headerSets); + setValue(field.key, headerSets); + setFormOpen(false); + }; + + + const onEditClick = (headerSet: HeaderSet, index: number) => { + setFormOpen(true); + setHeaderSetToEdit(headerSet); + setHeaderSetIndexToEdit(index); + }; + + const onCancelForm = () => { + setFormOpen(false); + setHeaderSetToEdit(null); + setHeaderSetIndexToEdit(null); + }; + + + const fields: FormField[] = [ + { + key: "name", + enabled: true, + optional: false, + editable: true, + documentation: "Name of the header", + value: headerSetToEdit?.name || "", + valueTypeConstraint: "string", + label: "Name", + type: "text" + }, + { + key: "type", + enabled: true, + optional: false, + editable: true, + documentation: "Type of the header", + value: headerSetToEdit?.type || "", + valueTypeConstraint: "string", + label: "Type", + type: "SINGLE_SELECT", + items: field.items, + }, + { + key: "optional", + enabled: true, + optional: false, + editable: true, + documentation: "Required or Optional", + value: headerSetToEdit?.optional ?? false as any, + valueTypeConstraint: "boolean", + label: "Optional", + type: "FLAG", + } + ]; + + return ( + + + {field.label} + + {field.documentation} + {headerSets.map((headerSet, index) => ( + + + + ))} + {!formOpen && + + + Add + + } + {formOpen && + +
+ + } + + ); +} + +interface HeaderSetItemProps { + headerSet: HeaderSet; + readonly: boolean; + index: number; + onDelete: (index: number) => void; + onEditClick: (headerSet: HeaderSet, index: number) => void; +} + +function HeaderSetItem(props: HeaderSetItemProps) { + const { headerSet, readonly, index, onDelete, onEditClick } = props; + + const handleDelete = () => { + onDelete(index); + }; + const handleEdit = () => { + if (!readonly) { + onEditClick(headerSet, index); + } + }; + + return ( + + + + + + + {headerSet.type ? headerSet.type.toUpperCase() : headerSet.name.toUpperCase()} {headerSet.optional ? "" : "*"} + + + +
+ {headerSet.name} +
+ {!readonly && ( + + + + + + + + + )} +
+
+ ); +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/IdentifierField.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/IdentifierField.tsx index 0b5d0392521..9c12801af94 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/IdentifierField.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/IdentifierField.tsx @@ -18,10 +18,10 @@ import { TextField } from "@wso2/ui-toolkit"; import React, { useState, useCallback } from "react"; -import { capitalize, debounce } from "lodash"; +import { debounce } from "lodash"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { useFormContext } from "../../context"; -import { getPropertyFromFormField } from "./utils"; +import { capitalize, getPropertyFromFormField } from "./utils"; import { FormField } from "../Form/types"; export interface IdentifierFieldProps { field: FormField; diff --git a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx index fc58d1f00ed..f48170523dd 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx @@ -28,13 +28,10 @@ import { import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { Global, css } from "@emotion/react"; import styled from "@emotion/styled"; -import { NavigationBar } from "./components/NavigationBar"; import { LoadingRing } from "./components/Loader"; import { DataMapper } from "./views/DataMapper"; import { ERDiagram } from "./views/ERDiagram"; import { GraphQLDiagram } from "./views/GraphQLDiagram"; -import { SequenceDiagram } from "./views/SequenceDiagram"; -import { Overview } from "./views/Overview"; import { ServiceDesigner } from "./views/BI/ServiceDesigner"; import { WelcomeView, @@ -46,7 +43,7 @@ import { TestFunctionForm } from "./views/BI"; import { handleRedo, handleUndo } from "./utils/utils"; -import { FunctionDefinition, ServiceDeclaration } from "@wso2/syntax-tree"; +import { FunctionDefinition } from "@wso2/syntax-tree"; import { URI, Utils } from "vscode-uri"; import { Typography } from "@wso2/ui-toolkit"; import { PanelType, useVisualizerContext } from "./Context"; @@ -71,6 +68,7 @@ import { ServiceClassConfig } from "./views/BI/ServiceClassEditor/ServiceClassCo 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"; const globalStyles = css` *, @@ -78,6 +76,33 @@ const globalStyles = css` *::after { box-sizing: border-box; } + + @keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + .loading-dots::after { + content: ''; + animation: dots 1.5s infinite; + } + + @keyframes dots { + 0%, 20% { content: ''; } + 40% { content: '.'; } + 60% { content: '..'; } + 80%, 100% { content: '...'; } + } +`; + +const ProgressRing = styled(VSCodeProgressRing)` + height: 50px; + width: 50px; + margin: 1.5rem; `; const VisualizerContainer = styled.div` @@ -98,6 +123,50 @@ const PopUpContainer = styled.div` z-index: 2000; `; +const LoadingViewContainer = styled.div` + background-color: var(--vscode-editor-background); + height: 100vh; + width: 100%; + display: flex; + font-family: var(--vscode-font-family); +`; + +const LoadingContent = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + height: 100%; + width: 100%; + padding-top: 30vh; + text-align: center; + max-width: 500px; + margin: 0 auto; + animation: fadeIn 1s ease-in-out; +`; + +const LoadingTitle = styled.h1` + color: var(--vscode-foreground); + font-size: 1.5em; + font-weight: 400; + margin: 0; + letter-spacing: -0.02em; + line-height: normal; +`; + +const LoadingSubtitle = styled.p` + color: var(--vscode-descriptionForeground); + font-size: 13px; + margin: 0.5rem 0 2rem 0; + opacity: 0.8; +`; + +const LoadingText = styled.div` + color: var(--vscode-foreground); + font-size: 13px; + font-weight: 500; +`; + const MainPanel = () => { const { rpcClient } = useRpcContext(); const { sidePanel, setSidePanel, popupMessage, setPopupMessage, activePanel } = useVisualizerContext(); @@ -199,7 +268,7 @@ const MainPanel = () => { case MACHINE_VIEW.BIDiagram: setViewComponent( { case MACHINE_VIEW.GraphQLDiagram: setViewComponent(); break; - case MACHINE_VIEW.SequenceDiagram: - setViewComponent( - - ); - break; case MACHINE_VIEW.BallerinaUpdateView: setNavActive(false); setViewComponent(); @@ -310,7 +374,7 @@ const MainPanel = () => { case MACHINE_VIEW.EditConnectionWizard: setViewComponent( ); @@ -337,6 +401,16 @@ const MainPanel = () => { /> ); break; + case MACHINE_VIEW.AddConfigVariables: + setViewComponent( + + ); + break; default: setNavActive(false); setViewComponent(); @@ -376,6 +450,20 @@ const MainPanel = () => { {/* {navActive && } */} {viewComponent && {viewComponent}} + {!viewComponent && ( + + + + + Loading Integration + Setting up your integration environment + + Please wait + + + + + )} {sidePanel !== "EMPTY" && sidePanel === "ADD_CONNECTION" && ( )} diff --git a/workspaces/ballerina/ballerina-visualizer/src/PopupPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/PopupPanel.tsx index 7403b94c95a..785666cb223 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/PopupPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/PopupPanel.tsx @@ -83,7 +83,7 @@ const PopupPanel = (props: PopupPanelProps) => { setViewComponent( <> diff --git a/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx b/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx index 3d5eb3ce48b..22df08b7a63 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx @@ -20,9 +20,71 @@ import React, { useEffect } from "react"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { AIMachineStateValue, MachineStateValue } from "@wso2/ballerina-core"; import MainPanel from "./MainPanel"; +import styled from '@emotion/styled'; import { LoadingRing } from "./components/Loader"; import AIPanel from "./views/AIPanel/AIPanel"; import { AgentChat } from "./views/AgentChatPanel/AgentChat"; +import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"; +import { Global, css } from '@emotion/react'; + +const ProgressRing = styled(VSCodeProgressRing)` + height: 50px; + width: 50px; + margin: 1.5rem; +`; + +const LoadingContent = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + height: 100%; + width: 100%; + padding-top: 30vh; + text-align: center; + max-width: 500px; + margin: 0 auto; + animation: fadeIn 1s ease-in-out; +`; + +const LoadingTitle = styled.h1` + color: var(--vscode-foreground); + font-size: 1.5em; + font-weight: 400; + margin: 0; + letter-spacing: -0.02em; + line-height: normal; +`; + +const LoadingSubtitle = styled.p` + color: var(--vscode-descriptionForeground); + font-size: 13px; + margin: 0.5rem 0 2rem 0; + opacity: 0.8; +`; + +const LoadingText = styled.div` + color: var(--vscode-foreground); + font-size: 13px; + font-weight: 500; +`; + +const globalStyles = css` + @keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } + } + .loading-dots::after { + content: ''; + animation: dots 1.5s infinite; + } + @keyframes dots { + 0%, 20% { content: ''; } + 40% { content: '.'; } + 60% { content: '..'; } + 80%, 100% { content: '...'; } + } +`; const MODES = { VISUALIZER: "visualizer", @@ -61,7 +123,7 @@ export function Visualizer({ mode }: { mode: string }) { case MODES.VISUALIZER: return case MODES.AI: - return + return case MODES.AGENT_CHAT: return } @@ -75,6 +137,32 @@ const VisualizerComponent = React.memo(({ state }: { state: MachineStateValue }) case typeof state === 'object' && 'viewActive' in state && state.viewActive === "viewReady": return ; default: - return ; + return ; } }); + +const LanguageServerLoadingView = () => { + return ( +
+ + + + + Activating Language Server + + + Preparing your Ballerina development environment + + + Initializing + + +
+ ); +}; 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 0fd779d90de..8e5857a9f86 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 @@ -529,6 +529,12 @@ const AIChat: React.FC = () => { case Command.DataMap: { switch (parsedInput.templateId) { case "mappings-for-records": + // TODO: Update this to use the LS API for validating function names + const invalidPattern = /[<>\/\(\)\{\}\[\]\\!@#$%^&*+=|;:'",.?`~]/; + if (invalidPattern.test(parsedInput.placeholderValues.functionName)) { + throw new Error("Please provide a valid function name without special characters."); + } + await processMappingParameters( inputText, { @@ -1565,12 +1571,6 @@ const AIChat: React.FC = () => { const functionName = parameters.functionName; - const invalidPattern = /[<>\/\(\)\{\}\[\]\\!@#$%^&*_+=|;:'",.?`~]/; - - if (invalidPattern.test(functionName)) { - throw new Error("Please provide a valid function name without special characters."); - } - const projectImports = await rpcClient.getBIDiagramRpcClient().getAllImports(); const activeFile = await rpcClient.getAiPanelRpcClient().getActiveFile(); const projectComponents = await rpcClient.getBIDiagramRpcClient().getProjectComponents(); 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 2cca0b5265b..187496c71f4 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AIChatAgentWizard.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/AIChatAgentWizard.tsx @@ -18,7 +18,7 @@ import { useEffect, useState } from 'react'; import { EVENT_TYPE, ListenerModel } from '@wso2/ballerina-core'; -import { View, ViewContent, TextField, Button } from '@wso2/ui-toolkit'; +import { View, ViewContent, TextField, Button, Typography } from '@wso2/ui-toolkit'; import styled from '@emotion/styled'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; import { LoadingContainer } from '../../styles'; @@ -195,7 +195,7 @@ export function AIChatAgentWizard(props: AIChatAgentWizardProps) { onClick={handleCreateService} disabled={isCreating || !!nameError || !agentName} > - {isCreating ? 'Creating...' : 'Create'} + {isCreating ? Creating... : 'Create'} {isCreating && } 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 24cc57c8e4e..a142feb3bd1 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/MemoryManagerConfig.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/MemoryManagerConfig.tsx @@ -18,7 +18,7 @@ import React, { useEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; -import { CodeData, FlowNode } from "@wso2/ballerina-core"; +import { CodeData, FlowNode, NodeMetadata } from "@wso2/ballerina-core"; import { FormField, FormValues } from "@wso2/ballerina-side-panel"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { convertConfig } from "../../../utils/bi"; @@ -70,7 +70,6 @@ export function MemoryManagerConfig(props: MemoryManagerConfigProps): JSX.Elemen const agentFilePath = useRef(""); const agentNodeRef = useRef(); const moduleConnectionNodes = useRef([]); - const selectedMemoryManagerFlowNode = useRef(); useEffect(() => { initPanel(); @@ -306,7 +305,7 @@ export function MemoryManagerConfig(props: MemoryManagerConfigProps): JSX.Elemen }} value={ selectedMemoryManagerCodeData?.object || - (agentCallNode?.metadata?.data?.memory?.type as string) || + ((agentCallNode?.metadata?.data as NodeMetadata)?.memory?.type as string) || memoryManagerDropdownPlaceholder } containerSx={{ width: "100%" }} @@ -324,6 +323,7 @@ export function MemoryManagerConfig(props: MemoryManagerConfigProps): JSX.Elemen targetLineRange={agentNodeRef.current.codedata.lineRange} onSubmit={handleOnSave} disableSaveButton={savingForm} + isSaving={savingForm} /> )} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/ModelConfig.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/ModelConfig.tsx index 3f9932ae6fc..d9627eacd68 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/ModelConfig.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/ModelConfig.tsx @@ -18,7 +18,7 @@ import React, { useEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; -import { CodeData, FlowNode, NodeProperties } from "@wso2/ballerina-core"; +import { CodeData, FlowNode, NodeMetadata, 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"; @@ -215,7 +215,7 @@ export function ModelConfig(props: ModelConfigProps): JSX.Element { setSelectedModelCodeData(selectedModelCodeData); fetchModelNodeTemplate(selectedModelCodeData); }} - value={selectedModelCodeData?.object || (agentCallNode?.metadata.data?.model?.type as string)} + value={selectedModelCodeData?.object || ((agentCallNode?.metadata.data as NodeMetadata)?.model?.type as string)} containerSx={{ width: "100%" }} /> @@ -231,6 +231,7 @@ export function ModelConfig(props: ModelConfigProps): JSX.Element { targetLineRange={selectedModel?.codedata.lineRange} onSubmit={handleOnSave} disableSaveButton={savingForm} + isSaving={savingForm} /> )} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/index.tsx index 4f0987f1908..e2d469bde84 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/AIChatAgent/index.tsx @@ -167,7 +167,7 @@ export function AIAgentDesigner(props: AIAgentDesignerProps) { const handleServiceTryIt = () => { const basePath = serviceModel.properties?.basePath?.value?.trim() ?? ""; const listener = serviceModel.properties?.listener?.value?.trim(); - const commands = ["ballerina.tryit", false, undefined, { basePath, listener }]; + const commands = ["ballerina.tryIt", false, undefined, { basePath, listener }]; rpcClient.getCommonRpcClient().executeCommand({ commands }); }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentListView/EventIntegrationPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentListView/EventIntegrationPanel.tsx index c5a991487e5..c25f47f32f1 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentListView/EventIntegrationPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentListView/EventIntegrationPanel.tsx @@ -61,7 +61,7 @@ export function EventIntegrationPanel(props: EventIntegrationPanelProps) { .map((item, index) => { return ( } title="GraphQL Service" @@ -73,6 +74,7 @@ export function IntegrationAPIPanel(props: IntegrationAPIPanelProps) { isBeta /> } title="TCP Service" diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentListView/OtherArtifactsPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentListView/OtherArtifactsPanel.tsx index 5c5b813f5aa..8627b2072a6 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentListView/OtherArtifactsPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentListView/OtherArtifactsPanel.tsx @@ -61,7 +61,7 @@ export function OtherArtifactsPanel(props: OtherArtifactsPanelProps) { await rpcClient.getVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: { - view: MACHINE_VIEW.ViewConfigVariables, + view: MACHINE_VIEW.AddConfigVariables, }, }); } else if (key === DIRECTORY_MAP.FUNCTION) { @@ -93,6 +93,7 @@ export function OtherArtifactsPanel(props: OtherArtifactsPanelProps) { } title="Function" @@ -100,6 +101,7 @@ export function OtherArtifactsPanel(props: OtherArtifactsPanelProps) { /> {isNPSupported && } title="Natural Function" onClick={() => handleClick(DIRECTORY_MAP.NP_FUNCTION)} 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 ab06a1103eb..2ed07dace3b 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 @@ -105,6 +105,7 @@ export interface ConfigProps { fileName: string; org: string; package: string; + addNew?: boolean; } interface CategoryWithModules { @@ -136,7 +137,7 @@ export function ViewConfigurableVariables(props?: ConfigProps) { const { rpcClient } = useRpcContext(); const [configVariables, setConfigVariables] = useState({}); const [errorMessage, setErrorMessage] = useState(''); - const [isAddConfigVariableFormOpen, setAddConfigVariableFormOpen] = useState(false); + const [isAddConfigVariableFormOpen, setAddConfigVariableFormOpen] = useState(props?.addNew || false); const [searchValue, setSearchValue] = React.useState(''); const [categoriesWithModules, setCategoriesWithModules] = useState([]); const [selectedModule, setSelectedModule] = useState(null); @@ -424,6 +425,7 @@ export function ViewConfigurableVariables(props?: ConfigProps) { }} /> ` display: flex; flex-direction: column; - gap: 16px; - margin-top: 24px; - height: ${(props: { isHalfView: boolean }) => (props.isHalfView ? "30vh" : "80vh")}; gap: 8px; - height: 80vh; - overflow-y: scroll; margin-top: 16px; + height: ${(props: { isHalfView: boolean }) => (props.isHalfView ? "30vh" : "calc(100vh - 200px)")}; + overflow-y: scroll; `; const GridContainer = styled.div<{ isHalfView?: boolean }>` display: grid; grid-template-columns: ${(props: { isHalfView: boolean }) => props.isHalfView ? "unset" : "repeat(auto-fill, minmax(200px, 1fr))"}; - gap: 16px; gap: 12px; width: 100%; `; @@ -310,6 +306,7 @@ export function ConnectorView(props: ConnectorViewProps) { {category.items?.map((connector, index) => { return ( void; } export function EditConnectionWizard(props: EditConnectionWizardProps) { - const { fileName, connectionName, onClose } = props; + const { projectUri, 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); useEffect(() => { @@ -72,6 +74,10 @@ export function EditConnectionWizard(props: EditConnectionWizardProps) { onClose?.(); return; } + const connectionFile = connector.codedata.lineRange.fileName; + let connectionFilePath = Utils.joinPath(URI.file(projectUri), connectionFile).fsPath; + setFilePath(connectionFilePath); + setConnection(connector); const formProperties = getFormProperties(connector); console.log(">>> Connector form properties", formProperties); @@ -83,7 +89,7 @@ export function EditConnectionWizard(props: EditConnectionWizardProps) { if (connection) { setUpdatingContent(true); - if (fileName === "") { + if (filePath === "") { console.error(">>> Error updating source code. No source file found"); setUpdatingContent(false); return; @@ -92,7 +98,7 @@ export function EditConnectionWizard(props: EditConnectionWizardProps) { rpcClient .getBIDiagramRpcClient() .getSourceCode({ - filePath: fileName, + filePath: filePath, flowNode: node, isConnector: true, }) @@ -183,7 +189,7 @@ export function EditConnectionWizard(props: EditConnectionWizardProps) { > { }; export interface DiagramWrapperProps { - syntaxTree: STNode; projectPath: string; filePath?: string; view?: FocusFlowDiagramView; } export function DiagramWrapper(param: DiagramWrapperProps) { - const { syntaxTree, projectPath, filePath, view } = param; + const { projectPath, filePath, view } = param; const { rpcClient } = useRpcContext(); const [showSequenceDiagram, setShowSequenceDiagram] = useState(false); @@ -170,6 +169,7 @@ export function DiagramWrapper(param: DiagramWrapperProps) { const [serviceType, setServiceType] = useState(""); const [basePath, setBasePath] = useState(""); const [listener, setListener] = useState(""); + const [parentMetadata, setParentMetadata] = useState(); useEffect(() => { rpcClient.getVisualizerLocation().then((location) => { @@ -224,11 +224,14 @@ export function DiagramWrapper(param: DiagramWrapperProps) { setLoadingDiagram(true); }; - const handleReadyDiagram = (fileName?: string) => { + const handleReadyDiagram = (fileName?: string, parentMetadata?: ParentMetadata) => { setLoadingDiagram(false); if (fileName) { setFileName(fileName); } + if (parentMetadata) { + setParentMetadata(parentMetadata); + } }; const handleEdit = (fileUri?: string) => { @@ -237,49 +240,23 @@ export function DiagramWrapper(param: DiagramWrapperProps) { view === FOCUS_FLOW_DIAGRAM_VIEW.NP_FUNCTION ? MACHINE_VIEW.BINPFunctionForm : MACHINE_VIEW.BIFunctionForm, - identifier: (syntaxTree as ResourceAccessorDefinition).functionName.value, + identifier: parentMetadata?.label || "", documentUri: fileUri, }; rpcClient.getVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: context }); }; - let isAutomation = false; - let isResource = false; - let isRemote = false; - let isAgent = false; - let method = ""; - const parameters = getParameters(syntaxTree); - const returnType = getReturnType(syntaxTree); - - if (syntaxTree && STKindChecker.isResourceAccessorDefinition(syntaxTree)) { - isResource = true; - method = (syntaxTree as ResourceAccessorDefinition).functionName.value; - } else if (syntaxTree && STKindChecker.isFunctionDefinition(syntaxTree)) { - isResource = false; - method = syntaxTree.functionName.value; - } else if (syntaxTree && STKindChecker.isObjectMethodDefinition(syntaxTree)) { - isRemote = syntaxTree.qualifierList?.some((qualifier) => STKindChecker.isRemoteKeyword(qualifier)) || false; - method = syntaxTree.functionName.value; - } else { - // FIXME: this is a temporary fix to navigate back to overview when the syntax tree is undefined - console.error(">>> cannot get method, syntaxTree is undefined"); - rpcClient - .getVisualizerRpcClient() - .openView({ type: EVENT_TYPE.OPEN_VIEW, location: { view: MACHINE_VIEW.Overview } }); - return; - } - - if (!isResource && method === "main") { - isAutomation = true; - } - - if (serviceType === "ai") { - isAgent = true; - } + let isAutomation = parentMetadata?.kind === "Function" && parentMetadata?.label === "main"; + let isResource = parentMetadata?.kind === "Resource"; + let isRemote = parentMetadata?.kind === "Remote Function"; + let isAgent = parentMetadata?.kind === "AI Chat Agent"; + let isNPFunction = view === FOCUS_FLOW_DIAGRAM_VIEW.NP_FUNCTION; + const parameters = parentMetadata?.parameters?.join(", ") || ""; + const returnType = parentMetadata?.return || ""; const handleResourceTryIt = (methodValue: string, pathValue: string) => { const resource = serviceType === "http" ? { methodValue, pathValue } : undefined; - const commands = ["ballerina.tryit", false, resource, { basePath, listener }]; + const commands = ["ballerina.tryIt", false, resource, { basePath, listener }]; rpcClient.getCommonRpcClient().executeCommand({ commands }); }; @@ -288,12 +265,12 @@ export function DiagramWrapper(param: DiagramWrapperProps) { {isResource && !isAutomation && ( - {method} - {getResourcePath(syntaxTree)} + {parentMetadata?.accessor || ""} + {parentMetadata?.label || ""} {parameters && ( ({parameters}) @@ -313,7 +290,7 @@ export function DiagramWrapper(param: DiagramWrapperProps) { serviceType === "http" || isAgent ? ( handleResourceTryIt(method, getResourcePath(syntaxTree))} + onClick={() => handleResourceTryIt(parentMetadata?.accessor || "", parentMetadata?.label || "")} > - {method} + {parentMetadata?.label || ""} {parameters && ( ({parameters}) @@ -352,11 +329,11 @@ export function DiagramWrapper(param: DiagramWrapperProps) { )} {!isResource && !isAutomation && !isRemote && ( - {method} + {parentMetadata?.label || ""} {parameters && ( ({parameters}) @@ -374,7 +351,7 @@ export function DiagramWrapper(param: DiagramWrapperProps) { } actions={ - handleEdit(fileName)}> + handleEdit(fileName)}> Edit @@ -383,12 +360,12 @@ export function DiagramWrapper(param: DiagramWrapperProps) { )} {!isResource && isAutomation && ( - - ({getParameters(syntaxTree)}) + + ({parameters}) {returnType && ( @@ -401,7 +378,7 @@ export function DiagramWrapper(param: DiagramWrapperProps) { } actions={ - handleEdit(fileName)}> + handleEdit(fileName)}> Edit @@ -430,22 +407,18 @@ export function DiagramWrapper(param: DiagramWrapperProps) { )} {showSequenceDiagram ? ( ) : view ? ( ) : ( { - resourcePath += STKindChecker.isResourcePathSegmentParam(path) ? path.source : path?.value; - }); - } - return resourcePath; -} - -function getParameters(syntaxTree: STNode) { - if (!syntaxTree) { - console.error(">>> cannot get parameters, syntaxTree is undefined"); - return ""; - } - if ( - STKindChecker.isResourceAccessorDefinition(syntaxTree) || - (STKindChecker.isObjectMethodDefinition(syntaxTree) && - syntaxTree.qualifierList?.some((qualifier) => STKindChecker.isRemoteKeyword(qualifier))) - ) { - return syntaxTree.functionSignature.parameters - .map((param) => { - if (!STKindChecker.isCommaToken(param)) { - return `${param.paramName.value}: ${param.typeName.source.trim()}`; - } - return null; - }) - .filter(Boolean) - .join(", "); - } else if ( - STKindChecker.isFunctionDefinition(syntaxTree) && - STKindChecker.isExpressionFunctionBody(syntaxTree.functionBody) && - syntaxTree.functionBody.expression.kind === "NaturalExpression" - ) { - return syntaxTree.functionSignature.parameters - .map((param) => { - if ( - !STKindChecker.isCommaToken(param) && - param.paramName.value !== "prompt" && - param.paramName.value !== "context" - ) { - return `${param.paramName.value}: ${param.typeName.source.trim()}`; - } - return null; - }) - .filter(Boolean) - .join(", "); - } else if (STKindChecker.isFunctionDefinition(syntaxTree)) { - return syntaxTree.functionSignature.parameters - .map((param) => { - if (!STKindChecker.isCommaToken(param)) { - return `${param.paramName.value}: ${param.typeName.source.trim()}`; - } - return null; - }) - .filter(Boolean) - .join(", "); - } - return ""; -} - -function getReturnType(syntaxTree: STNode) { - if (!syntaxTree) { - console.error(">>> cannot get return type, syntaxTree is undefined"); - return ""; - } - if (STKindChecker.isFunctionDefinition(syntaxTree)) { - return syntaxTree.functionSignature.returnTypeDesc?.type.source || ""; - } - if (STKindChecker.isResourceAccessorDefinition(syntaxTree) || STKindChecker.isObjectMethodDefinition(syntaxTree)) { - return syntaxTree.functionSignature.returnTypeDesc?.type.source || ""; - } - return ""; -} - export default DiagramWrapper; 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 fb56e814623..11fc9662839 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/PanelManager.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/PanelManager.tsx @@ -17,7 +17,7 @@ */ import { PanelContainer, NodeList, ExpressionFormField } from "@wso2/ballerina-side-panel"; -import { FlowNode, LineRange, SubPanel, SubPanelView, FUNCTION_TYPE, ToolData } from "@wso2/ballerina-core"; +import { FlowNode, LineRange, SubPanel, SubPanelView, FUNCTION_TYPE, ToolData, NodeMetadata } from "@wso2/ballerina-core"; import { InlineDataMapper } from "../../InlineDataMapper"; import { HelperView } from "../HelperView"; import FormGenerator from "../Forms/FormGenerator"; @@ -139,6 +139,11 @@ export function PanelManager(props: PanelManagerProps) { setPanelView(SidePanelView.ADD_TOOL); }; + const handleSubmitAndClose = () => { + onSubmitForm(); + onClose(); + }; + const findSubPanelComponent = (subPanel: SubPanel) => { switch (subPanel.view) { case SubPanelView.INLINE_DATA_MAPPER: @@ -183,30 +188,30 @@ export function PanelManager(props: PanelManagerProps) { agentCallNode={selectedNode} fileName={fileName} lineRange={targetLineRange} - onSave={onClose} + onSave={handleSubmitAndClose} /> ); case SidePanelView.ADD_TOOL: - return ; + return ; case SidePanelView.NEW_TOOL: - return ; + return ; case SidePanelView.AGENT_TOOL: - const selectedTool = selectedNode?.metadata.data.tools?.find( + const selectedTool = (selectedNode?.metadata.data as NodeMetadata).tools?.find( (tool) => tool.name === selectedClientName ); - return ; + return ; case SidePanelView.AGENT_MODEL: - return ; + return ; case SidePanelView.AGENT_CONFIG: - return ; + return ; case SidePanelView.AGENT_MEMORY_MANAGER: - return ; + return ; case SidePanelView.FUNCTION_LIST: return ( diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx index 1861a58fbc2..1bdd1913c49 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx @@ -16,7 +16,7 @@ * under the License. */ -import { useEffect, useRef, useState, useMemo } from "react"; +import { useEffect, useRef, useState, useMemo, useCallback } from "react"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import styled from "@emotion/styled"; import { MemoizedDiagram } from "@wso2/bi-diagram"; @@ -40,6 +40,7 @@ import { ToolData, DIRECTORY_MAP, UpdatedArtifactsResponse, + ParentMetadata, } from "@wso2/ballerina-core"; import { @@ -53,7 +54,7 @@ import { applyModifications, textToModifications } from "../../../utils/utils"; import { PanelManager, SidePanelView } from "./PanelManager"; import { findFunctionByName, transformCategories } from "./utils"; import { ExpressionFormField, Category as PanelCategory } from "@wso2/ballerina-side-panel"; -import { cloneDeep } from "lodash"; +import { cloneDeep, debounce } from "lodash"; import { findFlowNodeByModuleVarName, getAgentFilePath, @@ -75,14 +76,13 @@ const SpinnerContainer = styled.div` `; export interface BIFlowDiagramProps { - syntaxTree: STNode; // INFO: this is used to make the diagram rerender when code changes projectPath: string; onUpdate: () => void; - onReady: (fileName: string) => void; + onReady: (fileName: string, parentMetadata?: ParentMetadata) => void; } export function BIFlowDiagram(props: BIFlowDiagramProps) { - const { syntaxTree, projectPath, onUpdate, onReady } = props; + const { projectPath, onUpdate, onReady } = props; const { rpcClient } = useRpcContext(); const [model, setModel] = useState(); @@ -106,18 +106,17 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { const initialCategoriesRef = useRef([]); const showEditForm = useRef(false); const selectedNodeMetadata = useRef<{ nodeId: string, metadata: any, fileName: string }>(); - const selectedModel = useRef(); useEffect(() => { - getFlowModel(); - }, [syntaxTree]); + debouncedGetFlowModel(); + }, []); useEffect(() => { rpcClient.onParentPopupSubmitted((parent: ParentPopupData) => { + console.log(">>> on parent popup submitted", parent); if (parent.artifactType === DIRECTORY_MAP.FUNCTION || parent.artifactType === DIRECTORY_MAP.NP_FUNCTION || parent.artifactType === DIRECTORY_MAP.DATA_MAPPER) { handleOnSelectNode(selectedNodeMetadata.current.nodeId, selectedNodeMetadata.current.metadata, selectedNodeMetadata.current.fileName); } else { - console.log(">>> on parent popup submitted", parent); if (!topNodeRef.current || !targetRef.current) { console.error(">>> No parent or target found"); return; @@ -128,6 +127,13 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { }); }, [rpcClient]); + const debouncedGetFlowModel = useCallback( + debounce(() => { + getFlowModel(); + }, 1000), + [] + ); + const getFlowModel = () => { setShowProgressIndicator(true); onUpdate(); @@ -140,9 +146,13 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { .getBIDiagramRpcClient() .getFlowModel() .then((model) => { + console.log(">>> flow model", model); if (model?.flowModel) { setModel(model.flowModel); - onReady(model.flowModel.fileName); + const parentMetadata = model.flowModel.nodes.find( + (node) => node.codedata.node === "EVENT_START" + )?.metadata.data as ParentMetadata | undefined; + onReady(model.flowModel.fileName, parentMetadata); } }) .finally(() => { @@ -220,7 +230,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { // restore original model if (originalFlowModel.current) { - getFlowModel(); + debouncedGetFlowModel(); originalFlowModel.current = undefined; setSuggestedModel(undefined); suggestedText.current = undefined; @@ -419,12 +429,12 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { // Find the correct artifact by currentIdentifier (id) let currentArtifact = artifacts.artifacts.at(0); artifacts.artifacts.forEach((artifact) => { - if (artifact.id === currentIdentifier) { + if (artifact.id === currentIdentifier || artifact.name === currentIdentifier) { currentArtifact = artifact; } // Check if artifact has resources and find within those if (artifact.resources && artifact.resources.length > 0) { - const resource = artifact.resources.find((resource) => resource.id === currentIdentifier); + const resource = artifact.resources.find((resource) => resource.id === currentIdentifier || resource.name === currentIdentifier); if (resource) { currentArtifact = resource; } @@ -549,6 +559,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { if (!updatedNode) { console.log(">>> No updated node found"); updatedNode = selectedNodeRef.current; + debouncedGetFlowModel(); } setShowProgressIndicator(true); rpcClient @@ -570,6 +581,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { }) .finally(() => { setShowProgressIndicator(false); + debouncedGetFlowModel(); }); }; @@ -591,6 +603,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { if (!agentNodeDeleteResponse) { console.error(">>> Error deleting agent node", node); setShowProgressIndicator(false); + debouncedGetFlowModel(); return; } } @@ -600,6 +613,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { selectedNodeRef.current = undefined; handleOnCloseSidePanel(); setShowProgressIndicator(false); + debouncedGetFlowModel(); }; const handleOnAddComment = (comment: string, target: LineRange) => { @@ -681,14 +695,6 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { id: node.codedata, }) .then((response) => { - // const nodesWithCustomForms = ["IF", "FORK"]; - // if (!response.flowNode.properties && !nodesWithCustomForms.includes(response.flowNode.codedata.node)) { - // console.log(">>> Node doesn't have properties. Don't show edit form", response.flowNode); - // setShowProgressIndicator(false); - // showEditForm.current = false; - // return; - // } - nodeTemplateRef.current = response.flowNode; showEditForm.current = true; setSidePanelView(SidePanelView.FORM); @@ -696,6 +702,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { }) .finally(() => { setShowProgressIndicator(false); + debouncedGetFlowModel(); }); }; @@ -931,6 +938,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { alert("Failed to remove memory manager. Please try again."); } finally { setShowProgressIndicator(false); + debouncedGetFlowModel(); } }; @@ -964,6 +972,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { setSidePanelView(SidePanelView.ADD_TOOL); setShowSidePanel(true); setShowProgressIndicator(false); + debouncedGetFlowModel(); }, 500); }; @@ -1014,6 +1023,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { alert(`Failed to remove tool "${tool.name}". Please try again.`); } finally { setShowProgressIndicator(false); + debouncedGetFlowModel(); } }; 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 5e437637910..51e7fc9dbad 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FocusFlowDiagram/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FocusFlowDiagram/index.tsx @@ -41,7 +41,8 @@ import { ExpressionProperty, TRIGGER_CHARACTERS, TriggerCharacter, - TextEdit + TextEdit, + ParentMetadata } from "@wso2/ballerina-core"; import { @@ -70,16 +71,14 @@ const SpinnerContainer = styled.div` `; export interface BIFocusFlowDiagramProps { - syntaxTree: STNode; // INFO: this is used to make the diagram rerender when code changes projectPath: string; filePath: string; - view: FocusFlowDiagramView; onUpdate: () => void; - onReady: (fileName: string) => void; + onReady: (fileName: string, parentMetadata?: ParentMetadata) => void; } export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { - const { syntaxTree, projectPath, filePath, onUpdate, onReady, view } = props; + const { projectPath, filePath, onUpdate, onReady } = props; const { rpcClient } = useRpcContext(); const [model, setModel] = useState(); @@ -105,8 +104,8 @@ export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { const expressionOffsetRef = useRef(0); // To track the expression offset on adding import statements useEffect(() => { - getFlowModel(); - }, [syntaxTree]); + debouncedGetFlowModel(); + }, []); useEffect(() => { rpcClient.onProjectContentUpdated((state: boolean) => { @@ -121,6 +120,13 @@ export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { }); }, [rpcClient]); + const debouncedGetFlowModel = useCallback( + debounce(() => { + getFlowModel(); + }, 1000), + [rpcClient] + ); + const getFlowModel = () => { setShowProgressIndicator(true); onUpdate(); @@ -133,22 +139,25 @@ export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { .getBIDiagramRpcClient() .getFlowModel() .then(async (model) => { + console.log(">>> focus diagram flow model", model); if (model?.flowModel) { - if (isNaturalFunction(syntaxTree, view)) { - const node = await rpcClient.getBIDiagramRpcClient().getFunctionNode({ - projectPath, - fileName: filePath, - functionName: syntaxTree.functionName.value - }); - - setSelectedNode(node.functionDefinition); - - if (node?.functionDefinition) { - const flowNode = getFlowNodeForNaturalFunction(node.functionDefinition); - model.flowModel.nodes.push(flowNode); - setModel(model.flowModel); - onReady(filePath); - } + const functionName = (model.flowModel.nodes.find((node) => node.codedata.node === "EVENT_START")?.metadata.data as ParentMetadata).label || ""; + const node = await rpcClient.getBIDiagramRpcClient().getFunctionNode({ + projectPath, + fileName: filePath, + functionName: functionName + }); + + setSelectedNode(node.functionDefinition); + + if (node?.functionDefinition) { + const flowNode = getFlowNodeForNaturalFunction(node.functionDefinition); + model.flowModel.nodes.push(flowNode); + setModel(model.flowModel); + const parentMetadata = model.flowModel.nodes.find( + (node) => node.codedata.node === "EVENT_START" + )?.metadata.data as ParentMetadata | undefined; + onReady(model.flowModel.fileName, parentMetadata); } } }) @@ -171,7 +180,7 @@ export function BIFocusFlowDiagram(props: BIFocusFlowDiagramProps) { if (originalFlowModel.current) { // const updatedModel = removeDraftNodeFromDiagram(model); // setModel(updatedModel); - getFlowModel(); + debouncedGetFlowModel(); originalFlowModel.current = undefined; setSuggestedModel(undefined); suggestedText.current = undefined; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/index.tsx index 91a7d1a5970..18f934a342d 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/index.tsx @@ -17,7 +17,7 @@ */ import React, { useEffect, useState } from "react"; -import { Button, Icon, LocationSelector, TextField, Typography } from "@wso2/ui-toolkit"; +import { Button, Icon, LocationSelector, TextField, Typography,Codicon,Tooltip } from "@wso2/ui-toolkit"; import styled from "@emotion/styled"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { BodyText } from "../../styles"; @@ -47,6 +47,48 @@ const IconButton = styled.div` } `; +const PreviewContainer = styled.div` + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + padding: 8px 12px; + display: inline-flex; + align-items: center; + width: fit-content; + height: 28px; + gap: 8px; + background-color: var(--vscode-editor-background); + * { + cursor: default !important; + } +`; + +const InputPreviewWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 3px; + margin: 20px 0; +`; + +const PreviewText = styled(Typography)` + color: var(--vscode-sideBarTitle-foreground); + opacity: 0.5; + font-family: var(--vscode-editor-font-family, 'Monaco', 'Menlo', 'Ubuntu Mono', monospace); + word-break: break-all; + min-width: 100px; + display: flex; + align-items: center; + line-height: 1; +`; + +const PreviewIcon = styled(Codicon)` + display: flex; + align-items: center; +`; + +const sanitizeProjectName = (name: string): string => { + return name.replace(/[^a-z0-9]/gi, '_').toLowerCase(); +}; + export function ProjectForm() { const { rpcClient } = useRpcContext(); const [selectedModule, setSelectedModule] = useState("Main"); @@ -94,13 +136,25 @@ export function ProjectForm() { Name your integration and select a location to start building. - + + + {name && ( + + + + + {sanitizeProjectName(name)} + + + + )} + void; onReady: () => void; } export function BISequenceDiagram(props: BISequenceDiagramProps) { - const { syntaxTree, onUpdate, onReady } = props; + const { onUpdate, onReady } = props; const { rpcClient } = useRpcContext(); const [flowModel, setModel] = useState(undefined); useEffect(() => { getSequenceModel(); - }, [syntaxTree]); + }, []); + + useEffect(() => { + rpcClient.onProjectContentUpdated((content) => { + getSequenceModel(); + }); + }, []); const getSequenceModel = () => { onUpdate(); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/ParamItem.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/ParamItem.tsx index ec9f9e77645..8679fd3ba7b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/ParamItem.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/ParamItem.tsx @@ -80,4 +80,4 @@ export function ParamItem(props: ParamItemProps) { ); -} `` +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/Parameters.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/Parameters.tsx index a883d5b5288..baa362b0139 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/Parameters.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/Parameters.tsx @@ -19,7 +19,7 @@ import React, { useState } from 'react'; -import { Codicon, Divider, LinkButton, Typography } from '@wso2/ui-toolkit'; +import { Codicon, Divider, LinkButton, Typography, CheckBox, CheckBoxGroup } from '@wso2/ui-toolkit'; import styled from '@emotion/styled'; import { ParamEditor } from './ParamEditor'; import { ParamItem } from './ParamItem'; @@ -53,6 +53,7 @@ export function Parameters(props: ParametersProps) { const payloadParameters = parameters.filter(param => param.httpParamType && param.httpParamType === "PAYLOAD"); const advancedDisabledParameters = parameters.filter(param => !param.httpParamType && !param.enabled); const advancedEnabledParameters = parameters.filter(param => !param.httpParamType && param.enabled); + const advancedAllParameters = parameters.filter(param => !param.httpParamType).sort((a, b) => b.metadata.label.localeCompare(a.metadata.label)); const [editModel, setEditModel] = useState(undefined); @@ -106,6 +107,13 @@ export function Parameters(props: ParametersProps) { setEditModel(undefined); }; + const onAdvancedChecked = (param: ParameterModel, checked: boolean) => { + param.enabled = checked; + param.name.value = param.metadata.label.toLowerCase().replace(/ /g, "_"); + onChange(parameters.map(p => p.metadata.label === param.metadata.label ? param : p)); + setEditModel(undefined); + }; + const onChangeParam = (param: ParameterModel) => { setEditModel(param); }; @@ -191,7 +199,8 @@ export function Parameters(props: ParametersProps) { {/* <-------------------- Advanced Parameters Start --------------------> */} - + {/* TODO: REMOVE THE OLD ADVANCED PARAMETERS */} + {/* Advanced Parameters {showAdvanced ? "Hide" : "Show"} @@ -224,9 +233,31 @@ export function Parameters(props: ParametersProps) { onCancel={onParamEditCancel} /> } - + */} {/* <-------------------- Advanced Parameters End --------------------> */} + {/* <-------------------- Advanced Parameters Checkbox Start --------------------> */} + + <> + + Advanced Parameters + + + { + advancedAllParameters.map((param: ParameterModel, index) => ( + onAdvancedChecked(param, checked)} + /> + )) + } + + + + {/* <-------------------- Advanced Parameters Checkbox End --------------------> */} + ); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResourceResponse.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResourceResponse.tsx index a9cd6c004f6..6270a56959e 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResourceResponse.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResourceResponse.tsx @@ -23,7 +23,7 @@ import { Codicon, LinkButton, Typography } from '@wso2/ui-toolkit'; import styled from '@emotion/styled'; import { ResponseItem } from './ResponseItem'; import { ResponseEditor } from './ResponseEditor'; -import { HTTP_METHOD } from '../../../utils'; +import { getDefaultResponse, HTTP_METHOD } from '../../../utils'; import { ReturnTypeModel, StatusCodeResponse } from '@wso2/ballerina-core'; export interface ResourceParamProps { @@ -53,15 +53,6 @@ export function ResourceResponse(props: ResourceParamProps) { const [editModel, setEditModel] = useState(undefined); - const advancedEnabled = response.responses.filter(param => param.isHttpResponseType && param.enabled); - const advancedDisabled = response.responses.filter(param => param.isHttpResponseType && !param.enabled); - - const [showAdvanced, setShowAdvanced] = useState(advancedEnabled.length > 0); - - const handleAdvanceParamToggle = () => { - setShowAdvanced(!showAdvanced); - }; - const onEdit = (param: StatusCodeResponse, id: number) => { setEditingSegmentId(id); const schema = response.schema["statusCodeResponse"] as StatusCodeResponse; @@ -75,6 +66,7 @@ export function ResourceResponse(props: ResourceParamProps) { const onAddClick = () => { setEditingSegmentId(999); + (response.schema["statusCodeResponse"] as StatusCodeResponse).statusCode.value = getDefaultResponse(method); setEditModel(_.cloneDeep(response.schema["statusCodeResponse"]) as StatusCodeResponse); }; @@ -101,23 +93,6 @@ export function ResourceResponse(props: ResourceParamProps) { setEditModel(undefined); }; - const onHandleResponseAdd = () => { - const param: StatusCodeResponse = response.responses.find(item => item.isHttpResponseType); - const index: number = 0; - const updatedParameters: StatusCodeResponse[] = [...response.responses]; - param.enabled = true; - updatedParameters[index] = param; - onChange({ ...response, responses: updatedParameters }); - }; - - const onDisable = (index: number) => { - const param: StatusCodeResponse = response.responses[index]; - const updatedParameters: StatusCodeResponse[] = [...response.responses]; - param.enabled = false; - updatedParameters[index] = param; - onChange({ ...response, responses: updatedParameters }); - }; - const onParamEditCancel = (id?: number) => { setEditModel(undefined); setEditingSegmentId(-1); @@ -126,9 +101,6 @@ export function ResourceResponse(props: ResourceParamProps) { return ( {!editModel && response.responses.map((response: StatusCodeResponse, index) => { - if (index === 0) { - return; - } return ( } - {!editModel && - - Advanced Response - {showAdvanced ? "Hide" : "Show"} - - } - {!editModel && showAdvanced && - advancedDisabled.map((param) => ( - - - - <>{param.type.value} - - - )) - } - {!editModel && showAdvanced && - advancedEnabled.map((response, index) => ( - onDisable(index)} - onEditClick={() => onEdit(response, index)} - /> - )) - } ); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResponseEditor.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResponseEditor.tsx index ded29bfe757..463f0d45986 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResponseEditor.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResponseEditor.tsx @@ -19,15 +19,14 @@ import { useEffect, useState } from 'react'; -import { CheckBox, Divider, Tabs, Typography } from '@wso2/ui-toolkit'; +import { Divider, OptionProps, Typography } from '@wso2/ui-toolkit'; import { EditorContainer, EditorContent } from '../../../styles'; -import { LineRange, PropertyModel, StatusCodeResponse, responseCodes } from '@wso2/ballerina-core'; -import { getDefaultResponse, getTitleFromResponseCode, HTTP_METHOD } from '../../../utils'; +import { LineRange, PropertyModel, ResponseCode, StatusCodeResponse } from '@wso2/ballerina-core'; +import { getDefaultResponse, getTitleFromStatusCodeAndType, HTTP_METHOD } from '../../../utils'; import { FormField, FormImports, FormValues } from '@wso2/ballerina-side-panel'; import FormGeneratorNew from '../../../../Forms/FormGeneratorNew'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; import { URI, Utils } from 'vscode-uri'; -import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'; import { getImportsForProperty } from '../../../../../../utils/bi'; @@ -51,21 +50,25 @@ export function ResponseEditor(props: ParamProps) { const { rpcClient } = useRpcContext(); const [filePath, setFilePath] = useState(''); - const [currentView, setCurrentView] = useState(response.type.value ? Views.EXISTING : Views.NEW); + const [responseCodes, setResponseCodes] = useState([]); const [targetLineRange, setTargetLineRange] = useState(); const [newFields, setNewFields] = useState([]); useEffect(() => { - rpcClient.getVisualizerLocation().then(res => { setFilePath(Utils.joinPath(URI.file(res.projectUri), 'main.bal').fsPath) }); + rpcClient.getServiceDesignerRpcClient().getResourceReturnTypes({ filePath: undefined, context: undefined }).then((res) => { + console.log("Resource Return Types: ", res); + setResponseCodes(res.completions); + rpcClient.getVisualizerLocation().then(res => { setFilePath(Utils.joinPath(URI.file(res.projectUri), 'main.bal').fsPath) }); + }); }, []); const handleOnCancel = () => { onCancel(index); }; - const convertPropertyToFormField = (property: PropertyModel, isArray?: boolean, items?: string[]) => { + const convertPropertyToFormField = (property: PropertyModel, items?: string[]) => { const converted: FormField = { key: "", label: property.metadata.label, @@ -73,8 +76,9 @@ export function ResponseEditor(props: ParamProps) { optional: property.optional, editable: property.editable, enabled: property.enabled, + advanced: property.advanced, documentation: property.metadata.description, - value: isArray ? property.values || [] : property.value, + value: property.value, items: property.items || items, diagnostics: property.diagnostics, valueTypeConstraint: property.valueTypeConstraint, @@ -82,7 +86,8 @@ export function ResponseEditor(props: ParamProps) { return converted; } - const updateNewFields = (res: StatusCodeResponse) => { + const updateNewFields = (res: StatusCodeResponse, hasBody: boolean = true) => { + const NO_BODY_TYPES = ["http:Response", "http:NoContent", "error"]; const defaultItems = [ "", "string", @@ -92,32 +97,59 @@ export function ResponseEditor(props: ParamProps) { "int[]", "boolean[]" ]; - const fields = [ + + // Special Condition to check http:Response to re-direct to Dynamic Status code + if (NO_BODY_TYPES.includes(res.type.value)) { + res.statusCode.value = ""; + // Handle the error type to set the default status code to 500 + if (res.type.value === "error") { + res.statusCode.value = "500"; + } + hasBody = false; + } + + const fields: FormField[] = [ { ...convertPropertyToFormField(res.statusCode), key: `statusCode`, - value: getTitleFromResponseCode(Number(res.statusCode.value)), - items: responseCodes.map(code => code.title), - }, - { + value: getTitleFromStatusCodeAndType(responseCodes, res.statusCode.value, res.type.value), + itemOptions: getCategorizedOptions(responseCodes), + onValueChange: (value: string) => { + const responseCodeData = responseCodes.find(code => getTitleFromStatusCodeAndType(responseCodes, code.statusCode, code.type) === value); + res.statusCode.value = responseCodeData.statusCode; + res.type.value = responseCodeData.type; + if (NO_BODY_TYPES.includes(responseCodeData.type)) { + updateNewFields(res, false); + } else { + updateNewFields(res, true); + } + } + } + ]; + + if (hasBody) { + fields.push({ ...convertPropertyToFormField(res.body), key: `body`, - }, - { + }); + fields.push({ ...convertPropertyToFormField(res.name), key: `name`, - }, - { - ...convertPropertyToFormField(res.headers, true, defaultItems), + },); + fields.push({ + ...convertPropertyToFormField(res.headers, defaultItems), key: `headers`, - } - ]; + }); + } + setNewFields(fields); }; useEffect(() => { - updateNewFields(response); - }, [response]); + if (responseCodes.length > 0) { + updateNewFields(response); + } + }, [response, responseCodes]); const existingFields: FormField[] = [ @@ -132,7 +164,7 @@ export function ResponseEditor(props: ParamProps) { if (dataValues['name']) { return true; } - const code = responseCodes.find(code => code.title === dataValues['statusCode']).code; + const code = responseCodes.find(code => getTitleFromStatusCodeAndType(responseCodes, code.statusCode, code.type) === dataValues['statusCode']).statusCode; const defaultCode = getDefaultResponse(method); // Set optional false for the response name @@ -166,7 +198,7 @@ export function ResponseEditor(props: ParamProps) { console.log("Add New Response: ", dataValues); if (isValidResponse(dataValues)) { // Set the values - const code = responseCodes.find(code => code.title === dataValues['statusCode']).code; + const code = responseCodes.find(code => getTitleFromStatusCodeAndType(responseCodes, code.statusCode, code.type) === dataValues['statusCode']).statusCode; response.statusCode.value = String(code); response.body.value = dataValues['body']; response.name.value = dataValues['name']; @@ -176,17 +208,6 @@ export function ResponseEditor(props: ParamProps) { } } - const handleOnExistingSubmit = (dataValues: FormValues, formImports: FormImports) => { - console.log("Add Existing Type: ", dataValues); - response.type.value = dataValues['type']; - response.type.imports = getImportsForProperty('type', formImports); - response.statusCode.value = ''; - response.body.value = ''; - response.name.value = ''; - response.headers.values = []; - onSave(response, index); - } - useEffect(() => { if (rpcClient) { rpcClient @@ -201,24 +222,79 @@ export function ResponseEditor(props: ParamProps) { } }, [filePath, rpcClient]); - const handleCheckChange = (view: string) => { - if (currentView === Views.EXISTING) { - setCurrentView(Views.NEW); - } else { - setCurrentView(Views.EXISTING); + // Helper to create a header option (non-selectable) + const createHeaderOption = (label: string, marginBlockEnd: number = 3): OptionProps => ({ + id: `header-${label}`, + content: ( + {label} + ), + value: `header-${label}`, + disabled: true, // Make header non-selectable + }); + + // Helper to create a regular option + const createOption = (item: ResponseCode): OptionProps => ({ + id: `${item.statusCode}-${item.type}`, + content: ( + + {item.statusCode !== "Dynamic" ? `${item.statusCode} ` : "Dynamic"} - {item.label} + + ), + value: `${item.statusCode} - ${item.label}`, + }); + + // Main function to categorize and flatten the list + function getCategorizedOptions(responseCodes: ResponseCode[]): OptionProps[] { + const dynamic = responseCodes.filter(i => i.type === "http:Response"); + const error = responseCodes.filter(i => i.type === "error"); + const userDefined = responseCodes.filter(i => i.category === "User Defined"); + const preBuilt = responseCodes.filter(i => + ["1XX", "2XX", "3XX", "4XX", "5XX"].includes(i.category) + ); + let options: OptionProps[] = []; + + if (userDefined.length) { + options.push(createHeaderOption("User Defined Responses", 0)); + options = options.concat(userDefined.map(createOption)); } - }; + if (preBuilt.filter(i => i.category === "2XX").length > 0) { + options.push(createHeaderOption("2XX - Success", userDefined.length > 0 ? 3 : 0)); + options = options.concat(preBuilt.filter(i => i.category === "2XX").map(createOption)); + } + if (preBuilt.filter(i => i.category === "1XX").length > 0) { + options.push(createHeaderOption("1XX - Informational")); + options = options.concat(preBuilt.filter(i => i.category === "1XX").map(createOption)); + } + if (preBuilt.filter(i => i.category === "3XX").length > 0) { + options.push(createHeaderOption("3XX - Redirection")); + options = options.concat(preBuilt.filter(i => i.category === "3XX").map(createOption)); + } + if (preBuilt.filter(i => i.category === "4XX").length > 0) { + options.push(createHeaderOption("4XX - Client Error")); + options = options.concat(preBuilt.filter(i => i.category === "4XX").map(createOption)); + } + if (preBuilt.filter(i => i.category === "5XX").length > 0) { + options.push(createHeaderOption("5XX - Server Error")); + options = options.concat(preBuilt.filter(i => i.category === "5XX").map(createOption)); + } + if (error.length) { + options.push(createHeaderOption("Error Response")); + options = options.concat(error.map(createOption)); + } + if (dynamic.length) { + options.push(createHeaderOption("Infer from Response")); + options = options.concat(dynamic.map(createOption)); + } + return options; + } return ( Response Configuration - - Use Existing - - {currentView === Views.NEW && filePath && targetLineRange && + {filePath && targetLineRange &&
-
} - {currentView === Views.EXISTING && filePath && targetLineRange && -
-
}
diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResponseItem.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResponseItem.tsx index a014b402bcd..4245ae0c636 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResponseItem.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourceResponse/ResponseItem.tsx @@ -60,7 +60,7 @@ export function ResponseItem(props: ParamItemProps) { - {getFormattedResponse(response, method)} + {response.type.value === "http:Response" ? "Dynamic" : getFormattedResponse(response, method)} } @@ -70,7 +70,7 @@ export function ResponseItem(props: ParamItemProps) { className={readonly ? disabledHeaderLabel : headerLabelStyles} onClick={handleEdit} > - {response.body.value || response.type.value} + {response.type.value || response.body.value || "anydata"} {!readonly && ( diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ServiceConfigForm.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ServiceConfigForm.tsx index a630cd3ad3f..7c9b00cc470 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ServiceConfigForm.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ServiceConfigForm.tsx @@ -233,7 +233,7 @@ function convertConfig(listener: ServiceModel): FormField[] { editable: true, enabled: expression.enabled ?? true, optional: expression.optional, - value: expression.valueType === "MULTIPLE_SELECT" ? (expression.value ? [expression.value] : [expression.items[0]]) : expression.value, + value: expression.valueType === "MULTIPLE_SELECT" ? (expression.values && expression.values.length > 0 ? expression.values : (expression.value ? [expression.value] : [expression.items[0]])) : expression.value, valueTypeConstraint: expression.valueTypeConstraint, advanced: expression.advanced, diagnostics: [], diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/components/ResourceAccordionV2.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/components/ResourceAccordionV2.tsx new file mode 100644 index 00000000000..f81f0f17b23 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/components/ResourceAccordionV2.tsx @@ -0,0 +1,266 @@ +/** + * 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 styled from '@emotion/styled'; +import { Button, Codicon, Confirm, Icon } from '@wso2/ui-toolkit'; +import { CodeData, FunctionModel, ProjectStructureArtifactResponse } from '@wso2/ballerina-core'; +import { useRpcContext } from '@wso2/ballerina-rpc-client'; + +type MethodProp = { + color: string; + hasLeftMargin?: boolean; +}; + +type ContainerProps = { + borderColor?: string; + haveErrors?: boolean; +}; + +type ButtonSectionProps = { + isExpanded?: boolean; +}; + +type HeaderProps = { + expandable?: boolean; +} + +const AccordionContainer = styled.div` + margin-top: 10px; + overflow: hidden; + background-color: var(--vscode-editorHoverWidget-background); + &:hover { + background-color: var(--vscode-list-hoverBackground); + cursor: pointer; + } + border: ${(p: ContainerProps) => p.haveErrors ? "1px solid red" : "none"}; +`; + +const AccordionHeader = styled.div` + padding: 10px; + cursor: pointer; + display: grid; + grid-template-columns: 3fr 1fr; +`; + +const LinkButtonWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; + padding: 0 16px; + + :hover { + outline: 1px solid var(--vscode-inputOption-activeBorder); + } +`; + +const ButtonWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-size: 10px; + width: 40px; +`; + +const MethodBox = styled.div` + display: flex; + justify-content: center; + height: 25px; + min-width: 70px; + width: auto; + margin-left: ${(p: MethodProp) => p.hasLeftMargin ? "10px" : "0px"}; + text-align: center; + padding: 3px 5px 3px 5px; + background-color: ${(p: MethodProp) => p.color}; + color: #FFF; + align-items: center; + font-weight: bold; +`; + +const MethodSection = styled.div` + display: flex; + gap: 4px; +`; + +const verticalIconStyles = { + transform: "rotate(90deg)", + ":hover": { + backgroundColor: "var(--vscode-welcomePage-tileHoverBackground)", + } +}; + +const ButtonSection = styled.div` + display: flex; + align-items: center; + margin-left: auto; + gap: ${(p: ButtonSectionProps) => p.isExpanded ? "8px" : "6px"}; +`; + +const AccordionContent = styled.div` + padding: 10px; +`; + +const MethodPath = styled.span` + align-self: center; + margin-left: 10px; +`; + +const colors = { + "GET": '#3d7eff', + "PUT": '#fca130', + "POST": '#49cc90', + "DELETE": '#f93e3e', + "PATCH": '#986ee2', + "OPTIONS": '#0d5aa7', + "HEAD": '#9012fe' +} + + +export interface ResourceAccordionPropsV2 { + resource: ProjectStructureArtifactResponse; + onEditResource: (resource: FunctionModel) => void; + onDeleteResource: (resource: FunctionModel) => void; + onResourceImplement: (resource: FunctionModel) => void; + readOnly?: boolean; +} + +export function ResourceAccordionV2(params: ResourceAccordionPropsV2) { + const { resource, onEditResource, onDeleteResource, onResourceImplement, readOnly } = params; + + const [isOpen, setIsOpen] = useState(false); + const [isConfirmOpen, setConfirmOpen] = useState(false); + const [confirmEl, setConfirmEl] = React.useState(null); + const { rpcClient } = useRpcContext(); + + + const toggleAccordion = () => { + setIsOpen(!isOpen); + }; + + const getFunctionModel = async () => { + const codeData: CodeData = { + lineRange: { + fileName: resource.path, + startLine: { line: resource.position.startLine, offset: resource.position.startColumn }, + endLine: { line: resource.position.endLine, offset: resource.position.endColumn }, + } + } + const functionModel = await rpcClient.getServiceDesignerRpcClient().getFunctionFromSource({ filePath: resource.path, codedata: codeData }); + return functionModel; + } + + const handleEditResource = async (e: React.MouseEvent) => { + e.stopPropagation(); // Stop the event propagation + const functionModel = await getFunctionModel(); + onEditResource(functionModel.function); + }; + + const handleOpenConfirm = () => { + setConfirmOpen(true); + }; + + const handleDeleteResource = (e: React.MouseEvent) => { + e.stopPropagation(); // Stop the event propagation + setConfirmEl(e.currentTarget); + handleOpenConfirm(); + }; + + const handleConfirm = async (status: boolean) => { + if (status) { + const functionModel = await getFunctionModel(); + onDeleteResource && onDeleteResource(functionModel.function); + } + setConfirmOpen(false); + setConfirmEl(null); + }; + + const handleResourceImplement = async () => { + const functionModel = await getFunctionModel(); + onResourceImplement(functionModel.function); + } + + function getColorByMethod(method: string) { + switch (method) { + case "get-api": + return colors.GET; + case "put-api": + return colors.PUT; + case "post-api": + return colors.POST; + case "delete-api": + return colors.DELETE; + case "patch-api": + return colors.PATCH; + case "options-api": + return colors.OPTIONS; + case "head-api": + return colors.HEAD; + default: + return '#876036'; // Default color + } + } + + return ( + + + + + {resource.icon.split("-")[0].toUpperCase()} + + {resource.name} + + + <> + + + + + + + + + ); +}; + diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/index.tsx index 2be2308ee03..4a1a1d83179 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/index.tsx @@ -31,7 +31,7 @@ import { ProjectStructureArtifactResponse, PropertyModel, } from "@wso2/ballerina-core"; -import { Button, Codicon, Icon, LinkButton, Typography, View } from "@wso2/ui-toolkit"; +import { Button, Codicon, Icon, LinkButton, Typography, View, TextField } from "@wso2/ui-toolkit"; import styled from "@emotion/styled"; import { ResourceAccordion } from "./components/ResourceAccordion"; import { PanelContainer } from "@wso2/ballerina-side-panel"; @@ -42,6 +42,7 @@ import { applyModifications, isPositionChanged } from "../../../utils/utils"; import { TopNavigationBar } from "../../../components/TopNavigationBar"; import { TitleBar } from "../../../components/TitleBar"; import { LoadingRing } from "../../../components/Loader"; +import { ResourceAccordionV2 } from "./components/ResourceAccordionV2"; const LoadingContainer = styled.div` display: flex; @@ -80,6 +81,13 @@ const ButtonText = styled.span` width: 100%; `; +const HeaderContainer = styled.div` + display: flex; + padding: 15px; + align-items: center; + justify-content: space-between; +`; + interface ServiceDesignerProps { filePath: string; position: NodePosition; @@ -99,6 +107,9 @@ export function ServiceDesigner(props: ServiceDesignerProps) { const [projectListeners, setProjectListeners] = useState([]); const prevPosition = useRef(position); + const [resources, setResources] = useState([]); + const [searchValue, setSearchValue] = useState(""); + useEffect(() => { if (!serviceModel || isPositionChanged(prevPosition.current, position)) { fetchService(position); @@ -136,6 +147,11 @@ export function ServiceDesigner(props: ServiceDesignerProps) { if (listeners.length > 0) { setProjectListeners(listeners); } + const services = res.directoryMap[DIRECTORY_MAP.SERVICE]; + if (services.length > 0) { + const selectedService = services.find((service) => service.name === serviceIdentifier); + setResources(selectedService.resources); + } }); }; @@ -293,7 +309,7 @@ export function ServiceDesigner(props: ServiceDesignerProps) { const handleServiceTryIt = () => { const basePath = serviceModel.properties?.basePath?.value?.trim(); const listener = serviceModel.properties?.listener?.value?.trim(); - const commands = ["ballerina.tryit", false, undefined, { basePath, listener }]; + const commands = ["ballerina.tryIt", false, undefined, { basePath, listener }]; rpcClient.getCommonRpcClient().executeCommand({ commands }); } @@ -342,6 +358,10 @@ export function ServiceDesigner(props: ServiceDesignerProps) { } }; + const handleSearch = (event: React.ChangeEvent) => { + setSearchValue(event.target.value); + }; + const haveServiceTypeName = serviceModel?.properties["serviceTypeName"]?.value; return ( @@ -429,33 +449,58 @@ export function ServiceDesigner(props: ServiceDesignerProps) { ))} - - - Available {serviceModel.moduleName === "http" ? "Resources" : "Functions"} - - - {serviceModel.functions - .filter( - (functionModel) => - (serviceModel.moduleName === "http" - ? functionModel.kind === "RESOURCE" - : true) && functionModel.enabled - ) - .map((functionModel, index) => ( - { }} - onEditResource={handleFunctionEdit} - onDeleteResource={handleFunctionDelete} - onResourceImplement={handleOpenDiagram} - /> - ))} - + + + Available {serviceModel.moduleName === "http" ? "Resources" : "Functions"} + + + {serviceModel.moduleName === "http" && resources.length > 10 && ( + + )} + + {serviceModel.moduleName === "http" && ( + + {resources + .filter((resource) => { + const search = searchValue.toLowerCase(); + const nameMatch = resource.name && resource.name.toLowerCase().includes(search); + const iconMatch = resource.icon && resource.icon.toLowerCase().includes(search); + return nameMatch || iconMatch; + }) + .map((resource, index) => ( + + ))} + + )} + {serviceModel.moduleName !== "http" && ( + + {serviceModel.functions + .filter( + (functionModel) => functionModel.kind === "REMOTE" && functionModel.enabled + ) + .map((functionModel, index) => ( + { }} + onEditResource={handleFunctionEdit} + onDeleteResource={handleFunctionDelete} + onResourceImplement={handleOpenDiagram} + /> + ))} + + )} )} {functionModel && functionModel.kind === "RESOURCE" && ( diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/utils.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/utils.tsx index 3295d052be6..36c351821a2 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/utils.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/utils.tsx @@ -16,7 +16,7 @@ * under the License. */ -import { responseCodes } from '@wso2/ballerina-core'; +import { ResponseCode } from '@wso2/ballerina-core'; export enum HTTP_METHOD { "GET" = "GET", @@ -26,24 +26,40 @@ export enum HTTP_METHOD { "PATCH" = "PATCH" } -export function getDefaultResponse(httpMethod: HTTP_METHOD): number { +export function getDefaultResponse(httpMethod: HTTP_METHOD): string { switch (httpMethod.toUpperCase()) { case HTTP_METHOD.GET: - return 200; + return "200"; case HTTP_METHOD.PUT: - return 200; + return "200"; case HTTP_METHOD.POST: - return 201; + return "201"; case HTTP_METHOD.DELETE: - return 200; + return "200"; case HTTP_METHOD.PATCH: - return 200; + return "200"; default: - return 200; + return "200"; } } -export function getTitleFromResponseCode(code: number): string { - const response = responseCodes.find((response) => response.code === code); - return response ? response.title : ""; +export function getTitleFromStatusCodeAndType(responseCodes: ResponseCode[], statusCode: string, type: string): string { + let responseCode: ResponseCode | undefined; + + if (statusCode && type) { + // If both statusCode and type are provided, find by both + responseCode = responseCodes.find(res => res.statusCode === statusCode && res.type === type); + // If not found with both, fallback to statusCode only + if (!responseCode) { + responseCode = responseCodes.find(res => res.statusCode === statusCode); + } + } else if (statusCode) { + // If only statusCode is provided, find by statusCode only + responseCode = responseCodes.find(res => res.statusCode === statusCode); + } else if (type) { + // If only type is provided, find by type only + responseCode = responseCodes.find(res => res.type === type); + } + + return responseCode ? `${responseCode.statusCode} - ${responseCode.label}` : ""; } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/index.tsx index 0f40dab4ac5..12c418783de 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/index.tsx @@ -97,7 +97,6 @@ export const FormTypeEditor = (props: FormTypeEditorProps) => { line: targetLineRange.startLine.line, offset: targetLineRange.startLine.offset }, - typeConstraint: "anydata" }) .then((types) => { setBasicTypes(getTypes(types)); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/WelcomeView/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/WelcomeView/index.tsx index 8ea36322cf8..fd094e16f21 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/WelcomeView/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/WelcomeView/index.tsx @@ -179,7 +179,7 @@ export function WelcomeView(props: WelcomeViewProps) { const openSamples = () => { rpcClient.getCommonRpcClient().openExternalUrl({ - url: "https://bi.docs.wso2.com/learn/samples/message-transformation/" + url: "https://bi.docs.wso2.com/integration-guides/integration-as-api/message-transformation/" }) }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/GraphQLDiagram/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/GraphQLDiagram/index.tsx index 1f149d74b69..925faf37976 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/GraphQLDiagram/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/GraphQLDiagram/index.tsx @@ -227,7 +227,7 @@ export function GraphQLDiagram(props: GraphQLDiagramProps) { } actions={ - + Edit diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/SequenceDiagram/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/SequenceDiagram/index.tsx deleted file mode 100644 index 923d24a0c9f..00000000000 --- a/workspaces/ballerina/ballerina-visualizer/src/views/SequenceDiagram/index.tsx +++ /dev/null @@ -1,158 +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, useState } from "react"; -import { useRpcContext } from "@wso2/ballerina-rpc-client"; -import { - LowCodeDiagram, - initVisitor, - PositioningVisitor, - SizingVisitor, - SymbolVisitor, - cleanLocalSymbols, - cleanModuleLevelSymbols, - getSymbolInfo, -} from "@wso2/ballerina-low-code-diagram"; -import { NodePosition, STKindChecker, STNode, traversNode } from "@wso2/syntax-tree"; -import styled from "@emotion/styled"; -import { PanelType, useVisualizerContext } from "../../Context"; -import { ComponentInfo, ConnectorInfo, removeStatement, STModification } from "@wso2/ballerina-core"; -import { URI } from "vscode-uri"; -import { fetchConnectorInfo, retrieveUsedAction } from "../Connectors/ConnectorWizard/utils"; - -enum MESSAGE_TYPE { - ERROR, - WARNING, - INFO, -} - -const Container = styled.div` - width: 100%; - height: calc(100vh - 50px); -`; - -const MessageContainer = styled.div({ - width: "100%", - height: "100%", - display: "flex", - justifyContent: "center", - alignItems: "center", -}); - -interface SequenceDiagramProps { - syntaxTree: STNode; - applyModifications: (modifications: STModification[]) => void; -} - -export function SequenceDiagram(props: SequenceDiagramProps) { - const { syntaxTree, applyModifications } = props; - const { rpcClient } = useRpcContext(); - const { setStatementPosition, setActivePanel, setComponentInfo, setActiveFileInfo, activeFileInfo } = useVisualizerContext(); - - useEffect(() => { - getSequenceModel(); - }, [syntaxTree]); - - const getSequenceModel = () => { - rpcClient - .getLangClientRpcClient() - .getSyntaxTree() - .then(async (model) => { - const parsedModel = sizingAndPositioningST(model.syntaxTree); - const filePath = (await rpcClient.getVisualizerLocation()).documentUri; - const fullST = await rpcClient.getLangClientRpcClient().getST({ - documentIdentifier: { uri: URI.file(filePath).toString() } - }); - setActiveFileInfo({ fullST: fullST?.syntaxTree, filePath, activeSequence: parsedModel }); - }); - }; - - // TODO: Refactor this function - function sizingAndPositioningST( - st: STNode, - experimentalEnabled?: boolean, - showMessage?: ( - arg: string, - messageType: MESSAGE_TYPE, - ignorable: boolean, - filePath?: string, - fileContent?: string, - bypassChecks?: boolean - ) => void - ): STNode { - traversNode(st, initVisitor); - const sizingVisitor = new SizingVisitor(experimentalEnabled); - traversNode(st, sizingVisitor); - if (showMessage && sizingVisitor.getConflictResulutionFailureStatus()) { - showMessage( - "Something went wrong in the diagram rendering.", - MESSAGE_TYPE.ERROR, - false, - undefined, - undefined, - true - ); - } - traversNode(st, new PositioningVisitor()); - cleanLocalSymbols(); - cleanModuleLevelSymbols(); - traversNode(st, SymbolVisitor); - const clone = { ...st }; - return clone; - } - - const handleAddComponent = (position: NodePosition) => { - setStatementPosition(position); - setActivePanel({ isActive: true, name: PanelType.CONSTRUCTPANEL }); - } - - const handleEditComponent = async (model: STNode, targetPosition: NodePosition, componentType: string, connectorInfo?: ConnectorInfo) => { - setStatementPosition(targetPosition); - setComponentInfo({ model, position: targetPosition, componentType, connectorInfo }); - setActivePanel({ isActive: true, name: PanelType.STATEMENTEDITOR }); - } - - const handleDeleteComponent = (model: STNode) => { - const modifications: STModification[] = []; - - // delete action - if (STKindChecker.isIfElseStatement(model) && !model.viewState.isMainIfBody) { - const ifElseRemovePosition = model.position; - ifElseRemovePosition.endLine = model.elseBody.elseBody.position.startLine; - ifElseRemovePosition.endColumn = model.elseBody.elseBody.position.startColumn; - - const deleteConfig: STModification = removeStatement(ifElseRemovePosition); - modifications.push(deleteConfig); - applyModifications(modifications); - } else { - const deleteAction: STModification = removeStatement( - model.position - ); - modifications.push(deleteAction); - applyModifications(modifications); - } - } - - return ( - <> - {!!activeFileInfo?.activeSequence && - - } - - ); -} diff --git a/workspaces/ballerina/bi-diagram/src/components/Diagram.tsx b/workspaces/ballerina/bi-diagram/src/components/Diagram.tsx index 1850793913e..7fb0fea48fe 100644 --- a/workspaces/ballerina/bi-diagram/src/components/Diagram.tsx +++ b/workspaces/ballerina/bi-diagram/src/components/Diagram.tsx @@ -145,7 +145,6 @@ export function Diagram(props: DiagramProps) { const addTargetVisitor = new LinkTargetVisitor(model, nodes); traverseFlow(flowModel, addTargetVisitor); - console.log(">>> getDiagramData", { flowModel, nodes, links }); return { nodes, links }; }; diff --git a/workspaces/ballerina/bi-diagram/src/components/DiagramCanvas.tsx b/workspaces/ballerina/bi-diagram/src/components/DiagramCanvas.tsx index 6915bdc93e2..834dc2bf263 100644 --- a/workspaces/ballerina/bi-diagram/src/components/DiagramCanvas.tsx +++ b/workspaces/ballerina/bi-diagram/src/components/DiagramCanvas.tsx @@ -66,6 +66,7 @@ export function DiagramCanvas(props: DiagramCanvasProps) { void; } @@ -50,6 +51,7 @@ export class NodeLinkModel extends DefaultLinkModel { disabled = false; alignBottom = false; linkBottomOffset = LINK_BOTTOM_OFFSET; + linkCounter?: number; onAddClick?: () => void; constructor(label?: string); @@ -91,6 +93,9 @@ export class NodeLinkModel extends DefaultLinkModel { if ((options as NodeLinkModelOptions).alignBottom === true) { this.alignBottom = (options as NodeLinkModelOptions).alignBottom; } + if ((options as NodeLinkModelOptions).linkCounter) { + this.linkCounter = (options as NodeLinkModelOptions).linkCounter; + } } if ((options as NodeLinkModelOptions).onAddClick) { this.onAddClick = (options as NodeLinkModelOptions).onAddClick; diff --git a/workspaces/ballerina/bi-diagram/src/components/NodeLink/NodeLinkWidget.tsx b/workspaces/ballerina/bi-diagram/src/components/NodeLink/NodeLinkWidget.tsx index bc1840121d5..172d26a7aa7 100644 --- a/workspaces/ballerina/bi-diagram/src/components/NodeLink/NodeLinkWidget.tsx +++ b/workspaces/ballerina/bi-diagram/src/components/NodeLink/NodeLinkWidget.tsx @@ -115,7 +115,12 @@ export const NodeLinkWidget: React.FC = ({ link, engine }) }; return ( - setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > = ({ link, engine }) /> props?.isActiveBreakpoint ? ThemeColors.DEBUGGER_BREAKPOINT_BACKGROUND : ThemeColors.SURFACE_DIM}; @@ -306,7 +307,7 @@ interface AgentCallNodeWidgetProps { onClick?: (node: FlowNode) => void; } -export interface NodeWidgetProps extends Omit {} +export interface NodeWidgetProps extends Omit { } export function AgentCallNodeWidget(props: AgentCallNodeWidgetProps) { const { model, engine, onClick } = props; @@ -479,9 +480,10 @@ export function AgentCallNodeWidget(props: AgentCallNodeWidgetProps) { const disabled = model.node.suggested; const nodeTitle = "AI Agent"; const hasError = nodeHasError(model.node); - const tools = model.node.metadata?.data?.tools || []; - if (model.node.metadata.data?.agent) { - model.node.metadata.data.agent = sanitizeAgentData(model.node.metadata.data.agent); + const nodeMetadata = model?.node.metadata.data as NodeMetadata; + const tools = nodeMetadata?.tools || []; + if (nodeMetadata?.agent) { + nodeMetadata.agent = sanitizeAgentData(nodeMetadata.agent); } let containerHeight = NODE_HEIGHT + AGENT_NODE_TOOL_SECTION_GAP + AGENT_NODE_ADD_TOOL_BUTTON_WIDTH + AGENT_NODE_TOOL_GAP * 2; @@ -490,7 +492,7 @@ export function AgentCallNodeWidget(props: AgentCallNodeWidgetProps) { } return ( - + - {model.node.metadata?.data.memory ? ( + {nodeMetadata?.memory ? (
Memory - {model.node.metadata.data.memory?.type || + {nodeMetadata?.memory?.type || "MessageWindowChatMemory"}
@@ -600,9 +602,9 @@ export function AgentCallNodeWidget(props: AgentCallNodeWidgetProps) { - {model.node.metadata.data?.agent?.role ? ( + {nodeMetadata?.agent?.role ? ( - {model.node.metadata.data.agent.role} + {nodeMetadata?.agent?.role} ) : ( @@ -610,10 +612,10 @@ export function AgentCallNodeWidget(props: AgentCallNodeWidgetProps) { )} - {model.node.metadata.data?.agent?.instructions ? ( + {nodeMetadata?.agent?.instructions ? ( - {model.node.metadata.data.agent.instructions} + {nodeMetadata.agent.instructions} ) : ( @@ -660,7 +662,7 @@ export function AgentCallNodeWidget(props: AgentCallNodeWidgetProps) { fill={ThemeColors.ON_SURFACE} style={{ pointerEvents: "none" }} > - {getLlmModelIcons(model.node.metadata.data.model?.type)} + {getLlmModelIcons(nodeMetadata?.model?.type)} ( onToolClick(tool)} css={css` cursor: pointer; @@ -861,11 +862,10 @@ export function AgentCallNodeWidget(props: AgentCallNodeWidgetProps) { {/* Add "Add new tool" button below all tools */} 0 - ? (tools.length + 1) * (NODE_HEIGHT + AGENT_NODE_TOOL_GAP) + AGENT_NODE_TOOL_SECTION_GAP - : NODE_HEIGHT + AGENT_NODE_TOOL_SECTION_GAP - })`} + transform={`translate(-11, ${tools.length > 0 + ? (tools.length + 1) * (NODE_HEIGHT + AGENT_NODE_TOOL_GAP) + AGENT_NODE_TOOL_SECTION_GAP + : NODE_HEIGHT + AGENT_NODE_TOOL_SECTION_GAP + })`} onClick={onAddToolClick} style={{ cursor: "pointer" }} > diff --git a/workspaces/ballerina/bi-diagram/src/components/nodes/EmptyNode/EmptyNodeModel.ts b/workspaces/ballerina/bi-diagram/src/components/nodes/EmptyNode/EmptyNodeModel.ts index d48940d0e82..8357206c0a1 100644 --- a/workspaces/ballerina/bi-diagram/src/components/nodes/EmptyNode/EmptyNodeModel.ts +++ b/workspaces/ballerina/bi-diagram/src/components/nodes/EmptyNode/EmptyNodeModel.ts @@ -29,8 +29,9 @@ export class EmptyNodeModel extends NodeModel { readonly showButton: boolean; topNode: FlowNode | Branch; // top statement node or parent block node target: LinePosition; + visibleBtnCounter: number; - constructor(id: string, visible = true, button = false) { + constructor(id: string, visible = true, button = false, visibleBtnCounter = 0) { super({ id, type: NodeTypes.EMPTY_NODE, @@ -40,6 +41,7 @@ export class EmptyNodeModel extends NodeModel { this.addOutPort("out"); this.visible = visible; this.showButton = button; + this.visibleBtnCounter = visibleBtnCounter; } addPort(port: T): T { diff --git a/workspaces/ballerina/bi-diagram/src/components/nodes/EmptyNode/EmptyNodeWidget.tsx b/workspaces/ballerina/bi-diagram/src/components/nodes/EmptyNode/EmptyNodeWidget.tsx index 8b7c62dfd26..889f6e6b598 100644 --- a/workspaces/ballerina/bi-diagram/src/components/nodes/EmptyNode/EmptyNodeWidget.tsx +++ b/workspaces/ballerina/bi-diagram/src/components/nodes/EmptyNode/EmptyNodeWidget.tsx @@ -170,6 +170,7 @@ export function EmptyNodeWidget(props: EmptyNodeWidgetProps) { /> >> node factory visitor started"); } + private createNodeLinkWithCounter(sourceNode: NodeModel, targetNode: NodeModel, options?: NodeLinkModelOptions): NodeLinkModel { + options = { + ...options, + linkCounter: this.linkCounter++, + }; + return createNodesLink(sourceNode, targetNode, options); + } + private updateNodeLinks(node: FlowNode, nodeModel: NodeModel, options?: NodeLinkModelOptions): void { if (node.viewState?.startNodeId) { // new sub flow start const startNode = this.nodes.find((n) => n.getID() === node.viewState.startNodeId); - const link = createNodesLink(startNode, nodeModel, options); + const link = this.createNodeLinkWithCounter(startNode, nodeModel, options); if (link) { this.links.push(link); } this.lastNodeModel = undefined; } else if (this.lastNodeModel) { - const link = createNodesLink(this.lastNodeModel, nodeModel, options); + const link = this.createNodeLinkWithCounter(this.lastNodeModel, nodeModel, options); if (link) { this.links.push(link); } @@ -93,7 +103,7 @@ export class NodeFactoryVisitor implements BaseVisitor { } private createEmptyNode(id: string, x: number, y: number, visible = true, showButton = false): EmptyNodeModel { - const nodeModel = new EmptyNodeModel(id, visible, showButton); + const nodeModel = new EmptyNodeModel(id, visible, showButton, this.visibleBtnCounter++); nodeModel.setPosition(x, y); this.nodes.push(nodeModel); return nodeModel; @@ -214,7 +224,7 @@ export class NodeFactoryVisitor implements BaseVisitor { return; } - const link = createNodesLink(ifNodeModel, firstChildNodeModel, { + const link = this.createNodeLinkWithCounter(ifNodeModel, firstChildNodeModel, { id: getBranchInLinkId(node.id, branch.label, index), label: getBranchLabel(branch), }); @@ -262,13 +272,13 @@ export class NodeFactoryVisitor implements BaseVisitor { branchEmptyNodeModel.metadata?.draft ? false : true // else branch is draft ); const noElseBranch = branchEmptyNodeModel.metadata?.draft; - const linkIn = createNodesLink(ifNodeModel, branchEmptyNode, { + const linkIn = this.createNodeLinkWithCounter(ifNodeModel, branchEmptyNode, { id: getBranchInLinkId(node.id, branch.label, index), label: noElseBranch ? "" : getBranchLabel(branch), brokenLine: noElseBranch, showAddButton: false, }); - const linkOut = createNodesLink(branchEmptyNode, endIfEmptyNode, { + const linkOut = this.createNodeLinkWithCounter(branchEmptyNode, endIfEmptyNode, { brokenLine: true, showAddButton: false, alignBottom: true, @@ -286,7 +296,7 @@ export class NodeFactoryVisitor implements BaseVisitor { return; } - const link = createNodesLink(lastChildNodeModel, endIfEmptyNode, { + const link = this.createNodeLinkWithCounter(lastChildNodeModel, endIfEmptyNode, { alignBottom: true, brokenLine: lastNode.returning, showAddButton: !lastNode.returning, @@ -351,7 +361,7 @@ export class NodeFactoryVisitor implements BaseVisitor { if (branch.children && branch.children.length > 0) { const firstChildNodeModel = this.getBranchStartNode(branch); if (firstChildNodeModel) { - const link = createNodesLink(containerNodeModel, firstChildNodeModel); + const link = this.createNodeLinkWithCounter(containerNodeModel, firstChildNodeModel); if (link) { this.links.push(link); } @@ -381,10 +391,10 @@ export class NodeFactoryVisitor implements BaseVisitor { true, true ); - const linkIn = createNodesLink(containerNodeModel, branchEmptyNode, { + const linkIn = this.createNodeLinkWithCounter(containerNodeModel, branchEmptyNode, { showAddButton: false, }); - const linkOut = createNodesLink(branchEmptyNode, endContainerEmptyNode, { + const linkOut = this.createNodeLinkWithCounter(branchEmptyNode, endContainerEmptyNode, { showAddButton: false, alignBottom: true, }); @@ -401,7 +411,7 @@ export class NodeFactoryVisitor implements BaseVisitor { return; } - const endLink = createNodesLink(lastChildNodeModel, endContainerEmptyNode, { + const endLink = this.createNodeLinkWithCounter(lastChildNodeModel, endContainerEmptyNode, { alignBottom: true, showAddButton: !lastNode.returning, }); @@ -457,7 +467,7 @@ export class NodeFactoryVisitor implements BaseVisitor { containerStartEmptyNode.setParentFlowNode(node); this.nodes.push(containerStartEmptyNode); - this.updateNodeLinks(node, containerStartEmptyNode); + // this.updateNodeLinks(node, containerStartEmptyNode); this.addSuggestionsButton(node); this.lastNodeModel = undefined; } @@ -486,7 +496,7 @@ export class NodeFactoryVisitor implements BaseVisitor { if (bodyBranch.children && bodyBranch.children.length > 0) { const firstChildNodeModel = this.getBranchStartNode(bodyBranch); if (firstChildNodeModel) { - const link = createNodesLink(containerStartEmptyNodeModel, firstChildNodeModel); + const link = this.createNodeLinkWithCounter(containerStartEmptyNodeModel, firstChildNodeModel); if (link) { this.links.push(link); } @@ -501,7 +511,7 @@ export class NodeFactoryVisitor implements BaseVisitor { // // link last node of body branch to container node model // const lastNodeModel = this.getBranchEndNode(bodyBranch); // if (lastNodeModel) { - // const link = createNodesLink(lastNodeModel, containerNodeModel); + // const link = this.createNodeLinkWithCounter(lastNodeModel, containerNodeModel); // if (link) { // this.links.push(link); // } @@ -538,7 +548,7 @@ export class NodeFactoryVisitor implements BaseVisitor { true ); branchEmptyNode.setParentFlowNode(node); - const linkIn = createNodesLink(containerStartEmptyNodeModel, branchEmptyNode, { + const linkIn = this.createNodeLinkWithCounter(containerStartEmptyNodeModel, branchEmptyNode, { showAddButton: false, }); if (linkIn) { @@ -548,7 +558,7 @@ export class NodeFactoryVisitor implements BaseVisitor { const lastNodeModel = this.getBranchEndNode(bodyBranch); if (lastNodeModel) { - const linkOut = createNodesLink(lastNodeModel, containerNodeModel, { + const linkOut = this.createNodeLinkWithCounter(lastNodeModel, containerNodeModel, { showAddButton: lastNodeModel.getID() !== getCustomNodeId(node.id, bodyBranch.label), showArrow: false, }); diff --git a/workspaces/ballerina/bi-diagram/src/visitors/SizingVisitor.ts b/workspaces/ballerina/bi-diagram/src/visitors/SizingVisitor.ts index ec4d9a92f2c..a2e11a8d7a3 100644 --- a/workspaces/ballerina/bi-diagram/src/visitors/SizingVisitor.ts +++ b/workspaces/ballerina/bi-diagram/src/visitors/SizingVisitor.ts @@ -16,6 +16,7 @@ * under the License. */ +import { NodeMetadata } from "@wso2/ballerina-core"; import { AGENT_NODE_ADD_TOOL_BUTTON_WIDTH, AGENT_NODE_TOOL_GAP, @@ -247,7 +248,8 @@ export class SizingVisitor implements BaseVisitor { const containerRightWidth = halfNodeWidth + NODE_GAP_X + NODE_HEIGHT + LABEL_HEIGHT + LABEL_WIDTH; // Calculate node height based on node type - const tools = node.metadata?.data?.tools || []; + const nodeMetadata = node.metadata.data as NodeMetadata; + const tools = nodeMetadata?.tools || []; const numberOfCircles = tools.length || 0; let containerHeight = NODE_HEIGHT + AGENT_NODE_TOOL_SECTION_GAP + AGENT_NODE_ADD_TOOL_BUTTON_WIDTH + AGENT_NODE_TOOL_GAP * 2; if (numberOfCircles > 0) { diff --git a/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/EntryNodeWidget.tsx b/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/EntryNodeWidget.tsx index b8b3a6be23e..aa0c0e21e91 100644 --- a/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/EntryNodeWidget.tsx +++ b/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/EntryNodeWidget.tsx @@ -92,6 +92,21 @@ const Accessor = styled(StyledText)` font-family: "GilmerBold"; `; +const ResourceAccessor = styled(StyledText)<{ color?: string }>` + text-transform: uppercase; + font-family: "GilmerBold"; + background-color: ${(props) => props.color}; + color: #FFF; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + justify-content: center; + min-width: 60px; + text-align: center; + align-items: center; + font-weight: bold; +`; + const Description = styled(StyledText)` font-size: 12px; max-width: ${ENTRY_NODE_WIDTH - 80}px; @@ -175,6 +190,16 @@ const ViewAllButtonWrapper = styled.div` width: 100%; `; +const colors = { + "GET": '#3d7eff', + "PUT": '#fca130', + "POST": '#49cc90', + "DELETE": '#f93e3e', + "PATCH": '#986ee2', + "OPTIONS": '#0d5aa7', + "HEAD": '#9012fe' +} + interface EntryNodeWidgetProps { model: EntryNodeModel; engine: DiagramEngine; @@ -185,20 +210,24 @@ export interface NodeWidgetProps extends Omit export function EntryNodeWidget(props: EntryNodeWidgetProps) { const { model, engine } = props; const [isHovered, setIsHovered] = React.useState(false); - const { - onServiceSelect, - onAutomationSelect, - onDeleteComponent, - onFunctionSelect, + const { + onServiceSelect, + onAutomationSelect, + onDeleteComponent, + onFunctionSelect, expandedNodes, - onToggleNodeExpansion + onToggleNodeExpansion } = useDiagramContext(); const [menuAnchorEl, setMenuAnchorEl] = useState(null); const isMenuOpen = Boolean(menuAnchorEl); const handleOnClick = () => { if (model.type === "service") { - onServiceSelect(model.node as CDService); + if ((model.node as CDService).type === "ai:Service") { + onFunctionSelect(serviceFunctions[0]); + } else { + onServiceSelect(model.node as CDService); + } } else { onAutomationSelect(model.node as CDAutomation); } @@ -270,7 +299,7 @@ export function EntryNodeWidget(props: EntryNodeWidgetProps) { const isExpanded = expandedNodes.has(model.node.uuid); const hasMoreFunctions = serviceFunctions.length > 3; - + let visibleFunctions; if (serviceFunctions.length <= 3 || isExpanded) { // Show all functions if ≤3 total or if expanded @@ -332,7 +361,7 @@ export function EntryNodeWidget(props: EntryNodeWidgetProps) { onClick={handleOnClick} > {getNodeIcon()} -
+
{getNodeTitle()} {getNodeDescription()}
@@ -382,6 +411,27 @@ export function EntryNodeWidget(props: EntryNodeWidgetProps) { ); } +export function getColorByMethod(method: string) { + switch (method.toUpperCase()) { + case "GET": + return colors.GET; + case "PUT": + return colors.PUT; + case "POST": + return colors.POST; + case "DELETE": + return colors.DELETE; + case "PATCH": + return colors.PATCH; + case "OPTIONS": + return colors.OPTIONS; + case "HEAD": + return colors.HEAD; + default: + return '#876036'; // Default color + } +} + function FunctionBox(props: { func: CDFunction | CDResourceFunction; model: EntryNodeModel; engine: DiagramEngine }) { const { func, model, engine } = props; const [isHovered, setIsHovered] = useState(false); @@ -411,7 +461,9 @@ function FunctionBox(props: { func: CDFunction | CDResourceFunction; model: Entr onMouseLeave={() => setIsHovered(false)} > {(func as CDResourceFunction).accessor && ( - {getAccessorDisplay((func as CDResourceFunction).accessor, isGraphQL)} + + {getAccessorDisplay((func as CDResourceFunction).accessor, isGraphQL)} + )} {isGraphQL && !(func as CDResourceFunction).accessor && (func as CDFunction).name && ( Mutation diff --git a/workspaces/ballerina/data-mapper-view/src/components/DataMapper/DataMapper.tsx b/workspaces/ballerina/data-mapper-view/src/components/DataMapper/DataMapper.tsx index e634926f9c8..055a15e7bcc 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/DataMapper/DataMapper.tsx +++ b/workspaces/ballerina/data-mapper-view/src/components/DataMapper/DataMapper.tsx @@ -163,6 +163,11 @@ export interface ExpressionInfo { label?: string; } +export interface InternalError { + hasError: boolean; + message?: string; +} + export interface DMNode { // the parent node of the selected node stNode: STNode; @@ -257,7 +262,7 @@ export function DataMapperC(props: DataMapperViewProps) { const [dmContext, setDmContext] = useState(); const [dmNodes, setDmNodes] = useState(); const [shouldRestoreTypes, setShouldRestoreTypes] = useState(true); - const [hasInternalError, setHasInternalError] = useState(false); + const [internalError, setInternalError] = useState(); const [errorKind, setErrorKind] = useState(); const [isSelectionComplete, setIsSelectionComplete] = useState(false); const [currentReferences, setCurrentReferences] = useState([]); @@ -410,7 +415,7 @@ export function DataMapperC(props: DataMapperViewProps) { dispatchSelection({ type: ViewOption.INITIALIZE, payload: { prevST, selectedST: selectedST || defaultSt } }); } catch (e) { - setHasInternalError(true); + setInternalError({ hasError: true}); // tslint:disable-next-line:no-console console.error(e); } @@ -492,7 +497,7 @@ export function DataMapperC(props: DataMapperViewProps) { setDmNodes(nodes); } } catch (e) { - setHasInternalError(true); + setInternalError({ hasError: true, message: e.message}); // tslint:disable-next-line:no-console console.error(e); } @@ -569,7 +574,7 @@ export function DataMapperC(props: DataMapperViewProps) { }; return ( - + {selection.state === DMState.INITIALIZED && (
diff --git a/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/Error/index.tsx b/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/Error/index.tsx deleted file mode 100644 index c8d541e5ae8..00000000000 --- a/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/Error/index.tsx +++ /dev/null @@ -1,53 +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 * as React from "react"; - -import { useStyles } from "./style"; -import { Typography } from "@wso2/ui-toolkit"; -import { ISSUES_URL } from "../../utils"; - -export default function Default() { - const classes = useStyles(); - - return ( -
-
- - - - - -
- - A problem occurred while rendering the Data Mapper. - - - Please raise an issue with the sample code in our issue tracker - -
- ); -} diff --git a/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/Error/style.ts b/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/Error/style.ts deleted file mode 100644 index 2f55e24ab9c..00000000000 --- a/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/Error/style.ts +++ /dev/null @@ -1,57 +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 { css } from "@emotion/css"; - -export const useStyles = () => ({ - root: css({ - flexGrow: 1, - margin: '25vh auto', - width: 'fit-content' - }), - errorContainer: css({ - display: "flex", - justifyContent: "center", - alignItems: "center", - flexDirection: "column" - }), - errorTitle: css({ - color: "var(--vscode-badge-background)", - textAlign: "center" - }), - errorMsg: css({ - paddingTop: "16px", - color: "var(--vscode-checkbox-border)", - textAlign: "center" - }), - errorImg: css({ - display: 'flex', - justifyContent: 'center', - width: '100%' - }), - gridContainer: css({ - height: "100%" - }), - link: css({ - color: "var(--vscode-editor-selectionBackground)", - textDecoration: "underline", - "&:hover, &:focus, &:active": { - color: "var(--vscode-editor-selectionBackground)", - textDecoration: "underline", - } - }) -}); diff --git a/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/index.tsx b/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/index.tsx index 8831d90c525..5d222ac1fb9 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/index.tsx +++ b/workspaces/ballerina/data-mapper-view/src/components/DataMapper/ErrorBoundary/index.tsx @@ -17,19 +17,52 @@ */ import * as React from "react"; -import ErrorScreen from "./Error"; +import { WarningBanner } from "../Warning/DataMapperWarning"; +import { css, keyframes } from "@emotion/css"; +import { Typography } from "@wso2/ui-toolkit"; +import { ISSUES_URL } from "../utils"; + +const fadeIn = keyframes` + from { opacity: 0.5; } + to { opacity: 1; } +`; + +const classes = { + errorContainer: css({ + display: 'flex', + flexDirection: 'column', + opacity: 0.7 + }), + errorBanner: css({ + borderColor: "var(--vscode-errorForeground)" + }), + errorMessage: css({ + zIndex: 1, + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '500px', + animation: `${fadeIn} 0.5s ease-in-out` + }), + errorTitle: css({ + marginTop: "10px" + }) +} export interface DataMapperErrorBoundaryProps { hasError: boolean; + message?: string; children?: React.ReactNode; } -export class DataMapperErrorBoundaryC extends React.Component { - state = { hasError: false } +export class DataMapperErrorBoundaryC extends React.Component { + state = { hasError: false, message: "" } static getDerivedStateFromProps(props: DataMapperErrorBoundaryProps) { return { - hasError: props.hasError + hasError: props.hasError, + message: props.message }; } @@ -42,9 +75,27 @@ export class DataMapperErrorBoundaryC extends React.Component + + A problem occurred while rendering the Data Mapper. + + + Please raise an issue with the sample code in our issue tracker + +
+ ); + render() { if (this.state.hasError) { - return ; + return ( +
+ +
+ ); } return this.props?.children; } diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Label/ArrayMappingOptionsWidget.tsx b/workspaces/ballerina/data-mapper-view/src/components/Diagram/Label/ArrayMappingOptionsWidget.tsx index f5b4e7d79b0..f7304d96e3a 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Label/ArrayMappingOptionsWidget.tsx +++ b/workspaces/ballerina/data-mapper-view/src/components/Diagram/Label/ArrayMappingOptionsWidget.tsx @@ -145,7 +145,7 @@ export function ArrayMappingOptionsWidget(props: ArrayMappingOptionsWidgetProps) }, { id: "a2a-inner", - label: getItemElement("a2a-inner", "Map Array Elements Individually"), + label: getItemElement("a2a-inner", "Map Using Query Expression"), onClick: onClickMapIndividualElements } ]; @@ -158,7 +158,7 @@ export function ArrayMappingOptionsWidget(props: ArrayMappingOptionsWidgetProps) }, { id: "a2s-inner", - label: getItemElement("a2s-inner", "Map Array Elements Individually and access element"), + label: getItemElement("a2s-inner", "Map via Query Expression and access element"), onClick: () => onClickMapIndividualElements(ClauseType.Select, true) } ]; diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Link/link-utils.ts b/workspaces/ballerina/data-mapper-view/src/components/Diagram/Link/link-utils.ts index 36f651f780a..91777eccc70 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Link/link-utils.ts +++ b/workspaces/ballerina/data-mapper-view/src/components/Diagram/Link/link-utils.ts @@ -141,10 +141,6 @@ export function removePendingMappingTempLinkIfExists(link: LinkModel) { } export function userActionRequiredMapping(mappingType: MappingType, targetPort: PortModel): boolean { - if (targetPort instanceof RecordFieldPortModel && !targetPort?.parentModel) { - // No user action required provided for root target ports. - return false; - } return mappingType === MappingType.ArrayToArray || mappingType === MappingType.ArrayToSingleton || mappingType === MappingType.RecordToRecord diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/LinkState/CreateLinkState.ts b/workspaces/ballerina/data-mapper-view/src/components/Diagram/LinkState/CreateLinkState.ts index b4ad5cd4170..84c6882362a 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/LinkState/CreateLinkState.ts +++ b/workspaces/ballerina/data-mapper-view/src/components/Diagram/LinkState/CreateLinkState.ts @@ -60,7 +60,7 @@ export class CreateLinkState extends State { } if (element instanceof RequiredParamNode - || element instanceof FromClauseNode + || element instanceof FromClauseNode || element instanceof LetExpressionNode || element instanceof ModuleVariableNode || element instanceof EnumTypeNode diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/LinkConnector/LinkConnectorNode.ts b/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/LinkConnector/LinkConnectorNode.ts index 361976b4771..0a5ed2dde43 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/LinkConnector/LinkConnectorNode.ts +++ b/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/LinkConnector/LinkConnectorNode.ts @@ -70,7 +70,7 @@ export class LinkConnectorNode extends DataMapperNodeModel { public value: string; public diagnostics: Diagnostic[]; public hidden: boolean; - public hasInitialized: boolean; + public hasScreenWidthChanged: boolean; constructor( public context: IDataMapperContext, @@ -151,11 +151,7 @@ export class LinkConnectorNode extends DataMapperNodeModel { node.recordField, targetPortPrefix, (portId: string) => node.getPort(portId) as RecordFieldPortModel, rootName); - const previouslyHidden = this.hidden; this.hidden = this.targetMappedPort?.portName !== this.targetPort?.portName; - if (this.hidden !== previouslyHidden) { - this.hasInitialized = false; - } } } }); @@ -163,7 +159,7 @@ export class LinkConnectorNode extends DataMapperNodeModel { } initLinks(): void { - if (this.hasInitialized) { + if (this.hasScreenWidthChanged) { return; } if (!this.hidden) { @@ -258,7 +254,6 @@ export class LinkConnectorNode extends DataMapperNodeModel { }) } } - this.hasInitialized = true; } updateSource(): void { @@ -349,4 +344,8 @@ export class LinkConnectorNode extends DataMapperNodeModel { } } + + public setHasScreenWidthChanged(hasScreenWidthChanged: boolean): void { + this.hasScreenWidthChanged = hasScreenWidthChanged; + } } diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/QueryExpression/QueryExpressionNode.ts b/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/QueryExpression/QueryExpressionNode.ts index 83e82d96f48..892236a1ea8 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/QueryExpression/QueryExpressionNode.ts +++ b/workspaces/ballerina/data-mapper-view/src/components/Diagram/Node/QueryExpression/QueryExpressionNode.ts @@ -76,7 +76,7 @@ export class QueryExpressionNode extends DataMapperNodeModel { public targetFieldFQN: string; public hidden: boolean; - public hasInitialized: boolean; + public hasScreenWidthChanged: boolean; constructor( public context: IDataMapperContext, @@ -279,19 +279,14 @@ export class QueryExpressionNode extends DataMapperNodeModel { }); } - const previouslyHidden = this.hidden; this.hidden = this.targetPort?.hidden; - - if (this.hidden !== previouslyHidden) { - this.hasInitialized = false; - } while (this.targetPort && this.targetPort.hidden){ this.targetPort = this.targetPort.parentModel; } } initLinks(): void { - if (this.hasInitialized) { + if (this.hasScreenWidthChanged) { return; } if (!this.hidden) { @@ -357,7 +352,6 @@ export class QueryExpressionNode extends DataMapperNodeModel { this.getModel().addAll(link); } } - this.hasInitialized = true; } public updatePosition() { @@ -395,4 +389,8 @@ export class QueryExpressionNode extends DataMapperNodeModel { this.context.applyModifications(modifications); } + + public setHasScreenWidthChanged(hasScreenWidthChanged: boolean): void { + this.hasScreenWidthChanged = hasScreenWidthChanged; + } } diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/OverriddenLinkLayer/LabelWidget.tsx b/workspaces/ballerina/data-mapper-view/src/components/Diagram/OverriddenLinkLayer/LabelWidget.tsx index 357623ca782..f6120d0df40 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/OverriddenLinkLayer/LabelWidget.tsx +++ b/workspaces/ballerina/data-mapper-view/src/components/Diagram/OverriddenLinkLayer/LabelWidget.tsx @@ -81,11 +81,35 @@ export class OveriddenLabelWidget extends React.Component { }; const pathCentre = path.getPointAtLength(position); + const canvas = this.props.engine.getCanvas(); + + // Get canvas boundaries + const canvasWidth = canvas?.offsetWidth || 0; + const canvasHeight = canvas?.offsetHeight || 0; + + // Calculate initial centered position + let x = pathCentre.x - labelDimensions.width / 2 + this.props.label.getOptions().offsetX; + let y = pathCentre.y - labelDimensions.height / 2 + this.props.label.getOptions().offsetY; + + // Apply boundary constraints to keep label fully visible + // Ensure label doesn't go off the left edge + if (x < 0) { + x = 0; + } + // Ensure label doesn't go off the right edge + if (x + labelDimensions.width > canvasWidth) { + x = canvasWidth - labelDimensions.width; + } + // Ensure label doesn't go off the top edge + if (y < 0) { + y = 0; + } + // Ensure label doesn't go off the bottom edge + if (y + labelDimensions.height > canvasHeight) { + y = canvasHeight - labelDimensions.height; + } - const labelCoordinates = { - x: pathCentre.x - labelDimensions.width / 2 + this.props.label.getOptions().offsetX, - y: pathCentre.y - labelDimensions.height / 2 + this.props.label.getOptions().offsetY - }; + const labelCoordinates = { x, y }; this.ref.current.style.transform = `translate(${labelCoordinates.x}px, ${labelCoordinates.y}px)`; }; diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/utils/dm-utils.ts b/workspaces/ballerina/data-mapper-view/src/components/Diagram/utils/dm-utils.ts index fdc15029658..067f9cd91f6 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/utils/dm-utils.ts +++ b/workspaces/ballerina/data-mapper-view/src/components/Diagram/utils/dm-utils.ts @@ -973,7 +973,7 @@ export function getOutputPortForField(fields: STNode[], } const outputSearchValue = useDMSearchStore.getState().outputSearch; - const memberAccessRegex = /\.\d+$/; + const memberAccessRegex = /(.+\.\d+(?:\.[a-zA-Z]+)*)/; const isMemberAccessPattern = memberAccessRegex.test(portIdBuffer); const lastPortIdSegment = portIdBuffer.split('.').slice(-1)[0]; if (outputSearchValue !== '' @@ -1725,11 +1725,30 @@ export function getFnDefForFnCall(node: FunctionCall): FnDefInfo { export function getFilteredMappings(mappings: FieldAccessToSpecificFied[], searchValue: string) { return mappings.filter(mapping => { if (mapping) { + if (searchValue === "") { + return true; + } + const lastField = mapping.fields[mapping.fields.length - 1]; const fieldName = STKindChecker.isSpecificField(lastField) ? lastField.fieldName?.value || lastField.fieldName.source : lastField.source; - return searchValue === "" || fieldName.toLowerCase().includes(searchValue.toLowerCase()); + const isFieldMatch = fieldName.toLowerCase().includes(searchValue.toLowerCase()); + if (isFieldMatch) { + return true; + } + + const elementsBeforeListConstructors = getElementsBeforeListConstructors(mapping.fields); + if (elementsBeforeListConstructors) { + for (const element of elementsBeforeListConstructors) { + const fieldName = STKindChecker.isSpecificField(element) + ? element.fieldName?.value || element.fieldName.source + : element.source; + if (fieldName.toLowerCase().includes(searchValue.toLowerCase())) { + return true; + } + } + } } }); } @@ -2213,3 +2232,26 @@ export const getSearchFilteredOutput = (type: TypeField) => { } return null; } + +/** + * Returns an array of STNodes(fields) that appear before each ListConstructor in the fields array. + * @param fields - Array of STNodes to search through + * @returns Array of STNodes that appear before each ListConstructor + */ +export function getElementsBeforeListConstructors(fields: STNode[]): STNode[] { + if (!fields || fields.length < 2) { + return []; + } + + const elementsBeforeListConstructors: STNode[] = []; + + // Find all indices where ListConstructors occur and collect elements before them + fields.forEach((field, index) => { + if (STKindChecker.isListConstructor(field) && index > 0) { + // Add the element before this ListConstructor + elementsBeforeListConstructors.push(fields[index - 1]); + } + }); + + return elementsBeforeListConstructors; +} diff --git a/workspaces/ballerina/data-mapper-view/src/components/Diagram/visitors/NodeInitVisitor.ts b/workspaces/ballerina/data-mapper-view/src/components/Diagram/visitors/NodeInitVisitor.ts index 8921cb6b715..35d32f839c1 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Diagram/visitors/NodeInitVisitor.ts +++ b/workspaces/ballerina/data-mapper-view/src/components/Diagram/visitors/NodeInitVisitor.ts @@ -81,6 +81,9 @@ import { QueryParentFindingVisitor } from "./QueryParentFindingVisitor" import { QueryExprFindingVisitorByPosition } from "./QueryExprFindingVisitorByPosition"; import { DefaultPortModel } from "@projectstorm/react-diagrams"; +const INNER_FROM_CLAUSE_ERROR_MESSAGE = `This mapping contains a query expression with inner 'from' clauses, which are not currently supported in the visual data mapper. +Please switch to the code editor to continue working with this mapping.`; + export class NodeInitVisitor implements Visitor { private inputParamNodes: DataMapperNodeModel[] = []; @@ -134,6 +137,12 @@ export class NodeInitVisitor implements Visitor { ); } else if (STKindChecker.isQueryExpression(bodyExpr)) { const { queryPipeline: { fromClause, intermediateClauses } } = bodyExpr; + + const hasInnerFromClauses = intermediateClauses.some(clause => STKindChecker.isFromClause(clause)); + if (hasInnerFromClauses) { + throw new Error(INNER_FROM_CLAUSE_ERROR_MESSAGE); + } + if (this.context.selection.selectedST.fieldPath === FUNCTION_BODY_QUERY) { isFnBodyQueryExpr = true; const selectClause = bodyExpr?.selectClause || bodyExpr?.resultClause; @@ -527,6 +536,12 @@ export class NodeInitVisitor implements Visitor { if (isSelectedExpr) { const { fromClause, intermediateClauses } = node.queryPipeline; + + const hasInnerFromClauses = intermediateClauses.some(clause => STKindChecker.isFromClause(clause)); + if (hasInnerFromClauses) { + throw new Error(INNER_FROM_CLAUSE_ERROR_MESSAGE); + } + if (parentIdentifier) { const intermediateClausesHeight = 100 + intermediateClauses.length * OFFSETS.INTERMEDIATE_CLAUSE_HEIGHT; // create output node diff --git a/workspaces/ballerina/data-mapper-view/src/components/Hooks/index.tsx b/workspaces/ballerina/data-mapper-view/src/components/Hooks/index.tsx index e0256401115..1832ab01a9e 100644 --- a/workspaces/ballerina/data-mapper-view/src/components/Hooks/index.tsx +++ b/workspaces/ballerina/data-mapper-view/src/components/Hooks/index.tsx @@ -121,6 +121,9 @@ export const useDiagramModel = ( if (prevScreenWidth.current !== screenWidth && diagramModel.getNodes().length > 0) { const diagModelNodes = diagramModel.getNodes() as DataMapperNodeModel[]; diagModelNodes.forEach(diagModelNode => { + if (diagModelNode instanceof LinkConnectorNode || diagModelNode instanceof QueryExpressionNode) { + diagModelNode.setHasScreenWidthChanged(true); + } const repositionedNode = nodes.find(newNode => isOutputNode(newNode) && newNode.id === diagModelNode.id); if (repositionedNode) { diagModelNode.setPosition(repositionedNode.getX(), repositionedNode.getY()); @@ -159,6 +162,7 @@ export const useDiagramModel = ( node.setModel(newModel); await node.initPorts(); if (node instanceof LinkConnectorNode || node instanceof QueryExpressionNode) { + node.setHasScreenWidthChanged(false); continue; } node.initLinks(); diff --git a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNode.ts b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNode.ts index d15e7b35a5e..eba900b1697 100644 --- a/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNode.ts +++ b/workspaces/ballerina/inline-data-mapper/src/components/Diagram/Node/ObjectOutput/ObjectOutputNode.ts @@ -24,7 +24,6 @@ import { DataMapperNodeModel } from "../commons/DataMapperNode"; import { getFilteredMappings, getSearchFilteredOutput, hasNoOutputMatchFound } from "../../utils/search-utils"; import { getTypeName } from "../../utils/type-utils"; import { OBJECT_OUTPUT_TARGET_PORT_PREFIX } from "../../utils/constants"; -import { STNode } from "@wso2/syntax-tree"; import { findInputNode } from "../../utils/node-utils"; import { InputOutputPortModel } from "../../Port"; import { DataMapperLinkModel } from "../../Link"; diff --git a/workspaces/ballerina/type-diagram/package.json b/workspaces/ballerina/type-diagram/package.json index 2250d0af7e9..bed4ec8467f 100644 --- a/workspaces/ballerina/type-diagram/package.json +++ b/workspaces/ballerina/type-diagram/package.json @@ -28,7 +28,7 @@ "closest": "^0.0.1", "dagre": "^0.8.5", "file-loader": "^6.2.0", - "html-to-image": "^1.11.13", + "html-to-image": "^1.11.11", "lodash": "^4.17.21", "pathfinding": "^0.4.18", "paths-js": "^0.4.11", diff --git a/workspaces/ballerina/type-diagram/src/Diagram.tsx b/workspaces/ballerina/type-diagram/src/Diagram.tsx index 9a98cae7de8..1c3e3e7812b 100644 --- a/workspaces/ballerina/type-diagram/src/Diagram.tsx +++ b/workspaces/ballerina/type-diagram/src/Diagram.tsx @@ -63,7 +63,7 @@ export function TypeDiagram(props: TypeDiagramProps) { const drawDiagram = (focusedNode?: string) => { let diagramModel; - + // Create diagram model based on type if (isGraphql && rootService) { console.log("Modeling graphql diagram"); @@ -72,24 +72,24 @@ export function TypeDiagram(props: TypeDiagramProps) { console.log("Modeling entity diagram"); diagramModel = entityModeller(typeModel, focusedNode); } - + if (diagramModel) { // Setup initial model diagramModel.addLayer(new OverlayLayerModel()); diagramEngine.setModel(diagramModel); setDiagramModel(diagramModel); - + // Layout and focus handling setTimeout(() => { dagreEngine.redistribute(diagramEngine.getModel()); - + if (selectedNodeId) { const selectedModel = diagramEngine.getModel().getNode(selectedNodeId); focusToNode(selectedModel, diagramEngine.getModel().getZoomLevel(), diagramEngine); } else if (diagramEngine?.getCanvas()?.getBoundingClientRect) { diagramEngine.zoomToFitNodes({ margin: 10, maxZoom: 1 }); } - + // Cleanup and refresh diagramEngine.getModel().removeLayer( diagramEngine.getModel().getLayers().find(layer => layer instanceof OverlayLayerModel) @@ -133,7 +133,7 @@ export function TypeDiagram(props: TypeDiagramProps) { {diagramEngine?.getModel() && diagramModel ?
- + { const link = document.createElement('a'); - link.download = 'er-diagram.jpeg'; + link.download = 'type-diagram.jpeg'; link.href = dataUrl; link.click(); }) diff --git a/workspaces/ballerina/type-diagram/src/components/entity-relationship/EntityNode/EntityHead/EntityHead.tsx b/workspaces/ballerina/type-diagram/src/components/entity-relationship/EntityNode/EntityHead/EntityHead.tsx index dab70a6c754..8678eb24aee 100644 --- a/workspaces/ballerina/type-diagram/src/components/entity-relationship/EntityNode/EntityHead/EntityHead.tsx +++ b/workspaces/ballerina/type-diagram/src/components/entity-relationship/EntityNode/EntityHead/EntityHead.tsx @@ -24,7 +24,7 @@ import { EntityHead, EntityName } from '../styles'; import { CtrlClickGo2Source } from '../../../common/CtrlClickHandler/CtrlClickGo2Source'; import { DiagramContext } from '../../../common'; import styled from '@emotion/styled'; -import { Button, Item, Menu, MenuItem, Popover } from '@wso2/ui-toolkit'; +import { Button, Item, Menu, MenuItem, Popover, ThemeColors } from '@wso2/ui-toolkit'; import { MoreVertIcon } from '../../../../resources'; import { GraphQLIcon } from '../../../../resources/assets/icons/GraphqlIcon'; @@ -68,11 +68,25 @@ const HeaderWrapper = styled.div` width: 100%; `; +const ImportedLabel = styled.span` + background-color: ${ThemeColors.SURFACE_CONTAINER}; + border-radius: 3px; + color: ${ThemeColors.ON_SURFACE_VARIANT}; + font-family: GilmerRegular; + font-size: 10px; + height: 20px; + line-height: 20px; + padding: 0 6px; + margin-left: 8px; + white-space: nowrap; +`; + export function EntityHeadWidget(props: ServiceHeadProps) { const { engine, node, isSelected } = props; const { setFocusedNodeId, onEditNode, goToSource } = useContext(DiagramContext); const displayName: string = node.getID()?.slice(node.getID()?.lastIndexOf(':') + 1); + const isImported = !node?.entityObject?.editable; const [anchorEl, setAnchorEl] = useState(null); const isMenuOpen = Boolean(anchorEl); @@ -82,7 +96,7 @@ export function EntityHeadWidget(props: ServiceHeadProps) { }; const onNodeEdit = () => { - if (onEditNode) { + if (onEditNode && node?.entityObject?.editable) { if (node.isGraphqlRoot) { onEditNode(node.getID(), true); } else { @@ -103,13 +117,23 @@ export function EntityHeadWidget(props: ServiceHeadProps) { } const menuItems: Item[] = [ + ...(node?.entityObject?.editable ? [ + { + id: "edit", + label: "Edit", + onClick: () => onNodeEdit(), + }, + { + id: "goToSource", + label: "Source", + onClick: () => onGoToSource() + } + ] : []), { - id: "edit", - label: "Edit", - onClick: () => onNodeEdit(), - }, - { id: "goToSource", label: "Source", onClick: () => onGoToSource() }, - { id: "focusView", label: "Focused View", onClick: () => onFocusedView() } + id: "focusView", + label: "Focused View", + onClick: () => onFocusedView() + } ]; const isClickable = true; @@ -119,7 +143,7 @@ export function EntityHeadWidget(props: ServiceHeadProps) { )} {displayName} + {isImported && ( + + Imported Type + + )} {/* {selectedNodeId === node.getID() && ( @@ -155,7 +184,7 @@ export function EntityHeadWidget(props: ServiceHeadProps) { )} */} - + diff --git a/workspaces/ballerina/type-diagram/src/utils/model-mapper/entityModelMapper.ts b/workspaces/ballerina/type-diagram/src/utils/model-mapper/entityModelMapper.ts index 4f4dce09719..01ebf591024 100644 --- a/workspaces/ballerina/type-diagram/src/utils/model-mapper/entityModelMapper.ts +++ b/workspaces/ballerina/type-diagram/src/utils/model-mapper/entityModelMapper.ts @@ -61,8 +61,8 @@ function createEntityLinks(entityNodes: Map): EntityLinkMod let sourcePort = sourceNode.getPort(`right-${sourceNode.getID()}/${member.name}`); let targetPort = targetNode.getPort(`left-${ref}`); - const linkId = `entity-link-${sourceNode.getID()}-${ref}`; - let link = new EntityLinkModel(undefined, linkId); // REMOVE cardinalities + const linkTestID = `node-link-${sourceNode.getID()}/${member.name}-${ref}`; + let link = new EntityLinkModel(undefined, linkTestID); // REMOVE cardinalities entityLinks.push(createLinks(sourcePort, targetPort, link)); } }); diff --git a/workspaces/ballerina/type-editor/src/TypeEditor/ClassEditor.tsx b/workspaces/ballerina/type-editor/src/TypeEditor/ClassEditor.tsx index 0dce42f3f2a..4451e82e7c5 100644 --- a/workspaces/ballerina/type-editor/src/TypeEditor/ClassEditor.tsx +++ b/workspaces/ballerina/type-editor/src/TypeEditor/ClassEditor.tsx @@ -351,7 +351,7 @@ export function ClassEditor({ type, onChange, isGraphql, onValidationError }: Cl {isGraphql ? 'Object Fields' : 'Resource Methods'} -
+
diff --git a/workspaces/ballerina/type-editor/src/TypeEditor/EnumEditor.tsx b/workspaces/ballerina/type-editor/src/TypeEditor/EnumEditor.tsx index a60fa821b58..739ed4ad7be 100644 --- a/workspaces/ballerina/type-editor/src/TypeEditor/EnumEditor.tsx +++ b/workspaces/ballerina/type-editor/src/TypeEditor/EnumEditor.tsx @@ -214,7 +214,7 @@ export function EnumEditor({ type, onChange, onValidationError }: EnumEditorProp Members -
+
@@ -239,6 +239,7 @@ export function EnumEditor({ type, onChange, onValidationError }: EnumEditorProp
diff --git a/workspaces/ballerina/type-editor/src/TypeEditor/Tabs/TypeCreatorTab.tsx b/workspaces/ballerina/type-editor/src/TypeEditor/Tabs/TypeCreatorTab.tsx index 47dcfd736a3..1de72ef4fed 100644 --- a/workspaces/ballerina/type-editor/src/TypeEditor/Tabs/TypeCreatorTab.tsx +++ b/workspaces/ballerina/type-editor/src/TypeEditor/Tabs/TypeCreatorTab.tsx @@ -463,6 +463,7 @@ export function TypeCreatorTab(props: TypeCreatorTabProps) { {isNewType && ( ({ @@ -477,6 +478,7 @@ export function TypeCreatorTab(props: TypeCreatorTabProps) {
@@ -231,6 +231,7 @@ export function UnionEditor({ type, onChange, rpcClient, onValidationError }: Un /> + > + {`Import ${extension.toUpperCase()} File`} + + ); } diff --git a/workspaces/bi/bi-extension/CHANGELOG.md b/workspaces/bi/bi-extension/CHANGELOG.md index 6ef515a5e6a..13ebd3ff85c 100644 --- a/workspaces/bi/bi-extension/CHANGELOG.md +++ b/workspaces/bi/bi-extension/CHANGELOG.md @@ -1,5 +1,51 @@ # Change log +## **1.1.0** (2025-07-14) + +### Major Features + +- **Bundled Language Server**: Ballerina Language Server is now bundled with the extension, eliminating separate installation requirements and improving startup performance +- **Configurable Editor v2**: Complete redesign of the configuration editor with enhanced UI/UX and improved functionality +- **Type Editor Revamp**: A redesign of the type editor to improve feature discoverability and deliver a better user experience + +### Added + +- Enhanced AI file upload support with additional file types for improved analysis capabilities +- Documentation display in Signature Help for a better developer experience during code completion +- Enhanced service resource creation with comprehensive validation system for base paths, resource action calls, reserved keywords, and new UX for creating HTTP responses + +### Changed + +- **Integration Management**: Refactored artifacts management and navigation +- **UI Components**: + - Type Diagram and GraphQL designer with improved visual presentation +- **Developer Experience**: + - Enhanced renaming editor functionality + - Enhanced Form and Input Editor with Markdown support + - Updated imported types display as view-only nodes for clarity + +### Fixed + +- **Extension Stability**: + - Resolved extension startup and activation issues for reliable performance +- **Data Mapping & Visualization**: + - Fixed issues when working with complex data types from imported modules + - Improved visualization of array types and nested data structures + - Enhanced connection line display in design diagrams +- **Testing & Debugging**: + - Fixed GraphQL testing functionality for seamless API testing + - Improved service testing support across different Ballerina versions + - Enhanced test explorer compatibility with legacy projects +- **Configuration Management**: + - Resolved configuration file editing and creation issues + - Fixed form rendering problems that could cause UI freezing +- **Cross-Platform Support**: + - Enhanced Windows compatibility for Java development kit integration + - Improved file path handling across different operating systems +- **User Interface**: + - Fixed theme-related display issues in command interfaces + + ## **1.0.3** (2024-05-28) ### Fixes diff --git a/workspaces/bi/bi-extension/package.json b/workspaces/bi/bi-extension/package.json index 872b065ec37..5dea0196d3e 100644 --- a/workspaces/bi/bi-extension/package.json +++ b/workspaces/bi/bi-extension/package.json @@ -2,7 +2,7 @@ "name": "ballerina-integrator", "displayName": "WSO2 Integrator: BI", "description": "An extension which gives a development environment for designing, developing, debugging, and testing integration solutions.", - "version": "1.0.5", + "version": "1.1.1", "publisher": "wso2", "icon": "resources/images/wso2-ballerina-integrator-logo.png", "repository": { @@ -69,7 +69,7 @@ "menus": { "view/item/context": [ { - "command": "ballerina.show.visualizer", + "command": "ballerina.showVisualizer", "when": "view == BI.project-explorer && viewItem == bi-project", "group": "inline" }, @@ -113,6 +113,11 @@ "when": "view == BI.project-explorer && viewItem == configurations", "group": "inline" }, + { + "command": "BI.project-explorer.view-configuration", + "when": "view == BI.project-explorer && viewItem == configurations", + "group": "inline" + }, { "command": "BI.project-explorer.add-data-mapper", "when": "view == BI.project-explorer && viewItem == dataMappers", diff --git a/workspaces/bi/bi-extension/src/project-explorer/project-explorer-provider.ts b/workspaces/bi/bi-extension/src/project-explorer/project-explorer-provider.ts index 0e14b31893e..4eb396051c6 100644 --- a/workspaces/bi/bi-extension/src/project-explorer/project-explorer-provider.ts +++ b/workspaces/bi/bi-extension/src/project-explorer/project-explorer-provider.ts @@ -274,6 +274,17 @@ function getEntriesBI(components: ProjectStructureResponse): ProjectExplorerEntr } entries.push(dataMappers); + // ---------- Configurations ---------- + const configs = new ProjectExplorerEntry( + "Configurations", + vscode.TreeItemCollapsibleState.Expanded, + null, + 'config', + false + ); + configs.contextValue = "configurations"; + entries.push(configs); + // ---------- Natural Functions ---------- if (extension.isNPSupported) { const naturalFunctions = new ProjectExplorerEntry( diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/ConfigEditor.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/ConfigEditor.ts index 55cd67fd0b1..486864d03f7 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/ConfigEditor.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/ConfigEditor.ts @@ -55,6 +55,7 @@ export class ConfigEditor { console.log(`Verify config variable ${variableName}`); const configVariableItem = this.webView.locator(`div#${variableName}-variable`); await configVariableItem.waitFor({ state: 'visible', timeout: 30000 }); + await configVariableItem.click(); // Verify the variable name and default const variableExists = await configVariableItem.isVisible(); @@ -125,6 +126,7 @@ export class ConfigEditor { console.log(`Verify warning for config variable ${variableName}`); const configVariableItem = this.webView.locator(`div#${variableName}-variable`); await configVariableItem.waitFor({ state: 'visible', timeout: 30000 }); + await configVariableItem.click(); // Verify the Required warning text is visible const requiredWarningText = configVariableItem.locator('span', { hasText: 'Required' }); @@ -133,6 +135,29 @@ export class ConfigEditor { expect(requiredTextVisible, `Required warning text for variable "${variableName}" was not verified successfully.`).toBe(true); } + public async verifyNoWarning(variableName: string) { + console.log(`Verify no warning for config variable ${variableName}`); + const configVariableItem = this.webView.locator(`div#${variableName}-variable`); + await configVariableItem.waitFor({ state: 'visible', timeout: 30000 }); + await configVariableItem.click(); + + // Wait for the Required warning text to be hidden or detached + const requiredWarningText = configVariableItem.locator('span', { hasText: 'Required' }); + await requiredWarningText.waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {}); + const requiredTextVisible = await requiredWarningText.isVisible().catch(() => false); + expect(requiredTextVisible, `Required warning text for variable "${variableName}" was verified when it should not be.`).toBe(false); + } + + public async verifyNumberofWarningIntegration(number: number) { + console.log(`Verify number of warnings in package integration`); + // Get the warning count span inside the integration container + const warningCountSpan = this.webView.locator('span#integration-warning-count'); + await warningCountSpan.waitFor({ state: 'visible', timeout: 10000 }); + const warningCountText = await warningCountSpan.textContent(); + const warningCount = parseInt(warningCountText || '0', 10); + expect(warningCount, `Expected ${number} warnings, but found ${warningCount}.`).toBe(number); + } + public async getSelectedPackage(): Promise { const titleDiv = this.webView.locator('div#TitleDiv h2'); await titleDiv.waitFor({ state: 'visible', timeout: 30000 }); diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/ai-chat-service.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/ai-chat-service.spec.ts new file mode 100644 index 00000000000..4cb7e41adbc --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/ai-chat-service.spec.ts @@ -0,0 +1,72 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('AI Chat Agent Tests', { + tag: '@group1', + }, async () => { + initTest(); + test('Create AI Chat Agent', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new AI Chat Agent in test attempt: ', testAttempt); + // Creating a AI Chat Agent + await addArtifact('AI Chat Agent', 'ai-agent-card'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + const sampleName = `sample${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'NameName of the agent': { + type: 'input', + value: sampleName, + } + } + }); + await form.submit('Create'); + + // Check if the diagram canvas is visible + const diagramCanvas = artifactWebView.locator('#bi-diagram-canvas'); + await diagramCanvas.waitFor({ state: 'visible', timeout: 30000 }); + + const diagramTitle = artifactWebView.locator('h2', { hasText: 'AI Chat Agent' }); + await diagramTitle.waitFor(); + + // Check if the agent call node is visible + const agentCallNode = artifactWebView.locator('[data-testid="agent-call-node"]'); + await agentCallNode.waitFor(); + + // Check if the AI Chat Agent is created in the project explorer + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `AI Agent Services - /${sampleName}`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/graphql-service.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/graphql-service.spec.ts new file mode 100644 index 00000000000..7a16af4f38d --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/graphql-service.spec.ts @@ -0,0 +1,100 @@ +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('GraphQL Service Tests', { + tag: '@group1', + }, async () => { + initTest(); + test('Create GraphQL Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('GraphQL Service', 'graphql-service-card'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + const sampleName = `/sample${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Service Base Path*': { + type: 'input', + value: sampleName, + } + } + }); + await form.submit('Create'); + + // Check if the type diagram canvas is visible + const typeDiagram = artifactWebView.locator('[data-testid="type-diagram"]'); + await typeDiagram.waitFor(); + + // Check if the service name is visible + const context = artifactWebView.locator(`text=${sampleName}`).first(); + await context.waitFor(); + + // Check if the AI Chat Agent is created in the project explorer + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `GraphQL Service`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing GraphQL Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + const editBtn = artifactWebView.locator('[data-testid="edit-service-btn"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + const sampleName = `/editedSample${testAttempt}`; + await form.fill({ + values: { + 'Service Base Path*': { + type: 'input', + value: sampleName, + } + } + }); + await form.submit('Save'); + + // Check if the type diagram canvas is visible + const typeDiagram = artifactWebView.locator('[data-testid="type-diagram"]'); + await typeDiagram.waitFor({ state: 'visible', timeout: 30000 }); + + // Check if the service name is visible + const context = artifactWebView.locator(`text=${sampleName}`).first(); + await context.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/service/service.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/http-service.spec.ts similarity index 89% rename from workspaces/bi/bi-extension/src/test/e2e-playwright-tests/service/service.spec.ts rename to workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/http-service.spec.ts index 743152cf0e1..b9f1825f770 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/service/service.spec.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/http-service.spec.ts @@ -21,11 +21,11 @@ import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; import { ProjectExplorer } from '../ProjectExplorer'; export default function createTests() { - test.describe('API Tests', { + test.describe('HTTP Service Tests', { tag: '@group1', }, async () => { initTest(); - test('Create Service', async ({ }, testInfo) => { + test('Create HTTP Service', async ({ }, testInfo) => { const testAttempt = testInfo.retry + 1; console.log('Creating a new service in test attempt: ', testAttempt); // Creating a HTTP Service @@ -39,7 +39,7 @@ export default function createTests() { await form.switchToFormView(false, artifactWebView); await form.fill({ values: { - 'Service base path*': { + 'Service Base Path*': { type: 'input', value: sampleName, } @@ -56,22 +56,22 @@ export default function createTests() { } }); - test('Editing Service', async ({ }, testInfo) => { + test('Editing HTTP Service', async ({ }, testInfo) => { const testAttempt = testInfo.retry + 1; console.log('Editing a service in test attempt: ', testAttempt); const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); if (!artifactWebView) { throw new Error('WSO2 Integrator: BI webview not found'); } - const editBtn = artifactWebView.getByRole('button', { name: ' Edit' }); + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); await editBtn.waitFor(); await editBtn.click({ force: true }); const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); await form.switchToFormView(false, artifactWebView); - const sampleName = `/newSample${testAttempt}`; + const sampleName = `/editedSample${testAttempt}`; await form.fill({ values: { - 'Service base path*': { + 'Service Base Path*': { type: 'input', value: sampleName, } diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/tcp-service.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/tcp-service.spec.ts new file mode 100644 index 00000000000..81ce1722079 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/api-services/tcp-service.spec.ts @@ -0,0 +1,103 @@ +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('TCP Service Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create TCP Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('TCP Service', 'tcp-service-card'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + // Create a new listener + listenerName = `listenerTcp${testAttempt}`; + const listenerPort = `6060`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + }, + 'localPort': { + type: 'textarea', + value: listenerPort, + } + } + }); + await form.submit('Next'); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + // Create a new TCP Service + const configTitle = artifactWebView.locator('h3', { hasText: 'TCP Service Configuration' }); + await configTitle.waitFor(); + + await form.submit('Create'); + + const context = artifactWebView.locator(`text="onConnect"`); + await context.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `TCP Service`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing TCP Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Save'); + + const context = artifactWebView.locator(`text="onConnect"`); + await context.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/automation/automation.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/automation/automation.spec.ts index 4bd717d9b05..7975bb9f2f0 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/automation/automation.spec.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/automation/automation.spec.ts @@ -32,6 +32,12 @@ export default function createTests() { throw new Error('WSO2 Integrator: BI webview not found'); } await artifactWebView.getByRole('button', { name: 'Create' }).click(); + + const diagramCanvas = artifactWebView.locator('#bi-diagram-canvas'); + await diagramCanvas.waitFor({ state: 'visible', timeout: 30000 }); + + const diagramTitle = artifactWebView.locator('h2', { hasText: 'Automation' }); + await diagramTitle.waitFor(); }); }); } diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/components/Diagram.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/components/Diagram.ts new file mode 100644 index 00000000000..ae51fbed0f6 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/components/Diagram.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. + */ + +import { Frame, Locator, Page } from '@playwright/test'; +import { switchToIFrame } from '@wso2/playwright-vscode-tester'; + +export class Diagram { + private diagramWebView!: Frame; + + constructor(private _page: Page) {} + + public async init() { + const webview = await switchToIFrame('WSO2 Integrator: BI', this._page); + if (!webview) { + throw new Error('Failed to switch to Diagram View iframe'); + } + this.diagramWebView = webview; + } + + public getDigramWebView(): Frame { + return this.diagramWebView; + } + + /** + * Used when the plus button is not visible initially. This will hover the link and click on the plus button. + * @param index - Index of the link to hover and click on the plus button. This can be found via the data-testid of the link. It will have the format `diagram-link-${index}` + */ + public async clickHoverAddButtonByIndex(index: number): Promise { + const link = (await this.getDiagramContainer()).getByTestId(`diagram-link-${index}`); + await link.hover(); + const addButton = link.getByTestId(`link-add-button-${index}`); + await addButton.waitFor(); + await this._page.pause(); + await addButton.click(); + } + + /** + * Used when the plus button is visible. This will click on the plus button. + * @param index - Index of the plus button to click. This can be found via the data-testid of the plus button. It will have the format `empty-node-add-button-${index}` + */ + public async clickAddButtonByIndex(index: number): Promise { + const addButton = (await this.getDiagramContainer()).getByTestId(`empty-node-add-button-${index}`); + await addButton.click(); + } + + private async getDiagramContainer(): Promise { + const container = this.diagramWebView.getByTestId('bi-diagram-canvas'); + await container.waitFor(); + return container; + } +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/components/SidePanel.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/components/SidePanel.ts new file mode 100644 index 00000000000..428dff66214 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/components/SidePanel.ts @@ -0,0 +1,43 @@ +/** + * 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 { Frame, Locator, Page } from "@playwright/test"; + +export class SidePanel { + private sidePanel!: Locator; + + constructor(private _container: Frame, private _page: Page) {} + + public async init() { + this.sidePanel = this._container.getByTestId('side-panel'); + await this.sidePanel.waitFor(); + } + + public getLocator(): Locator { + return this.sidePanel; + } + + /** + * Click on a node in the side panel by title + * @param nodeTitle - Title of the node to click. This can be found via the title attribute of the node. + */ + public async clickNode(nodeTitle: string): Promise { + const nodeContainer = this.getLocator().getByTitle(nodeTitle); + await nodeContainer.click(); + } +} \ No newline at end of file diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/configuration/configuration.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/configuration/configuration.spec.ts index 5705d10f50a..c3c6d689d7f 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/configuration/configuration.spec.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/configuration/configuration.spec.ts @@ -54,7 +54,7 @@ export default function createTests() { await form.switchToFormView(false, configurationWebView); await form.fill({ values: { - 'Variable name*Name of the variable': { + 'Variable Name*Name of the variable': { type: 'input', value: 'time', }, @@ -68,17 +68,15 @@ export default function createTests() { } } }); - + const documentationField = await configurationWebView.locator('textarea[name="documentation"]'); await documentationField.fill('This is the description of the time config variable'); await configurationWebView.getByRole('button', { name: 'Save' }).click(); - await configEditor.verifyConfigurableVariable('time', '100', ''); + // Edit the configurable variable await configEditor.editConfigurableVariable('time'); - - // Fill the form fields const editForm = new Form(page.page, 'WSO2 Integrator: BI', configurationWebView); await editForm.switchToFormView(false, configurationWebView); await editForm.fill({ @@ -93,18 +91,17 @@ export default function createTests() { await configurationWebView.getByRole('button', { name: 'Save' }).click(); await configEditor.verifyConfigurableVariable('time', '200', ''); + // Add a config toml value to the configurable variable through inline editor await configEditor.addConfigTomlValue('time', '500'); await configEditor.verifyConfigurableVariable('time', '200', '500'); - // Create a new configurable variable with no default value + // Create a new configurable variable with no default value and verify warning await configEditor.addNewConfigurableVariable(); - - // Fill the form fields const addForm = new Form(page.page, 'WSO2 Integrator: BI', configurationWebView); await addForm.switchToFormView(false, configurationWebView); await addForm.fill({ values: { - 'Variable name*Name of the variable': { + 'Variable Name*Name of the variable': { type: 'input', value: 'place', }, @@ -114,17 +111,63 @@ export default function createTests() { } } }); - await configurationWebView.getByRole('button', { name: 'Save' }).click(); await configEditor.verifyConfigurableVariable('place', '', ''); await configEditor.verifyWarning('place'); + // Create a new configurable variable with no default value + await configEditor.addNewConfigurableVariable(); + const addNewForm = new Form(page.page, 'WSO2 Integrator: BI', configurationWebView); + await addNewForm.switchToFormView(false, configurationWebView); + await addNewForm.fill({ + values: { + 'Variable Name*Name of the variable': { + type: 'input', + value: 'destination', + }, + 'Variable Type': { + type: 'textarea', + value: 'string' + } + } + }); + + await configurationWebView.getByRole('button', { name: 'Save' }).click(); + await configEditor.verifyConfigurableVariable('destination', '', ''); + await configEditor.verifyWarning('destination'); + + // Verify 2 warnings in the integration package + await configEditor.verifyNumberofWarningIntegration(2); + + // Add value to library config variable and check if warning is removed + await configEditor.addConfigTomlValue('place', 'new-string-value'); + await configEditor.verifyConfigurableVariable('place', '', 'new-string-value'); + await configEditor.verifyNoWarning('place'); + + // Click run integration button and check for missing configurations popup + await page.page.locator('a[role="button"][aria-label="Run Integration"]').click(); + await page.page.getByText('Missing required configurations in Config.toml file', { exact: true }).waitFor(); + await page.page.getByRole('button', { name: 'Update Configurables' }).click(); + + // Delete the configurable variable await configEditor.deleteConfigVariable('place'); + // Add config value for missing configurable variables + await configEditor.addConfigTomlValue('destination', 'new-destination-value'); + await configEditor.verifyConfigurableVariable('destination', '', 'new-destination-value'); + + // Add value to library config variable await configEditor.selectPackage('ballerinax/wso2.controlplane'); await configEditor.addConfigTomlValue('dashboard', 'example-dashboard'); await configEditor.verifyConfigurableVariable('dashboard', '', 'example-dashboard'); + // Click run integration button and check for missing configurations popup + await page.page.locator('a[role="button"][aria-label="Run Integration"]').click(); + + // Verify vs code terminal is opened + const terminalPanel = page.page.locator('div.composite.panel#terminal'); + await terminalPanel.waitFor({ state: 'visible', timeout: 60000 }); + }); }); } diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/azure.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/azure.spec.ts new file mode 100644 index 00000000000..581a2acc081 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/azure.spec.ts @@ -0,0 +1,109 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Azure Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create Azure Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Azure Integration', 'trigger-asb'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerAzure${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + }, + 'connectionString': { + type: 'textarea', + value: '"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test"', + }, + 'entityConfig': { + type: 'textarea', + value: `{ queueName: "testQueue" }`, + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'Azure Service Bus Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Create'); + + const onMessage = artifactWebView.locator(`text="onMessage"`); + await onMessage.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `Azure Service Bus Event Handler`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Azure Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'Azure Service Bus Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Save'); + + const onMessage = artifactWebView.locator(`text="onMessage"`); + await onMessage.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/github.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/github.spec.ts new file mode 100644 index 00000000000..fe9ec768a02 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/github.spec.ts @@ -0,0 +1,113 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Github Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create Github Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Github Integration', 'trigger-trigger-github'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerGithub${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'GitHub Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.fill({ + values: { + 'Event Channel': { + type: 'dropdown', + value: 'IssuesService', + } + } + }); + + await form.submit('Create'); + + const onOpened = artifactWebView.locator(`text="onOpened"`); + await onOpened.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `github:IssuesService`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Github Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'GitHub Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + const selectedEventChannel = artifactWebView.locator(`[current-value="IssuesService"]`); + await selectedEventChannel.waitFor(); + + await form.submit('Save'); + + const onOpened = artifactWebView.locator(`text="onOpened"`); + await onOpened.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/kafka.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/kafka.spec.ts new file mode 100644 index 00000000000..f7e4efe11cb --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/kafka.spec.ts @@ -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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Kafka Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create Kafka Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Kafka Integration', 'trigger-kafka'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerTcp${testAttempt}`; + const bootstrapServers = `"localhost:9092"`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + }, + 'bootstrapServers': { + type: 'textarea', + value: bootstrapServers, + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'Kafka Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Create'); + + const onConsumerRecord = artifactWebView.locator(`text="onConsumerRecord"`); + await onConsumerRecord.waitFor(); + + const onError = artifactWebView.locator(`text="onError"`); + await onError.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `Kafka Event Handler`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Kafka Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'Kafka Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Save'); + + const onConsumerRecord = artifactWebView.locator(`text="onConsumerRecord"`); + await onConsumerRecord.waitFor(); + + const onError = artifactWebView.locator(`text="onError"`); + await onError.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/mqtt.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/mqtt.spec.ts new file mode 100644 index 00000000000..3be62b17187 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/mqtt.spec.ts @@ -0,0 +1,113 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('MQTT Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create MQTT Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('MQTT Integration', 'trigger-mqtt'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerMqtt${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + }, + 'serverUri': { + type: 'textarea', + value: `"tcp://localhost:1883"`, + }, + 'clientId': { + type: 'textarea', + value: `"clientId${testAttempt}"`, + }, + 'subscriptions': { + type: 'textarea', + value: `"testTopic"`, + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'MQTT Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Create'); + + const onMessage = artifactWebView.locator(`text="onMessage"`); + await onMessage.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `MQTT Event Handler`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing MQTT Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'MQTT Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Save'); + + const onMessage = artifactWebView.locator(`text="onMessage"`); + await onMessage.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/rabbitmq.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/rabbitmq.spec.ts new file mode 100644 index 00000000000..4159c7505b4 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/rabbitmq.spec.ts @@ -0,0 +1,133 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('RabbitMQ Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create RabbitMQ Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('RabbitMQ Integration', 'trigger-rabbitmq'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerRabbitmq${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + }, + 'host': { + type: 'textarea', + value: `"localhost"`, + }, + 'port': { + type: 'textarea', + value: '5676', + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'RabbitMQ Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.fill({ + values: { + 'Queue Name*The name of the queue': { + type: 'input', + value: '"testQueue"', + } + } + }); + + await form.submit('Create'); + + const onMessage = artifactWebView.locator(`text="onMessage"`); + await onMessage.waitFor(); + + const onError = artifactWebView.locator(`text="onError"`); + await onError.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `RabbitMQ Event Handler`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing RabbitMQ Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'RabbitMQ Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.fill({ + values: { + 'Queue Name*The name of the queue': { + type: 'input', + value: '"editedTestQueue"', + } + } + }); + + await form.submit('Save'); + + const onMessage = artifactWebView.locator(`text="onMessage"`); + await onMessage.waitFor(); + + const onError = artifactWebView.locator(`text="onError"`); + await onError.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/salesforce.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/salesforce.spec.ts new file mode 100644 index 00000000000..0b0f9b49eca --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/salesforce.spec.ts @@ -0,0 +1,123 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Salesforce Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create Salesforce Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Salesforce Integration', 'trigger-salesforce'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerSalesforce${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + }, + 'auth': { + type: 'textarea', + value: `{ username: "test", password: "test" }`, + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'Salesforce Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Create'); + + const onCreate = artifactWebView.locator(`text="onCreate"`); + await onCreate.waitFor(); + + const onUpdate = artifactWebView.locator(`text="onUpdate"`); + await onUpdate.waitFor(); + + const onDelete = artifactWebView.locator(`text="onDelete"`); + await onDelete.waitFor(); + + const onRestore = artifactWebView.locator(`text="onRestore"`); + await onRestore.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `Salesforce Event Handler`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Salesforce Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'Salesforce Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Save'); + + const onCreate = artifactWebView.locator(`text="onCreate"`); + await onCreate.waitFor(); + + const onUpdate = artifactWebView.locator(`text="onUpdate"`); + await onUpdate.waitFor(); + + const onDelete = artifactWebView.locator(`text="onDelete"`); + await onDelete.waitFor(); + + const onRestore = artifactWebView.locator(`text="onRestore"`); + await onRestore.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/twillio.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/twillio.spec.ts new file mode 100644 index 00000000000..1cf2ce709b6 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/event-integrations/twillio.spec.ts @@ -0,0 +1,149 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Twillio Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create Twillio Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Twillio Integration', 'trigger-trigger-twilio'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerTwillio${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'Twilio Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.fill({ + values: { + 'Event Channel': { + type: 'dropdown', + value: 'CallStatusService', + } + } + }); + + await form.submit('Create'); + + const onQueued = artifactWebView.locator(`text="onQueued"`); + await onQueued.waitFor(); + + const onRinging = artifactWebView.locator(`text="onRinging"`); + await onRinging.waitFor(); + + const onInProgress = artifactWebView.locator(`text="onInProgress"`); + await onInProgress.waitFor(); + + const onBusy = artifactWebView.locator(`text="onBusy"`); + await onBusy.waitFor(); + + const onFailed = artifactWebView.locator(`text="onFailed"`); + await onFailed.waitFor(); + + const onNoAnswer = artifactWebView.locator(`text="onNoAnswer"`); + await onNoAnswer.waitFor(); + + const onCanceled = artifactWebView.locator(`text="onCanceled"`); + await onCanceled.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `twilio:CallStatusService`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Twillio Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'Twilio Event Handler Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + const selectedEventChannel = artifactWebView.locator(`[current-value="CallStatusService"]`); + await selectedEventChannel.waitFor(); + + await form.submit('Save'); + + const onQueued = artifactWebView.locator(`text="onQueued"`); + await onQueued.waitFor(); + + const onRinging = artifactWebView.locator(`text="onRinging"`); + await onRinging.waitFor(); + + const onInProgress = artifactWebView.locator(`text="onInProgress"`); + await onInProgress.waitFor(); + + const onBusy = artifactWebView.locator(`text="onBusy"`); + await onBusy.waitFor(); + + const onFailed = artifactWebView.locator(`text="onFailed"`); + await onFailed.waitFor(); + + const onNoAnswer = artifactWebView.locator(`text="onNoAnswer"`); + await onNoAnswer.waitFor(); + + const onCanceled = artifactWebView.locator(`text="onCanceled"`); + await onCanceled.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/expression-editor/expression-editor.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/expression-editor/expression-editor.spec.ts new file mode 100644 index 00000000000..1f39c0e7004 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/expression-editor/expression-editor.spec.ts @@ -0,0 +1,58 @@ +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { Diagram } from '../components/Diagram'; +import { SidePanel } from '../components/SidePanel'; + +export default function createTests() { + test.describe('Expression Editor Tests', { + tag: '@group1', + }, async () => { + initTest(); + test('Retrieving suggestions', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Retrieving suggestions: ', testAttempt); + + // Create an automation + await addArtifact('Automation', 'automation'); + + /* Uncomment this code if the timeout issue persists */ + // // FIXME:Remove this once timeout issue is fixed + // await new Promise((resolve) => setTimeout(resolve, 3000)); + + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + await artifactWebView.getByRole('button', { name: 'Create' }).click(); + + // Add a node to the diagram + const diagram = new Diagram(page.page); + await diagram.init(); + await diagram.clickAddButtonByIndex(1); + + // Click on the node in the side panel + const sidePanel = new SidePanel(artifactWebView, page.page); + await sidePanel.init(); + await sidePanel.clickNode('Declare Variable'); + }); + }); +} \ No newline at end of file diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/file-integrations/directory.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/file-integrations/directory.spec.ts new file mode 100644 index 00000000000..e1eabaff7f1 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/file-integrations/directory.spec.ts @@ -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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Directory Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create Directory Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Directory Integration', 'trigger-file'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerDirectory${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + }, + 'path': { + type: 'textarea', + value: '"/tmp/wso2/bi/sample"', + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'Directory Service Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Create'); + + const onCreate = artifactWebView.locator(`text="onCreate"`); + await onCreate.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `Directory Service`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Directory Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'Directory Service Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Save'); + + const onCreate = artifactWebView.locator(`text="onCreate"`); + await onCreate.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/file-integrations/ftp.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/file-integrations/ftp.spec.ts new file mode 100644 index 00000000000..486d8dce359 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/file-integrations/ftp.spec.ts @@ -0,0 +1,101 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('FTP Integration Tests', { + tag: '@group1', + }, async () => { + let listenerName: string; + initTest(); + test('Create FTP Integration', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new service in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('FTP Integration', 'trigger-ftp'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + // Create a new listener + listenerName = `listenerFtp${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*The name of the listener': { + type: 'input', + value: listenerName, + } + } + }); + await form.submit('Next'); + + // Check for title + const configTitle = artifactWebView.locator('h3', { hasText: 'FTP Service Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Create'); + + const onFileChange = artifactWebView.locator(`text="onFileChange"`); + await onFileChange.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `FTP Service`], true); + + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing FTP Service', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + + const configTitle = artifactWebView.locator('h3', { hasText: 'FTP Service Configuration' }); + await configTitle.waitFor(); + + const selectedListener = artifactWebView.locator(`[current-value="${listenerName}"]`); + await selectedListener.waitFor(); + + await form.submit('Save'); + + const onFileChange = artifactWebView.locator(`text="onFileChange"`); + await onFileChange.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/connection.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/connection.spec.ts new file mode 100644 index 00000000000..045fc6e4ed8 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/connection.spec.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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Connection Artifact Tests', { + tag: '@group1', + }, async () => { + initTest(); + test('Create Connection Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new connection in test attempt: ', testAttempt); + // Creating a HTTP Connection + await addArtifact('HTTP Connection', 'connection'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + + const cardHttp = artifactWebView.locator('#connector-http'); + await cardHttp.waitFor(); + await cardHttp.click({ force: true }); + + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + const connectionName = `sample${testAttempt}`; + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Url': { + type: 'textarea', + value: '"https://foo.bar/baz"', + }, + 'Connection Name*Name of the connection': { + type: 'input', + value: connectionName, + } + } + }); + await page.page.waitForTimeout(1000); // Wait for the form button to be enabled + await form.submit('Create'); + + const connectionCard = artifactWebView.getByText(connectionName, { exact: true }).first(); + await connectionCard.waitFor(); + + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `${connectionName}`], true); + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + }); +} + + diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/data-mapper.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/data-mapper.spec.ts new file mode 100644 index 00000000000..11d5a4c319b --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/data-mapper.spec.ts @@ -0,0 +1,88 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Data Mapper Artifact Tests', { + tag: '@group1', + }, async () => { + let functionName = ''; + initTest(); + test('Create Data Mapper Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new data mapper in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Data Mapper Artifact', 'data-mapper'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + functionName = `sample${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Data Mapper Name*Name of the data mapper': { + type: 'input', + value: functionName, + } + } + }); + await form.submit('Create'); + const context = artifactWebView.locator(`text=${functionName}`); + await context.waitFor(); + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `${functionName}`], true); + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Data Mapper Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a data mapper in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + const editBtn = artifactWebView.locator('#bi-edit'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Return Type': { + type: 'textarea', + value: 'string', + } + } + }); + await form.submit('Save'); + const context = artifactWebView.locator(`text=${functionName}`); + await context.waitFor(); + const contextReturnType = artifactWebView.locator('span:has(i.fw-bi-return)', { hasText: 'string' }); + await contextReturnType.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/function.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/function.spec.ts new file mode 100644 index 00000000000..392b250c33c --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/function.spec.ts @@ -0,0 +1,88 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Function Artifact Tests', { + tag: '@group1', + }, async () => { + let functionName = ''; + initTest(); + test('Create Function Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new function in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Function Artifact', 'bi-function'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + functionName = `sample${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*Name of the function': { + type: 'input', + value: functionName, + } + } + }); + await form.submit('Create'); + const context = artifactWebView.locator(`text=${functionName}`); + await context.waitFor(); + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `${functionName}`], true); + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Function Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a function in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + const editBtn = artifactWebView.locator('#bi-edit'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Return Type': { + type: 'textarea', + value: 'string', + } + } + }); + await form.submit('Save'); + const context = artifactWebView.locator(`text=${functionName}`); + await context.waitFor(); + const contextReturnType = artifactWebView.locator('span:has(i.fw-bi-return)', { hasText: 'string' }); + await contextReturnType.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/np.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/np.spec.ts new file mode 100644 index 00000000000..c54318048da --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/np.spec.ts @@ -0,0 +1,88 @@ + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Natural Function Artifact Tests', { + tag: '@group1', + }, async () => { + let functionName = ''; + initTest(); + test('Create Natural Function Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new natural function in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('Natural Function Artifact', 'bi-ai-function'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + functionName = `sample${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Name*Name of the natural function': { + type: 'input', + value: functionName, + } + } + }); + await form.submit('Create'); + const context = artifactWebView.locator(`text=${functionName}`); + await context.waitFor(); + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `${functionName}`], true); + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Natural Function Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a natural function in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + const editBtn = artifactWebView.locator('#bi-edit'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Return Type': { + type: 'textarea', + value: 'string', + } + } + }); + await form.submit('Save'); + const context = artifactWebView.locator(`text=${functionName}`); + await context.waitFor(); + const contextReturnType = artifactWebView.locator('span:has(i.fw-bi-return)', { hasText: 'string' }); + await contextReturnType.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/type.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/type.spec.ts new file mode 100644 index 00000000000..7de82f3a02d --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/other-artifacts/type.spec.ts @@ -0,0 +1,87 @@ + + +/** + * 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 { test } from '@playwright/test'; +import { addArtifact, initTest, page } from '../utils'; +import { Form, switchToIFrame } from '@wso2/playwright-vscode-tester'; +import { ProjectExplorer } from '../ProjectExplorer'; + +export default function createTests() { + test.describe('Type Diagram Artifact Tests', { + tag: '@group1', + }, async () => { + initTest(); + test('Create Type Diagram Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating a new type diagram in test attempt: ', testAttempt); + // Creating a HTTP Service + await addArtifact('HTTP Service', 'http-service-card'); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + const sampleName = `/sample${testAttempt}`; + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + await form.fill({ + values: { + 'Service Base Path*': { + type: 'input', + value: sampleName, + } + } + }); + await form.submit('Create'); + const context = artifactWebView.locator(`text=${sampleName}`); + await context.waitFor(); + const projectExplorer = new ProjectExplorer(page.page); + await projectExplorer.findItem(['sample', `HTTP Service - ${sampleName}`], true); + const updateArtifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!updateArtifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + }); + + test('Editing Type Diagram Artifact', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Editing a service in test attempt: ', testAttempt); + const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + if (!artifactWebView) { + throw new Error('WSO2 Integrator: BI webview not found'); + } + const editBtn = artifactWebView.locator('vscode-button[title="Edit Service"]'); + await editBtn.waitFor(); + await editBtn.click({ force: true }); + const form = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await form.switchToFormView(false, artifactWebView); + const sampleName = `/editedSample${testAttempt}`; + await form.fill({ + values: { + 'Service Base Path*': { + type: 'input', + value: sampleName, + } + } + }); + await form.submit('Save'); + const context = artifactWebView.locator(`text=${sampleName}`); + await context.waitFor(); + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/test.list.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/test.list.ts index 1ca51a1f5fd..d4d64a2b899 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/test.list.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/test.list.ts @@ -22,9 +22,32 @@ const fs = require('fs'); const path = require('path'); const videosFolder = path.join(__dirname, '..', 'test-resources', 'videos'); -import service from './service/service.spec'; import automation from './automation/automation.spec'; + +import httpService from './api-services/http-service.spec'; +import aiChatService from './api-services/ai-chat-service.spec'; +import graphqlService from './api-services/graphql-service.spec'; +import tcpService from './api-services/tcp-service.spec'; + +import kafkaIntegration from './event-integrations/kafka.spec'; +import rabbitmqIntegration from './event-integrations/rabbitmq.spec'; +import mqttIntegration from './event-integrations/mqtt.spec'; +import azureIntegration from './event-integrations/azure.spec'; +import salesforceIntegration from './event-integrations/salesforce.spec'; +import twillioIntegration from './event-integrations/twillio.spec'; +import githubIntegration from './event-integrations/github.spec'; + +import ftpIntegration from './file-integrations/ftp.spec'; +import directoryIntegration from './file-integrations/directory.spec'; + +import functionArtifact from './other-artifacts/function.spec'; +import naturalFunctionArtifact from './other-artifacts/np.spec'; +import dataMapperArtifact from './other-artifacts/data-mapper.spec'; +import typeDiagramArtifact from './other-artifacts/type.spec'; +import connectionArtifact from './other-artifacts/connection.spec'; + import configuration from './configuration/configuration.spec'; +import typeTest from './type/type.spec'; test.describe.configure({ mode: 'default' }); @@ -35,9 +58,40 @@ test.beforeAll(async () => { console.log('>>> Starting test suite'); }); -test.describe(service); +// <----Automation Test----> test.describe(automation); + +// <----AI Chat Service Test----> +test.describe(aiChatService); + +// <----Integration as API Test----> +test.describe(httpService); +test.describe(graphqlService); +test.describe(tcpService); + +// <----Event Integration Test----> +test.describe(kafkaIntegration); +test.describe(rabbitmqIntegration); +test.describe(mqttIntegration); +test.describe(azureIntegration); +test.describe(salesforceIntegration); +test.describe(twillioIntegration); +test.describe(githubIntegration); + +// <----File Integration Test----> +test.describe(ftpIntegration); +test.describe(directoryIntegration); + +// <----Other Artifacts Test----> +test.describe(functionArtifact); +test.describe(naturalFunctionArtifact); +test.describe(dataMapperArtifact); // TODO: Fix this test +test.describe(typeDiagramArtifact); // TODO: Fix this test +test.describe(connectionArtifact); +test.describe(configuration); // TODO: Fix this test + test.describe(configuration); +test.describe(typeTest); test.afterAll(async () => { console.log(`>>> Finished test suite`); diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts new file mode 100644 index 00000000000..fef11ddb937 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts @@ -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 { Frame, Locator, Page } from '@playwright/test'; +import { Form } from '@wso2/playwright-vscode-tester'; + +/** + * Utility class for type editor test operations + */ +export class TypeEditorUtils { + constructor(private page: Page, private webView: Frame) { } + + /** + * Wait for element to be visible and interactable + */ + async waitForElement(locator: Locator, timeout: number = 60000): Promise { + await locator.waitFor({ state: 'visible', timeout }); + } + + /** + * Fill an identifier field (double-click and type) + */ + async fillIdentifierField(index: number = 0, value: string): Promise { + const field = this.webView.locator('[data-testid="identifier-field"]').nth(index); + await this.waitForElement(field); + await field.dblclick(); + await field.type(value); + } + + /** + * Fill a type field (double-click and type) + */ + async fillTypeField(index: number = 0, value: string): Promise { + const field = this.webView.locator('[data-testid="type-field"]').nth(index); + await this.waitForElement(field); + await field.dblclick(); + await field.type(value); + } + + /** + * Add a new enum member with the given name + */ + async addEnumMember(memberName: string): Promise { + const addButton = this.webView.locator('[data-testid="add-member-button"]'); + await addButton.click(); + + // Get the last identifier field (newly added) + const memberFields = this.webView.locator('[data-testid="identifier-field"]'); + const count = await memberFields.count(); + await this.fillIdentifierField(count - 1, memberName); + } + + /** + * Delete an enum member by index + */ + async deleteEnumMember(index: number): Promise { + const deleteButton = this.webView.locator(`[data-testid="delete-member-${index}"]`); + await this.waitForElement(deleteButton); + await deleteButton.click(); + } + + /** + * Add a new record field with name and type + */ + async addRecordField(fieldName: string, fieldType: string): Promise { + const addButton = this.webView.locator('[data-testid="add-field-button"]'); + await this.waitForElement(addButton); + await addButton.click(); + + // Fill the newly added field (last in the form) + const identifierFields = this.webView.locator('[data-testid="identifier-field"]'); + + const fieldCount = await identifierFields.count(); + const lastIndex = fieldCount - 1; + + await this.fillIdentifierField(lastIndex, fieldName); + await this.fillTypeField(lastIndex, fieldType); + } + + /** + * Add a function to service class + */ + async addFunction(functionName: string, returnType: string): Promise { + const addButton = this.webView.locator('[data-testid="function-add-button"]'); + await this.waitForElement(addButton); + await addButton.click(); + + // Fill the newly added function (last in the form) + const identifierFields = this.webView.locator('[data-testid="identifier-field"]'); + + const fieldCount = await identifierFields.count(); + const lastIndex = fieldCount - 1; + + await this.fillIdentifierField(lastIndex, functionName); + await this.fillTypeField(lastIndex, returnType); + } + + /** + * Create a type using the form with name and kind + */ + async createType(name: string, kind: 'Enum' | 'Union' | 'Record' | 'Service Class'): Promise { + const form = new Form(this.page, 'WSO2 Integrator: BI', this.webView); + await form.switchToFormView(false, this.webView); + + await form.fill({ + values: { + 'Name': { + type: 'input', + value: name, + }, + 'Kind': { + type: 'dropdown', + value: kind, + } + } + }); + + return form; + } + + /** + * Save form and wait for completion + */ + async saveAndWait(form: Form): Promise { + await form.submit('Save'); + await this.page.waitForTimeout(2000); + await this.page.waitForLoadState('domcontentloaded'); + } + + /** + * Click Add Type button + */ + async clickAddType(): Promise { + const addTypeButton = this.webView.getByRole('button', { name: 'Add Type' }); + await this.waitForElement(addTypeButton); + await addTypeButton.click(); + } + + /** + * Verify that a type node exists in the diagram + */ + async verifyTypeNodeExists(typeName: string): Promise { + const typeElement = this.webView.locator(`[data-testid="type-node-${typeName}"]`); + await this.waitForElement(typeElement); + } + + /** + * Verify that a link exists between two types + */ + async verifyTypeLink(fromType: string, field: string, toType: string): Promise { + const linkTestId = `node-link-${fromType}/${field}-${toType}`; + const linkElement = this.webView.locator(`[data-testid="${linkTestId}"]`); + await this.waitForElement(linkElement); + } + + /** + * Edit an existing type by clicking its menu + */ + async editType(typeName: string): Promise { + const menuButton = this.webView.locator(`[data-testid="type-node-${typeName}-menu"]`); + await this.waitForElement(menuButton); + await menuButton.click(); + + const editMenuItem = this.webView.getByText('Edit', { exact: true }); + await this.waitForElement(editMenuItem); + await editMenuItem.click(); + + // Wait for type editor to load + const typeEditorContent = this.webView.locator('[data-testid="type-editor-container"]'); + await this.waitForElement(typeEditorContent); + } + + /** + * Wait for type editor to be ready + */ + async waitForTypeEditor(): Promise { + await this.page.waitForTimeout(2000); + await this.page.waitForLoadState('domcontentloaded'); + + const typeEditorContent = this.webView.locator('[data-testid="type-editor-container"]'); + await this.waitForElement(typeEditorContent); + } + + /** + * Create an enum type with multiple members + */ + async createEnumType(enumName: string, members: string[]): Promise { + const form = await this.createType(enumName, 'Enum'); + + // Fill the first member (already exists) + if (members.length > 0) { + await this.fillIdentifierField(0, members[0]); + } + + // Add additional members + for (let i = 1; i < members.length; i++) { + await this.addEnumMember(members[i]); + } + + return form; + } + + /** + * Create a union type with specified types + */ + async createUnionType(unionName: string, types: string[]): Promise { + const form = await this.createType(unionName, 'Union'); + + // Fill union types + for (let i = 0; i < types.length; i++) { + await this.fillTypeField(i, types[i]); + } + + return form; + } + + /** + * Create a record type with specified fields + */ + async createRecordType(recordName: string, fields: Array<{ name: string, type: string }>): Promise { + const form = await this.createType(recordName, 'Record'); + + // Add fields + for (const field of fields) { + await this.addRecordField(field.name, field.type); + } + + return form; + } + + /** + * Create a service class with functions + */ + async createServiceClass(className: string, functions: Array<{ name: string, returnType: string }>): Promise { + const form = await this.createType(className, 'Service Class'); + + // Add functions + for (const func of functions) { + await this.addFunction(func.name, func.returnType); + } + + return form; + } +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal new file mode 100644 index 00000000000..d31bc73a0a1 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal @@ -0,0 +1,27 @@ + +enum Role1 { + Admin, + Sales, + Marketing +} + +type Id1 int|string; + +type Employee1 record {| + Id1 id; + Role1 role; +|}; + +service class Project1 { + function init() { + } + + resource function get employeeDetails() returns Employee1 { + do { + panic error("Unimplemented function"); + } on fail error err { + //handle error + panic error("Unhandled error"); + } + } +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts new file mode 100644 index 00000000000..0efcc63a7f2 --- /dev/null +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.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 { test } from '@playwright/test'; +import { addArtifact, initTest, page, getWebview, verifyGeneratedSource } from '../utils'; +import { Form } from '@wso2/playwright-vscode-tester'; +import { TypeEditorUtils } from './TypeEditorUtils'; +import path from 'path'; + +export default function createTests() { + test.describe('Type Editor Tests', { + tag: '@group1', + }, async () => { + initTest(); + + test('Create Types from Scratch', async ({ }, testInfo) => { + const testAttempt = testInfo.retry + 1; + console.log('Creating types from scratch in test attempt: ', testAttempt); + + // Navigate to type editor + await addArtifact('Type', 'type'); + + // Wait for page to be stable before accessing iframe + await page.page.waitForLoadState('networkidle'); + + // Get webview directly from utils + const artifactWebView = await getWebview('WSO2 Integrator: BI', page); + const typeUtils = new TypeEditorUtils(page.page, artifactWebView); + + // Wait for type editor to be ready + await typeUtils.waitForTypeEditor(); + + // ENUM: Role + const enumName = `Role${testAttempt}`; + + // Create enum with members, delete one, then save + const enumForm = await typeUtils.createEnumType(enumName, ['Admin', 'Engineer', 'Sales', 'Marketing']); + await typeUtils.deleteEnumMember(1); // Delete 'Engineer' + await typeUtils.saveAndWait(enumForm); + await typeUtils.verifyTypeNodeExists(enumName); + + // UNION: Id + await typeUtils.clickAddType(); + const unionName = `Id${testAttempt}`; + const unionForm = await typeUtils.createUnionType(unionName, ['int', 'string']); + await typeUtils.saveAndWait(unionForm); + await typeUtils.verifyTypeNodeExists(unionName); + + // RECORD: Employee (initially with just id field) + await typeUtils.clickAddType(); + const recordName = `Employee${testAttempt}`; + const recordForm = await typeUtils.createRecordType(recordName, [ + { name: 'id', type: unionName } + ]); + await typeUtils.saveAndWait(recordForm); + await typeUtils.verifyTypeNodeExists(recordName); + + // Verify link + await typeUtils.verifyTypeLink(recordName, 'id', unionName); + + // Edit Employee type to add role field + await typeUtils.editType(recordName); + await typeUtils.addRecordField('role', enumName); + const editForm = new Form(page.page, 'WSO2 Integrator: BI', artifactWebView); + await typeUtils.saveAndWait(editForm); + await typeUtils.verifyTypeLink(recordName, 'role', enumName); + + // Create Service Class: Project + await typeUtils.clickAddType(); + const serviceClassName = `Project${testAttempt}`; + const serviceForm = await typeUtils.createServiceClass(serviceClassName, [ + { name: 'employeeDetails', returnType: recordName } + ]); + await typeUtils.saveAndWait(serviceForm); + await typeUtils.verifyTypeNodeExists(serviceClassName); + await typeUtils.verifyTypeLink(serviceClassName, 'employeeDetails', recordName); + + // Verify the generated types.bal matches testOutput.bal + const expectedFilePath = path.join(__dirname, 'testOutput.bal'); + await verifyGeneratedSource('types.bal', expectedFilePath); + + }); + }); +} diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/utils.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/utils.ts index 96a61922c61..40593e4a954 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/utils.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/utils.ts @@ -62,7 +62,7 @@ export async function toggleNotifications(disable: boolean) { export async function setupBallerinaIntegrator() { await page.selectSidebarItem('WSO2 Integrator: BI'); - const webview = await switchToIFrame('WSO2 Integrator: BI', page.page); + const webview = await getWebview('WSO2 Integrator: BI', page); if (!webview) { throw new Error('WSO2 Integrator: BI webview not found'); } @@ -86,10 +86,51 @@ export async function setupBallerinaIntegrator() { } } +export async function getWebview(viewName: string, page: ExtendedPage) { + let webview; + let retryCount = 0; + const maxRetries = 3; + + while (retryCount < maxRetries) { + try { + await page.page.waitForLoadState('domcontentloaded'); + await page.page.waitForTimeout(1000); + + webview = await switchToIFrame(viewName, page.page); + if (webview) { + return webview; + } + // If webview is falsy, treat it as a failed attempt + console.log(`Attempt ${retryCount + 1} failed: switchToIFrame returned ${webview}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.includes('Frame was detached')) { + console.log(`Frame was detached, retrying (${retryCount + 1}/${maxRetries})`); + } else { + console.log(`Attempt ${retryCount + 1} failed to access iframe:`, message); + } + } + + // Always increment retry count after each attempt + retryCount++; + + // Only retry if we haven't reached max retries + if (retryCount < maxRetries) { + await page.page.waitForTimeout(2000); + try { + await page.selectSidebarItem(viewName); + } catch (sidebarError) { + console.log('Failed to reselect sidebar item:', sidebarError); + } + } + } + throw new Error(`Failed to access iframe for ${viewName} after ${maxRetries} attempts`); +} + export async function createProject(page: ExtendedPage, projectName?: string) { console.log('Creating new project'); await setupBallerinaIntegrator(); - const webview = await switchToIFrame('WSO2 Integrator: BI', page.page, 60000); + const webview = await getWebview('WSO2 Integrator: BI', page); if (!webview) { throw new Error('WSO2 Integrator: BI webview not found'); } @@ -108,7 +149,7 @@ export async function createProject(page: ExtendedPage, projectName?: string) { } }); await form.submit('Create Integration'); - const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + const artifactWebView = await getWebview('WSO2 Integrator: BI', page); if (!artifactWebView) { throw new Error('WSO2 Integrator: BI webview not found'); } @@ -148,7 +189,7 @@ export function initTest(newProject: boolean = false, skipProjectCreation: boole export async function addArtifact(artifactName: string, testId: string) { console.log(`Adding artifact: ${artifactName}`); - const artifactWebView = await switchToIFrame('WSO2 Integrator: BI', page.page); + const artifactWebView = await getWebview('WSO2 Integrator: BI', page); if (!artifactWebView) { throw new Error('WSO2 Integrator: BI webview not found'); } @@ -162,7 +203,7 @@ export async function addArtifact(artifactName: string, testId: string) { export async function enableICP() { console.log('Enabling ICP'); - const webview = await switchToIFrame('WSO2 Integrator: BI', page.page); + const webview = await getWebview('WSO2 Integrator: BI', page); if (!webview) { throw new Error('WSO2 Integrator: BI webview not found'); } @@ -172,3 +213,45 @@ export async function enableICP() { await icpToggle.click(); } } + +/** + * Normalize source code for comparison + */ +function normalizeSource(source: string): string { + return source + .replace(/\r\n/g, '\n') // Normalize line endings + .replace(/\t/g, ' ') // Convert tabs to spaces + .split('\n') + .map(line => line.trimEnd()) // Remove trailing whitespace + .filter(line => line.trim() !== '') // Remove empty lines + .join('\n') + .trim(); +} + +/** + * Compare a generated .bal file with an expected .bal file + * @param generatedFileName - Name of the generated file (e.g., 'types.bal') + * @param expectedFilePath - Path to the expected file (e.g., path to testOutput.bal) + */ +export async function verifyGeneratedSource(generatedFileName: string, expectedFilePath: string): Promise { + const { expect } = await import('@playwright/test'); + + // Generated file is in the project sample folder + const generatedFilePath = path.join(newProjectPath, 'sample', generatedFileName); + + if (!fs.existsSync(generatedFilePath)) { + throw new Error(`Generated file not found at: ${generatedFilePath}`); + } + + if (!fs.existsSync(expectedFilePath)) { + throw new Error(`Expected file not found at: ${expectedFilePath}`); + } + + const actualContent = fs.readFileSync(generatedFilePath, 'utf-8'); + const expectedContent = fs.readFileSync(expectedFilePath, 'utf-8'); + + const normalizedActual = normalizeSource(actualContent); + const normalizedExpected = normalizeSource(expectedContent); + + expect(normalizedActual).toBe(normalizedExpected); +} diff --git a/workspaces/common-libs/font-wso2-vscode/src/icons/bi-import.svg b/workspaces/common-libs/font-wso2-vscode/src/icons/bi-import.svg index 3f5ec81b668..ea552be6921 100644 --- a/workspaces/common-libs/font-wso2-vscode/src/icons/bi-import.svg +++ b/workspaces/common-libs/font-wso2-vscode/src/icons/bi-import.svg @@ -15,6 +15,4 @@ ~ specific language governing permissions and limitations ~ under the License. --> - - - + \ No newline at end of file diff --git a/workspaces/common-libs/ui-toolkit/src/components/Dropdown/Dropdown.tsx b/workspaces/common-libs/ui-toolkit/src/components/Dropdown/Dropdown.tsx index ebbde338363..9d9d1929008 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/Dropdown/Dropdown.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/Dropdown/Dropdown.tsx @@ -30,6 +30,7 @@ export interface OptionProps { id?: string; content?: string | ReactNode; value: any; + disabled?: boolean; } export interface DropdownProps extends ComponentProps<"select"> { @@ -122,7 +123,7 @@ export const Dropdown = React.forwardRef((prop )} {items?.map((item: OptionProps) => ( - + {item?.content || item.value} ))} diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/HelperPane/index.tsx b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/HelperPane/index.tsx index 698c96f05aa..071e6bce849 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/HelperPane/index.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/HelperPane/index.tsx @@ -65,11 +65,12 @@ export const Arrow = styled.div` ${(props: StyleBase) => props.sx} `; -const PanelViewContainer = styled.div` +const PanelViewContainer = styled.div<{ sx?: CSSProperties }>` height: 100%; display: flex; flex-direction: column; overflow-y: auto; + ${({ sx }: { sx?: CSSProperties }) => sx} `; const PanelTabContainer = styled.div<{ isActive: boolean }>` @@ -155,7 +156,7 @@ const IconButtonContainer = styled.div` & p, & i { - color: var(--vscode-button-background); + color: var(--vscode-notebook-focusedEditorBorder); } & p:hover, @@ -379,13 +380,13 @@ const Loader: React.FC = ({ columns, rows, sections }) => { ); } -const PanelView: React.FC = ({ children, id }) => { +const PanelView: React.FC = ({ children, id, sx }) => { const { activePanelIndex } = useHelperPanePanelContext(); return ( <> {activePanelIndex === id && ( - + {React.Children.toArray(children).length > 0 ? ( children ) : ( diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/types/helperPane.ts b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/types/helperPane.ts index 97d162e1a18..31e54ae14e7 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/types/helperPane.ts +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/types/helperPane.ts @@ -76,6 +76,7 @@ export type LoadingSectionProps = { export type PanelViewProps = PropsWithChildren<{ id: number; + sx?: CSSProperties; }>; export type PanelTabProps = { diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Form/ExpressionEditor.tsx b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Form/ExpressionEditor.tsx index 754e0c101c4..a386cc996d6 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Form/ExpressionEditor.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Form/ExpressionEditor.tsx @@ -90,6 +90,7 @@ export const ExpressionEditor = forwardRef { // Position polling to detect any position changes - let lastPosition = { top: 0, left: 0, width: 0, height: 0 }; + let lastPosition = { top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0 }; let animationFrameId: number; const checkPositionChange = () => { @@ -163,7 +164,9 @@ export const ExpressionEditor = forwardRef - {getHelperPane(value, handleChange, helperPaneHeight)} + {getHelperPane(value, handleChange, helperPaneHeight, height)} {arrowPosition && ( )} diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Token/index.tsx b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Token/index.tsx index 55ce31ae630..8408a40458a 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Token/index.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Token/index.tsx @@ -211,7 +211,9 @@ export const TokenEditor = ({ onFocus, onBlur, getExpressionEditorIcon, - editorSx + editorSx, + height, + enableFullscreen = false }: TokenEditorProps) => { const [isFocused, setIsFocused] = useState(false); const containerRef = useRef(null); @@ -226,15 +228,22 @@ export const TokenEditor = ({ const [helperPanePosition, setHelperPanePosition] = useState({ top: 0, left: 0 }); const [helperPaneArrowPosition, setHelperPaneArrowPosition] = useState({ top: 0, left: 0 }); const [calculatedHelperPaneOrigin, setCalculatedHelperPaneOrigin] = useState('auto'); + const [isFullscreen, setIsFullscreen] = useState(false); const monacoEditorRef = useRef(); + let helperPaneStyle: StyleBase; + if (height) { + helperPaneStyle = { sx: { height: `${height}px`, overflowY: 'auto' } }; + } const updatePosition = throttle(() => { if (containerRef.current) { const calculatedHelperPaneOrigin = getHelperPaneWithEditorOrigin(containerRef, helperPaneOrigin); setCalculatedHelperPaneOrigin(calculatedHelperPaneOrigin); - setHelperPanePosition(getHelperPaneWithEditorPosition(containerRef, calculatedHelperPaneOrigin)); + const computedHelperPanePosition = getHelperPaneWithEditorPosition(containerRef, calculatedHelperPaneOrigin); + setHelperPanePosition(computedHelperPanePosition); + const newHelperPanePosition = isFullscreen ? { top: 0, left: computedHelperPanePosition.left } : computedHelperPanePosition; setHelperPaneArrowPosition( - getHelperPaneWithEditorArrowPosition(containerRef, calculatedHelperPaneOrigin, helperPanePosition) + getHelperPaneWithEditorArrowPosition(containerRef, calculatedHelperPaneOrigin, newHelperPanePosition) ); } }, 10); @@ -256,7 +265,7 @@ export const TokenEditor = ({ resizeObserver.disconnect(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isHelperPaneOpen]); + }, [isHelperPaneOpen, isFullscreen]); const updateNodeInfo = () => { const selection = window.getSelection(); @@ -541,9 +550,20 @@ export const TokenEditor = ({ } } + const handleFullscreenToggle = () => { + setIsFullscreen(!isFullscreen); + }; + + const fullScreenStyle: StyleBase = isFullscreen ? { + sx: { + top: 0, + height: '100vh' + } + } : {}; + const getHelperPaneWithEditorComponent = (): JSX.Element => { return createPortal( - + {/* Title and close button */} Expression Editor - +
+ { enableFullscreen && ( + + )} + +
@@ -591,25 +622,27 @@ export const TokenEditor = ({ - {/* Helper pane content */} - {getHelperPane(handleHelperPaneChange, handleAddFunction)} - - {/* Action buttons for the helper pane */} - - - - +
+ {/* Helper pane content */} + {getHelperPane(handleHelperPaneChange, handleAddFunction, 400, isFullscreen)} + + {/* Action buttons for the helper pane */} + + + + +
{/* Side arrow of the helper pane */} {helperPaneArrowPosition && ( diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/constants/token.ts b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/constants/token.ts index 716210bbfe4..9f155d7a650 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/constants/token.ts +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/constants/token.ts @@ -18,3 +18,4 @@ export const HELPER_PANE_WITH_EDITOR_WIDTH = 400; export const HELPER_PANE_WITH_EDITOR_HEIGHT = 580; +export const HELPER_PANE_EX_BTN_OFFSET = 20; diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/common.ts b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/common.ts index e7e5555addc..d4bf06fa9a5 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/common.ts +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/common.ts @@ -191,7 +191,7 @@ export type ExpressionEditorRef = { /* <------ Types related to the helper pane ------> */ -export type HelperPaneOrigin = 'bottom' | 'left' | 'right' | 'auto'; +export type HelperPaneOrigin = 'bottom' | 'top' | 'left' | 'right' | 'auto'; export type HelperPaneHeight = 'full' | '3/4' | 'default'; diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/form.ts b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/form.ts index 1d7d9fc5824..f28c85b2dd4 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/form.ts +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/form.ts @@ -30,13 +30,16 @@ type HelperPaneConditionalProps = { helperPaneWidth?: number; // - Helper pane styles helperPaneSx?: CSSProperties; + // - Height of the helper pane in pixels, used when helperPaneHeight is 'default' + height?: number; // - Callback function to open/close the helper pane changeHelperPaneState: (isOpen: boolean) => void; - // - Get the helper panel component + // - Get the helper pane component getHelperPane: ( value: string, onChange: (value: string, updatedCursorPosition: number) => void, - helperPaneHeight: HelperPaneHeight + helperPaneHeight: HelperPaneHeight, + height?: number, ) => ReactNode; // - Get a custom icon for the expression editor getExpressionEditorIcon?: () => ReactNode; @@ -46,6 +49,7 @@ type HelperPaneConditionalProps = { helperPaneHeight?: never; helperPaneWidth?: never; helperPaneSx?: never; + height?: never; changeHelperPaneState?: never; getHelperPane?: never; getExpressionEditorIcon?: never; diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/token.ts b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/token.ts index d2f7ff4169b..01d38cc0551 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/token.ts +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/types/token.ts @@ -28,16 +28,18 @@ type TokenEditorBaseProps = { actionButtons?: ActionButtonType[]; startAdornment?: ReactNode; endAdornment?: ReactNode; + enableFullscreen?: boolean; onChange: (value: string) => void; onFocus?: () => void; onBlur?: () => void; getExpressionEditorIcon?: () => ReactNode; + height?: number; editorSx?: CSSProperties; }; type HelperPaneConditionalProps = | { - getHelperPane: (onChange: (value: string) => void, addFunction: (signature: string) => void) => JSX.Element; + getHelperPane: (onChange: (value: string) => void, addFunction: (signature: string) => void, height?: number, isFullscreen?: boolean) => JSX.Element; helperPaneOrigin?: HelperPaneOrigin; changeHelperPaneState: (state: boolean) => void; isHelperPaneOpen: boolean; diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/utils/form.tsx b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/utils/form.tsx index fb5937d1d2e..9ce9ea323ae 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/utils/form.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/utils/form.tsx @@ -98,6 +98,9 @@ export const getHelperPanePosition = ( if (window.innerHeight - rect.top < HELPER_PANE_HEIGHT / 2) { position.top = window.innerHeight - HELPER_PANE_HEIGHT; } + if (window.innerHeight < HELPER_PANE_HEIGHT) { + position.top = 0; + } return position; }; diff --git a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/utils/token.ts b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/utils/token.ts index 58d5bae222e..b56a50e4ff6 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/utils/token.ts +++ b/workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/utils/token.ts @@ -22,7 +22,8 @@ import { HELPER_PANE_WITH_EDITOR_HEIGHT, HELPER_PANE_WITH_EDITOR_WIDTH, ARROW_HEIGHT, - ARROW_OFFSET + ARROW_OFFSET, + HELPER_PANE_EX_BTN_OFFSET } from '../constants'; import { HelperPaneOrigin, HelperPanePosition } from '../types'; @@ -105,6 +106,13 @@ export const getHelperPaneWithEditorOrigin = ( return 'left'; } else if (window.innerWidth - (rect.left + rect.width) > HELPER_PANE_WITH_EDITOR_WIDTH + ARROW_HEIGHT) { return 'right'; + } else if (rect.top > window.innerHeight / 2) { + // Checks if there is enough space in the top to display the helper pane + const expTop = rect.top - HELPER_PANE_WITH_EDITOR_HEIGHT - HELPER_PANE_EX_BTN_OFFSET; + if (expTop < 0) { + return 'bottom'; + } + return 'top'; } return 'bottom'; @@ -119,6 +127,9 @@ export const getHelperPaneWithEditorPosition = ( if (helperPaneOrigin === 'bottom') { return { top: rect.top + rect.height, left: rect.left }; } + if (helperPaneOrigin === 'top') { + return { top: (rect.top - HELPER_PANE_WITH_EDITOR_HEIGHT - HELPER_PANE_EX_BTN_OFFSET), left: rect.left }; + } const position: HelperPanePosition = { top: 0, left: 0 }; /* In the best case scenario, the helper pane should be poping up on the right of left side @@ -138,6 +149,9 @@ export const getHelperPaneWithEditorPosition = ( if (window.innerHeight - rect.top < HELPER_PANE_WITH_EDITOR_HEIGHT / 2) { position.top = window.innerHeight - HELPER_PANE_WITH_EDITOR_HEIGHT; } + if (window.innerHeight < HELPER_PANE_WITH_EDITOR_HEIGHT) { + position.top = 0; + } return position; }; diff --git a/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx b/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx index 2aa07673d01..2670cb2428d 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx @@ -127,11 +127,12 @@ export const SidePanel: React.FC = (props: SidePanelProps) => { {visible && ( <> { overlay && isOpen && } - + {children} {subPanel && ( Promise; getSupportedMIVersionsHigherThan: (param:string) => Promise; getProjectDetails: () => Promise; + updateProperties: (params: UpdatePropertiesRequest) => Promise; + reloadDependencies: () => Promise; updateDependencies: (params: UpdateDependenciesRequest) => Promise; updatePomValues: (params: UpdatePomValuesRequest) => Promise; updateConfigFileValues: (params: UpdateConfigValuesRequest) => Promise; diff --git a/workspaces/mi/mi-core/src/rpc-types/mi-visualizer/rpc-type.ts b/workspaces/mi/mi-core/src/rpc-types/mi-visualizer/rpc-type.ts index db9cd0e07dd..0daec95ed07 100644 --- a/workspaces/mi/mi-core/src/rpc-types/mi-visualizer/rpc-type.ts +++ b/workspaces/mi/mi-core/src/rpc-types/mi-visualizer/rpc-type.ts @@ -42,6 +42,7 @@ import { ReadmeContentResponse, AddConfigurableRequest, ProjectDetailsResponse, + UpdatePropertiesRequest, UpdateDependenciesRequest, UpdatePomValuesRequest, UpdateConfigValuesRequest, @@ -90,6 +91,8 @@ export const downloadJavaFromMI: RequestType = { method: `${_pre export const downloadMI: RequestType = { method: `${_preFix}/downloadMI` }; export const getSupportedMIVersionsHigherThan: RequestType = { method: `${_preFix}/getSupportedMIVersionsHigherThan` }; export const getProjectDetails: RequestType = { method: `${_preFix}/getProjectDetails` }; +export const updateProperties: RequestType = { method: `${_preFix}/updateProperties` }; +export const reloadDependencies: RequestType = { method: `${_preFix}/reloadDependencies` }; export const updateDependencies: RequestType = { method: `${_preFix}/updateDependencies` }; export const updatePomValues: RequestType = { method: `${_preFix}/updatePomValues` }; export const updateConfigFileValues: RequestType = { method: `${_preFix}/updateConfigFileValues` }; diff --git a/workspaces/mi/mi-core/src/rpc-types/mi-visualizer/types.ts b/workspaces/mi/mi-core/src/rpc-types/mi-visualizer/types.ts index cdcb79fdae7..ec3d1373890 100644 --- a/workspaces/mi/mi-core/src/rpc-types/mi-visualizer/types.ts +++ b/workspaces/mi/mi-core/src/rpc-types/mi-visualizer/types.ts @@ -106,6 +106,7 @@ export interface AdvancedProjectDetails { export interface PomNodeDetails { value: string; + type?: string; key?: string; displayValue?: string; range?: STRange | STRange[]; @@ -121,6 +122,7 @@ export interface PrimaryDetails { export interface BuildDetails { dockerDetails: DockerDetails; + enableFatCar: PomNodeDetails; advanceDetails: AdvanceDetails; } @@ -148,9 +150,16 @@ export interface PluginDetatils { export interface DependenciesDetails { connectorDependencies: DependencyDetails[]; + integrationProjectDependencies: DependencyDetails[]; otherDependencies: DependencyDetails[]; } +export interface PropertyDetails { + name: string; + value: string; + range?: STRange; +} + export interface DependencyDetails { groupId: string; artifact: string; @@ -177,6 +186,9 @@ export interface UpdateConfigValuesRequest { configValues: PomNodeDetails[]; } +export interface UpdatePropertiesRequest { + properties: PropertyDetails[]; +} export interface UpdateDependenciesRequest { dependencies: DependencyDetails[]; } @@ -185,6 +197,10 @@ export interface UpdateConfigValuesResponse { textEdits: TextEdit[]; } +export interface UpdatePropertiesResponse { + textEdits: TextEdit[]; +} + export interface UpdateDependenciesResponse { textEdits: TextEdit[]; } diff --git a/workspaces/mi/mi-diagram/package.json b/workspaces/mi/mi-diagram/package.json index a1467c9e314..26ee2de9cef 100644 --- a/workspaces/mi/mi-diagram/package.json +++ b/workspaces/mi/mi-diagram/package.json @@ -53,7 +53,7 @@ "react-json-view": "latest", "react-json-view-lite": "latest", "react-markdown": "~10.1.0", - "html-to-image": "1.11.13" + "html-to-image": "1.11.11" }, "devDependencies": { "@storybook/addon-essentials": "^8.6.14", diff --git a/workspaces/mi/mi-diagram/src/components/Form/FormExpressionField/index.tsx b/workspaces/mi/mi-diagram/src/components/Form/FormExpressionField/index.tsx index 8ed516bcd22..2b381f00815 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/FormExpressionField/index.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/FormExpressionField/index.tsx @@ -254,7 +254,6 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => { const cursorPosition = expressionRef.current?.shadowRoot?.querySelector('textarea')?.selectionStart; const updatedValue = currentValue.slice(0, cursorPosition) + value + currentValue.slice(cursorPosition); const updatedCursorPosition = cursorPosition + value.length; - // Update the value in the expression editor onChange(updatedValue, updatedCursorPosition); // Focus the expression editor @@ -274,7 +273,11 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => { position, 'default', () => handleChangeHelperPaneState(false), - handleHelperPaneChange + handleHelperPaneChange, + undefined, + undefined, + 380, + false ); }, [expressionRef.current, handleChangeHelperPaneState, nodeRange, getHelperPane]); @@ -391,7 +394,7 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => {
{!isAIFill && -
+
void, addFunction: (value: string) => void) => { + const handleGetHelperPane = useCallback((onChange: (value: string) => void, addFunction: (value: string) => void, height?: number, isFullscreen?: boolean) => { const position = nodeRange ? nodeRange?.start == nodeRange?.end ? nodeRange.start : { line: nodeRange.start.line, character: nodeRange.start.character + 1 } : undefined; - return getHelperPane( position, 'default', () => handleChangeHelperPaneState(false), onChange, addFunction, - { width: 'auto', border: '1px solid var(--dropdown-border)' } + { width: 'auto', border: '1px solid var(--dropdown-border)' }, + height, + true, + isFullscreen ); }, [nodeRange, handleChangeHelperPaneState, getHelperPane]); @@ -140,6 +142,7 @@ export const FormTokenEditor = ({ actionButtons={actionButtons} getHelperPane={handleGetHelperPane} isHelperPaneOpen={isHelperPaneOpen} + enableFullscreen changeHelperPaneState={setIsHelperPaneOpen} getExpressionEditorIcon={getExpressionEditorIcon} startAdornment={ diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx index dea73526b32..50f4935595e 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx @@ -29,6 +29,7 @@ type PanelPageProps = { type CategoryPageProps = { position: Position; + isHelperPaneHeightOverflow?: boolean; setCurrentPage: (page: Page) => void; onClose: () => void; onChange: (value: string) => void; @@ -50,6 +51,7 @@ const DataPanel = ({ setCurrentPage }: PanelPageProps) => { export const CategoryPage = ({ position, + isHelperPaneHeightOverflow = false, setCurrentPage, onChange, addFunction @@ -65,10 +67,10 @@ export const CategoryPage = ({ - + - + diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ConfigsPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ConfigsPage.tsx index b093f64c6f0..a4dce993cf9 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ConfigsPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ConfigsPage.tsx @@ -52,18 +52,20 @@ const ButtonPanel = styled.div` type ConfigsPageProps = { position: Position; + hideSearch?: boolean; onChange: (value: string) => void; }; /* Validation schema for the config form */ const schema = yup.object({ configName: yup.string().required('Config Name is required'), - configType: yup.string().oneOf(['string', 'cert'] as const).required('Config Type is required') + configType: yup.string().oneOf(['string', 'cert'] as const).required('Config Type is required'), + configValue: yup.string().required('Config Value is required').required('Config Value is required') }); type ConfigFormData = yup.InferType; -export const ConfigsPage = ({ position, onChange }: ConfigsPageProps) => { +export const ConfigsPage = ({ position, onChange, hideSearch }: ConfigsPageProps) => { const { rpcClient } = useVisualizerContext(); const firstRender = useRef(true); const [isLoading, setIsLoading] = useState(false); @@ -112,7 +114,8 @@ export const ConfigsPage = ({ position, onChange }: ConfigsPageProps) => { // Handle form submission rpcClient.getMiDiagramRpcClient().saveConfig({ configName: data.configName, - configType: data.configType + configType: data.configType, + configValue: data.configValue }).then(({ success }) => { if (success) { // Retrieve the updated config info @@ -147,10 +150,12 @@ export const ConfigsPage = ({ position, onChange }: ConfigsPageProps) => { <> {!isFormOpen ? ( <> - + { !hideSearch && ( + + )} {filteredConfigInfo?.map((config) => ( getHelperPaneCompletionItem(config, onChange, getCompletionItemIcon) @@ -165,12 +170,6 @@ export const ConfigsPage = ({ position, onChange }: ConfigsPageProps) => { - { { id: '2', content: 'cert', value: 'cert' } ]} /> +