diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000000..1364c37fdb --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,12 @@ +# CodeRabbit configuration - https://docs.coderabbit.ai/getting-started/yaml-configuration +# Repo config overrides org-level settings. Explicitly list all branches to review +# so main stays covered while adding feature/*, release/* and hotfix/* +language: "en-US" +reviews: + auto_review: + enabled: true + base_branches: + - "main" + - "feature/.*" + - "release/.*" + - "hotfix/.*" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d20386f825..53223dfc7c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,10 @@ rush.json @hevayo @gigara @kanushka /common/ @hevayo @gigara @kanushka /workspaces/common-libs/ @hevayo @gigara @tharindulak /workspaces/mi/ @hevayo @gigara @kaumini +workspaces/ballerina/bi-diagram/src/test/__snapshots__ @hevayo @kanushka +workspaces/ballerina/component-diagram/src/test/__snapshots__ @hevayo @kanushka +workspaces/ballerina/sequence-diagram/src/test/__snapshots__ @hevayo @kanushka +workspaces/ballerina/type-diagram/src/test/__snapshots__ @hevayo @kanushka /workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts @hevayo @axewilledge @kanushka /workspaces/ballerina/ballerina-rpc-client @hevayo @axewilledge @kanushka /workspaces/choreo/ @kaje94 diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index b8ff8c6078..a1a71801be 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -55,7 +55,13 @@ inputs: BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID: type: string BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL: - type: string + type: string + COPILOT_ROOT_URL: + type: string + COPILOT_DEV_ROOT_URL: + type: string + APPINSIGHTS_INSTRUMENTATION_KEY: + type: string MI_AUTH_ORG: type: string MI_AUTH_CLIENT_ID: @@ -169,6 +175,9 @@ runs: BALLERINA_DEV_COPLIOT_AUTH_ORG: ${{ inputs.BALLERINA_DEV_COPLIOT_AUTH_ORG }} BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID: ${{ inputs.BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID }} BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL: ${{ inputs.BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL }} + COPILOT_ROOT_URL: ${{ inputs.COPILOT_ROOT_URL }} + COPILOT_DEV_ROOT_URL: ${{ inputs.COPILOT_DEV_ROOT_URL }} + APPINSIGHTS_INSTRUMENTATION_KEY: ${{ inputs.APPINSIGHTS_INSTRUMENTATION_KEY }} MI_AUTH_ORG: ${{ inputs.MI_AUTH_ORG }} MI_AUTH_CLIENT_ID: ${{ inputs.MI_AUTH_CLIENT_ID }} PLATFORM_DEFAULT_GHAPP_CLIENT_ID: ${{ inputs.PLATFORM_DEFAULT_GHAPP_CLIENT_ID }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce5c20ca89..f61ce21bdd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -209,6 +209,9 @@ jobs: BALLERINA_DEV_COPLIOT_AUTH_ORG: ${{ secrets.BALLERINA_DEV_COPLIOT_AUTH_ORG }} BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID: ${{ secrets.BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID }} BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL: ${{ secrets.BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL }} + COPILOT_ROOT_URL: ${{ secrets.COPILOT_ROOT_URL }} + COPILOT_DEV_ROOT_URL: ${{ secrets.COPILOT_DEV_ROOT_URL }} + APPINSIGHTS_INSTRUMENTATION_KEY: ${{ secrets.APPINSIGHTS_INSTRUMENTATION_KEY }} MI_AUTH_ORG: ${{ secrets.MI_AUTH_ORG }} MI_AUTH_CLIENT_ID: ${{ secrets.MI_AUTH_CLIENT_ID }} PLATFORM_DEFAULT_GHAPP_CLIENT_ID: ${{ secrets.PLATFORM_DEFAULT_GHAPP_CLIENT_ID }} @@ -278,6 +281,51 @@ jobs: balVersion: ${{ steps.set-version.outputs.balVersion }} balHome: ${{ steps.set-version.outputs.balHome }} + ExtTest_Ballerina_Diagrams: + name: Run Ballerina diagram snapshot tests + needs: Build_Stage + if: ${{ inputs.runTests || (inputs.isReleaseBuild && (inputs.ballerina || inputs.bi)) || needs.Build_Stage.outputs.runBalExtTests == 'true' || github.base_ref == 'release-ballerina' || github.base_ref == 'release-bi' }} + timeout-minutes: 30 + runs-on: ${{ inputs.runOnAWS && inputs.awsRunnerId || 'ubuntu-latest' }} + steps: + - name: Restore build + uses: actions/download-artifact@v4 + with: + name: ExtBuild + path: ./ + + - name: Set up workspace + run: | + unzip build.zip + rm build.zip + + - name: Setup Rush + uses: gigara/setup-rush@v1.2.0 + with: + pnpm: 10.10.0 + node: 22.x + rush-install: true + + - name: Run BI diagram snapshot tests + run: | + cd workspaces/ballerina/bi-diagram + xvfb-run --auto-servernum pnpm run test + + - name: Run component diagram snapshot tests + run: | + cd workspaces/ballerina/component-diagram + xvfb-run --auto-servernum pnpm run test + + - name: Run type diagram snapshot tests + run: | + cd workspaces/ballerina/type-diagram + xvfb-run --auto-servernum pnpm run test + + - name: Run sequence diagram snapshot tests + run: | + cd workspaces/ballerina/sequence-diagram + xvfb-run --auto-servernum pnpm run test + ExtTest_MI: name: Run MI diagram tests needs: Build_Stage diff --git a/.github/workflows/daily-build.yml b/.github/workflows/daily-build.yml index 9073f8f800..faffee8c55 100644 --- a/.github/workflows/daily-build.yml +++ b/.github/workflows/daily-build.yml @@ -10,6 +10,7 @@ jobs: with: bi: true mi: true + ballerina: true runTests: true runMIE2ETests: true diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 7341a84e57..1816d6844f 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -45,6 +45,7 @@ jobs: wso2-platform: false choreo: false apk: false + runTests: true version: ${{ inputs.version }} Release: diff --git a/.gitignore b/.gitignore index f2518547c7..9a1eb30570 100644 --- a/.gitignore +++ b/.gitignore @@ -83,5 +83,7 @@ package.json.backup pnpm-lock.yaml +TODO.md + # AI evaluation results **/results diff --git a/.trivyignore b/.trivyignore index e42fe3a681..f50b417ac0 100644 --- a/.trivyignore +++ b/.trivyignore @@ -7,3 +7,9 @@ CVE-2020-36851 # No fix released by the author CVE-2025-14505 + +# Library is used in nested dependencies and not directly used by our codebase. No fix released by the author. +CVE-2025-69873 + +# Library is used in nested dependencies and not directly used by our codebase. No fix released by the author. +CVE-2026-26996 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 2f9a4a6244..3268021522 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,7 +27,7 @@ "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}/workspaces/ballerina/ballerina-extension", - "--extensionDevelopmentPath=${workspaceFolder}/workspaces/bi/bi-extension" + "--extensionDevelopmentPath=${workspaceFolder}/workspaces/bi/bi-extension", ], "env": { "LS_EXTENSIONS_PATH": "", @@ -41,7 +41,35 @@ "${workspaceFolder}/workspaces/ballerina/ballerina-extension/dist/**/*.js", "${workspaceFolder}/workspaces/bi/bi-extension/out/**/*.js" ], - "preLaunchTask": "watch-all", + "preLaunchTask": "watch-ballerina-bi", + "envFile": "${workspaceFolder}/workspaces/ballerina/ballerina-extension/.env" + }, + { + "name": "Ballerina, BI & Platform Extensions", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/workspaces/ballerina/ballerina-extension", + "--extensionDevelopmentPath=${workspaceFolder}/workspaces/bi/bi-extension", + "--extensionDevelopmentPath=${workspaceFolder}/workspaces/wso2-platform/wso2-platform-extension" + ], + "env": { + "LS_EXTENSIONS_PATH": "", + "LSDEBUG": "false", + "WEB_VIEW_WATCH_MODE": "true", + "WEB_VIEW_DEV_HOST": "http://localhost:9000", + "BALLERINA_STAGE_CENTRAL": "false", + + "PLATFORM_WEB_VIEW_DEV_MODE": "true", + "PLATFORM_WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", + }, + "outFiles": [ + "${workspaceFolder}/workspaces/ballerina/ballerina-extension/dist/**/*.js", + "${workspaceFolder}/workspaces/bi/bi-extension/out/**/*.js", + "${workspaceFolder}/workspaces/wso2-platform/wso2-platform-extension/dist/**/*.js", + ], + "preLaunchTask": "watch-ballerina-bi-platform", "envFile": "${workspaceFolder}/workspaces/ballerina/ballerina-extension/.env" }, { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 29a7996066..b10d3f87e8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,9 +12,13 @@ "problemMatcher": ["$tsc"] }, { - "label": "watch-all", + "label": "watch-ballerina-bi", "dependsOn": ["watch-ballerina", "watch-bi"] }, + { + "label": "watch-ballerina-bi-platform", + "dependsOn": ["watch-ballerina", "watch-bi", "npm: watch-wso2-platform"] + }, { "label": "watch-ballerina", "type": "npm", diff --git a/common/autoinstallers/rush-plugins/package.json b/common/autoinstallers/rush-plugins/package.json index 79e1f23ce5..9662e5671b 100644 --- a/common/autoinstallers/rush-plugins/package.json +++ b/common/autoinstallers/rush-plugins/package.json @@ -7,6 +7,6 @@ } }, "dependencies": { - "@gigara/rush-github-action-build-cache-plugin": "1.1.4" + "@gigara/rush-github-action-build-cache-plugin": "1.1.8" } } diff --git a/common/autoinstallers/rush-plugins/pnpm-lock.yaml b/common/autoinstallers/rush-plugins/pnpm-lock.yaml index 5bda77c359..f5c64ea3e1 100644 --- a/common/autoinstallers/rush-plugins/pnpm-lock.yaml +++ b/common/autoinstallers/rush-plugins/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@gigara/rush-github-action-build-cache-plugin': - specifier: 1.1.4 - version: 1.1.4 + specifier: 1.1.8 + version: 1.1.8 packages: @@ -48,9 +48,12 @@ packages: resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} engines: {node: '>=20.0.0'} - '@azure/core-http-compat@2.3.1': - resolution: {integrity: sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==} + '@azure/core-http-compat@2.3.2': + resolution: {integrity: sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==} engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 '@azure/core-lro@2.7.2': resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} @@ -80,16 +83,16 @@ packages: resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} engines: {node: '>=20.0.0'} - '@azure/storage-blob@12.30.0': - resolution: {integrity: sha512-peDCR8blSqhsAKDbpSP/o55S4sheNwSrblvCaHUZ5xUI73XA7ieUGGwrONgD/Fng0EoDe1VOa3fAQ7+WGB3Ocg==} + '@azure/storage-blob@12.31.0': + resolution: {integrity: sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==} engines: {node: '>=20.0.0'} '@azure/storage-common@12.3.0': resolution: {integrity: sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ==} engines: {node: '>=20.0.0'} - '@gigara/rush-github-action-build-cache-plugin@1.1.4': - resolution: {integrity: sha512-KL87XJSiKwXEXtAj9g1CSOSFa02URE+PHZy/hnbLQNQuL/yGr3gvC+ZEhiYC5GL7/a+ZD+yaGJmU/KMX7NatOg==} + '@gigara/rush-github-action-build-cache-plugin@1.1.8': + resolution: {integrity: sha512-lkZbhG11/26hibbfNoEyCN2L2GcpY1YzO6rsEWozcOmxVct82Hr7xKspmwUavo4dpA38FlJQMqBM3w5kz/uVAg==} '@protobuf-ts/runtime-rpc@2.11.1': resolution: {integrity: sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==} @@ -97,8 +100,8 @@ packages: '@protobuf-ts/runtime@2.11.1': resolution: {integrity: sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==} - '@typespec/ts-http-runtime@0.3.2': - resolution: {integrity: sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==} + '@typespec/ts-http-runtime@0.3.3': + resolution: {integrity: sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA==} engines: {node: '>=20.0.0'} agent-base@7.1.4: @@ -127,8 +130,8 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - fast-xml-parser@5.3.4: - resolution: {integrity: sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==} + fast-xml-parser@5.3.6: + resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==} hasBin: true http-proxy-agent@7.0.2: @@ -174,7 +177,7 @@ snapshots: '@actions/io': 2.0.0 '@azure/abort-controller': 1.1.0 '@azure/core-rest-pipeline': 1.22.2 - '@azure/storage-blob': 12.30.0 + '@azure/storage-blob': 12.31.0 '@protobuf-ts/runtime-rpc': 2.11.1 semver: 6.3.1 transitivePeerDependencies: @@ -229,13 +232,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/core-http-compat@2.3.1': + '@azure/core-http-compat@2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2)': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-client': 1.10.1 '@azure/core-rest-pipeline': 1.22.2 - transitivePeerDependencies: - - supports-color '@azure/core-lro@2.7.2': dependencies: @@ -257,7 +258,7 @@ snapshots: '@azure/core-tracing': 1.3.1 '@azure/core-util': 1.13.1 '@azure/logger': 1.3.0 - '@typespec/ts-http-runtime': 0.3.2 + '@typespec/ts-http-runtime': 0.3.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -269,29 +270,29 @@ snapshots: '@azure/core-util@1.13.1': dependencies: '@azure/abort-controller': 2.1.2 - '@typespec/ts-http-runtime': 0.3.2 + '@typespec/ts-http-runtime': 0.3.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color '@azure/core-xml@1.5.0': dependencies: - fast-xml-parser: 5.3.4 + fast-xml-parser: 5.3.6 tslib: 2.8.1 '@azure/logger@1.3.0': dependencies: - '@typespec/ts-http-runtime': 0.3.2 + '@typespec/ts-http-runtime': 0.3.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/storage-blob@12.30.0': + '@azure/storage-blob@12.31.0': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.10.1 '@azure/core-client': 1.10.1 - '@azure/core-http-compat': 2.3.1 + '@azure/core-http-compat': 2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2) '@azure/core-lro': 2.7.2 '@azure/core-paging': 1.6.2 '@azure/core-rest-pipeline': 1.22.2 @@ -299,17 +300,17 @@ snapshots: '@azure/core-util': 1.13.1 '@azure/core-xml': 1.5.0 '@azure/logger': 1.3.0 - '@azure/storage-common': 12.3.0 + '@azure/storage-common': 12.3.0(@azure/core-client@1.10.1) events: 3.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/storage-common@12.3.0': + '@azure/storage-common@12.3.0(@azure/core-client@1.10.1)': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.10.1 - '@azure/core-http-compat': 2.3.1 + '@azure/core-http-compat': 2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2) '@azure/core-rest-pipeline': 1.22.2 '@azure/core-tracing': 1.3.1 '@azure/core-util': 1.13.1 @@ -317,9 +318,10 @@ snapshots: events: 3.3.0 tslib: 2.8.1 transitivePeerDependencies: + - '@azure/core-client' - supports-color - '@gigara/rush-github-action-build-cache-plugin@1.1.4': + '@gigara/rush-github-action-build-cache-plugin@1.1.8': dependencies: '@actions/cache': 5.0.5 '@actions/core': 2.0.3 @@ -332,7 +334,7 @@ snapshots: '@protobuf-ts/runtime@2.11.1': {} - '@typespec/ts-http-runtime@0.3.2': + '@typespec/ts-http-runtime@0.3.3': dependencies: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -357,7 +359,7 @@ snapshots: events@3.3.0: {} - fast-xml-parser@5.3.4: + fast-xml-parser@5.3.6: dependencies: strnum: 2.1.2 diff --git a/common/config/rush/.pnpmfile.cjs b/common/config/rush/.pnpmfile.cjs index 907f3abecb..f4a29d688e 100644 --- a/common/config/rush/.pnpmfile.cjs +++ b/common/config/rush/.pnpmfile.cjs @@ -41,7 +41,7 @@ module.exports = { pkg.dependencies['eslint'] = '^9.27.0'; } if (pkg.dependencies['fast-xml-parser']) { - pkg.dependencies['fast-xml-parser'] = '5.3.4'; + pkg.dependencies['fast-xml-parser'] = '5.3.6'; } if (pkg.dependencies['lodash']) { pkg.dependencies['lodash'] = '4.17.23'; @@ -55,9 +55,6 @@ module.exports = { if (pkg.dependencies['eslint']) { pkg.dependencies['eslint'] = '^9.27.0'; } - if (pkg.dependencies['fast-xml-parser']) { - pkg.dependencies['fast-xml-parser'] = '5.3.4'; - } if (pkg.dependencies['hono']) { pkg.dependencies['hono'] = '^4.11.7'; } @@ -92,7 +89,7 @@ module.exports = { pkg.devDependencies['eslint'] = '^9.27.0'; } if (pkg.devDependencies['fast-xml-parser']) { - pkg.devDependencies['fast-xml-parser'] = '5.3.4'; + pkg.devDependencies['fast-xml-parser'] = '5.3.6'; } if (pkg.devDependencies['lodash']) { pkg.devDependencies['lodash'] = '4.17.23'; @@ -106,9 +103,6 @@ module.exports = { if (pkg.devDependencies['eslint']) { pkg.devDependencies['eslint'] = '^9.27.0'; } - if (pkg.devDependencies['fast-xml-parser']) { - pkg.devDependencies['fast-xml-parser'] = '5.3.4'; - } if (pkg.devDependencies['hono']) { pkg.devDependencies['hono'] = '^4.11.7'; } diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 4eac4d3c63..48cf698141 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -385,6 +385,9 @@ importers: '@wso2/syntax-tree': specifier: workspace:* version: link:../syntax-tree + '@wso2/wso2-platform-core': + specifier: workspace:* + version: link:../../wso2-platform/wso2-platform-core handlebars: specifier: 4.7.8 version: 4.7.8 @@ -447,23 +450,26 @@ importers: ../../workspaces/ballerina/ballerina-extension: dependencies: '@ai-sdk/amazon-bedrock': - specifier: 4.0.4 - version: 4.0.4(zod@4.1.8) + specifier: ^4.0.52 + version: 4.0.60(zod@4.1.8) '@ai-sdk/anthropic': - specifier: 3.0.2 - version: 3.0.2(zod@4.1.8) + specifier: ^3.0.39 + version: 3.0.44(zod@4.1.8) + '@ai-sdk/google-vertex': + specifier: ^4.0.27 + version: 4.0.58(zod@4.1.8) '@iarna/toml': - specifier: 2.2.5 + specifier: ^2.2.5 version: 2.2.5 '@types/lodash': - specifier: 4.14.200 - version: 4.14.200 + specifier: ^4.14.200 + version: 4.17.17 '@vscode/test-electron': - specifier: 2.5.2 + specifier: ^2.5.2 version: 2.5.2 '@vscode/vsce': - specifier: 3.7.0 - version: 3.7.0 + specifier: ^3.7.0 + version: 3.7.1 '@wso2/ballerina-core': specifier: workspace:* version: link:../ballerina-core @@ -483,28 +489,28 @@ importers: specifier: workspace:* version: link:../../wso2-platform/wso2-platform-core ai: - specifier: 6.0.7 - version: 6.0.7(zod@4.1.8) + specifier: ^6.0.77 + version: 6.0.86(zod@4.1.8) cors-anywhere: - specifier: 0.4.4 + specifier: ^0.4.4 version: 0.4.4 del-cli: - specifier: 5.1.0 + specifier: ^5.1.0 version: 5.1.0 dotenv: - specifier: 16.5.0 + specifier: ~16.5.0 version: 16.5.0 file-uri-to-path: - specifier: 2.0.0 + specifier: ^2.0.0 version: 2.0.0 glob: - specifier: 11.1.0 + specifier: ^11.1.0 version: 11.1.0 handlebars: - specifier: 4.7.8 + specifier: ~4.7.8 version: 4.7.8 jwt-decode: - specifier: 4.0.0 + specifier: ^4.0.0 version: 4.0.0 lodash: specifier: 4.17.23 @@ -572,6 +578,9 @@ importers: zod: specifier: 4.1.8 version: 4.1.8 + zustand: + specifier: 5.0.5 + version: 5.0.5(@types/react@18.2.0)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)) devDependencies: '@sentry/webpack-plugin': specifier: 1.20.1 @@ -579,6 +588,9 @@ importers: '@types/chai': specifier: 4.3.9 version: 4.3.9 + '@types/js-yaml': + specifier: 4.0.9 + version: 4.0.9 '@types/mocha': specifier: 10.0.3 version: 10.0.3 @@ -925,6 +937,9 @@ importers: '@wso2/syntax-tree': specifier: workspace:* version: link:../syntax-tree + '@wso2/wso2-platform-core': + specifier: workspace:* + version: link:../../wso2-platform/wso2-platform-core monaco-editor: specifier: 0.44.0 version: 0.44.0 @@ -1019,6 +1034,9 @@ importers: '@wso2/ui-toolkit': specifier: workspace:* version: link:../../common-libs/ui-toolkit + '@wso2/wso2-platform-core': + specifier: workspace:* + version: link:../../wso2-platform/wso2-platform-core lodash: specifier: 4.17.23 version: 4.17.23 @@ -1128,6 +1146,9 @@ importers: '@tanstack/react-query': specifier: 5.77.1 version: 5.77.1(react@18.2.0) + '@tanstack/react-query-persist-client': + specifier: 5.77.1 + version: 5.77.1(@tanstack/react-query@5.77.1(react@18.2.0))(react@18.2.0) '@types/lodash': specifier: 4.17.16 version: 4.17.16 @@ -1194,6 +1215,9 @@ importers: highlight.js: specifier: 11.11.1 version: 11.11.1 + js-yaml: + specifier: 4.1.1 + version: 4.1.1 katex: specifier: ^0.16.27 version: 0.16.28 @@ -1239,6 +1263,9 @@ importers: remark-math: specifier: ^6.0.0 version: 6.0.0 + swagger-ui-react: + specifier: 5.22.0 + version: 5.22.0(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) vscode-uri: specifier: 3.1.0 version: 3.1.0 @@ -1246,6 +1273,9 @@ importers: specifier: 1.6.1 version: 1.6.1 devDependencies: + '@types/js-yaml': + specifier: 4.0.5 + version: 4.0.5 '@types/lodash.debounce': specifier: 4.0.6 version: 4.0.6 @@ -1264,6 +1294,9 @@ importers: '@types/react-syntax-highlighter': specifier: 15.5.13 version: 15.5.13 + '@types/swagger-ui-react': + specifier: 5.18.0 + version: 5.18.0 '@types/vscode-webview': specifier: 1.57.5 version: 1.57.5 @@ -2155,12 +2188,36 @@ importers: specifier: 18.2.0 version: 18.2.0(react@18.2.0) devDependencies: + '@babel/core': + specifier: 7.27.1 + version: 7.27.1 + '@babel/preset-env': + specifier: 7.27.2 + version: 7.27.2(@babel/core@7.27.1) + '@babel/preset-react': + specifier: 7.27.1 + version: 7.27.1(@babel/core@7.27.1) + '@babel/preset-typescript': + specifier: 7.27.1 + version: 7.27.1(@babel/core@7.27.1) '@storybook/react': specifier: 6.3.7 - version: 6.3.7(@babel/core@7.29.0)(@types/react@18.2.0)(@types/webpack@4.41.40)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1) + version: 6.3.7(@babel/core@7.27.1)(@types/react@18.2.0)(@types/webpack@4.41.40)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1) + '@testing-library/dom': + specifier: 10.4.0 + version: 10.4.0 + '@testing-library/jest-dom': + specifier: 6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: 16.3.0 + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.2.0)(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/dagre': specifier: 0.7.52 version: 0.7.52 + '@types/jest': + specifier: 29.5.14 + version: 29.5.14 '@types/lodash': specifier: 4.17.16 version: 4.17.16 @@ -2170,9 +2227,15 @@ importers: '@types/react-dom': specifier: 18.2.0 version: 18.2.0 + '@types/react-test-renderer': + specifier: 18.3.0 + version: 18.3.0 '@typescript-eslint/eslint-plugin': specifier: 8.32.1 version: 8.32.1(@typescript-eslint/parser@8.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.8.3) + babel-jest: + specifier: 29.7.0 + version: 29.7.0(@babel/core@7.27.1) copyfiles: specifier: 2.4.1 version: 2.4.1 @@ -2185,9 +2248,27 @@ importers: eslint-plugin-unused-imports: specifier: 4.1.4 version: 4.1.4(@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.2(jiti@2.6.1)) + identity-obj-proxy: + specifier: 3.0.0 + version: 3.0.0 + jest: + specifier: 29.7.0 + version: 29.7.0(@types/node@22.15.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)) + jest-environment-jsdom: + specifier: 29.7.0 + version: 29.7.0 prettier: specifier: 3.5.3 version: 3.5.3 + pretty-format: + specifier: 29.7.0 + version: 29.7.0 + react-test-renderer: + specifier: 18.3.0 + version: 18.3.0(react@18.2.0) + ts-jest: + specifier: 29.3.4 + version: 29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(jest@29.7.0(@types/node@22.15.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)))(typescript@5.8.3) typescript: specifier: 5.8.3 version: 5.8.3 @@ -2476,6 +2557,27 @@ importers: '@babel/core': specifier: 7.27.1 version: 7.27.1 + '@babel/preset-env': + specifier: 7.27.2 + version: 7.27.2(@babel/core@7.27.1) + '@babel/preset-react': + specifier: 7.27.1 + version: 7.27.1(@babel/core@7.27.1) + '@babel/preset-typescript': + specifier: 7.27.1 + version: 7.27.1(@babel/core@7.27.1) + '@testing-library/dom': + specifier: 10.4.0 + version: 10.4.0 + '@testing-library/jest-dom': + specifier: 6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: 16.3.0 + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.2.0)(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@types/jest': + specifier: 29.5.14 + version: 29.5.14 '@types/react': specifier: 18.2.0 version: 18.2.0 @@ -2491,6 +2593,9 @@ importers: '@wso2/ui-toolkit': specifier: workspace:* version: link:../../common-libs/ui-toolkit + babel-jest: + specifier: 29.7.0 + version: 29.7.0(@babel/core@7.27.1) babel-loader: specifier: 10.0.0 version: 10.0.0(@babel/core@7.27.1)(webpack@5.104.1) @@ -2500,18 +2605,30 @@ importers: css-loader: specifier: 7.1.2 version: 7.1.2(webpack@5.104.1) + identity-obj-proxy: + specifier: 3.0.0 + version: 3.0.0 + jest: + specifier: 29.7.0 + version: 29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + jest-environment-jsdom: + specifier: 29.7.0 + version: 29.7.0 source-map-loader: specifier: 5.0.0 version: 5.0.0(webpack@5.104.1) style-loader: specifier: 4.0.0 version: 4.0.0(webpack@5.104.1) + ts-jest: + specifier: 29.3.4 + version: 29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(jest@29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)))(typescript@5.8.3) ts-loader: specifier: 9.4.1 version: 9.4.1(typescript@5.8.3)(webpack@5.104.1) webpack: specifier: 5.104.1 - version: 5.104.1(webpack-cli@6.0.1) + version: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@6.0.1) webpack-cli: specifier: 6.0.1 version: 6.0.1(webpack-dev-server@5.2.3)(webpack@5.104.1) @@ -3818,8 +3935,8 @@ importers: specifier: 16.5.0 version: 16.5.0 fast-xml-parser: - specifier: 5.3.4 - version: 5.3.4 + specifier: 5.3.6 + version: 5.3.6 find-process: specifier: 1.4.10 version: 1.4.10 @@ -4108,8 +4225,8 @@ importers: specifier: 1.0.20 version: 1.0.20 fast-xml-parser: - specifier: 5.3.4 - version: 5.3.4 + specifier: 5.3.6 + version: 5.3.6 lodash: specifier: 4.17.23 version: 4.17.23 @@ -4595,8 +4712,8 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - '@ai-sdk/amazon-bedrock@4.0.4': - resolution: {integrity: sha512-ssy90ibTrbszGJnYF98vS7bJQtmIp1355iWgKHd2cS8uG8FrBQyYWZZNOSq5dIB3SRzfqKjsyUHKJyF6f7J95w==} + '@ai-sdk/amazon-bedrock@4.0.60': + resolution: {integrity: sha512-LNzRryLknon4fmnR6ySKhP6PosFBtlnG6KD40zd91ZQsn3dlrs4I9Fe54o+Gm38FqJufGF16yNiI/zvZur/OGQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -4607,8 +4724,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/anthropic@3.0.2': - resolution: {integrity: sha512-D6iSsrOYryBSPsFtOiEDv54jnjVCU/flIuXdjuRY7LdikB0KGjpazN8Dt4ONXzL+ux69ds2nzFNKke/w/fgLAA==} + '@ai-sdk/anthropic@3.0.44': + resolution: {integrity: sha512-ke1NldgohWJ7sWLqm9Um9TVIOrtg8Y8AecWeB6PgaLt+paTPisAsyNfe8FNOVusuv58ugLBqY/78AkhUmbjXHA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -4619,8 +4736,20 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.6': - resolution: {integrity: sha512-oEpwjM0PIaSUErtZI8Ag+gQ+ZelysRWA96N5ahvOc5e9d7QkKJWF0POWx0nI1qBxvmUSw7ca0sLTVw+J5yn7Tg==} + '@ai-sdk/gateway@3.0.46': + resolution: {integrity: sha512-zH1UbNRjG5woOXXFOrVCZraqZuFTtmPvLardMGcgLkzpxKV0U3tAGoyWKSZ862H+eBJfI/Hf2yj/zzGJcCkycg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google-vertex@4.0.58': + resolution: {integrity: sha512-i19ovUxTopZt4Z8ICbSzrww1ESiNJjMxbkxFia0pncfteZMA2FOAgFazGOIOIGZexRuHrpZ7cWID3DdEtubxBQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@3.0.29': + resolution: {integrity: sha512-x0hcU10AA+i1ZUQHloGD5qXWsB+Y8qnxlmFUef6Ly4rB53MGVbQExkI9nOKiCO3mu2TGiiNoQMeKWSeQVLfRUA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -4631,8 +4760,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.2': - resolution: {integrity: sha512-KaykkuRBdF/ffpI5bwpL4aSCmO/99p8/ci+VeHwJO8tmvXtiVAb99QeyvvvXmL61e9Zrvv4GBGoajW19xdjkVQ==} + '@ai-sdk/provider-utils@4.0.15': + resolution: {integrity: sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -4641,8 +4770,8 @@ packages: resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} - '@ai-sdk/provider@3.0.1': - resolution: {integrity: sha512-2lR4w7mr9XrydzxBSjir4N6YMGdXD+Np1Sh0RXABh7tWdNFFwIeRI1Q+SaYZMbfL8Pg8RRLcrxQm51yxTLhokg==} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} '@alloc/quick-lru@5.2.0': @@ -5848,8 +5977,8 @@ packages: '@codemirror/lint@6.8.5': resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==} - '@codemirror/merge@6.11.2': - resolution: {integrity: sha512-NO5EJd2rLRbwVWLgMdhIntDIhfDtMOKYEZgqV5WnkNUS2oXOCVWLPjG/kgl/Jth2fGiOuG947bteqxP9nBXmMg==} + '@codemirror/merge@6.12.0': + resolution: {integrity: sha512-o+36bbapcEHf4Ux75pZ4CKjMBUd14parA0uozvWVlacaT+uxaA3DDefEvWYjngsKU+qsrDe/HOOfsw0Q72pLjA==} '@codemirror/search@6.6.0': resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} @@ -7226,8 +7355,8 @@ packages: '@lezer/cpp@1.1.5': resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} - '@lezer/css@1.3.0': - resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==} + '@lezer/css@1.3.1': + resolution: {integrity: sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==} '@lezer/go@1.0.1': resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} @@ -8597,14 +8726,14 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@redhat-developer/locators@1.18.1': - resolution: {integrity: sha512-fnb5prprb8jTzx9Vl2DiJq55XK7aZVwpDZV/iLY/pYsNWEn42/rLSyMUNDrvlhnFryqmVstmDUIr4eGE3iyDMw==} + '@redhat-developer/locators@1.19.0': + resolution: {integrity: sha512-BW2QRxuI94IuSTDZjMsNQ7OGODJkdD42hXjBe9PaqFtC0S56j4X+qG3QlDT2wf42ePKlitXoMMYh+Z/r1pIaKg==} peerDependencies: '@redhat-developer/page-objects': '>=1.0.0' selenium-webdriver: '>=4.6.1' - '@redhat-developer/page-objects@1.18.1': - resolution: {integrity: sha512-clwXbTAykhqJqUkdTWUtS7jwY1eQVhlgQia6GfmMHMLGxqMbHCgHVm0rEYoR1C050XWYxtfsEpnsmUB9Kjcs9A==} + '@redhat-developer/page-objects@1.19.0': + resolution: {integrity: sha512-uUOH31br3Ibjv4xZWF/fi0TPsOMe1oiwDm3SblJBYP/Q1rauMD/sKOQt3LGe1iZr/J9LsC8Bk0D/saqQAkVwjQ==} peerDependencies: selenium-webdriver: '>=4.6.1' typescript: '>=4.6.2' @@ -8927,8 +9056,8 @@ packages: resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.23.0': - resolution: {integrity: sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg==} + '@smithy/core@3.23.2': + resolution: {integrity: sha512-HaaH4VbGie4t0+9nY3tNBRSxVTr96wzIqexUa6C2qx3MPePAuz7lIxPxYtt1Wc//SPfJLNoZJzfdt0B6ksj2jA==} engines: {node: '>=18.0.0'} '@smithy/credential-provider-imds@4.2.8': @@ -8991,12 +9120,12 @@ packages: resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.14': - resolution: {integrity: sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag==} + '@smithy/middleware-endpoint@4.4.16': + resolution: {integrity: sha512-L5GICFCSsNhbJ5JSKeWFGFy16Q2OhoBizb3X2DrxaJwXSEujVvjG9Jt386dpQn2t7jINglQl0b4K/Su69BdbMA==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.31': - resolution: {integrity: sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg==} + '@smithy/middleware-retry@4.4.33': + resolution: {integrity: sha512-jLqZOdJhtIL4lnA9hXnAG6GgnJlo1sD3FqsTxm9wSfjviqgWesY/TMBVnT84yr4O0Vfe0jWoXlfFbzsBVph3WA==} engines: {node: '>=18.0.0'} '@smithy/middleware-serde@4.2.9': @@ -9043,8 +9172,8 @@ packages: resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.11.3': - resolution: {integrity: sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg==} + '@smithy/smithy-client@4.11.5': + resolution: {integrity: sha512-xixwBRqoeP2IUgcAl3U9dvJXc+qJum4lzo3maaJxifsZxKUYLfVfCXvhT4/jD01sRrHg5zjd1cw2Zmjr4/SuKQ==} engines: {node: '>=18.0.0'} '@smithy/types@4.12.0': @@ -9079,12 +9208,12 @@ packages: resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.30': - resolution: {integrity: sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng==} + '@smithy/util-defaults-mode-browser@4.3.32': + resolution: {integrity: sha512-092sjYfFMQ/iaPH798LY/OJFBcYu0sSK34Oy9vdixhsU36zlZu8OcYjF3TD4e2ARupyK7xaxPXl+T0VIJTEkkg==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.33': - resolution: {integrity: sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA==} + '@smithy/util-defaults-mode-node@4.2.35': + resolution: {integrity: sha512-miz/ggz87M8VuM29y7jJZMYkn7+IErM5p5UgKIf8OtqVs/h2bXr1Bt3uTsREsI/4nK8a0PQERbAPsVPVNIsG7Q==} engines: {node: '>=18.0.0'} '@smithy/util-endpoints@3.2.8': @@ -10571,104 +10700,104 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@swagger-api/apidom-ast@1.4.0': - resolution: {integrity: sha512-vaN7kA/okd3dWn45BRdsPHgGfMQdjL3TqRcxTmb41q6SLRJezINTE5nyJ8qVmH9bverOTAfn5IjYziwhV0lh7A==} + '@swagger-api/apidom-ast@1.5.0': + resolution: {integrity: sha512-sSfoHE1Bb/FM4//sqsU9VN1refIRGd/RlIwsWJ132TJyEtMAw/Blo68hl4zTGh6ob89OZSKqQ0oZNmwickYhxA==} - '@swagger-api/apidom-core@1.4.0': - resolution: {integrity: sha512-otHtWG8J/0bvaKThUfbSuYG9ega1jAM4ugIcxQRRozOMS7r1+pTwntbY0my5MJwZ+TeBJfVA39NRjMPiPdGttg==} + '@swagger-api/apidom-core@1.5.0': + resolution: {integrity: sha512-esQ++pv78gCr8ZDdM9dfEjrOymcELzFmJCe5i3q8Q2pQSSkW+FUsUCaSP6mdB+HHqz4H9L+Jy9N7h4e/uVsCVQ==} - '@swagger-api/apidom-error@1.4.0': - resolution: {integrity: sha512-oYVvGSE/y7g2mNnfNPq1IrEhhEQ0Faz1YWZWpaLF786iT0lunzzph9JG1Q9YSl+Z5yZ6XsmbKSOdvql+gK9xCw==} + '@swagger-api/apidom-error@1.5.0': + resolution: {integrity: sha512-lSc/7wS/t4y6KmueouAopzZnm1r1/5QzcOUPmwQcDrnoLLqtfd2Hkp+v3a0vqHbbyUi+lHYmCA0JoITzbJYcgQ==} - '@swagger-api/apidom-json-pointer@1.4.0': - resolution: {integrity: sha512-LAMOgJaKy2id2zTmMmQzSEh4HahXYfWoR6pBsdNIKaLWx6sirznFnpKHa2+shfjksAkm0Gng5/eKO1jcdySOrw==} + '@swagger-api/apidom-json-pointer@1.5.0': + resolution: {integrity: sha512-YiRA7n6aOnXD6ZrZcp6avZiVz55rihye5gp4QB3+qEtfcOvFIG6jYRRawXO/1dj9haZvq/2gQys+kJSEQEhMhg==} - '@swagger-api/apidom-ns-api-design-systems@1.4.0': - resolution: {integrity: sha512-uRTqvqSUdQwkiNlO9QMPcpI5ZF7PbwnavFwIkupbkZDb5r+kSdgu4bepXY9BjzvVeiOWqyvviIfTeccVcVOCeA==} + '@swagger-api/apidom-ns-api-design-systems@1.5.0': + resolution: {integrity: sha512-jt8hCcN/NhmZPMMGy7DNbw1LHu7gUqcNkG840aC2OUVj+qHp7CDG7wing0F2zIW3+fDfw+UIiVzFy7oVbiiheg==} - '@swagger-api/apidom-ns-arazzo-1@1.4.0': - resolution: {integrity: sha512-gUWldcU8cCN4zYfpGdgK+rXFkcmNCv/g9eXOvQaAzYHp+N8mccRl5E0weEDRVvn9kUOA0bew7NjEEZhMulblWQ==} + '@swagger-api/apidom-ns-arazzo-1@1.5.0': + resolution: {integrity: sha512-ATkcwJfyu3rhwLwicCmARhfQM6/KRGZ2+Y5HuG5smPxzzDdgb0o5/VP9goSvxgfAveIJsEWFbP6CMm/LW4Gy3A==} - '@swagger-api/apidom-ns-asyncapi-2@1.4.0': - resolution: {integrity: sha512-QCzE1Dio18x/1oyElrMzwCOjnpQxlWErhcd4EIJCC61cblkHQC7feuhuZHVhH7dYqBehNH3lvJC/rBnUz8Fmlg==} + '@swagger-api/apidom-ns-asyncapi-2@1.5.0': + resolution: {integrity: sha512-luYRreqMhHRSGQWEOt8I8Pd12Up2tl66w1oCBBJHwzc97j8a9sN6S0s6FuJS+LLajJgyDwwL6msgP5r92W9Ppw==} - '@swagger-api/apidom-ns-asyncapi-3@1.4.0': - resolution: {integrity: sha512-XcPsuGVKO8ed8WDvA56UAUR6b8YgsL+JB/pQrSnUm2KRajg6+736c8L83a2EZB0ulaZOY2+Ex/bkuYNak0ceJA==} + '@swagger-api/apidom-ns-asyncapi-3@1.5.0': + resolution: {integrity: sha512-Xvj6tAVchSkki9rWb5d9KzHbr7f0EmO8tZ6rfqEQHlx+XGAOoMzlRrwjvPvrn/8+wOI8E/AxVw25TuRfxiHMAQ==} - '@swagger-api/apidom-ns-json-schema-2019-09@1.4.0': - resolution: {integrity: sha512-xvsYdcfk00vcDYAK+qA2PBQs/uyuYUc3Bubv19s8rRd+gykdzm+7vZjYEMXIZuqn4ao+DDe7kIFipx31Cz4zTw==} + '@swagger-api/apidom-ns-json-schema-2019-09@1.5.0': + resolution: {integrity: sha512-xd4Fhs3YSqKNDO7yhs7u+mcrEi9gBIo2i1NjUDAX4YLs4QPwACq4ruEp1VihSrgunv/A1dQvmQcLpjpTsoqtIg==} - '@swagger-api/apidom-ns-json-schema-2020-12@1.4.0': - resolution: {integrity: sha512-FOadSRcNmKmXDi71DpOaNyXIvIY13FSFzCVLoMrIJfAVMmuuWN+hgBjedjnp8mkRjzTW96XRjAuCQ1WUT2OCFA==} + '@swagger-api/apidom-ns-json-schema-2020-12@1.5.0': + resolution: {integrity: sha512-+WdWwrLpdZaEDqS5uzVUJ34emCZSNOzqs2AfrxopOenbQM/Th29lHrTt2Zu6L4xhlp8HTUaqIb8nnkjVd/wheg==} - '@swagger-api/apidom-ns-json-schema-draft-4@1.4.0': - resolution: {integrity: sha512-LKzAUqHEAgm4WmuYbhPCDycr1nO/G14NwNG35RQJHVZi+FRrvzFgD80h5yNNzYjXYtNQrRGtKPTR6LXImZjWag==} + '@swagger-api/apidom-ns-json-schema-draft-4@1.5.0': + resolution: {integrity: sha512-YxGZcTjXuKOPX2DHhSM3SBtxzriXs/8blNIBouIxqtalythud+nyi2sx1pQllBraBDsqQl3q5nCNXufum+TrNw==} - '@swagger-api/apidom-ns-json-schema-draft-6@1.4.0': - resolution: {integrity: sha512-QoxxQw0xDpFe2SYeX96Gt2rp1MSQcvSoiiV9yR6G9j0Jbr3bipFiI6rK4OFSXq3Wcalfccjz77aiG42K9QDWUQ==} + '@swagger-api/apidom-ns-json-schema-draft-6@1.5.0': + resolution: {integrity: sha512-OOzP1Xi/GETOQ4qEksoESsq7CV7mW51aYk8T7yKArWocT3dJ9pn6smmpNmd3MnGVXJFumscKHCQzYrjnCQE6/A==} - '@swagger-api/apidom-ns-json-schema-draft-7@1.4.0': - resolution: {integrity: sha512-jnjuj4n26FhS2ZXwA6n86PlRi2sZ5GIP18JBRLGwDxTRCEqguX7Ar1NgKDn9VCoCmSZQ04ebLoipFGafvlA3VA==} + '@swagger-api/apidom-ns-json-schema-draft-7@1.5.0': + resolution: {integrity: sha512-lL8S8Qmm5gc386bYfO4ptL1BVlN5Htg3QITXew8y6EItAo8syXCPkWy7h8JXC76vGVMNLr1KAISuZ0dzqz3Yow==} - '@swagger-api/apidom-ns-openapi-2@1.4.0': - resolution: {integrity: sha512-2Uxy+dv4oU5Zw03Twkoxz1FWz4117Sz6UEWUmRaFsijGpaxWkauy6tBpp/U43D86eMRpkR6Jb6OA+UTaENYxxQ==} + '@swagger-api/apidom-ns-openapi-2@1.5.0': + resolution: {integrity: sha512-i8quM2983OaWJf4/44CeFwzZI0Kr0W4i54lfbKw88kcZWgYB5OXqdUcbOX9g15z2bd+IpmLzmeWjY5O0WiXWKg==} - '@swagger-api/apidom-ns-openapi-3-0@1.4.0': - resolution: {integrity: sha512-012Zx/r6ncajjc98AzZNDUpt1ShXwutUtrvCuBoz8dwQzpJfPc2HNGMM6tOvYX/isqzeCG7+zlKEG8tEv0X1RA==} + '@swagger-api/apidom-ns-openapi-3-0@1.5.0': + resolution: {integrity: sha512-P+97SVL+uomuO1lmByBcXZLkZjHJCZ/7hTPbMmmm40f0J6Se46tRuakImaQLtZld5X067aSxYVo9/npm+VecMQ==} - '@swagger-api/apidom-ns-openapi-3-1@1.4.0': - resolution: {integrity: sha512-ZW91r56RyalrNdxB4vnJNOXLNi/iQZSyvUjNDkJAO5e8/z3Hf7evU1ftCpgvsQVwiVXOG+b5KfgOw3Lwbc+a2w==} + '@swagger-api/apidom-ns-openapi-3-1@1.5.0': + resolution: {integrity: sha512-8tIL0Kz+3FYelW+zoWd8/5Pj5neDOD4nOV4GJbmLntfQRjboi1ki/eoKqHJvhBBrMSzgBZidgBauzK7HipTZ8A==} - '@swagger-api/apidom-parser-adapter-api-design-systems-json@1.4.0': - resolution: {integrity: sha512-XtLjh4dHQZwLkqhYSA2Uizu33/MrJsnk2KYXujokMJN4BkfGa6thzzvjTUCC7ZFQW6hV2aMm6tmwm51UdWCCMQ==} + '@swagger-api/apidom-parser-adapter-api-design-systems-json@1.5.0': + resolution: {integrity: sha512-BeX09CAvJYuwwg6uxtHBWOdrviF7Q0ylve2Qc4U5M8TST5Cff2X8H6y26WU6ss6te/VsFYMtkZw6uYf78m9kXA==} - '@swagger-api/apidom-parser-adapter-api-design-systems-yaml@1.4.0': - resolution: {integrity: sha512-1adlUHS7X1FAFVFb/3XnLh/D8PLcbBcf40OXmIpdQW92fIUdhhUJfuthTD6i4no0bEplkFATyO6Yd6LYNAGSCw==} + '@swagger-api/apidom-parser-adapter-api-design-systems-yaml@1.5.0': + resolution: {integrity: sha512-OBP/ccJLkZA0TEgzZvzuVOnv/2ic1Q3xW1BkTEGUapAqG0qBuUFNVjiIBVWKbPA88BP4vamvTxtT/nhnraC04w==} - '@swagger-api/apidom-parser-adapter-arazzo-json-1@1.4.0': - resolution: {integrity: sha512-Xqzi8ciYxNaMIXLtWh/80xQAh6ulqd/yOxDXJKdQqAQSExGwRI3bXHo0ynIvMJIkO049VKDhvqmTv1vEmZwQUA==} + '@swagger-api/apidom-parser-adapter-arazzo-json-1@1.5.0': + resolution: {integrity: sha512-mpJoq0zhvweYoctl8IfjcmYPQVWaFptEpNq0AtjXYvHp1bNNTLV/Q+oRKOO4odSPTyoHVzJjahtwgxP1DqXBag==} - '@swagger-api/apidom-parser-adapter-arazzo-yaml-1@1.4.0': - resolution: {integrity: sha512-HkWOuNjKnzgaa/ZCSLT1v+kqBof1dou6m3dXUfWw/hTpXk4Pp/kwKtqz9n+LCvG2bNfVEZFvwgGF3N6ZSujdog==} + '@swagger-api/apidom-parser-adapter-arazzo-yaml-1@1.5.0': + resolution: {integrity: sha512-WNC4C8U+cb8kprYFWziMtnO+9/mZuJ5BYXCe79vCH9G9CTDcmoDD1HeYkpAon4BKAI4jNbsZ4boF09ineAHFSQ==} - '@swagger-api/apidom-parser-adapter-asyncapi-json-2@1.4.0': - resolution: {integrity: sha512-i0WWw0xPZvPf3jTET0YzE8INu7PyNTyypD09WCKhhKgmFpNmZ2SHbHTP6+/OTTRu4nId9+icD6cHKCma0/W2tQ==} + '@swagger-api/apidom-parser-adapter-asyncapi-json-2@1.5.0': + resolution: {integrity: sha512-9xL8PTHqBv1oBZpTlST596n/TBAuHRa+t41OrThQIg+6mqkQFy6jqf/nFFe4B6MQyOURNLcII4VNcdBVwvHSpQ==} - '@swagger-api/apidom-parser-adapter-asyncapi-json-3@1.4.0': - resolution: {integrity: sha512-zKklrdThBa9yqmYeZYyX5zd0NKCLIUOIkV1HtMXsCg2f9ceTHldtenn3RvqYMY0EDd6IpVRfLfOhUNPpttTLOg==} + '@swagger-api/apidom-parser-adapter-asyncapi-json-3@1.5.0': + resolution: {integrity: sha512-ySX+YuSMiJUR5CMox3GBotiT9vtA7jdZz+ZD7XC2zb4RGfayGtHX02QBqBarlgoZIVorP4+ps6ol0+Zg55vbYA==} - '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@1.4.0': - resolution: {integrity: sha512-+hzzfCSJ8KaoApYmI22aYKoXi0La0/696FaOnCloPloKZwcPvjR5S7WxleZTMuAcXzeM4R/92LbD5BTUein4Ow==} + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@1.5.0': + resolution: {integrity: sha512-E+SY7E1nE4A40fgrZvKy0U0nXYAWk4sHTCIp0uxpfkzBXq5wp7IU+wr6E56THzuNF0HaNMzzSfi6agNad30bPg==} - '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3@1.4.0': - resolution: {integrity: sha512-R7cbEbFYTJ8hCV04VkaRap54T0t4Q+v9gXvxVf+Kn1csRXoR0O/yJBakCcmv7taswzv+1dCWbjJOJSHDJFIB0w==} + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3@1.5.0': + resolution: {integrity: sha512-NCwY+ZcmEjwabbDp1/uQtnNWfSd1KI9ikM6McM4aOWHp4b24AdClbTl1W5QXskM2f9g45TadmWR8Ol7yex6H3g==} - '@swagger-api/apidom-parser-adapter-json@1.4.0': - resolution: {integrity: sha512-QfwjuG1UaEYGf+tpPfN6GAZTJezhq2uy5Dgxmxe1f9Z/0V3mZmmdFck+NEXU698tAvFsf+qMeFmeR22EB4mzSg==} + '@swagger-api/apidom-parser-adapter-json@1.5.0': + resolution: {integrity: sha512-pSavF+9zuaaEoQ5lHqfYoHpo4sKBAnZGQcLAGI6ZhpNOHkxFkcmDd/hNYI0gM9g7NuwX6+xFBZi29OYsA4qtlw==} - '@swagger-api/apidom-parser-adapter-openapi-json-2@1.4.0': - resolution: {integrity: sha512-1crFQpjzGod/wdW8BE1C8j/qMe02EaUlXt8ZWePns3GzEWSEdxRuiiNh/b0EzXVlAUStcRigzaajNsn+KT3tUA==} + '@swagger-api/apidom-parser-adapter-openapi-json-2@1.5.0': + resolution: {integrity: sha512-O9ndJK/oOiPnY0UmrqIDYFAL4bODT3PJFa8bVVORKr+ZdUPg/xRZL/kgWgA8uOBfTuKU57sQvFvTOV8omyeURQ==} - '@swagger-api/apidom-parser-adapter-openapi-json-3-0@1.4.0': - resolution: {integrity: sha512-K7r2qZ3wNu+ql4yUsTILbMCTZ5BY0deEi+ig+9KskRqjJ/0TZeeqq3rj8GCgHw7ocQh2WnMBeGGX1YO8bWBAUQ==} + '@swagger-api/apidom-parser-adapter-openapi-json-3-0@1.5.0': + resolution: {integrity: sha512-qxh/sV54E6Y8VknvzAu/21r8ElxB1N3mmCnxTh1q6AUo6MX0SWCmnL1E2v3/2/HlJAA6yHW4RjwdKAv90Wklxw==} - '@swagger-api/apidom-parser-adapter-openapi-json-3-1@1.4.0': - resolution: {integrity: sha512-oECL5TrqbYzVRv17Jco1G3KruZkh7N7iKhfqgBcb1irZ4ozXNlv1FtZ8bZxpczXxvGhSkSLFKbEd0pHZaO0iPA==} + '@swagger-api/apidom-parser-adapter-openapi-json-3-1@1.5.0': + resolution: {integrity: sha512-DAMwQlrZj8b9lBj4G4huWLPoOHmOASe0JFFfhu18kmcCFtUSOR5UsCTvbJDyOuztOyKCT/HIMXp3/OEKbU8y6w==} - '@swagger-api/apidom-parser-adapter-openapi-yaml-2@1.4.0': - resolution: {integrity: sha512-Ai0j0oInutDAb0Lvpb/UBhatc0QOLgdYpAPupafvjCEFt6fTpomO2HjNA+4Z+0cqH69ggKgQ/ldJ5G8FgqKcrw==} + '@swagger-api/apidom-parser-adapter-openapi-yaml-2@1.5.0': + resolution: {integrity: sha512-KHPrgAY1g5Fucy+BFDdzUrGaFaVr86d3IgWFvifnKUQGpz96zMMaGG+IWsUoSfoAQaJSHpz+SVnNFaIF2sLOfQ==} - '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@1.4.0': - resolution: {integrity: sha512-DD6JwfHcywPxZl7e3UqeAU4qdzjQaVudUnnFHWH83m6qWEHldN1+vSfQTzn4DQgi9dz88XfIakUJ1HFHIfEBhg==} + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@1.5.0': + resolution: {integrity: sha512-owFoZyqiRsSUtDEuSafLGWHwupVeh+pcspfnQy2sl7bmYmsi73NMJpsdIMcizlyeOqav0MWQ7FNrBZOSctiPIQ==} - '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@1.4.0': - resolution: {integrity: sha512-FWGMgP1iJGIXISLSyz0j/Pthux11H712gPYzaL7DRZz8wevQvgc4TrYF4hHZwSvN8xpEkJLURBclfwsLDKzIXQ==} + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@1.5.0': + resolution: {integrity: sha512-9NcPLDbXZTftx9cMDh9iW+nXLeFFFTCuRyXKvtV+tu3PEiACMUIi0ij5J6+NZ3sBQVaEp7qF7nqzPIT2JRZHzw==} - '@swagger-api/apidom-parser-adapter-yaml-1-2@1.4.0': - resolution: {integrity: sha512-rqVxBEiuCyBbNhnFUsBxPEewFjKDBYkiFqJGmwNo79G51wJOK9GnSjT1AyMZ+fW3RDlDZ+HR8IojQCykhYoAGg==} + '@swagger-api/apidom-parser-adapter-yaml-1-2@1.5.0': + resolution: {integrity: sha512-TBpyfwQ0aFWmVlpI4aKMVwoPM3bLxtc9MzDchjo5BXWLfpYIXNbI72VzzeEeUnauC9I5kTomT4xQKZQzNtEGnw==} - '@swagger-api/apidom-reference@1.4.0': - resolution: {integrity: sha512-MRqShsNMzigiJxFNYWF4YyPfzVNyEAm1E4DngYENuZxV+Hf+2Zri+DEk3FL6JPoDLLx/UYO1EB9S8VuPZeNT6g==} + '@swagger-api/apidom-reference@1.5.0': + resolution: {integrity: sha512-6PrvPqAecUcYkMlJGK4g7wEcpSAMTAOysxGAxfF42JRD6pAkAqAiGAzCGrucZXzK9UyRqfchloUlv/5fqnQ+CQ==} '@swaggerexpert/cookie@2.0.2': resolution: {integrity: sha512-DPI8YJ0Vznk4CT+ekn3rcFNq1uQwvUHZhH6WvTSPD0YKBIlMS9ur2RYKghXuxxOiqOam/i4lHJH4xTIiTgs3Mg==} @@ -10778,11 +10907,20 @@ packages: '@tanstack/query-persist-client-core@4.27.0': resolution: {integrity: sha512-A+dPA7zG0MJOMDeBc/2WcKXW4wV2JMkeBVydobPW9G02M4q0yAj7vI+7SmM2dFuXyIvxXp4KulCywN6abRKDSQ==} + '@tanstack/query-persist-client-core@5.77.1': + resolution: {integrity: sha512-m7VJE03p4TbfBBzkZAFwhmfLIM4W0ufDnbhQ7oIOy430+AEBKfSX9KJzftSgyQ94TjVqQO4QtfUoprhwOrMAlA==} + '@tanstack/react-query-persist-client@4.28.0': resolution: {integrity: sha512-xNpi3YdPOQIyYkKhByYDqTlyCeqICWFhV5PWkoVxYfzlRK6HYX4s+9Int407jEvhBz9cGC4OaL7rd6bynCFrYg==} peerDependencies: '@tanstack/react-query': 4.28.0 + '@tanstack/react-query-persist-client@5.77.1': + resolution: {integrity: sha512-6cOqoZdSYaMxCI8ObtDeikzGsxaFojo8CcbZoUyGs78SphF0pTSi8dTk2p24OP8v0OZuNXC+qDmRMo6sJznAIA==} + peerDependencies: + '@tanstack/react-query': ^5.77.1 + react: ^18 || ^19 + '@tanstack/react-query@4.0.10': resolution: {integrity: sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==} peerDependencies: @@ -11285,8 +11423,8 @@ packages: '@types/pretty-hrtime@1.0.3': resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} - '@types/prismjs@1.26.6': - resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -11788,8 +11926,8 @@ packages: resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==} engines: {node: '>= 20'} - '@vercel/oidc@3.0.5': - resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} '@vitest/expect@2.0.5': @@ -12251,8 +12389,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - ai@6.0.7: - resolution: {integrity: sha512-kLzSXHdW6cAcb2mFSIfkbfzxYqqjrUnyhrB1sg855qlC+6XkLI8hmwFE8f/4SnjmtcTDOnkIaVjWoO5i5Ir0bw==} + ai@6.0.86: + resolution: {integrity: sha512-U2W2LBCHA/pr0Ui7vmmsjBiLEzBbZF3yVHNy7Rbzn7IX+SvoQPFM5rN74hhfVzZoE8zBuGD4nLLk+j0elGacvQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -13156,8 +13294,8 @@ packages: balanced-match@2.0.0: resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} - balanced-match@4.0.2: - resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} + balanced-match@4.0.3: + resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} engines: {node: 20 || >=22} base16@1.0.0: @@ -13201,6 +13339,9 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary-extensions@1.13.1: resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} engines: {node: '>=0.10.0'} @@ -13255,8 +13396,8 @@ packages: bonjour-service@1.3.0: resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} - bonjour@3.5.0: - resolution: {integrity: sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==} + bonjour@3.5.1: + resolution: {integrity: sha512-xONzj4PfpPJw6xSqCcT2SmQkBOXpUINUz3o3qXcWJwYlXbkZNcNaUae0o5lle7tKt4HHV6dTgkIRhAXZ3nBMsQ==} boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -13570,11 +13711,11 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-db@1.0.30001769: - resolution: {integrity: sha512-YUJlOqWziYAdXSnL60FU2Won9HqW/IY77M8xgDFMRj5+StEnycCnlKvORrMPAevdyfYd8W6pjQ2XaEiMyDocWA==} + caniuse-db@1.0.30001770: + resolution: {integrity: sha512-i1X+jYz8PDS/HHEaTN8YgX1G5yw2sPFVl00AlWnpz0lX3MLnXtowKCIOBZIQ05p24tLG6WUCP7ygi3TTXRuJ/g==} - caniuse-lite@1.0.30001769: - resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + caniuse-lite@1.0.30001770: + resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==} canvas@3.2.1: resolution: {integrity: sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==} @@ -14373,8 +14514,8 @@ packages: webpack: optional: true - css-loader@7.1.3: - resolution: {integrity: sha512-frbERmjT0UC5lMheWpJmMilnt9GEhbZJN/heUb7/zaJYeIzj5St9HvDcfshzzOqbsS+rYpMk++2SD3vGETDSyA==} + css-loader@7.1.2: + resolution: {integrity: sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==} engines: {node: '>= 18.12.0'} peerDependencies: '@rspack/core': 0.x || 1.x @@ -14832,9 +14973,6 @@ packages: dns-equal@1.0.0: resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==} - dns-packet@1.3.4: - resolution: {integrity: sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==} - dns-packet@5.6.1: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} @@ -15680,8 +15818,8 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-parser@5.3.4: - resolution: {integrity: sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==} + fast-xml-parser@5.3.6: + resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==} hasBin: true fastest-levenshtein@1.0.16: @@ -15756,6 +15894,10 @@ packages: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -16160,6 +16302,14 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + gaxios@7.1.3: + resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -16416,6 +16566,14 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + google-auth-library@10.5.0: + resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -16468,6 +16626,10 @@ packages: growly@1.3.0: resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + gtoken@8.0.0: + resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} + engines: {node: '>=18'} + gud@1.0.0: resolution: {integrity: sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==} @@ -16682,8 +16844,8 @@ packages: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} - hono@4.11.9: - resolution: {integrity: sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==} + hono@4.11.10: + resolution: {integrity: sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==} engines: {node: '>=16.9.0'} hookified@1.15.1: @@ -17527,8 +17689,8 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} - is-wsl@3.1.0: - resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} is2@2.0.9: @@ -18292,6 +18454,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -18456,8 +18621,8 @@ packages: resolution: {integrity: sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==} engines: {node: '>=4'} - launch-editor@2.12.0: - resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} + launch-editor@2.13.0: + resolution: {integrity: sha512-u+9asUHMJ99lA15VRMXw5XKfySFR9dGXwgsgS14YTbUq3GITP58mIM32At90P5fZ+MUId5Yw+IwI/yKub7jnCQ==} lazy-cache@1.0.4: resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==} @@ -19276,8 +19441,8 @@ packages: minimalistic-crypto-utils@1.0.1: resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} - minimatch@10.2.0: - resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==} + minimatch@10.2.1: + resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} engines: {node: 20 || >=22} minimatch@3.0.3: @@ -19451,10 +19616,6 @@ packages: multicast-dns-service-types@1.1.0: resolution: {integrity: sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==} - multicast-dns@6.2.3: - resolution: {integrity: sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==} - hasBin: true - multicast-dns@7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true @@ -19574,6 +19735,10 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + node-fetch-commonjs@3.3.2: resolution: {integrity: sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -21034,8 +21199,8 @@ packages: prosemirror-keymap@1.2.2: resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} - prosemirror-markdown@1.13.4: - resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} + prosemirror-markdown@1.13.2: + resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==} prosemirror-model@1.25.4: resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} @@ -21996,8 +22161,9 @@ packages: resolve@1.6.0: resolution: {integrity: sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==} - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} hasBin: true responselike@3.0.0: @@ -22245,7 +22411,7 @@ packages: resolution: {integrity: sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==} engines: {node: '>= 18.12.0'} peerDependencies: - '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 + '@rspack/core': 0.x || 1.x node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 sass: ^1.3.0 sass-embedded: '*' @@ -24044,8 +24210,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.21.0: - resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} engines: {node: '>=20.18.1'} unfetch@4.2.0: @@ -24841,8 +25007,8 @@ packages: webpack-sources@1.4.3: resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.2.2: @@ -25350,11 +25516,11 @@ snapshots: '@adobe/css-tools@4.4.4': {} - '@ai-sdk/amazon-bedrock@4.0.4(zod@4.1.8)': + '@ai-sdk/amazon-bedrock@4.0.60(zod@4.1.8)': dependencies: - '@ai-sdk/anthropic': 3.0.2(zod@4.1.8) - '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.1.8) + '@ai-sdk/anthropic': 3.0.44(zod@4.1.8) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@4.1.8) '@smithy/eventstream-codec': 4.2.8 '@smithy/util-utf8': 4.2.0 aws4fetch: 1.0.20 @@ -25366,10 +25532,10 @@ snapshots: '@ai-sdk/provider-utils': 3.0.12(zod@4.1.11) zod: 4.1.11 - '@ai-sdk/anthropic@3.0.2(zod@4.1.8)': + '@ai-sdk/anthropic@3.0.44(zod@4.1.8)': dependencies: - '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.1.8) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@4.1.8) zod: 4.1.8 '@ai-sdk/gateway@2.0.0(zod@4.1.11)': @@ -25379,11 +25545,28 @@ snapshots: '@vercel/oidc': 3.0.3 zod: 4.1.11 - '@ai-sdk/gateway@3.0.6(zod@4.1.8)': + '@ai-sdk/gateway@3.0.46(zod@4.1.8)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@4.1.8) + '@vercel/oidc': 3.1.0 + zod: 4.1.8 + + '@ai-sdk/google-vertex@4.0.58(zod@4.1.8)': + dependencies: + '@ai-sdk/anthropic': 3.0.44(zod@4.1.8) + '@ai-sdk/google': 3.0.29(zod@4.1.8) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@4.1.8) + google-auth-library: 10.5.0 + zod: 4.1.8 + transitivePeerDependencies: + - supports-color + + '@ai-sdk/google@3.0.29(zod@4.1.8)': dependencies: - '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.1.8) - '@vercel/oidc': 3.0.5 + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@4.1.8) zod: 4.1.8 '@ai-sdk/provider-utils@3.0.12(zod@4.1.11)': @@ -25393,9 +25576,9 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.1.11 - '@ai-sdk/provider-utils@4.0.2(zod@4.1.8)': + '@ai-sdk/provider-utils@4.0.15(zod@4.1.8)': dependencies: - '@ai-sdk/provider': 3.0.1 + '@ai-sdk/provider': 3.0.8 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 zod: 4.1.8 @@ -25404,7 +25587,7 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/provider@3.0.1': + '@ai-sdk/provider@3.0.8': dependencies: json-schema: 0.4.0 @@ -25503,7 +25686,7 @@ snapshots: '@aws-sdk/util-user-agent-node': 3.816.0 '@aws-sdk/xml-builder': 3.804.0 '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.23.0 + '@smithy/core': 3.23.2 '@smithy/eventstream-serde-browser': 4.2.8 '@smithy/eventstream-serde-config-resolver': 4.3.8 '@smithy/eventstream-serde-node': 4.2.8 @@ -25514,21 +25697,21 @@ snapshots: '@smithy/invalid-dependency': 4.2.8 '@smithy/md5-js': 4.2.8 '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.14 - '@smithy/middleware-retry': 4.4.31 + '@smithy/middleware-endpoint': 4.4.16 + '@smithy/middleware-retry': 4.4.33 '@smithy/middleware-serde': 4.2.9 '@smithy/middleware-stack': 4.2.8 '@smithy/node-config-provider': 4.3.8 '@smithy/node-http-handler': 4.4.10 '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.30 - '@smithy/util-defaults-mode-node': 4.2.33 + '@smithy/util-defaults-mode-browser': 4.3.32 + '@smithy/util-defaults-mode-node': 4.2.35 '@smithy/util-endpoints': 3.2.8 '@smithy/util-middleware': 4.2.8 '@smithy/util-retry': 4.2.8 @@ -25554,26 +25737,26 @@ snapshots: '@aws-sdk/util-user-agent-browser': 3.804.0 '@aws-sdk/util-user-agent-node': 3.816.0 '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.23.0 + '@smithy/core': 3.23.2 '@smithy/fetch-http-handler': 5.3.9 '@smithy/hash-node': 4.2.8 '@smithy/invalid-dependency': 4.2.8 '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.14 - '@smithy/middleware-retry': 4.4.31 + '@smithy/middleware-endpoint': 4.4.16 + '@smithy/middleware-retry': 4.4.33 '@smithy/middleware-serde': 4.2.9 '@smithy/middleware-stack': 4.2.8 '@smithy/node-config-provider': 4.3.8 '@smithy/node-http-handler': 4.4.10 '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.30 - '@smithy/util-defaults-mode-node': 4.2.33 + '@smithy/util-defaults-mode-browser': 4.3.32 + '@smithy/util-defaults-mode-node': 4.2.35 '@smithy/util-endpoints': 3.2.8 '@smithy/util-middleware': 4.2.8 '@smithy/util-retry': 4.2.8 @@ -25585,15 +25768,15 @@ snapshots: '@aws-sdk/core@3.816.0': dependencies: '@aws-sdk/types': 3.804.0 - '@smithy/core': 3.23.0 + '@smithy/core': 3.23.2 '@smithy/node-config-provider': 4.3.8 '@smithy/property-provider': 4.2.8 '@smithy/protocol-http': 5.3.8 '@smithy/signature-v4': 5.3.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 '@smithy/util-middleware': 4.2.8 - fast-xml-parser: 5.3.4 + fast-xml-parser: 5.3.6 tslib: 2.8.1 '@aws-sdk/credential-provider-env@3.816.0': @@ -25612,7 +25795,7 @@ snapshots: '@smithy/node-http-handler': 4.4.10 '@smithy/property-provider': 4.2.8 '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 '@smithy/util-stream': 4.5.12 tslib: 2.8.1 @@ -25749,11 +25932,11 @@ snapshots: '@aws-sdk/core': 3.816.0 '@aws-sdk/types': 3.804.0 '@aws-sdk/util-arn-parser': 3.804.0 - '@smithy/core': 3.23.0 + '@smithy/core': 3.23.2 '@smithy/node-config-provider': 4.3.8 '@smithy/protocol-http': 5.3.8 '@smithy/signature-v4': 5.3.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 '@smithy/util-config-provider': 4.2.0 '@smithy/util-middleware': 4.2.8 @@ -25772,7 +25955,7 @@ snapshots: '@aws-sdk/core': 3.816.0 '@aws-sdk/types': 3.804.0 '@aws-sdk/util-endpoints': 3.808.0 - '@smithy/core': 3.23.0 + '@smithy/core': 3.23.2 '@smithy/protocol-http': 5.3.8 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -25792,26 +25975,26 @@ snapshots: '@aws-sdk/util-user-agent-browser': 3.804.0 '@aws-sdk/util-user-agent-node': 3.816.0 '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.23.0 + '@smithy/core': 3.23.2 '@smithy/fetch-http-handler': 5.3.9 '@smithy/hash-node': 4.2.8 '@smithy/invalid-dependency': 4.2.8 '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.14 - '@smithy/middleware-retry': 4.4.31 + '@smithy/middleware-endpoint': 4.4.16 + '@smithy/middleware-retry': 4.4.33 '@smithy/middleware-serde': 4.2.9 '@smithy/middleware-stack': 4.2.8 '@smithy/node-config-provider': 4.3.8 '@smithy/node-http-handler': 4.4.10 '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.30 - '@smithy/util-defaults-mode-node': 4.2.33 + '@smithy/util-defaults-mode-browser': 4.3.32 + '@smithy/util-defaults-mode-node': 4.2.35 '@smithy/util-endpoints': 3.2.8 '@smithy/util-middleware': 4.2.8 '@smithy/util-retry': 4.2.8 @@ -26421,7 +26604,7 @@ snapshots: '@babel/plugin-proposal-object-rest-spread@7.12.1(@babel/core@7.12.9)': dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.10.4 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.12.9) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.12.9) @@ -27845,7 +28028,7 @@ snapshots: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 '@lezer/common': 1.5.1 - '@lezer/css': 1.3.0 + '@lezer/css': 1.3.1 '@codemirror/lang-go@6.0.1': dependencies: @@ -27864,7 +28047,7 @@ snapshots: '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.8 '@lezer/common': 1.5.1 - '@lezer/css': 1.3.0 + '@lezer/css': 1.3.1 '@lezer/html': 1.3.13 '@codemirror/lang-java@6.0.2': @@ -28048,7 +28231,7 @@ snapshots: '@codemirror/view': 6.38.8 crelt: 1.0.6 - '@codemirror/merge@6.11.2': + '@codemirror/merge@6.12.0': dependencies: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 @@ -28972,9 +29155,9 @@ snapshots: react-dom: 18.2.0(react@18.2.0) use-sync-external-store: 1.6.0(react@18.2.0) - '@hono/node-server@1.19.9(hono@4.11.9)': + '@hono/node-server@1.19.9(hono@4.11.10)': dependencies: - hono: 4.11.9 + hono: 4.11.10 '@hookform/resolvers@2.8.0(react-hook-form@7.56.4(react@18.2.0))': dependencies: @@ -29039,7 +29222,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -29048,7 +29231,7 @@ snapshots: '@jest/console@30.0.0': dependencies: '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 jest-message-util: 30.0.0 jest-util: 30.0.0 @@ -29090,6 +29273,41 @@ snapshots: - supports-color - utf-8-validate + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 16.18.126 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@16.18.126)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3))': dependencies: '@jest/console': 29.7.0 @@ -29097,14 +29315,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.15.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@16.18.126)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -29175,14 +29393,14 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-mock: 29.7.0 '@jest/environment@30.0.0': dependencies: '@jest/fake-timers': 30.0.0 '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-mock: 30.0.0 '@jest/expect-utils@29.7.0': @@ -29223,7 +29441,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -29232,7 +29450,7 @@ snapshots: dependencies: '@jest/types': 30.0.0 '@sinonjs/fake-timers': 13.0.5 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-message-util: 30.0.0 jest-mock: 30.0.0 jest-util: 30.0.0 @@ -29267,12 +29485,12 @@ snapshots: '@jest/pattern@30.0.0': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-regex-util: 30.0.0 '@jest/pattern@30.0.1': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-regex-util: 30.0.1 '@jest/reporters@25.5.1': @@ -29314,7 +29532,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -29343,7 +29561,7 @@ snapshots: '@jest/transform': 30.0.0 '@jest/types': 30.0.0 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 @@ -29540,7 +29758,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/yargs': 15.0.20 chalk: 4.1.2 @@ -29549,7 +29767,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -29569,7 +29787,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -29904,7 +30122,7 @@ snapshots: '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.8 - '@lezer/css@1.3.0': + '@lezer/css@1.3.1': dependencies: '@lezer/common': 1.5.1 '@lezer/highlight': 1.2.3 @@ -30043,7 +30261,7 @@ snapshots: dependencies: '@codemirror/lang-markdown': 6.5.0 '@codemirror/language-data': 6.5.2 - '@codemirror/merge': 6.11.2 + '@codemirror/merge': 6.12.0 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.8 '@codesandbox/sandpack-react': 2.20.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -30294,7 +30512,7 @@ snapshots: '@modelcontextprotocol/sdk@1.26.0': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.9) + '@hono/node-server': 1.19.9(hono@4.11.10) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -30304,7 +30522,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1 express-rate-limit: 8.2.1(express@5.2.1) - hono: 4.11.9 + hono: 4.11.10 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -30638,7 +30856,7 @@ snapshots: optionalDependencies: '@types/webpack': 5.28.5(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@6.0.1) type-fest: 4.41.0 - webpack-dev-server: 5.2.3(webpack-cli@6.0.1)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@6.0.1)(webpack@5.104.1) webpack-hot-middleware: 2.26.1 '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@5.28.5(@swc/core@1.15.11(@swc/helpers@0.5.18)))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.3(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))))(webpack-hot-middleware@2.26.1)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18)))': @@ -30658,7 +30876,7 @@ snapshots: webpack-dev-server: 5.2.3(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))) webpack-hot-middleware: 2.26.1 - '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@5.28.5(webpack-cli@4.10.0))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.3)(webpack-hot-middleware@2.26.1)(webpack@5.105.2)': + '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@5.28.5(webpack-cli@4.10.0))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.3)(webpack-hot-middleware@2.26.1)(webpack@5.104.1)': dependencies: ansi-html: 0.0.9 core-js-pure: 3.48.0 @@ -30668,14 +30886,14 @@ snapshots: react-refresh: 0.11.0 schema-utils: 4.3.3 source-map: 0.7.6 - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) optionalDependencies: '@types/webpack': 5.28.5(webpack-cli@4.10.0) type-fest: 4.41.0 - webpack-dev-server: 5.2.3(webpack-cli@4.10.0)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@4.10.0)(webpack@5.104.1) webpack-hot-middleware: 2.26.1 - '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@5.28.5(webpack-cli@5.1.4))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.3)(webpack-hot-middleware@2.26.1)(webpack@5.105.2)': + '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@5.28.5(webpack-cli@5.1.4))(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.3)(webpack-hot-middleware@2.26.1)(webpack@5.104.1)': dependencies: ansi-html: 0.0.9 core-js-pure: 3.48.0 @@ -30685,14 +30903,14 @@ snapshots: react-refresh: 0.11.0 schema-utils: 4.3.3 source-map: 0.7.6 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) optionalDependencies: '@types/webpack': 5.28.5(webpack-cli@5.1.4) type-fest: 4.41.0 - webpack-dev-server: 5.2.3(webpack-cli@5.1.4)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@5.1.4)(webpack@5.104.1) webpack-hot-middleware: 2.26.1 - '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@5.28.5)(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.3(webpack@5.105.2))(webpack-hot-middleware@2.26.1)(webpack@5.105.2)': + '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@5.28.5)(react-refresh@0.11.0)(type-fest@4.41.0)(webpack-dev-server@5.2.3(webpack@5.104.1))(webpack-hot-middleware@2.26.1)(webpack@5.104.1)': dependencies: ansi-html: 0.0.9 core-js-pure: 3.48.0 @@ -30702,7 +30920,7 @@ snapshots: react-refresh: 0.11.0 schema-utils: 4.3.3 source-map: 0.7.6 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) optionalDependencies: '@types/webpack': 5.28.5(webpack-cli@5.1.4) type-fest: 4.41.0 @@ -30718,11 +30936,11 @@ snapshots: react-refresh: 0.11.0 schema-utils: 4.3.3 source-map: 0.7.6 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) optionalDependencies: '@types/webpack': 5.28.5(webpack-cli@5.1.4) type-fest: 4.41.0 - webpack-dev-server: 5.2.3(webpack-cli@5.1.4)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@5.1.4)(webpack@5.104.1) webpack-hot-middleware: 2.26.1 '@popperjs/core@2.11.8': {} @@ -32496,12 +32714,12 @@ snapshots: dependencies: react: 18.2.0 - '@redhat-developer/locators@1.18.1(@redhat-developer/page-objects@1.18.1(selenium-webdriver@4.40.0)(typescript@5.8.3))(selenium-webdriver@4.40.0)': + '@redhat-developer/locators@1.19.0(@redhat-developer/page-objects@1.19.0(selenium-webdriver@4.40.0)(typescript@5.8.3))(selenium-webdriver@4.40.0)': dependencies: - '@redhat-developer/page-objects': 1.18.1(selenium-webdriver@4.40.0)(typescript@5.8.3) + '@redhat-developer/page-objects': 1.19.0(selenium-webdriver@4.40.0)(typescript@5.8.3) selenium-webdriver: 4.40.0 - '@redhat-developer/page-objects@1.18.1(selenium-webdriver@4.40.0)(typescript@5.8.3)': + '@redhat-developer/page-objects@1.19.0(selenium-webdriver@4.40.0)(typescript@5.8.3)': dependencies: clipboardy: 5.3.0 clone-deep: 4.0.1 @@ -32757,7 +32975,7 @@ snapshots: '@sentry/webpack-plugin@1.20.1(encoding@0.1.13)': dependencies: '@sentry/cli': 1.77.3(encoding@0.1.13) - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 transitivePeerDependencies: - encoding - supports-color @@ -32837,7 +33055,7 @@ snapshots: '@smithy/util-middleware': 4.2.8 tslib: 2.8.1 - '@smithy/core@3.23.0': + '@smithy/core@3.23.2': dependencies: '@smithy/middleware-serde': 4.2.9 '@smithy/protocol-http': 5.3.8 @@ -32941,9 +33159,9 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.14': + '@smithy/middleware-endpoint@4.4.16': dependencies: - '@smithy/core': 3.23.0 + '@smithy/core': 3.23.2 '@smithy/middleware-serde': 4.2.9 '@smithy/node-config-provider': 4.3.8 '@smithy/shared-ini-file-loader': 4.4.3 @@ -32952,12 +33170,12 @@ snapshots: '@smithy/util-middleware': 4.2.8 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.31': + '@smithy/middleware-retry@4.4.33': dependencies: '@smithy/node-config-provider': 4.3.8 '@smithy/protocol-http': 5.3.8 '@smithy/service-error-classification': 4.2.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 '@smithy/util-middleware': 4.2.8 '@smithy/util-retry': 4.2.8 @@ -33031,10 +33249,10 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/smithy-client@4.11.3': + '@smithy/smithy-client@4.11.5': dependencies: - '@smithy/core': 3.23.0 - '@smithy/middleware-endpoint': 4.4.14 + '@smithy/core': 3.23.2 + '@smithy/middleware-endpoint': 4.4.16 '@smithy/middleware-stack': 4.2.8 '@smithy/protocol-http': 5.3.8 '@smithy/types': 4.12.0 @@ -33079,20 +33297,20 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.30': + '@smithy/util-defaults-mode-browser@4.3.32': dependencies: '@smithy/property-provider': 4.2.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.33': + '@smithy/util-defaults-mode-node@4.2.35': dependencies: '@smithy/config-resolver': 4.4.6 '@smithy/credential-provider-imds': 4.2.8 '@smithy/node-config-provider': 4.3.8 '@smithy/property-provider': 4.2.8 - '@smithy/smithy-client': 4.11.3 + '@smithy/smithy-client': 4.11.5 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -33669,7 +33887,7 @@ snapshots: '@storybook/builder-webpack5': 6.5.9(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) transitivePeerDependencies: - '@storybook/mdx2-csf' - eslint @@ -34788,8 +35006,8 @@ snapshots: terser-webpack-plugin: 5.3.14(webpack@5.104.1) ts-dedent: 2.2.0 util-deprecate: 1.0.2 - webpack: 5.105.2(webpack-cli@4.10.0) - webpack-dev-middleware: 4.3.0(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@4.10.0) + webpack-dev-middleware: 4.3.0(webpack@5.104.1) webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.4.6 optionalDependencies: @@ -34873,7 +35091,7 @@ snapshots: browser-assert: 1.2.1 case-sensitive-paths-webpack-plugin: 2.4.0 constants-browserify: 1.0.0 - css-loader: 6.11.0(webpack@5.105.2) + css-loader: 6.11.0(webpack@5.104.1) express: 4.22.1 fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.104.1) fs-extra: 11.3.0 @@ -34947,7 +35165,7 @@ snapshots: case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 constants-browserify: 1.0.0 - css-loader: 6.11.0(webpack@5.105.2) + css-loader: 6.11.0(webpack@5.104.1) es-module-lexer: 1.7.0 fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.104.1) html-webpack-plugin: 5.6.6(webpack@5.104.1) @@ -34962,8 +35180,8 @@ snapshots: url: 0.11.4 util: 0.12.5 util-deprecate: 1.0.2 - webpack: 5.105.2(webpack-cli@5.1.4) - webpack-dev-middleware: 6.1.3(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@5.1.4) + webpack-dev-middleware: 6.1.3(webpack@5.104.1) webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.6.2 optionalDependencies: @@ -35524,7 +35742,7 @@ snapshots: optionalDependencies: typescript: 5.8.3 - '@storybook/core-client@6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.105.2)': + '@storybook/core-client@6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.104.1)': dependencies: '@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/channel-postmessage': 6.5.16 @@ -35660,7 +35878,7 @@ snapshots: ts-dedent: 2.2.0 unfetch: 4.2.0 util-deprecate: 1.0.2 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) optionalDependencies: typescript: 4.9.4 @@ -36645,7 +36863,7 @@ snapshots: '@storybook/core-server': 6.5.9(@storybook/builder-webpack5@6.5.9(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0))(@storybook/manager-webpack5@6.5.9(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0))(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) optionalDependencies: '@storybook/builder-webpack5': 6.5.9(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) '@storybook/manager-webpack5': 6.5.9(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) @@ -36667,7 +36885,7 @@ snapshots: '@storybook/core-server': 6.5.9(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@4.9.4) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) optionalDependencies: typescript: 4.9.4 transitivePeerDependencies: @@ -37247,7 +37465,7 @@ snapshots: read-pkg-up: 7.0.1 regenerator-runtime: 0.13.11 resolve-from: 5.0.0 - style-loader: 2.0.0(webpack@5.105.2) + style-loader: 2.0.0(webpack@5.104.1) telejson: 6.0.8 terser-webpack-plugin: 5.3.14(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack@5.104.1) ts-dedent: 2.2.0 @@ -37297,13 +37515,13 @@ snapshots: read-pkg-up: 7.0.1 regenerator-runtime: 0.13.11 resolve-from: 5.0.0 - style-loader: 2.0.0(webpack@5.105.2) + style-loader: 2.0.0(webpack@5.104.1) telejson: 6.0.8 terser-webpack-plugin: 5.3.14(webpack@5.104.1) ts-dedent: 2.2.0 util-deprecate: 1.0.2 - webpack: 5.105.2(webpack-cli@4.10.0) - webpack-dev-middleware: 4.3.0(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@4.10.0) + webpack-dev-middleware: 4.3.0(webpack@5.104.1) webpack-virtual-modules: 0.4.6 optionalDependencies: typescript: 5.8.3 @@ -37493,7 +37711,7 @@ snapshots: semver: 7.7.4 storybook: 8.6.14(prettier@3.5.3) tsconfig-paths: 4.2.0 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -37649,7 +37867,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.105.2)': + '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.104.1)': dependencies: debug: 4.4.3(supports-color@8.1.1) endent: 2.1.0 @@ -37677,7 +37895,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.105.2)': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.104.1)': dependencies: debug: 4.4.3(supports-color@8.1.1) endent: 2.1.0 @@ -37687,7 +37905,7 @@ snapshots: react-docgen-typescript: 2.4.0(typescript@5.8.3) tslib: 2.8.1 typescript: 5.8.3 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) transitivePeerDependencies: - supports-color @@ -37933,7 +38151,7 @@ snapshots: '@storybook/csf': 0.0.2--canary.4566f4d.1 '@storybook/docs-tools': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/node-logger': 6.5.16 - '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.105.2) + '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.104.1) '@storybook/semver': 7.3.2 '@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/estree': 0.0.51 @@ -38061,7 +38279,7 @@ snapshots: '@storybook/csf': 0.0.2--canary.4566f4d.1 '@storybook/docs-tools': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/node-logger': 6.5.16 - '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.105.2) + '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0(typescript@5.8.3)(webpack@5.104.1) '@storybook/semver': 7.3.2 '@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/estree': 0.0.51 @@ -38151,7 +38369,7 @@ snapshots: require-from-string: 2.0.2 ts-dedent: 2.2.0 util-deprecate: 1.0.2 - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) optionalDependencies: '@babel/core': 7.27.1 '@storybook/builder-webpack5': 6.5.9(eslint@9.39.2(jiti@2.6.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack-cli@4.10.0) @@ -38216,7 +38434,7 @@ snapshots: require-from-string: 2.0.2 ts-dedent: 2.2.0 util-deprecate: 1.0.2 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) optionalDependencies: '@babel/core': 7.29.0 typescript: 4.9.4 @@ -38794,20 +39012,20 @@ snapshots: regenerator-runtime: 0.13.11 resolve-from: 5.0.0 - '@swagger-api/apidom-ast@1.4.0': + '@swagger-api/apidom-ast@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-error': 1.4.0 + '@swagger-api/apidom-error': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) unraw: 3.0.0 - '@swagger-api/apidom-core@1.4.0': + '@swagger-api/apidom-core@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-ast': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 + '@swagger-api/apidom-ast': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 '@types/ramda': 0.30.2 minim: 0.23.8 ramda: 0.30.1 @@ -38815,246 +39033,246 @@ snapshots: short-unique-id: 5.3.2 ts-mixer: 6.0.4 - '@swagger-api/apidom-error@1.4.0': + '@swagger-api/apidom-error@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-json-pointer@1.4.0': + '@swagger-api/apidom-json-pointer@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 '@swaggerexpert/json-pointer': 2.10.2 - '@swagger-api/apidom-ns-api-design-systems@1.4.0': + '@swagger-api/apidom-ns-api-design-systems@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-1': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 optional: true - '@swagger-api/apidom-ns-arazzo-1@1.4.0': + '@swagger-api/apidom-ns-arazzo-1@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-json-schema-2020-12': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-json-schema-2020-12': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 optional: true - '@swagger-api/apidom-ns-asyncapi-2@1.4.0': + '@swagger-api/apidom-ns-asyncapi-2@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-json-schema-draft-7': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-json-schema-draft-7': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 optional: true - '@swagger-api/apidom-ns-asyncapi-3@1.4.0': + '@swagger-api/apidom-ns-asyncapi-3@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-asyncapi-2': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-asyncapi-2': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 optional: true - '@swagger-api/apidom-ns-json-schema-2019-09@1.4.0': + '@swagger-api/apidom-ns-json-schema-2019-09@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 - '@swagger-api/apidom-ns-json-schema-draft-7': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 + '@swagger-api/apidom-ns-json-schema-draft-7': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 - '@swagger-api/apidom-ns-json-schema-2020-12@1.4.0': + '@swagger-api/apidom-ns-json-schema-2020-12@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 - '@swagger-api/apidom-ns-json-schema-2019-09': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 + '@swagger-api/apidom-ns-json-schema-2019-09': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 - '@swagger-api/apidom-ns-json-schema-draft-4@1.4.0': + '@swagger-api/apidom-ns-json-schema-draft-4@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-ast': 1.4.0 - '@swagger-api/apidom-core': 1.4.0 + '@swagger-api/apidom-ast': 1.5.0 + '@swagger-api/apidom-core': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 - '@swagger-api/apidom-ns-json-schema-draft-6@1.4.0': + '@swagger-api/apidom-ns-json-schema-draft-6@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 - '@swagger-api/apidom-ns-json-schema-draft-4': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 + '@swagger-api/apidom-ns-json-schema-draft-4': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 - '@swagger-api/apidom-ns-json-schema-draft-7@1.4.0': + '@swagger-api/apidom-ns-json-schema-draft-7@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 - '@swagger-api/apidom-ns-json-schema-draft-6': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 + '@swagger-api/apidom-ns-json-schema-draft-6': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 - '@swagger-api/apidom-ns-openapi-2@1.4.0': + '@swagger-api/apidom-ns-openapi-2@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 - '@swagger-api/apidom-ns-json-schema-draft-4': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 + '@swagger-api/apidom-ns-json-schema-draft-4': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 optional: true - '@swagger-api/apidom-ns-openapi-3-0@1.4.0': + '@swagger-api/apidom-ns-openapi-3-0@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 - '@swagger-api/apidom-ns-json-schema-draft-4': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 + '@swagger-api/apidom-ns-json-schema-draft-4': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 - '@swagger-api/apidom-ns-openapi-3-1@1.4.0': + '@swagger-api/apidom-ns-openapi-3-1@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-ast': 1.4.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-json-pointer': 1.4.0 - '@swagger-api/apidom-ns-json-schema-2020-12': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-0': 1.4.0 + '@swagger-api/apidom-ast': 1.5.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-json-pointer': 1.5.0 + '@swagger-api/apidom-ns-json-schema-2020-12': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) ts-mixer: 6.0.4 - '@swagger-api/apidom-parser-adapter-api-design-systems-json@1.4.0': + '@swagger-api/apidom-parser-adapter-api-design-systems-json@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-api-design-systems': 1.4.0 - '@swagger-api/apidom-parser-adapter-json': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-api-design-systems': 1.5.0 + '@swagger-api/apidom-parser-adapter-json': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-api-design-systems-yaml@1.4.0': + '@swagger-api/apidom-parser-adapter-api-design-systems-yaml@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-api-design-systems': 1.4.0 - '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-api-design-systems': 1.5.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-arazzo-json-1@1.4.0': + '@swagger-api/apidom-parser-adapter-arazzo-json-1@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-arazzo-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-json': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-arazzo-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-json': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-arazzo-yaml-1@1.4.0': + '@swagger-api/apidom-parser-adapter-arazzo-yaml-1@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-arazzo-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-arazzo-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-asyncapi-json-2@1.4.0': + '@swagger-api/apidom-parser-adapter-asyncapi-json-2@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-asyncapi-2': 1.4.0 - '@swagger-api/apidom-parser-adapter-json': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-asyncapi-2': 1.5.0 + '@swagger-api/apidom-parser-adapter-json': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-asyncapi-json-3@1.4.0': + '@swagger-api/apidom-parser-adapter-asyncapi-json-3@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-asyncapi-3': 1.4.0 - '@swagger-api/apidom-parser-adapter-json': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-asyncapi-3': 1.5.0 + '@swagger-api/apidom-parser-adapter-json': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@1.4.0': + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-asyncapi-2': 1.4.0 - '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-asyncapi-2': 1.5.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3@1.4.0': + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-asyncapi-3': 1.4.0 - '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-asyncapi-3': 1.5.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-json@1.4.0': + '@swagger-api/apidom-parser-adapter-json@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-ast': 1.4.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 + '@swagger-api/apidom-ast': 1.5.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) @@ -39063,78 +39281,78 @@ snapshots: web-tree-sitter: 0.24.5 optional: true - '@swagger-api/apidom-parser-adapter-openapi-json-2@1.4.0': + '@swagger-api/apidom-parser-adapter-openapi-json-2@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-openapi-2': 1.4.0 - '@swagger-api/apidom-parser-adapter-json': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-openapi-2': 1.5.0 + '@swagger-api/apidom-parser-adapter-json': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-openapi-json-3-0@1.4.0': + '@swagger-api/apidom-parser-adapter-openapi-json-3-0@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-0': 1.4.0 - '@swagger-api/apidom-parser-adapter-json': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.5.0 + '@swagger-api/apidom-parser-adapter-json': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-openapi-json-3-1@1.4.0': + '@swagger-api/apidom-parser-adapter-openapi-json-3-1@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-json': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-json': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-openapi-yaml-2@1.4.0': + '@swagger-api/apidom-parser-adapter-openapi-yaml-2@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-openapi-2': 1.4.0 - '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-openapi-2': 1.5.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@1.4.0': + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-0': 1.4.0 - '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.5.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@1.4.0': + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.5.0 '@types/ramda': 0.30.2 ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optional: true - '@swagger-api/apidom-parser-adapter-yaml-1-2@1.4.0': + '@swagger-api/apidom-parser-adapter-yaml-1-2@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-ast': 1.4.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 + '@swagger-api/apidom-ast': 1.5.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 '@tree-sitter-grammars/tree-sitter-yaml': 0.7.1(tree-sitter@0.22.4) '@types/ramda': 0.30.2 ramda: 0.30.1 @@ -39143,11 +39361,11 @@ snapshots: web-tree-sitter: 0.24.5 optional: true - '@swagger-api/apidom-reference@1.4.0': + '@swagger-api/apidom-reference@1.5.0': dependencies: '@babel/runtime-corejs3': 7.29.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 '@types/ramda': 0.30.2 axios: 1.13.5 minimatch: 7.4.6 @@ -39155,28 +39373,28 @@ snapshots: ramda: 0.30.1 ramda-adjunct: 5.1.0(ramda@0.30.1) optionalDependencies: - '@swagger-api/apidom-json-pointer': 1.4.0 - '@swagger-api/apidom-ns-arazzo-1': 1.4.0 - '@swagger-api/apidom-ns-asyncapi-2': 1.4.0 - '@swagger-api/apidom-ns-openapi-2': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-0': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-api-design-systems-json': 1.4.0 - '@swagger-api/apidom-parser-adapter-api-design-systems-yaml': 1.4.0 - '@swagger-api/apidom-parser-adapter-arazzo-json-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-arazzo-yaml-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-asyncapi-json-2': 1.4.0 - '@swagger-api/apidom-parser-adapter-asyncapi-json-3': 1.4.0 - '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2': 1.4.0 - '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3': 1.4.0 - '@swagger-api/apidom-parser-adapter-json': 1.4.0 - '@swagger-api/apidom-parser-adapter-openapi-json-2': 1.4.0 - '@swagger-api/apidom-parser-adapter-openapi-json-3-0': 1.4.0 - '@swagger-api/apidom-parser-adapter-openapi-json-3-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-openapi-yaml-2': 1.4.0 - '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0': 1.4.0 - '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1': 1.4.0 - '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.4.0 + '@swagger-api/apidom-json-pointer': 1.5.0 + '@swagger-api/apidom-ns-arazzo-1': 1.5.0 + '@swagger-api/apidom-ns-asyncapi-2': 1.5.0 + '@swagger-api/apidom-ns-openapi-2': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-api-design-systems-json': 1.5.0 + '@swagger-api/apidom-parser-adapter-api-design-systems-yaml': 1.5.0 + '@swagger-api/apidom-parser-adapter-arazzo-json-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-arazzo-yaml-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-asyncapi-json-2': 1.5.0 + '@swagger-api/apidom-parser-adapter-asyncapi-json-3': 1.5.0 + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2': 1.5.0 + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3': 1.5.0 + '@swagger-api/apidom-parser-adapter-json': 1.5.0 + '@swagger-api/apidom-parser-adapter-openapi-json-2': 1.5.0 + '@swagger-api/apidom-parser-adapter-openapi-json-3-0': 1.5.0 + '@swagger-api/apidom-parser-adapter-openapi-json-3-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-openapi-yaml-2': 1.5.0 + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0': 1.5.0 + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1': 1.5.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.5.0 transitivePeerDependencies: - debug @@ -39263,11 +39481,21 @@ snapshots: dependencies: '@tanstack/query-core': 4.27.0 + '@tanstack/query-persist-client-core@5.77.1': + dependencies: + '@tanstack/query-core': 5.77.1 + '@tanstack/react-query-persist-client@4.28.0(@tanstack/react-query@4.28.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))': dependencies: '@tanstack/query-persist-client-core': 4.27.0 '@tanstack/react-query': 4.28.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@tanstack/react-query-persist-client@5.77.1(@tanstack/react-query@5.77.1(react@18.2.0))(react@18.2.0)': + dependencies: + '@tanstack/query-persist-client-core': 5.77.1 + '@tanstack/react-query': 5.77.1(react@18.2.0) + react: 18.2.0 + '@tanstack/react-query@4.0.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/query-core': 4.43.0 @@ -39414,7 +39642,7 @@ snapshots: '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 - minimatch: 10.2.0 + minimatch: 10.2.1 path-browserify: 1.0.1 '@tsconfig/node10@1.0.12': {} @@ -39479,7 +39707,7 @@ snapshots: '@types/bonjour@3.5.13': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/braces@3.0.5': {} @@ -39508,7 +39736,7 @@ snapshots: '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 4.19.8 - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/connect@3.4.38': dependencies: @@ -39516,7 +39744,7 @@ snapshots: '@types/cross-spawn@6.0.6': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/dagre@0.7.52': {} @@ -39602,7 +39830,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/handlebars@4.1.0': dependencies: @@ -39631,7 +39859,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/is-function@1.0.3': {} @@ -39671,7 +39899,7 @@ snapshots: '@types/jsdom@20.0.1': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -39819,7 +40047,7 @@ snapshots: '@types/pretty-hrtime@1.0.3': {} - '@types/prismjs@1.26.6': {} + '@types/prismjs@1.26.5': {} '@types/prop-types@15.7.15': {} @@ -39894,7 +40122,7 @@ snapshots: '@types/resolve@1.17.1': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/resolve@1.20.2': {} @@ -39918,7 +40146,7 @@ snapshots: '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/send@1.2.1': dependencies: @@ -39931,12 +40159,12 @@ snapshots: '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/send': 0.17.6 '@types/sockjs@0.3.36': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/source-list-map@0.1.6': {} @@ -40047,7 +40275,7 @@ snapshots: dependencies: '@types/node': 22.15.24 tapable: 2.3.0 - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) transitivePeerDependencies: - '@swc/core' - esbuild @@ -40059,7 +40287,7 @@ snapshots: dependencies: '@types/node': 22.15.24 tapable: 2.3.0 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) transitivePeerDependencies: - '@swc/core' - esbuild @@ -40070,7 +40298,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@types/ws@8.2.1': dependencies: @@ -40466,7 +40694,7 @@ snapshots: '@vercel/oidc@3.0.3': {} - '@vercel/oidc@3.0.5': {} + '@vercel/oidc@3.1.0': {} '@vitest/expect@2.0.5': dependencies: @@ -40904,67 +41132,67 @@ snapshots: '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0)(webpack@5.104.1)': dependencies: - webpack: 5.105.2(webpack-cli@4.10.0) - webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@4.10.0) + webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.104.1) - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.105.2)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.104.1)': dependencies: - webpack: 5.105.2(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1) - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.2)': + '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.104.1)': dependencies: - webpack: 5.105.2(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.104.1) '@webpack-cli/info@1.5.0(webpack-cli@4.10.0)': dependencies: envinfo: 7.21.0 - webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.105.2) + webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.104.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.105.2)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.104.1)': dependencies: - webpack: 5.105.2(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1) - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.2)': + '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.104.1)': dependencies: - webpack: 5.105.2(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.104.1) '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)': dependencies: - webpack-cli: 4.10.0(webpack@5.105.2) + webpack-cli: 4.10.0(webpack@5.104.1) '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)(webpack-dev-server@5.2.3)': dependencies: - webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.105.2) + webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.104.1) optionalDependencies: - webpack-dev-server: 5.2.3(webpack-cli@4.10.0)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@4.10.0)(webpack@5.104.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.3)(webpack@5.105.2)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.3)(webpack@5.104.1)': dependencies: - webpack: 5.105.2(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.2.3)(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1) optionalDependencies: - webpack-dev-server: 5.2.3(webpack-cli@5.1.4)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@5.1.4)(webpack@5.104.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.105.2)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.104.1)': dependencies: - webpack: 5.105.2(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.104.1) - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.3)(webpack@5.105.2)': + '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.3)(webpack@5.104.1)': dependencies: webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-dev-server@5.2.3)(webpack@5.104.1) optionalDependencies: - webpack-dev-server: 5.2.3(webpack-cli@6.0.1)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@6.0.1)(webpack@5.104.1) - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.105.2)': + '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.104.1)': dependencies: - webpack: 5.105.2(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.104.1) '@xmldom/xmldom@0.7.13': {} @@ -41101,11 +41329,11 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.1.11 - ai@6.0.7(zod@4.1.8): + ai@6.0.86(zod@4.1.8): dependencies: - '@ai-sdk/gateway': 3.0.6(zod@4.1.8) - '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.1.8) + '@ai-sdk/gateway': 3.0.46(zod@4.1.8) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.15(zod@4.1.8) '@opentelemetry/api': 1.9.0 zod: 4.1.8 @@ -41528,7 +41756,7 @@ snapshots: autoprefixer@10.4.19(postcss@8.5.3): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -41538,7 +41766,7 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.4): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -41548,7 +41776,7 @@ snapshots: autoprefixer@6.7.7: dependencies: browserslist: 1.7.7 - caniuse-db: 1.0.30001769 + caniuse-db: 1.0.30001770 normalize-range: 0.1.2 num2fraction: 1.2.2 postcss: 5.2.18 @@ -41557,7 +41785,7 @@ snapshots: autoprefixer@7.1.6: dependencies: browserslist: 2.11.3 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 normalize-range: 0.1.2 num2fraction: 1.2.2 postcss: 6.0.23 @@ -41566,7 +41794,7 @@ snapshots: autoprefixer@9.8.8: dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 normalize-range: 0.1.2 num2fraction: 1.2.2 picocolors: 0.2.1 @@ -41873,7 +42101,7 @@ snapshots: '@babel/core': 7.27.1 find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) babel-messages@6.23.0: dependencies: @@ -42478,9 +42706,7 @@ snapshots: balanced-match@2.0.0: {} - balanced-match@4.0.2: - dependencies: - jackspeak: 4.2.3 + balanced-match@4.0.3: {} base16@1.0.0: {} @@ -42514,6 +42740,8 @@ snapshots: big.js@5.2.2: {} + bignumber.js@9.3.1: {} + binary-extensions@1.13.1: {} binary-extensions@2.3.0: {} @@ -42590,13 +42818,13 @@ snapshots: fast-deep-equal: 3.1.3 multicast-dns: 7.2.5 - bonjour@3.5.0: + bonjour@3.5.1: dependencies: array-flatten: 2.1.2 deep-equal: 1.1.2 dns-equal: 1.0.0 dns-txt: 2.0.2 - multicast-dns: 6.2.3 + multicast-dns: 7.2.5 multicast-dns-service-types: 1.1.0 boolbase@1.0.0: {} @@ -42657,7 +42885,7 @@ snapshots: brace-expansion@5.0.2: dependencies: - balanced-match: 4.0.2 + balanced-match: 4.0.3 braces@3.0.3: dependencies: @@ -42725,17 +42953,17 @@ snapshots: browserslist@1.7.7: dependencies: - caniuse-db: 1.0.30001769 + caniuse-db: 1.0.30001770 electron-to-chromium: 1.5.286 browserslist@2.11.3: dependencies: - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 electron-to-chromium: 1.5.286 browserslist@4.14.2: dependencies: - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 electron-to-chromium: 1.5.286 escalade: 3.2.0 node-releases: 1.1.77 @@ -42743,7 +42971,7 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 electron-to-chromium: 1.5.286 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -43026,20 +43254,20 @@ snapshots: caniuse-api@1.6.1: dependencies: browserslist: 1.7.7 - caniuse-db: 1.0.30001769 + caniuse-db: 1.0.30001770 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 caniuse-api@3.0.0: dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-db@1.0.30001769: {} + caniuse-db@1.0.30001770: {} - caniuse-lite@1.0.30001769: {} + caniuse-lite@1.0.30001770: {} canvas@3.2.1: dependencies: @@ -43162,7 +43390,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.21.0 + undici: 7.22.0 whatwg-mimetype: 4.0.0 chokidar@1.7.0: @@ -43329,7 +43557,7 @@ snapshots: clipboardy@4.0.0: dependencies: execa: 8.0.1 - is-wsl: 3.1.0 + is-wsl: 3.1.1 is64bit: 2.0.0 clipboardy@5.3.0: @@ -43337,7 +43565,7 @@ snapshots: clipboard-image: 0.1.0 execa: 9.6.1 is-wayland: 0.1.0 - is-wsl: 3.1.0 + is-wsl: 3.1.1 is64bit: 2.0.0 powershell-utils: 0.2.0 @@ -43804,6 +44032,21 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 + create-jest@29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@22.15.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)): dependencies: '@jest/types': 29.6.3 @@ -43980,7 +44223,7 @@ snapshots: semver: 6.3.1 webpack: 4.47.0 - css-loader@5.2.7(webpack@5.105.2): + css-loader@5.2.7(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.4) loader-utils: 2.0.4 @@ -44020,7 +44263,7 @@ snapshots: optionalDependencies: webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18)) - css-loader@6.11.0(webpack@5.105.2): + css-loader@6.11.0(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.4) postcss: 8.5.4 @@ -44031,9 +44274,9 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) - css-loader@7.1.3(webpack@5.105.2): + css-loader@7.1.2(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.4) postcss: 8.5.4 @@ -44573,11 +44816,6 @@ snapshots: dns-equal@1.0.0: {} - dns-packet@1.3.4: - dependencies: - ip: 1.1.9 - safe-buffer: 5.2.1 - dns-packet@5.6.1: dependencies: '@leichtgewicht/ip-codec': 2.0.5 @@ -45308,7 +45546,7 @@ snapshots: object.hasown: 1.1.4 object.values: 1.2.1 prop-types: 15.8.1 - resolve: 2.0.0-next.5 + resolve: 2.0.0-next.6 semver: 6.3.1 string.prototype.matchall: 4.0.12 @@ -45329,7 +45567,7 @@ snapshots: object.fromentries: 2.0.8 object.values: 1.2.1 prop-types: 15.8.1 - resolve: 2.0.0-next.5 + resolve: 2.0.0-next.6 semver: 6.3.1 string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 @@ -45796,7 +46034,7 @@ snapshots: fast-uri@3.1.0: {} - fast-xml-parser@5.3.4: + fast-xml-parser@5.3.6: dependencies: strnum: 2.1.2 @@ -45879,6 +46117,10 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -45921,7 +46163,7 @@ snapshots: schema-utils: 3.3.0 webpack: 4.47.0 - file-loader@6.2.0(webpack@5.105.2): + file-loader@6.2.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 @@ -46144,7 +46386,7 @@ snapshots: fork-ts-checker-webpack-plugin@4.1.6: dependencies: - '@babel/code-frame': 7.10.4 + '@babel/code-frame': 7.29.0 chalk: 2.4.2 micromatch: 4.0.8 minimatch: 3.1.2 @@ -46286,7 +46528,7 @@ snapshots: typescript: 5.8.3 webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18)) - fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.105.2): + fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.104.1): dependencies: '@babel/code-frame': 7.29.0 chalk: 4.1.2 @@ -46301,9 +46543,9 @@ snapshots: semver: 7.7.4 tapable: 2.3.0 typescript: 5.8.3 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.105.2): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.104.1): dependencies: '@babel/code-frame': 7.29.0 chalk: 4.1.2 @@ -46534,6 +46776,23 @@ snapshots: strip-ansi: 6.0.1 wide-align: 1.1.5 + gaxios@7.1.3: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + rimraf: 5.0.5 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.3 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + generator-function@2.0.1: {} generic-names@4.0.0: @@ -46691,7 +46950,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.2.3 - minimatch: 10.2.0 + minimatch: 10.2.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 2.0.1 @@ -46700,7 +46959,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.2.3 - minimatch: 10.2.0 + minimatch: 10.2.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 2.0.1 @@ -46850,6 +47109,20 @@ snapshots: globrex@0.1.2: {} + google-auth-library@10.5.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.3 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + gtoken: 8.0.0 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} got@13.0.0: @@ -46932,6 +47205,13 @@ snapshots: growly@1.3.0: {} + gtoken@8.0.0: + dependencies: + gaxios: 7.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + gud@1.0.0: {} gunzip-maybe@1.4.2: @@ -47269,7 +47549,7 @@ snapshots: dependencies: parse-passwd: 1.0.0 - hono@4.11.9: {} + hono@4.11.10: {} hookified@1.15.1: {} @@ -48094,7 +48374,7 @@ snapshots: dependencies: is-docker: 2.2.1 - is-wsl@3.1.0: + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -48323,7 +48603,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.1(babel-plugin-macros@3.1.0) @@ -48349,7 +48629,7 @@ snapshots: '@jest/expect': 30.0.0 '@jest/test-result': 30.0.0 '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.1(babel-plugin-macros@3.1.0) @@ -48424,6 +48704,25 @@ snapshots: - supports-color - utf-8-validate + jest-cli@29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@22.15.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)) @@ -48516,6 +48815,99 @@ snapshots: - supports-color - utf-8-validate + jest-config@29.7.0(@types/node@16.18.126)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)): + dependencies: + '@babel/core': 7.27.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.1) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 16.18.126 + ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@16.18.126)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)): + dependencies: + '@babel/core': 7.27.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.1) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 16.18.126 + ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)): + dependencies: + '@babel/core': 7.27.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.1) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.15.19 + ts-node: 10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.15.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)): dependencies: '@babel/core': 7.27.1 @@ -48726,7 +49118,7 @@ snapshots: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -48759,7 +49151,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -48768,7 +49160,7 @@ snapshots: '@jest/environment': 30.0.0 '@jest/fake-timers': 30.0.0 '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-mock: 30.0.0 jest-util: 30.0.0 jest-validate: 30.0.0 @@ -48809,7 +49201,7 @@ snapshots: dependencies: '@jest/types': 26.6.2 '@types/graceful-fs': 4.1.9 - '@types/node': 22.15.24 + '@types/node': 16.18.126 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -48827,7 +49219,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.15.24 + '@types/node': 16.18.126 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -48842,7 +49234,7 @@ snapshots: jest-haste-map@30.0.0: dependencies: '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -48900,7 +49292,10 @@ snapshots: pretty-format: 25.5.0 throat: 5.0.0 transitivePeerDependencies: + - bufferutil + - canvas - supports-color + - utf-8-validate jest-leak-detector@25.5.0: dependencies: @@ -49035,19 +49430,19 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-util: 29.7.0 jest-mock@30.0.0: dependencies: '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-util: 30.0.0 jest-mock@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-util: 30.2.0 jest-pnp-resolver@1.2.3(jest-resolve@25.5.1): @@ -49104,7 +49499,7 @@ snapshots: dependencies: browser-resolve: 1.11.3 is-builtin-module: 1.0.0 - resolve: 1.6.0 + resolve: 1.22.11 jest-resolve@22.4.3: dependencies: @@ -49180,7 +49575,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -49206,7 +49601,7 @@ snapshots: '@jest/test-result': 30.0.0 '@jest/transform': 30.0.0 '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 @@ -49287,7 +49682,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -49314,7 +49709,7 @@ snapshots: '@jest/test-result': 30.0.0 '@jest/transform': 30.0.0 '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 cjs-module-lexer: 2.2.0 collect-v8-coverage: 1.0.3 @@ -49338,7 +49733,7 @@ snapshots: jest-serializer@26.6.2: dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 graceful-fs: 4.2.11 jest-snapshot@20.0.3: @@ -49459,7 +49854,7 @@ snapshots: jest-util@26.6.2: dependencies: '@jest/types': 26.6.2 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 graceful-fs: 4.2.11 is-ci: 2.0.0 @@ -49468,7 +49863,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -49477,7 +49872,7 @@ snapshots: jest-util@30.0.0: dependencies: '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -49486,7 +49881,7 @@ snapshots: jest-util@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -49557,7 +49952,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.24 + '@types/node': 16.18.126 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -49568,7 +49963,7 @@ snapshots: dependencies: '@jest/test-result': 30.0.0 '@jest/types': 30.0.0 - '@types/node': 22.15.24 + '@types/node': 16.18.126 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -49587,7 +49982,7 @@ snapshots: jest-worker@26.6.2: dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 merge-stream: 2.0.0 supports-color: 7.2.0 @@ -49599,14 +49994,14 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@30.0.0: dependencies: - '@types/node': 22.15.24 + '@types/node': 16.18.126 '@ungap/structured-clone': 1.3.0 jest-util: 30.0.0 merge-stream: 2.0.0 @@ -49627,6 +50022,18 @@ snapshots: - supports-color - utf-8-validate + jest@29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@22.15.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)) @@ -49866,6 +50273,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-loader@0.5.7: {} @@ -50040,7 +50451,7 @@ snapshots: dependencies: package-json: 4.0.1 - launch-editor@2.12.0: + launch-editor@2.13.0: dependencies: picocolors: 1.1.1 shell-quote: 1.8.3 @@ -51176,7 +51587,7 @@ snapshots: dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) minim@0.23.8: dependencies: @@ -51186,7 +51597,7 @@ snapshots: minimalistic-crypto-utils@1.0.1: {} - minimatch@10.2.0: + minimatch@10.2.1: dependencies: brace-expansion: 5.0.2 @@ -51456,11 +51867,6 @@ snapshots: multicast-dns-service-types@1.1.0: {} - multicast-dns@6.2.3: - dependencies: - dns-packet: 1.3.4 - thunky: 1.1.0 - multicast-dns@7.2.5: dependencies: dns-packet: 5.6.1 @@ -51551,6 +51957,13 @@ snapshots: node-domexception@1.0.0: {} + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + node-fetch-commonjs@3.3.2: dependencies: node-domexception: 1.0.0 @@ -51643,12 +52056,12 @@ snapshots: node-loader@2.0.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) - node-loader@2.1.0(webpack@5.105.2): + node-loader@2.1.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) node-notifier@5.4.5: dependencies: @@ -52663,7 +53076,7 @@ snapshots: postcss: 8.5.3 semver: 7.7.4 optionalDependencies: - webpack: 5.105.2(webpack-cli@6.0.1) + webpack: 5.104.1(webpack-cli@6.0.1) transitivePeerDependencies: - typescript @@ -53119,7 +53532,7 @@ snapshots: prism-react-renderer@2.4.1(react@18.2.0): dependencies: - '@types/prismjs': 1.26.6 + '@types/prismjs': 1.26.5 clsx: 2.1.1 react: 18.2.0 @@ -53239,7 +53652,7 @@ snapshots: prosemirror-state: 1.4.3 w3c-keyname: 2.2.8 - prosemirror-markdown@1.13.4: + prosemirror-markdown@1.13.2: dependencies: '@types/markdown-it': 14.1.2 markdown-it: 14.1.1 @@ -54727,9 +55140,12 @@ snapshots: dependencies: path-parse: 1.0.7 - resolve@2.0.0-next.5: + resolve@2.0.0-next.6: dependencies: + es-errors: 1.3.0 is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -54879,7 +55295,7 @@ snapshots: rollup@1.32.1: dependencies: '@types/estree': 1.0.8 - '@types/node': 22.15.24 + '@types/node': 16.18.126 acorn: 7.4.1 rollup@4.41.0: @@ -55014,7 +55430,7 @@ snapshots: dependencies: klona: 2.0.6 neo-async: 2.6.2 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) optionalDependencies: sass: 1.89.0 @@ -55455,13 +55871,13 @@ snapshots: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) - source-map-loader@5.0.0(webpack@5.105.2): + source-map-loader@5.0.0(webpack@5.104.1): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) source-map-resolve@0.6.0: dependencies: @@ -55926,13 +56342,13 @@ snapshots: schema-utils: 2.7.1 webpack: 4.47.0 - style-loader@1.3.0(webpack@5.105.2): + style-loader@1.3.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 2.7.1 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) - style-loader@2.0.0(webpack@5.105.2): + style-loader@2.0.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 @@ -55946,11 +56362,11 @@ snapshots: dependencies: webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18)) - style-loader@3.3.4(webpack@5.105.2): + style-loader@3.3.4(webpack@5.104.1): dependencies: - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) - style-loader@4.0.0(webpack@5.105.2): + style-loader@4.0.0(webpack@5.104.1): dependencies: webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@6.0.1) @@ -56099,7 +56515,7 @@ snapshots: svg-tags@1.0.0: {} - svg-url-loader@8.0.0(webpack@5.105.2): + svg-url-loader@8.0.0(webpack@5.104.1): dependencies: file-loader: 6.2.0(webpack@5.104.1) webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@6.0.1) @@ -56190,11 +56606,11 @@ snapshots: dependencies: '@babel/runtime-corejs3': 7.29.0 '@scarf/scarf': 1.4.0 - '@swagger-api/apidom-core': 1.4.0 - '@swagger-api/apidom-error': 1.4.0 - '@swagger-api/apidom-json-pointer': 1.4.0 - '@swagger-api/apidom-ns-openapi-3-1': 1.4.0 - '@swagger-api/apidom-reference': 1.4.0 + '@swagger-api/apidom-core': 1.5.0 + '@swagger-api/apidom-error': 1.5.0 + '@swagger-api/apidom-json-pointer': 1.5.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.5.0 + '@swagger-api/apidom-reference': 1.5.0 '@swaggerexpert/cookie': 2.0.2 deepmerge: 4.3.1 fast-json-patch: 3.1.1 @@ -56301,7 +56717,7 @@ snapshots: dependencies: '@swc/core': 1.15.11(@swc/helpers@0.5.18) '@swc/counter': 0.1.3 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) symbol-tree@3.2.4: {} @@ -56646,7 +57062,7 @@ snapshots: schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.46.0 - webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@6.0.1) + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.15.11(@swc/helpers@0.5.18) @@ -56928,6 +57344,26 @@ snapshots: typescript: 3.9.10 yargs-parser: 18.1.3 + ts-jest@29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(jest@29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)))(typescript@5.8.3): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@22.15.19)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3)) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.4 + type-fest: 4.41.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.27.1 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.1) + ts-jest@29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(jest@29.7.0(@types/node@22.15.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.24)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 @@ -57002,7 +57438,7 @@ snapshots: semver: 7.7.4 source-map: 0.7.6 typescript: 5.8.3 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) ts-loader@9.5.2(typescript@5.8.3)(webpack@5.104.1): dependencies: @@ -57056,6 +57492,27 @@ snapshots: optionalDependencies: '@swc/core': 1.15.11(@swc/helpers@0.5.18) + ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.19)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.15.19 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 8.0.3 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.11(@swc/helpers@0.5.18) + optional: true + ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@22.15.21)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -57557,7 +58014,7 @@ snapshots: undici-types@6.21.0: {} - undici@7.21.0: {} + undici@7.22.0: {} unfetch@4.2.0: {} @@ -57883,7 +58340,7 @@ snapshots: schema-utils: 3.3.0 webpack: 4.47.0(webpack-cli@6.0.1) optionalDependencies: - file-loader: 6.2.0(webpack@5.105.2) + file-loader: 6.2.0(webpack@5.104.1) url-parse-lax@1.0.0: dependencies: @@ -58164,8 +58621,8 @@ snapshots: vscode-extension-tester@8.14.1(mocha@11.4.0)(typescript@5.8.3): dependencies: - '@redhat-developer/locators': 1.18.1(@redhat-developer/page-objects@1.18.1(selenium-webdriver@4.40.0)(typescript@5.8.3))(selenium-webdriver@4.40.0) - '@redhat-developer/page-objects': 1.18.1(selenium-webdriver@4.40.0)(typescript@5.8.3) + '@redhat-developer/locators': 1.19.0(@redhat-developer/page-objects@1.19.0(selenium-webdriver@4.40.0)(typescript@5.8.3))(selenium-webdriver@4.40.0) + '@redhat-developer/page-objects': 1.19.0(selenium-webdriver@4.40.0)(typescript@5.8.3) '@types/selenium-webdriver': 4.35.5 '@vscode/vsce': 3.7.1 c8: 10.1.3 @@ -58191,8 +58648,8 @@ snapshots: vscode-extension-tester@8.14.1(mocha@11.5.0)(typescript@5.8.3): dependencies: - '@redhat-developer/locators': 1.18.1(@redhat-developer/page-objects@1.18.1(selenium-webdriver@4.40.0)(typescript@5.8.3))(selenium-webdriver@4.40.0) - '@redhat-developer/page-objects': 1.18.1(selenium-webdriver@4.40.0)(typescript@5.8.3) + '@redhat-developer/locators': 1.19.0(@redhat-developer/page-objects@1.19.0(selenium-webdriver@4.40.0)(typescript@5.8.3))(selenium-webdriver@4.40.0) + '@redhat-developer/page-objects': 1.19.0(selenium-webdriver@4.40.0)(typescript@5.8.3) '@types/selenium-webdriver': 4.35.5 '@vscode/vsce': 3.7.1 c8: 10.1.3 @@ -58384,10 +58841,10 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-cli@4.10.0(webpack-dev-server@5.2.3)(webpack@5.105.2): + webpack-cli@4.10.0(webpack-dev-server@5.2.3)(webpack@5.104.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0)(webpack@5.105.2) + '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0)(webpack@5.104.1) '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0) '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0)(webpack-dev-server@5.2.3) colorette: 2.0.20 @@ -58397,15 +58854,15 @@ snapshots: import-local: 3.2.0 interpret: 2.2.0 rechoir: 0.7.1 - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) webpack-merge: 5.10.0 optionalDependencies: - webpack-dev-server: 5.2.3(webpack-cli@4.10.0)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@4.10.0)(webpack@5.104.1) - webpack-cli@4.10.0(webpack@5.105.2): + webpack-cli@4.10.0(webpack@5.104.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0)(webpack@5.105.2) + '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0)(webpack@5.104.1) '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0) '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0) colorette: 2.0.20 @@ -58415,15 +58872,15 @@ snapshots: import-local: 3.2.0 interpret: 2.2.0 rechoir: 0.7.1 - webpack: 5.105.2(webpack-cli@4.10.0) + webpack: 5.104.1(webpack-cli@4.10.0) webpack-merge: 5.10.0 - webpack-cli@5.1.4(webpack-dev-server@5.2.3)(webpack@5.105.2): + webpack-cli@5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.105.2) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.105.2) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.3)(webpack@5.105.2) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.104.1) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.104.1) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.3)(webpack@5.104.1) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -58432,17 +58889,17 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) webpack-merge: 5.10.0 optionalDependencies: - webpack-dev-server: 5.2.3(webpack-cli@5.1.4)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@5.1.4)(webpack@5.104.1) - webpack-cli@5.1.4(webpack@5.105.2): + webpack-cli@5.1.4(webpack@5.104.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.105.2) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.105.2) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.105.2) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.104.1) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.104.1) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.104.1) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -58451,15 +58908,15 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) webpack-merge: 5.10.0 - webpack-cli@6.0.1(webpack-dev-server@5.2.3)(webpack@5.105.2): + webpack-cli@6.0.1(webpack-dev-server@5.2.3)(webpack@5.104.1): dependencies: '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.2) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.2) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.3)(webpack@5.105.2) + '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.104.1) + '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.104.1) + '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.3)(webpack@5.104.1) colorette: 2.0.20 commander: 12.1.0 cross-spawn: 7.0.6 @@ -58471,14 +58928,14 @@ snapshots: webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@6.0.1) webpack-merge: 6.0.1 optionalDependencies: - webpack-dev-server: 5.2.3(webpack-cli@6.0.1)(webpack@5.105.2) + webpack-dev-server: 5.2.3(webpack-cli@6.0.1)(webpack@5.104.1) - webpack-cli@6.0.1(webpack@5.105.2): + webpack-cli@6.0.1(webpack@5.104.1): dependencies: '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.2) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.2) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.2) + '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.104.1) + '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.104.1) + '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.104.1) colorette: 2.0.20 commander: 12.1.0 cross-spawn: 7.0.6 @@ -58487,7 +58944,7 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.105.2(webpack-cli@6.0.1) + webpack: 5.104.1(webpack-cli@6.0.1) webpack-merge: 6.0.1 webpack-dev-middleware@1.12.2(webpack@3.8.1): @@ -58526,7 +58983,7 @@ snapshots: webpack: 4.47.0 webpack-log: 2.0.0 - webpack-dev-middleware@4.3.0(webpack@5.105.2): + webpack-dev-middleware@4.3.0(webpack@5.104.1): dependencies: colorette: 1.4.0 mem: 8.1.1 @@ -58556,7 +59013,7 @@ snapshots: optionalDependencies: webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18)) - webpack-dev-middleware@6.1.3(webpack@5.105.2): + webpack-dev-middleware@6.1.3(webpack@5.104.1): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -58564,7 +59021,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) webpack-dev-middleware@7.4.5(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))): dependencies: @@ -58578,7 +59035,7 @@ snapshots: webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18)) optional: true - webpack-dev-middleware@7.4.5(webpack@5.105.2): + webpack-dev-middleware@7.4.5(webpack@5.104.1): dependencies: colorette: 2.0.20 memfs: 4.56.10 @@ -58587,13 +59044,13 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.105.2(webpack-cli@5.1.4) + webpack: 5.104.1(webpack-cli@5.1.4) webpack-dev-server@2.11.3(webpack@3.8.1): dependencies: ansi-html: 0.0.7 array-includes: 3.1.9 - bonjour: 3.5.0 + bonjour: 3.5.1 chokidar: 2.1.8 compression: 1.8.1 connect-history-api-fallback: 1.6.0 @@ -58640,7 +59097,7 @@ snapshots: graceful-fs: 4.2.11 http-proxy-middleware: 2.0.9(@types/express@4.17.25) ipaddr.js: 2.3.0 - launch-editor: 2.12.0 + launch-editor: 2.13.0 open: 10.2.0 p-retry: 6.2.1 schema-utils: 4.3.3 @@ -58648,11 +59105,11 @@ snapshots: serve-index: 1.9.2 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.105.2) + webpack-dev-middleware: 7.4.5(webpack@5.104.1) ws: 8.19.0 optionalDependencies: - webpack: 5.105.2(webpack-cli@4.10.0) - webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@4.10.0) + webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.104.1) transitivePeerDependencies: - bufferutil - debug @@ -58660,7 +59117,7 @@ snapshots: - utf-8-validate optional: true - webpack-dev-server@5.2.3(webpack-cli@5.1.4)(webpack@5.105.2): + webpack-dev-server@5.2.3(webpack-cli@5.1.4)(webpack@5.104.1): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -58680,7 +59137,7 @@ snapshots: graceful-fs: 4.2.11 http-proxy-middleware: 2.0.9(@types/express@4.17.25) ipaddr.js: 2.3.0 - launch-editor: 2.12.0 + launch-editor: 2.13.0 open: 10.2.0 p-retry: 6.2.1 schema-utils: 4.3.3 @@ -58688,18 +59145,18 @@ snapshots: serve-index: 1.9.2 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.105.2) + webpack-dev-middleware: 7.4.5(webpack@5.104.1) ws: 8.19.0 optionalDependencies: - webpack: 5.105.2(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.2.3)(webpack@5.105.2) + webpack: 5.104.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1) transitivePeerDependencies: - bufferutil - debug - supports-color - utf-8-validate - webpack-dev-server@5.2.3(webpack-cli@6.0.1)(webpack@5.105.2): + webpack-dev-server@5.2.3(webpack-cli@6.0.1)(webpack@5.104.1): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -58719,7 +59176,7 @@ snapshots: graceful-fs: 4.2.11 http-proxy-middleware: 2.0.9(@types/express@4.17.25) ipaddr.js: 2.3.0 - launch-editor: 2.12.0 + launch-editor: 2.13.0 open: 10.2.0 p-retry: 6.2.1 schema-utils: 4.3.3 @@ -58727,7 +59184,7 @@ snapshots: serve-index: 1.9.2 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.105.2) + webpack-dev-middleware: 7.4.5(webpack@5.104.1) ws: 8.19.0 optionalDependencies: webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack-cli@6.0.1) @@ -58758,7 +59215,7 @@ snapshots: graceful-fs: 4.2.11 http-proxy-middleware: 2.0.9(@types/express@4.17.25) ipaddr.js: 2.3.0 - launch-editor: 2.12.0 + launch-editor: 2.13.0 open: 10.2.0 p-retry: 6.2.1 schema-utils: 4.3.3 @@ -58831,7 +59288,7 @@ snapshots: source-list-map: 2.0.1 source-map: 0.6.1 - webpack-sources@3.3.3: {} + webpack-sources@3.3.4: {} webpack-virtual-modules@0.2.2: dependencies: @@ -58976,7 +59433,7 @@ snapshots: tapable: 2.3.0 terser-webpack-plugin: 5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 transitivePeerDependencies: - '@swc/core' - esbuild @@ -59008,7 +59465,7 @@ snapshots: tapable: 2.3.0 terser-webpack-plugin: 5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.25.12)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.25.12)) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 transitivePeerDependencies: - '@swc/core' - esbuild @@ -59040,9 +59497,9 @@ snapshots: tapable: 2.3.0 terser-webpack-plugin: 5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack@5.104.1) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 5.1.4(webpack@5.105.2) + webpack-cli: 5.1.4(webpack@5.104.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -59074,15 +59531,15 @@ snapshots: tapable: 2.3.0 terser-webpack-plugin: 5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.18))(webpack@5.104.1) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 6.0.1(webpack-dev-server@5.2.3)(webpack@5.105.2) + webpack-cli: 6.0.1(webpack-dev-server@5.2.3)(webpack@5.104.1) transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpack@5.105.2(webpack-cli@4.10.0): + webpack@5.104.1(webpack-cli@4.10.0): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -59108,15 +59565,15 @@ snapshots: tapable: 2.3.0 terser-webpack-plugin: 5.3.16(webpack@5.104.1) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.105.2) + webpack-cli: 4.10.0(webpack-dev-server@5.2.3)(webpack@5.104.1) transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpack@5.105.2(webpack-cli@5.1.4): + webpack@5.104.1(webpack-cli@5.1.4): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -59142,15 +59599,15 @@ snapshots: tapable: 2.3.0 terser-webpack-plugin: 5.3.16(webpack@5.104.1) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 5.1.4(webpack@5.105.2) + webpack-cli: 5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1) transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpack@5.105.2(webpack-cli@6.0.1): + webpack@5.104.1(webpack-cli@6.0.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -59176,9 +59633,9 @@ snapshots: tapable: 2.3.0 terser-webpack-plugin: 5.3.16(webpack@5.104.1) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 6.0.1(webpack@5.105.2) + webpack-cli: 6.0.1(webpack@5.104.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -59425,7 +59882,7 @@ snapshots: wsl-utils@0.1.0: dependencies: - is-wsl: 3.1.0 + is-wsl: 3.1.1 x-default-browser@0.4.0: optionalDependencies: diff --git a/common/scripts/run-diagram-tests.js b/common/scripts/run-diagram-tests.js new file mode 100755 index 0000000000..478ed65303 --- /dev/null +++ b/common/scripts/run-diagram-tests.js @@ -0,0 +1,218 @@ +#!/usr/bin/env node + +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { execSync } = require('child_process'); +const path = require('path'); + +// ANSI color codes +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + cyan: '\x1b[36m', + gray: '\x1b[90m', +}; + +const diagram_packages = [ + { name: 'bi-diagram', path: 'workspaces/ballerina/bi-diagram' }, + { name: 'sequence-diagram', path: 'workspaces/ballerina/sequence-diagram' }, + { name: 'component-diagram', path: 'workspaces/ballerina/component-diagram' }, + { name: 'type-diagram', path: 'workspaces/ballerina/type-diagram' }, + { name: 'mi-diagram', path: 'workspaces/mi/mi-diagram' }, +]; + +function parseTestResults(output) { + const results = { + testSuites: { passed: 0, failed: 0, total: 0 }, + tests: { passed: 0, failed: 0, total: 0 }, + snapshots: { passed: 0, failed: 0, updated: 0, total: 0 }, + time: 'N/A', + failedTests: [], + }; + + // Parse test suites + const testSuitesMatch = output.match(/Test Suites:\s+(?:(\d+)\s+failed,\s*)?(\d+)\s+passed,\s*(\d+)\s+total/); + if (testSuitesMatch) { + results.testSuites.failed = parseInt(testSuitesMatch[1] || '0'); + results.testSuites.passed = parseInt(testSuitesMatch[2] || '0'); + results.testSuites.total = parseInt(testSuitesMatch[3] || '0'); + } + + // Parse tests + const testsMatch = output.match(/Tests:\s+(?:(\d+)\s+failed,\s*)?(\d+)\s+passed,\s*(\d+)\s+total/); + if (testsMatch) { + results.tests.failed = parseInt(testsMatch[1] || '0'); + results.tests.passed = parseInt(testsMatch[2] || '0'); + results.tests.total = parseInt(testsMatch[3] || '0'); + } + + // Parse snapshots + const snapshotsMatch = output.match(/Snapshots:\s+(?:(\d+)\s+failed,\s*)?(?:(\d+)\s+updated,\s*)?(\d+)\s+passed,\s*(\d+)\s+total/); + if (snapshotsMatch) { + results.snapshots.failed = parseInt(snapshotsMatch[1] || '0'); + results.snapshots.updated = parseInt(snapshotsMatch[2] || '0'); + results.snapshots.passed = parseInt(snapshotsMatch[3] || '0'); + results.snapshots.total = parseInt(snapshotsMatch[4] || '0'); + } + + // Parse time + const timeMatch = output.match(/Time:\s+([\d.]+\s*s)/); + if (timeMatch) { + results.time = timeMatch[1]; + } + + // Extract failed test names + const failPattern = /●\s+(.+?)(?:\n|$)/g; + let match; + while ((match = failPattern.exec(output)) !== null) { + if (match[1] && !match[1].includes('expect(')) { + results.failedTests.push(match[1].trim()); + } + } + + return results; +} + +function runTest(packageInfo) { + const { name, path: packagePath } = packageInfo; + + console.log(`\n${colors.cyan}${colors.bright}Running tests for ${name}...${colors.reset}`); + console.log(`${colors.gray}Location: ${packagePath}${colors.reset}\n`); + + let output = ''; + let success = true; + + try { + output = execSync('pnpm run test 2>&1', { + cwd: path.join(process.cwd(), packagePath), + encoding: 'utf-8', + maxBuffer: 10 * 1024 * 1024, // 10MB buffer + }); + } catch (error) { + // Jest returns non-zero exit code when tests fail, but output is still valid + output = error.stdout || error.output?.join('') || ''; + success = false; + } + + const results = parseTestResults(output); + + // Determine success based on actual test results, not just exit code + const actualSuccess = results.tests.failed === 0 && results.testSuites.failed === 0; + + return { name, success: actualSuccess, results, output }; +} + +function printResult(result) { + const { name, success, results } = result; + const icon = success ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`; + + console.log(`${icon} ${colors.bright}${name}${colors.reset}`); + console.log(` Test Suites: ${formatCount(results.testSuites.passed, results.testSuites.failed, results.testSuites.total)}`); + console.log(` Tests: ${formatCount(results.tests.passed, results.tests.failed, results.tests.total)}`); + console.log(` Snapshots: ${formatCount(results.snapshots.passed, results.snapshots.failed, results.snapshots.total, results.snapshots.updated)}`); + console.log(` Time: ${results.time}`); + + if (results.failedTests.length > 0) { + console.log(` ${colors.red}Failed tests:${colors.reset}`); + results.failedTests.slice(0, 5).forEach(test => { + console.log(` ${colors.red}•${colors.reset} ${test}`); + }); + if (results.failedTests.length > 5) { + console.log(` ${colors.gray}... and ${results.failedTests.length - 5} more${colors.reset}`); + } + } +} + +function formatCount(passed, failed, total, updated) { + let result = ''; + + if (failed > 0) { + result += `${colors.red}${failed} failed${colors.reset}, `; + } + if (updated && updated > 0) { + result += `${colors.yellow}${updated} updated${colors.reset}, `; + } + result += `${colors.green}${passed} passed${colors.reset}`; + result += `, ${total} total`; + + return result; +} + +function printSummary(allResults) { + console.log(`\n${colors.bright}${'='.repeat(60)}${colors.reset}`); + console.log(`${colors.bright}${colors.cyan}SUMMARY${colors.reset}`); + console.log(`${colors.bright}${'='.repeat(60)}${colors.reset}\n`); + + const totals = { + testSuites: { passed: 0, failed: 0, total: 0 }, + tests: { passed: 0, failed: 0, total: 0 }, + snapshots: { passed: 0, failed: 0, updated: 0, total: 0 }, + }; + + allResults.forEach(result => { + printResult(result); + console.log(''); + + // Accumulate totals + totals.testSuites.passed += result.results.testSuites.passed; + totals.testSuites.failed += result.results.testSuites.failed; + totals.testSuites.total += result.results.testSuites.total; + + totals.tests.passed += result.results.tests.passed; + totals.tests.failed += result.results.tests.failed; + totals.tests.total += result.results.tests.total; + + totals.snapshots.passed += result.results.snapshots.passed; + totals.snapshots.failed += result.results.snapshots.failed; + totals.snapshots.updated += result.results.snapshots.updated; + totals.snapshots.total += result.results.snapshots.total; + }); + + console.log(`${colors.bright}OVERALL TOTALS:${colors.reset}`); + console.log(` Test Suites: ${formatCount(totals.testSuites.passed, totals.testSuites.failed, totals.testSuites.total)}`); + console.log(` Tests: ${formatCount(totals.tests.passed, totals.tests.failed, totals.tests.total)}`); + console.log(` Snapshots: ${formatCount(totals.snapshots.passed, totals.snapshots.failed, totals.snapshots.total, totals.snapshots.updated)}`); + + const allPassed = allResults.every(r => r.success); + console.log(`\n${colors.bright}${'='.repeat(60)}${colors.reset}`); + + if (allPassed) { + console.log(`${colors.green}${colors.bright}✓ All diagram tests passed!${colors.reset}\n`); + process.exit(0); + } else { + console.log(`${colors.red}${colors.bright}✗ Some diagram tests failed!${colors.reset}\n`); + process.exit(1); + } +} + +// Main execution +console.log(`${colors.bright}${colors.cyan}Running Diagram Snapshot Tests${colors.reset}`); +console.log(`${colors.gray}Testing ${diagram_packages.length} diagram packages...${colors.reset}`); + +const allResults = []; + +for (const pkg of diagram_packages) { + const result = runTest(pkg); + allResults.push(result); +} + +printSummary(allResults); diff --git a/package.json b/package.json index 818f530ff3..20cb7467f6 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "build:wso2-platform": "rush build --to wso2-platform", "build:mi": "rush build --to micro-integrator", "build:api-designer": "rush build --to api-designer", + "test:diagrams": "node common/scripts/run-diagram-tests.js", + "test:updateDiagramsSnapshots": "cd workspaces/ballerina/bi-diagram && npm run test:updateSnapshots && cd ../sequence-diagram && npm run test:updateSnapshots && cd ../component-diagram && npm run test:updateSnapshots && cd ../type-diagram && npm run test:updateSnapshots", "setup-e2e-test-debug": "workspaces/ballerina/ballerina-extension/node_modules/vscode-extension-tester/out/cli.js get-vscode && workspaces/ballerina/ballerina-extension/node_modules/vscode-extension-tester/out/cli.js get-chromedriver", "ui-toolkit-stories": "npm run --prefix ./workspaces/common-libs/ui-toolkit serve-storybook", "prepare": "husky" diff --git a/workspaces/ballerina/ballerina-core/package.json b/workspaces/ballerina/ballerina-core/package.json index 6cdc718406..030b9c7a37 100644 --- a/workspaces/ballerina/ballerina-core/package.json +++ b/workspaces/ballerina/ballerina-core/package.json @@ -24,6 +24,7 @@ "handlebars": "4.7.8", "mousetrap": "1.6.5", "react": "18.2.0", + "@wso2/wso2-platform-core": "workspace:*", "tree-kill": "1.2.2", "vscode-uri": "3.0.8", "@types/mousetrap": "1.6.11", diff --git a/workspaces/ballerina/ballerina-core/src/index.ts b/workspaces/ballerina/ballerina-core/src/index.ts index df051da1ca..78ad207855 100644 --- a/workspaces/ballerina/ballerina-core/src/index.ts +++ b/workspaces/ballerina/ballerina-core/src/index.ts @@ -91,6 +91,7 @@ export * from "./rpc-types/icp-service/rpc-type"; export * from "./rpc-types/agent-chat"; export * from "./rpc-types/agent-chat/interfaces"; export * from "./rpc-types/agent-chat/rpc-type"; +export * from "./rpc-types/platform-ext"; // ------ History class and interface --------> diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts b/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts index d2d8f2462c..bedc09fe72 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/bi.ts @@ -20,6 +20,7 @@ import { NodePosition } from "@wso2/syntax-tree"; import { LinePosition } from "./common"; import { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"; import { ValueTypeConstraint } from "../rpc-types/ai-agent/interfaces"; +import { Type } from "./extended-lang-client"; export type { NodePosition }; @@ -126,6 +127,7 @@ export type Imports = { export type FormFieldInputType = "TEXT" | "BOOLEAN" | "IDENTIFIER" | + "AUTOCOMPLETE" | "SINGLE_SELECT" | "MULTIPLE_SELECT" | "TEXTAREA" | @@ -148,15 +150,28 @@ export type FormFieldInputType = "TEXT" | "ai:Prompt" | "FIXED_PROPERTY" | "REPEATABLE_PROPERTY" | - "MAPPING_EXPRESSION_SET" | - "MAPPING_EXPRESSION" | "ENUM" | "DM_JOIN_CLAUSE_RHS_EXPRESSION" | "RECORD_MAP_EXPRESSION" | + "REPEATABLE_MAP" | "PROMPT" | + "RECORD_FIELD_SELECTOR" | "SQL_QUERY" | "CLAUSE_EXPRESSION" | - "SLIDER"; + "SLIDER" | + "HEADER_SET" | + "DROPDOWN_CHOICE" | + "CUSTOM_DROPDOWN" | + "ACTION_TYPE" | + "ACTION_EXPRESSION" | + "VIEW" | + "SERVICE_PATH" | + "ACTION_PATH" | + "NUMBER" | + "REPEATABLE_LIST" | + "CONDITIONAL_FIELDS" | + "DOC_TEXT" + ; export interface BaseType { fieldType: FormFieldInputType; @@ -188,11 +203,23 @@ export interface IdentifierType extends BaseType { scope: FieldScope; } +export interface RecordFieldSelectorType extends BaseType { + fieldType: "RECORD_FIELD_SELECTOR"; + recordSelectorType: RecordSelectorType; +} + +export interface RecordSelectorType { + rootType: Type; + referencedTypes: Type[]; +} + + export type InputType = | BaseType | DropdownType | TemplateType - | IdentifierType; + | IdentifierType + | RecordFieldSelectorType; export type Property = { metadata: Metadata; @@ -448,6 +475,7 @@ export type NodePropertyKey = | "store" | "systemPrompt" | "targetType" + | "testConfigValue" | "toolKitName" | "tools" | "type" diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts index d003b56b63..686bf3eb93 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts @@ -40,7 +40,8 @@ export enum TypeKind { Unknown = "$CompilationError$", Anydata = "anydata", Byte = "byte", - Json = "json" + Json = "json", + Xml = "xml", } export enum InputCategory { @@ -50,7 +51,8 @@ export enum InputCategory { Enum = "enum", Parameter = "parameter", Variable = "variable", - LocalVariable = "local-variable" + LocalVariable = "local-variable", + ConvertedVariable = "converted-variable" } export enum IntermediateClauseType { @@ -91,6 +93,7 @@ export interface IOType { isDeepNested?: boolean; ref?: string; typeInfo?: TypeInfo; + convertedField?: IOType; } export interface Mapping { @@ -172,6 +175,7 @@ export interface IOTypeField { isIterationVariable?: boolean; isGroupingKey?: boolean; typeInfo?: TypeInfo; + convertedVariable?: IORoot; } export interface EnumMember { 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 618125eb4a..8b7b731943 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts @@ -497,15 +497,31 @@ export interface ClausePositionResponse { } export interface ConvertExpressionRequest { - outputType: string; - expression: string; - expressionType: string; + outputType: string; + expression: string; + expressionType: string; } export interface ConvertExpressionResponse { convertedExpression: string; } +export interface CreateConvertedVariableRequest { + // Data Mapper related + filePath: string; + codedata: CodeData; + varName: string; + targetField: string; + subMappingName?: string; + + // Converting variable related + variableName: string; + isInput: boolean; + typeName: string; + parentTypeName?: string; + imports?: Imports; +} + export interface GraphqlDesignServiceParams { filePath: string; startLine: LinePosition; @@ -868,6 +884,7 @@ export type BISourceCodeResponse = { export type BIDeleteByComponentInfoRequest = { filePath: string; component: ComponentInfo; + nodeType?: string; } export type BIDeleteByComponentInfoResponse = { @@ -1301,6 +1318,7 @@ export interface ListenersRequest { orgName?: string; pkgName?: string; listenerTypeName?: string; + removeDeprecated?: boolean; } export interface ListenersResponse { hasListeners: boolean; @@ -1315,6 +1333,7 @@ export interface ListenerModelRequest { type?: string; }; filePath: string; + removeDeprecated?: boolean; } export interface ListenerModelResponse { listener: ListenerModel; @@ -1498,6 +1517,8 @@ export interface Member { optional?: boolean; imports?: Imports; readonly?: boolean; + selected?: boolean; + typeName?: string; isGraphqlId?: boolean; } @@ -1866,6 +1887,12 @@ export type OpenAPIClientDeleteResponse = { // <-------- Deployment Related -------> +export interface ProjectScopeMapping { + projectPath: string; + projectTitle: string; + integrationTypes?: SCOPE[]; +} + export interface DeploymentRequest { integrationTypes: SCOPE[]; } @@ -1874,6 +1901,10 @@ export interface DeploymentResponse { isCompleted: boolean; } +export interface WorkspaceDeploymentRequest { + projectScopes: ProjectScopeMapping[]; + rootDirectory: string; +} // 2201.12.3 -> New Project Component Artifacts Tree diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts index db39742c0b..df5f0f1a6d 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts @@ -51,6 +51,7 @@ export interface AIPanelAPI { // General Functions // ================================== getLoginMethod: () => Promise; + isPlatformExtensionAvailable: () => Promise; getDefaultPrompt: () => Promise; //starting args getAIMachineSnapshot: () => Promise; //login state machine clearInitialPrompt: () => void; //starting args diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts index 9cadd3e946..4bcbe84887 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts @@ -51,6 +51,7 @@ import { RequestType, NotificationType } from "vscode-messenger-common"; const _preFix = "ai-panel"; export const getLoginMethod: RequestType = { method: `${_preFix}/getLoginMethod` }; +export const isPlatformExtensionAvailable: RequestType = { method: `${_preFix}/isPlatformExtensionAvailable` }; export const getDefaultPrompt: RequestType = { method: `${_preFix}/getDefaultPrompt` }; export const getAIMachineSnapshot: RequestType = { method: `${_preFix}/getAIMachineSnapshot` }; export const clearInitialPrompt: NotificationType = { method: `${_preFix}/clearInitialPrompt` }; @@ -77,7 +78,7 @@ export const isUserAuthenticated: RequestType = { method: `${_pre export const openAIPanel: RequestType = { method: `${_preFix}/openAIPanel` }; export const isPlanModeFeatureEnabled: RequestType = { method: `${_preFix}/isPlanModeFeatureEnabled` }; export const getSemanticDiff: RequestType = { method: `${_preFix}/getSemanticDiff` }; -export const getAffectedPackages: RequestType = { method: `${_preFix}/getAffectedPackages` }; +export const getAffectedPackages: NotificationType = { method: `${_preFix}/getAffectedPackages` }; export const isWorkspaceProject: RequestType = { method: `${_preFix}/isWorkspaceProject` }; export const acceptChanges: RequestType = { method: `${_preFix}/acceptChanges` }; export const declineChanges: RequestType = { method: `${_preFix}/declineChanges` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/index.ts index 89f86b680b..74f9f9fe37 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/index.ts @@ -73,6 +73,7 @@ import { UpdateTypesRequest, UpdateTypesResponse, DeploymentRequest, + WorkspaceDeploymentRequest, DeploymentResponse, OpenAPIClientGenerationRequest, OpenAPIGeneratedModulesRequest, @@ -118,6 +119,7 @@ import { RecordsInWorkspaceMentions, BuildMode, DevantMetadata, + WorkspaceDevantMetadata, GeneratedClientSaveResponse, AddProjectToWorkspaceRequest, DeleteProjectRequest, @@ -163,6 +165,7 @@ export interface BIDiagramAPI { openReadme: (params: OpenReadmeRequest) => void; renameIdentifier: (params: RenameIdentifierRequest) => Promise; deployProject: (params: DeploymentRequest) => Promise; + deployWorkspace: (params: WorkspaceDeploymentRequest) => Promise; openAIChat: (params: AIChatRequest) => void; getSignatureHelp: (params: SignatureHelpRequest) => Promise; buildProject: (mode: BuildMode) => void; @@ -201,6 +204,7 @@ export interface BIDiagramAPI { getRecordNames: () => Promise; getFunctionNames: () => Promise; getDevantMetadata: () => Promise; + getWorkspaceDevantMetadata: () => Promise; generateOpenApiClient: (params: OpenAPIClientGenerationRequest) => Promise; getOpenApiGeneratedModules: (params: OpenAPIGeneratedModulesRequest) => Promise; deleteOpenApiGeneratedModules: (params: OpenAPIClientDeleteRequest) => Promise; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/interfaces.ts index 2a81c68bfe..67a8ccac53 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/interfaces.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/interfaces.ts @@ -192,6 +192,20 @@ export interface DevantMetadata { hasLocalChanges?: boolean; } +export interface WorkspaceDevantMetadata { + isLoggedIn?: boolean; + hasAnyComponent?: boolean; + hasLocalChanges?: boolean; + projectsMetadata?: ProjectDevantMetadata[]; +} + +export interface ProjectDevantMetadata { + projectPath: string; + projectName?: string; + hasComponent?: boolean; + hasLocalChanges?: boolean; +} + export interface GeneratedClientSaveResponse { errorMessage?: string; } diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/rpc-type.ts index b16443a209..e2541761ac 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/bi-diagram/rpc-type.ts @@ -74,6 +74,7 @@ import { UpdateTypesRequest, UpdateTypesResponse, DeploymentRequest, + WorkspaceDeploymentRequest, DeploymentResponse, OpenAPIClientGenerationRequest, OpenAPIGeneratedModulesRequest, @@ -120,6 +121,7 @@ import { RecordsInWorkspaceMentions, BuildMode, DevantMetadata, + WorkspaceDevantMetadata, GeneratedClientSaveResponse, AddProjectToWorkspaceRequest, DeleteProjectRequest, @@ -166,6 +168,7 @@ export const getReadmeContent: RequestType = { method: `${_preFix}/openReadme` }; export const renameIdentifier: NotificationType = { method: `${_preFix}/renameIdentifier` }; export const deployProject: RequestType = { method: `${_preFix}/deployProject` }; +export const deployWorkspace: RequestType = { method: `${_preFix}/deployWorkspace` }; export const openAIChat: NotificationType = { method: `${_preFix}/openAIChat` }; export const getSignatureHelp: RequestType = { method: `${_preFix}/getSignatureHelp` }; export const buildProject: NotificationType = { method: `${_preFix}/buildProject` }; @@ -204,6 +207,7 @@ export const searchNodes: RequestType = { method: `${_preFix}/getRecordNames` }; export const getFunctionNames: RequestType = { method: `${_preFix}/getFunctionNames` }; export const getDevantMetadata: RequestType = { method: `${_preFix}/getDevantMetadata` }; +export const getWorkspaceDevantMetadata: RequestType = { method: `${_preFix}/getWorkspaceDevantMetadata` }; export const generateOpenApiClient: RequestType = { method: `${_preFix}/generateOpenApiClient` }; export const getOpenApiGeneratedModules: RequestType = { method: `${_preFix}/getOpenApiGeneratedModules` }; export const deleteOpenApiGeneratedModules: RequestType = { method: `${_preFix}/deleteOpenApiGeneratedModules` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts index a98708821c..4124e8601c 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts @@ -16,6 +16,7 @@ * under the License. */ +import { QuickPickItem } from "vscode"; import { BallerinaDiagnosticsRequest, BallerinaDiagnosticsResponse, @@ -33,7 +34,12 @@ import { WorkspaceRootResponse, ShowErrorMessageRequest, WorkspaceTypeResponse, - SampleDownloadRequest + SetWebviewCacheRequestParam, + ShowInfoModalRequest, + SampleDownloadRequest, + ShowQuickPickRequest, + DefaultOrgNameResponse, + PublishToCentralResponse } from "./interfaces"; export interface CommonRPCAPI { @@ -50,7 +56,15 @@ export interface CommonRPCAPI { isNPSupported: () => Promise; getWorkspaceRoot: () => Promise; showErrorMessage: (params: ShowErrorMessageRequest) => void; + showInformationModal: (params: ShowInfoModalRequest) => Promise; + showQuickPick: (params: ShowQuickPickRequest) => Promise; getCurrentProjectTomlValues: () => Promise>; getWorkspaceType: () => Promise; + setWebviewCache: (params: SetWebviewCacheRequestParam) => void; + restoreWebviewCache: (params: IDBValidKey) => unknown; + clearWebviewCache: (params: IDBValidKey) => void; downloadSelectedSampleFromGithub: (params: SampleDownloadRequest) => Promise; + getDefaultOrgName: () => Promise; + publishToCentral: () => Promise; + hasCentralPATConfigured: () => Promise; } diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/common/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/common/interfaces.ts index 680777ccea..1129944e02 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/common/interfaces.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/common/interfaces.ts @@ -20,6 +20,7 @@ import { Diagnostic } from "vscode-languageserver-types"; import { Completion } from "../../interfaces/extended-lang-client"; import { NodePosition } from "@wso2/syntax-tree"; +import { QuickPickItem, QuickPickOptions } from "vscode"; export interface TypeResponse { data: Completion[]; @@ -91,6 +92,16 @@ export interface ShowErrorMessageRequest { message: string; } +export interface ShowInfoModalRequest { + message: string; + items?: string[]; +} + +export interface ShowQuickPickRequest { + items: QuickPickItem[]; + options?: QuickPickOptions; +} + export interface TomlWorkspace { packages: string[]; } @@ -109,12 +120,38 @@ export interface WorkspaceTomlValues { export interface PackageTomlValues { package: TomlPackage; + tool?: { + openapi?: { + id: string; + targetModule: string; + filePath: string; + }[]; + } +} + +export interface SettingsTomlValues { + central: { + accesstoken: string; + }; } export interface WorkspaceTypeResponse { type: "SINGLE_PROJECT" | "MULTIPLE_PROJECTS" | "BALLERINA_WORKSPACE" | "VSCODE_WORKSPACE" | "UNKNOWN" } +export interface SetWebviewCacheRequestParam { + cacheKey: IDBValidKey; + data: unknown; +} export interface SampleDownloadRequest { zipFileName: string; } + +export interface DefaultOrgNameResponse { + orgName: string; +} + +export interface PublishToCentralResponse { + success: boolean; + message?: string; +} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/common/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/common/rpc-type.ts index 9b7379b10d..0e18954e54 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/common/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/common/rpc-type.ts @@ -17,6 +17,7 @@ * * THIS FILE INCLUDES AUTO GENERATED CODE */ +import { QuickPickItem } from "vscode"; import { BallerinaDiagnosticsRequest, BallerinaDiagnosticsResponse, @@ -34,7 +35,11 @@ import { WorkspaceRootResponse, ShowErrorMessageRequest, WorkspaceTypeResponse, - SampleDownloadRequest + SetWebviewCacheRequestParam, + ShowInfoModalRequest, + SampleDownloadRequest, + ShowQuickPickRequest, + PublishToCentralResponse } from "./interfaces"; import { RequestType, NotificationType } from "vscode-messenger-common"; @@ -52,6 +57,14 @@ export const experimentalEnabled: RequestType = { method: `${_pre export const isNPSupported: RequestType = { method: `${_preFix}/isNPSupported` }; export const getWorkspaceRoot: RequestType = { method: `${_preFix}/getWorkspaceRoot` }; export const showErrorMessage: NotificationType = { method: `${_preFix}/showErrorMessage` }; +export const showInformationModal: RequestType = { method: `${_preFix}/showInformationModal` }; +export const showQuickPick: RequestType = { method: `${_preFix}/showQuickPick` }; export const getCurrentProjectTomlValues: RequestType = { method: `${_preFix}/getCurrentProjectTomlValues` }; export const getWorkspaceType: RequestType = { method: `${_preFix}/getWorkspaceType` }; +export const SetWebviewCache: RequestType = { method: `${_preFix}/setWebviewCache` }; +export const RestoreWebviewCache: RequestType = { method: `${_preFix}/restoreWebviewCache` }; +export const ClearWebviewCache: RequestType = { method: `${_preFix}/clearWebviewCache` }; export const downloadSelectedSampleFromGithub: RequestType = { method: `${_preFix}/downloadSelectedSampleFromGithub` }; +export const getDefaultOrgName: RequestType = { method: `${_preFix}/getDefaultOrgName` }; +export const publishToCentral: RequestType = { method: `${_preFix}/publishToCentral` }; +export const hasCentralPATConfigured: RequestType = { method: `${_preFix}/hasCentralPATConfigured` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/index.ts index 66b208400b..4e6ab5cf1f 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/index.ts @@ -47,7 +47,8 @@ import { ClausePositionRequest, ClausePositionResponse, ConvertExpressionRequest, - ConvertExpressionResponse + ConvertExpressionResponse, + CreateConvertedVariableRequest } from "../../interfaces/extended-lang-client"; export interface DataMapperAPI { @@ -72,5 +73,6 @@ export interface DataMapperAPI { getExpandedDMFromDMModel: (params: DMModelRequest) => Promise; getProcessTypeReference: (params: ProcessTypeReferenceRequest) => Promise; getConvertedExpression: (params: ConvertExpressionRequest) => Promise; + createConvertedVariable: (params: CreateConvertedVariableRequest) => Promise; clearTypeCache: () => Promise; } diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/rpc-type.ts index 04643f0a2d..35fae7784f 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/rpc-type.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/data-mapper/rpc-type.ts @@ -49,7 +49,8 @@ import { ClausePositionRequest, ClausePositionResponse, ConvertExpressionRequest, - ConvertExpressionResponse + ConvertExpressionResponse, + CreateConvertedVariableRequest } from "../../interfaces/extended-lang-client"; import { RequestType } from "vscode-messenger-common"; @@ -75,4 +76,5 @@ export const getClausePosition: RequestType = { method: `${_preFix}/getExpandedDMFromDMModel` }; export const getProcessTypeReference: RequestType = { method: `${_preFix}/getProcessTypeReference` }; export const getConvertedExpression: RequestType = { method: `${_preFix}/getConvertedExpression` }; +export const createConvertedVariable: RequestType = { method: `${_preFix}/createConvertedVariable` }; export const clearTypeCache: RequestType = { method: `${_preFix}/clearTypeCache` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/index.ts new file mode 100644 index 0000000000..969f9ac4e7 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/index.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2026, 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 { GetMarketplaceListReq,MarketplaceListResp, GetMarketplaceIdlReq, MarketplaceIdlResp, ConnectionListItem, GetConnectionsReq, DeleteLocalConnectionsConfigReq, GetMarketplaceItemReq, MarketplaceItem, GetConnectionItemReq, ConnectionDetailed, CreateLocalConnectionsConfigReq, CreateThirdPartyConnectionReq, CreateComponentConnectionReq, GetComponentsReq, ComponentKind } from "@wso2/wso2-platform-core" +import { DeleteDevantTempConfigReq, GenerateCustomConnectorFromOASReq, GenerateCustomConnectorFromOASResp, AddDevantTempConfigReq, AddDevantTempConfigResp, ReplaceDevantTempConfigValuesReq, RegisterDevantMarketplaceServiceReq, InitializeDevantOASConnectionReq, InitializeDevantOASConnectionResp } from "./interfaces"; +export * from "./rpc-type" +export * from "./utils" + +// TODO: check if we can directly use the wso2-extension api interface +export interface PlatformExtAPI { + // BI ext handlers + generateCustomConnectorFromOAS: (params: GenerateCustomConnectorFromOASReq) => Promise + initializeDevantOASConnection: (params: InitializeDevantOASConnectionReq) => Promise + addDevantTempConfig: (params: AddDevantTempConfigReq) => Promise + deleteDevantTempConfigs: (params: DeleteDevantTempConfigReq) => Promise + replaceDevantTempConfigValues: (params: ReplaceDevantTempConfigValuesReq) => Promise + // Platform ext proxies + createThirdPartyConnection: (params: CreateThirdPartyConnectionReq) => Promise + createInternalConnection: (params: CreateComponentConnectionReq) => Promise + registerDevantMarketplaceService: (params: RegisterDevantMarketplaceServiceReq) => Promise + getMarketplaceItems: (params: GetMarketplaceListReq) => Promise; + getMarketplaceItem: (params: GetMarketplaceItemReq) => Promise; + getMarketplaceIdl: (params: GetMarketplaceIdlReq) => Promise; + getConnections: (params: GetConnectionsReq) => Promise; + getConnection: (params: GetConnectionItemReq) => Promise; + getComponentList: (params: GetComponentsReq) => Promise; + deleteLocalConnectionsConfig: (params: DeleteLocalConnectionsConfigReq) => void; + getDevantConsoleUrl: () => Promise; + refreshConnectionList: () => Promise; + setConnectedToDevant: (connected: boolean) => void; + setSelectedComponent: (componentId: string) => void; + setSelectedEnv: (envId: string) => void; + deployIntegrationInDevant: () => void; + createConnectionConfig: (params: CreateLocalConnectionsConfigReq) => Promise; +} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/interfaces.ts new file mode 100644 index 0000000000..0446bbab0f --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/interfaces.ts @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2026, 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 { + ComponentKind, + ConnectionConfigurations, + ConnectionDetailed, + ConnectionListItem, + ContextItemEnriched, + Environment, + MarketplaceIdlTypes, + MarketplaceItem, + MarketplaceServiceTypes, + UserInfo, +} from "@wso2/wso2-platform-core"; +import { AvailableNode } from "../../interfaces/bi"; +import { ModuleVarDecl } from "@wso2/syntax-tree/lib/syntax-tree-interfaces"; + +export interface GenerateCustomConnectorFromOASReq { + connectionName: string; + marketplaceItem: MarketplaceItem; + securityType?: "" | "oauth" | "apikey"; +} + +export interface GenerateCustomConnectorFromOASResp { + connectionNode?: AvailableNode; +} + +export interface InitializeDevantOASConnectionReq { + name: string; + visibility: string; + securityType: "" | "oauth" | "apikey"; + marketplaceItem: MarketplaceItem; + configurations: ConnectionConfigurations; + devantConfigs: DevantTempConfig[]; +} + +export interface InitializeDevantOASConnectionResp { + connectionName?: string; +} + +export interface RegisterDevantMarketplaceServiceReq { + name: string; + idlType: MarketplaceIdlTypes; + serviceType: MarketplaceServiceTypes; + idlFilePath?: string; + + configs: DevantTempConfig[]; +} + +export interface AddDevantTempConfigReq { + name: string; + newLine?: boolean; +} + +export interface AddDevantTempConfigResp { + configNode: ModuleVarDecl; +} + +export interface DeleteDevantTempConfigReq { + nodes: ModuleVarDecl[]; +} + +export interface ReplaceDevantTempConfigValuesReq { + createdConnection: ConnectionDetailed; + configs: DevantTempConfig[]; +} + +export interface PlatformExtConnectionState { + loading?: boolean; + list?: ConnectionListItem[]; + connectedToDevant?: boolean; +} + +export interface PlatformExtState { + isLoggedIn: boolean; + userInfo: UserInfo | null; + hasPossibleComponent?: boolean; + hasLocalChanges?: boolean; + components: ComponentKind[]; + selectedComponent?: ComponentKind; + selectedContext?: ContextItemEnriched; + envs?: Environment[]; + selectedEnv?: Environment; + devantConns?: PlatformExtConnectionState; +} + +export enum DevantConnectionFlow { + // Create related flows + CREATE_INTERNAL_OAS = "CREATE_INTERNAL_OAS", + CREATE_INTERNAL_OTHER = "CREATE_INTERNAL_OTHER", + CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR = "CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR", + CREATE_THIRD_PARTY_OAS = "CREATE_THIRD_PARTY_OAS", + CREATE_THIRD_PARTY_OTHER = "CREATE_THIRD_PARTY_OTHER", + CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR = "CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR", + REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR = "REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR", + REGISTER_CREATE_THIRD_PARTY_FROM_OAS = "REGISTER_CREATE_THIRD_PARTY_FROM_OAS", + // Import related flows + IMPORT_INTERNAL_OAS = "IMPORT_INTERNAL_OAS", + IMPORT_INTERNAL_OTHER = "IMPORT_INTERNAL_OTHER", + IMPORT_INTERNAL_OTHER_SELECT_BI_CONNECTOR = "IMPORT_INTERNAL_OTHER_SELECT_BI_CONNECTOR", + IMPORT_THIRD_PARTY_OAS = "IMPORT_THIRD_PARTY_OAS", + IMPORT_THIRD_PARTY_OTHER = "IMPORT_THIRD_PARTY_OTHER", + IMPORT_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR = "IMPORT_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR", +} + +export interface DevantTempConfig { + id: string; + name: string; + value: string; + isSecret: boolean; + node?: ModuleVarDecl; + description?: string; + type?: string; +} diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/rpc-type.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/rpc-type.ts new file mode 100644 index 0000000000..eaf95bbffb --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/rpc-type.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026, 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 { ComponentKind, ConnectionDetailed, ConnectionListItem, CreateComponentConnectionReq, CreateLocalConnectionsConfigReq, CreateThirdPartyConnectionReq, DeleteLocalConnectionsConfigReq, GetComponentsReq, GetConnectionItemReq, GetConnectionsReq, GetMarketplaceIdlReq, GetMarketplaceItemReq, GetMarketplaceListReq,MarketplaceIdlResp,MarketplaceItem,MarketplaceListResp } from "@wso2/wso2-platform-core" +import { NotificationType, RequestType } from "vscode-messenger-common"; +import { AddDevantTempConfigReq, AddDevantTempConfigResp, DeleteDevantTempConfigReq, GenerateCustomConnectorFromOASReq, GenerateCustomConnectorFromOASResp, InitializeDevantOASConnectionReq, InitializeDevantOASConnectionResp, PlatformExtState, RegisterDevantMarketplaceServiceReq, ReplaceDevantTempConfigValuesReq } from "./interfaces"; + +const _preFix = "platform-ext"; +// BI ext handlers +export const generateCustomConnectorFromOAS: RequestType = { method: `${_preFix}/generateCustomConnectorFromOAS` }; +export const initializeDevantOASConnection: RequestType = { method: `${_preFix}/initializeDevantOASConnection` }; +export const addDevantTempConfig: RequestType = { method: `${_preFix}/addDevantTempConfig` }; +export const deleteDevantTempConfigs: RequestType = { method: `${_preFix}/deleteDevantTempConfigs` }; +export const replaceDevantTempConfigValues: RequestType = { method: `${_preFix}/replaceDevantTempConfigValues` }; + +// Platform ext proxies +export const registerDevantMarketplaceService: RequestType = { method: `${_preFix}/registerDevantMarketplaceService` }; +export const createThirdPartyConnection: RequestType = { method: `${_preFix}/createThirdPartyConnection` }; +export const createInternalConnection: RequestType = { method: `${_preFix}/createInternalConnection` }; +export const getMarketplaceItems: RequestType = { method: `${_preFix}/getMarketplaceItems` }; +export const getMarketplaceItem: RequestType = { method: `${_preFix}/getMarketplaceItem` }; +export const getMarketplaceIdl: RequestType = { method: `${_preFix}/getMarketplaceIdl` }; +export const getConnections: RequestType = { method: `${_preFix}/getConnections` }; +export const getConnection: RequestType = { method: `${_preFix}/getConnection` }; +export const getComponentList: RequestType = { method: `${_preFix}/getComponentList` }; +export const deleteLocalConnectionsConfig: RequestType = { method: `${_preFix}/deleteLocalConnectionsConfig` }; +export const getDevantConsoleUrl: RequestType = { method: `${_preFix}/getDevantConsoleUrl` }; +export const refreshConnectionList: RequestType = { method: `${_preFix}/refreshConnectionList` }; +export const getPlatformStore: RequestType = { method: `${_preFix}/getPlatformStore` }; +export const setConnectedToDevant: RequestType = { method: `${_preFix}/setConnectedToDevant` }; +export const setSelectedComponent: RequestType = { method: `${_preFix}/setSelectedComponent` }; +export const setSelectedEnv: RequestType = { method: `${_preFix}/setSelectedEnv` }; +export const deployIntegrationInDevant: RequestType = { method: `${_preFix}/deployIntegrationInDevant` }; +export const createConnectionConfig: RequestType = { method: `${_preFix}/createConnectionConfig` }; + +// Notifications +export const onPlatformExtStoreStateChange: NotificationType = { method: `${_preFix}/onPlatformExtStoreStateChange` }; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/utils.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/utils.ts new file mode 100644 index 0000000000..a8f41e3275 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/utils.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2026, 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 { DevantScopes } from "@wso2/wso2-platform-core"; + +const INTEGRATION_API_MODULES = ["http", "graphql", "tcp"]; +const EVENT_INTEGRATION_MODULES = ["kafka", "rabbitmq", "salesforce", "trigger.github", "mqtt", "asb"]; +const FILE_INTEGRATION_MODULES = ["ftp", "file"]; +const AI_AGENT_MODULE = "ai"; + +export function findDevantScopeByModule(moduleName: string): DevantScopes | undefined { + if (AI_AGENT_MODULE === moduleName) { + return DevantScopes.AI_AGENT; + } else if (INTEGRATION_API_MODULES.includes(moduleName)) { + return DevantScopes.INTEGRATION_AS_API; + } else if (EVENT_INTEGRATION_MODULES.includes(moduleName)) { + return DevantScopes.EVENT_INTEGRATION; + } else if (FILE_INTEGRATION_MODULES.includes(moduleName)) { + return DevantScopes.FILE_INTEGRATION; + } +} \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/visualizer/index.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/visualizer/index.ts index 529f9571b1..9ae748d13e 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/visualizer/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/visualizer/index.ts @@ -19,13 +19,13 @@ import { HistoryEntry } from "../../history"; import { ProjectStructureArtifactResponse, UpdatedArtifactsResponse } from "../../interfaces/bi"; import { ColorThemeKind } from "../../state-machine-types"; -import { AddToUndoStackRequest, HandleApprovalPopupCloseRequest, JoinProjectPathRequest, JoinProjectPathResponse, OpenViewRequest, ReopenApprovalViewRequest, UndoRedoStateResponse, SaveEvalThreadRequest, SaveEvalThreadResponse } from "./interfaces"; +import { AddToUndoStackRequest, HandleApprovalPopupCloseRequest, JoinProjectPathRequest, JoinProjectPathResponse, OpenViewRequest, ReopenApprovalViewRequest, UndoRedoStateResponse, SaveEvalThreadRequest, SaveEvalThreadResponse, GoBackRequest } from "./interfaces"; export interface VisualizerAPI { openView: (params: OpenViewRequest) => void; getHistory: () => Promise; addToHistory: (entry: HistoryEntry) => void; - goBack: () => void; + goBack: (params: GoBackRequest) => void; goHome: () => void; goSelected: (index: number) => void; undo: (count: number) => Promise; diff --git a/workspaces/ballerina/ballerina-core/src/rpc-types/visualizer/interfaces.ts b/workspaces/ballerina/ballerina-core/src/rpc-types/visualizer/interfaces.ts index 8f5a2e52bb..d1c22cfe2c 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/visualizer/interfaces.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/visualizer/interfaces.ts @@ -53,11 +53,13 @@ export interface AddToUndoStackRequest { export interface JoinProjectPathRequest { segments: string | string[]; codeData?: CodeData; + checkExists?: boolean; } export interface JoinProjectPathResponse { filePath: string; projectPath: string; + exists?: boolean; } export interface HandleApprovalPopupCloseRequest { @@ -77,3 +79,6 @@ export interface SaveEvalThreadResponse { success: boolean; error?: string; } +export interface GoBackRequest { + identifier?: string; +} diff --git a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts index de8db4f618..b99f92f019 100644 --- a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts +++ b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts @@ -57,6 +57,7 @@ export enum SCOPE { EVENT_INTEGRATION = "event-integration", FILE_INTEGRATION = "file-integration", AI_AGENT = "ai-agent", + LIBRARY = "library", ANY = "any" } @@ -175,6 +176,7 @@ export interface ConfigurationCollectorMetadata { name: string; description: string; type?: "string" | "int"; + secret?: boolean; }>; existingValues?: Record; message: string; @@ -469,6 +471,7 @@ export interface ConfigurationCollectionEvent { name: string; description: string; type?: "string" | "int"; + secret?: boolean; }>; existingValues?: Record; message: string; @@ -517,7 +520,7 @@ export const approvalOverlayState: NotificationType = { me export type AIMachineStateValue = | 'Initialize' // (checking auth, first load) | 'Unauthenticated' // (show login window) - | { Authenticating: 'determineFlow' | 'ssoFlow' | 'apiKeyFlow' | 'validatingApiKey' | 'awsBedrockFlow' | 'validatingAwsCredentials' } // hierarchical substates + | { Authenticating: 'determineFlow' | 'ssoFlow' | 'apiKeyFlow' | 'validatingApiKey' | 'awsBedrockFlow' | 'validatingAwsCredentials' | 'vertexAiFlow' | 'validatingVertexAiCredentials' } // hierarchical substates | 'Authenticated' // (ready, main view) | 'Disabled'; // (optional: if AI Chat is globally unavailable) @@ -528,6 +531,8 @@ export enum AIMachineEventType { SUBMIT_API_KEY = 'SUBMIT_API_KEY', AUTH_WITH_AWS_BEDROCK = 'AUTH_WITH_AWS_BEDROCK', SUBMIT_AWS_CREDENTIALS = 'SUBMIT_AWS_CREDENTIALS', + AUTH_WITH_VERTEX_AI = 'AUTH_WITH_VERTEX_AI', + SUBMIT_VERTEX_AI_CREDENTIALS = 'SUBMIT_VERTEX_AI_CREDENTIALS', LOGOUT = 'LOGOUT', SILENT_LOGOUT = "SILENT_LOGOUT", COMPLETE_AUTH = 'COMPLETE_AUTH', @@ -548,6 +553,13 @@ export type AIMachineEventMap = { region: string; sessionToken?: string; }; + [AIMachineEventType.AUTH_WITH_VERTEX_AI]: undefined; + [AIMachineEventType.SUBMIT_VERTEX_AI_CREDENTIALS]: { + projectId: string; + location: string; + clientEmail: string; + privateKey: string; + }; [AIMachineEventType.LOGOUT]: undefined; [AIMachineEventType.SILENT_LOGOUT]: undefined; [AIMachineEventType.COMPLETE_AUTH]: undefined; @@ -691,7 +703,8 @@ export enum TaskStatus { export enum TaskTypes { SERVICE_DESIGN = "service_design", CONNECTIONS_INIT = "connections_init", - IMPLEMENTATION = "implementation" + IMPLEMENTATION = "implementation", + TESTING = "testing" } /** @@ -727,24 +740,19 @@ export type OperationType = "CODE_FOR_USER_REQUIREMENT" | "TESTS_FOR_USER_REQUIR export enum LoginMethod { BI_INTEL = 'biIntel', ANTHROPIC_KEY = 'anthropic_key', - DEVANT_ENV = 'devant_env', - AWS_BEDROCK = 'aws_bedrock' + AWS_BEDROCK = 'aws_bedrock', + VERTEX_AI = 'vertex_ai' } export interface BIIntelSecrets { accessToken: string; - refreshToken: string; + expiresAt?: number; // Unix timestamp in milliseconds } export interface AnthropicKeySecrets { apiKey: string; } -export interface DevantEnvSecrets { - accessToken: string; - expiresAt: number; -} - interface AwsBedrockSecrets { accessKeyId: string; secretAccessKey: string; @@ -752,6 +760,13 @@ interface AwsBedrockSecrets { sessionToken?: string; } +export interface VertexAiSecrets { + projectId: string; + location: string; + clientEmail: string; + privateKey: string; +} + export type AuthCredentials = | { loginMethod: LoginMethod.BI_INTEL; @@ -761,13 +776,13 @@ export type AuthCredentials = loginMethod: LoginMethod.ANTHROPIC_KEY; secrets: AnthropicKeySecrets; } - | { - loginMethod: LoginMethod.DEVANT_ENV; - secrets: DevantEnvSecrets; - } | { loginMethod: LoginMethod.AWS_BEDROCK; secrets: AwsBedrockSecrets; + } + | { + loginMethod: LoginMethod.VERTEX_AI; + secrets: VertexAiSecrets; }; export interface AIUserToken { diff --git a/workspaces/ballerina/ballerina-extension/.env.example b/workspaces/ballerina/ballerina-extension/.env.example index f50f4148a2..272c41e7e7 100644 --- a/workspaces/ballerina/ballerina-extension/.env.example +++ b/workspaces/ballerina/ballerina-extension/.env.example @@ -1,13 +1,2 @@ -# Prod Copilot -BALLERINA_ROOT_URL=https://dev-tools.wso2.com/ballerina-copilot -BALLERINA_AUTH_ORG= -BALLERINA_AUTH_CLIENT_ID= -BALLERINA_AUTH_REDIRECT_URL=https://eae690d5-80c3-4fb7-9bc5-e8d747cca11b.e1-us-east-azure.choreoapps.dev -BALLERINA_DEFAULT_COPLIOT_CODE_API_KEY= -BALLERINA_DEFAULT_COPLIOT_ASK_API_KEY= - -# Dev Copilot -BALLERINA_DEV_COPLIOT_ROOT_URL= -BALLERINA_DEV_COPLIOT_AUTH_ORG= -BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID= -BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL= +COPILOT_ROOT_URL=https://7eff1239-64bb-4663-b256-30a00d187a5c-prod.e1-us-east-azure.choreoapis.dev/copilot +COPILOT_DEV_ROOT_URL=https://7eff1239-64bb-4663-b256-30a00d187a5c-dev.e1-us-east-azure.choreoapis.dev/copilot diff --git a/workspaces/ballerina/ballerina-extension/CHANGELOG.md b/workspaces/ballerina/ballerina-extension/CHANGELOG.md index 90b9d90c25..895f7af027 100644 --- a/workspaces/ballerina/ballerina-extension/CHANGELOG.md +++ b/workspaces/ballerina/ballerina-extension/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to the **Ballerina** extension will be documented in this fi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added + +- **Persist Database Support** — Added support for persist database workflows in BI, including multiple database connections. +- **Library Projects** — Added end-to-end support for library projects, including creation improvements, new overview page, `lib.bal` validator import, publishing to Ballerina Central, and deployment enforcement when deploying workspaces to Devant. +- **BI Copilot** — Added new agent capabilities including library search/get tools, ConfigCollector, test-runner integration, plan-mode toggle, new/old review preview, telemetry insights, and support for agent evaluations. +- **Data Mapper** — Added support for JSON/XML mappings, DSS query input/output mapping generation, module-level construct consolidation, and diagnostics support in clause forms. +- **Developer Experience** — Added support for Devant connections in BI and remote server debugging improvements. + +### Changed + +- **Copilot Authentication & Config** — Migrated Copilot to the Devant auth flow, updated environment keys and pipeline inputs, and improved BI Copilot configuration handling. +- **Editor & Forms** — Implemented new array/map editor experience, introduced dependent type editor behavior for persist forms, and improved record configuration modal UX and layout. +- **Service Designer & Connectors** — Updated FTP service-designer flows and reordered Devant marketplace placement in connector selection views. + +### Fixed + +- **Forms & Validation** — Fixed project create-form validation regressions, if/match form behavior, response editor checkbox issues, loader styling, and form diagnostics handling edge cases. +- **Expression & Type Editing** — Fixed SQL editor rendering for query fields, imported-type import insertion, user-defined type visibility in non-workspace projects, and function-call related create-function action visibility. +- **Service & Resource Flows** — Fixed service designer/configurable view issues, resource header value handling, path sanitization for `.` resource paths, and XML corruption during data-service editing. +- **Copilot & Agent Flow** — Fixed multi-turn chat state persistence, chat agent creation with listener support, config-collector placeholder handling, and login notification issues for default model provider configuration. +- **Security** — Applied vulnerability and dependency security fixes across BI extension components. + + + ## [5.8.0](https://github.com/wso2/vscode-extensions/compare/ballerina-5.7.3...ballerina-5.8.0) - 2026-02-14 ### Added diff --git a/workspaces/ballerina/ballerina-extension/package.json b/workspaces/ballerina/ballerina-extension/package.json index 4488343a49..c4e505e65a 100644 --- a/workspaces/ballerina/ballerina-extension/package.json +++ b/workspaces/ballerina/ballerina-extension/package.json @@ -328,6 +328,9 @@ ], "default": "integrated", "description": "Indicates the terminal kind to launch the debugging process in." + }, + "choreoConnect": { + "description": "Connect with Choreo/Devant when launching the app" } } }, @@ -1309,28 +1312,30 @@ "copyJSLibs": "copyfiles -f ../ballerina-visualizer/build/*.js resources/jslibs && copyfiles -f ../trace-visualizer/build/*.js resources/jslibs" }, "dependencies": { - "@ai-sdk/amazon-bedrock": "4.0.4", - "@ai-sdk/anthropic": "3.0.2", - "@iarna/toml": "2.2.5", - "@types/lodash": "4.14.200", - "@vscode/test-electron": "2.5.2", - "@vscode/vsce": "3.7.0", + "@ai-sdk/amazon-bedrock": "^4.0.52", + "@ai-sdk/anthropic": "^3.0.39", + "@ai-sdk/google-vertex": "^4.0.27", + "@iarna/toml": "^2.2.5", + "@types/lodash": "^4.14.200", + "@vscode/test-electron": "^2.5.2", + "@vscode/vsce": "^3.7.0", "@wso2/ballerina-core": "workspace:*", "@wso2/ballerina-visualizer": "workspace:*", "@wso2/font-wso2-vscode": "workspace:*", "@wso2/syntax-tree": "workspace:*", "@wso2/trace-visualizer": "workspace:*", "@wso2/wso2-platform-core": "workspace:*", - "ai": "6.0.7", - "cors-anywhere": "0.4.4", - "del-cli": "5.1.0", - "dotenv": "16.5.0", - "file-uri-to-path": "2.0.0", - "glob": "11.1.0", - "handlebars": "4.7.8", - "jwt-decode": "4.0.0", + "ai": "^6.0.77", + "cors-anywhere": "^0.4.4", + "del-cli": "^5.1.0", + "dotenv": "~16.5.0", + "file-uri-to-path": "^2.0.0", + "glob": "^11.1.0", + "handlebars": "~4.7.8", + "jwt-decode": "^4.0.0", "lodash": "4.17.23", "monaco-languageclient": "0.13.1-next.9", + "zustand": "5.0.5", "node-fetch": "3.3.2", "node-schedule": "2.1.1", "portfinder": "1.0.32", @@ -1380,6 +1385,7 @@ "ts-loader": "9.5.0", "tslint": "6.1.3", "typescript": "5.8.3", + "@types/js-yaml": "4.0.9", "vscode-debugadapter-testsupport": "1.51.0", "vscode-extension-tester": "5.10.0", "webpack": "5.104.1", diff --git a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts index 8528bf94c3..ed6e9336a7 100644 --- a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts +++ b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts @@ -45,6 +45,7 @@ import { extension } from './BalExtensionContext'; import { registerAgentChatRpcHandlers } from './rpc-managers/agent-chat/rpc-handler'; import { ArtifactsUpdated, ArtifactNotificationHandler } from './utils/project-artifacts-handler'; import { registerMigrateIntegrationRpcHandlers } from './rpc-managers/migrate-integration/rpc-handler'; +import { registerPlatformExtRpcHandlers } from './rpc-managers/platform-ext/rpc-handler'; export class RPCLayer { static _messenger: Messenger = new Messenger(); @@ -92,6 +93,7 @@ export class RPCLayer { registerAiAgentRpcHandlers(RPCLayer._messenger); registerIcpServiceRpcHandlers(RPCLayer._messenger); registerAgentChatRpcHandlers(RPCLayer._messenger); + registerPlatformExtRpcHandlers(RPCLayer._messenger); // ----- AI Webview RPC Methods registerAiPanelRpcHandlers(RPCLayer._messenger); @@ -132,6 +134,7 @@ async function getContext(): Promise { isBI: context.isBI, isInDevant: context.isInDevant, projectPath: context.projectPath, + workspacePath: context.workspacePath, serviceType: context.serviceType, type: context.type, isGraphql: context.isGraphql, 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 d1f9514532..837ad5c068 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extended-language-client.ts @@ -284,7 +284,8 @@ import { WSDLApiClientGenerationRequest, WSDLApiClientGenerationResponse, CopilotSearchLibrariesBySearchRequest, - CopilotSearchLibrariesBySearchResponse + CopilotSearchLibrariesBySearchResponse, + CreateConvertedVariableRequest } from "@wso2/ballerina-core"; import { BallerinaExtension } from "./index"; import { debug, handlePullModuleProgress } from "../utils"; @@ -388,6 +389,7 @@ enum EXTENDED_APIS { DATA_MAPPER_CLAUSE_POSITION = 'dataMapper/clausePosition', DATA_MAPPER_CLEAR_TYPE_CACHE = 'dataMapper/clearTypeCache', DATA_MAPPER_CONVERT_EXPRESSION = 'dataMapper/convertExpression', + DATA_MAPPER_CREATE_CONVERTED_VARIABLE = 'dataMapper/convertType', VIEW_CONFIG_VARIABLES_V2 = 'configEditorV2/getConfigVariables', UPDATE_CONFIG_VARIABLES_V2 = 'configEditorV2/updateConfigVariable', DELETE_CONFIG_VARIABLE_V2 = 'configEditorV2/deleteConfigVariable', @@ -888,6 +890,10 @@ export class ExtendedLangClient extends LanguageClient implements ExtendedLangCl return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CONVERT_EXPRESSION, params); } + async createConvertedVariable(params: CreateConvertedVariableRequest): Promise { + return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CREATE_CONVERTED_VARIABLE, params); + } + async clearTypeCache(): Promise { return this.sendRequest(EXTENDED_APIS.DATA_MAPPER_CLEAR_TYPE_CACHE); } diff --git a/workspaces/ballerina/ballerina-extension/src/core/extension.ts b/workspaces/ballerina/ballerina-extension/src/core/extension.ts index a9f8a7e086..7565c7ee69 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extension.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extension.ts @@ -1693,14 +1693,21 @@ export class BallerinaExtension { debug("[VERSION] Starting Ballerina version detection..."); debug(`[VERSION] Input parameters - ballerinaHome: '${ballerinaHome}', overrideBallerinaHome: ${overrideBallerinaHome}`); - try { - // Initialize with fresh environment - debug("[VERSION] Syncing environment variables..."); - await this.syncEnvironment(); - debug("[VERSION] Environment sync completed"); - } catch (error) { - debug(`[VERSION] Warning: Failed to sync environment: ${error}`); - // Continue anyway, don't fail the whole process + // Use BALLERINA_HOME in WSO2 Integrator if set, otherwise fallback to system PATH + if (process.env.WSO2_INTEGRATOR_RUNTIME && process.env.BALLERINA_HOME) { + debug(`[VERSION] Detected WSO2 Integrator environment with BALLERINA_HOME: ${process.env.BALLERINA_HOME}`); + ballerinaHome = process.env.BALLERINA_HOME; + overrideBallerinaHome = true; + } else { + try { + // Initialize with fresh environment + debug("[VERSION] Syncing environment variables..."); + await this.syncEnvironment(); + debug("[VERSION] Environment sync completed"); + } catch (error) { + debug(`[VERSION] Warning: Failed to sync environment: ${error}`); + // Continue anyway, don't fail the whole process + } } // Log current environment for debugging diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/activator.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/activator.ts index c6c49ed911..e7cc97a483 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/activator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/activator.ts @@ -29,7 +29,8 @@ import { SIGN_IN_BI_COPILOT } from './constants'; import { - REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE, + TOKEN_NOT_AVAILABLE_ERROR_MESSAGE, + NO_AUTH_CREDENTIALS_FOUND, TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL } from '../..//utils/ai/auth'; import { AIStateMachine } from '../../views/ai-panel/aiMachine'; @@ -60,6 +61,12 @@ export interface GenerateAgentForTestResult { export let langClient: ExtendedLangClient; +/** Tracks the active post-login auth subscription so it can be cleaned up before creating a new one. */ +let lastAuthSubscription: { unsubscribe: () => void } | null = null; + +/** How long (ms) to wait for the user to complete login before auto-cancelling the subscription. */ +const AUTH_SUBSCRIPTION_TIMEOUT_MS = 5 * 60 * 1000; + export function activateAIFeatures(ballerinaExternalInstance: BallerinaExtension) { langClient = ballerinaExternalInstance.langClient; @@ -163,9 +170,55 @@ export function activateAIFeatures(ballerinaExternalInstance: BallerinaExtension window.showInformationMessage(DEFAULT_PROVIDER_ADDED); } } catch (error) { - if ((error as Error).message === REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE || (error as Error).message === TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL) { + if ((error as Error).message === TOKEN_NOT_AVAILABLE_ERROR_MESSAGE || (error as Error).message === TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL || (error as Error).message === NO_AUTH_CREDENTIALS_FOUND) { window.showWarningMessage(LOGIN_REQUIRED_WARNING_FOR_DEFAULT_MODEL, SIGN_IN_BI_COPILOT).then(selection => { if (selection === SIGN_IN_BI_COPILOT) { + // Dispose any previous subscription before creating a new one + if (lastAuthSubscription) { + lastAuthSubscription.unsubscribe(); + lastAuthSubscription = null; + } + + let timeoutHandle: ReturnType | null = null; + + // Subscribe to state changes to auto-retry after successful login + const subscription = AIStateMachine.service().subscribe((state) => { + if (state.value === 'Authenticated') { + // Clear timeout and module-scoped reference, then unsubscribe + if (timeoutHandle !== null) { + clearTimeout(timeoutHandle); + timeoutHandle = null; + } + lastAuthSubscription = null; + subscription.unsubscribe(); + // Retry the configuration automatically + addConfigFile(configPath).then(result => { + if (result) { + window.showInformationMessage(DEFAULT_PROVIDER_ADDED); + } + }).catch(retryError => { + window.showErrorMessage(`Failed to configure default model: ${(retryError as Error).message}`); + }); + } + }); + + lastAuthSubscription = subscription; + + // Guard against the user never completing login + timeoutHandle = setTimeout(() => { + if (lastAuthSubscription === subscription) { + lastAuthSubscription = null; + } + subscription.unsubscribe(); + }, AUTH_SUBSCRIPTION_TIMEOUT_MS); + + // If stuck in Authenticating from a previous cancelled login, reset it to allow a new login attempt + const currentState = AIStateMachine.state(); + if (typeof currentState === 'object' && 'Authenticating' in currentState) { + AIStateMachine.service().send(AIMachineEventType.CANCEL_LOGIN); + } + + // Trigger the login flow AIStateMachine.service().send(AIMachineEventType.LOGIN); } }); diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/prompts.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/prompts.ts index 45321533fc..7def14f7eb 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/prompts.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/prompts.ts @@ -21,6 +21,7 @@ import { TASK_WRITE_TOOL_NAME } from "./tools/task-writer"; import { FILE_BATCH_EDIT_TOOL_NAME, FILE_SINGLE_EDIT_TOOL_NAME, FILE_WRITE_TOOL_NAME } from "./tools/text-editor"; import { CONNECTOR_GENERATOR_TOOL } from "./tools/connector-generator"; import { CONFIG_COLLECTOR_TOOL } from "./tools/config-collector"; +import { TEST_RUNNER_TOOL_NAME } from "./tools/test-runner"; import { getLanglibInstructions } from "../utils/libs/langlibs"; import { formatCodebaseStructure, formatCodeContext } from "./utils"; import { GenerateAgentCodeRequest, OperationType, ProjectSource } from "@wso2/ballerina-core"; @@ -72,6 +73,9 @@ This plan will be visible to the user and the execution will be guided on the ta - This step should only contain the Client initialization. 3. 'implementation' - for all the other implementations. Have resource function implementations in its own task. +4. 'testing' +- Responsible for writing test cases that cover the core logic of the implementation. +- Include this task only if the user has explicitly asked for tests. Skip it otherwise. #### Task Breakdown Example 1. Create the HTTP service contract @@ -97,7 +101,8 @@ This plan will be visible to the user and the execution will be guided on the ta - First use ${LIBRARY_SEARCH_TOOL} with relevant keywords to discover available libraries - Then use ${LIBRARY_GET_TOOL} to fetch full details for the discovered libraries - If NO suitable library is found, call ${CONNECTOR_GENERATOR_TOOL} to generate connector from OpenAPI spec - - Before marking the task as completed, use the ${DIAGNOSTICS_TOOL_NAME} tool to check for compilation errors and fix them. Introduce a a new subtask if needed to fix errors. + - Before marking the task as completed, use ${DIAGNOSTICS_TOOL_NAME} to check for compilation errors and fix them. Introduce a new subtask if needed. + - Once compilation is clean and the project contains test cases, run the tests. - Mark task as completed using ${TASK_WRITE_TOOL_NAME} (send ALL tasks) - The tool will wait for TASK COMPLETION APPROVAL from the user - Once approved (success: true), immediately start the next task @@ -110,11 +115,17 @@ This plan will be visible to the user and the execution will be guided on the ta - Keep language simple and non-technical when responding - No need to add manual progress indicators - the task list shows what you're working on +## Test Runner +When running tests: +1. Tell the user what is being tested in one line. +2. Use ${TEST_RUNNER_TOOL_NAME} to run the test suite. +3. Only if there are failures or errors, briefly mention what failed and fix them, then re-run. + ## Edit Mode In the tags, you will see if Edit mode is enabled. When its enabled, you must follow the below instructions strictly. ### Step 1: Create High-Level Design -Create a very high-level and concise design plan for the given user requirement. Avoid using ${TASK_WRITE_TOOL_NAME} tool in this mode. +Silently plan the implementation approach in your reasoning. Do NOT output any design explanation to the user. Avoid using ${TASK_WRITE_TOOL_NAME} tool in this mode. ### Step 2: Identify necessary libraries Identify the libraries required to implement the user requirement. Use ${LIBRARY_SEARCH_TOOL} to discover relevant libraries, then use ${LIBRARY_GET_TOOL} to fetch their full details. @@ -123,9 +134,9 @@ Identify the libraries required to implement the user requirement. Use ${LIBRARY Write/modify the Ballerina code to implement the user requirement. Use the ${FILE_BATCH_EDIT_TOOL_NAME}, ${FILE_SINGLE_EDIT_TOOL_NAME}, ${FILE_WRITE_TOOL_NAME} tools to write/modify the code. ### Step 4: Validate the code -Once the task is done, Always use ${DIAGNOSTICS_TOOL_NAME} tool to check for compilation errors and fix them. -You can use this tool multiple times after making changes to ensure there are no compilation errors. -If you think you can't fix the error after multiple attempts, make sure to keep bring the code into a good state and finish off the task. +Once the code is written, always use ${DIAGNOSTICS_TOOL_NAME} to check for compilation errors and fix them. You may call it multiple times after making changes. +If errors cannot be resolved after multiple attempts, bring the code to a good state and finish the task. +Once compilation is clean and the project contains test cases, run the tests. ### Step 5: Provide a consise summary Once the code is written and validated, provide a very concise summary of the overall changes made. Avoid adding detailed explanations and NEVER create documentations files via ${FILE_WRITE_TOOL_NAME}. @@ -144,7 +155,7 @@ When generating Ballerina code strictly follow these syntax and structure guidel - In the library API documentation, if the service type is specified as generic, adhere to the instructions specified there on writing the service. - For GraphQL service related queries, if the user hasn't specified their own GraphQL Schema, write the proposed GraphQL schema for the user query right after the explanation before generating the Ballerina code. Use the same names as the GraphQL Schema when defining record types. - Some libaries has instructions field in their API documentation. Follow those instructions strictly when using those libraries. -- You should only generate tests if the user explicitly asks for them in the query. You must use the 'ballerina/test' and whatever services associated when writing tests. Respect the instructions field in ballerina/test library and testGenerationInstruction field in whatever library associated with the service in the library API documentation when writing tests. +- When writing tests, use the 'ballerina/test' module and any service-specific test libraries. Respect the instructions field in ballerina/test library and the testGenerationInstruction field in the associated service library API documentation when writing tests. ${getLanglibInstructions()} @@ -152,12 +163,9 @@ ${getLanglibInstructions()} - If the codebase structure shows connector modules in generated/moduleName, import using: import packageName.moduleName ## Code Structure -- Define required configurables for the query. Use only string, int, decimal, boolean types in configurable variables. -- For sensitive configuration values (API keys, tokens, passwords), use ${CONFIG_COLLECTOR_TOOL} in COLLECT mode. Variable names are converted to lowercase without underscores in Config.toml. You MUST use the exact Config.toml names in your Ballerina configurables to avoid runtime errors. -- When generating tests that need configuration values: - - Use COLLECT mode with isTestConfig: true - - The tool will automatically read existing values from Config.toml (if exists), ask user to reuse or modify for testing, and save to tests/Config.toml - - Example: { mode: "collect", variables: [...], isTestConfig: true } +- Define required configurables for the query. Use only string, int, decimal, boolean types in configurable variables. Never assign hardcoded default values to configurables. +- For sensitive configuration values (API keys, tokens, passwords), declare them as Ballerina configurables in the code. Use camelCase names that match exactly between the configurable declaration and Config.toml. +- Use ${CONFIG_COLLECTOR_TOOL} in COLLECT mode only immediately before running or testing — never during code writing. When running tests, use isTestConfig: true. - Initialize any necessary clients with the correct configuration based on the retrieved libraries at the module level (before any function or service declarations). - Implement the main function OR service to address the query requirements. @@ -188,6 +196,7 @@ ${getLanglibInstructions()} - When making replacements inside an existing file, provide the **exact old string** and the **exact new string** with all newlines, spaces, and indentation, being mindful to replace nearby occurrences together to minimize the number of tool calls. - Do NOT create a new markdown file to document each change or summarize your work unless specifically requested by the user. - Do not manually add/modify toml files (Ballerina.toml/Dependencies.toml). For Config.toml configuration management, use ${CONFIG_COLLECTOR_TOOL}. +- NEVER read Config.toml or tests/Config.toml directly. Use ${CONFIG_COLLECTOR_TOOL} CHECK mode to inspect configuration status — actual values must never be visible to you. - Prefer modifying existing bal files over creating new files unless explicitly asked to create a new file in the query. ${getNPSuffix(projects, op)} diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tool-registry.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tool-registry.ts index 003b2a3ce3..d14403111c 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tool-registry.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tool-registry.ts @@ -41,6 +41,7 @@ import { getHealthcareLibraryProviderTool, HEALTHCARE_LIBRARY_PROVIDER_TOOL } fr import { createConnectorGeneratorTool, CONNECTOR_GENERATOR_TOOL } from './tools/connector-generator'; import { LIBRARY_SEARCH_TOOL, getLibrarySearchTool } from './tools/library-search'; import { createConfigCollectorTool, CONFIG_COLLECTOR_TOOL } from './tools/config-collector'; +import { createTestRunnerTool, TEST_RUNNER_TOOL_NAME } from './tools/test-runner'; export interface ToolRegistryOptions { eventHandler: CopilotEventHandler; @@ -101,5 +102,6 @@ export function createToolRegistry(opts: ToolRegistryOptions) { createReadExecute(eventHandler, tempProjectPath) ), [DIAGNOSTICS_TOOL_NAME]: createDiagnosticsTool(tempProjectPath, eventHandler), + [TEST_RUNNER_TOOL_NAME]: createTestRunnerTool(tempProjectPath, eventHandler), }; } diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/config-collector.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/config-collector.ts index 062ee98273..1877e8251f 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/config-collector.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/config-collector.ts @@ -23,7 +23,6 @@ import { CopilotEventHandler } from "../../utils/events"; import { approvalManager } from "../../state/ApprovalManager"; import { ConfigVariable, - createConfigWithPlaceholders, getAllConfigStatus, validateVariableName, writeConfigValuesToConfig, @@ -38,13 +37,14 @@ const CONFIG_FILE_PATH = "Config.toml"; const TEST_CONFIG_FILE_PATH = "tests/Config.toml"; const ConfigVariableSchema = z.object({ - name: z.string().describe("Variable name (e.g., API_KEY)"), + name: z.string().describe("Variable name in camelCase — must match the Ballerina configurable identifier exactly"), description: z.string().describe("Human-readable description"), type: z.enum(["string", "int"]).optional().describe("Data type: string (default) or int"), + secret: z.boolean().optional().describe("Mark as true for sensitive values (API keys, passwords, tokens) to render as a masked input"), }); const ConfigCollectorSchema = z.object({ - mode: z.enum(["create", "create_and_collect", "collect", "check"]).describe("Operation mode"), + mode: z.enum(["collect", "check"]).describe("Operation mode"), filePath: z.string().optional().describe("Path to config file (for check mode)"), variables: z.array(ConfigVariableSchema).optional().describe("Configuration variables"), variableNames: z.array(z.string()).optional().describe("Variable names for check mode"), @@ -52,7 +52,7 @@ const ConfigCollectorSchema = z.object({ }); interface ConfigCollectorInput { - mode: "create" | "create_and_collect" | "collect" | "check"; + mode: "collect" | "check"; filePath?: string; variables?: ConfigVariable[]; variableNames?: string[]; @@ -113,68 +113,30 @@ export function createConfigCollectorTool( ) { return tool({ description: ` -Manages configuration values in Config.toml for Ballerina integrations securely. Use this tool when user requirements involve API keys, passwords, database credentials, or other sensitive configuration. +Manages configuration values in Config.toml for Ballerina integrations securely. -IMPORTANT: Before calling COLLECT or CREATE_AND_COLLECT modes, briefly tell the user what configuration values you need and why. +IMPORTANT: Only call COLLECT mode immediately before executing the project (running or testing). Do NOT call it during code writing or implementation — even if the code has sensitive configurables. Write the code first, then collect config only when you are about to run or test. Operation Modes: -1. CREATE: Create Config.toml with placeholder variables only - - If file already exists, returns current variable status (same as check mode) instead of overwriting - - Use when you need to set up config structure before collecting configuration values - - Example: { mode: "create", variables: [{ name: "API_KEY", description: "Stripe API key", type: "string" }] } - -2. CREATE_AND_COLLECT: Create config AND immediately request configuration values (most efficient) - - If file already exists, skips create and goes straight to collect - - Use for new integrations that need configuration values right away - - Tell user first what you need - - Example: { mode: "create_and_collect", variables: [{ name: "API_KEY", description: "Stripe API key", type: "string" }] } - -3. COLLECT: Request configuration values from user - - Creates Config.toml if it doesn't exist - - Pre-populates existing values for easy editing - - Use when Config.toml already exists and needs configuration values - - Tell user first what you need - - Example: { mode: "collect", variables: [{ name: "API_KEY", description: "Stripe API key", type: "string" }] } - -4. CHECK: Check which configuration values are filled/missing - - Returns status metadata only, NEVER actual configuration values - - Example: { mode: "check", variableNames: ["API_KEY", "DB_PASSWORD"], filePath: "Config.toml" } - - Returns: { success: true, status: { API_KEY: "filled", DB_PASSWORD: "missing" } } - -5. COLLECT with isTestConfig: Request configuration values for test Config.toml - - Use when generating tests that need configuration values - - Set isTestConfig: true - - Tool automatically: - * Reads existing configuration from Config.toml (if exists) - * Writes to tests/Config.toml - * Creates tests/ directory if needed - - UI behavior depends on what's in main config: - * If placeholders found: Shows "Configuration values needed" - * If actual values found: Shows "Found existing values. You can reuse or update them for testing." - * If mixed: Shows "Complete the remaining configuration values" - - Works even if main Config.toml doesn't exist (shows empty form) - - Example: { mode: "collect", variables: [{ name: "API_KEY", description: "Stripe API key", type: "string" }], isTestConfig: true } - -IMPORTANT: When generating tests that use configurables, ALWAYS use isTestConfig: true. -This ensures tests have their own Config.toml in the tests/ directory. - -VARIABLE NAMING (CRITICAL): -Variable names are converted: API_KEY → apikey, DB_HOST → dbhost (lowercase, no underscores). -You MUST use identical names in Config.toml and Ballerina code. - -Correct: - Tool: { name: "DB_HOST" } - Config.toml: dbhost = "localhost" - Code: configurable string dbhost = ?; - -Incorrect (DO NOT DO): - Tool: { name: "DB_HOST" } - Config.toml: dbhost = "localhost" - Code: configurable string dbHost = ?; // WRONG - mismatch causes runtime error +1. COLLECT: Collect configuration values from the user + - Call ONLY immediately before running or testing the project — never during code writing + - Shows a form; nothing is written until the user confirms. If skipped, no file is created or modified + - Pre-populates from existing Config.toml if it exists + - When running tests, use isTestConfig: true — this is the only collect call needed; writes to tests/Config.toml after user confirms + - Example: { mode: "collect", variables: [{ name: "stripeApiKey", description: "Stripe API key", secret: true }] } + - Example (test): { mode: "collect", variables: [...], isTestConfig: true } + +2. CHECK: Inspect which values are filled or missing — can be called at any time + - Returns status only, never actual values + - Example: { mode: "check", variableNames: ["dbPassword", "apiKey"], filePath: "Config.toml" } + - Returns: { status: { dbPassword: "filled", apiKey: "missing" } } + +VARIABLE NAMING: +Use camelCase names that match exactly the Ballerina configurable identifier. The name is written as-is to Config.toml. SECURITY: - You NEVER see actual configuration values -- Tool returns only status: { API_KEY: "filled" } +- Tool returns only status: { dbPassword: "filled" } - NEVER hardcode configuration values in code`, inputSchema: ConfigCollectorSchema, execute: async (input) => { @@ -243,26 +205,6 @@ export async function ConfigCollectorTool( try { switch (input.mode) { - case "create": - return await handleCreateMode( - input.variables, - paths, - eventHandler, - requestId, - input.isTestConfig, - modifiedFiles - ); - - case "create_and_collect": - return await handleCreateAndCollectMode( - input.variables, - paths, - eventHandler, - requestId, - input.isTestConfig, - modifiedFiles - ); - case "collect": return await handleCollectMode( input.variables, @@ -290,101 +232,6 @@ export async function ConfigCollectorTool( } } -async function handleCreateMode( - variables: ConfigVariable[], - paths: ConfigCollectorPaths, - eventHandler: CopilotEventHandler, - requestId: string, - isTestConfig?: boolean, - modifiedFiles?: string[] -): Promise { - // Validate variable names - const validationError = validateConfigVariables(variables); - if (validationError) { return validationError; } - - const configPath = getConfigPath(paths.tempPath, isTestConfig); - const configFileName = getConfigFileName(isTestConfig); - - // If file already exists, delegate to check mode to inform the agent of current status - if (fs.existsSync(configPath)) { - console.log(`[ConfigCollector] CREATE mode - ${configFileName} already exists, delegating to check mode`); - const variableNames = variables.map((v) => v.name); - const checkResult = await handleCheckMode(variableNames, undefined, paths, isTestConfig); - return { - ...checkResult, - message: `${configFileName} already exists. ${checkResult.message}. Use collect mode to update values.`, - }; - } - - console.log(`[ConfigCollector] CREATE mode - Creating ${configFileName} with placeholders`); - - eventHandler({ - type: "configuration_collection_event", - requestId, - stage: "creating_file", - message: isTestConfig - ? "Creating configuration file for tests..." - : "Creating configuration file...", - isTestConfig, - }); - - createConfigWithPlaceholders(configPath, variables, false); - - if (modifiedFiles && !modifiedFiles.includes(configFileName)) { - modifiedFiles.push(configFileName); - } - - eventHandler({ - type: "configuration_collection_event", - requestId, - stage: "done", - message: isTestConfig - ? "Configuration file for tests created" - : "Configuration file created", - isTestConfig, - }); - - return { - success: true, - message: `Created ${configFileName} with ${variables.length} placeholder variable(s). Use collect mode to request configuration values from user.`, - }; -} - -async function handleCreateAndCollectMode( - variables: ConfigVariable[], - paths: ConfigCollectorPaths, - eventHandler: CopilotEventHandler, - requestId: string, - isTestConfig?: boolean, - modifiedFiles?: string[] -): Promise { - const configPath = getConfigPath(paths.tempPath, isTestConfig); - - // If file already exists, skip create and go straight to collect - if (!fs.existsSync(configPath)) { - const createResult = await handleCreateMode( - variables, - paths, - eventHandler, - requestId, - isTestConfig, - modifiedFiles - ); - if (!createResult.success) { - return createResult; - } - } - - return await handleCollectMode( - variables, - paths, - eventHandler, - requestId, - isTestConfig, - modifiedFiles - ); -} - async function handleCollectMode( variables: ConfigVariable[], paths: ConfigCollectorPaths, @@ -406,36 +253,7 @@ async function handleCollectMode( ? (fs.existsSync(configPath) ? configPath : mainConfigPath) : configPath; - // Capture whether the test config already existed - const testConfigPreExisted = isTestConfig && fs.existsSync(configPath); - - // Create config file if it doesn't exist - if (!fs.existsSync(configPath)) { - console.log(`[ConfigCollector] Creating ${getConfigFileName(isTestConfig)}`); - - // Emit creating_file stage - eventHandler({ - type: "configuration_collection_event", - requestId, - stage: "creating_file", - message: isTestConfig - ? "Setting up tests/Config.toml..." - : "Setting up Config.toml...", - isTestConfig, - }); - - createConfigWithPlaceholders(configPath, variables, false); - - // Track modified file - if (modifiedFiles) { - const configFileName = getConfigFileName(isTestConfig); - if (!modifiedFiles.includes(configFileName)) { - modifiedFiles.push(configFileName); - } - } - } - - // Read existing configuration values from source config (if they exist) + // Read existing configuration values from source config (if they exist) for pre-populating the form const existingValues = readExistingConfigValues( sourceConfigPath, variables.map(v => v.name) @@ -452,9 +270,7 @@ async function handleCollectMode( // Determine the message to show to user const userMessage = isTestConfig ? (analysis.hasActualValues - ? (testConfigPreExisted - ? "Found existing test values. You can update them." - : "Found values from main config. You can reuse or update them for testing.") + ? "Found values from main config. You can reuse or update them for testing." : "Test configuration values needed") : (analysis.hasActualValues ? "Update configuration values" @@ -472,8 +288,6 @@ async function handleCollectMode( ); if (!userResponse.provided) { - // User cancelled - const configFileName = getConfigFileName(isTestConfig); eventHandler({ type: "configuration_collection_event", requestId, @@ -484,8 +298,8 @@ async function handleCollectMode( return { success: false, - message: `User cancelled configuration collection${userResponse.comment ? ": " + userResponse.comment : ""}. ${configFileName} has placeholder values. You can ask user to provide values later using collect mode.`, - error: `User cancelled${userResponse.comment ? ": " + userResponse.comment : ""}`, + message: `User skipped configuration collection${userResponse.comment ? ": " + userResponse.comment : ""}. You can ask user to provide values later using collect mode.`, + error: `User skipped${userResponse.comment ? ": " + userResponse.comment : ""}`, errorCode: "USER_CANCELLED", }; } @@ -545,7 +359,7 @@ async function handleCheckMode( if (!fs.existsSync(configPath)) { return { success: false, - message: `${configFileName} not found. Use create or collect mode to create it.`, + message: `${configFileName} not found. Use collect mode to create it.`, error: "FILE_NOT_FOUND", errorCode: "FILE_NOT_FOUND", }; diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/task-writer.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/task-writer.ts index 904baf6202..c8f394619e 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/task-writer.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/task-writer.ts @@ -36,7 +36,7 @@ export interface TaskWriteResult { export const TaskInputSchema = z.object({ description: z.string().min(1).describe("Clear, actionable description of the task to be implemented"), status: z.enum([TaskStatus.PENDING, TaskStatus.IN_PROGRESS, TaskStatus.COMPLETED]).describe("Current status of the task. Use 'pending' for tasks not started, 'in_progress' when actively working on it, 'completed' when work is finished."), - type: z.enum([TaskTypes.SERVICE_DESIGN, TaskTypes.CONNECTIONS_INIT, TaskTypes.IMPLEMENTATION]).describe("Type of the implementation task. service_design will only generate the http service contract. not the implementation. connections_init will only generate the connection initializations. All of the other tasks will be of type implementation.") + type: z.enum([TaskTypes.SERVICE_DESIGN, TaskTypes.CONNECTIONS_INIT, TaskTypes.IMPLEMENTATION, TaskTypes.TESTING]).describe("Type of the implementation task. service_design: creates the HTTP service contract only (no implementation). connections_init: creates connection/client initializations only. implementation: all other implementation tasks. testing: writing test cases for the implemented logic — include only if the user has explicitly asked for tests.") }); const TaskWriteInputSchema = z.object({ @@ -184,6 +184,15 @@ Rules: } } else if (taskCategories.inProgress.length > 0) { console.log(`[TaskWrite Tool] Task in progress: ${taskCategories.inProgress[0].description}`); + eventHandler({ + type: "tool_result", + toolName: TASK_WRITE_TOOL_NAME, + toolOutput: { + success: true, + message: `Started working on: ${taskCategories.inProgress[0].description}`, + tasks: allTasks + } + }); } } diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts new file mode 100644 index 0000000000..b92930a727 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2026, 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 { tool } from 'ai'; +import { z } from 'zod'; +import child_process from 'child_process'; +import { CopilotEventHandler } from '../../utils/events'; +import { extension } from '../../../../BalExtensionContext'; +import { DIAGNOSTICS_TOOL_NAME } from './diagnostics'; + +export const TEST_RUNNER_TOOL_NAME = "runTests"; + +export interface TestRunResult { + output: string; +} + +const TestRunnerInputSchema = z.object({}); + +/** + * Creates the test runner tool for the AI agent. + * + * Executes `bal test` in the temp project directory and returns the full output + * so the agent can diagnose failures and fix them before completing a task. + * + * @param tempProjectPath - Path to the temporary project directory (agent's working dir) + * @param eventHandler - Event handler to emit tool execution events to the visualizer + * @returns Tool instance for running the Ballerina test suite + */ +export function createTestRunnerTool( + tempProjectPath: string, + eventHandler: CopilotEventHandler +) { + return tool({ + description: `Runs \`bal test\` in the current Ballerina project and returns the raw output. + +**Prerequisites:** The project must compile cleanly. Always run \`${DIAGNOSTICS_TOOL_NAME}\` first and resolve all compilation errors before invoking this tool — tests cannot run on code that does not compile. + +**REQUIRED before calling this tool:** You MUST tell the user what is being tested (e.g. which functions or scenarios the test cases cover). Do NOT invoke this tool without first informing the user. + +**When to use:** +- After compilation is clean and the project contains test cases +- After modifying existing code, to confirm tests still pass +- After writing new test cases, to validate them + +**Output:** Returns the full raw \`bal test\` output. Read the output carefully to identify which tests passed or failed, then fix any failures before marking the task as complete. +`, + inputSchema: TestRunnerInputSchema, + execute: async (_input: Record, context?: { toolCallId?: string }): Promise => { + const toolCallId = context?.toolCallId || `fallback-${Date.now()}`; + + eventHandler({ + type: "tool_call", + toolName: TEST_RUNNER_TOOL_NAME, + toolCallId, + }); + + const result = await runBallerinaTests(tempProjectPath); + + eventHandler({ + type: "tool_result", + toolName: TEST_RUNNER_TOOL_NAME, + toolCallId, + toolOutput: { summary: parseTestSummary(result.output) } + }); + + return result; + } + }); +} + +function parseTestSummary(output: string): string { + const passingMatch = output.match(/(\d+)\s+passing/); + const failingMatch = output.match(/(\d+)\s+failing/); + if (passingMatch) { + const passing = parseInt(passingMatch[1]); + const failing = failingMatch ? parseInt(failingMatch[1]) : 0; + const total = passing + failing; + return `Tests completed: ${passing}/${total} passing`; + } + return "Tests completed"; +} + +/** + * Executes `bal test` in the given directory and parses the output. + */ +async function runBallerinaTests(cwd: string): Promise { + return new Promise((resolve) => { + const balCmd = extension.ballerinaExtInstance.getBallerinaCmd(); + const command = `${balCmd} test`; + + console.log(`[TestRunner] Running: ${command} in ${cwd}`); + + child_process.exec(command, { cwd }, (err, stdout, stderr) => { + const output = [stdout, stderr].filter(Boolean).join('\n').trim(); + + console.log(`[TestRunner] Completed. Exit code: ${err?.code ?? 0}`); + resolve({ output }); + }); + }); +} diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/text-editor.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/text-editor.ts index 1ed9c089ea..1655b3b903 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/text-editor.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/text-editor.ts @@ -86,6 +86,8 @@ const VALID_FILE_EXTENSIONS = [ '.bal', '.toml', '.md', '.sql' ]; +const RESTRICTED_READ_FILES = ['Config.toml']; + const MAX_LINE_LENGTH = 2000; const PREVIEW_LENGTH = 200; @@ -693,6 +695,17 @@ export function createReadExecute( }; } + // Block reads of restricted files (e.g. Config.toml) in any path + const fileName = file_path.replace(/\\/g, '/').split('/').pop() ?? ''; + if (RESTRICTED_READ_FILES.includes(fileName)) { + console.error(`[FileReadTool] Blocked read of restricted file: ${file_path}`); + return { + success: false, + message: `Reading '${file_path}' is not permitted.`, + error: `Error: ${ErrorMessages.INVALID_FILE_PATH}` + }; + } + const fullPath = path.join(tempProjectPath, file_path); // Check if file exists @@ -889,7 +902,8 @@ export function createBatchEditTool(execute: MultiEditExecute) { export function createReadTool(execute: ReadExecute) { return tool({ description: `Reads a file from the local filesystem. - ALWAYS prefer reading files mentioned in the ser’s message in the chat history first. Only use this tool if you need to read a file that is not present in the chat history. + ALWAYS prefer reading files mentioned in the user’s message in the chat history first. Only use this tool if you need to read a file that is not present in the chat history. + NOTE: The following files are restricted and cannot be read: ${RESTRICTED_READ_FILES.join(", ")}. Usage: - The file_path parameter must be an filename only, do not include any directories unless the user specifically requests it. - You can optionally specify a line offset and limit (especially handy for long files). diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/constants.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/constants.ts index b076633d1d..226c8c56b6 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/constants.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/constants.ts @@ -28,3 +28,4 @@ export const ERROR_NO_BALLERINA_SOURCES = "No Ballerina sources"; export const LOGIN_REQUIRED_WARNING = "Please sign in to BI Copilot to use this feature."; export const LOGIN_REQUIRED_WARNING_FOR_DEFAULT_MODEL = "Please sign in to BI Copilot to configure the WSO2 default model provider."; export const DEFAULT_PROVIDER_ADDED = "WSO2 default model provider configuration values were added to the Config.toml file."; +export const LLM_API_BASE_PATH = "/llm-api/v1.0"; diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/state/ApprovalManager.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/state/ApprovalManager.ts index aadec4204e..e17a36db7d 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/state/ApprovalManager.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/state/ApprovalManager.ts @@ -448,12 +448,12 @@ export class ApprovalManager { } this.connectorSpecs.clear(); - // Cancel configuration requests - for (const [requestId, resolver] of this.configurationRequests.entries()) { + // Resolve configuration requests as skipped so callers handle it as a normal skip + for (const [, resolver] of this.configurationRequests.entries()) { if (resolver.timeoutId) { clearTimeout(resolver.timeoutId); } - resolver.reject(error); + resolver.resolve({ provided: false, comment: reason }); } this.configurationRequests.clear(); } diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts index c2210f2296..cd4f493064 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/utils.ts @@ -21,35 +21,29 @@ import path from "path"; import vscode, { Uri, workspace } from 'vscode'; import { StateMachine } from "../../stateMachine"; -import { getRefreshedAccessToken, REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE } from '../../utils/ai/auth'; +import { + getRefreshedAccessToken, + TOKEN_NOT_AVAILABLE_ERROR_MESSAGE, + getAuthCredentials, + isPlatformExtensionAvailable, + isDevantUserLoggedIn, + getPlatformStsToken, + exchangeStsToCopilotToken, + storeAuthCredentials, + NO_AUTH_CREDENTIALS_FOUND +} from '../../utils/ai/auth'; import { AIStateMachine } from '../../views/ai-panel/aiMachine'; import { AIMachineEventType } from '@wso2/ballerina-core/lib/state-machine-types'; -import { CONFIG_FILE_NAME, ERROR_NO_BALLERINA_SOURCES, PROGRESS_BAR_MESSAGE_FROM_WSO2_DEFAULT_MODEL } from './constants'; +import { CONFIG_FILE_NAME, ERROR_NO_BALLERINA_SOURCES, LLM_API_BASE_PATH, PROGRESS_BAR_MESSAGE_FROM_WSO2_DEFAULT_MODEL } from './constants'; import { getCurrentBallerinaProjectFromContext } from '../config-generator/configGenerator'; -import { BallerinaProject, LoginMethod } from '@wso2/ballerina-core'; +import { BallerinaProject, LoginMethod, AuthCredentials } from '@wso2/ballerina-core'; import { BallerinaExtension } from 'src/core'; -import { getAuthCredentials } from '../../utils/ai/auth'; const config = workspace.getConfiguration('ballerina'); const isDevantDev = process.env.CLOUD_ENV === "dev"; -export const BACKEND_URL: string = config.get('rootUrl') || isDevantDev ? process.env.BALLERINA_DEV_COPLIOT_ROOT_URL : process.env.BALLERINA_ROOT_URL; -export const AUTH_ORG: string = config.get('authOrg') || isDevantDev ? process.env.BALLERINA_DEV_COPLIOT_AUTH_ORG : process.env.BALLERINA_AUTH_ORG; -export const AUTH_CLIENT_ID: string = config.get('authClientID') || isDevantDev ? process.env.BALLERINA_DEV_COPLIOT_AUTH_CLIENT_ID : process.env.BALLERINA_AUTH_CLIENT_ID; -export const AUTH_REDIRECT_URL: string = config.get('authRedirectURL') || isDevantDev ? process.env.BALLERINA_DEV_COPLIOT_AUTH_REDIRECT_URL : process.env.BALLERINA_AUTH_REDIRECT_URL; +export const BACKEND_URL: string = config.get('rootUrl') || (isDevantDev ? process.env.COPILOT_DEV_ROOT_URL : process.env.COPILOT_ROOT_URL); -export const DEVANT_STS_TOKEN_CONFIG: string = config.get('cloudStsToken') || process.env.CLOUD_STS_TOKEN; - -//TODO: Move to configs after custom URL approved -const DEVANT_DEV_EXCHANGE_URL = 'https://e95488c8-8511-4882-967f-ec3ae2a0f86f-dev.e1-us-east-azure.choreoapis.dev/ballerina-copilot/devant-token-exchange-ser/v1.0/exchange'; -const DEVANT_PROD_EXCHANGE_URL = 'https://e95488c8-8511-4882-967f-ec3ae2a0f86f-prod.e1-us-east-azure.choreoapis.dev/ballerina-copilot/devant-token-exchange-ser/v1.0/exchange'; - -export function getDevantExchangeUrl(): string { - if (isDevantDev) { - return DEVANT_DEV_EXCHANGE_URL; - } else { - return DEVANT_PROD_EXCHANGE_URL; - } -} +export const DEVANT_TOKEN_EXCHANGE_URL: string = BACKEND_URL + "/auth-api/v1.0/auth/token-exchange"; // This refers to old backend before FE Migration. We need to eventually remove this. export const OLD_BACKEND_URL: string = BACKEND_URL + "/v2.0"; @@ -145,36 +139,44 @@ export async function getConfigFilePath(ballerinaExtInstance: BallerinaExtension } export async function getTokenForDefaultModel() { - try { - const credentials = await getAuthCredentials(); + // Priority 1: Check stored credentials + const credentials = await getAuthCredentials(); + if (credentials) { if (!credentials) { - throw new Error('No authentication credentials found.'); + throw new Error(NO_AUTH_CREDENTIALS_FOUND); } // Check login method and handle accordingly if (credentials.loginMethod === LoginMethod.BI_INTEL) { - // Keep existing behavior for BI Intel - refresh token + // Re-exchange STS token to get a fresh token const token = await getRefreshedAccessToken(); return token; - } else if (credentials.loginMethod === LoginMethod.DEVANT_ENV) { - // For Devant, return stored access token - return credentials.secrets.accessToken; } else { - // For anything else, show error const errorMessage = 'This feature is only available for BI Intelligence users.'; vscode.window.showErrorMessage(errorMessage); throw new Error(errorMessage); } - } catch (error) { - throw error; } -} -export async function getBackendURL(): Promise { - return new Promise(async (resolve) => { - resolve(OLD_BACKEND_URL); - }); + // Priority 2: No stored credentials — check Devant Platform extension + if (isPlatformExtensionAvailable()) { + const isLoggedIn = await isDevantUserLoggedIn(); + if (isLoggedIn) { + const stsToken = await getPlatformStsToken(); + if (stsToken) { + const secrets = await exchangeStsToCopilotToken(stsToken); + const newCredentials: AuthCredentials = { + loginMethod: LoginMethod.BI_INTEL, + secrets + }; + await storeAuthCredentials(newCredentials); + return secrets.accessToken; + } + } + } + + throw new Error(TOKEN_NOT_AVAILABLE_ERROR_MESSAGE); } // Function to find a file in a case-insensitive way @@ -272,9 +274,17 @@ export async function addConfigFile(configPath: string): Promise { const token: string | null = await getTokenForDefaultModel(); if (token === null) { AIStateMachine.service().send(AIMachineEventType.LOGOUT); - throw new Error(REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE); + throw new Error(TOKEN_NOT_AVAILABLE_ERROR_MESSAGE); + } + const openAiEpUrl = BACKEND_URL + LLM_API_BASE_PATH + "/openai"; + const success = addDefaultModelConfig(configPath, token, openAiEpUrl); + + // Also update tests/Config.toml if a tests folder exists + const testsDir = path.join(configPath, 'tests'); + if (fs.existsSync(testsDir) && fs.statSync(testsDir).isDirectory()) { + addDefaultModelConfig(testsDir, token, openAiEpUrl); } - const success = addDefaultModelConfig(configPath, token, await getBackendURL()); + if (success) { return true; } diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/utils/ai-client.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/utils/ai-client.ts index dd1ecc17da..ce6de54d35 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/utils/ai-client.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/utils/ai-client.ts @@ -16,12 +16,14 @@ import { createAnthropic } from "@ai-sdk/anthropic"; import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; -import { getAccessToken, getLoginMethod, getRefreshedAccessToken, getAwsBedrockCredentials, refreshDevantToken } from "../../../utils/ai/auth"; +import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"; +import { getAccessToken, getLoginMethod, getRefreshedAccessToken, getAwsBedrockCredentials, getVertexAiCredentials } from "../../../utils/ai/auth"; import { AIStateMachine } from "../../../views/ai-panel/aiMachine"; import { BACKEND_URL } from "../utils"; -import { AIMachineEventType, AnthropicKeySecrets, LoginMethod, BIIntelSecrets, DevantEnvSecrets } from "@wso2/ballerina-core"; +import { LLM_API_BASE_PATH } from "../constants"; +import { AIMachineEventType, AnthropicKeySecrets, LoginMethod, BIIntelSecrets } from "@wso2/ballerina-core"; -export const ANTHROPIC_HAIKU = "claude-3-5-haiku-20241022"; +export const ANTHROPIC_HAIKU = "claude-haiku-4-5-20251001"; export const ANTHROPIC_SONNET_4 = "claude-sonnet-4-5-20250929"; type AnthropicModel = @@ -56,7 +58,11 @@ let cachedAnthropic: ReturnType | null = null; let cachedAuthMethod: LoginMethod | null = null; /** - * Reusable fetch function that handles authentication with token refresh + * Reusable fetch function that handles authentication with token refresh. + * Uses tiered refresh strategy for BI_INTEL: + * 1. Try STS token re-exchange via platform extension + * 2. If both fail, logout the user + * * @param input - The URL, Request object, or string to fetch * @param options - Fetch options * @returns Promise @@ -70,17 +76,12 @@ export async function fetchWithAuth(input: string | URL | Request, options: Requ "Content-Type": "application/json", 'User-Agent': 'Ballerina-VSCode-Plugin', 'Connection': 'keep-alive', + 'x-product': 'bi', + 'x-usage-context': 'copilot', + 'x-metadata': JSON.stringify({ isCloudEditor: !!process.env.CLOUD_ENV }), }; - if (credentials && loginMethod === LoginMethod.DEVANT_ENV) { - // For DEVANT_ENV, use Bearer token (exchanged from STS token) - const secrets = credentials.secrets as DevantEnvSecrets; - if (secrets.accessToken && secrets.accessToken.trim() !== "") { - headers["Authorization"] = `Bearer ${secrets.accessToken}`; - } else { - console.warn("DevantEnv access token missing, this may cause authentication issues"); - } - } else if (credentials && loginMethod === LoginMethod.BI_INTEL) { + if (credentials && loginMethod === LoginMethod.BI_INTEL) { // For BI_INTEL, use Bearer token const secrets = credentials.secrets as BIIntelSecrets; headers["Authorization"] = `Bearer ${secrets.accessToken}`; @@ -95,38 +96,36 @@ export async function fetchWithAuth(input: string | URL | Request, options: Requ let response = await fetch(input, options); console.log("Response status: ", response.status); - // Handle token expiration for both BI_INTEL and DEVANT_ENV methods + // Handle token expiration for BI_INTEL method with tiered refresh if (response.status === 401) { if (loginMethod === LoginMethod.BI_INTEL) { - console.log("Token expired. Refreshing BI_INTEL token..."); - const newToken = await getRefreshedAccessToken(); - if (newToken) { - options.headers = { - ...options.headers, - 'Authorization': `Bearer ${newToken}`, - }; - response = await fetch(input, options); - } else { - AIStateMachine.service().send(AIMachineEventType.LOGOUT); - return; - } - } else if (loginMethod === LoginMethod.DEVANT_ENV) { - console.log("Token expired. Refreshing DEVANT_ENV token..."); + console.log("Token expired. Attempting tiered refresh for BI_INTEL..."); + try { - const newToken = await refreshDevantToken(); + // Tiered refresh: STS token re-exchange via platform extension + const newToken = await getRefreshedAccessToken(); if (newToken) { + console.log("Token refreshed via STS exchange"); options.headers = { ...options.headers, 'Authorization': `Bearer ${newToken}`, }; response = await fetch(input, options); + + // If still 401 after refresh, logout + if (response.status === 401) { + console.log("Still unauthorized after token refresh. Logging out."); + AIStateMachine.service().send(AIMachineEventType.SILENT_LOGOUT); + return; + } } else { - AIStateMachine.service().send(AIMachineEventType.LOGOUT); + console.log("Token refresh returned null. Logging out."); + AIStateMachine.service().send(AIMachineEventType.SILENT_LOGOUT); return; } - } catch (error) { - console.error("Failed to refresh Devant token:", error); - AIStateMachine.service().send(AIMachineEventType.LOGOUT); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + AIStateMachine.service().send(AIMachineEventType.SILENT_LOGOUT); return; } } @@ -144,7 +143,7 @@ export async function fetchWithAuth(input: string | URL | Request, options: Requ return response; } catch (error: any) { if (error?.message === "TOKEN_EXPIRED") { - AIStateMachine.service().send(AIMachineEventType.LOGOUT); + AIStateMachine.service().send(AIMachineEventType.SILENT_LOGOUT); } else { throw error; } @@ -160,8 +159,8 @@ export const getAnthropicClient = async (model: AnthropicModel): Promise => // Recreate client if login method has changed or no cached instance if (!cachedAnthropic || cachedAuthMethod !== loginMethod) { - let url = BACKEND_URL + "/intelligence-api/v1.0/claude"; - if (loginMethod === LoginMethod.BI_INTEL || loginMethod === LoginMethod.DEVANT_ENV) { + let url = BACKEND_URL + LLM_API_BASE_PATH + "/claude"; + if (loginMethod === LoginMethod.BI_INTEL) { cachedAnthropic = createAnthropic({ baseURL: url, apiKey: "xx", // dummy value; real auth is via fetchWithAuth @@ -203,6 +202,34 @@ export const getAnthropicClient = async (model: AnthropicModel): Promise => const bedrockModelId = `${regionalPrefix}.${baseModelId}`; return bedrock(bedrockModelId); + } else if (loginMethod === LoginMethod.VERTEX_AI) { + const vertexCredentials = await getVertexAiCredentials(); + if (!vertexCredentials) { + throw new Error('Vertex AI credentials not found'); + } + + const vertexAnthropic = createVertexAnthropic({ + project: vertexCredentials.projectId, + location: vertexCredentials.location, + googleAuthOptions: { + credentials: { + client_email: vertexCredentials.clientEmail, + private_key: vertexCredentials.privateKey, + }, + }, + }); + + const vertexModelMap: Record = { + [ANTHROPIC_HAIKU]: "claude-3-5-haiku@20241022", + [ANTHROPIC_SONNET_4]: "claude-sonnet-4-5@20250929", + }; + + const vertexModelId = vertexModelMap[model]; + if (!vertexModelId) { + throw new Error(`Unsupported model for Vertex AI: ${model}`); + } + + return vertexAnthropic(vertexModelId); } else { throw new Error(`Unsupported login method: ${loginMethod}`); } @@ -231,6 +258,7 @@ export const getProviderCacheControl = async (): Promise = switch (loginMethod) { case LoginMethod.AWS_BEDROCK: return { bedrock: { cachePoint: { type: 'default' } } }; + case LoginMethod.VERTEX_AI: case LoginMethod.ANTHROPIC_KEY: case LoginMethod.BI_INTEL: default: diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/utils/libs/function-registry.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/utils/libs/function-registry.ts index 44fe02353f..6b22595c50 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/utils/libs/function-registry.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/utils/libs/function-registry.ts @@ -247,6 +247,9 @@ Now, based on the provided libraries, clients, and functions, and the user query messages: messages, schema: getFunctionsResponseSchema, abortSignal: new AbortController().signal, + providerOptions: { + anthropic: { structuredOutputMode: 'jsonTool' }, + }, }); const libList = object as GetFunctionsResponse; @@ -836,6 +839,9 @@ Think step-by-step to choose the required types in order to solve the given ques messages: messages, schema: getTypesResponseSchema, abortSignal: new AbortController().signal, + providerOptions: { + anthropic: { structuredOutputMode: 'jsonTool' }, + }, }); const libList = object as GetTypesResponse; diff --git a/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts b/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts index 94c158ff4c..54e86cbd7e 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/bi/activator.ts @@ -156,12 +156,12 @@ export function activate(context: BallerinaExtension) { commands.registerCommand(BI_COMMANDS.TOGGLE_TRACE_LOGS, toggleTraceLogs); - commands.registerCommand(BI_COMMANDS.CREATE_BI_PROJECT, (params) => { + commands.registerCommand(BI_COMMANDS.CREATE_BI_PROJECT, async (params) => { let path: string; if (params.createAsWorkspace) { - path = createBIWorkspace(params); + path = await createBIWorkspace(params); } else { - path = createBIProjectPure(params); + path = await createBIProjectPure(params); } return 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 44b15bd004..fbe1b8472e 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts @@ -69,6 +69,7 @@ import { prepareAndGenerateConfig, cleanAndValidateProject } from '../config-gen import { extension } from '../../BalExtensionContext'; import * as fs from 'fs'; import { findHighestVersionJdk } from '../../utils/server/server'; +import { PlatformExtRpcManager } from '../../rpc-managers/platform-ext/rpc-manager'; const BALLERINA_COMMAND = "ballerina.command"; const EXTENDED_CLIENT_CAPABILITIES = "capabilities"; @@ -94,7 +95,11 @@ class DebugConfigProvider implements DebugConfigurationProvider { if (config.noDebug && (extension.ballerinaExtInstance.enabledRunFast() || StateMachine.context().isBI)) { await handleMainFunctionParams(config); } - return getModifiedConfigs(_folder, config); + const configs = await getModifiedConfigs(_folder, config); + + // connect to Devant if applicable + await new PlatformExtRpcManager().setupDevantProxyForDebugging(configs); + return configs; } } @@ -669,7 +674,7 @@ class BIRunAdapter extends LoggingDebugSession { } // Use the current process environment which should have the updated PATH - const env = process.env; + const env = { ...process.env, ...((args as any)?.env || {}) }; debugLog(`[BIRunAdapter] Creating shell execution with env. PATH length: ${env.PATH?.length || 0}`); // Determine the correct working directory for the task diff --git a/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts b/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts index 29450ad835..56f168b91f 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts @@ -19,21 +19,21 @@ import { BI_COMMANDS, DIRECTORY_MAP, EVENT_TYPE, MACHINE_VIEW, SCOPE, findScopeByModule } from "@wso2/ballerina-core"; import { CommandIds as PlatformCommandIds, - IWso2PlatformExtensionAPI, - ICommitAndPuhCmdParams, + ICommitAndPushCmdParams, ICreateComponentCmdParams, } from "@wso2/wso2-platform-core"; import { BallerinaExtension } from "../../core"; import { openView, StateMachine } from "../../stateMachine"; -import { commands, extensions, window } from "vscode"; +import { commands, window } from "vscode"; import * as path from "path"; import * as fs from "fs"; import { debug } from "../../utils"; +import { getPlatformExtensionAPI } from "../../utils/ai/auth"; export function activateDevantFeatures(_ballerinaExtInstance: BallerinaExtension) { const cloudToken = process.env.CLOUD_STS_TOKEN; if (cloudToken) { - // Set the connection token context + // Set the connection token context for Devant UI features commands.executeCommand("setContext", "devant.editor", true); } @@ -46,19 +46,15 @@ const handleComponentPushToDevant = async () => { return; } - const platformExt = extensions.getExtension("wso2.wso2-platform"); - if (!platformExt) { + const platformExtAPI = await getPlatformExtensionAPI(); + if (!platformExtAPI) { return; } - if (!platformExt.isActive) { - await platformExt.activate(); - } - const platformExtAPI: IWso2PlatformExtensionAPI = platformExt.exports; if (isGitRepo(projectRoot)) { // push changes to repo if component for the directory already exists await commands.executeCommand(PlatformCommandIds.CommitAndPushToGit, { componentPath: projectRoot, - } as ICommitAndPuhCmdParams); + } as ICommitAndPushCmdParams); } else if (platformExtAPI.getDirectoryComponents(projectRoot)?.length) { debug(`project url: ${projectRoot}`); // push changes to repo if component for the directory already exists @@ -69,7 +65,7 @@ const handleComponentPushToDevant = async () => { } await commands.executeCommand(PlatformCommandIds.CommitAndPushToGit, { componentPath: projectRoot, - } as ICommitAndPuhCmdParams); + } as ICommitAndPushCmdParams); } else { // create a new component if it doesn't exist for the directory if (!StateMachine.context().projectStructure) { @@ -148,20 +144,3 @@ function isGitRepo(dir: string): boolean { } return false; } - -// TODO: -// need to move all platform ext api calls to separate client. -// after that, delete this function -export const getDevantStsToken = async (): Promise => { - try { - const platformExt = extensions.getExtension("wso2.wso2-platform"); - if (!platformExt) { - return ""; - } - const platformExtAPI: IWso2PlatformExtensionAPI = await platformExt.activate(); - const stsToken = await platformExtAPI.getStsToken(); - return stsToken; - } catch (err) { - return ""; - } -}; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-extension/src/features/natural-programming/utils.ts b/workspaces/ballerina/ballerina-extension/src/features/natural-programming/utils.ts index 587e15e149..93e1d95de1 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/natural-programming/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/natural-programming/utils.ts @@ -39,13 +39,14 @@ import { import { isNumber } from 'lodash'; import { HttpStatusCode } from 'axios'; import { AIMachineEventType, BallerinaProject, BIIntelSecrets, LoginMethod } from '@wso2/ballerina-core'; -import { isBallerinaProjectAsync, OLD_BACKEND_URL } from '../ai/utils'; +import { BACKEND_URL, isBallerinaProjectAsync } from '../ai/utils'; import { getCurrentBallerinaProjectFromContext } from '../config-generator/configGenerator'; import { BallerinaExtension } from 'src/core'; -import { getAccessToken as getAccesstokenFromUtils, getLoginMethod, getRefreshedAccessToken, REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE, TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL } from '../../utils/ai/auth'; +import { getAccessToken as getAccesstokenFromUtils, getLoginMethod, getRefreshedAccessToken, TOKEN_NOT_AVAILABLE_ERROR_MESSAGE, TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL } from '../../utils/ai/auth'; import { AIStateMachine } from '../../views/ai-panel/aiMachine'; import { performApiDocsDriftCheck, performDocumentationDriftCheck } from './drift-check'; import { ApiDocsDriftResponse, DocumentationDriftResponse } from './drift-check/schemas'; +import { LLM_API_BASE_PATH } from '../ai/constants'; export async function getLLMDiagnostics(projectPath: string, diagnosticCollection : vscode.DiagnosticCollection): Promise { @@ -463,12 +464,6 @@ export function getPluginConfig(): BallerinaPluginConfig { return vscode.workspace.getConfiguration('ballerina'); } -export async function getBackendURL(): Promise { - return new Promise(async (resolve) => { - resolve(OLD_BACKEND_URL); - }); -} - export async function getAccessToken(): Promise { return new Promise(async (resolve) => { let token: string; @@ -586,7 +581,7 @@ export async function getTokenForNaturalFunction() { } return token; } catch (error) { - if ((error as Error).message === REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE || (error as Error).message === TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL) { + if ((error as Error).message === TOKEN_NOT_AVAILABLE_ERROR_MESSAGE || (error as Error).message === TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL) { vscode.window.showWarningMessage(LOGIN_REQUIRED_WARNING); } throw error; @@ -675,8 +670,8 @@ export async function addConfigFile(configPath: string, isNaturalFunctionsAvaila AIStateMachine.service().send(AIMachineEventType.LOGOUT); return; } - - addDefaultModelConfigForNaturalFunctions(configPath, token, await getBackendURL(), isNaturalFunctionsAvailableInBallerinaOrg); + const openAiEpUrl = BACKEND_URL + LLM_API_BASE_PATH + "/openai"; + addDefaultModelConfigForNaturalFunctions(configPath, token, openAiEpUrl, isNaturalFunctionsAvailableInBallerinaOrg); } catch (error) { AIStateMachine.service().send(AIMachineEventType.LOGOUT); return; diff --git a/workspaces/ballerina/ballerina-extension/src/features/telemetry/index.ts b/workspaces/ballerina/ballerina-extension/src/features/telemetry/index.ts index 14834db27b..0fb1e8d3eb 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/telemetry/index.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/telemetry/index.ts @@ -20,9 +20,7 @@ import TelemetryReporter from "vscode-extension-telemetry"; import { BallerinaExtension } from "../../core"; import { getLoginMethod, getBiIntelId } from "../../utils/ai/auth"; -//Ballerina-VSCode-Extention repo key as default -const DEFAULT_KEY = "3a82b093-5b7b-440c-9aa2-3b8e8e5704e7"; -const INSTRUMENTATION_KEY = process.env.CODE_SERVER_ENV && process.env.VSCODE_CHOREO_INSTRUMENTATION_KEY ? process.env.VSCODE_CHOREO_INSTRUMENTATION_KEY : DEFAULT_KEY; +const INSTRUMENTATION_KEY = process.env.CODE_SERVER_ENV && process.env.VSCODE_CHOREO_INSTRUMENTATION_KEY ? process.env.VSCODE_CHOREO_INSTRUMENTATION_KEY : process.env.APPINSIGHTS_INSTRUMENTATION_KEY; const isWSO2User = process.env.VSCODE_CHOREO_USER_EMAIL ? process.env.VSCODE_CHOREO_USER_EMAIL.endsWith('@wso2.com') : false; const isAnonymous = process.env.VSCODE_CHOREO_USER_EMAIL ? process.env.VSCODE_CHOREO_USER_EMAIL.endsWith('@choreo.dev') : false; const CORRELATION_ID = process.env.VSCODE_CHOREO_CORRELATION_ID ? process.env.VSCODE_CHOREO_CORRELATION_ID : ''; diff --git a/workspaces/ballerina/ballerina-extension/src/features/test-explorer/commands.ts b/workspaces/ballerina/ballerina-extension/src/features/test-explorer/commands.ts index ebac3176b1..651ba96e54 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/test-explorer/commands.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/test-explorer/commands.ts @@ -312,7 +312,7 @@ function hasEvaluationGroup(testFunction: any): boolean { // Check if "evaluations" is in the groups array const hasEvaluation = groupsField.value.some((group: string) => { - return group === EVALUATION_GROUP; + return group.replace(/^"|"$/g, '') === EVALUATION_GROUP; }); return hasEvaluation; } diff --git a/workspaces/ballerina/ballerina-extension/src/features/test-explorer/runner.ts b/workspaces/ballerina/ballerina-extension/src/features/test-explorer/runner.ts index 796559a549..5c8ba4341b 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/test-explorer/runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/test-explorer/runner.ts @@ -18,7 +18,7 @@ */ import { exec } from 'child_process'; -import { CancellationToken, TestRunRequest, TestMessage, TestRun, TestItem, debug, Uri, WorkspaceFolder, DebugConfiguration, workspace, TestRunProfileKind } from 'vscode'; +import { CancellationToken, TestRunRequest, TestMessage, TestRun, TestItem, debug, Uri, WorkspaceFolder, DebugConfiguration, workspace, TestRunProfileKind, commands, window } from 'vscode'; import { EVALUATION_GROUP, testController } from './activator'; import { StateMachine } from "../../stateMachine"; import { isTestFunctionItem, isTestGroupItem, isProjectGroupItem } from './discover'; @@ -26,6 +26,7 @@ import { extension } from '../../BalExtensionContext'; import { constructDebugConfig } from "../debugger"; const fs = require('fs'); import path from 'path'; +import { EvaluationReportWebview } from '../../views/evaluation-report/webview'; /** * Extract project path from a test item @@ -117,8 +118,9 @@ function isAiEvaluations(test: TestItem): boolean { function buildTestCommand(test: TestItem, executor: string, projectName: string | undefined, testCaseNames?: string[]): string { if (isAiEvaluations(test)) { // Evaluations tests use group-based execution with test report + const testsPart = testCaseNames && testCaseNames.length > 0 ? ` --tests ${testCaseNames.join(',')}` : ''; const projectPart = projectName ? ` ${projectName}` : ''; - return `${executor} test --groups ${EVALUATION_GROUP} --test-report --test-report-dir=evaluation-reports${projectPart}`; + return `${executor} test --groups ${EVALUATION_GROUP} --test-report --test-report-dir=evaluation-reports${testsPart}${projectPart}`; } else { // Standard tests use code coverage and optional test filtering const testsPart = testCaseNames && testCaseNames.length > 0 ? ` --tests ${testCaseNames.join(',')}` : ''; @@ -205,7 +207,13 @@ export async function runHandler(request: TestRunRequest, token: CancellationTok const endTime = Date.now(); const timeElapsed = calculateTimeElapsed(startTime, endTime, testItems); - reportTestResults(run, testItems, timeElapsed, projectPath).then(() => { + reportTestResults(run, testItems, timeElapsed, projectPath).then(async () => { + if (isAiEvaluations(test)) { + const reportUri = await findLatestEvaluationReport(workingDirectory); + if (reportUri) { + await openEvaluationReport(reportUri); + } + } endGroup(test, true, run); }).catch(() => { endGroup(test, false, run); @@ -214,7 +222,13 @@ export async function runHandler(request: TestRunRequest, token: CancellationTok const endTime = Date.now(); const timeElapsed = calculateTimeElapsed(startTime, endTime, testItems); - reportTestResults(run, testItems, timeElapsed, projectPath).then(() => { + reportTestResults(run, testItems, timeElapsed, projectPath).then(async () => { + if (isAiEvaluations(test)) { + const reportUri = await findLatestEvaluationReport(workingDirectory); + if (reportUri) { + await openEvaluationReport(reportUri); + } + } endGroup(test, true, run); }).catch(() => { endGroup(test, false, run); @@ -239,7 +253,13 @@ export async function runHandler(request: TestRunRequest, token: CancellationTok const endTime = Date.now(); const timeElapsed = calculateTimeElapsed(startTime, endTime, testItems); - reportTestResults(run, testItems, timeElapsed, projectPath).then(() => { + reportTestResults(run, testItems, timeElapsed, projectPath).then(async () => { + if (isAiEvaluations(test)) { + const reportUri = await findLatestEvaluationReport(workingDirectory); + if (reportUri) { + await openEvaluationReport(reportUri); + } + } endGroup(test, true, run); }).catch(() => { endGroup(test, false, run); @@ -248,7 +268,13 @@ export async function runHandler(request: TestRunRequest, token: CancellationTok const endTime = Date.now(); const timeElapsed = calculateTimeElapsed(startTime, endTime, testItems); - reportTestResults(run, testItems, timeElapsed, projectPath).then(() => { + reportTestResults(run, testItems, timeElapsed, projectPath).then(async () => { + if (isAiEvaluations(test)) { + const reportUri = await findLatestEvaluationReport(workingDirectory); + if (reportUri) { + await openEvaluationReport(reportUri); + } + } endGroup(test, true, run); }).catch(() => { endGroup(test, false, run); @@ -274,7 +300,13 @@ export async function runHandler(request: TestRunRequest, token: CancellationTok const endTime = Date.now(); const timeElapsed = calculateTimeElapsed(startTime, endTime, testItems); - reportTestResults(run, testItems, timeElapsed, projectPath, true).then(() => { + reportTestResults(run, testItems, timeElapsed, projectPath, true).then(async () => { + if (isAiEvaluations(test)) { + const reportUri = await findLatestEvaluationReport(workingDirectory); + if (reportUri) { + await openEvaluationReport(reportUri); + } + } endGroup(test, true, run); }).catch(() => { endGroup(test, false, run); @@ -283,13 +315,17 @@ export async function runHandler(request: TestRunRequest, token: CancellationTok const endTime = Date.now(); const timeElapsed = calculateTimeElapsed(startTime, endTime, testItems); - reportTestResults(run, testItems, timeElapsed, projectPath, true).then(() => { + reportTestResults(run, testItems, timeElapsed, projectPath, true).then(async () => { + if (isAiEvaluations(test)) { + const reportUri = await findLatestEvaluationReport(workingDirectory); + if (reportUri) { + await openEvaluationReport(reportUri); + } + } endGroup(test, true, run); }).catch(() => { endGroup(test, false, run); }); - }).finally(() => { - run.end(); }); } }); @@ -415,6 +451,47 @@ export async function readTestJson(file): Promise { } } +async function findLatestEvaluationReport(workingDirectory: string): Promise { + const reportsDir = path.join(workingDirectory, 'evaluation-reports'); + + if (!fs.existsSync(reportsDir)) { + return undefined; + } + + try { + const files = fs.readdirSync(reportsDir); + const htmlFiles = files + .filter((file: string) => file.endsWith('.html')) + .map((file: string) => ({ + name: file, + path: path.join(reportsDir, file), + mtime: fs.statSync(path.join(reportsDir, file)).mtime + })) + .sort((a: { mtime: Date }, b: { mtime: Date }) => b.mtime.getTime() - a.mtime.getTime()); + + if (htmlFiles.length > 0) { + return Uri.file(htmlFiles[0].path); + } + } catch (error) { + console.error('Error finding evaluation report:', error); + } + + return undefined; +} + +async function openEvaluationReport(reportUri: Uri): Promise { + try { + // Show notification (non-blocking, no button) + window.showInformationMessage('Evaluation report generated'); + + // Open report in webview + await EvaluationReportWebview.createOrShow(reportUri); + } catch (error) { + console.error('Failed to open evaluation report:', error); + window.showErrorMessage('Failed to open evaluation report'); + } +} + function endGroup(test: TestItem, allPassed: boolean, run: TestRun) { if (allPassed) { run.passed(test); diff --git a/workspaces/ballerina/ballerina-extension/src/features/tryit/utils.ts b/workspaces/ballerina/ballerina-extension/src/features/tryit/utils.ts index 4e54f77555..9087135951 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/tryit/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/tryit/utils.ts @@ -28,8 +28,8 @@ export const TRYIT_TEMPLATE = `/* {{#each paths}} {{#each this}} -/* -{{#unless ../../isResourceMode}}#### {{uppercase @key}} {{@../key}}{{/unless}} +{{#unless ../../isResourceMode}}/* +#### {{uppercase @key}} {{@../key}} {{#if parameters}} {{#with (groupParams parameters)}} @@ -56,6 +56,7 @@ export const TRYIT_TEMPLATE = `/* {{/with}} {{/if}} */ +{{/unless}} ### {{uppercase @key}} http://localhost:{{../../port}}{{trim ../../basePath}}{{{@../key}}}{{queryParams parameters}}{{#if parameters}}{{headerParams parameters}}{{/if}} {{#if requestBody}}Content-Type: {{getContentType requestBody}} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-agent/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-agent/rpc-manager.ts index 0ca219219f..c900abee83 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-agent/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-agent/rpc-manager.ts @@ -451,6 +451,8 @@ export class AiAgentRpcManager implements AIAgentAPI { } else { toolsValue = `[${mcpToolKitVarName}]`; } + } else { + toolsValue = `[${mcpToolKitVarName}]`; } // Set the updated tools value diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts index 8ea82657ac..c0bbb2d4c4 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts @@ -48,6 +48,7 @@ import { generateOpenAPI, GenerateOpenAPIRequest, getActiveTempDir, + getAffectedPackages, getAIMachineSnapshot, getChatMessages, getCheckpoints, @@ -57,12 +58,12 @@ import { getGeneratedDocumentation, getLoginMethod, getSemanticDiff, - getAffectedPackages, - isWorkspaceProject, getServiceNames, isCopilotSignedIn, isPlanModeFeatureEnabled, + isPlatformExtensionAvailable, isUserAuthenticated, + isWorkspaceProject, markAlertShown, MetadataWithAttachments, openAIPanel, @@ -91,6 +92,7 @@ import { AiPanelRpcManager } from "./rpc-manager"; export function registerAiPanelRpcHandlers(messenger: Messenger) { const rpcManger = new AiPanelRpcManager(); messenger.onRequest(getLoginMethod, () => rpcManger.getLoginMethod()); + messenger.onRequest(isPlatformExtensionAvailable, () => rpcManger.isPlatformExtensionAvailable()); messenger.onRequest(getDefaultPrompt, () => rpcManger.getDefaultPrompt()); messenger.onRequest(getAIMachineSnapshot, () => rpcManger.getAIMachineSnapshot()); messenger.onNotification(clearInitialPrompt, () => rpcManger.clearInitialPrompt()); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts index a5bff09014..5e42c9633e 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts @@ -46,7 +46,6 @@ import { import * as fs from 'fs'; import path from "path"; import { extensions, workspace } from 'vscode'; -import { URI } from "vscode-uri"; import { isNumber } from "lodash"; import { getServiceDeclarationNames } from "../../../src/features/ai/documentation/utils"; @@ -56,13 +55,11 @@ import { extension } from "../../BalExtensionContext"; import { openChatWindowWithCommand } from "../../features/ai/data-mapper/index"; import { generateDocumentationForService } from "../../features/ai/documentation/generator"; import { generateOpenAPISpec } from "../../features/ai/openapi/index"; -import { OLD_BACKEND_URL } from "../../features/ai/utils"; import { submitFeedback as submitFeedbackUtil } from "../../features/ai/utils/feedback"; import { sendGenerationKeptTelemetry, sendGenerationDiscardTelemetry } from "../../features/ai/utils/generation-response"; -import { fetchWithAuth } from "../../features/ai/utils/ai-client"; import { getLLMDiagnosticArrayAsString } from "../../features/natural-programming/utils"; import { StateMachine, updateView } from "../../stateMachine"; -import { getLoginMethod, loginGithubCopilot } from "../../utils/ai/auth"; +import { getLoginMethod, isPlatformExtensionAvailable, loginGithubCopilot } from "../../utils/ai/auth"; import { normalizeCodeContext } from "../../views/ai-panel/codeContextUtils"; import { refreshDataMapper } from "../data-mapper/utils"; import { @@ -73,6 +70,7 @@ import { addToIntegration, cleanDiagnosticMessages, searchDocumentation } from " import { onHideReviewActions } from '@wso2/ballerina-core'; import { createExecutionContextFromStateMachine, createExecutorConfig, generateAgent } from '../../features/ai/agent/index'; import { integrateCodeToWorkspace } from "../../features/ai/agent/utils"; +import { WI_EXTENSION_ID } from "../../features/ai/constants"; import { ContextTypesExecutor } from '../../features/ai/executors/datamapper/ContextTypesExecutor'; import { FunctionMappingExecutor } from '../../features/ai/executors/datamapper/FunctionMappingExecutor'; import { InlineMappingExecutor } from '../../features/ai/executors/datamapper/InlineMappingExecutor'; @@ -81,7 +79,6 @@ import { cleanupTempProject } from "../../features/ai/utils/project/temp-project import { RPCLayer } from '../../RPCLayer'; import { chatStateStorage } from '../../views/ai-panel/chatStateStorage'; import { restoreWorkspaceSnapshot } from '../../views/ai-panel/checkpoint/checkpointUtils'; -import { WI_EXTENSION_ID } from "../../features/ai/constants"; export class AiPanelRpcManager implements AIPanelAPI { @@ -92,6 +89,10 @@ export class AiPanelRpcManager implements AIPanelAPI { }); } + async isPlatformExtensionAvailable(): Promise { + return isPlatformExtensionAvailable(); + } + async getDefaultPrompt(): Promise { let defaultPrompt: AIPanelPrompt = extension.aiChatDefaultPrompt; diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-handler.ts index dbb0602208..85cf2ef909 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-handler.ts @@ -56,7 +56,9 @@ import { deleteType, DeleteTypeRequest, DeploymentRequest, + WorkspaceDeploymentRequest, deployProject, + deployWorkspace, EndOfFileRequest, ExpressionCompletionsRequest, ExpressionDiagnosticsRequest, @@ -84,6 +86,7 @@ import { getDataMapperCompletions, getDesignModel, getDevantMetadata, + getWorkspaceDevantMetadata, getEnclosedFunction, getEndOfFile, getExpressionCompletions, @@ -198,6 +201,7 @@ export function registerBiDiagramRpcHandlers(messenger: Messenger) { messenger.onNotification(openReadme, (args: OpenReadmeRequest) => rpcManger.openReadme(args)); messenger.onRequest(renameIdentifier, (args: RenameIdentifierRequest) => rpcManger.renameIdentifier(args)); messenger.onRequest(deployProject, (args: DeploymentRequest) => rpcManger.deployProject(args)); + messenger.onRequest(deployWorkspace, (args: WorkspaceDeploymentRequest) => rpcManger.deployWorkspace(args)); messenger.onNotification(openAIChat, (args: AIChatRequest) => rpcManger.openAIChat(args)); messenger.onRequest(getSignatureHelp, (args: SignatureHelpRequest) => rpcManger.getSignatureHelp(args)); messenger.onNotification(buildProject, (args: BuildMode) => rpcManger.buildProject(args)); @@ -237,6 +241,7 @@ export function registerBiDiagramRpcHandlers(messenger: Messenger) { messenger.onRequest(getRecordNames, () => rpcManger.getRecordNames()); messenger.onRequest(getFunctionNames, () => rpcManger.getFunctionNames()); messenger.onRequest(getDevantMetadata, () => rpcManger.getDevantMetadata()); + messenger.onRequest(getWorkspaceDevantMetadata, () => rpcManger.getWorkspaceDevantMetadata()); messenger.onRequest(generateOpenApiClient, (args: OpenAPIClientGenerationRequest) => rpcManger.generateOpenApiClient(args)); messenger.onRequest(getOpenApiGeneratedModules, (args: OpenAPIGeneratedModulesRequest) => rpcManger.getOpenApiGeneratedModules(args)); messenger.onRequest(deleteOpenApiGeneratedModules, (args: OpenAPIClientDeleteRequest) => rpcManger.deleteOpenApiGeneratedModules(args)); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/rpc-manager.ts index bdca95d1f0..c64e505377 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 @@ -70,8 +70,11 @@ import { ValidateProjectFormRequest, ValidateProjectFormResponse, DeploymentRequest, + WorkspaceDeploymentRequest, DeploymentResponse, DevantMetadata, + WorkspaceDevantMetadata, + ProjectDevantMetadata, Diagnostics, EndOfFileRequest, ExpressionCompletionsRequest, @@ -145,13 +148,18 @@ import { AvailableNode, Item, Category, - NodePosition + NodePosition, + PackageTomlValues } from "@wso2/ballerina-core"; import * as fs from "fs"; import * as path from 'path'; import * as vscode from "vscode"; -import { ICreateComponentCmdParams, IWso2PlatformExtensionAPI, CommandIds as PlatformExtCommandIds } from "@wso2/wso2-platform-core"; +import { + ICreateComponentCmdParams, + IWso2PlatformExtensionAPI, + CommandIds as PlatformExtCommandIds +} from "@wso2/wso2-platform-core"; import { ShellExecution, Task, @@ -163,7 +171,6 @@ import { } from "vscode"; import { DebugProtocol } from "vscode-debugprotocol"; import { extension } from "../../BalExtensionContext"; -import { notifyBreakpointChange } from "../../RPCLayer"; import { OLD_BACKEND_URL } from "../../features/ai/utils"; import { fetchWithAuth } from "../../features/ai/utils/ai-client"; import { cleanAndValidateProject, getCurrentBIProject } from "../../features/config-generator/configGenerator"; @@ -171,14 +178,29 @@ import { BreakpointManager } from "../../features/debugger/breakpoint-manager"; import { StateMachine, updateView } from "../../stateMachine"; import { getAccessToken, getLoginMethod } from "../../utils/ai/auth"; import { getCompleteSuggestions } from '../../utils/ai/completions'; -import { README_FILE, addProjectToExistingWorkspace, convertProjectToWorkspace, createBIAutomation, createBIFunction, createBIProjectPure, createBIWorkspace, deleteProjectFromWorkspace, openInVSCode, validateProjectPath } from "../../utils/bi"; +import { + addProjectToExistingWorkspace, + convertProjectToWorkspace, + createBIAutomation, + createBIFunction, + createBIProjectPure, + createBIWorkspace, + deleteProjectFromWorkspace, + openInVSCode +, validateProjectPath } from "../../utils/bi"; import { writeBallerinaFileDidOpen } from "../../utils/modification"; import { updateSourceCode } from "../../utils/source-utils"; import { getView } from "../../utils/state-machine-utils"; +import { PlatformExtRpcManager } from "../platform-ext/rpc-manager"; import { openAIPanelWithPrompt } from "../../views/ai-panel/aiMachine"; -import { chatStateStorage } from "../../views/ai-panel/chatStateStorage"; import { checkProjectDiagnostics, removeUnusedImports } from "../ai-panel/repair-utils"; import { getCurrentBallerinaProject } from "../../utils/project-utils"; +import { CommonRpcManager } from "../common/rpc-manager"; +import * as toml from "@iarna/toml"; +import { readOrWriteReadmeContent } from "./utils"; +import { chatStateStorage } from "../../views/ai-panel/chatStateStorage"; +import { getRepoRoot } from "../platform-ext/platform-utils"; + export class BiDiagramRpcManager implements BIDiagramAPI { OpenConfigTomlRequest: (params: OpenConfigTomlRequest) => Promise; @@ -282,7 +304,7 @@ export class BiDiagramRpcManager implements BIDiagramAPI { const nodeKind = params.flowNode.codedata.node; const skipFormatting = nodeKind === 'DATA_MAPPER_CREATION' || nodeKind === 'FUNCTION_CREATION'; const artifactData = params.artifactData || this.getArtifactDataFromNodeKind(nodeKind); - const artifacts = await updateSourceCode({ textEdits: model.textEdits, artifactData, description: this.getSourceDescription(params)}, params.isHelperPaneChange, skipFormatting); + const artifacts = await updateSourceCode({ textEdits: model.textEdits, artifactData, description: this.getSourceDescription(params) }, params.isHelperPaneChange, skipFormatting); resolve({ artifacts }); } }) @@ -479,7 +501,7 @@ export class BiDiagramRpcManager implements BIDiagramAPI { return new Promise((resolve) => { const fileNameOrPath = params.filePath; let filePath = fileNameOrPath; - if (path.basename(fileNameOrPath) === fileNameOrPath) { + if (!path.isAbsolute(fileNameOrPath)) { filePath = path.join(StateMachine.context().projectPath, fileNameOrPath); } StateMachine.langClient() @@ -641,10 +663,10 @@ export class BiDiagramRpcManager implements BIDiagramAPI { async createProject(params: ProjectRequest): Promise { if (params.createAsWorkspace) { - const workspaceRoot = createBIWorkspace(params); + const workspaceRoot = await createBIWorkspace(params); openInVSCode(workspaceRoot); } else { - const projectRoot = createBIProjectPure(params); + const projectRoot = await createBIProjectPure(params); openInVSCode(projectRoot); } } @@ -812,19 +834,8 @@ export class BiDiagramRpcManager implements BIDiagramAPI { // get next suggestion const copilot_token = await extension.context.secrets.get("GITHUB_COPILOT_TOKEN"); if (!copilot_token) { - let token: string; - const loginMethod = await getLoginMethod(); - if (loginMethod === LoginMethod.BI_INTEL) { - const credentials = await getAccessToken(); - const secrets = credentials.secrets as BIIntelSecrets; - token = secrets.accessToken; - } - if (!token) { - //TODO: Do we need to prompt to login here? If so what? Copilot or Ballerina AI? - resolve(undefined); - return; - } - suggestedContent = await this.getCompletionsWithHostedAI(token, copilotContext); + resolve(undefined); + return; } else { const resp = await getCompleteSuggestions({ prefix: copilotContext.prefix, @@ -910,31 +921,7 @@ export class BiDiagramRpcManager implements BIDiagramAPI { } async handleReadmeContent(params: ReadmeContentRequest): Promise { - return new Promise((resolve) => { - const projectPath = params.projectPath; - const readmePath = projectPath ? path.join(projectPath, README_FILE) : undefined; - if (!readmePath) { - resolve({ content: "" }); - return; - } - if (params.read) { - if (!fs.existsSync(readmePath)) { - resolve({ content: "" }); - } else { - const content = fs.readFileSync(readmePath, "utf8"); - console.log(">>> Read content:", content); - resolve({ content }); - } - } else { - if (!fs.existsSync(readmePath)) { - fs.writeFileSync(readmePath, params.content); - console.log(">>> Created and saved readme.md with content:", params.content); - } else { - fs.writeFileSync(readmePath, params.content); - console.log(">>> Updated readme.md with content:", params.content); - } - } - }); + return readOrWriteReadmeContent(params); } async getExpressionCompletions(params: ExpressionCompletionsRequest): Promise { @@ -971,7 +958,7 @@ export class BiDiagramRpcManager implements BIDiagramAPI { const projectPath = StateMachine.context().projectPath; const showLibraryConfigVariables = extension.ballerinaExtInstance.showLibraryConfigVariables(); - // if params includeLibraries is not set, then use settings + // if params includeLibraries is not set, then use settings const includeLibraries = params?.includeLibraries !== undefined ? params.includeLibraries : showLibraryConfigVariables !== false; @@ -1082,7 +1069,6 @@ export class BiDiagramRpcManager implements BIDiagramAPI { } - async getReadmeContent(params: ReadmeContentRequest): Promise { return new Promise((resolve) => { const projectPath = params.projectPath; @@ -1137,34 +1123,102 @@ export class BiDiagramRpcManager implements BIDiagramAPI { async deployProject(params: DeploymentRequest): Promise { const scopes = params.integrationTypes; - let integrationType: SCOPE; - - if (scopes.length === 1) { - integrationType = scopes[0]; - } else { - // Show a quick pick to select deployment option - const selectedScope = await window.showQuickPick(scopes, { - placeHolder: 'You have different types of artifacts within this integration. Select the artifact type to be deployed' - }); - integrationType = selectedScope as SCOPE; - } + const integrationType = await this.selectIntegrationType(scopes); if (!integrationType) { return { isCompleted: true }; } - const deployementParams: ICreateComponentCmdParams = { + const deploymentParams: ICreateComponentCmdParams = { integrationType: integrationType as any, - buildPackLang: "ballerina", // Example language + buildPackLang: "ballerina", name: path.basename(StateMachine.context().projectPath), componentDir: StateMachine.context().projectPath, extName: "Devant" }; - commands.executeCommand(PlatformExtCommandIds.CreateNewComponent, deployementParams); + await commands.executeCommand(PlatformExtCommandIds.CreateNewComponent, deploymentParams); return { isCompleted: true }; } + async deployWorkspace(params: WorkspaceDeploymentRequest): Promise { + const projectScopes = params.projectScopes; + if (!projectScopes?.length) { + window.showWarningMessage("No deployable projects found in the workspace."); + return { isCompleted: true }; + } + const deploymentParams: ICreateComponentCmdParams[] = []; + + // If there is only one project in the workspace and it has multiple integration types, + // ask the user to pick the type similar to the single project deploy flow. + if (projectScopes.length === 1) { + const { projectPath, integrationTypes } = projectScopes[0]; + + const integrationType = await this.selectIntegrationType(integrationTypes); + + if (!integrationType) { + return { isCompleted: true }; + } + + const deployementParam: ICreateComponentCmdParams = { + integrationType: integrationType as any, + buildPackLang: "ballerina", + name: path.basename(projectPath), + componentDir: projectPath, + extName: "Devant", + supportedIntegrationTypes: integrationTypes as any[] + }; + deploymentParams.push(deployementParam); + } else { + for (const projectScope of projectScopes) { + const { projectPath, integrationTypes } = projectScope; + if (!integrationTypes?.length) { + window.showWarningMessage(`No integration types found for ${path.basename(projectPath)}.`); + continue; + } + + const deployementParam: ICreateComponentCmdParams = { + // Use the first type as default, user can change in the UI + integrationType: integrationTypes[0] as any, + buildPackLang: "ballerina", + name: path.basename(projectPath), + componentDir: projectPath, + extName: "Devant", + // Pass all available types so user can select in the component form + supportedIntegrationTypes: integrationTypes as any[] + }; + deploymentParams.push(deployementParam); + } + } + + if (deploymentParams.length === 0) { + return { isCompleted: true }; + } + + await commands.executeCommand( + PlatformExtCommandIds.CreateMultipleNewComponents, + deploymentParams, + params.rootDirectory + ); + return { isCompleted: true }; + } + + private async selectIntegrationType(integrationTypes: SCOPE[]): Promise { + if (!integrationTypes || integrationTypes.length === 0) { + return undefined; + } + + if (integrationTypes.length === 1) { + return integrationTypes[0]; + } + + const selectedScope = await window.showQuickPick(integrationTypes, { + placeHolder: 'You have different types of artifacts within this integration. Select the artifact type to be deployed' + }); + + return selectedScope as SCOPE; + } + openAIChat(params: AIChatRequest): void { if (params.readme) { openAIPanelWithPrompt({ @@ -1358,6 +1412,15 @@ export class BiDiagramRpcManager implements BIDiagramAPI { }); }; + if (params.nodeType === "connection-node") { + // If its a Devant connection, need to delete it from Devant backend as well + await new PlatformExtRpcManager().deleteBiDevantConnection({ + filePath: params.filePath, + ...params.component + }); + } + + // If there are diagnostics, remove unused imports first, then delete component if (projectDiags.length > 0) { return new Promise((resolve, reject) => { @@ -1587,7 +1650,7 @@ export class BiDiagramRpcManager implements BIDiagramAPI { async getTypes(params: GetTypesRequest): Promise { let filePath = params.filePath; - if (!filePath && StateMachine.context()?.projectPath){ + if (!filePath && StateMachine.context()?.projectPath) { const projectPath = StateMachine.context().projectPath; const ballerinaFiles = await getBallerinaFiles(Uri.file(projectPath).fsPath); filePath = ballerinaFiles.at(0); @@ -1915,6 +1978,86 @@ export class BiDiagramRpcManager implements BIDiagramAPI { } } + async getWorkspaceDevantMetadata(): Promise { + let isLoggedIn = false; + let hasAnyComponent = false; + let hasLocalChanges = false; + const projectsMetadata: ProjectDevantMetadata[] = []; + + try { + // Get workspace structure + const workspaceStructure = await this.getProjectStructure(); + if (!workspaceStructure || !workspaceStructure.workspacePath) { + return { isLoggedIn: false, hasAnyComponent: false, hasLocalChanges: false }; + } + + const repoRoot = getRepoRoot(workspaceStructure.workspacePath); + if (!repoRoot) { + return { isLoggedIn: false, hasAnyComponent: false, hasLocalChanges: false }; + } + + const platformExt = extensions.getExtension("wso2.wso2-platform"); + if (!platformExt) { + // Check for context.yaml as fallback + const contextYamlPath = path.join(repoRoot, ".choreo", "context.yaml"); + const hasContextYaml = fs.existsSync(contextYamlPath); + return { + isLoggedIn: false, + hasAnyComponent: hasContextYaml, + hasLocalChanges: false + }; + } + + const platformExtAPI: IWso2PlatformExtensionAPI = await platformExt.activate(); + isLoggedIn = platformExtAPI.isLoggedIn(); + hasLocalChanges = await platformExtAPI.localRepoHasChanges(repoRoot); + + // Check each project in the workspace + for (const project of workspaceStructure.projects) { + const projectPath = project.projectPath; + const projectName = project.projectTitle || project.projectName; + + let projectHasComponent = false; + let projectHasLocalChanges = false; + + if (isLoggedIn) { + const components = platformExtAPI.getDirectoryComponents(projectPath); + projectHasComponent = components.length > 0; + if (projectHasComponent) { + hasAnyComponent = true; + // Only check local changes for deployed projects + projectHasLocalChanges = await platformExtAPI.localRepoHasChanges(projectPath); + } + } + + projectsMetadata.push({ + projectPath, + projectName, + hasComponent: projectHasComponent, + hasLocalChanges: projectHasLocalChanges + }); + } + + // If not logged in, check for context.yaml as fallback + if (!isLoggedIn) { + const contextYamlPath = path.join(repoRoot, ".choreo", "context.yaml"); + if (fs.existsSync(contextYamlPath)) { + hasAnyComponent = true; + } + } + + return { + isLoggedIn, + hasAnyComponent, + hasLocalChanges, + projectsMetadata + }; + } catch (err) { + console.error("failed to call getWorkspaceDevantMetadata: ", err); + return { isLoggedIn, hasAnyComponent, hasLocalChanges }; + } + } + async getRecordConfig(params: GetRecordConfigRequest): Promise { return new Promise((resolve, reject) => { StateMachine.langClient().getRecordConfig(params).then((res) => { @@ -2005,14 +2148,10 @@ export class BiDiagramRpcManager implements BIDiagramAPI { } async generateOpenApiClient(params: OpenAPIClientGenerationRequest): Promise { - return new Promise((resolve, reject) => { - const projectPath = StateMachine.context().projectPath; - const request: OpenAPIClientGenerationRequest = { - openApiContractPath: params.openApiContractPath, - projectPath: projectPath, - module: params.module - }; - StateMachine.langClient().openApiGenerateClient(request).then(async (res) => { + return new Promise(async (resolve, reject) => { + try { + const res = await StateMachine.langClient().openApiGenerateClient(params); + if (!res.source || !res.source.textEditsMap) { console.error("textEditsMap is undefined or null"); reject(new Error("textEditsMap is undefined or null")); @@ -2034,13 +2173,35 @@ export class BiDiagramRpcManager implements BIDiagramAPI { skipPayloadCheck: true }); console.log(">>> Applied text edits for openapi client"); + + // check if params.openApiContractPath is within the project path + if (params.openApiContractPath.startsWith(params.projectPath)) { + const updatedSpecPath = params.openApiContractPath.replace(params.projectPath, '.'); + // Replace the file path of the openapi spec to be relative path in the toml + const tomlValues = await new CommonRpcManager().getCurrentProjectTomlValues(); + const updatedToml: Partial = { + ...tomlValues, + tool: { + ...tomlValues?.tool, + openapi: tomlValues.tool?.openapi?.map((item) => { + if (item.id === params.module) { + return { ...item, filePath: updatedSpecPath }; + } + return item; + }), + }, + }; + const balTomlPath = path.join(params.projectPath, "Ballerina.toml"); + const updatedTomlContent = toml.stringify(JSON.parse(JSON.stringify(updatedToml))); + fs.writeFileSync(balTomlPath, updatedTomlContent, "utf-8"); + } } resolve({}); - }).catch((error) => { + } catch (error) { console.log(">>> error generating openapi client", error); reject(error); - }); + } }); } @@ -2175,19 +2336,6 @@ export class BiDiagramRpcManager implements BIDiagramAPI { } } -export function getRepoRoot(projectRoot: string): string | undefined { - // traverse up the directory tree until .git directory is found - const gitDir = path.join(projectRoot, ".git"); - if (fs.existsSync(gitDir)) { - return projectRoot; - } - // path is root return undefined - if (projectRoot === path.parse(projectRoot).root) { - return undefined; - } - return getRepoRoot(path.join(projectRoot, "..")); -} - export async function getBallerinaFiles(dir: string): Promise { let files: string[] = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/utils.ts index d87771e59b..76f044e2f3 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/bi-diagram/utils.ts @@ -16,12 +16,15 @@ * under the License. */ -import { NodeProperties } from "@wso2/ballerina-core"; +import { NodeProperties, ReadmeContentRequest, ReadmeContentResponse } from "@wso2/ballerina-core"; import { NodePosition, STNode, traversNode } from "@wso2/syntax-tree"; +import { TextEdit } from "@wso2/ballerina-core"; +import { Position, Range, Uri, workspace, WorkspaceEdit } from "vscode"; +import * as fs from "fs"; +import * as path from 'path'; import { FunctionFindingVisitor } from "../../utils/function-finding-visitor"; -import { Position, Range, Uri, workspace, WorkspaceEdit } from "vscode"; -import { TextEdit } from "@wso2/ballerina-core"; +import { README_FILE } from "../../utils/bi"; export const DATA_MAPPING_FILE_NAME = "data_mappings.bal"; @@ -114,4 +117,58 @@ function findHeaderCommentEndLine(content: string): number { } return headerEndLine; -} \ No newline at end of file +} + +/** + * Resolves the path to the project's README file using case-insensitive matching. + * On case-sensitive filesystems, finds "readme.md", "Readme.md", "README.md", etc. + * @param projectPath - Absolute path to the project directory + * @returns Full path to the existing README file, or undefined if not found + */ +export function resolveReadmePath(projectPath: string): string | undefined { + try { + const entries = fs.readdirSync(projectPath, { withFileTypes: true }); + const match = entries.find( + (e) => e.isFile() && e.name.toLowerCase() === README_FILE.toLowerCase() + ); + return match ? path.join(projectPath, match.name) : undefined; + } catch { + return undefined; + } +} + +/** + * Reads or writes the project's README.md file. + * When `params.read` is true, reads the file at `{projectPath}/README.md` (or any case variant + * such as readme.md) and returns its content. If the file does not exist, returns an empty string. + * When `params.read` is false, writes `params.content` to the README file (creating it if missing + * as README.md) and returns the written content. + * @param params - Request containing `projectPath`, and either `read: true` for read mode + * or `content` for write mode. + * @returns A promise that resolves with `{ content: string }` — the read or written content. + */ +export async function readOrWriteReadmeContent(params: ReadmeContentRequest): Promise { + const projectPath = params.projectPath; + if (!projectPath) { + return { content: "" }; + } + const canonicalPath = path.join(projectPath, README_FILE); + const existingReadmePath = resolveReadmePath(projectPath); + const readmePath = existingReadmePath ?? canonicalPath; + + if (params.read) { + if (!existingReadmePath) { + return { content: "" }; + } + const content = fs.readFileSync(readmePath, "utf8"); + return { content }; + } + + const contentToWrite = params.content ?? ""; + if (!existingReadmePath) { + fs.writeFileSync(canonicalPath, contentToWrite); + } else { + fs.writeFileSync(readmePath, contentToWrite); + } + return { content: contentToWrite }; +} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts index f618654ce4..1cd026337f 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts @@ -19,30 +19,41 @@ */ import { BallerinaDiagnosticsRequest, + ClearWebviewCache, CommandsRequest, + FileOrDirRequest, + GoToSourceRequest, + OpenExternalUrlRequest, + RestoreWebviewCache, + RunExternalCommandRequest, + SetWebviewCache, + SetWebviewCacheRequestParam, + ShowErrorMessageRequest, + showInformationModal, + WorkspaceFileRequest, downloadSelectedSampleFromGithub, executeCommand, experimentalEnabled, - FileOrDirRequest, getBallerinaDiagnostics, getCurrentProjectTomlValues, + getDefaultOrgName, getTypeCompletions, getWorkspaceFiles, getWorkspaceRoot, getWorkspaceType, goToSource, - GoToSourceRequest, + hasCentralPATConfigured, isNPSupported, openExternalUrl, - OpenExternalUrlRequest, + publishToCentral, runBackgroundTerminalCommand, - RunExternalCommandRequest, SampleDownloadRequest, selectFileOrDirPath, selectFileOrFolderPath, showErrorMessage, - ShowErrorMessageRequest, - WorkspaceFileRequest + ShowInfoModalRequest, + showQuickPick, + ShowQuickPickRequest } from "@wso2/ballerina-core"; import { Messenger } from "vscode-messenger"; import { CommonRpcManager } from "./rpc-manager"; @@ -62,7 +73,15 @@ export function registerCommonRpcHandlers(messenger: Messenger) { messenger.onRequest(isNPSupported, () => rpcManger.isNPSupported()); messenger.onRequest(getWorkspaceRoot, () => rpcManger.getWorkspaceRoot()); messenger.onNotification(showErrorMessage, (args: ShowErrorMessageRequest) => rpcManger.showErrorMessage(args)); + messenger.onRequest(showInformationModal, (params: ShowInfoModalRequest) => rpcManger.showInformationModal(params)); + messenger.onRequest(showQuickPick, (params: ShowQuickPickRequest) => rpcManger.showQuickPick(params)); messenger.onRequest(getCurrentProjectTomlValues, () => rpcManger.getCurrentProjectTomlValues()); messenger.onRequest(getWorkspaceType, () => rpcManger.getWorkspaceType()); + messenger.onRequest(SetWebviewCache, (params: SetWebviewCacheRequestParam) => rpcManger.setWebviewCache(params)); + messenger.onRequest(RestoreWebviewCache, (params: string) => rpcManger.restoreWebviewCache(params)); + messenger.onRequest(ClearWebviewCache, (params: string) => rpcManger.clearWebviewCache(params)); messenger.onRequest(downloadSelectedSampleFromGithub, (args: SampleDownloadRequest) => rpcManger.downloadSelectedSampleFromGithub(args)); + messenger.onRequest(getDefaultOrgName, () => rpcManger.getDefaultOrgName()); + messenger.onRequest(publishToCentral, () => rpcManger.publishToCentral()); + messenger.onRequest(hasCentralPATConfigured, () => rpcManger.hasCentralPATConfigured()); } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-manager.ts index 9dc94adda1..aab66b0e95 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-manager.ts @@ -26,44 +26,57 @@ import { CommonRPCAPI, Completion, CompletionParams, + DefaultOrgNameResponse, DiagnosticData, FileOrDirRequest, FileOrDirResponse, GoToSourceRequest, OpenExternalUrlRequest, PackageTomlValues, + PublishToCentralResponse, RunExternalCommandRequest, RunExternalCommandResponse, SampleDownloadRequest, + SettingsTomlValues, ShowErrorMessageRequest, SyntaxTree, TypeResponse, WorkspaceFileRequest, WorkspaceRootResponse, WorkspacesFileResponse, - WorkspaceTypeResponse + WorkspaceTypeResponse, + SetWebviewCacheRequestParam, + ShowInfoModalRequest, + ShowQuickPickRequest, } from "@wso2/ballerina-core"; import child_process from 'child_process'; import path from "path"; import os from "os"; import fs from "fs"; import * as unzipper from 'unzipper'; -import { commands, env, MarkdownString, ProgressLocation, Uri, window, workspace } from "vscode"; +import { commands, env, MarkdownString, ProgressLocation, QuickPickItem, Uri, window, workspace } from "vscode"; import { URI } from "vscode-uri"; +import { parse } from "@iarna/toml"; import { extension } from "../../BalExtensionContext"; import { StateMachine } from "../../stateMachine"; import { getProjectTomlValues, goToSource } from "../../utils"; +import { getUsername } from "../../utils/bi"; import { askFileOrFolderPath, askFilePath, askProjectPath, BALLERINA_INTEGRATOR_ISSUES_URL, findWorkspaceTypeFromWorkspaceFolders, + getFirstBalaPath, + getPublishConfirmation, + getReadmeStatus, + getTargetProjectForPublish, getUpdatedSource, handleDownloadFile, + handleReadmeSetup, selectSampleDownloadPath } from "./utils"; import { VisualizerWebview } from "../../views/visualizer/webview"; @@ -190,6 +203,28 @@ export class CommonRpcManager implements CommonRPCAPI { resolve({ path: "" }); } else { const filePath = selectedFile[0].fsPath; + const projectPath = StateMachine.context().projectPath; + if (projectPath && !filePath.startsWith(projectPath)) { + const resp = await window.showErrorMessage('The selected file is not within your project. Do you want to move it inside the project?', { modal: true }, 'Yes'); + if (resp === 'Yes') { + // Move the file inside the project + const fileName = path.basename(filePath); + const newFilePath = path.join(projectPath, fileName); + // if newFilePath already exists, append a number to the file name + let counter = 1; + let finalFilePath = newFilePath; + while (fs.existsSync(finalFilePath)) { + const parsedPath = path.parse(newFilePath); + finalFilePath = path.join(parsedPath.dir, `${parsedPath.name}-${counter}${parsedPath.ext}`); + counter++; + } + fs.copyFileSync(filePath, finalFilePath); + resolve({ path: finalFilePath }); + return; + } + resolve({ path: "" }); + return; + } resolve({ path: filePath }); } } else { @@ -258,13 +293,21 @@ export class CommonRpcManager implements CommonRPCAPI { window.showErrorMessage(messageWithLink.value); } + async showInformationModal(params: ShowInfoModalRequest): Promise { + return window.showInformationMessage(params?.message, {modal: true}, ...(params?.items || [])); + } + + async showQuickPick(params: ShowQuickPickRequest): Promise { + return window.showQuickPick(params.items, params?.options); + } + async isNPSupported(): Promise { return extension.ballerinaExtInstance.isNPSupported; } async getCurrentProjectTomlValues(): Promise> { const tomlValues = await getProjectTomlValues(StateMachine.context().projectPath); - return tomlValues ?? {}; + return tomlValues ?? {}; } async getWorkspaceType(): Promise { @@ -415,4 +458,130 @@ export class CommonRpcManager implements CommonRPCAPI { } return isSuccess; } + + async setWebviewCache(params: SetWebviewCacheRequestParam): Promise { + await extension.context.workspaceState.update(params.cacheKey, params.data); + } + + async restoreWebviewCache(cacheKey: string): Promise { + return extension.context.workspaceState.get(cacheKey); + } + + async clearWebviewCache(cacheKey: string): Promise { + await extension.context.workspaceState.update(cacheKey, undefined); + } + + async getDefaultOrgName(): Promise { + return { orgName: getUsername() }; + } + + async publishToCentral(): Promise { + const failResponse = (): PublishToCentralResponse => ({ success: false, message: '' }); + + const project = getTargetProjectForPublish(); + if (!project) { + return failResponse(); + } + + const { projectPath, projectName, artifactType } = project; + const readmeStatus = await getReadmeStatus(projectPath); + const confirmation = getPublishConfirmation(projectName, artifactType, readmeStatus); + + const confirmed = await window.showInformationMessage( + confirmation.message, + { modal: true }, + confirmation.primaryButton + ); + if (!confirmed) { + return failResponse(); + } + + const readmeHandled = await handleReadmeSetup(readmeStatus, projectPath, projectName, artifactType); + if (readmeHandled) { + return failResponse(); + } + + const result = await this.packAndPushToCentral(projectPath); + this.showPublishResult(result); + return result; + } + + private async packAndPushToCentral(projectPath: string): Promise { + const result: PublishToCentralResponse = { success: false, message: '' }; + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Publishing project to Ballerina Central', + cancellable: false + }, + async (progress) => { + try { + progress.report({ message: 'Packing...' }); + const packResult = await this.runPackCommand(projectPath); + if (packResult.error) { + result.message = packResult.message ?? ''; + return; + } + + progress.report({ message: 'Publishing...' }); + const balaFilePath = getFirstBalaPath(projectPath); + if (!balaFilePath) { + result.message = 'No publishable artifact found at the target/bala directory'; + return; + } + + const pushResult = await this.runPushCommand(balaFilePath); + if (pushResult.error) { + result.message = pushResult.message ?? ''; + return; + } + result.success = true; + } catch (error) { + console.error('Failed to publish project to Ballerina Central:', error); + } + } + ); + + return result; + } + + private async runPackCommand(projectPath: string): Promise { + return this.runBackgroundTerminalCommand({ command: `bal pack "${projectPath}"` }); + } + + private async runPushCommand(balaFilePath: string): Promise { + return this.runBackgroundTerminalCommand({ command: `bal push "${balaFilePath}"` }); + } + + private showPublishResult(result: PublishToCentralResponse): void { + if (result.success) { + window.showInformationMessage('Project published to ballerina central successfully'); + } else { + window.showErrorMessage(result.message || 'Failed to publish project to Ballerina Central'); + } + } + + async hasCentralPATConfigured(): Promise { + // check if the central PAT is configured in the environment variable + const token = process.env.BALLERINA_CENTRAL_ACCESS_TOKEN; + if (token !== undefined && token !== '') { + return true; + } + + // check if the central PAT is configured in the settings.toml + const settingsTomlFilePath = path.join(os.homedir(), '.ballerina', 'settings.toml'); + if (fs.existsSync(settingsTomlFilePath)) { + const tomlContent = await fs.promises.readFile(settingsTomlFilePath, 'utf-8'); + try { + const tomlValues = parse(tomlContent) as Partial; + const token = tomlValues.central?.accesstoken; + return token !== undefined && token !== ''; + } catch (error) { + return false; + } + } + + return false; + } } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts index d8505f5315..7ea4188f59 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts @@ -18,16 +18,21 @@ import * as os from 'os'; import { NodePosition } from "@wso2/syntax-tree"; -import { Position, Progress, Range, Uri, window, workspace, WorkspaceEdit } from "vscode"; +import { StateMachine } from "../../stateMachine"; +import { Position, Progress, Range, Uri, ViewColumn, window, workspace, WorkspaceEdit } from "vscode"; import { PROJECT_KIND, ProjectInfo, TextEdit, WorkspaceTypeResponse } from "@wso2/ballerina-core"; import axios from 'axios'; import fs from 'fs'; +import * as path from 'path'; + import { checkIsBallerinaPackage, checkIsBallerinaWorkspace, getBallerinaPackages, hasMultipleBallerinaPackages } from '../../utils'; +import { readOrWriteReadmeContent, resolveReadmePath } from '../bi-diagram/utils'; +import { README_FILE } from '../../utils/bi'; export const BALLERINA_INTEGRATOR_ISSUES_URL = "https://github.com/wso2/product-ballerina-integrator/issues"; @@ -90,7 +95,7 @@ export async function askFilePath() { canSelectFiles: true, canSelectFolders: false, canSelectMany: false, - defaultUri: Uri.file(os.homedir()), + defaultUri: Uri.file(StateMachine.context().projectPath ?? os.homedir()), filters: { 'Files': ['yaml', 'json', 'yml', 'graphql', 'wsdl'] }, @@ -250,3 +255,90 @@ export async function findWorkspaceTypeFromWorkspaceFolders(): Promise p.projectPath === projectPath); + if (!target) { + return null; + } + const projectName = target.projectTitle || target.projectName; + const artifactType = target.isLibrary ? 'library' : 'integration'; + return { projectPath, projectName, artifactType }; +} + +export async function getReadmeStatus(projectPath: string): Promise<'missing' | 'empty' | 'ready'> { + if (!isReadmeExists(projectPath)) { + return 'missing'; + } + const { content } = await readOrWriteReadmeContent({ projectPath, read: true }); + return content === '' ? 'empty' : 'ready'; +} + +export function getPublishConfirmation( + projectName: string, + artifactType: string, + readmeStatus: 'missing' | 'empty' | 'ready' +): { message: string; primaryButton: string } { + if (readmeStatus === 'missing') { + return { + message: `"${projectName}" requires a README.md before it can be published to Ballerina Central. Please try again after creating the README.md file.`, + primaryButton: 'Create README' + }; + } + if (readmeStatus === 'empty') { + return { + message: `"${projectName}" contains an empty README.md file. Please enter a description for your ${artifactType} and try again.`, + primaryButton: 'Edit README' + }; + } + return { + message: `Publish "${projectName}" to Ballerina Central? Your ${artifactType} will be made available to the Ballerina community.`, + primaryButton: 'Publish to Central' + }; +} + + +export async function handleReadmeSetup( + readmeStatus: 'missing' | 'empty' | 'ready', + projectPath: string, + projectName: string, + artifactType: string +): Promise { + if (readmeStatus === 'missing') { + const content = `# ${projectName} ${artifactType}\n\nAdd your ${artifactType} description here.`; + await readOrWriteReadmeContent({ projectPath, content, read: false }); + openReadmeInEditor(projectPath); + return true; + } + if (readmeStatus === 'empty') { + openReadmeInEditor(projectPath); + return true; + } + return false; +} + +function openReadmeInEditor(projectPath: string): void { + const readmePath = resolveReadmePath(projectPath) ?? path.join(projectPath, README_FILE); + workspace.openTextDocument(readmePath).then((doc) => { + window.showTextDocument(doc, ViewColumn.Beside); + }); +} + +export function getFirstBalaPath(projectPath: string): string | null { + const balaDirPath = path.join(projectPath, 'target', 'bala'); + if (!fs.existsSync(balaDirPath)) { + return null; + } + const files = fs.readdirSync(balaDirPath); + return files.length > 0 ? path.join(balaDirPath, files[0]) : null; +} + +export function isReadmeExists(projectPath: string): boolean { + const existingReadmePath = resolveReadmePath(projectPath); + return existingReadmePath !== undefined; +} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-handler.ts index b77af293c1..5a82b68dc0 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-handler.ts @@ -29,6 +29,8 @@ import { ConvertExpressionRequest, convertToQuery, ConvertToQueryRequest, + createConvertedVariable, + CreateConvertedVariableRequest, DataMapperModelRequest, DataMapperSourceRequest, deleteClause, @@ -87,5 +89,6 @@ export function registerDataMapperRpcHandlers(messenger: Messenger) { messenger.onRequest(getExpandedDMFromDMModel, (args: DMModelRequest) => rpcManger.getExpandedDMFromDMModel(args)); messenger.onRequest(getProcessTypeReference, (args: ProcessTypeReferenceRequest) => rpcManger.getProcessTypeReference(args)); messenger.onRequest(getConvertedExpression, (args: ConvertExpressionRequest) => rpcManger.getConvertedExpression(args)); + messenger.onRequest(createConvertedVariable, (args: CreateConvertedVariableRequest) => rpcManger.createConvertedVariable(args)); messenger.onRequest(clearTypeCache, () => rpcManger.clearTypeCache()); } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-manager.ts index d09867f659..2a848acb53 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/rpc-manager.ts @@ -27,6 +27,7 @@ import { ConvertExpressionRequest, ConvertExpressionResponse, ConvertToQueryRequest, + CreateConvertedVariableRequest, DataMapperAPI, DataMapperModelRequest, DataMapperModelResponse, @@ -428,4 +429,26 @@ export class DataMapperRpcManager implements DataMapperAPI { }); }); } + + async createConvertedVariable(params: CreateConvertedVariableRequest): Promise { + return new Promise(async (resolve) => { + await StateMachine + .langClient() + .createConvertedVariable(params) + .then((resp) => { + console.log(">>> Data mapper create converted variable response", resp); + updateAndRefreshDataMapper( + resp.textEdits, + params.filePath, + params.codedata, + params.varName, + params.targetField, + params.subMappingName + ) + .then(() => { + resolve({ textEdits: resp.textEdits }); + }); + }); + }); + } } diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts index 9b8e12ca60..98d5ca829c 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts @@ -115,7 +115,7 @@ export async function updateSourceCodeIteratively(updateSourceCodeRequest: Updat const filePaths = Object.keys(textEdits); if (filePaths.length == 1) { - return await updateSourceCode({ ...updateSourceCodeRequest, description: 'Data Mapper Update' }); + return await updateSourceCode({ ...updateSourceCodeRequest, description: 'Data Mapper Update' }, undefined, true); } // TODO: Remove this once the designModelService/publishArtifacts API supports simultaneous file changes @@ -138,7 +138,7 @@ export async function updateSourceCodeIteratively(updateSourceCodeRequest: Updat let updatedArtifacts: ProjectStructureArtifactResponse[]; for (const request of requests) { - updatedArtifacts = await updateSourceCode({ ...request, description: 'Data Mapper Update' }); + updatedArtifacts = await updateSourceCode({ ...request, description: 'Data Mapper Update' }, undefined, true); } return updatedArtifacts; @@ -216,7 +216,7 @@ export async function updateSubMappingSource( name: string ): Promise { try { - await updateSourceCode({ textEdits: textEdits, description: 'Sub Mapping Update' }); + await updateSourceCode({ textEdits: textEdits, description: 'Sub Mapping Update' }, undefined, true); return await fetchSubMappingCodeData(filePath, codedata, name); } catch (error) { console.error(`Failed to update source for sub mapping "${name}" in ${filePath}:`, error); @@ -581,6 +581,17 @@ function processTypeKind( return processTypeReference(type.ref, parentId, model, visitedRefs); } break; + case TypeKind.Json: + case TypeKind.Xml: + if (type.convertedVariable) { + return { + convertedField: processConvertedVariable(type.convertedVariable, model, visitedRefs) + }; + } else if (type.fields) { + return { + fields: processTypeFields(type as RecordType, parentId, model, visitedRefs) + }; + } } return {}; } @@ -721,6 +732,34 @@ function processUnion( }); } +/** + * Processes a converted variable for JSON/XML types + */ +function processConvertedVariable( + convertedVariable: IORoot, + model: DMModel, + visitedRefs: Set +): IOType { + const fieldId = convertedVariable.name; + + if (model.traversingRoot) { + model.focusInputRootMap[fieldId] = model.traversingRoot; + } + + return { + id: fieldId, + name: fieldId, + displayName: convertedVariable.displayName, + typeName: convertedVariable.typeName, + kind: convertedVariable.kind, + category: convertedVariable.category, + isFocused: true, + ...(convertedVariable.optional && { optional: convertedVariable.optional }), + ...(convertedVariable.typeInfo && { typeInfo: convertedVariable.typeInfo }), + ...processTypeKind(convertedVariable, fieldId, model, visitedRefs) + }; +} + /** * Processes a type reference and returns the appropriate IOType structure */ diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/icp-service/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/icp-service/rpc-manager.ts index 4f73cf6b2c..50699876fd 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/icp-service/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/icp-service/rpc-manager.ts @@ -36,7 +36,7 @@ export class ICPServiceRpcManager implements ICPServiceAPI { return new Promise(async (resolve) => { const context = StateMachine.context(); try { - const projectPath: string = context.projectPath; + const projectPath: string = params.projectPath || context.projectPath; const param = { projectPath }; const res: TestSourceEditResponse = await context.langClient.addICP(param); await updateSourceCode({ textEdits: res.textEdits, description: 'ICP Creation' }); @@ -52,7 +52,7 @@ export class ICPServiceRpcManager implements ICPServiceAPI { return new Promise(async (resolve) => { const context = StateMachine.context(); try { - const projectPath: string = context.projectPath; + const projectPath: string = params.projectPath || context.projectPath; const param = { projectPath }; const res: TestSourceEditResponse = await context.langClient.disableICP(param); await updateSourceCode({ textEdits: res.textEdits, description: 'ICP Disable' }); @@ -69,7 +69,7 @@ export class ICPServiceRpcManager implements ICPServiceAPI { return new Promise(async (resolve) => { const context = StateMachine.context(); try { - const projectPath: string = context.projectPath; + const projectPath: string = params.projectPath || context.projectPath; const param = { projectPath }; const res: ICPEnabledResponse = await context.langClient.isIcpEnabled(param); resolve(res); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/platform-store.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/platform-store.ts new file mode 100644 index 0000000000..17c627879b --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/platform-store.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2026, 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 { createStore } from "zustand"; +import { persist } from "zustand/middleware"; +import { + PlatformExtConnectionState, + PlatformExtState, +} from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { getWorkspaceStateStore } from "./platform-utils"; + +interface PlatformExtStore { + state: PlatformExtState; + setState: (params: Partial) => void; + setConnectionState: (params: Partial) => void; +} + +const initialState: PlatformExtState = { + isLoggedIn: false, + userInfo: null, + components: [], + devantConns: { list: [], loading: false, connectedToDevant: true }, +}; + +export const platformExtStore = createStore( + persist( + (set, get) => ({ + state: initialState, + setState: (params: Partial) => { + set(({ state }) => ({ state: { ...state, ...params } })); + }, + setConnectionState: (params: Partial) => { + set(({ state }) => ({ state: { ...state, devantConns: { ...state.devantConns, ...params } } })); + }, + }), + getWorkspaceStateStore("bi-platform-storage"), + ), +); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/platform-utils.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/platform-utils.ts new file mode 100644 index 0000000000..a8bc75f96d --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/platform-utils.ts @@ -0,0 +1,450 @@ +/** + * Copyright (c) 2026, 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 { SyntaxTree } from "@wso2/ballerina-core"; +import { ModulePart, STKindChecker, CaptureBindingPattern } from "@wso2/syntax-tree"; +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { StateMachine } from "../../stateMachine"; +import { Uri, WorkspaceEdit, workspace } from "vscode"; +import { OpenAPIDefinition } from "./types"; +import * as yaml from "js-yaml"; +import { MarketplaceItem } from "@wso2/wso2-platform-core"; +import { extension } from "../../BalExtensionContext"; +import { PersistOptions, createJSONStorage } from "zustand/middleware"; +import Handlebars from "handlebars"; +import { updateSourceCode } from "../../utils"; + +export const getConfigFileUri = () => { + const configBalFile = path.join(StateMachine.context().projectPath, "config.bal"); + const configBalFileUri = Uri.file(configBalFile); + if (!fs.existsSync(configBalFile)) { + // create new config.bal if it doesn't exist + fs.writeFileSync(configBalFile, ""); + } + return configBalFileUri; +}; + +export const addConfigurable = async ( + configBalFileUri: Uri, + params: { configName: string; configEnvName: string }[], +) => { + const configBalEdits = new WorkspaceEdit(); + + // if import doesn't exist, add it + const syntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: configBalFileUri.toString() }, + })) as SyntaxTree; + if ( + !(syntaxTree?.syntaxTree as ModulePart)?.imports?.some((item) => item.source?.includes("import ballerina/os")) + ) { + const balOsImportTemplate = Templates.importBalOs(); + configBalEdits.insert(configBalFileUri, new vscode.Position(0, 0), balOsImportTemplate); + } + + const newConfigEditLine = (syntaxTree?.syntaxTree?.position?.endLine ?? 0) + 1; + configBalEdits.insert(configBalFileUri, new vscode.Position(newConfigEditLine, 0), Templates.emptyLine()); + + for (const item of params) { + const newConfigTemplate = Templates.newEnvConfigurable({ + CONFIG_NAME: item.configName, + CONFIG_ENV_NAME: item.configEnvName, + }); + configBalEdits.insert(configBalFileUri, new vscode.Position(newConfigEditLine, 0), newConfigTemplate); + } + + await updateSourceCode({ + textEdits: { [configBalFileUri.toString()]: configBalEdits.get(configBalFileUri) || [] }, + skipPayloadCheck: true, + }); +}; + +export const addProxyConfigurable = async (configBalFileUri: Uri) => { + const configBalEdits = new WorkspaceEdit(); + + const syntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: configBalFileUri.toString() }, + })) as SyntaxTree; + if ( + !(syntaxTree?.syntaxTree as ModulePart)?.imports?.some((item) => item.source?.includes("import ballerina/http")) + ) { + const importHttpTemplate = Templates.importBalHttp(); + configBalEdits.insert(configBalFileUri, new vscode.Position(0, 0), importHttpTemplate); + } + + if ( + !(syntaxTree?.syntaxTree as ModulePart)?.members?.find( + (member) => + STKindChecker.isModuleVarDecl(member) && + (member.typedBindingPattern?.bindingPattern as CaptureBindingPattern)?.variableName?.value === + "devantProxyConfig", + ) + ) { + const proxyConfigTemplate = Templates.proxyConfigurable(); + const newConfigEditLine = (syntaxTree?.syntaxTree?.position?.endLine ?? 0) + 1; + configBalEdits.insert(configBalFileUri, new vscode.Position(newConfigEditLine, 0), proxyConfigTemplate); + } + + await workspace.applyEdit(configBalEdits); +}; + +export const addConnection = async ( + connectionName: string, + moduleName: string, + securityType: "" | "oauth" | "apikey", + requireProxy: boolean, + configs: { + apiKeyVarName: string; + svsUrlVarName: string; + tokenUrlVarName?: string; + tokenClientIdVarName?: string; + tokenClientSecretVarName?: string; + }, +): Promise<{ connName: string; connFileUri: Uri }> => { + const matchingBalProj = StateMachine.context().projectStructure?.projects?.find( + (item) => item.projectPath === StateMachine.context().projectPath, + ); + if (!matchingBalProj) { + throw new Error(`Failed to find bal project for :${StateMachine.context().projectPath}`); + } + + const packageName = matchingBalProj.projectName; + const connectionBalFile = path.join(StateMachine.context().projectPath, "connections.bal"); + const connectionBalFileUri = Uri.file(connectionBalFile); + if (!fs.existsSync(connectionBalFile)) { + fs.writeFileSync(connectionBalFile, ""); + } + + const connBalEdits = new WorkspaceEdit(); + + // if import doesn't exist, add it + const syntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: connectionBalFileUri.toString() }, + })) as SyntaxTree; + + if ( + !(syntaxTree?.syntaxTree as ModulePart)?.imports?.some((item) => + item.source?.includes(`import ${packageName}/${moduleName}`), + ) + ) { + const connImportTemplate = Templates.importConnection({ PACKAGE_NAME: packageName, MODULE_NAME: moduleName }); + connBalEdits.insert(connectionBalFileUri, new vscode.Position(0, 0), connImportTemplate); + } + + const newConnEditLine = (syntaxTree?.syntaxTree?.position?.endLine ?? 0) + 1; + connBalEdits.insert(connectionBalFileUri, new vscode.Position(newConnEditLine, 0), Templates.emptyLine()); + + let baseName = connectionName?.replaceAll("-", "_").replaceAll(" ", "_"); + let candidate = baseName; + let counter = 1; + while ( + (syntaxTree.syntaxTree as ModulePart)?.members?.some( + (k) => (k.typedBindingPattern?.bindingPattern as CaptureBindingPattern)?.variableName?.value === candidate, + ) + ) { + candidate = `${baseName}${counter}`; + counter++; + } + + let newConnTemplate = ""; + if (securityType === "") { + newConnTemplate = Templates.newConnectionNoSecurity({ + CONNECTION_NAME: candidate, + MODULE_NAME: moduleName, + SERVICE_URL_VAR_NAME: configs.svsUrlVarName, + }); + } else if (securityType === "oauth") { + newConnTemplate = Templates.newConnectionWithOAuth({ + requireProxy, + API_KEY_VAR_NAME: configs.apiKeyVarName, + CONNECTION_NAME: candidate, + MODULE_NAME: moduleName, + SERVICE_URL_VAR_NAME: configs.svsUrlVarName, + CLIENT_ID: configs.tokenClientIdVarName, + CLIENT_SECRET: configs.tokenClientSecretVarName, + TOKEN_URL: configs.tokenUrlVarName, + }); + } else if (securityType === "apikey") { + newConnTemplate = Templates.newConnectionWithApiKey({ + requireProxy, + API_KEY_VAR_NAME: configs.apiKeyVarName, + CONNECTION_NAME: candidate, + MODULE_NAME: moduleName, + SERVICE_URL_VAR_NAME: configs.svsUrlVarName, + }); + } + + connBalEdits.insert(connectionBalFileUri, new vscode.Position(newConnEditLine, 0), newConnTemplate); + + await workspace.applyEdit(connBalEdits); + return { connName: candidate, connFileUri: connectionBalFileUri }; +}; + +export const getYamlString = (yamlString: string) => { + try { + if (/%[0-9A-Fa-f]{2}/.test(yamlString)) { + const decoded = decodeURIComponent(yamlString); + if ( + decoded !== yamlString && + (decoded.includes("\n") || decoded.includes(":") || /openapi/i.test(decoded)) + ) { + return decoded; + } + } + return yamlString; + } catch { + return yamlString; + } +}; + +export const processOpenApiWithApiKeyAuth = (yamlString: string, securityType: "" | "oauth" | "apikey"): string => { + try { + const openApiDefinition = yaml.load(getYamlString(yamlString)) as OpenAPIDefinition; + const oAuthSchemaName = "DevantOAuth2"; + const apiKeySchemaName = "DevantApiKeyAuth"; + + if (!openApiDefinition) { + throw new Error("Invalid YAML: Unable to parse OpenAPI definition"); + } + + if (!openApiDefinition.components) { + openApiDefinition.components = {}; + } + + if (!openApiDefinition.components.securitySchemes && securityType !== "") { + openApiDefinition.components.securitySchemes = {}; + } + + if (securityType === "oauth") { + openApiDefinition.components.securitySchemes[oAuthSchemaName] = { + type: "oauth2", + flows: { + clientCredentials: { + tokenUrl: "tokenURL", + scopes: {}, + }, + }, + }; + } else if (securityType === "apikey") { + openApiDefinition.components.securitySchemes[apiKeySchemaName] = { + type: "apiKey", + in: "header", + name: "Choreo-API-Key", + "x-ballerina-name": "choreoAPIKey", + }; + } + + if (!openApiDefinition.security && securityType !== "") { + openApiDefinition.security = []; + } + if (securityType === "oauth") { + openApiDefinition.security.push({ [oAuthSchemaName]: [], [apiKeySchemaName]: [] }); + } else if (securityType === "apikey") { + openApiDefinition.security.push({ [apiKeySchemaName]: [] }); + } + + if (openApiDefinition.paths) { + for (const path in openApiDefinition.paths) { + for (const method in openApiDefinition.paths[path]) { + if (openApiDefinition.paths[path]?.[method]?.security) { + if (securityType === "oauth") { + openApiDefinition.paths[path]?.[method]?.security.push({ + [oAuthSchemaName]: [], + [apiKeySchemaName]: [], + }); + } else if (securityType === "apikey") { + openApiDefinition.paths[path]?.[method]?.security.push({ [apiKeySchemaName]: [] }); + } + } + } + } + } + + if (!openApiDefinition.servers || openApiDefinition.servers.length === 0) { + openApiDefinition.servers = [{ url: "http://localhost:8080" }]; + } + + openApiDefinition.servers.forEach((server) => { + if (typeof server.url === "string" && server.url.endsWith("/")) { + server.url = server.url.slice(0, -1); + } + }); + + return yaml.dump(openApiDefinition); + } catch (error) { + throw new Error( + `Failed to process OpenAPI definition: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } +}; + +export const getWorkspaceStateStore = (storeName: string): PersistOptions => { + const version = "v1"; + return { + name: `${storeName}-${version}`, + storage: createJSONStorage(() => ({ + getItem: async (name) => { + const value = await extension.context.workspaceState.get(name); + return value ? (value as string) : ""; + }, + removeItem: (name) => extension.context.workspaceState.update(name, undefined), + setItem: (name, value) => extension.context.workspaceState.update(name, value), + })), + skipHydration: true, + }; +}; + +export const Templates = { + emptyLine: () => { + const template = Handlebars.compile(`\n`); + return template({}); + }, + newEnvConfigurable: (params: { CONFIG_NAME: string; CONFIG_ENV_NAME: string }) => { + const template = Handlebars.compile( + `configurable string {{CONFIG_NAME}} = os:getEnv("{{CONFIG_ENV_NAME}}");\n`, + ); + return template(params); + }, + newDefaultEnvConfigurable: (params: { CONFIG_NAME: string }) => { + const template = Handlebars.compile(`configurable string {{CONFIG_NAME}} = ?;\n`); + return template(params); + }, + importBalOs: () => { + const template = Handlebars.compile(`import ballerina/os;\n`); + return template({}); + }, + importBalHttp: () => { + const template = Handlebars.compile(`import ballerina/http;\n`); + return template({}); + }, + importConnection: (params: { PACKAGE_NAME: string; MODULE_NAME: string }) => { + const template = Handlebars.compile(`import {{PACKAGE_NAME}}.{{MODULE_NAME}};\n`); + return template(params); + }, + proxyConfigurable: () => { + const template = Handlebars.compile(` +configurable string? devantProxyHost = (); +configurable int? devantProxyPort = (); +http:ProxyConfig? devantProxyConfig = devantProxyHost is string && devantProxyPort is int ? { host: devantProxyHost, port: devantProxyPort } : (); +\n`); + return template({}); + }, + newConnectionNoSecurity: (params: { + MODULE_NAME: string; + CONNECTION_NAME: string; + SERVICE_URL_VAR_NAME: string; + }) => { + return `final ${params.MODULE_NAME}:Client ${params.CONNECTION_NAME} = check new (config = { timeout: 30 }, serviceUrl = ${params.SERVICE_URL_VAR_NAME});\n`; + }, + newConnectionWithApiKey: (params: { + requireProxy: boolean; + MODULE_NAME: string; + CONNECTION_NAME: string; + SERVICE_URL_VAR_NAME: string; + API_KEY_VAR_NAME: string; + }) => { + return `final ${params.MODULE_NAME}:Client ${ + params.CONNECTION_NAME + } = check new (apiKeyConfig = { choreoAPIKey: ${params.API_KEY_VAR_NAME} }, config = { ${ + params.requireProxy ? "proxy: devantProxyConfig, " : "" + }timeout: 60 }, serviceUrl = ${params.SERVICE_URL_VAR_NAME});\n`; + }, + newConnectionWithOAuth: (params: { + requireProxy: boolean; + MODULE_NAME: string; + CONNECTION_NAME: string; + SERVICE_URL_VAR_NAME: string; + API_KEY_VAR_NAME: string; + TOKEN_URL: string; + CLIENT_ID: string; + CLIENT_SECRET: string; + }) => { + // todo: get params from LS + return `final ${params.MODULE_NAME}:Client ${ + params.CONNECTION_NAME + } = check new (config = { auth: { tokenUrl: ${params.TOKEN_URL}, clientId: ${params.CLIENT_ID}, clientSecret: ${ + params.CLIENT_SECRET + } }, ${params.requireProxy ? "proxy: devantProxyConfig, " : ""}timeout: 60 }, serviceUrl = ${ + params.SERVICE_URL_VAR_NAME + });\n`; + }, +}; + +export const hasContextYaml = (projectPath: string): boolean => { + try { + const repoRoot = getRepoRoot(projectPath); + if (repoRoot) { + const contextYamlPath = path.join(repoRoot, ".choreo", "context.yaml"); + if (fs.existsSync(contextYamlPath)) { + return true; + } + } + return false; + } catch { + return false; + } +}; + +export function getRepoRoot(projectRoot: string): string | undefined { + // traverse up the directory tree until .git directory is found + const gitDir = path.join(projectRoot, ".git"); + if (fs.existsSync(gitDir)) { + return projectRoot; + } + // path is root return undefined + if (projectRoot === path.parse(projectRoot).root) { + return undefined; + } + return getRepoRoot(path.join(projectRoot, "..")); +} + +export function getDomain(rawURL: string): string { + try { + const parsedURL = new URL(rawURL); + return parsedURL.hostname; + } catch (error) { + throw new Error(""); + } +} + +/** + * Finds a unique connection name by checking against existing marketplace items. + * If the base name exists, appends a numeric counter until a unique name is found. + * If the initial name is shorter than 3 characters, appends '-connection' to it. + */ +export const findUniqueConnectionName = (name: string, existingMarketplaceItems: MarketplaceItem[]): string => { + // If name is too short, append '-connection' + let baseName = name; + if (baseName.length < 3) { + baseName = `${baseName}-connection`; + } + + const existingNames = new Set(existingMarketplaceItems.map((item) => item.name.toLowerCase())); + + // Check if the base name exists + let uniqueName = baseName; + let counter = 1; + + while (existingNames.has(uniqueName.toLowerCase())) { + uniqueName = `${baseName}${counter}`; + counter++; + } + + return uniqueName; +}; diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/rpc-handler.ts new file mode 100644 index 0000000000..a438a0ffa0 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/rpc-handler.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2026, 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 { getMarketplaceItems, getMarketplaceIdl, getConnections, deleteLocalConnectionsConfig, getDevantConsoleUrl, getMarketplaceItem, getConnection, onPlatformExtStoreStateChange, refreshConnectionList, getPlatformStore, setConnectedToDevant, setSelectedComponent, deployIntegrationInDevant, deleteDevantTempConfigs, generateCustomConnectorFromOAS, addDevantTempConfig, setSelectedEnv, createConnectionConfig, replaceDevantTempConfigValues, registerDevantMarketplaceService, createThirdPartyConnection, initializeDevantOASConnection, createInternalConnection, getComponentList } from "@wso2/ballerina-core"; +import { Messenger } from "vscode-messenger"; +import { PlatformExtRpcManager } from "./rpc-manager"; +import { CreateComponentConnectionReq, CreateLocalConnectionsConfigReq, CreateThirdPartyConnectionReq, DeleteLocalConnectionsConfigReq, GetComponentsReq, GetConnectionItemReq, GetConnectionsReq, GetMarketplaceIdlReq, GetMarketplaceItemReq, GetMarketplaceListReq, } from "@wso2/wso2-platform-core"; +import { AddDevantTempConfigReq, DeleteDevantTempConfigReq, GenerateCustomConnectorFromOASReq, InitializeDevantOASConnectionReq, RegisterDevantMarketplaceServiceReq, ReplaceDevantTempConfigValuesReq } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { platformExtStore } from "./platform-store"; +import { debug } from "../../utils"; + +export function registerPlatformExtRpcHandlers(messenger: Messenger) { + const rpcManger = new PlatformExtRpcManager(); + rpcManger.initStateSubscription(messenger).catch((err) => { + debug(`Failed to init platform ext state: ${err?.message}`); + }); + // BI ext handlers + messenger.onRequest(generateCustomConnectorFromOAS, (params: GenerateCustomConnectorFromOASReq) => rpcManger.generateCustomConnectorFromOAS(params)); + messenger.onRequest(initializeDevantOASConnection, (params: InitializeDevantOASConnectionReq) => rpcManger.initializeDevantOASConnection(params)); + messenger.onRequest(registerDevantMarketplaceService, (params: RegisterDevantMarketplaceServiceReq) => rpcManger.registerDevantMarketplaceService(params)); + messenger.onRequest(addDevantTempConfig, (params: AddDevantTempConfigReq) => rpcManger.addDevantTempConfig(params)); + messenger.onRequest(deleteDevantTempConfigs, (params: DeleteDevantTempConfigReq) => rpcManger.deleteDevantTempConfigs(params)); + messenger.onRequest(replaceDevantTempConfigValues, (params: ReplaceDevantTempConfigValuesReq) => rpcManger.replaceDevantTempConfigValues(params)); + // Platform ext proxies + messenger.onRequest(createThirdPartyConnection, (params: CreateThirdPartyConnectionReq) => rpcManger.createThirdPartyConnection(params)); + messenger.onRequest(createInternalConnection, (params: CreateComponentConnectionReq) => rpcManger.createInternalConnection(params)); + messenger.onRequest(getMarketplaceItems, (params: GetMarketplaceListReq) => rpcManger.getMarketplaceItems(params)); + messenger.onRequest(getMarketplaceItem, (params: GetMarketplaceItemReq) => rpcManger.getMarketplaceItem(params)); + messenger.onRequest(getMarketplaceIdl, (params: GetMarketplaceIdlReq) => rpcManger.getMarketplaceIdl(params)); + messenger.onRequest(getConnections, (params: GetConnectionsReq) => rpcManger.getConnections(params)); + messenger.onRequest(getConnection, (params: GetConnectionItemReq) => rpcManger.getConnection(params)); + messenger.onRequest(getComponentList, (params: GetComponentsReq) => rpcManger.getComponentList(params)); + messenger.onRequest(deleteLocalConnectionsConfig, (params: DeleteLocalConnectionsConfigReq) => rpcManger.deleteLocalConnectionsConfig(params)); + messenger.onRequest(getDevantConsoleUrl, () => rpcManger.getDevantConsoleUrl()); + messenger.onRequest(refreshConnectionList, () => rpcManger.refreshConnectionList()); + messenger.onRequest(setConnectedToDevant, (params: boolean) => rpcManger.setConnectedToDevant(params)); + messenger.onRequest(setSelectedComponent, (params: string) => rpcManger.setSelectedComponent(params)); + messenger.onRequest(setSelectedEnv, (params: string) => rpcManger.setSelectedEnv(params)); + messenger.onRequest(deployIntegrationInDevant, () => rpcManger.deployIntegrationInDevant()); + messenger.onRequest(createConnectionConfig, (params: CreateLocalConnectionsConfigReq) => rpcManger.createConnectionConfig(params)); + messenger.onRequest(getPlatformStore, () => platformExtStore.getState().state); +} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/rpc-manager.ts new file mode 100644 index 0000000000..571daf40f2 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/rpc-manager.ts @@ -0,0 +1,1033 @@ +/** + * Copyright (c) 2026, 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 { + onPlatformExtStoreStateChange, + PlatformExtAPI, + SyntaxTree, + DIRECTORY_MAP, + findDevantScopeByModule, + AvailableNode, +} from "@wso2/ballerina-core"; +import { Uri, window, WorkspaceEdit } from "vscode"; +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { + ConnectionListItem, + DeleteLocalConnectionsConfigReq, + GetConnectionsReq, + GetMarketplaceIdlReq, + GetMarketplaceItemReq, + GetMarketplaceListReq, + IWso2PlatformExtensionAPI, + MarketplaceIdlResp, + MarketplaceItem, + MarketplaceListResp, + ServiceInfoVisibilityEnum, + GetConnectionItemReq, + StartProxyServerResp, + StopProxyServerReq, + ConnectionDetailed, + CommandIds as PlatformExtCommandIds, + DevantScopes, + ICreateComponentCmdParams, + ComponentKind, + ICmdParamsBase, + RegisterMarketplaceConfigMap, + Project, + Organization, + CreateLocalConnectionsConfigReq, + CreateThirdPartyConnectionReq, + CreateComponentConnectionReq, + GetComponentsReq, +} from "@wso2/wso2-platform-core"; +import { log } from "../../utils/logger"; +import { + AddDevantTempConfigReq, + AddDevantTempConfigResp, + DeleteDevantTempConfigReq, + GenerateCustomConnectorFromOASReq, + GenerateCustomConnectorFromOASResp, + InitializeDevantOASConnectionReq, + InitializeDevantOASConnectionResp, + RegisterDevantMarketplaceServiceReq, + ReplaceDevantTempConfigValuesReq, +} from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { StateMachine } from "../../stateMachine"; +import { CaptureBindingPattern, ModulePart, STKindChecker } from "@wso2/syntax-tree"; +import { DeleteBiDevantConnectionReq } from "./types"; +import { platformExtStore } from "./platform-store"; +import { Messenger } from "vscode-messenger"; +import { VisualizerWebview } from "../../views/visualizer/webview"; +import { + addConfigurable, + addConnection, + addProxyConfigurable, + findUniqueConnectionName, + getConfigFileUri, + hasContextYaml, + processOpenApiWithApiKeyAuth, + Templates, +} from "./platform-utils"; +import { debounce } from "lodash"; +import { BiDiagramRpcManager } from "../bi-diagram/rpc-manager"; +import { updateSourceCode } from "../../utils"; +import { getPlatformExtensionAPI } from "../../utils/ai/auth"; + +export class PlatformExtRpcManager implements PlatformExtAPI { + static platformExtAPI: IWso2PlatformExtensionAPI; + private async getPlatformExt() { + if (PlatformExtRpcManager.platformExtAPI) { + return PlatformExtRpcManager.platformExtAPI; + } + const platformExtAPI = await getPlatformExtensionAPI(); + if (!platformExtAPI) { + throw new Error("platform ext not installed"); + } + PlatformExtRpcManager.platformExtAPI = platformExtAPI; + return platformExtAPI; + } + + private async initAuthState() { + const platformExt = await this.getPlatformExt(); + const userInfo = platformExt.getAuthState().userInfo; + const selectedContext = platformExt.getSelectedContext(); + platformExtStore.getState().setState({ userInfo, isLoggedIn: !!userInfo, selectedContext }); + + if (selectedContext?.project) { + const envs = await platformExt.getProjectEnvs({ + orgId: selectedContext?.org?.id?.toString(), + orgUuid: selectedContext?.org?.uuid, + projectId: selectedContext?.project?.id, + }); + const selectedEnv = + envs.find((env) => env.id === platformExtStore.getState().state?.selectedEnv?.id) || envs[0]; + platformExtStore.getState().setState({ envs, selectedEnv }); + } + + platformExt.subscribeAuthState((authState) => { + platformExtStore.getState().setState({ userInfo: authState.userInfo, isLoggedIn: !!authState.userInfo }); + }); + + const debouncedEnvListRefresh = debounce(async (org?: Organization, project?: Project) => { + if (org && project) { + const envs = await platformExt.getProjectEnvs({ + orgId: org.id?.toString(), + orgUuid: org.uuid, + projectId: project.id, + }); + const selectedEnv = + envs.find((env) => env.id === platformExtStore.getState().state?.selectedEnv?.id) || envs[0]; + platformExtStore.getState().setState({ envs, selectedEnv }); + } + }, 1000); + + platformExt.subscribeContextState(async (selectedContext) => { + platformExtStore.getState().setState({ selectedContext }); + debouncedEnvListRefresh(selectedContext?.org, selectedContext?.project); + }); + } + + private async initFileWatcher() { + const platformExt = await this.getPlatformExt(); + const debouncedOnFilChange = debounce(async () => { + if (StateMachine.context().projectPath) { + const hasLocalChanges = await platformExt.localRepoHasChanges(StateMachine.context().projectPath); + platformExtStore.getState().setState({ hasLocalChanges }); + } + }, 1000); + + if (vscode.workspace.workspaceFolders?.length > 0) { + const fileWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(vscode.workspace.workspaceFolders[0], "**/*"), + ); + fileWatcher.onDidCreate(debouncedOnFilChange); + fileWatcher.onDidChange(debouncedOnFilChange); + fileWatcher.onDidDelete(debouncedOnFilChange); + } + } + + private async initProjectPathWatcher(projectPath: string) { + const platformExt = await this.getPlatformExt(); + let components: ComponentKind[] = []; + let matchingComponent: ComponentKind; + let hasLocalChanges = false; + let hasProjectYaml = false; + if (projectPath) { + components = platformExt.getDirectoryComponents(projectPath); + matchingComponent = components.find( + (item) => platformExtStore.getState().state?.selectedComponent?.metadata?.id === item.metadata?.id, + ); + hasLocalChanges = await platformExt.localRepoHasChanges(projectPath); + hasProjectYaml = hasContextYaml(projectPath); + await this.debouncedRefreshConnectionList(); + } + + platformExtStore.getState().setState({ + components, + selectedComponent: matchingComponent || components[0], + hasLocalChanges, + hasPossibleComponent: components.length > 0 || hasProjectYaml, + }); + + const unsubscribeDirCompWatcher = platformExt.subscribeDirComponents(projectPath, (components) => { + const hasProjectYaml = hasContextYaml(projectPath); + const matchingComponent = components.find( + (item) => platformExtStore.getState().state?.selectedComponent?.metadata?.id === item.metadata?.id, + ); + platformExtStore.getState().setState({ + components, + selectedComponent: matchingComponent || components[0], + hasPossibleComponent: components.length > 0 || hasProjectYaml, + }); + }); + return unsubscribeDirCompWatcher; + } + + private async initSelfStoreSubscription(messenger: Messenger) { + platformExtStore.subscribe((state, prevState) => { + messenger.sendNotification( + onPlatformExtStoreStateChange, + { type: "webview", webviewType: VisualizerWebview.viewType }, + state.state, + ); + + let refetchConnections = false; + if (!state.state?.isLoggedIn && prevState?.state?.isLoggedIn) { + // if user is logging out + // todo: check if this needs to be enabled again + // platformExtStore.getState().setState({connections: []}); + } else if ( + state.state?.selectedComponent && + state.state?.selectedComponent.metadata?.id !== prevState?.state?.selectedComponent?.metadata?.id + ) { + // if component selection has changed + // todo: remove connections related to previous component + // todo: test after applying fix to support multiple components + // platformExtStore.getState().setState({connections: platformExtStore.getState().state?.connections?.filter(item=>item.componentId)}); + refetchConnections = true; + } else if ( + state.state?.selectedContext?.project && + state.state?.selectedContext?.project?.id !== prevState.state?.selectedContext?.project?.id + ) { + // if project selection has changed + platformExtStore.getState().setConnectionState({ list: [] }); + refetchConnections = true; + } + + if (refetchConnections) { + this.debouncedRefreshConnectionList(); + } + }); + } + + public async initStateSubscription(messenger: Messenger) { + await platformExtStore.persist.rehydrate(); + await this.initAuthState(); + let projectPath = StateMachine.context()?.projectPath; + let disposeProjectPathWatcher = await this.initProjectPathWatcher(projectPath); + if (projectPath) { + this.debouncedRefreshConnectionList(); + } + await this.initFileWatcher(); + const debouncedInitProjectPathWatcher = debounce( + async (projectPath: string) => await this.initProjectPathWatcher(projectPath), + 250, + ); + StateMachine.service().subscribe(async (state) => { + if (state.context?.projectPath && state.context?.projectPath !== projectPath) { + projectPath = state.context?.projectPath; + if (disposeProjectPathWatcher) { + disposeProjectPathWatcher(); + } + + disposeProjectPathWatcher = await debouncedInitProjectPathWatcher(projectPath); + } + }); + + await this.initSelfStoreSubscription(messenger); + } + + async getMarketplaceItems(params: GetMarketplaceListReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return platformExt?.getMarketplaceItems(params); + } catch (err) { + log(`Failed to invoke getMarketplaceItems: ${err}`); + } + } + + async getMarketplaceItem(params: GetMarketplaceItemReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return platformExt?.getMarketplaceItem(params); + } catch (err) { + log(`Failed to invoke getMarketplaceItem: ${err}`); + } + } + + async getMarketplaceIdl(params: GetMarketplaceIdlReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return platformExt?.getMarketplaceIdl(params); + } catch (err) { + log(`Failed to invoke getMarketplaceIdl: ${err}`); + } + } + + async getConnections(params: GetConnectionsReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return platformExt?.getConnections(params); + } catch (err) { + log(`Failed to invoke getConnections: ${err}`); + } + } + + async getConnection(params: GetConnectionItemReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return platformExt?.getConnection(params); + } catch (err) { + log(`Failed to invoke getConnection: ${err}`); + } + } + + async getComponentList(params: GetComponentsReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return platformExt?.getComponentList(params); + } catch (err) { + log(`Failed to invoke getComponentList: ${err}`); + } + } + + async deleteLocalConnectionsConfig(params: DeleteLocalConnectionsConfigReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + platformExt?.deleteLocalConnectionsConfig(params); + } catch (err) { + log(`Failed to delete connection config: ${err}`); + } + } + + async getDevantConsoleUrl(): Promise { + try { + const platformExt = await this.getPlatformExt(); + return await platformExt?.getDevantConsoleUrl(); + } catch (err) { + log(`Failed to delete connection config: ${err}`); + } + } + + async createConnectionConfig(params: CreateLocalConnectionsConfigReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return await platformExt?.createConnectionConfig(params); + } catch (err) { + log(`Failed to create connection config: ${err}`); + } + } + + async createThirdPartyConnection(params: CreateThirdPartyConnectionReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return await platformExt?.createThirdPartyConnection(params); + } catch (err) { + log(`Failed to create 3rd party connection: ${err}`); + } + } + + async createInternalConnection(params: CreateComponentConnectionReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return await platformExt?.createComponentConnection(params); + } catch (err) { + log(`Failed to create Devant connection: ${err}`); + } + } + + async stopProxyServer(params: StopProxyServerReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return platformExt?.stopProxyServer(params); + } catch (err) { + log(`Failed to delete connection config: ${err}`); + } + } + + setSelectedComponent(componentId: string): void { + const selectedComponent = platformExtStore + .getState() + .state?.components?.find((item) => item.metadata?.id === componentId); + if (selectedComponent) { + platformExtStore.getState().setState({ selectedComponent }); + } + } + + setSelectedEnv(envId: string): void { + const selectedEnv = platformExtStore.getState().state?.envs?.find((item) => item?.id === envId); + if (selectedEnv) { + platformExtStore.getState().setState({ selectedEnv }); + } + } + + setConnectedToDevant(connected: boolean): void { + platformExtStore.getState().setConnectionState({ connectedToDevant: connected }); + } + + async deployIntegrationInDevant(): Promise { + const projectStructure = await new BiDiagramRpcManager().getProjectStructure(); + if (!projectStructure) { + return; + } + + const project = projectStructure.projects.find( + (project) => project.projectPath === StateMachine.context()?.projectPath, + ); + if (!project) { + return; + } + + const services = project.directoryMap[DIRECTORY_MAP.SERVICE]; + const automation = project.directoryMap[DIRECTORY_MAP.AUTOMATION]; + + let scopes: DevantScopes[] = []; + if (services?.length > 0) { + const svcScopes = services + .map((svc) => findDevantScopeByModule(svc?.moduleName)) + .filter((svc) => svc !== undefined); + scopes.push(...Array.from(new Set(svcScopes))); + } + if (automation?.length > 0) { + scopes.push(DevantScopes.AUTOMATION); + } + + let integrationType: DevantScopes; + + if (scopes.length === 1) { + integrationType = scopes[0]; + } else if (scopes?.length > 1) { + const selectedScope = await window.showQuickPick(scopes, { + placeHolder: + "You have multiple artifact types within this project. Select the artifact type to be deployed", + }); + if (!selectedScope) { + return; + } + integrationType = selectedScope as DevantScopes; + } + + const deployementParams: ICreateComponentCmdParams = { + integrationType: integrationType, + buildPackLang: "ballerina", + name: path.basename(StateMachine.context().projectPath), + componentDir: StateMachine.context().projectPath, + extName: "Devant", + }; + vscode.commands.executeCommand(PlatformExtCommandIds.CreateNewComponent, deployementParams); + } + + async getAllConnections(): Promise { + try { + const platformExt = await this.getPlatformExt(); + if ( + platformExtStore.getState().state.isLoggedIn && + platformExtStore.getState().state.selectedContext?.project?.id + ) { + const projectPromise = platformExt.getConnections({ + orgId: platformExtStore.getState().state.selectedContext?.org?.id?.toString(), + projectId: platformExtStore.getState().state.selectedContext?.project?.id, + componentId: "", + }); + + const componentPromise: Promise = platformExtStore.getState().state + .selectedComponent + ? platformExt.getConnections({ + orgId: platformExtStore.getState().state.selectedContext?.org?.id?.toString(), + projectId: platformExtStore.getState().state.selectedContext?.project?.id, + componentId: platformExtStore.getState().state.selectedComponent?.metadata?.id, + }) + : Promise.resolve([]); + + const [projectConnections, componentConnections] = await Promise.all([ + projectPromise, + componentPromise, + ]); + + return [...componentConnections, ...projectConnections]; + } + return []; + } catch (err) { + log(`Failed to get all connections: ${err}`); + } + } + + async setupDevantProxyForDebugging(debugConfig: vscode.DebugConfiguration): Promise { + // check if choreoConnect is provided as param, if so use pass those as param + const devantProxyResp = await this.startProxyServer(debugConfig); + + if (devantProxyResp?.proxyServerPort) { + debugConfig.env = { ...(debugConfig.env || {}), ...devantProxyResp.envVars }; + if (devantProxyResp.requiresProxy) { + debugConfig.env.BAL_CONFIG_VAR_DEVANTPROXYHOST = "127.0.0.1"; + debugConfig.env.BAL_CONFIG_VAR_DEVANTPROXYPORT = `${devantProxyResp.proxyServerPort}`; + } else { + delete debugConfig.env.BAL_CONFIG_VAR_DEVANTPROXYHOST; + delete debugConfig.env.BAL_CONFIG_VAR_DEVANTPROXYPORT; + } + + const disposable = vscode.debug.onDidTerminateDebugSession((session) => { + if (session.configuration === debugConfig) { + this.stopProxyServer({ proxyPort: devantProxyResp.proxyServerPort }); + disposable.dispose(); + } + }); + } + } + + async startProxyServer( + debugConfig: vscode.DebugConfiguration, + ): Promise { + // todo: need to take in params from config + try { + const platformExt = await this.getPlatformExt(); + const configBalFile = path.join(StateMachine.context().projectPath, "config.bal"); + const configBalFileUri = Uri.file(configBalFile); + const syntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: configBalFileUri.toString() }, + })) as SyntaxTree; + let requiresProxy = false; + if ( + (syntaxTree?.syntaxTree as ModulePart)?.members?.find( + (member) => + STKindChecker.isModuleVarDecl(member) && + (member.typedBindingPattern?.bindingPattern as CaptureBindingPattern)?.variableName?.value === + "devantProxyConfig", + ) + ) { + requiresProxy = true; + } + + if (debugConfig.request === "launch" && debugConfig?.choreoConnect) { + if (!platformExtStore.getState().state?.isLoggedIn) { + window + .showErrorMessage( + "You must log in before connecting to devant environment. Retry after logging in.", + "Login", + ) + .then((res) => { + if (res === "Login") { + vscode.commands.executeCommand(PlatformExtCommandIds.SignIn, { + extName: "Devant", + } as ICmdParamsBase); + } + }); + return; + } + + if (!platformExtStore.getState().state?.selectedContext?.project) { + window + .showErrorMessage( + "Pease associate your directory with Devant project in order to connect to Devant while running or debugging", + "Manage Project", + ) + .then((res) => { + if (res === "Manage Project") { + vscode.commands.executeCommand(PlatformExtCommandIds.ManageDirectoryContext, { + extName: "Devant", + } as ICmdParamsBase); + } + }); + return; + } + } + + if ( + debugConfig.request === "launch" && + platformExtStore.getState().state?.isLoggedIn && + platformExtStore.getState().state?.selectedContext?.org && + platformExtStore.getState().state?.selectedContext?.project && + // todo: check and fetch configs of only the connections used + // platformExtStore.getState().state?.devantConns?.list?.filter((item) => item.isUsed)?.length > 0 && + platformExtStore.getState().state?.devantConns?.connectedToDevant + ) { + // TODO: need to check whether at least one devant connection being used + const resp = await window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Connecting to Devant before running/debugging the application...", + }, + () => + platformExt?.startProxyServer({ + orgId: platformExtStore.getState().state?.selectedContext?.org?.id?.toString(), + project: + debugConfig?.choreoConnect?.project || + platformExtStore.getState().state?.selectedContext?.project?.id, + component: + debugConfig?.choreoConnect?.component || + platformExtStore.getState().state?.selectedComponent?.metadata?.id || + "", + env: + debugConfig?.choreoConnect?.env || + platformExtStore?.getState().state?.selectedEnv?.name || + "", + skipConnection: debugConfig?.choreoConnect?.skipConnection || [], + }), + ); + return { ...resp, requiresProxy }; + } + return { envVars: {}, proxyServerPort: 0, requiresProxy }; + } catch (err) { + log(`Failed to delete connection config: ${err}`); + return { envVars: {}, proxyServerPort: 0, requiresProxy: false }; + } + } + + async deleteBiDevantConnection(params: DeleteBiDevantConnectionReq): Promise { + try { + StateMachine.setEditMode(); + const platformExt = await this.getPlatformExt(); + const syntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: Uri.file(params.filePath).toString() }, + })) as SyntaxTree; + + const matchingConnection = (syntaxTree.syntaxTree as ModulePart)?.members?.find((member) => { + return ( + member.position?.startLine === params?.startLine && + member.position?.startColumn === params?.startColumn && + member.position?.endLine === params?.endLine && + member.position?.endColumn === params?.endColumn + ); + }); + + if (matchingConnection && STKindChecker.isModuleVarDecl(matchingConnection)) { + const connectionName = (matchingConnection.typedBindingPattern?.bindingPattern as CaptureBindingPattern) + ?.variableName?.value; + if (connectionName) { + const projectPath = StateMachine.context().projectPath; + const devantUrl = await this.getDevantConsoleUrl(); + + const selected = platformExtStore.getState().state?.selectedContext; + const matchingConnListItem = platformExtStore + .getState() + .state?.devantConns?.list.find( + (connItem) => connItem.name?.replaceAll("-", "_").replaceAll(" ", "_") === connectionName, + ); + if (matchingConnListItem) { + await this.deleteLocalConnectionsConfig({ + componentDir: projectPath, + connectionName: matchingConnListItem.name, + }); + if (matchingConnListItem?.componentId) { + await platformExt.deleteConnection({ + componentPath: projectPath, + connectionId: matchingConnListItem.groupUuid, + connectionName: matchingConnListItem.name, + orgId: selected.org.id.toString(), + }); + } else { + window + .showInformationMessage( + "In-order to delete your project level Devant connection, please head over to Devant console", + "Open Devant", + ) + .then((resp) => { + if (resp === "Open Devant") { + vscode.env.openExternal( + Uri.parse( + `${devantUrl}/organizations/${selected.org.handle}/projects/${selected.project.id}/admin/connections`, + ), + ); + } + }); + } + } + } + } + + this.refreshConnectionList(); + StateMachine.setReadyMode(); + } catch (err) { + StateMachine.setReadyMode(); + window.showErrorMessage(`Failed to delete Devant connection: ${(err as Error).message}`); + log(`Failed to invoke deleteDevantConnection: ${err}`); + } + } + + async initializeDevantOASConnection( + params: InitializeDevantOASConnectionReq, + ): Promise { + try { + StateMachine.setEditMode(); + await this.generateCustomConnectorFromOAS({ + connectionName: params.name, + marketplaceItem: params.marketplaceItem, + securityType: params.securityType, + }); + const moduleName = params.name.replace(/[_\-\s]/g, "")?.toLowerCase(); + const configFileUri = getConfigFileUri(); + + const envIds = Object.keys(params.configurations || {}); + const firstEnvConfig = envIds.length > 0 ? params.configurations[envIds[0]] : undefined; + const connectionKeys = firstEnvConfig?.entries ?? {}; + + interface IkeyVal { + keyname: string; + envName: string; + } + interface Ikeys { + ChoreoAPIKey?: IkeyVal; + ServiceURL?: IkeyVal; + TokenURL?: IkeyVal; + ConsumerKey?: IkeyVal; + ConsumerSecret?: IkeyVal; + } + const keys: Ikeys = {}; + + const deleteTempConfigBalEdits = new WorkspaceEdit(); + const configBalFileUri = getConfigFileUri(); + + for (const entry of params.devantConfigs) { + if (entry.node) { + deleteTempConfigBalEdits.delete( + configBalFileUri, + new vscode.Range( + new vscode.Position(entry.node.position.startLine, entry.node.position.startColumn), + new vscode.Position(entry.node.position.endLine, entry.node.position.endColumn), + ), + ); + } + + keys[entry.id] = { + keyname: entry.name, + envName: connectionKeys[entry.id].envVariableName, + }; + } + if (deleteTempConfigBalEdits.size > 0) { + await updateSourceCode({ + textEdits: { [configBalFileUri.toString()]: deleteTempConfigBalEdits.get(configBalFileUri) || [] }, + skipPayloadCheck: true, + }); + } + + await addConfigurable( + configFileUri, + Object.values(keys).map((item) => ({ configName: item.keyname, configEnvName: item.envName })), + ); + + const requireProxy = [ + ServiceInfoVisibilityEnum.Organization.toString(), + ServiceInfoVisibilityEnum.Project.toString(), + ].includes(params.visibility); + + if (requireProxy) { + await addProxyConfigurable(configFileUri); + } + + const resp = await addConnection(params.name, moduleName, params.securityType, requireProxy, { + apiKeyVarName: keys?.ChoreoAPIKey?.keyname, + svsUrlVarName: keys?.ServiceURL?.keyname, + tokenClientIdVarName: keys?.ConsumerKey?.keyname, + tokenClientSecretVarName: keys?.ConsumerSecret?.keyname, + tokenUrlVarName: keys?.TokenURL?.keyname, + }); + + StateMachine.setReadyMode(); + return { connectionName: resp.connName }; + } catch (err) { + StateMachine.setReadyMode(); + window.showErrorMessage(`Failed to initialize Devant connection: ${(err as Error).message}`); + log(`Failed to initialize Devant connection: ${err}`); + } + } + + async generateCustomConnectorFromOAS( + params: GenerateCustomConnectorFromOASReq, + ): Promise { + try { + const platformExt = await this.getPlatformExt(); + const projectPath = StateMachine.context().projectPath; + + const serviceIdl = await platformExt?.getMarketplaceIdl({ + orgId: platformExtStore.getState().state?.selectedContext?.org.id?.toString(), + serviceId: params.marketplaceItem.serviceId, + }); + + const choreoDir = path.join(projectPath, ".choreo"); + if (!fs.existsSync(choreoDir)) { + fs.mkdirSync(choreoDir, { recursive: true }); + } + + const moduleName = params.connectionName.replace(/[_\-\s]/g, "")?.toLowerCase(); + const filePath = path.join(choreoDir, `${moduleName}-spec.yaml`); + + if (serviceIdl?.idlType === "OpenAPI" && serviceIdl.content) { + const updatedDef = processOpenApiWithApiKeyAuth(serviceIdl.content, params.securityType); + fs.writeFileSync(filePath, updatedDef, "utf8"); + } + + const diagram = new BiDiagramRpcManager(); + await diagram.generateOpenApiClient({ + module: moduleName, + openApiContractPath: filePath, + projectPath, + }); + + const connectors = await diagram.search({ + filePath: StateMachine.context().documentUri, + queryMap: { limit: 60 }, + searchKind: "CONNECTOR", + }); + + const localCategory = connectors?.categories?.find((item) => item.metadata?.label === "Local"); + if (localCategory) { + const matchingLocalConnector = localCategory?.items?.find( + (item) => (item as AvailableNode)?.codedata?.module === moduleName, + ); + if (matchingLocalConnector) { + return { connectionNode: matchingLocalConnector as AvailableNode }; + } + } + + return { connectionNode: null }; + } catch (err) { + StateMachine.setReadyMode(); + window.showErrorMessage(`Failed to invoke generateCustomConnectorFromOAS: ${(err as Error).message}`); + log(`Failed to invoke generateCustomConnectorFromOAS: ${err}`); + } + } + + async deleteDevantTempConfigs(params: DeleteDevantTempConfigReq): Promise { + try { + const configBalFileUri = getConfigFileUri(); + + const configBalEdits = new WorkspaceEdit(); + for (const node of params.nodes) { + configBalEdits.delete( + configBalFileUri, + new vscode.Range( + new vscode.Position(node.position.startLine, node.position.startColumn), + new vscode.Position(node.position.endLine, node.position.endColumn), + ), + ); + } + + await updateSourceCode({ + textEdits: { [configBalFileUri.toString()]: configBalEdits.get(configBalFileUri) || [] }, + skipPayloadCheck: true, + }); + } catch (err) { + log(`Failed to invoke deleteDevantTempConfigs: ${err}`); + } + } + + async addDevantTempConfig(params: AddDevantTempConfigReq): Promise { + try { + const configBalFileUri = getConfigFileUri(); + const syntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: configBalFileUri.toString() }, + })) as SyntaxTree; + + const newConfigEditLine = (syntaxTree?.syntaxTree?.position?.endLine ?? 0) + 1; + const configBalEdits = new WorkspaceEdit(); + + if (params.newLine) { + configBalEdits.insert( + configBalFileUri, + new vscode.Position(newConfigEditLine, 0), + Templates.emptyLine(), + ); + } + + const newConfigTemplate = Templates.newDefaultEnvConfigurable({ CONFIG_NAME: params.name }); + configBalEdits.insert(configBalFileUri, new vscode.Position(newConfigEditLine, 0), newConfigTemplate); + + await updateSourceCode({ + textEdits: { [configBalFileUri.toString()]: configBalEdits.get(configBalFileUri) || [] }, + skipPayloadCheck: true, + }); + + const updatedSyntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: configBalFileUri.toString() }, + })) as SyntaxTree; + + const matchingConfig = (updatedSyntaxTree?.syntaxTree as ModulePart)?.members?.find((member) => { + return ( + (member.typedBindingPattern?.bindingPattern as CaptureBindingPattern)?.variableName?.value === + params.name + ); + }); + if (STKindChecker.isModuleVarDecl(matchingConfig)) { + return { configNode: matchingConfig }; + } + + throw new Error("failed to add new temp config"); + } catch (err) { + log(`Failed to invoke addDevantTempConfig: ${err}`); + } + } + + async registerDevantMarketplaceService(params: RegisterDevantMarketplaceServiceReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + + const marketplaceItems = await platformExt.getMarketplaceItems({ + orgId: platformExtStore.getState().state?.selectedContext?.org?.id?.toString(), + request: { + query: params.name, + limit: 100, + networkVisibilityFilter: "all", + sortBy: "createdTime", + }, + }); + + let idlContent = ""; + if (params.idlFilePath) { + // read contents of idlFilePath and convert it to base64 + const idlFileContent = await fs.promises.readFile(params.idlFilePath, { encoding: "utf-8" }); + idlContent = Buffer.from(idlFileContent).toString("base64"); + } + + const envs = await platformExt.getProjectEnvs({ + orgId: platformExtStore.getState().state?.selectedContext?.org?.id?.toString(), + orgUuid: platformExtStore.getState().state?.selectedContext?.org?.uuid, + projectId: platformExtStore.getState().state?.selectedContext?.project?.id, + }); + + const configs: RegisterMarketplaceConfigMap = {}; + for (const env of envs) { + const endpointName = `${env.name}Endpoint`; + if (env.critical) { + configs[endpointName] = { + name: endpointName, + environmentTemplateIds: [env.templateId], + values: params.configs?.map((item) => ({ key: item.name, value: "" })), + }; + } else { + configs[endpointName] = { + name: endpointName, + environmentTemplateIds: [env.templateId], + values: params.configs?.map((item) => ({ key: item.name, value: item.value || "" })), + }; + } + } + + const registeredMarketplaceItem = await platformExt?.registerMarketplaceConnection({ + orgId: platformExtStore.getState().state?.selectedContext?.org?.id?.toString(), + orgUuid: platformExtStore.getState().state?.selectedContext?.org?.uuid, + projectId: platformExtStore.getState().state?.selectedContext?.project?.id, + serviceType: params.serviceType, + idlType: params.idlType, + idlContent, + configs, + schemaEntries: params.configs?.map((item) => ({ + name: item.name, + type: "string", + isSensitive: item.isSecret, + })), + name: findUniqueConnectionName(params.name, marketplaceItems.data), + }); + + const marketplaceService = await platformExt.getMarketplaceItem({ + orgId: platformExtStore.getState().state?.selectedContext?.org?.id?.toString(), + serviceId: registeredMarketplaceItem.serviceId, + }); + + return marketplaceService; + } catch (err) { + window.showErrorMessage(`Failed to create Devant connection: ${(err as Error).message}`); + log(`Failed to invoke registerDevantMarketplaceService: ${err}`); + } + } + + async replaceDevantTempConfigValues(params: ReplaceDevantTempConfigValuesReq): Promise { + try { + const syntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: getConfigFileUri().toString() }, + })) as SyntaxTree; + + const envIds = Object.keys(params.createdConnection.configurations || {}); + const firstEnvConfig = envIds.length > 0 ? params.createdConnection.configurations[envIds[0]] : undefined; + const connectionKeys = firstEnvConfig?.entries ?? {}; + + let hasUpdatedConfig = false; + const configBalEdits = new WorkspaceEdit(); + + for (const config of params.configs) { + const matchingConfigEntry = Object.values(connectionKeys).find((item) => item.key === config.id); + if (matchingConfigEntry && config.node) { + hasUpdatedConfig = true; + configBalEdits.replace( + getConfigFileUri(), + new vscode.Range( + new vscode.Position( + config.node.initializer.position.startLine, + config.node.initializer.position.startColumn, + ), + new vscode.Position( + config.node.initializer.position.endLine, + config.node.initializer.position.endColumn, + ), + ), + `os:getEnv("${matchingConfigEntry.envVariableName}")`, + ); + } + } + + if (hasUpdatedConfig) { + if ( + !(syntaxTree?.syntaxTree as ModulePart)?.imports?.some((item) => + item.source?.includes("import ballerina/os"), + ) + ) { + const balOsImportTemplate = Templates.importBalOs(); + configBalEdits.insert(getConfigFileUri(), new vscode.Position(0, 0), balOsImportTemplate); + } + + await updateSourceCode({ + textEdits: { [getConfigFileUri().toString()]: configBalEdits.get(getConfigFileUri()) || [] }, + skipPayloadCheck: true, + }); + } + } catch (err) { + window.showErrorMessage(`Failed to invoke replaceDevantTempConfigValues: ${(err as Error).message}`); + log(`Failed to invoke replaceDevantTempConfigValues: ${err}`); + } + } + + debouncedRefreshConnectionList = debounce(() => this.refreshConnectionList(), 500); + + async refreshConnectionList(): Promise { + try { + platformExtStore.getState().setConnectionState({ loading: true }); + const connections = await this.getAllConnections(); + platformExtStore.getState().setConnectionState({ list: connections, loading: false }); + // TODO in order to improve speed during debugging, we need to bring cache connections secrets in Devant + /* + 1. store connection with secret info in bal ext + 2. start proxy server. need to pass secure host list. + 3. leave the server running + 4. on extension exit, kill the server if its running + */ + } catch (err) { + platformExtStore.getState().setConnectionState({ loading: false }); + log(`Failed to refresh connection list: ${err}`); + } + } +} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/types.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/types.ts new file mode 100644 index 0000000000..9cf600ff8f --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/types.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface DeleteBiDevantConnectionReq { + filePath: string; + startLine: number; + startColumn: number; + endLine: number; + endColumn: number; +} + +// OpenAPI 3.0 type definitions +export interface OpenAPISecurityScheme { + type: "apiKey" | "http" | "oauth2" | "openIdConnect"; + description?: string; + name?: string; + in?: "query" | "header" | "cookie"; + scheme?: string; + bearerFormat?: string; + flows?: any; + openIdConnectUrl?: string; + "x-ballerina-name"?: string; +} + +export interface OpenAPIComponents { + schemas?: Record; + responses?: Record; + parameters?: Record; + examples?: Record; + requestBodies?: Record; + headers?: Record; + securitySchemes?: Record; + links?: Record; + callbacks?: Record; +} + +export interface OpenAPIInfo { + title: string; + version: string; + description?: string; + termsOfService?: string; + contact?: any; + license?: any; +} + +export interface OpenAPIDefinition { + openapi: string; + info: OpenAPIInfo; + servers?: any[]; + paths: Record; + components?: OpenAPIComponents; + security?: any[]; + tags?: any[]; + externalDocs?: any; +} diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/visualizer/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/visualizer/rpc-handler.ts index 447ddcb2d9..cd6b532515 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/visualizer/rpc-handler.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/visualizer/rpc-handler.ts @@ -43,7 +43,8 @@ import { undoRedoState, updateCurrentArtifactLocation, UpdatedArtifactsResponse, - reviewAccepted + reviewAccepted, + GoBackRequest } from "@wso2/ballerina-core"; import { Messenger } from "vscode-messenger"; import { VisualizerRpcManager } from "./rpc-manager"; @@ -53,7 +54,7 @@ export function registerVisualizerRpcHandlers(messenger: Messenger) { messenger.onNotification(openView, (args: OpenViewRequest) => rpcManger.openView(args)); messenger.onRequest(getHistory, () => rpcManger.getHistory()); messenger.onNotification(addToHistory, (args: HistoryEntry) => rpcManger.addToHistory(args)); - messenger.onNotification(goBack, () => rpcManger.goBack()); + messenger.onNotification(goBack, (args: GoBackRequest) => rpcManger.goBack(args)); messenger.onNotification(goHome, () => rpcManger.goHome()); messenger.onNotification(goSelected, (args: number) => rpcManger.goSelected(args)); messenger.onRequest(undo, (count: number) => rpcManger.undo(count)); diff --git a/workspaces/ballerina/ballerina-extension/src/rpc-managers/visualizer/rpc-manager.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/visualizer/rpc-manager.ts index 495a374a10..2f290eef02 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/visualizer/rpc-manager.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/visualizer/rpc-manager.ts @@ -19,6 +19,7 @@ import { AddToUndoStackRequest, ColorThemeKind, EVENT_TYPE, + GoBackRequest, HandleApprovalPopupCloseRequest, HistoryEntry, JoinProjectPathRequest, @@ -65,9 +66,9 @@ export class VisualizerRpcManager implements VisualizerAPI { }); } - goBack(): void { + goBack(params: GoBackRequest): void { history.pop(); - updateView(); + updateView(false, params?.identifier); } async getHistory(): Promise { @@ -238,7 +239,7 @@ export class VisualizerRpcManager implements VisualizerAPI { return; } const filePath = Array.isArray(params.segments) ? Utils.joinPath(URI.file(projectPath), ...params.segments) : Utils.joinPath(URI.file(projectPath), params.segments); - resolve({ filePath: filePath.fsPath, projectPath: projectPath }); + resolve({ filePath: filePath.fsPath, projectPath: projectPath, exists: params.checkExists ? fs.existsSync(filePath.fsPath) : undefined }); }); } async undoRedoState(): Promise { diff --git a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts index a64366f381..e68ed8d174 100644 --- a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts @@ -859,7 +859,10 @@ export function openView(type: EVENT_TYPE, viewLocation: VisualizerLocation, res stateService.send({ type: type, viewLocation: viewLocation }); } -export function updateView(refreshTreeView?: boolean) { +export function updateView(refreshTreeView?: boolean, updatedIdentifier?: string) { + if (StateMachinePopup.isActive()) { + return; + } let lastView = getLastHistory(); // Step over to the next location if the last view is skippable if (!refreshTreeView && lastView?.location.view.includes("SKIP")) { @@ -885,12 +888,12 @@ export function updateView(refreshTreeView?: boolean) { // These changes will be revisited in the revamp project.directoryMap[targetedArtifactType].forEach((artifact: ProjectStructureArtifactResponse) => { - if (artifact.id === currentIdentifier || artifact.name === currentIdentifier) { + if (artifact.id === currentIdentifier || artifact.name === currentIdentifier || artifact.id === updatedIdentifier || artifact.name === updatedIdentifier) { currentArtifact = artifact; } // Check if artifact has resources and find within those if (artifact.resources && artifact.resources.length > 0) { - const resource = artifact.resources.find((resource: ProjectStructureArtifactResponse) => resource.id === currentIdentifier || resource.name === currentIdentifier); + const resource = artifact.resources.find((resource: ProjectStructureArtifactResponse) => resource.id === currentIdentifier || resource.name === currentIdentifier || resource.id === updatedIdentifier || resource.name === updatedIdentifier); if (resource) { currentArtifact = resource; } diff --git a/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts b/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts index ea36238dc9..43f66f31ba 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/ai/auth.ts @@ -18,16 +18,32 @@ import * as vscode from 'vscode'; import { extension } from "../../BalExtensionContext"; -import { AUTH_CLIENT_ID, AUTH_ORG, getDevantExchangeUrl } from '../../features/ai/utils'; +import { DEVANT_TOKEN_EXCHANGE_URL } from '../../features/ai/utils'; import axios from 'axios'; +import { AuthCredentials, BIIntelSecrets, LoginMethod } from '@wso2/ballerina-core'; +import { IWso2PlatformExtensionAPI } from '@wso2/wso2-platform-core'; import { jwtDecode, JwtPayload } from 'jwt-decode'; -import { AuthCredentials, DevantEnvSecrets, LoginMethod } from '@wso2/ballerina-core'; -import { checkDevantEnvironment } from '../../views/ai-panel/utils'; -import { getDevantStsToken } from '../../features/devant/activator'; -export const REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE = "Refresh token is not available."; +export const TOKEN_NOT_AVAILABLE_ERROR_MESSAGE = "Access token is not available."; +export const PLATFORM_EXTENSION_ID = 'wso2.wso2-platform'; export const TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL = "Token refresh is only supported for BI Intelligence authentication"; -export const AUTH_CREDENTIALS_SECRET_KEY = 'BallerinaAuthCredentials'; +export const AUTH_CREDENTIALS_SECRET_KEY = 'CopilotAuthCredentials'; +export const NO_AUTH_CREDENTIALS_FOUND = "No authentication credentials found."; + +/** + * Get the WSO2 Platform extension API, activating it if needed. + * Returns undefined if the extension is not installed. + */ +export const getPlatformExtensionAPI = async (): Promise => { + const platformExt = vscode.extensions.getExtension(PLATFORM_EXTENSION_ID); + if (!platformExt) { + return undefined; + } + if (!platformExt.isActive) { + await platformExt.activate(); + } + return platformExt.exports as IWso2PlatformExtensionAPI; +}; //TODO: What if user doesnt have github copilot. //TODO: Where does auth git get triggered @@ -120,6 +136,88 @@ async function copilotTokenExists() { return copilotToken !== undefined && copilotToken !== ''; } +// ================================== +// Platform Extension (Devant) Auth Utils +// ================================== + +/** + * Check if the WSO2 Platform extension is installed + */ +export const isPlatformExtensionAvailable = (): boolean => { + return !!vscode.extensions.getExtension(PLATFORM_EXTENSION_ID); +}; + +/** + * Get STS token from the platform extension + */ +export const getPlatformStsToken = async (): Promise => { + try { + const api = await getPlatformExtensionAPI(); + if (!api) { + return undefined; + } + return await api.getStsToken(); + } catch (error) { + console.error('Error getting STS token from platform extension:', error); + return undefined; + } +}; + +/** + * Check if user is logged into Devant via platform extension + */ +export const isDevantUserLoggedIn = async (): Promise => { + try { + const api = await getPlatformExtensionAPI(); + if (!api) { + return false; + } + return api.isLoggedIn(); + } catch (error) { + console.error('Error checking Devant login status:', error); + return false; + } +}; + +/** + * Exchange STS token for Copilot token via the token exchange endpoint + */ +export const exchangeStsToCopilotToken = async (stsToken: string): Promise => { + try { + const response = await axios.post(DEVANT_TOKEN_EXCHANGE_URL, { + subjectToken: stsToken + }, { + headers: { 'Content-Type': 'application/json' }, + validateStatus: () => true + }); + + if (response.status === 201 || response.status === 200) { + const { access_token, expires_in } = response.data; + return { + accessToken: access_token, + expiresAt: Date.now() + (expires_in * 1000) + }; + } + + throw new Error(response.data?.message || response.data?.reason || `Status ${response.status}`); + } catch (error) { + const reason = error instanceof Error ? error.message : 'Unknown error'; + vscode.window.showErrorMessage(`BI Copilot authentication failed: ${reason}`); + throw error; + } +}; + +/** + * Refresh the Copilot token using the STS token from platform extension + */ +export const refreshTokenViaStsExchange = async (): Promise => { + const stsToken = await getPlatformStsToken(); + if (!stsToken) { + throw new Error('Failed to get STS token from platform extension'); + } + return await exchangeStsToCopilotToken(stsToken); +}; + // ================================== // Structured Auth Credentials Utils // ================================== @@ -150,16 +248,11 @@ export const clearAuthCredentials = async (): Promise => { // BI Copilot Auth Utils // ================================== export const getLoginMethod = async (): Promise => { - // Priority 1: Check devant environment first - const devantCredentials = await checkDevantEnvironment(); - if (devantCredentials) { - return devantCredentials.loginMethod; - } - - // Priority 2: Check stored credentials + // Priority 1: Check Anthropic API key from environment if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim() !== "") { return LoginMethod.ANTHROPIC_KEY; } + // Priority 2: Check stored credentials const credentials = await getAuthCredentials(); if (credentials) { return credentials.loginMethod; @@ -170,50 +263,42 @@ export const getLoginMethod = async (): Promise => { export const getAccessToken = async (): Promise => { return new Promise(async (resolve, reject) => { try { - // Priority 1: Check devant environment (highest priority) - const devantCredentials = await checkDevantEnvironment(); - if (devantCredentials) { - resolve(devantCredentials); - return; - } - - // Priority 2: Check stored credentials + // Priority 1: Check Anthropic API key from environment if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim() !== "") { resolve({ loginMethod: LoginMethod.ANTHROPIC_KEY, secrets: { apiKey: process.env.ANTHROPIC_API_KEY.trim() } }); return; } + // Priority 2: Check stored credentials const credentials = await getAuthCredentials(); if (credentials) { switch (credentials.loginMethod) { case LoginMethod.BI_INTEL: try { - const { accessToken } = credentials.secrets; - let finalToken = accessToken; - - // Decode token and check expiration - const decoded = jwtDecode(accessToken); - const now = Math.floor(Date.now() / 1000); - if (decoded.exp && decoded.exp < now) { - finalToken = await getRefreshedAccessToken(); + const secrets = credentials.secrets as BIIntelSecrets; + let finalSecrets = secrets; + + // Check expiration with 5-minute buffer using expiresAt + const now = Date.now(); + const bufferMs = 5 * 60 * 1000; // 5 minutes + const isExpired = secrets.expiresAt && (secrets.expiresAt - bufferMs) < now; + + if (isExpired) { + await getRefreshedAccessToken(); + // Get updated credentials after refresh + const updatedCreds = await getAuthCredentials(); + if (updatedCreds && updatedCreds.loginMethod === LoginMethod.BI_INTEL) { + finalSecrets = updatedCreds.secrets as BIIntelSecrets; + } } resolve({ loginMethod: LoginMethod.BI_INTEL, - secrets: { - accessToken: finalToken, - refreshToken: credentials.secrets.refreshToken - } + secrets: finalSecrets }); return; - } catch (err) { - if (axios.isAxiosError(err)) { - const status = err.response?.status; - if (status === 400) { - reject(new Error("TOKEN_EXPIRED")); - return; - } - } - reject(err); + } catch (err: any) { + // Any failure to refresh BI_INTEL token means user needs to re-login + reject(new Error("TOKEN_EXPIRED")); return; } @@ -221,11 +306,11 @@ export const getAccessToken = async (): Promise => resolve(credentials); return; - case LoginMethod.DEVANT_ENV: + case LoginMethod.AWS_BEDROCK: resolve(credentials); return; - case LoginMethod.AWS_BEDROCK: + case LoginMethod.VERTEX_AI: resolve(credentials); return; @@ -276,113 +361,48 @@ export const getBiIntelId = async (): Promise => { }; +export const getVertexAiCredentials = async (): Promise<{ + projectId: string; + location: string; + clientEmail: string; + privateKey: string; +} | undefined> => { + const credentials = await getAuthCredentials(); + if (!credentials || credentials.loginMethod !== LoginMethod.VERTEX_AI) { + return undefined; + } + return credentials.secrets; +}; + export const getRefreshedAccessToken = async (): Promise => { return new Promise(async (resolve, reject) => { - const CommonReqHeaders = { - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf8', - 'Accept': 'application/json' - }; - try { const credentials = await getAuthCredentials(); if (!credentials || credentials.loginMethod !== LoginMethod.BI_INTEL) { throw new Error(TOKEN_REFRESH_ONLY_SUPPORTED_FOR_BI_INTEL); } - const { refreshToken } = credentials.secrets; - if (!refreshToken) { - reject(new Error(REFRESH_TOKEN_NOT_AVAILABLE_ERROR_MESSAGE)); - return; - } - - const params = new URLSearchParams({ - client_id: AUTH_CLIENT_ID, - refresh_token: refreshToken, - grant_type: 'refresh_token', - scope: 'openid email' - }); - - const response = await axios.post(`https://api.asgardeo.io/t/${AUTH_ORG}/oauth2/token`, params.toString(), { headers: CommonReqHeaders }); + // Try refreshing via STS token exchange from platform extension + try { + console.log('Refreshing token via STS exchange...'); + const newSecrets = await refreshTokenViaStsExchange(); - const newAccessToken = response.data.access_token; - const newRefreshToken = response.data.refresh_token; + // Update stored credentials + const updatedCredentials: AuthCredentials = { + loginMethod: LoginMethod.BI_INTEL, + secrets: newSecrets + }; + await storeAuthCredentials(updatedCredentials); - // Update stored credentials - const updatedCredentials: AuthCredentials = { - ...credentials, - secrets: { - accessToken: newAccessToken, - refreshToken: newRefreshToken - } - }; - await storeAuthCredentials(updatedCredentials); - - resolve(newAccessToken); + resolve(newSecrets.accessToken); + return; + } catch (stsError) { + console.error('STS token exchange failed:', stsError); + // If STS exchange fails, we can't refresh - reject + reject(new Error('Token refresh failed. Please login again.')); + } } catch (error: any) { reject(error); } }); }; - -// ================================== -// Devant STS Token Exchange Utils -// ================================== - -/** - * Exchanges a Choreo STS token for a Devant Bearer token - * @param choreoStsToken The Choreo STS token to exchange - * @returns DevantEnvSecrets containing the access token and calculated expiry time - */ -export const exchangeStsToken = async (choreoStsToken: string): Promise => { - try { - const response = await axios.post(getDevantExchangeUrl(), { - choreo_sts_token: choreoStsToken - }, { - headers: { - 'Content-Type': 'application/json' - } - }); - - const { access_token, expires_in } = response.data; - const devantEnv: DevantEnvSecrets = { - accessToken: access_token, - expiresAt: Date.now() + (expires_in * 1000) // Convert seconds to milliseconds - }; - - await storeAuthCredentials({ - loginMethod: LoginMethod.DEVANT_ENV, - secrets: devantEnv - }); - return devantEnv; - } catch (error: any) { - console.error('Error exchanging STS token:', error); - throw new Error(`Failed to exchange STS token: ${error.message}`); - } -}; - -/** - * Refreshes the Devant token by fetching a new STS token and exchanging it - * This is called when a 401 error occurs during DEVANT_ENV authentication - * @returns The new access token - */ -export const refreshDevantToken = async (): Promise => { - try { - // Get fresh STS token from platform extension - const newStsToken = await getDevantStsToken(); - - if (!newStsToken) { - throw new Error('Failed to retrieve STS token from platform extension'); - } - - // Exchange for new Bearer token - const newSecrets = await exchangeStsToken(newStsToken); - - // Update stored credentials (this is in-memory only for DEVANT_ENV) - // Note: checkDevantEnvironment already handles the storage, so we just return the token - - return newSecrets.accessToken; - } catch (error: any) { - console.error('Error refreshing Devant token:', error); - throw error; - } -}; diff --git a/workspaces/ballerina/ballerina-extension/src/utils/bi.ts b/workspaces/ballerina/ballerina-extension/src/utils/bi.ts index c817053b70..fc6c50c245 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/bi.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/bi.ts @@ -43,10 +43,12 @@ import { parse } from "@iarna/toml"; import { getProjectTomlValues } from "./config"; import { extension } from "../BalExtensionContext"; -export const README_FILE = "readme.md"; +export const README_FILE = "README.md"; export const FUNCTIONS_FILE = "functions.bal"; export const DATA_MAPPING_FILE = "data_mappings.bal"; +export const VALIDATOR_PACKAGE_NAME = "wso2/strict.library"; + /** * Interface for the processed project information */ @@ -112,6 +114,10 @@ generated/ # Contains configuration values used during development time. # See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. Config.toml + +# File used to enable development-time tracing. +# This should not be committed to version control. +trace_enabled.bal `; export function getUsername(): string { @@ -297,7 +303,7 @@ function setupProjectInfo(projectRequest: ProjectRequest): ProcessedProjectInfo }; } -export function createBIWorkspace(projectRequest: ProjectRequest): string { +export async function createBIWorkspace(projectRequest: ProjectRequest): Promise { const ballerinaTomlContent = ` [workspace] packages = ["${projectRequest.packageName}"] @@ -312,7 +318,7 @@ packages = ["${projectRequest.packageName}"] writeBallerinaFileDidOpen(ballerinaTomlPath, ballerinaTomlContent); // Create Ballerina Package - createBIProjectPure({ ...projectRequest, projectPath: workspaceRoot, createDirectory: true }); + await createBIProjectPure({ ...projectRequest, projectPath: workspaceRoot, createDirectory: true }); // create settings.json file createVSCodeSettings(workspaceRoot); @@ -321,7 +327,7 @@ packages = ["${projectRequest.packageName}"] return workspaceRoot; } -export function createBIProjectPure(projectRequest: ProjectRequest): string { +export async function createBIProjectPure(projectRequest: ProjectRequest): Promise { const projectInfo = setupProjectInfo(projectRequest); const { projectRoot, finalOrgName, finalVersion, packageName: finalPackageName, integrationName } = projectInfo; @@ -333,7 +339,6 @@ export function createBIProjectPure(projectRequest: ProjectRequest): string { // Build the distribution line if version is available const distributionLine = distribution ? `distribution = "${distribution}"\n` : ''; - const libraryLine = projectRequest.isLibrary ? '\nlibrary = true' : ''; const ballerinaTomlContent = ` [package] org = "${finalOrgName}" @@ -382,6 +387,21 @@ sticky = true const datamappingsBalPath = path.join(projectRoot, 'data_mappings.bal'); writeBallerinaFileDidOpen(datamappingsBalPath, EMPTY); + if (projectRequest.isLibrary) { + const libraryBal = path.join(projectRoot, 'lib.bal'); + + // TODO: Enable pulling the validator package and adding the import to the lib.bal file + // once this this implemented: https://github.com/wso2/product-ballerina-integrator/issues/2409 + + // const libraryBalContent = `import ${VALIDATOR_PACKAGE_NAME} as _;`; + // try { + // await runBackgroundTerminalCommand(`bal pull ${VALIDATOR_PACKAGE_NAME}`); + // } catch (error) { + // console.error('Failed to pull validator package:', error); + // } + writeBallerinaFileDidOpen(libraryBal, EMPTY); + } + // Create .vscode configuration files createVSCodeSettingsWithLaunch(projectRoot); @@ -413,7 +433,7 @@ export async function convertProjectToWorkspace(params: AddProjectToWorkspaceReq createWorkspaceToml(newDirectory, currentPackageName); addToWorkspaceToml(newDirectory, params.packageName); - createProjectInWorkspace(params, newDirectory); + await createProjectInWorkspace(params, newDirectory); // create settings.json file createVSCodeSettings(newDirectory); @@ -425,7 +445,7 @@ export async function addProjectToExistingWorkspace(params: AddProjectToWorkspac const workspacePath = StateMachine.context().workspacePath; addToWorkspaceToml(workspacePath, params.packageName); - createProjectInWorkspace(params, workspacePath); + await createProjectInWorkspace(params, workspacePath); } function createWorkspaceToml(workspacePath: string, packageName: string) { @@ -537,7 +557,7 @@ function removePackageFromToml(tomlContent: string, packagePath: string): string } } -function createProjectInWorkspace(params: AddProjectToWorkspaceRequest, workspacePath: string): string { +async function createProjectInWorkspace(params: AddProjectToWorkspaceRequest, workspacePath: string): Promise { const projectRequest: ProjectRequest = { projectName: params.projectName, packageName: params.packageName, @@ -548,7 +568,7 @@ function createProjectInWorkspace(params: AddProjectToWorkspaceRequest, workspac isLibrary: params.isLibrary }; - return createBIProjectPure(projectRequest); + return await createBIProjectPure(projectRequest); } export function openInVSCode(projectRoot: string) { diff --git a/workspaces/ballerina/ballerina-extension/src/utils/config.ts b/workspaces/ballerina/ballerina-extension/src/utils/config.ts index d2ddc220fc..de73c71053 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/config.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/config.ts @@ -382,6 +382,15 @@ export function getOrgAndPackageName(projectInfo: ProjectInfo, projectPath: stri } export async function isLibraryProject(projectPath: string): Promise { - const tomlValues = await getProjectTomlValues(projectPath); - return tomlValues?.package?.library === true; + const libBalPath = path.join(projectPath, 'lib.bal'); + return fs.existsSync(libBalPath); + + // TODO: Enable checking the validator import in the lib.bal file + // once this this implemented: https://github.com/wso2/product-ballerina-integrator/issues/2409 + + // if (fs.existsSync(libBalPath)) { + // const libBalContent = fs.readFileSync(libBalPath, 'utf8'); + // return libBalContent.includes(`import ${VALIDATOR_PACKAGE_NAME} as _;`); + // } + // return false; } diff --git a/workspaces/ballerina/ballerina-extension/src/utils/project-artifacts.ts b/workspaces/ballerina/ballerina-extension/src/utils/project-artifacts.ts index 737f95fb1a..6c1539fcaf 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/project-artifacts.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/project-artifacts.ts @@ -192,7 +192,7 @@ async function getEntryValue(artifact: BaseArtifact, projectPath: string, icon: break; case DIRECTORY_MAP.SERVICE: // Do things related to service - entryValue.name = artifact.name; // GraphQL Service - /foo + entryValue.name = getServiceDisplayName(artifact); // GraphQL Service - /foo entryValue.icon = getCustomEntryNodeIcon(artifact.module); if (artifact.module === "ai") { entryValue.resources = []; @@ -249,6 +249,18 @@ async function getEntryValue(artifact: BaseArtifact, projectPath: string, icon: return entryValue; } +function getServiceDisplayName(artifact: BaseArtifact): string { + if (artifact.module !== "ftp") { + return artifact.name; + } + const accessor = artifact.accessor?.trim(); + if (!accessor) { + return artifact.name; + } + const suffix = ` - ${accessor}`; + return artifact.name.includes(suffix) ? artifact.name : `${artifact.name}${suffix}`; +} + /** * Maps an ARTIFACT_TYPE category key and a specific artifact to the corresponding DIRECTORY_MAP key and a default icon. * Note: The icon returned here is a base icon; `getEntryValue` might assign a more specific icon later based on the module. diff --git a/workspaces/ballerina/ballerina-extension/src/utils/toml-utils.ts b/workspaces/ballerina/ballerina-extension/src/utils/toml-utils.ts index f5cc1d5887..10401d9c3c 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/toml-utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/toml-utils.ts @@ -22,6 +22,7 @@ export interface ConfigVariable { name: string; description: string; type?: "string" | "int"; + secret?: boolean; } // Cache regex for performance @@ -83,91 +84,6 @@ export function getAllConfigStatus( /** * Create or update Config.toml with placeholder variables */ -export function createConfigWithPlaceholders( - configPath: string, - variables: ConfigVariable[], - overwrite: boolean = false -): void { - let config: Record = {}; - - // Read existing config if exists - if (fs.existsSync(configPath)) { - try { - const content = fs.readFileSync(configPath, "utf-8"); - config = parse(content) as Record; - } catch (error) { - console.error(`[TOML Utils] Error reading existing config:`, error); - throw error; - } - } - - // Add placeholder variables (convert API_KEY to apikey) - for (const variable of variables) { - const tomlKey = toTomlKey(variable.name); - - if (!Object.prototype.hasOwnProperty.call(config, tomlKey) || overwrite) { - config[tomlKey] = `\${${variable.name}}`; - } - } - - // Write back to file - try { - const dirPath = path.dirname(configPath); - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } - - const tomlContent = stringify(config); - fs.writeFileSync(configPath, tomlContent, "utf-8"); - - console.log(`[TOML Utils] Created/updated Config.toml with ${variables.length} placeholder(s)`); - } catch (error) { - console.error(`[TOML Utils] Error writing config:`, error); - throw error; - } -} - -/** - * Check configuration value status - returns metadata only, never actual values - */ -export function checkConfigurationStatus( - configPath: string, - variableNames: string[] -): Record { - const status: Record = {}; - - if (!fs.existsSync(configPath)) { - for (const name of variableNames) { - status[name] = "missing"; - } - return status; - } - - try { - const content = fs.readFileSync(configPath, "utf-8"); - const config = parse(content) as Record; - - for (const name of variableNames) { - const tomlKey = toTomlKey(name); - const value = getNestedValue(config, tomlKey); - - if (value === undefined || value === null) { - status[name] = "missing"; - } else if (typeof value === "string" && value.startsWith("${")) { - status[name] = "missing"; - } else { - status[name] = "filled"; - } - } - } catch (error) { - console.error(`[TOML Utils] Error checking configuration status:`, error); - for (const name of variableNames) { - status[name] = "missing"; - } - } - - return status; -} /** * Write configuration values to Config.toml - SECURITY: never logs values @@ -197,10 +113,9 @@ export function writeConfigValuesToConfig( } } - // Replace placeholders with actual values (convert API_KEY to apikey) const intKeys = new Set(); for (const [variableName, value] of Object.entries(configValues)) { - const tomlKey = toTomlKey(variableName); + const tomlKey = variableName; const varType = typeMap.get(variableName) || "string"; // Convert value based on type @@ -263,15 +178,7 @@ function getNestedValue(obj: any, key: string): any { } export function validateVariableName(name: string): boolean { - return /^[A-Z_0-9]+$/.test(name); -} - -/** - * Converts configuration variable name to TOML key format - * Example: API_KEY -> apikey, DB_HOST -> dbhost - */ -export function toTomlKey(variableName: string): string { - return variableName.toLowerCase().replace(/_/g, ""); + return /^[a-zA-Z][a-zA-Z0-9]*$/.test(name); } /** @@ -293,7 +200,7 @@ export function readExistingConfigValues( const config = parse(content) as Record; for (const name of variableNames) { - const tomlKey = toTomlKey(name); + const tomlKey = name; const value = getNestedValue(config, tomlKey); // Include the value if it exists and is not a placeholder diff --git a/workspaces/ballerina/ballerina-extension/src/utils/uri-handlers.ts b/workspaces/ballerina/ballerina-extension/src/utils/uri-handlers.ts index 795232e6da..e94dd0ef1c 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/uri-handlers.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/uri-handlers.ts @@ -20,7 +20,6 @@ import { window, Uri, ProviderResult, commands } from "vscode"; import { BallerinaExtension } from "../core"; import { handleOpenFile, handleOpenRepo } from "."; import { CMP_OPEN_VSCODE_URL, TM_EVENT_OPEN_FILE_URL_START, TM_EVENT_OPEN_REPO_URL_START, sendTelemetryEvent } from "../features/telemetry"; -import { exchangeAuthCode } from "../views/ai-panel/auth"; import { IOpenCompSrcCmdParams, CommandIds as PlatformExtCommandIds } from "@wso2/wso2-platform-core"; export function activateUriHandlers(ballerinaExtInstance: BallerinaExtension) { @@ -50,12 +49,9 @@ export function activateUriHandlers(ballerinaExtInstance: BallerinaExtension) { } break; case '/signin': - const query = new URLSearchParams(uri.query); - const code = query.get('code'); - if (code) { - exchangeAuthCode(code); - } - console.log("Auth code not found. Ignoring"); + // Legacy OAuth callback route - no longer used + // Authentication is now handled via Devant platform extension + console.log("Legacy /signin route called - authentication now uses Devant platform extension"); break; case '/open': const org = urlParams.get("org"); diff --git a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts index 1bf828d31d..31468e1723 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiMachine.ts @@ -18,11 +18,18 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { createMachine, assign, interpret } from 'xstate'; -import { AIMachineStateValue, AIPanelPrompt, AIMachineEventType, AIMachineContext, AIUserToken, AIMachineSendableEvent, LoginMethod, SHARED_COMMANDS } from '@wso2/ballerina-core'; +import { AIMachineStateValue, AIPanelPrompt, AIMachineEventType, AIMachineContext, AIMachineSendableEvent, LoginMethod, SHARED_COMMANDS } from '@wso2/ballerina-core'; import { AiPanelWebview } from './webview'; import { extension } from '../../BalExtensionContext'; import { getAccessToken, getLoginMethod } from '../../utils/ai/auth'; -import { checkToken, initiateInbuiltAuth, logout, validateApiKey, validateAwsCredentials } from './utils'; +import { checkToken, initiateDevantAuth, logout, validateApiKey, validateAwsCredentials, validateVertexAiCredentials } from './utils'; +import { + isDevantUserLoggedIn, + getPlatformStsToken, + exchangeStsToCopilotToken, + storeAuthCredentials, + getPlatformExtensionAPI +} from '../../utils/ai/auth'; import * as vscode from 'vscode'; import { notifyAiPromptUpdated } from '../../RPCLayer'; @@ -169,6 +176,12 @@ const aiMachine = createMachine({ actions: assign({ loginMethod: (_ctx) => LoginMethod.AWS_BEDROCK }) + }, + [AIMachineEventType.AUTH_WITH_VERTEX_AI]: { + target: 'Authenticating', + actions: assign({ + loginMethod: (_ctx) => LoginMethod.VERTEX_AI + }) } } }, @@ -189,6 +202,10 @@ const aiMachine = createMachine({ cond: (context) => context.loginMethod === LoginMethod.AWS_BEDROCK, target: 'awsBedrockFlow' }, + { + cond: (context) => context.loginMethod === LoginMethod.VERTEX_AI, + target: 'vertexAiFlow' + }, { target: 'ssoFlow' // default } @@ -291,6 +308,41 @@ const aiMachine = createMachine({ }) } } + }, + vertexAiFlow: { + on: { + [AIMachineEventType.SUBMIT_VERTEX_AI_CREDENTIALS]: { + target: 'validatingVertexAiCredentials', + actions: assign({ + errorMessage: (_ctx) => undefined + }) + }, + [AIMachineEventType.CANCEL_LOGIN]: { + target: '#ballerina-ai.Unauthenticated', + actions: assign({ + loginMethod: (_ctx) => undefined, + errorMessage: (_ctx) => undefined, + }) + } + } + }, + validatingVertexAiCredentials: { + invoke: { + id: 'validateVertexAiCredentials', + src: 'validateVertexAiCredentials', + onDone: { + target: '#ballerina-ai.Authenticated', + actions: assign({ + errorMessage: (_ctx) => undefined, + }) + }, + onError: { + target: 'vertexAiFlow', + actions: assign({ + errorMessage: (_ctx, event) => event.data?.message || 'Vertex AI credentials validation failed' + }) + } + } } } }, @@ -357,10 +409,31 @@ const aiMachine = createMachine({ const openLogin = async () => { return new Promise(async (resolve, reject) => { try { - const status = await initiateInbuiltAuth(); + // Check if already logged into Devant + const isLoggedIn = await isDevantUserLoggedIn(); + if (isLoggedIn) { + // Already logged in, exchange token + const stsToken = await getPlatformStsToken(); + if (!stsToken) { + throw new Error('Failed to get STS token from platform extension'); + } + + const secrets = await exchangeStsToCopilotToken(stsToken); + await storeAuthCredentials({ + loginMethod: LoginMethod.BI_INTEL, + secrets + }); + aiStateService.send(AIMachineEventType.COMPLETE_AUTH); + resolve(true); + return; + } + + // Not logged in, trigger platform extension login + const status = await initiateDevantAuth(); if (!status) { aiStateService.send(AIMachineEventType.CANCEL_LOGIN); } + // Auth completion will be handled by platform extension login state listener resolve(status); } catch (error) { reject(error); @@ -389,6 +462,19 @@ const validateAwsCredentialsService = async (_context: AIMachineContext, event: }); }; +const validateVertexAiCredentialsService = async (_context: AIMachineContext, event: any) => { + const { projectId, location, clientEmail, privateKey } = event.payload || {}; + if (!projectId || !location || !clientEmail || !privateKey) { + throw new Error('GCP Project ID, location, client email, and private key are required'); + } + return await validateVertexAiCredentials({ + projectId, + location, + clientEmail, + privateKey + }); +}; + const getTokenAfterAuth = async () => { const result = await getAccessToken(); const loginMethod = await getLoginMethod(); @@ -404,6 +490,7 @@ const aiStateService = interpret(aiMachine.withConfig({ openLogin: openLogin, validateApiKey: validateApiKeyService, validateAwsCredentials: validateAwsCredentialsService, + validateVertexAiCredentials: validateVertexAiCredentialsService, getTokenAfterAuth: getTokenAfterAuth, }, actions: { @@ -422,8 +509,52 @@ const isExtendedEvent = ( return typeof arg !== "string"; }; +/** + * Set up listener for platform extension login state changes. + * When user logs in via platform extension, we exchange the token and complete auth. + */ +const setupPlatformExtensionListener = () => { + getPlatformExtensionAPI().then( + (api) => { + if (!api || !api.subscribeIsLoggedIn) { + return; + } + api.subscribeIsLoggedIn(async (isLoggedIn: boolean) => { + const currentState = aiStateService.getSnapshot().value; + + // Only handle login events when we're in the SSO authentication flow + if (isLoggedIn && typeof currentState === 'object' && 'Authenticating' in currentState) { + try { + const stsToken = await getPlatformStsToken(); + if (!stsToken) { + console.error('Failed to get STS token after platform login'); + return; + } + + const secrets = await exchangeStsToCopilotToken(stsToken); + await storeAuthCredentials({ + loginMethod: LoginMethod.BI_INTEL, + secrets + }); + aiStateService.send(AIMachineEventType.COMPLETE_AUTH); + } catch (error) { + console.error('Failed to exchange token after platform login:', error); + aiStateService.send(AIMachineEventType.CANCEL_LOGIN); + } + } + }); + }, + (error) => { + console.error('Failed to activate platform extension for login listener:', error); + } + ); +}; + export const AIStateMachine = { - initialize: () => aiStateService.start(), + initialize: () => { + setupPlatformExtensionListener(); + return aiStateService.start(); + }, service: () => { return aiStateService; }, context: () => { return aiStateService.getSnapshot().context; }, state: () => { return aiStateService.getSnapshot().value as AIMachineStateValue; }, diff --git a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/auth.ts b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/auth.ts deleted file mode 100644 index f1be64ab87..0000000000 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/auth.ts +++ /dev/null @@ -1,93 +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 axios from 'axios'; -import { AUTH_CLIENT_ID, AUTH_ORG, AUTH_REDIRECT_URL } from '../../features/ai/utils'; -import { AIStateMachine } from './aiMachine'; -import { AIMachineEventType, AuthCredentials, LoginMethod } from '@wso2/ballerina-core'; -import { storeAuthCredentials } from '../../utils/ai/auth'; - -export interface AccessToken { - accessToken: string; - expirationTime?: number; - loginTime: string; - refreshToken?: string; -} - -const CommonReqHeaders = { - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf8', - 'Accept': 'application/json' -}; - -export async function getAuthUrl(callbackUri: string): Promise { - - // return `${this._config.loginUrl}?profile=vs-code&client_id=${this._config.clientId}` - // + `&state=${stateBase64}&code_challenge=${this._challenge.code_challenge}`; - const state = encodeURIComponent(btoa(JSON.stringify({ callbackUri }))); - return `https://api.asgardeo.io/t/${AUTH_ORG}/oauth2/authorize?response_type=code&redirect_uri=${AUTH_REDIRECT_URL}&client_id=${AUTH_CLIENT_ID}&scope=openid%20email&state=${state}`; -} - -export function getLogoutUrl(): string { - return `https://api.asgardeo.io/t/${AUTH_ORG}/oidc/logout`; -} - -export async function exchangeAuthCodeNew(authCode: string): Promise { - const params = new URLSearchParams({ - client_id: AUTH_CLIENT_ID, - code: authCode, - grant_type: 'authorization_code', - redirect_uri: AUTH_REDIRECT_URL, - scope: 'openid email' - }); - try { - const response = await axios.post(`https://api.asgardeo.io/t/${AUTH_ORG}/oauth2/token`, params.toString(), { headers: CommonReqHeaders }); - return { - accessToken: response.data.access_token, - refreshToken: response.data.refresh_token, - loginTime: new Date().toISOString(), - expirationTime: response.data.expires_in - }; - } catch (err) { - throw new Error(`Error while exchanging auth code to token: ${err}`); - } -} - -export async function exchangeAuthCode(authCode: string) { - if (!authCode) { - throw new Error("Auth code is not provided."); - } else { - try { - const response = await exchangeAuthCodeNew(authCode); - - // Store credentials in structured format - const credentials: AuthCredentials = { - loginMethod: LoginMethod.BI_INTEL, - secrets: { - accessToken: response.accessToken, - refreshToken: response.refreshToken ?? '' - } - }; - await storeAuthCredentials(credentials); - - AIStateMachine.sendEvent(AIMachineEventType.COMPLETE_AUTH); - } catch (error: any) { - const errMsg = "Error while signing in to Copilot! " + error?.message; - throw new Error(errMsg); - } - } -} diff --git a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/chatStateStorage.ts b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/chatStateStorage.ts index 87db288438..82b9812db4 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/chatStateStorage.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/chatStateStorage.ts @@ -26,6 +26,7 @@ import { } from '@wso2/ballerina-core/lib/state-machine-types'; import { Command } from '@wso2/ballerina-core'; import * as crypto from 'crypto'; +import { approvalManager } from '../../features/ai/state/ApprovalManager'; /** * Active execution handle @@ -695,6 +696,7 @@ export class ChatStateStorage { } console.log(`[ChatStateStorage] Aborting execution: ${execution.generationId} for thread: ${threadId}`); + approvalManager.cancelAllPending("Agent execution aborted by user"); execution.abortController.abort(); // Cleanup diff --git a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/index.ts b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/index.ts index b1fa5eba8f..77ab5332cf 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/index.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/index.ts @@ -1,2 +1 @@ export * from './activate'; -export * from './auth'; diff --git a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts index 59a5f05b27..9365bfbe71 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/ai-panel/utils.ts @@ -20,13 +20,20 @@ import * as vscode from 'vscode'; import { AIUserToken, LoginMethod, AuthCredentials } from '@wso2/ballerina-core'; import { createAnthropic } from '@ai-sdk/anthropic'; import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; +import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; -import { getAuthUrl, getLogoutUrl } from './auth'; import { extension } from '../../BalExtensionContext'; -import { getAccessToken, clearAuthCredentials, storeAuthCredentials, getLoginMethod, exchangeStsToken, getAuthCredentials } from '../../utils/ai/auth'; -import { DEVANT_STS_TOKEN_CONFIG } from '../../features/ai/utils'; +import { + getAccessToken, + getLoginMethod, + clearAuthCredentials, + storeAuthCredentials, + isPlatformExtensionAvailable, + isDevantUserLoggedIn, + getPlatformStsToken, + exchangeStsToCopilotToken +} from '../../utils/ai/auth'; import { getBedrockRegionalPrefix } from '../../features/ai/utils/ai-client'; -import { getDevantStsToken } from '../../features/devant/activator'; const LEGACY_ACCESS_TOKEN_SECRET_KEY = 'BallerinaAIUser'; const LEGACY_REFRESH_TOKEN_SECRET_KEY = 'BallerinaAIRefreshToken'; @@ -37,12 +44,38 @@ export const checkToken = async (): Promise => { // Clean up any legacy tokens on initialization await cleanupLegacyTokens(); + // First check if we have stored credentials const credentials = await getAccessToken(); - if (!credentials) { - resolve(undefined); + if (credentials) { + resolve(credentials); return; } - resolve(credentials); + + // No stored credentials - check if user is logged into Devant + if (isPlatformExtensionAvailable()) { + const isLoggedIn = await isDevantUserLoggedIn(); + if (isLoggedIn) { + // User is logged into Devant but no stored credentials + // Exchange STS token and store credentials + try { + const stsToken = await getPlatformStsToken(); + if (stsToken) { + const secrets = await exchangeStsToCopilotToken(stsToken); + const newCredentials: AuthCredentials = { + loginMethod: LoginMethod.BI_INTEL, + secrets + }; + await storeAuthCredentials(newCredentials); + resolve(newCredentials); + return; + } + } catch (exchangeError) { + console.error('Failed to exchange STS token during checkToken:', exchangeError); + } + } + } + + resolve(undefined); } catch (error) { reject(error); } @@ -63,13 +96,14 @@ const cleanupLegacyTokens = async (): Promise => { } }; -export const logout = async (isUserLogout: boolean = true) => { - // For user-initiated logout, check if we need to redirect to SSO logout - if (isUserLogout) { - const credentials = await checkToken(); - if (credentials.loginMethod === LoginMethod.BI_INTEL) { - const logoutURL = getLogoutUrl(); - vscode.env.openExternal(vscode.Uri.parse(logoutURL)); +export const logout = async (_isUserLogout: boolean = true) => { + // Sign out from the WSO2 Platform extension if logged in via BI_INTEL + const loginMethod = await getLoginMethod(); + if (loginMethod === LoginMethod.BI_INTEL && isPlatformExtensionAvailable()) { + try { + await vscode.commands.executeCommand('wso2.wso2-platform.sign.out'); + } catch (error) { + console.error('Error signing out from WSO2 Platform extension:', error); } } @@ -77,12 +111,18 @@ export const logout = async (isUserLogout: boolean = true) => { await clearAuthCredentials(); }; -export async function initiateInbuiltAuth() { - const callbackUri = await vscode.env.asExternalUri( - vscode.Uri.parse(`${vscode.env.uriScheme}://wso2.ballerina/signin`) - ); - const oauthURL = await getAuthUrl(callbackUri.toString()); - return vscode.env.openExternal(vscode.Uri.parse(oauthURL)); +/** + * Initiate Devant authentication via the platform extension. + * Returns true if login was triggered, false if platform extension is not available. + */ +export async function initiateDevantAuth(): Promise { + if (!isPlatformExtensionAvailable()) { + throw new Error('WSO2 Platform extension is not installed. Please install it to use BI Copilot.'); + } + + // Trigger platform extension login command + await vscode.commands.executeCommand('wso2.wso2-platform.sign.in'); + return true; } export const validateApiKey = async (apiKey: string, loginMethod: LoginMethod): Promise => { @@ -133,53 +173,6 @@ export const validateApiKey = async (apiKey: string, loginMethod: LoginMethod): } }; -export const checkDevantEnvironment = async (): Promise => { - // Check if CLOUD_STS_TOKEN environment variable exists (Devant flow identifier) - if (!('CLOUD_STS_TOKEN' in process.env)) { - return undefined; - } - - try { - // Check if a valid access token already exists to avoid redundant exchanges - const existingCredentials = await getAuthCredentials(); - - if (existingCredentials && existingCredentials.loginMethod === LoginMethod.DEVANT_ENV) { - // existing session, check expiry - const { expiresAt } = existingCredentials.secrets; - const now = Date.now(); - - // If token is still valid (not expired), return existing credentials - if (expiresAt && expiresAt > now) { - return existingCredentials; - } - } - if (existingCredentials && existingCredentials.loginMethod !== LoginMethod.DEVANT_ENV) { - // not devant - return undefined; - } - - // Get STS token from config or platform extension - const choreoStsToken = await getDevantStsToken() || DEVANT_STS_TOKEN_CONFIG; - - if (!choreoStsToken || choreoStsToken.trim() === '') { - console.warn('CLOUD_STS_TOKEN env variable exists but no STS token available'); - return undefined; - } - - // Exchange STS token for Bearer token (if no valid token exists or token expired) - const devantSecrets = await exchangeStsToken(choreoStsToken); - - // Return devant credentials without storing (always read from env and exchange on demand) - return { - loginMethod: LoginMethod.DEVANT_ENV, - secrets: devantSecrets - }; - } catch (error) { - console.error('Error in checkDevantEnvironment:', error); - return undefined; - } -}; - export const validateAwsCredentials = async (credentials: { accessKeyId: string; secretAccessKey: string; @@ -251,3 +244,61 @@ export const validateAwsCredentials = async (credentials: { throw new Error('Validation failed. Please check the log for more details.'); } }; + +export const validateVertexAiCredentials = async (credentials: { + projectId: string; + location: string; + clientEmail: string; + privateKey: string; +}): Promise => { + const { projectId, location, clientEmail, privateKey } = credentials; + + if (!projectId || !location || !clientEmail || !privateKey) { + throw new Error('GCP Project ID, location, client email, and private key are required.'); + } + + try { + const vertexAnthropic = createVertexAnthropic({ + project: projectId, + location: location, + googleAuthOptions: { + credentials: { + client_email: clientEmail, + private_key: privateKey, + }, + }, + }); + + await generateText({ + model: vertexAnthropic('claude-3-5-haiku@20241022'), + maxOutputTokens: 1, + messages: [{ role: 'user', content: 'Hi' }] + }); + + const authCredentials: AuthCredentials = { + loginMethod: LoginMethod.VERTEX_AI, + secrets: { + projectId, + location, + clientEmail, + privateKey + } + }; + await storeAuthCredentials(authCredentials); + + return { credentials: authCredentials }; + + } catch (error) { + console.error('Vertex AI validation failed:', error); + if (error instanceof Error) { + if (error.message.includes('401') || error.message.includes('authentication') || error.message.includes('UNAUTHENTICATED')) { + throw new Error('Invalid credentials. Please check your service account email and private key.'); + } else if (error.message.includes('403') || error.message.includes('PERMISSION_DENIED')) { + throw new Error('Permission denied. Please ensure your service account has access to Vertex AI.'); + } else if (error.message.includes('404') || error.message.includes('NOT_FOUND')) { + throw new Error('Project or location not found. Please check your GCP Project ID and location.'); + } + } + throw new Error('Validation failed. Please check the log for more details.'); + } +}; diff --git a/workspaces/ballerina/ballerina-extension/src/views/evaluation-report/webview.ts b/workspaces/ballerina/ballerina-extension/src/views/evaluation-report/webview.ts new file mode 100644 index 0000000000..88e16cfd2a --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/views/evaluation-report/webview.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Disposable, Uri, ViewColumn, WebviewPanel, window } from "vscode"; +import { extension } from "../../BalExtensionContext"; +import path from "path"; +import * as fs from "fs"; + +export class EvaluationReportWebview { + public static currentPanel: EvaluationReportWebview | undefined; + private _panel: WebviewPanel; + private _disposables: Disposable[] = []; + + private constructor(panel: WebviewPanel, reportContent: string, reportDir: Uri) { + this._panel = panel; + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + // Convert local resource paths to webview URIs + const processedHtml = this.processHtmlContent(reportContent, reportDir); + + this._panel.webview.html = processedHtml; + } + + private processHtmlContent(reportContent: string, reportDir: Uri): string { + // First, inject VS Code API script + let html = reportContent.replace( + "", + `` + ); + + // If wasn't found, try injecting at the start of body + if (html === reportContent) { + html = reportContent.replace( + /]*)>/i, + `` + ); + } + + // Convert relative paths to webview URIs + // Handle src="./..." or src="..." (relative paths) + html = html.replace(/(?:src|href)=["'](?!http|https|data:)([^"']+)["']/gi, (match, relativePath) => { + try { + const resourcePath = Uri.joinPath(reportDir, relativePath); + const webviewUri = this._panel.webview.asWebviewUri(resourcePath); + return match.replace(relativePath, webviewUri.toString()); + } catch (e) { + console.error('Failed to convert resource path:', relativePath, e); + return match; + } + }); + + return html; + } + + public static async createOrShow(reportUri: Uri): Promise { + const reportPath = reportUri.fsPath; + + // Validate file exists + if (!fs.existsSync(reportPath)) { + window.showErrorMessage(`Evaluation report not found: ${reportPath}`); + return; + } + + // Read HTML content + let reportContent: string; + try { + reportContent = fs.readFileSync(reportPath, 'utf8'); + } catch (error) { + window.showErrorMessage(`Failed to read evaluation report: ${error}`); + console.error('Failed to read evaluation report:', error); + return; + } + + const fileName = path.basename(reportPath); + const reportDir = Uri.file(path.dirname(reportPath)); + + // Dispose existing panel so the new one can be created with the correct localResourceRoots + if (EvaluationReportWebview.currentPanel) { + EvaluationReportWebview.currentPanel.dispose(); + } + + // Create new panel + const panel = window.createWebviewPanel( + "ballerinaEvaluationReport", + `Evaluation Report - ${fileName}`, + ViewColumn.Active, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [reportDir], + } + ); + + panel.iconPath = { + light: Uri.file(path.join(extension.context.extensionPath, "resources", "icons", "dark-icon.svg")), + dark: Uri.file(path.join(extension.context.extensionPath, "resources", "icons", "light-icon.svg")), + }; + + EvaluationReportWebview.currentPanel = new EvaluationReportWebview(panel, reportContent, reportDir); + } + + private updateContent(reportContent: string, reportDir: Uri): void { + const processedHtml = this.processHtmlContent(reportContent, reportDir); + this._panel.webview.html = processedHtml; + } + + public dispose(): void { + EvaluationReportWebview.currentPanel = undefined; + this._panel.dispose(); + + while (this._disposables.length) { + const disposable = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } +} diff --git a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts index 6a70e71d06..ec8a1f82e6 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts @@ -100,6 +100,16 @@ export class VisualizerWebview { } }, extension.context); + vscode.workspace.onDidSaveTextDocument((document) => { + const configTomlSaved = document.languageId === LANGUAGE.TOML && + document.fileName.endsWith("Config.toml"); + const state = StateMachine.state(); + const machineReady = typeof state === 'object' && 'viewActive' in state && state.viewActive === "viewReady"; + if (configTomlSaved && machineReady) { + sendUpdateNotificationToWebview(true); + } + }, extension.context); + vscode.workspace.onDidDeleteFiles(() => { sendUpdateNotificationToWebview(); }); diff --git a/workspaces/ballerina/ballerina-rpc-client/package.json b/workspaces/ballerina/ballerina-rpc-client/package.json index 26ac13c5c4..56b3e8d1c7 100644 --- a/workspaces/ballerina/ballerina-rpc-client/package.json +++ b/workspaces/ballerina/ballerina-rpc-client/package.json @@ -19,6 +19,7 @@ "monaco-editor": "0.44.0", "react": "18.2.0", "react-dom": "18.2.0", + "@wso2/wso2-platform-core": "workspace:*", "vscode-messenger-common": "0.4.5", "vscode-messenger-webview": "0.5.1", "vscode-languageserver-types": "3.17.5" diff --git a/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts b/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts index 599835e60d..fa734bbbbe 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts @@ -75,6 +75,7 @@ import { TestManagerServiceRpcClient } from "./rpc-clients"; import { AiAgentRpcClient } from "./rpc-clients/ai-agent/rpc-client"; import { ICPServiceRpcClient } from "./rpc-clients/icp-service/rpc-client"; import { AgentChatRpcClient } from "./rpc-clients/agent-chat/rpc-client"; +import { PlatformExtRpcClient } from "./rpc-clients/platform-ext/platform-ext-client"; export class BallerinaRpcClient { @@ -97,6 +98,7 @@ export class BallerinaRpcClient { private _aiAgent: AiAgentRpcClient; private _icpManager: ICPServiceRpcClient; private _agentChat: AgentChatRpcClient; + private _platformExt: PlatformExtRpcClient; constructor() { this.messenger = new Messenger(vscode); @@ -119,6 +121,7 @@ export class BallerinaRpcClient { this._aiAgent = new AiAgentRpcClient(this.messenger); this._icpManager = new ICPServiceRpcClient(this.messenger); this._agentChat = new AgentChatRpcClient(this.messenger); + this._platformExt = new PlatformExtRpcClient(this.messenger); } getAIAgentRpcClient(): AiAgentRpcClient { @@ -189,6 +192,10 @@ export class BallerinaRpcClient { return this._migrateIntegration; } + getPlatformRpcClient(): PlatformExtRpcClient { + return this._platformExt; + } + getVisualizerLocation(): Promise { return this.messenger.sendRequest(getVisualizerLocation, HOST_EXTENSION); } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts index 8819bea335..fbf8df00ed 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts @@ -67,6 +67,7 @@ import { generateOpenAPI, getAIMachineSnapshot, getActiveTempDir, + getAffectedPackages, getChatMessages, getCheckpoints, getDefaultPrompt, @@ -75,12 +76,12 @@ import { getGeneratedDocumentation, getLoginMethod, getSemanticDiff, - getAffectedPackages, - isWorkspaceProject, getServiceNames, isCopilotSignedIn, isPlanModeFeatureEnabled, + isPlatformExtensionAvailable, isUserAuthenticated, + isWorkspaceProject, markAlertShown, openAIPanel, openChatWindowWithCommand, @@ -107,6 +108,10 @@ export class AiPanelRpcClient implements AIPanelAPI { return this._messenger.sendRequest(getLoginMethod, HOST_EXTENSION); } + isPlatformExtensionAvailable(): Promise { + return this._messenger.sendRequest(isPlatformExtensionAvailable, HOST_EXTENSION); + } + getDefaultPrompt(): Promise { return this._messenger.sendRequest(getDefaultPrompt, HOST_EXTENSION); } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts index 1de3365b79..f7a3b7273c 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts @@ -60,6 +60,7 @@ import { DeleteTypeRequest, DeleteTypeResponse, DeploymentRequest, + WorkspaceDeploymentRequest, DeploymentResponse, DevantMetadata, EndOfFileRequest, @@ -141,6 +142,7 @@ import { deleteProject, deleteType, deployProject, + deployWorkspace, formDidClose, formDidOpen, generateOpenApiClient, @@ -159,6 +161,7 @@ import { getDataMapperCompletions, getDesignModel, getDevantMetadata, + getWorkspaceDevantMetadata, getEnclosedFunction, getEndOfFile, getExpressionCompletions, @@ -204,7 +207,8 @@ import { updateType, updateTypes, validateProjectPath, - verifyTypeDelete + verifyTypeDelete, + WorkspaceDevantMetadata } from "@wso2/ballerina-core"; import { HOST_EXTENSION } from "vscode-messenger-common"; import { Messenger } from "vscode-messenger-webview"; @@ -339,7 +343,7 @@ export class BiDiagramRpcClient implements BIDiagramAPI { getConfigVariableNodeTemplate(params: GetConfigVariableNodeTemplateRequest): Promise { return this._messenger.sendRequest(getConfigVariableNodeTemplate, HOST_EXTENSION, params); } - + OpenConfigTomlRequest(params: OpenConfigTomlRequest): Promise { return this._messenger.sendRequest(openConfigToml, HOST_EXTENSION, params); } @@ -364,6 +368,10 @@ export class BiDiagramRpcClient implements BIDiagramAPI { return this._messenger.sendRequest(deployProject, HOST_EXTENSION, params); } + deployWorkspace(params: WorkspaceDeploymentRequest): Promise { + return this._messenger.sendRequest(deployWorkspace, HOST_EXTENSION, params); + } + openAIChat(params: AIChatRequest): void { return this._messenger.sendNotification(openAIChat, HOST_EXTENSION, params); } @@ -512,6 +520,10 @@ export class BiDiagramRpcClient implements BIDiagramAPI { return this._messenger.sendRequest(getDevantMetadata, HOST_EXTENSION); } + getWorkspaceDevantMetadata(): Promise { + return this._messenger.sendRequest(getWorkspaceDevantMetadata, HOST_EXTENSION); + } + generateOpenApiClient(params: OpenAPIClientGenerationRequest): Promise { return this._messenger.sendRequest(generateOpenApiClient, HOST_EXTENSION, params); } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/common/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/common/rpc-client.ts index a773146176..274ff3c41a 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/common/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/common/rpc-client.ts @@ -23,11 +23,13 @@ import { CommandsRequest, CommandsResponse, CommonRPCAPI, + DefaultOrgNameResponse, FileOrDirRequest, FileOrDirResponse, GoToSourceRequest, OpenExternalUrlRequest, PackageTomlValues, + PublishToCentralResponse, RunExternalCommandRequest, RunExternalCommandResponse, SampleDownloadRequest, @@ -42,18 +44,30 @@ import { experimentalEnabled, getBallerinaDiagnostics, getCurrentProjectTomlValues, + getDefaultOrgName, getTypeCompletions, getWorkspaceFiles, getWorkspaceRoot, getWorkspaceType, goToSource, + hasCentralPATConfigured, isNPSupported, openExternalUrl, + publishToCentral, runBackgroundTerminalCommand, selectFileOrDirPath, selectFileOrFolderPath, - showErrorMessage + showErrorMessage, + SetWebviewCacheRequestParam, + SetWebviewCache, + RestoreWebviewCache, + ClearWebviewCache, + ShowInfoModalRequest, + showInformationModal, + ShowQuickPickRequest, + showQuickPick } from "@wso2/ballerina-core"; +import { QuickPickItem } from "vscode"; import { HOST_EXTENSION } from "vscode-messenger-common"; import { Messenger } from "vscode-messenger-webview"; @@ -116,6 +130,14 @@ export class CommonRpcClient implements CommonRPCAPI { return this._messenger.sendNotification(showErrorMessage, HOST_EXTENSION, params); } + showInformationModal(params: ShowInfoModalRequest): Promise { + return this._messenger.sendRequest(showInformationModal, HOST_EXTENSION, params); + } + + showQuickPick(params: ShowQuickPickRequest): Promise { + return this._messenger.sendRequest(showQuickPick, HOST_EXTENSION, params); + } + getCurrentProjectTomlValues(): Promise> { return this._messenger.sendRequest(getCurrentProjectTomlValues, HOST_EXTENSION); } @@ -124,7 +146,31 @@ export class CommonRpcClient implements CommonRPCAPI { return this._messenger.sendRequest(getWorkspaceType, HOST_EXTENSION); } + setWebviewCache(params: SetWebviewCacheRequestParam): Promise { + return this._messenger.sendRequest(SetWebviewCache, HOST_EXTENSION, params); + } + + restoreWebviewCache(params: IDBValidKey): Promise { + return this._messenger.sendRequest(RestoreWebviewCache, HOST_EXTENSION, params); + } + + clearWebviewCache(params: IDBValidKey): Promise { + return this._messenger.sendRequest(ClearWebviewCache, HOST_EXTENSION, params); + } + downloadSelectedSampleFromGithub(params: SampleDownloadRequest): Promise { return this._messenger.sendRequest(downloadSelectedSampleFromGithub, HOST_EXTENSION, params); } + + getDefaultOrgName(): Promise { + return this._messenger.sendRequest(getDefaultOrgName, HOST_EXTENSION); + } + + publishToCentral(): Promise { + return this._messenger.sendRequest(publishToCentral, HOST_EXTENSION); + } + + hasCentralPATConfigured(): Promise { + return this._messenger.sendRequest(hasCentralPATConfigured, HOST_EXTENSION); + } } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/data-mapper/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/data-mapper/rpc-client.ts index b049b5bbb1..5cd1e4117b 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/data-mapper/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/data-mapper/rpc-client.ts @@ -27,6 +27,7 @@ import { ConvertExpressionRequest, ConvertExpressionResponse, ConvertToQueryRequest, + CreateConvertedVariableRequest, DMModelRequest, DataMapperAPI, DataMapperModelRequest, @@ -55,6 +56,7 @@ import { addSubMapping, clearTypeCache, convertToQuery, + createConvertedVariable, deleteClause, deleteMapping, deleteSubMapping, @@ -167,6 +169,10 @@ export class DataMapperRpcClient implements DataMapperAPI { return this._messenger.sendRequest(getConvertedExpression, HOST_EXTENSION, params); } + createConvertedVariable(params: CreateConvertedVariableRequest): Promise { + return this._messenger.sendRequest(createConvertedVariable, HOST_EXTENSION, params); + } + clearTypeCache(): Promise { return this._messenger.sendRequest(clearTypeCache, HOST_EXTENSION); } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/platform-ext/platform-ext-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/platform-ext/platform-ext-client.ts new file mode 100644 index 0000000000..64c9cee6b4 --- /dev/null +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/platform-ext/platform-ext-client.ts @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2026, 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 { PlatformExtAPI, getMarketplaceItems, getMarketplaceItem, getMarketplaceIdl, getConnections, deleteLocalConnectionsConfig, getDevantConsoleUrl, getConnection, onPlatformExtStoreStateChange, refreshConnectionList, getPlatformStore, setConnectedToDevant, setSelectedComponent, deployIntegrationInDevant, deleteDevantTempConfigs, generateCustomConnectorFromOAS, addDevantTempConfig, setSelectedEnv, createConnectionConfig, replaceDevantTempConfigValues, registerDevantMarketplaceService, createThirdPartyConnection, initializeDevantOASConnection, createInternalConnection, getComponentList } from "@wso2/ballerina-core"; +import { HOST_EXTENSION } from "vscode-messenger-common"; +import { Messenger } from "vscode-messenger-webview"; +import { GetMarketplaceListReq,MarketplaceListResp, ComponentKind, GetMarketplaceIdlReq, MarketplaceIdlResp, ConnectionListItem, GetConnectionsReq, DeleteLocalConnectionsConfigReq, GetMarketplaceItemReq, MarketplaceItem, GetConnectionItemReq, ConnectionDetailed, CreateLocalConnectionsConfigReq, CreateThirdPartyConnectionReq, CreateComponentConnectionReq, GetComponentsReq } from "@wso2/wso2-platform-core" +import { AddDevantTempConfigReq, AddDevantTempConfigResp, DeleteDevantTempConfigReq, GenerateCustomConnectorFromOASReq, GenerateCustomConnectorFromOASResp, InitializeDevantOASConnectionReq, InitializeDevantOASConnectionResp, PlatformExtState, RegisterDevantMarketplaceServiceReq, ReplaceDevantTempConfigValuesReq } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; + +export class PlatformExtRpcClient implements PlatformExtAPI { + private _messenger: Messenger; + + constructor(messenger: Messenger) { + this._messenger = messenger; + } + + getPlatformStore(): Promise { + return this._messenger.sendRequest(getPlatformStore, HOST_EXTENSION, undefined); + } + + getMarketplaceItems(params: GetMarketplaceListReq): Promise { + return this._messenger.sendRequest(getMarketplaceItems, HOST_EXTENSION, params); + } + + getMarketplaceItem(params: GetMarketplaceItemReq): Promise { + return this._messenger.sendRequest(getMarketplaceItem, HOST_EXTENSION, params); + } + + getMarketplaceIdl(params: GetMarketplaceIdlReq): Promise { + return this._messenger.sendRequest(getMarketplaceIdl, HOST_EXTENSION, params); + } + + generateCustomConnectorFromOAS(params: GenerateCustomConnectorFromOASReq): Promise { + return this._messenger.sendRequest(generateCustomConnectorFromOAS, HOST_EXTENSION, params); + } + + initializeDevantOASConnection(params: InitializeDevantOASConnectionReq): Promise { + return this._messenger.sendRequest(initializeDevantOASConnection, HOST_EXTENSION, params); + } + + registerDevantMarketplaceService(params: RegisterDevantMarketplaceServiceReq): Promise { + return this._messenger.sendRequest(registerDevantMarketplaceService, HOST_EXTENSION, params); + } + + createThirdPartyConnection(params: CreateThirdPartyConnectionReq): Promise { + return this._messenger.sendRequest(createThirdPartyConnection, HOST_EXTENSION, params); + } + + createInternalConnection(params: CreateComponentConnectionReq): Promise { + return this._messenger.sendRequest(createInternalConnection, HOST_EXTENSION, params); + } + + replaceDevantTempConfigValues(params: ReplaceDevantTempConfigValuesReq): Promise { + return this._messenger.sendRequest(replaceDevantTempConfigValues, HOST_EXTENSION, params); + } + + addDevantTempConfig(params: AddDevantTempConfigReq): Promise { + return this._messenger.sendRequest(addDevantTempConfig, HOST_EXTENSION, params); + } + + deleteDevantTempConfigs(params: DeleteDevantTempConfigReq): Promise { + return this._messenger.sendRequest(deleteDevantTempConfigs, HOST_EXTENSION, params); + } + + getConnections(params: GetConnectionsReq): Promise { + return this._messenger.sendRequest(getConnections, HOST_EXTENSION, params); + } + + getConnection(params: GetConnectionItemReq): Promise { + return this._messenger.sendRequest(getConnection, HOST_EXTENSION, params); + } + + getComponentList(params: GetComponentsReq): Promise { + return this._messenger.sendRequest(getComponentList, HOST_EXTENSION, params); + } + + deleteLocalConnectionsConfig(params: DeleteLocalConnectionsConfigReq): Promise { + return this._messenger.sendRequest(deleteLocalConnectionsConfig, HOST_EXTENSION, params); + } + + getDevantConsoleUrl(): Promise { + return this._messenger.sendRequest(getDevantConsoleUrl, HOST_EXTENSION, undefined); + } + + createConnectionConfig(params: CreateLocalConnectionsConfigReq): Promise { + return this._messenger.sendRequest(createConnectionConfig, HOST_EXTENSION, params); + } + + onPlatformExtStoreStateChange(callback: (state: PlatformExtState) => void) { + this._messenger.onNotification(onPlatformExtStoreStateChange, callback); + } + + refreshConnectionList(): Promise { + return this._messenger.sendRequest(refreshConnectionList, HOST_EXTENSION, undefined); + } + + setConnectedToDevant(connected: boolean): Promise { + return this._messenger.sendRequest(setConnectedToDevant, HOST_EXTENSION, connected); + } + + setSelectedComponent(componentId: string): Promise { + return this._messenger.sendRequest(setSelectedComponent, HOST_EXTENSION, componentId); + } + + setSelectedEnv(envId: string): Promise { + return this._messenger.sendRequest(setSelectedEnv, HOST_EXTENSION, envId); + } + + deployIntegrationInDevant(): Promise { + return this._messenger.sendRequest(deployIntegrationInDevant, HOST_EXTENSION); + } +} diff --git a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/visualizer/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/visualizer/rpc-client.ts index ae7fb14ca4..fcd156221d 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/visualizer/rpc-client.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/visualizer/rpc-client.ts @@ -49,7 +49,8 @@ import { undo, undoRedoState, updateCurrentArtifactLocation, - reviewAccepted + reviewAccepted, + GoBackRequest } from "@wso2/ballerina-core"; import { HOST_EXTENSION } from "vscode-messenger-common"; import { Messenger } from "vscode-messenger-webview"; @@ -73,8 +74,8 @@ export class VisualizerRpcClient implements VisualizerAPI { return this._messenger.sendNotification(addToHistory, HOST_EXTENSION, entry); } - goBack(): void { - return this._messenger.sendNotification(goBack, HOST_EXTENSION); + goBack(params?: GoBackRequest): void { + return this._messenger.sendNotification(goBack, HOST_EXTENSION, params); } goHome(): void { diff --git a/workspaces/ballerina/ballerina-side-panel/package.json b/workspaces/ballerina/ballerina-side-panel/package.json index 8c63a27064..d521079dc9 100644 --- a/workspaces/ballerina/ballerina-side-panel/package.json +++ b/workspaces/ballerina/ballerina-side-panel/package.json @@ -45,6 +45,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "7.56.4", + "@wso2/wso2-platform-core": "workspace:*", "react-markdown": "10.1.0", "rehype-raw": "7.0.0", "remark-gfm": "4.0.1", 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 423ee0dff2..788d7ea007 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Form/index.tsx @@ -16,7 +16,7 @@ * under the License. */ -import React, { forwardRef, useMemo, useEffect, useState, useRef } from "react"; +import React, { forwardRef, useCallback, useMemo, useEffect, useState, useRef } from "react"; import { useForm } from "react-hook-form"; import ReactMarkdown from "react-markdown"; import { @@ -32,7 +32,7 @@ import { import styled from "@emotion/styled"; import { ExpressionFormField, FieldDerivation, FormExpressionEditorProps, FormField, FormImports, FormValues } from "./types"; -import { EditorFactory } from "../editors/EditorFactory"; +import { FieldFactory } from "../editors/FieldFactory"; import { getValueForDropdown, isDropdownField } from "../editors/utils"; import { Diagnostic, @@ -458,7 +458,7 @@ export const Form = forwardRef((props: FormProps) => { setValue, setError, clearErrors, - formState: { isValidating, errors, dirtyFields }, + formState: { isValidating, isValid: formStateIsValid, errors, dirtyFields }, } = useForm(); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); @@ -501,7 +501,7 @@ export const Form = forwardRef((props: FormProps) => { } else if (isDropdownField(field)) { defaultValues[field.key] = getValueForDropdown(field) ?? ""; } else if (field.type === "FLAG" && field.types?.length > 1) { - if (field.value && typeof field.value === "boolean") { + if (typeof field.value === "boolean") { defaultValues[field.key] = String(field.value); } else { @@ -517,7 +517,7 @@ export const Form = forwardRef((props: FormProps) => { if (field.key === "variable") { defaultValues[field.key] = formValues[field.key] ?? defaultValues[field.key] ?? ""; } - if (field.key === "parameters" && field.value.length === 0) { + if (field.key === "parameters" && field.value?.length && field.value.length === 0) { defaultValues[field.key] = formValues[field.key] ?? []; } @@ -555,6 +555,18 @@ export const Form = forwardRef((props: FormProps) => { diagnosticsMap.push({ key: field.key, diagnostics: [] }); } + + // Handle the case where the name is updated dynamically (e.g., from a sibling field's onValueChange like headerName) + // Sync from field.value when it differs from form - but preserve user edits (when field was manually touched) + if (field.key === "name" && field.value !== undefined && field.value !== null) { + const existingName = formValues[field.key]; + const newName = typeof field.value === "string" ? (formatJSONLikeString(field.value) ?? field.value) : String(field.value); + // Only sync from field when: form is stale (external update) or user hasn't edited the name field + if (existingName !== newName && !dirtyFields?.[field.key]) { + setValue(field.key, newName); + defaultValues[field.key] = newName; + } + } }); setDiagnosticsInfo(diagnosticsMap); reset(defaultValues); @@ -611,10 +623,12 @@ export const Form = forwardRef((props: FormProps) => { setActiveFormField(key); }; - const handleSetDiagnosticsInfo = (diagnostics: FormDiagnostics) => { - const otherDiagnostics = diagnosticsInfo?.filter((item) => item.key !== diagnostics.key) || []; - setDiagnosticsInfo([...otherDiagnostics, diagnostics]); - }; + const handleSetDiagnosticsInfo = useCallback((diagnostics: FormDiagnostics) => { + setDiagnosticsInfo(prev => { + const otherDiagnostics = prev?.filter((item) => item.key !== diagnostics.key) || []; + return [...otherDiagnostics, diagnostics]; + }); + }, []); const handleOpenSubPanel = (subPanel: SubPanel) => { let updatedSubPanel = subPanel; @@ -732,7 +746,7 @@ export const Form = forwardRef((props: FormProps) => { const canOpenInDataMapper = (selectedNode === "VARIABLE" && expressionField && - visualizableField?.isDataMapped) || + visualizableField?.isDataMapped) || selectedNode === "DATA_MAPPER_CREATION"; const canOpenInFunctionEditor = selectedNode === "FUNCTION_CREATION"; @@ -779,7 +793,11 @@ export const Form = forwardRef((props: FormProps) => { let diagnostics: Diagnostic[] = diagnosticsInfoItem.diagnostics || []; if (diagnostics.length === 0) { - clearErrors(key); + // Only clear errors that were set by the expression diagnostics system, + // not errors set by other validators (e.g., PathEditor) + if (errors[key]?.type === "expression_diagnostic") { + clearErrors(key); + } continue; } else { // Filter the BCE2066 diagnostics @@ -788,7 +806,7 @@ export const Form = forwardRef((props: FormProps) => { ); const diagnosticsMessage = diagnostics.map((d) => d.message).join("\n"); - setError(key, { type: "validate", message: diagnosticsMessage }); + setError(key, { type: "expression_diagnostic", message: diagnosticsMessage }); // If the severity is not ERROR, don't invalidate const hasErrorDiagnostics = diagnostics.some((d) => d.severity === 1); @@ -814,11 +832,12 @@ export const Form = forwardRef((props: FormProps) => { // Call onValidityChange when form validity changes useEffect(() => { if (onValidityChange) { - const formIsValid = isValid && !isValidating && Object.keys(errors).length === 0 && + // formStateIsValid captures errors from PathEditor and other validators (setError) + const formIsValid = isValid && formStateIsValid && !isValidating && Object.keys(errors).length === 0 && (!concertMessage || !concertRequired || isUserConcert) && !isIdentifierEditing && !isSubComponentEnabled; onValidityChange(formIsValid); } - }, [isValid, isValidating, errors, concertMessage, concertRequired, isUserConcert, isIdentifierEditing, isSubComponentEnabled, onValidityChange]); + }, [isValid, formStateIsValid, isValidating, errors, concertMessage, concertRequired, isUserConcert, isIdentifierEditing, isSubComponentEnabled, onValidityChange]); const handleIdentifierEditingStateChange = (isEditing: boolean) => { setIsIdentifierEditing(isEditing); @@ -830,7 +849,7 @@ export const Form = forwardRef((props: FormProps) => { const disableSaveButton = isValidating || props.disableSaveButton || (concertMessage && concertRequired && !isUserConcert) || - isIdentifierEditing || isSubComponentEnabled || isValidatingForm || Object.keys(errors).length > 0; + isIdentifierEditing || isSubComponentEnabled || isValidatingForm || !formStateIsValid || Object.keys(errors).length > 0; const handleShowMoreClick = () => { setIsMarkdownExpanded(!isMarkdownExpanded); @@ -909,8 +928,8 @@ export const Form = forwardRef((props: FormProps) => { if (data.expression === '' && visualizableField?.defaultValue) { data.expression = visualizableField.defaultValue; } - return handleOnSave({ - ...data, + return handleOnSave({ + ...data, editorConfig: { view: selectedNode === "VARIABLE" ? MACHINE_VIEW.InlineDataMapper : MACHINE_VIEW.DataMapper, displayMode: EditorDisplayMode.VIEW, @@ -922,8 +941,8 @@ export const Form = forwardRef((props: FormProps) => { const handleOnOpenInFunctionEditor = () => { setSavingButton('functionEditor'); handleSubmit((data) => { - return handleOnSave({ - ...data, + return handleOnSave({ + ...data, editorConfig: { view: MACHINE_VIEW.BIDiagram, displayMode: EditorDisplayMode.VIEW, @@ -1026,7 +1045,7 @@ export const Form = forwardRef((props: FormProps) => { const updatedField = updateFormFieldWithImports(field, formImports); renderedComponents.push( - { }); } - return renderedComponents; - })()} - {hasAdvanceFields && ( - - {optionalFieldsTitle} - - {!showAdvancedOptions && ( - - - Expand - - )} - {showAdvancedOptions && ( - - Collapse - - )} - - - )} - {hasAdvanceFields && - showAdvancedOptions && - formFields.map((field) => { - if (field.advanced && !field.hidden) { - const updatedField = updateFormFieldWithImports(field, formImports); - return ( - - handleOpenRecordEditor(open, updatedField, newType)) - } - subPanelView={subPanelView} - handleOnFieldFocus={handleOnFieldFocus} - recordTypeFields={recordTypeFields} - onIdentifierEditingStateChange={handleIdentifierEditingStateChange} - handleOnTypeChange={handleOnTypeChange} - onBlur={handleOnBlur} - /> - - ); - } - return null; - })} - {hasAdvanceFields && - showAdvancedOptions && - advancedChoiceFields.map((field) => { + return renderedComponents; + })()} + {hasAdvanceFields && ( + + {optionalFieldsTitle} + + {!showAdvancedOptions && ( + + + Expand + + )} + {showAdvancedOptions && ( + + Collapse + + )} + + + )} + {hasAdvanceFields && + showAdvancedOptions && + formFields.map((field) => { + if (field.advanced && !field.hidden) { const updatedField = updateFormFieldWithImports(field, formImports); return ( - { /> ); - })} - + } + return null; + })} + {hasAdvanceFields && + showAdvancedOptions && + advancedChoiceFields.map((field) => { + const updatedField = updateFormFieldWithImports(field, formImports); + return ( + + handleOpenRecordEditor(open, updatedField, newType)) + } + subPanelView={subPanelView} + handleOnFieldFocus={handleOnFieldFocus} + recordTypeFields={recordTypeFields} + onIdentifierEditingStateChange={handleIdentifierEditingStateChange} + handleOnTypeChange={handleOnTypeChange} + onBlur={handleOnBlur} + /> + + ); + })} + {!preserveOrder && (variableField || typeField || targetTypeField) && ( {variableField && ( - { /> )} {typeField && !isInferredReturnType && ( - { )} {targetTypeField && !targetTypeField.advanced && ( <> - string; // sanitized expression that will be rendered in the editor } +export type ExpressionEditorDevantProps = { + devantConfigs?: string[]; + onAddDevantConfig?: (name: string, value: string, isSecret: boolean) => Promise; +} + export type FormExpressionEditorProps = FormCompletionConditionalProps & FormTypeConditionalProps & FormHelperPaneConditionalProps & FormExpressionEditorBaseProps & ExpressionEditorFormProps & - SanitizedExpressionEditorProps; + SanitizedExpressionEditorProps & + ExpressionEditorDevantProps; export type FormImports = { [fieldKey: string]: Imports; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/GroupList/index.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/GroupList/index.tsx index 2ea46d1967..0a637e1591 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/GroupList/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/GroupList/index.tsx @@ -17,11 +17,13 @@ */ import React, { useState } from "react"; -import { Codicon, ThemeColors } from "@wso2/ui-toolkit"; +import { Button, Codicon, ThemeColors } from "@wso2/ui-toolkit"; import styled from "@emotion/styled"; import { CallIcon, LogIcon } from "../../resources"; import { Category, Node } from "./../NodeList/types"; import { stripHtmlTags } from "../Form/utils"; +import { ConnectionListItem } from "@wso2/wso2-platform-core"; +import { DownloadIcon } from "../../resources/icons/nodes/DownloadIcon"; import { formatMethodName } from "../../utils/formatMethodName"; @@ -38,6 +40,10 @@ namespace S { background-color: ${ThemeColors.SURFACE_DIM_2}; `; + export const DevantInputCard = styled(Card)` + opacity: 0.8; + `; + export const Row = styled.div<{}>` display: flex; flex-direction: row; @@ -54,6 +60,10 @@ namespace S { padding: 0 5px; `; + export const DevantPullTitleRow = styled(TitleRow)<{}>` + cursor: unset; + `; + export const Title = styled.div<{}>` font-size: 13px; `; @@ -154,10 +164,11 @@ interface GroupListProps { expand?: boolean; onSelect: (node: Node, category: string) => void; enableSingleNodeDirectNav?: boolean; + onImportDevantConn?: (devantConn: ConnectionListItem) => void; } export function GroupList(props: GroupListProps) { - const { category, expand, onSelect, enableSingleNodeDirectNav } = props; + const { category, expand, onSelect, enableSingleNodeDirectNav, onImportDevantConn } = props; const [showList, setShowList] = useState(expand ?? false); const [expandedTitleIndex, setExpandedTitleIndex] = useState(null); @@ -184,6 +195,16 @@ export function GroupList(props: GroupListProps) { setExpandedTitleIndex(null); }; + if (category.devant && category.unusedDevantConn) { + return ( + + ); + } + if (nodes.length === 0) { return null; } @@ -193,6 +214,13 @@ export function GroupList(props: GroupListProps) { {category.icon || } {category.title} + {category.tooltip && ( + + )} {isSingleNode ? ( @@ -236,6 +264,32 @@ export function GroupList(props: GroupListProps) { ); } +const UnusedDevantCard = (props: { + title: string; + devantConn: ConnectionListItem; + onImportDevantConn?: (devantConn: ConnectionListItem) => void; +}) => { + const { title, devantConn, onImportDevantConn } = props; + return ( + + + {} + {title || devantConn?.name} + + + + + + + ); +}; + export default GroupList; function getComponentTitle(node: Node) { diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/ModeSwitcher/index.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/ModeSwitcher/index.tsx index c3d8672f3f..96069e8cca 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/ModeSwitcher/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/ModeSwitcher/index.tsx @@ -16,41 +16,95 @@ * under the License. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Label, Slider, SwitchWrapper } from './styles'; import { InputMode } from '../editors/MultiModeExpressionEditor/ChipExpressionEditor/types'; import { getDefaultExpressionMode, getSecondaryMode } from '../editors/MultiModeExpressionEditor/ChipExpressionEditor/utils'; import { InputType } from '@wso2/ballerina-core'; +import { getEditorConfiguration } from '../editors/ExpressionField'; +import { useFormContext } from '../../context'; +import WarningPopup from '../WarningPopup'; interface ModeSwitcherProps { value: InputMode; + //TODO: Should be removed once fields with type field is fixed to + // update the types property correctly when changing the type. isRecordTypeField: boolean; onChange: (value: InputMode) => void; types: InputType[]; + fieldKey: string; } -const ModeSwitcher: React.FC = ({ value, isRecordTypeField, onChange, types }) => { +const ModeSwitcher: React.FC = ({ value, isRecordTypeField, onChange, types, fieldKey }) => { + + const { form } = useFormContext(); + const { getValues, setValue } = form; + const [showWarning, setShowWarning] = useState(false); + const [pendingMode, setPendingMode] = useState(null); + const defaultMode = useMemo( + //TODO: Should only return the getDefaultExpressionMode(types) once fields with type field is fixed to + // update the types property correctly when changing the type. () => isRecordTypeField ? InputMode.RECORD : getDefaultExpressionMode(types), [types, isRecordTypeField] ); const secondaryMode = useMemo( + //TODO: Should only return the getSecondaryMode(types) once fields with type field is fixed to + // update the types property correctly when changing the type. () => isRecordTypeField ? InputMode.EXP : getSecondaryMode(types), [types, isRecordTypeField] ); + const handleModeSwitch = (mode: InputMode) => { - onChange(mode); - } + const currentFieldValue = getValues(fieldKey); + const configForNewMode = getEditorConfiguration(mode); + let isValueCompatible = true; + if (mode === InputMode.BOOLEAN) { + isValueCompatible = false; + } + else { + isValueCompatible = configForNewMode.getIsValueCompatible ? configForNewMode.getIsValueCompatible(currentFieldValue) : true; + } + + if (!isValueCompatible) { + setPendingMode(mode); + setShowWarning(true); + } else { + onChange(mode); + } + }; + + const handleConfirmSwitch = () => { + if (pendingMode) { + onChange(pendingMode); + setValue(fieldKey, undefined); + setPendingMode(null); + } + setShowWarning(false); + }; + + const handleCancelSwitch = () => { + setPendingMode(null); + setShowWarning(false); + }; + const isChecked = value === secondaryMode; return ( - - - - - - + <> + + + + + + + + ); }; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/categoryConfig.ts b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/categoryConfig.ts index e82e146b2c..0767c13b55 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/categoryConfig.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/categoryConfig.ts @@ -20,9 +20,11 @@ export type CategoryActionType = 'connection' | 'function' | 'add'; export interface CategoryAction { type: CategoryActionType; + codeIcon?: string; + hideOnEmptyState?: boolean; tooltip: string; emptyStateLabel: string; - handlerKey: 'onAddConnection' | 'onAddFunction' | 'onAdd'; + handlerKey: 'onAddConnection' | 'onAddFunction' | 'onAdd' | 'onLinkDevantProject' | 'onRefreshDevantConnections'; condition?: (title: string) => boolean; // For special conditions like data mapper } @@ -38,12 +40,30 @@ export interface CategoryConfig { export const CATEGORY_CONFIGS: Record = { "Connections": { title: "Connections", - actions: [{ - type: 'connection', - tooltip: "Add Connection", - emptyStateLabel: "Add Connection", - handlerKey: 'onAddConnection' - }], + actions: [ + { + type: "connection", + codeIcon: "vm-connect", + tooltip: "Use Devant Connections", + emptyStateLabel: "", + hideOnEmptyState: true, + handlerKey: "onLinkDevantProject", + }, + { + type: "connection", + codeIcon: "refresh", + tooltip: "Refresh Devant Connections", + emptyStateLabel: "", + hideOnEmptyState: true, + handlerKey: "onRefreshDevantConnections", + }, + { + type: "connection", + tooltip: "Add Connection", + emptyStateLabel: "Add Connection", + handlerKey: "onAddConnection", + }, + ], showWhenEmpty: true, useConnectionContainer: true, fixed: true @@ -168,7 +188,11 @@ export const getCategoryConfig = (title: string): CategoryConfig | undefined => return CATEGORY_CONFIGS[title]; }; -export const shouldShowEmptyCategory = (title: string): boolean => { +export const shouldShowEmptyCategory = (title: string, isSubCategory: boolean): boolean => { + if (isSubCategory) { + // For subcategories, only show if it's "Current Integration" + return title === "Current Integration"; + } const config = getCategoryConfig(title); return config?.showWhenEmpty ?? false; }; 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 172ad06bea..dfeac94243 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx @@ -17,6 +17,7 @@ */ import React, { useEffect, useState } from "react"; +import ReactMarkdown from "react-markdown"; import { Button, Codicon, @@ -36,7 +37,9 @@ import { GroupListSkeleton } from "../Skeletons"; import GroupList from "../GroupList"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { getExpandedCategories, setExpandedCategories, getDefaultExpandedState } from "../../utils/localStorage"; +import { ConnectionListItem } from "@wso2/wso2-platform-core"; import { shouldShowEmptyCategory, shouldUseConnectionContainer, getCategoryActions, isCategoryFixed } from "./categoryConfig"; +import { stripHtmlTags } from "../Form/utils"; namespace S { export const Container = styled.div<{}>` @@ -116,6 +119,38 @@ namespace S { opacity: 0.5; `; + export const TooltipMarkdown = styled.div` + font-size: 12px; + line-height: 1.4; + font-family: var(--vscode-font-family); + + p { + margin: 0 0 6px 0; + } + + p:last-of-type { + margin-bottom: 0; + } + + pre { + display: none; + } + + code { + display: inline; + } + + ul, + ol { + margin: 6px 0; + padding-left: 18px; + } + + li { + margin: 2px 0; + } + `; + export const Component = styled.div<{ enabled?: boolean }>` display: flex; flex-direction: row; @@ -319,6 +354,9 @@ interface NodeListProps { onBack?: () => void; onClose?: () => void; searchPlaceholder?: string; + onImportDevantConn?: (devantConn: ConnectionListItem) => void; + onLinkDevantProject?: () => void; + onRefreshDevantConnections?: () => void; } export function NodeList(props: NodeListProps) { @@ -335,6 +373,9 @@ export function NodeList(props: NodeListProps) { onBack, onClose, searchPlaceholder, + onImportDevantConn, + onLinkDevantProject, + onRefreshDevantConnections, } = props; const [searchText, setSearchText] = useState(""); @@ -434,6 +475,31 @@ export function NodeList(props: NodeListProps) { } }; + const handleOnLinkDevantProject = () => { + if (onLinkDevantProject){ + onLinkDevantProject(); + } + } + + const handleOnRefreshDevantConnections = () => { + if (onRefreshDevantConnections){ + onRefreshDevantConnections(); + } + } + + const renderTooltipContent = (description?: string): React.ReactNode | undefined => { + const cleaned = stripHtmlTags(description || "").trim(); + if (!cleaned) { + return undefined; + } + + return ( + + {cleaned} + + ); + }; + const getNodesContainer = (items: (Node | Category)[], parentCategoryTitle?: string) => { const safeItems = items.filter((item) => item != null); const nodes = safeItems.filter((item): item is Node => "id" in item && !("title" in item)); @@ -450,7 +516,7 @@ export function NodeList(props: NodeListProps) { return ( 0} onSelect={handleAddNode} + onImportDevantConn={onImportDevantConn} enableSingleNodeDirectNav={enableSingleNodeDirectNav} /> )) @@ -571,14 +638,19 @@ export function NodeList(props: NodeListProps) { const content = ( <> {reorderedGroups.map((group, index) => { - const categoryActions = getCategoryActions(group.title, title); + // If subcategory is inside "Current Workspace", show "Current Integration" actions instead of + // the subcategory title when the subcategory referes to the current integration + const categoryActions = parentCategoryTitle === "Current Workspace" ? + ( group.title?.includes("(current)") ? getCategoryActions("Current Integration") : getCategoryActions(group.title)) + : + getCategoryActions(group.title); const config = categoryConfig[group.title] || { hasBackground: true }; const shouldShowSeparator = config.showSeparatorBefore; // Hide categories that don't have items, except for special categories that can add items if (!group || !group.items || group.items.length === 0) { // Only show empty categories if they have add functionality - if (!shouldShowEmptyCategory(group.title)) { + if (!shouldShowEmptyCategory(group.title, isSubCategory) && categoryActions.length === 0) { return null; } } @@ -614,7 +686,9 @@ export function NodeList(props: NodeListProps) { const handlers = { onAddConnection: handleAddConnection, onAddFunction: handleAddFunction, - onAdd: handleAdd + onAdd: handleAdd, + onLinkDevantProject: handleOnLinkDevantProject, + onRefreshDevantConnections: handleOnRefreshDevantConnections }; const handler = handlers[action.handlerKey]; @@ -634,7 +708,7 @@ export function NodeList(props: NodeListProps) { handler(); }} > - + ); @@ -658,32 +732,35 @@ export function NodeList(props: NodeListProps) { )} - {(isCategoryExpanded || isCategoryFixed(group.title)) && ( + {(isSubCategory || isCategoryExpanded || isCategoryFixed(group.title)) && ( <> - {(!group.items || group.items.length === 0) && + {(isSubCategory || (!group.items || group.items.length === 0)) && !searchText && !isSearching && categoryActions.map((action, actionIndex) => { const handlers = { onAddConnection: handleAddConnection, onAddFunction: handleAddFunction, - onAdd: handleAdd + onAdd: handleAdd, + onLinkDevantProject: handleOnLinkDevantProject, + onRefreshDevantConnections: handleOnRefreshDevantConnections }; const handler = handlers[action.handlerKey]; const propsHandler = props[action.handlerKey]; // Only render if the handler exists in props - if (!propsHandler || !handler) return null; + if (!propsHandler || !handler || action.hideOnEmptyState) return null; const buttonLabel = action.emptyStateLabel || addButtonLabel || "Add"; return ( - + {buttonLabel} ); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/types.ts b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/types.ts index 2decc07828..a921abc773 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/types.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/types.ts @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ +import { ConnectionListItem } from "@wso2/wso2-platform-core"; export type Item = Category | Node; @@ -24,6 +25,13 @@ export type Category = { icon?: JSX.Element; items: Item[]; isLoading?: boolean; + tooltip?: { + icon?: string; + color?: string; + text?: string; + } + devant?: ConnectionListItem; + unusedDevantConn?: boolean; }; export type Node = { 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 2af440e265..988f8d6393 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamManager.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/ParamManager/ParamManager.tsx @@ -27,7 +27,7 @@ import { Controller } from 'react-hook-form'; import { useFormContext } from '../../context'; import { Imports, NodeKind } from '@wso2/ballerina-core'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; -import { EditorFactory } from '../editors/EditorFactory'; +import { FieldFactory } from '../editors/FieldFactory'; import { buildRequiredRule, getFieldKeyForAdvanceProp } from '../editors/utils'; export interface Parameter { @@ -199,7 +199,7 @@ export function ParamManagerEditor(props: ParamManagerEditorProps) { if (getValues(advanceProp.key) === undefined) { setValue(advanceProp.key, advanceProp.value); } - return + return })} )} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/WarningPopup/styles.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/WarningPopup/styles.tsx index 5f190a8926..71f67862fe 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/WarningPopup/styles.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/WarningPopup/styles.tsx @@ -51,7 +51,7 @@ export const ModalBackdrop = styled.div({ display: 'flex', justifyContent: 'center', alignItems: 'center', - zIndex: 1000 + zIndex: 2000 }); export const ModalContent = styled.div<{ maxWidth: string }>(({ maxWidth }) => ({ @@ -62,5 +62,6 @@ export const ModalContent = styled.div<{ maxWidth: string }>(({ maxWidth }) => ( borderRadius: '4px', boxShadow: '0 4px 24px rgba(0, 0, 0, 0.4)', width: maxWidth, - textAlign: 'center' + textAlign: 'center', + zIndex: 2001 })); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ArrayEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ArrayEditor.tsx index 9e7dd6a7d0..0160f0ed40 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ArrayEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ArrayEditor.tsx @@ -87,7 +87,7 @@ interface ArrayEditorProps { recordTypeField?: RecordTypeField; } -export function ArrayEditor(props: ArrayEditorProps) { +function ArrayEditor(props: ArrayEditorProps) { const { field, label, ...rest } = props; const { form } = useFormContext(); const { unregister, setValue, watch } = form; @@ -159,14 +159,14 @@ export function ArrayEditor(props: ArrayEditorProps) { {field.documentation} {[...Array(editorCount)].map((_, index) => ( - + /> */} onDelete(index)} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/AutoCompleteEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/AutoCompleteEditor.tsx index 0b09d7d07c..a45b06f6b7 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/AutoCompleteEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/AutoCompleteEditor.tsx @@ -52,8 +52,13 @@ export function AutoCompleteEditor(props: AutoCompleteEditorProps) { required={!field.optional} disabled={!field.editable} onValueChange={(val: string) => { - setValue(field.key, val); - field.onValueChange?.(val); + // Preserve existing value when Combobox fires with empty on blur (e.g., click away without selecting) + const currentValue = value ?? getValueForDropdown(field) ?? field.value; + const newVal = (val === "" || val === undefined || val === null) && currentValue + ? currentValue + : val; + setValue(field.key, newVal); + field.onValueChange?.(newVal); }} sx={{ marginRight: "-4px", diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/CheckBoxConditionalEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/CheckBoxConditionalEditor.tsx index 12c068c0a3..fcc829d6cb 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/CheckBoxConditionalEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/CheckBoxConditionalEditor.tsx @@ -20,7 +20,7 @@ import React, { useEffect, useState } from "react"; import { FormField } from "../Form/types"; import { CheckBoxGroup, FormCheckBox } from "@wso2/ui-toolkit"; import styled from "@emotion/styled"; -import { EditorFactory } from "./EditorFactory"; +import { FieldFactory } from "./FieldFactory"; import { useFormContext } from "../../context"; import { getPrimaryInputType, PropertyModel } from "@wso2/ballerina-core"; @@ -128,7 +128,7 @@ export function CheckBoxConditionalEditor(props: CheckBoxConditionalEditorProps) {checked && checkedStateFields.length > 0 && ( <> {checkedStateFields.map((dfield, index) => ( - 0 && ( <> {uncheckedStateFields.map((dfield, index) => ( - {dynamicFields.filter(dfield => (field.advanced || !dfield.advanced)).map((dfield, index) => { return ( - ): Member[] { + // Check if this member references a named type + if (member.refs?.length) { + const refName = member.refs[0]; + if (visited.has(refName)) return []; // circular guard + const refType = referencedTypes.find(t => t.name === refName); + if (refType?.members) { + return refType.members.filter(m => m.kind === "FIELD"); + } + } + + // Check inline type object + if (typeof member.type === "string") return []; + const typeObj = member.type as Type; + if (!typeObj.members) return []; + + const children: Member[] = []; + for (const m of typeObj.members) { + if (m.kind === "FIELD") { + children.push(m); + } else if (m.kind === "TYPE" && m.refs?.length) { + const refName = m.refs[0]; + if (visited.has(refName)) continue; // circular guard + const refType = referencedTypes.find(t => t.name === refName); + if (refType?.members) { + children.push(...refType.members.filter(rm => rm.kind === "FIELD")); + } + } + } + return children; +} + +/** + * Toggles selection on a member and cascades to all its children. + */ +function toggleSelection(member: Member, selected: boolean, referencedTypes: Type[], visited: Set = new Set()): void { + member.selected = selected; + + // Track this member's ref to prevent cycles + const newVisited = new Set(visited); + if (member.refs?.length) { + newVisited.add(member.refs[0]); + } + + const children = resolveChildren(member, referencedTypes, newVisited); + for (const child of children) { + toggleSelection(child, selected, referencedTypes, newVisited); + } +} + +/** + * Checks if all FIELD members in the tree are selected. + */ +function areAllSelected(members: Member[], referencedTypes: Type[], visited: Set = new Set()): boolean { + for (const m of members) { + if (m.kind !== "FIELD") continue; + if (!m.selected) return false; + + const newVisited = new Set(visited); + if (m.refs?.length) newVisited.add(m.refs[0]); + + const children = resolveChildren(m, referencedTypes, newVisited); + if (children.length && !areAllSelected(children, referencedTypes, newVisited)) { + return false; + } + } + return true; +} + +/** + * Propagates selection state upwards by marking parent as selected if any child is selected. + */ +function propagateSelectionUpwards(members: Member[], referencedTypes: Type[], visited: Set = new Set()): void { + for (const member of members) { + const newVisited = new Set(visited); + if (member.refs?.length) newVisited.add(member.refs[0]); + + if (typeof member.type !== "string") { + const typeObj = member.type as Type; + if (typeObj.members?.length) { + for (const typeMember of typeObj.members) { + if (typeMember.kind !== "TYPE" || !typeMember.refs?.length) continue; + const refName = typeMember.refs[0]; + if (newVisited.has(refName)) continue; + const refType = referencedTypes.find(t => t.name === refName); + if (!refType?.members) continue; + + const anyRefFieldSelected = refType.members + .filter(m => m.kind === "FIELD") + .some(m => m.selected); + + if (typeMember.optional !== false) { + typeMember.selected = anyRefFieldSelected; + } + } + } + } + + const children = resolveChildren(member, referencedTypes, newVisited); + if (children.length > 0) { + // First, recursively propagate for children + propagateSelectionUpwards(children, referencedTypes, newVisited); + + // Then check if any child is selected + const anyChildSelected = children.some(child => child.selected); + + // Mirror child state upward: select parent when any child is selected, deselect parent when all children are deselected. + if (member.optional !== false) { + member.selected = anyChildSelected; + } + } + } +} + +/** + * Checks if a field has partial selection (some but not all children selected). + * Returns true when a parent has some (but not all) descendants selected. + */ +function hasPartialSelection(member: Member, referencedTypes: Type[], visited: Set = new Set()): boolean { + const newVisited = new Set(visited); + if (member.refs?.length) newVisited.add(member.refs[0]); + + const children = resolveChildren(member, referencedTypes, visited); + if (!children.length) return false; + + let anySelected = false; + let allSelected = true; + + for (const child of children) { + const childIsSelected = child.selected; + const childHasPartial = hasPartialSelection(child, referencedTypes, newVisited); + + // If any child has partial selection, parent is partial + if (childHasPartial) return true; + + if (childIsSelected) { + anySelected = true; + } else { + allSelected = false; + } + } + + // Partial if some (but not all) children are selected + return anySelected && !allSelected; +} + +// ─── Styled Components ────────────────────────────────────────── + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 16px; +`; + +const LabelContainer = styled.div` + display: flex; + align-items: center; + margin-bottom: 4px; +`; + +const Label = styled.label` + color: var(--vscode-editor-foreground); + text-transform: capitalize; + font-size: 13px; + font-family: var(--vscode-font-family); +`; + +const Description = styled.div` + font-size: 13px; + font-family: var(--vscode-font-family); + color: var(--vscode-list-deemphasizedForeground); + margin-bottom: 8px; +`; + +const Wrapper = styled.div` + border: 1px solid var(--vscode-dropdown-border); + border-radius: 4px; + background: var(--vscode-input-background); +`; + +const SearchArea = styled.div` + padding: 8px; +`; + +const Dropdown = styled.div` + border-top: 1px solid var(--vscode-dropdown-border); +`; + +const SelectAllRow = styled.div` + display: flex; + align-items: center; + padding: 8px 12px; + background: var(--vscode-editor-background); + border-bottom: 1px solid var(--vscode-dropdown-border); +`; + +const SelectAllText = styled.span` + font-size: 13px; + font-weight: 600; + color: var(--vscode-foreground); + margin-left: 8px; +`; + +const TreeContainer = styled.div` + max-height: 400px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 10px; + } + &::-webkit-scrollbar-track { + background: var(--vscode-scrollbarSlider-background); + } + &::-webkit-scrollbar-thumb { + background: var(--vscode-scrollbarSlider-hoverBackground); + border-radius: 5px; + } +`; + +const TreeItem = styled.div<{ depth: number }>` + display: flex; + align-items: center; + padding: 8px 12px; + padding-left: ${p => 12 + p.depth * 20}px; + border-bottom: 1px solid var(--vscode-dropdown-border); + + &:hover { + background: var(--vscode-list-hoverBackground); + } + &:last-child { + border-bottom: none; + } +`; + +const ExpandButton = styled.span<{ expanded: boolean }>` + display: inline-flex; + width: 16px; + height: 16px; + margin-right: 4px; + cursor: pointer; + transform: ${p => p.expanded ? "rotate(90deg)" : "rotate(0deg)"}; + transition: transform 0.2s; +`; + +const Spacer = styled.span` + width: 16px; + height: 16px; + margin-right: 4px; +`; + +const FieldLabel = styled.span` + flex: 1; + font-size: 13px; + color: var(--vscode-foreground); + margin-left: 8px; + cursor: pointer; + user-select: none; +`; + +const TypeTag = styled.span` + font-size: 11px; + padding: 2px 8px; + border-radius: 3px; + margin-left: 8px; + font-weight: 500; + background: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); +`; + + +// ─── Component ────────────────────────────────────────────────── + +export function DependentTypeEditor(props: DependentTypeEditorProps) { + const { field } = props; + const { form } = useFormContext(); + const { setValue, register } = form; + + const [search, setSearch] = useState(""); + const [expanded, setExpanded] = useState>(new Set()); + + // Extract the record selector type from field.types + const recordSelectorEntry = field.types?.find( + (t: any) => t.fieldType === "RECORD_FIELD_SELECTOR" + ) as any; + + const initialData = recordSelectorEntry?.recordSelectorType as RecordSelectorType | undefined; + + // Clone into a mutable ref (immutable props pattern) + const dataRef = useRef(undefined); + if (!dataRef.current && initialData) { + dataRef.current = JSON.parse(JSON.stringify(initialData)); + } + + const data = dataRef.current; + const rootType = data?.rootType; + const referencedTypes = data?.referencedTypes ?? []; + + // Register field and sync initial value + useEffect(() => { + register(field.key); + syncToForm(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [field.key, register]); + + // Sync current selection state back to form + const syncToForm = useCallback(() => { + if (!data) return; + const updatedTypes = (field.types ?? []).map((t: any) => { + if (t.fieldType === "RECORD_FIELD_SELECTOR") { + return { ...t, recordSelectorType: JSON.parse(JSON.stringify(data)) }; + } + return t; + }); + // field.types = updatedTypes; + setValue(field.key, updatedTypes); + }, [data, field, setValue]); + + // Force re-render by incrementing a dummy state + const [, forceUpdate] = useState(0); + const triggerUpdate = () => { + forceUpdate(v => v + 1); + syncToForm(); + }; + + // Handlers + const handleToggleField = (member: Member) => { + // Skip if field is required (cannot be unchecked) + if (member.optional === false) return; + + // If partial selection, clicking should select all (complete the selection) + // If selected, clicking should deselect all + // If unselected (and no partial), clicking should select all + const isPartial = hasPartialSelection(member, referencedTypes); + const shouldSelect = !member.selected || isPartial; + toggleSelection(member, shouldSelect, referencedTypes); + + // Propagate selection upwards - mark parents as selected if any child is selected + if (rootType?.members) { + propagateSelectionUpwards(rootType.members, referencedTypes); + } + + triggerUpdate(); + }; + + const handleSelectAll = () => { + if (!rootType?.members) return; + const allChecked = areAllSelected(rootType.members, referencedTypes); + for (const m of rootType.members) { + if (m.kind === "FIELD") { + toggleSelection(m, !allChecked, referencedTypes); + } + } + propagateSelectionUpwards(rootType.members, referencedTypes); + triggerUpdate(); + }; + + const toggleExpanded = (path: string, e: React.MouseEvent) => { + e.stopPropagation(); + setExpanded(prev => { + const next = new Set(prev); + next.has(path) ? next.delete(path) : next.add(path); + return next; + }); + }; + + // Recursive tree renderer + const renderTree = ( + members: Member[], + parentPath: string, + depth: number, + visited: Set = new Set() + ): React.ReactNode => { + return members + .filter(m => m.kind === "FIELD" && m.name) + .filter(m => !search || m.name!.toLowerCase().includes(search.toLowerCase())) + .map(m => { + const path = parentPath ? `${parentPath}.${m.name}` : m.name!; + + const newVisited = new Set(visited); + if (m.refs?.length) newVisited.add(m.refs[0]); + + const children = resolveChildren(m, referencedTypes, visited); + const isExpanded = expanded.has(path); + const isPartial = hasPartialSelection(m, referencedTypes, visited); + const isRequired = m.optional === false; + + return ( + + + {children.length ? ( + toggleExpanded(path, e)}> + + + ) : ( + + )} + + handleToggleField(m)} + /> + + handleToggleField(m)}>{m.name} + {m?.typeName} + + + {isExpanded && children.length > 0 && renderTree(children, path, depth + 1, newVisited)} + + ); + }); + }; + + if (!rootType) { + return ( + + + + {!field.optional && } + +
+ No type model available +
+
+ ); + } + + return ( + + + + {!field.optional && } + + {field.documentation && {field.documentation}} + + + + + + + + + + Select All Fields + + + + {renderTree(rootType.members, "", 0)} + + + + + ); +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/DropdownChoiceForm.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/DropdownChoiceForm.tsx index f5e950c25f..16f1b814e2 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/DropdownChoiceForm.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/DropdownChoiceForm.tsx @@ -24,7 +24,7 @@ import { FormField } from "../Form/types"; import { buildRequiredRule, capitalize, getValueForDropdown } from "./utils"; import { useFormContext } from "../../context"; import styled from "@emotion/styled"; -import { EditorFactory } from "./EditorFactory"; +import { FieldFactory } from "./FieldFactory"; interface DropdownChoiceFormProps { field: FormField; @@ -95,7 +95,7 @@ export function DropdownChoiceForm(props: DropdownChoiceFormProps) { {dynamicFields.map((dfield, index) => { if (!dfield.advanced && !dfield.optional) { return ( - void; openSubPanel?: (subPanel: SubPanel) => void; @@ -71,6 +75,7 @@ interface FormFieldEditorProps { export const EditorFactory = (props: FormFieldEditorProps) => { const { field, + fieldInputType, selectedNode, openRecordEditor, openSubPanel, @@ -84,58 +89,56 @@ export const EditorFactory = (props: FormFieldEditorProps) => { setSubComponentEnabled, handleNewTypeSelected, isContextTypeEditorSupported, - openFormTypeEditor, - scopeFieldAddon + openFormTypeEditor } = props; - const showWithExpressionEditor = field.types?.some(type => { - return type && ( - type.fieldType === "EXPRESSION" || - type.fieldType === "LV_EXPRESSION" || - type.fieldType === "ACTION_OR_EXPRESSION" || - type.fieldType === "TEXT" || - type.fieldType === "EXPRESSION_SET" || - type.fieldType === "TEXT_SET" || - type.fieldType === "MAPPING_EXPRESSION_SET" || - type.fieldType === "MAPPING_EXPRESSION" || - (type.fieldType === "SINGLE_SELECT" && isDropDownType(type)) || - type.fieldType === "RECORD_MAP_EXPRESSION" || - (field.type === "FLAG" && field.types?.length > 1) || - type.fieldType === "CLAUSE_EXPRESSION" - ); - }); + const showWithExpressionEditor = ( + fieldInputType.fieldType === "EXPRESSION" || + fieldInputType.fieldType === "LV_EXPRESSION" || + fieldInputType.fieldType === "ACTION_OR_EXPRESSION" || + fieldInputType.fieldType === "TEXT" || + fieldInputType.fieldType === "EXPRESSION_SET" || + fieldInputType.fieldType === "TEXT_SET" || + (fieldInputType.fieldType === "SINGLE_SELECT" && isDropDownType(fieldInputType)) || + fieldInputType.fieldType === "RECORD_MAP_EXPRESSION" || + fieldInputType.fieldType === "SQL_QUERY" || + fieldInputType.fieldType === "NUMBER" || + (fieldInputType.fieldType === "FLAG" && field.types?.length > 1) + ) if (!field.enabled || field.hidden) { return <>; - } else if (field.type === "SLIDER") { + } else if (fieldInputType.fieldType === "RECORD_FIELD_SELECTOR" && field.codedata?.kind === "PARAM_FOR_TYPE_INFER") { + return ; + } else if (fieldInputType.fieldType === "SLIDER") { return ; - } else if (field.type === "MULTIPLE_SELECT") { + } else if (fieldInputType.fieldType === "MULTIPLE_SELECT") { return ; - } else if (field.type === "HEADER_SET") { + } else if (fieldInputType.fieldType === "HEADER_SET") { return ; - } else if (field.type === "CHOICE") { + } else if (fieldInputType.fieldType === "CHOICE") { return ; - } else if (field.type === "DROPDOWN_CHOICE") { + } else if (fieldInputType.fieldType === "DROPDOWN_CHOICE") { return ; - } else if (field.type === "TEXTAREA" || field.type === "STRING" || field.type === "DOC_TEXT") { + } else if (fieldInputType.fieldType === "TEXTAREA" || fieldInputType.fieldType === "STRING" || fieldInputType.fieldType === "DOC_TEXT") { return ; - } else if (field.type === "FLAG" && !showWithExpressionEditor) { + } else if (fieldInputType.fieldType === "FLAG" && !showWithExpressionEditor) { return ; - } else if (field.type === "EXPRESSION" && field.key === "resourcePath") { + } else if (fieldInputType.fieldType === "EXPRESSION" && field.key === "resourcePath") { // HACK: this should fixed with the LS API. this is used to avoid the expression editor for resource path field. return ; - } else if (field.type?.toUpperCase() === "ENUM") { + } else if (fieldInputType.fieldType?.toUpperCase() === "ENUM") { // Enum is a dropdown field return ; - } else if (field.type?.toUpperCase() === "AUTOCOMPLETE") { + } else if (fieldInputType.fieldType?.toUpperCase() === "AUTOCOMPLETE") { return ; - } else if (field.type === "CUSTOM_DROPDOWN") { + } else if (fieldInputType.fieldType === "CUSTOM_DROPDOWN") { return ; - } else if (field.type === "FILE_SELECT" && field.editable) { + } else if (fieldInputType.fieldType === "FILE_SELECT" && field.editable) { return ; - } else if (field.type === "SINGLE_SELECT" && !showWithExpressionEditor && field.editable) { + } else if (fieldInputType.fieldType === "SINGLE_SELECT" && !showWithExpressionEditor && field.editable) { return ; - } else if (!field.items && (field.type === "ACTION_TYPE") && field.editable) { + } else if (!field.items && (fieldInputType.fieldType === "ACTION_TYPE") && field.editable) { return ( { handleNewTypeSelected={handleNewTypeSelected} /> ); - } else if (!field.items && (field.key === "type" || field.type === "TYPE") && field.editable) { + } else if (!field.items && (field.key === "type" || fieldInputType.fieldType === "TYPE") && field.editable) { return ( { /> ); - } else if (!field.items && (field.type === "RAW_TEMPLATE" || getPrimaryInputType(field.types)?.ballerinaType === "ai:Prompt") && field.editable) { + } else if (!field.items && (fieldInputType.fieldType === "RAW_TEMPLATE" || fieldInputType.ballerinaType === "ai:Prompt") && field.editable) { return ( { /> ); - } else if (!field.items && field.type === "ACTION_EXPRESSION") { + } else if (!field.items && fieldInputType.fieldType === "ACTION_EXPRESSION") { return ( { return ( { recordTypeField={recordTypeFields?.find(recordField => recordField.key === field.key)} /> ); - } else if (field.type === "VIEW") { + } else if (fieldInputType.fieldType === "VIEW") { // Skip this property return <>; - } else if(field.type === "REPEATABLE_PROPERTY" && (selectedNode === "DATA_MAPPER_CREATION" || selectedNode === "FUNCTION_CREATION")) { + } else if(fieldInputType.fieldType === "REPEATABLE_PROPERTY" && (selectedNode === "DATA_MAPPER_CREATION" || selectedNode === "FUNCTION_CREATION")) { return ; }else if ( - (field.type === "PARAM_MANAGER") || - (field.type === "REPEATABLE_PROPERTY" && isTemplateType(getPrimaryInputType(field.types))) + (fieldInputType.fieldType === "PARAM_MANAGER") || + (fieldInputType.fieldType === "REPEATABLE_PROPERTY" && isTemplateType(fieldInputType)) ) { return ; - } else if (field.type === "REPEATABLE_PROPERTY") { + } else if (fieldInputType.fieldType === "REPEATABLE_PROPERTY") { return ; - } else if (field.type === "IDENTIFIER" && !field.editable && field?.lineRange) { + } + + else if (fieldInputType.fieldType === "REPEATABLE_LIST") { + return ; + } else if (fieldInputType.fieldType === "REPEATABLE_MAP") { + return ; + } else if (fieldInputType.fieldType === "IDENTIFIER" && !field.editable && field?.lineRange) { return ; - } else if (field.type !== "IDENTIFIER" && !field.editable) { + } else if (fieldInputType.fieldType !== "IDENTIFIER" && !field.editable) { return ; - } else if (field.type === "IDENTIFIER" && field.editable) { + } else if (fieldInputType.fieldType === "IDENTIFIER" && field.editable) { return ; - } else if (field.type === "SERVICE_PATH" || field.type === "ACTION_PATH") { + } else if (fieldInputType.fieldType === "SERVICE_PATH" || fieldInputType.fieldType === "ACTION_PATH") { return ; - } else if (field.type === "CONDITIONAL_FIELDS" && field.editable) { + } else if (fieldInputType.fieldType === "CONDITIONAL_FIELDS" && field.editable) { // Conditional fields is a group of fields which are conditionally shown based on a checkbox field return ( ); - } else if (field.type === "DM_JOIN_CLAUSE_RHS_EXPRESSION") { + } else if (fieldInputType.fieldType === "DM_JOIN_CLAUSE_RHS_EXPRESSION") { // Expression field for Data Mapper join on condition RHS const clauseExpressionField: FormField = { ...field, @@ -241,6 +253,7 @@ export const EditorFactory = (props: FormFieldEditorProps) => { return ( { autoFocus, control, field, + fieldInputType, id, placeholder, required, @@ -391,23 +397,15 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { const key = fieldKey ?? field.key; const [focused, setFocused] = useState(false); - const [inputMode, setInputMode] = useState(recordTypeField ? InputMode.RECORD : InputMode.EXP); - const inputModeRef = useRef(inputMode); const [isExpressionEditorHovered, setIsExpressionEditorHovered] = useState(false); - const [showModeSwitchWarning, setShowModeSwitchWarning] = useState(false); const [formDiagnostics, setFormDiagnostics] = useState(field.diagnostics); const [isExpandedModalOpen, setIsExpandedModalOpen] = useState(false); - const targetInputModeRef = useRef(null); // Update formDiagnostics when field.diagnostics changes useEffect(() => { setFormDiagnostics(field.diagnostics); }, [field.diagnostics]); - // Keep inputModeRef in sync with inputMode state - useEffect(() => { - inputModeRef.current = inputMode; - }, [inputMode]); // If Form directly calls ExpressionEditor without setting targetLineRange and fileName through context @@ -425,14 +423,30 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { const exprRef = useRef(null); const anchorRef = useRef(null); - const { nodeInfo } = useFormContext(); + // This guard is here because the IF form and Match forms + // does not populate the context value since they use expressionEditor + // component directly instead of going through the FieldFactory. + // This should ideally be handled as followes. + // 1.) Refactor IF and Match forms to use FieldFactory component + // and LS property models + // 2.) Remove this guard and make sure all the usages of ExpressionEditor + // are wrapped with ModeSwitcherContext provider + const modeSwitcherContext = useModeSwitcherContext() ?? { + inputMode: InputMode.EXP, + isModeSwitcherEnabled: false, + isRecordTypeField: false, + onModeChange: () => { }, + types: undefined + }; + + const { inputMode } = modeSwitcherContext; // Use to fetch initial diagnostics const previousDiagnosticsFetchContext = useRef({ fetchedInitialDiagnostics: false, diagnosticsFetchedTargetLineRange: undefined }); - const fieldValue = (inputModeRef.current === InputMode.PROMPT || inputModeRef.current === InputMode.TEMPLATE) && rawExpression ? rawExpression(watch(key)) : watch(key); + const fieldValue = (inputMode === InputMode.PROMPT || inputMode === InputMode.TEMPLATE) && rawExpression ? rawExpression(watch(key)) : watch(key); // Initial render useEffect(() => { @@ -457,40 +471,6 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { } }, [fieldValue, targetLineRange]); - const getFallBackSelectedType = (): InputType => { - if ( - typeof field.value === 'string' && - field.value.trim() !== '' - ) { - return field?.types[field.types.length - 1]; - } - else { - return field?.types[0]; - } - } - - useEffect(() => { - // If recordTypeField is present, always use GUIDED mode - if (recordTypeField) { - setInputMode(InputMode.RECORD); - return; - } - if (field?.types.length === 0) { - setInputMode(InputMode.EXP); - return; - }; - let selectedInputType = field?.types.find(type => type.selected); - if (!selectedInputType) { - selectedInputType = getFallBackSelectedType(); - } - const inputMode = getInputModeFromTypes(selectedInputType); - if (!inputMode) { - setInputMode(InputMode.EXP); - return; - }; - setInputMode(inputMode); - }, [field?.types, recordTypeField]); - const handleFocus = async (controllerOnChange?: (value: string) => void) => { setFocused(true); @@ -560,7 +540,7 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { recordTypeField, field.type === "LV_EXPRESSION", field.types, - inputModeRef.current, + inputMode, ); }; @@ -568,54 +548,6 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { return await extractArgsFromFunction(value, getPropertyFromFormField(field), cursorPosition); }; - const isSwitchToPrimaryModeSafe = (expValue: string) => { - if (!expValue) return true; - const primaryInputType = getPrimaryInputType(field.types); - const primaryInputMode = getInputModeFromTypes(primaryInputType); - const valueConfigObject = getEditorConfiguration(primaryInputMode); - return valueConfigObject.getIsValueCompatible(expValue); - } - - const handleModeChange = (value: InputMode) => { - const raw = watch(key); - const currentValue = raw && typeof raw === "string" ? raw.trim() : ""; - if (inputMode !== InputMode.EXP) { - setInputMode(value); - return; - } - if (!isSwitchToPrimaryModeSafe(currentValue)) { - targetInputModeRef.current = value; - setShowModeSwitchWarning(true) - return; - } - setInputMode(value); - }; - - const handleModeSwitchWarningContinue = () => { - if (targetInputModeRef.current !== null) { - setInputMode(targetInputModeRef.current); - const targetMode = targetInputModeRef.current; - const shouldClearValue = [ - InputMode.PROMPT, - InputMode.TEMPLATE, - InputMode.TEXT, - InputMode.NUMBER, - InputMode.BOOLEAN, - ] - .includes(targetMode) && inputMode === InputMode.EXP; - if (shouldClearValue) { - setValue(key, ""); - } - targetInputModeRef.current = null; - } - setShowModeSwitchWarning(false); - }; - - const handleModeSwitchWarningCancel = () => { - targetInputModeRef.current = null; - setShowModeSwitchWarning(false); - }; - const handleOpenExpandedMode = () => { setIsExpandedModalOpen(true); }; @@ -637,18 +569,6 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { : `${field.documentation}.` : ''; - const isModeSwitcherRestricted = () => { - return !field.types || !(field.types.length > 1); - }; - - const isModeSwitcherAvailable = () => { - if (recordTypeField) return true; - if (isModeSwitcherRestricted()) return false; - if (!(focused || isExpressionEditorHovered)) return false; - if (!getInputModeFromTypes(getPrimaryInputType(field.types))) return false; - return true; - } - return ( {
- - - {field.label} - {field.defaultValue && { `(Default: ${field.defaultValue}) `}} + {field.label && ( + + + {field.label} + {field.defaultValue && { `(Default: ${field.defaultValue}) `}} {(required ?? !field.optional) && } - {getPrimaryInputType(field.types)?.ballerinaType && ( - - {sanitizeType(getPrimaryInputType(field.types)?.ballerinaType)} - - )} - - + {getPrimaryInputType(field.types)?.ballerinaType && ( + + {sanitizeType(getPrimaryInputType(field.types)?.ballerinaType)} + + )} + + + )} {documentation && {documentation}}
- - {isModeSwitcherAvailable() && ( + {modeSwitcherContext?.isModeSwitcherEnabled && isExpressionEditorHovered && ( + - )} - + + )}
)} @@ -743,7 +666,7 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { rules.validate = { pattern: (value: any) => { try { - const currentMode = inputModeRef.current; + const currentMode = inputMode; // Only validate in TEXT mode if (currentMode !== InputMode.TEXT) { @@ -794,9 +717,9 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { // clear field diagnostics setFormDiagnostics([]); // Use ref to get current mode (not stale closure value) - const currentMode = inputModeRef.current; - const rawValue = (currentMode === InputMode.PROMPT || currentMode === InputMode.TEMPLATE) && - rawExpression ? rawExpression(typeof updatedValue === 'string' ? updatedValue : JSON.stringify(updatedValue)) : updatedValue; + const currentMode = inputMode; + const rawValue = (currentMode === InputMode.PROMPT || currentMode === InputMode.TEMPLATE) && + rawExpression ? rawExpression(typeof updatedValue === 'string' ? updatedValue : JSON.stringify(updatedValue)) : updatedValue; onChange(rawValue); if (getExpressionEditorDiagnostics && (currentMode === InputMode.EXP || currentMode === InputMode.PROMPT || currentMode === InputMode.TEMPLATE)) { @@ -855,7 +778,7 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { formDiagnostics && formDiagnostics.length > 0 && d.message).join(', ')} /> } - {onOpenExpandedMode && toEditorMode(inputModeRef.current) && ( + {onOpenExpandedMode && toEditorMode(inputMode) && ( { // clear field diagnostics setFormDiagnostics([]); // Use ref to get current mode (not stale closure value) - const currentMode = inputModeRef.current; + const currentMode = inputMode; const rawValue = (currentMode === InputMode.PROMPT || currentMode === InputMode.TEMPLATE) && rawExpression ? rawExpression(updatedValue) : updatedValue; onChange(rawValue); - if (getExpressionEditorDiagnostics && (currentMode === InputMode.EXP || currentMode === InputMode.PROMPT || currentMode === InputMode.TEMPLATE)) { + if (getExpressionEditorDiagnostics && (inputMode === InputMode.EXP || inputMode === InputMode.PROMPT || inputMode === InputMode.TEMPLATE)) { getExpressionEditorDiagnostics( (required ?? !field.optional) || rawValue !== '', rawValue, @@ -902,7 +825,7 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { setIsExpandedModalOpen(false) }} onSave={handleSaveExpandedMode} - mode={toEditorMode(inputModeRef.current)!} + mode={toEditorMode(inputMode)!} completions={completions} fileName={effectiveFileName} targetLineRange={effectiveTargetLineRange} @@ -912,7 +835,7 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { getHelperPane={handleGetHelperPane} error={error} formDiagnostics={formDiagnostics} - inputMode={inputModeRef.current} + inputMode={inputMode} /> )} @@ -920,13 +843,6 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { }} /> - {showModeSwitchWarning && ( - - )}
); }; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx index 86f88497ff..3bb3167a2d 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx @@ -27,7 +27,7 @@ import { } from '@wso2/ui-toolkit'; import { S } from './ExpressionEditor'; import TextModeEditor from './MultiModeExpressionEditor/TextExpressionEditor/TextModeEditor'; -import { InputMode, TokenType } from './MultiModeExpressionEditor/ChipExpressionEditor/types'; +import { InputMode } from './MultiModeExpressionEditor/ChipExpressionEditor/types'; import { LineRange } from '@wso2/ballerina-core/lib/interfaces/common'; import { FormField, HelperpaneOnChangeOptions } from '../Form/types'; import { ChipExpressionEditorComponent } from './MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor'; @@ -38,10 +38,8 @@ import { EnumEditor } from './MultiModeExpressionEditor/EnumEditor/EnumEditor'; import { SQLExpressionEditor } from './MultiModeExpressionEditor/SqlExpressionEditor/SqlExpressionEditor'; import BooleanEditor from './MultiModeExpressionEditor/BooleanEditor/BooleanEditor'; import { getPrimaryInputType, isDropDownType } from '@wso2/ballerina-core'; -import { DynamicArrayBuilder } from './MultiModeExpressionEditor/DynamicArrayBuilder/DynamicArrayBuilder'; import { ChipExpressionEditorDefaultConfiguration } from './MultiModeExpressionEditor/ChipExpressionEditor/ChipExpressionDefaultConfig'; -import MappingConstructor from './MultiModeExpressionEditor/MappingConstructor/MappingConstructor'; -import MappingObjectConstructor from './MultiModeExpressionEditor/MappingObjectConstructor/MappingObjectConstructor'; +import { DynamicArrayBuilder } from './MultiModeExpressionEditor/DynamicArrayBuilder/DynamicArrayBuilder'; import { isRecord } from './utils'; export interface ExpressionFieldProps { @@ -153,41 +151,18 @@ export const ExpressionField: React.FC = (props: Expressio isInExpandedMode } = props; - if (inputMode === InputMode.MAP_EXP) { - return ( - } - label={field.label} - onChange={(val) => onChange(val, JSON.stringify(val).length)} - expressionFieldProps={props} - /> - ); - } - //below editors cannot have input value in record type if (isRecord(value)) return null; - if (inputMode === InputMode.ARRAY || inputMode === InputMode.TEXT_ARRAY) { return ( onChange(val, val.length)} - expressionFieldProps={props} - /> - ); - } - if (inputMode === InputMode.MAP) { - return ( - onChange(val, val.length)} expressionFieldProps={props} /> ); } - //below editors cannot have input value in array type if (Array.isArray(value)) return null; @@ -352,6 +327,7 @@ export const ExpressionField: React.FC = (props: Expressio isExpandedVersion={false} completions={completions} onChange={onChange} + onBlur={onBlur} value={value} sanitizedExpression={sanitizedExpression} rawExpression={rawExpression} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/FieldFactory.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FieldFactory.tsx new file mode 100644 index 0000000000..642c2e9802 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FieldFactory.tsx @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useRef } from "react"; +import { useEffect, useState, useMemo, useCallback } from "react"; +import styled from '@emotion/styled'; +import { EditorFactory, FormField, InputMode, useFormContext, Provider as FormContextProvider } from "../.."; +import { InputType, ExpressionProperty } from "@wso2/ballerina-core"; +import { NodeKind, NodeProperties, RecordTypeField, SubPanel, SubPanelView } from "@wso2/ballerina-core"; +import { CompletionItem } from "@wso2/ui-toolkit"; +import { getInputModeFromTypes } from "./MultiModeExpressionEditor/ChipExpressionEditor/utils"; +import { ModeSwitcherProvider } from "./ModeSwitcherContext"; + +const Container = styled.div` + width: 100%; +`; + +type FieldFactoryProps = { + field: FormField; + selectedNode?: NodeKind; + openRecordEditor?: (open: boolean, newType?: string | NodeProperties) => void; + openSubPanel?: (subPanel: SubPanel) => void; + subPanelView?: SubPanelView; + handleOnFieldFocus?: (key: string) => void; + onBlur?: () => void | Promise; + autoFocus?: boolean; + handleOnTypeChange?: () => void; + recordTypeFields?: RecordTypeField[]; + onIdentifierEditingStateChange?: (isEditing: boolean) => void; + setSubComponentEnabled?: (isAdding: boolean) => void; + handleNewTypeSelected?: (type: string | CompletionItem) => void; + scopeFieldAddon?: React.ReactNode; + isContextTypeEditorSupported?: boolean; + openFormTypeEditor?: (open: boolean, newType?: string) => void; +} + + +export const FieldFactory = (props: FieldFactoryProps) => { + const [renderingEditors, setRenderingEditors] = useState(null); + const [inputMode, setInputMode] = useState(InputMode.EXP); + const isModeSelectionDirty = useRef(false) + + const formContext = useFormContext(); + const { expressionEditor } = formContext; + + const updatedGetExpressionEditorDiagnostics = useCallback( + async ( + showDiagnostics: boolean, + expression: string, + key: string, + property: ExpressionProperty + ): Promise => { + const newTypes = property.types.map(type => ({ + ...type, + selected: getInputModeFromTypes(type) === inputMode + })); + const updatedProperty = { ...property, types: newTypes }; + expressionEditor?.getExpressionEditorDiagnostics?.( + showDiagnostics, + expression, + key, + updatedProperty + ) + }, + [expressionEditor, inputMode] + ); + + const updatedExpressionEditor = useMemo(() => { + if (!expressionEditor) { + return undefined; + } + return { + ...expressionEditor, + getExpressionEditorDiagnostics: updatedGetExpressionEditorDiagnostics + }; + }, [expressionEditor, updatedGetExpressionEditorDiagnostics]); + + const updatedFormContext = useMemo(() => ({ + ...formContext, + expressionEditor: updatedExpressionEditor + }), [formContext, updatedExpressionEditor]); + + const getInitialSelectedInputType = (): InputType => { + if (!props.field.types || props.field.types.length === 0) { + throw new Error("Field types are not defined"); + } + if (props.field.types.length === 1) { + return props.field.types[0]; + } + + const selectedType = props.field.types.find(type => type.selected); + if (selectedType) { + return selectedType; + } + + // Fallback for refactored models where all types can be unselected. + // Prioritize the last type (usually EXPRESSION mode) for multi-type fields. + return props.field.types[props.field.types.length - 1]; + } + + useEffect(() => { + if (!props.field.types || props.field.types.length === 0) { + throw new Error("Field types are not defined"); + } + + //TODO: Should be removed once fields with type field is fixed to + // update the types property correctly when changing the type. + if (props.recordTypeFields?.find(recordField => recordField.key === props.field.key)) { + setRenderingEditors([ + { fieldType: "RECORD_MAP_EXPRESSION", selected: true } as InputType, + { fieldType: "EXPRESSION", selected: false } as InputType + ]); + if (!isModeSelectionDirty.current) { + setInputMode(InputMode.RECORD); + updateFieldTypesSelection(InputMode.RECORD); + } + return; + } + + const newRenderingTypes = props.field.types.length === 1 + ? [props.field.types[0]] + : [props.field.types[0], props.field.types[props.field.types.length - 1]]; + setRenderingEditors(newRenderingTypes); + + if (!isModeSelectionDirty.current) { + const selectedInputType = getInitialSelectedInputType(); + const initialInputMode = getInputModeFromTypes(selectedInputType) || InputMode.EXP; + setInputMode(initialInputMode); + updateFieldTypesSelection(initialInputMode); + } + }, [props.field, props.recordTypeFields]); + + const isModeSwitcherEnabled = useMemo(() => { + return renderingEditors && renderingEditors.length > 1; + }, [renderingEditors]); + + const isRecordTypeField = useMemo(() => { + return !!props.recordTypeFields?.find(recordField => recordField.key === props.field.key); + }, [props.recordTypeFields, props.field.key]); + + const updateFieldTypesSelection = (targetMode: InputMode) => { + props.field.types?.forEach(type => { + type.selected = getInputModeFromTypes(type) === targetMode; + }); + }; + + const handleModeChange = useCallback((mode: InputMode) => { + setInputMode(mode); + isModeSelectionDirty.current = true; + updateFieldTypesSelection(mode); + }, []); + + const editorElements = useMemo(() => { + if (!renderingEditors) return null; + + if (!isModeSwitcherEnabled) { + return ; + + } + return renderingEditors.map((type, index) => { + if (inputMode !== getInputModeFromTypes(type)) return null; + return () + }); + }, [renderingEditors, isModeSwitcherEnabled, inputMode, props]); + + + return ( + + + + {editorElements} + + + + ); +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormArrayEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormArrayEditor.tsx new file mode 100644 index 0000000000..585f708b93 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormArrayEditor.tsx @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useRef, useState } from "react"; +import { InputType } from "@wso2/ballerina-core"; +import { Form, FormField, FormFieldEditorProps, FormValues, S, useFormContext, useModeSwitcherContext } from "../.."; +import { Codicon } from "@wso2/ui-toolkit/lib/components/Codicon/Codicon"; +import { ScrollableList, ScrollableListRef } from "@wso2/ui-toolkit/lib/components/ScrollableList/ScrollableList"; +import ModeSwitcher from "../ModeSwitcher"; +import { getArraySubFormFieldFromTypes, stringToRawArrayElements, buildStringArray, getRecordTypeFields } from "./utils"; + +export const FormArrayEditor = (props: FormFieldEditorProps & { + onChange: (value: any) => void; + value: any; +}) => { + const [repeatableFields, setRepeatableFields] = useState([]); + const { expressionEditor } = useFormContext(); + const scrollableListRef = useRef(null); + + const modeSwitcherContext = useModeSwitcherContext(); + + const handleAddNewItem = () => { + const key = crypto.randomUUID(); + if (!(props.field.types[0] as any).template) return; + const newField = getArraySubFormFieldFromTypes(key, (props.field.types[0] as any).template.types as InputType[]) + setRepeatableFields(prev => [...prev, newField]); + // Wait for the dom update + setTimeout(() => { + scrollableListRef.current?.scrollToBottom(); + }, 100); + } + + const handleFormOnChange = (_fieldKey: string, value: any, _allValues: FormValues, parentKey: string) => { + const newRepeatableFields = repeatableFields.map((formField) => { + if (formField.key === parentKey) { + return { ...formField, value }; + } + return formField; + }); + setRepeatableFields(newRepeatableFields); + props.onChange(newRepeatableFields); + } + + const handleModeSwitchValueChange = () => { + const stringValue = buildStringArray(repeatableFields); + props.onChange(stringValue); + } + + const handleDeleteItem = (keyToDelete: string) => { + const newRepeatableFields = repeatableFields.filter((formField) => formField.key !== keyToDelete); + setRepeatableFields(newRepeatableFields); + props.onChange(newRepeatableFields); + }; + + useEffect(() => { + if (!props.value) return; + if (JSON.stringify(props.value) === JSON.stringify(repeatableFields)) return; + let newValue = buildStringArray(props.value); + const initialValues = stringToRawArrayElements(newValue); + const initialFields = initialValues.map((val) => { + const key = crypto.randomUUID(); + return { + ...getArraySubFormFieldFromTypes(key, (props.field.types[0] as any).template.types as InputType[]), + value: val + } + }); + setRepeatableFields(initialFields); + }, [props.value, props.field.types]); + + return ( + + +
+
+ + + {props.field.label} + + + + {props.field.documentation} + +
+ {modeSwitcherContext?.isModeSwitcherEnabled && ( + + { + handleModeSwitchValueChange(); + modeSwitcherContext.onModeChange(value); + }} + types={modeSwitcherContext.types} + /> + + )} +
+
+ + { + repeatableFields.map((formField) => ( + +
+ handleDeleteItem(formField.key)} + /> +
+
{ + handleFormOnChange(fieldKey, value, allValues, formField.key); + }} + expressionEditor={{ + ...expressionEditor, + onCompletionItemSelect: expressionEditor?.onCompletionItemSelect, + getHelperPane: expressionEditor?.getHelperPane, + types: expressionEditor?.types, + referenceTypes: expressionEditor?.referenceTypes, + retrieveVisibleTypes: expressionEditor?.retrieveVisibleTypes, + getTypeHelper: expressionEditor?.getTypeHelper, + helperPaneHeight: expressionEditor?.helperPaneHeight + }} + submitText={'Save'} + nestedForm={true} + preserveOrder={true} + /> + + + ))} + + + + {repeatableFields.length === 0 ? "Initialize Array" : "Add New Item"} + + + ) +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormArrayEditorWrapper.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormArrayEditorWrapper.tsx new file mode 100644 index 0000000000..5c8f6668a1 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormArrayEditorWrapper.tsx @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from "react"; +import { Controller } from "react-hook-form"; +import { useFormContext } from "../../context"; +import { FormArrayEditor } from "./FormArrayEditor"; +import { FormFieldEditorProps } from "./EditorFactory"; + +export const FormArrayEditorWrapper = (props: FormFieldEditorProps) => { + const { form } = useFormContext(); + const { control } = form; + + return ( + ( + + )} + /> + ); +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditor.tsx index 14d2758b2f..d05fbed8fc 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditor.tsx @@ -158,7 +158,7 @@ export function FormMapEditor(props: FormMapEditorProps) { label: "Expression", description: "Expression", }, - valueType: "EXPRESSION", + types: [{fieldType: "EXPRESSION", selected: false}], value: expressionValue || "", optional: false, editable: true, @@ -169,7 +169,7 @@ export function FormMapEditor(props: FormMapEditorProps) { label: "Variable Name", description: "Name of the variable", }, - valueType: "IDENTIFIER", + types: [{fieldType: "IDENTIFIER", selected: false}], value: variableValue || "", optional: true, editable: true, diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditorNew.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditorNew.tsx new file mode 100644 index 0000000000..3f7da96b90 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditorNew.tsx @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useRef, useState } from "react"; +import { InputType } from "@wso2/ballerina-core"; +import { Form, FormValues, S, useFormContext, useModeSwitcherContext, FormField, FormFieldEditorProps } from "../.."; +import { Codicon } from "@wso2/ui-toolkit/lib/components/Codicon/Codicon"; +import { ScrollableList, ScrollableListRef } from "@wso2/ui-toolkit/lib/components/ScrollableList/ScrollableList"; +import ModeSwitcher from "../ModeSwitcher"; +import { getMapSubFormFieldFromTypes, buildStringMap, stringToRawObjectEntries, getRecordTypeFields } from "./utils"; + +export const FormMapEditorNew = (props: FormFieldEditorProps & { + onChange: (value: any) => void; + value: any; +}) => { + const [repeatableFields, setRepeatableFields] = useState([]); + const scrollableListRef = useRef(null); + const isInternalUpdate = useRef(false); + const { expressionEditor } = useFormContext(); + + const modeSwitcherContext = useModeSwitcherContext(); + + const processToOutputFormat = (fields: FormField[][]): Record => { + const output: Record = {}; + fields.forEach((field) => { + const keyField = field[0]; + const valueField = field[1]; + if (keyField.value) { + output[keyField.value as string] = valueField; + } + }); + return output; + } + + const processToInputFormat = (input: Record): FormField[][] => { + const fields: FormField[][] = []; + Object.entries(input).forEach(([key, value]) => { + const keyId = (value as FormField)?.key?.replace("mp-val-", "mp-key-") || crypto.randomUUID(); + const keyField: FormField = { + key: keyId, + label: "Key", + type: "IDENTIFIER", + optional: false, + editable: true, + documentation: "", + value: key, + types: [{ fieldType: "IDENTIFIER", selected: true }], + enabled: true + }; + const valueField: FormField = value as FormField; + fields.push([keyField, valueField]); + }); + return fields; + } + + const handleAddNewItem = () => { + const key = crypto.randomUUID(); + if (!(props.field.types[0] as any).template) return; + const newField = getMapSubFormFieldFromTypes(key, (props.field.types[0] as any).template.types as InputType[]) + setRepeatableFields(prev => [...prev, newField]); + isInternalUpdate.current = true; + // Wait for the dom update + setTimeout(() => { + scrollableListRef.current?.scrollToBottom(); + }, 100); + } + + const handleFormOnChange = (fieldKey: string, value: any, _allValues: FormValues, _parentKey: string) => { + const newRepeatableFields = repeatableFields.map((formFields) => { + // Check if any field in this array matches the fieldKey + const fieldIndex = formFields.findIndex(field => field.key === fieldKey); + if (fieldIndex !== -1) { + const newFields = [...formFields]; + newFields[fieldIndex] = { ...newFields[fieldIndex], value }; + return newFields; + } + return formFields; + }); + setRepeatableFields(newRepeatableFields); + isInternalUpdate.current = true; + props.onChange(processToOutputFormat(newRepeatableFields)); + } + + const handleModeSwitchValueChange = () => { + const stringValue = buildStringMap(repeatableFields); + props.onChange(stringValue); + } + + const handleDeleteItem = (keyToDelete: string) => { + const newRepeatableFields = repeatableFields.filter((formField) => formField[0].key !== keyToDelete); + setRepeatableFields(newRepeatableFields); + isInternalUpdate.current = true; + props.onChange(processToOutputFormat(newRepeatableFields)); + }; + + useEffect(() => { + if (!props.value) return; + if (isInternalUpdate.current) { + isInternalUpdate.current = false; + return; + } + let processedInputValue: string | FormField[][] = ""; + if (typeof props.value === 'string') { + processedInputValue = props.value; + } else { + processedInputValue = processToInputFormat(props.value); + } + let newValue = buildStringMap(processedInputValue); + const initialValues = stringToRawObjectEntries(newValue); + const initialFields = initialValues.map((val) => { + const key = crypto.randomUUID(); + const fields = getMapSubFormFieldFromTypes(key, (props.field.types[0] as any).template.types as InputType[]); + fields[0].value = val.key; + fields[1].value = val.value; + return fields; + }); + setRepeatableFields(initialFields); + }, [props.value, props.field.types]); + + return ( + + +
+
+ + + {props.field.label} + + + + {props.field.documentation} + +
+ {modeSwitcherContext?.isModeSwitcherEnabled && ( + + { + handleModeSwitchValueChange(); + modeSwitcherContext.onModeChange(value); + }} + types={modeSwitcherContext.types} + /> + + )} +
+
+ + { + repeatableFields.map((formField) => ( + +
+ handleDeleteItem(formField[0].key)} + /> +
+ { + handleFormOnChange(fieldKey, value, allValues, formField[0].key); + }} + expressionEditor={{ + ...expressionEditor, + onCompletionItemSelect: expressionEditor?.onCompletionItemSelect, + getHelperPane: expressionEditor?.getHelperPane, + types: expressionEditor?.types, + referenceTypes: expressionEditor?.referenceTypes, + retrieveVisibleTypes: expressionEditor?.retrieveVisibleTypes, + getTypeHelper: expressionEditor?.getTypeHelper, + helperPaneHeight: expressionEditor?.helperPaneHeight + }} + submitText={'Save'} + nestedForm={true} + preserveOrder={true} + /> +
+ + ))} +
+ + + {repeatableFields.length === 0 ? "Initialize Map" : "Add New Item"} + +
+ ) +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditorNewWrapper.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditorNewWrapper.tsx new file mode 100644 index 0000000000..208fa9eb4a --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/FormMapEditorNewWrapper.tsx @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from "react"; +import { Controller } from "react-hook-form"; +import { useFormContext } from "../../context"; +import { FormMapEditorNew } from "./FormMapEditorNew"; +import { FormFieldEditorProps } from "./EditorFactory"; + +export const FormMapEditorWrapper = (props: FormFieldEditorProps) => { + const { form } = useFormContext(); + const { control } = form; + + return ( + ( + + )} + /> + ); +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/HeaderSetEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/HeaderSetEditor.tsx index 510021adb0..d34f7617dc 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/HeaderSetEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/HeaderSetEditor.tsx @@ -231,7 +231,7 @@ export function HeaderSetEditor(props: HeaderSetEditorProps) { optional: false, editable: true, documentation: "Type of the header", - value: headerSetToEdit?.type || "", + value: headerSetToEdit?.type || field.items?.[0] || "", types: [{ fieldType: "SINGLE_SELECT", ballerinaType: "string", selected: false }], label: "Type", type: "SINGLE_SELECT", @@ -240,11 +240,11 @@ export function HeaderSetEditor(props: HeaderSetEditorProps) { { key: "optional", enabled: true, - optional: false, + optional: true, editable: true, documentation: "Required or Optional", value: headerSetToEdit?.optional ?? false as any, - types: [{ fieldType: "BOOLEAN", ballerinaType: "boolean", selected: false }], + types: [{ fieldType: "FLAG", selected: true }], label: "Optional", type: "FLAG", } 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 e5f0a68aa2..a107808f3f 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/IdentifierField.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/IdentifierField.tsx @@ -36,13 +36,18 @@ export function IdentifierField(props: IdentifierFieldProps) { const { expressionEditor, form } = useFormContext(); const { getExpressionEditorDiagnostics } = expressionEditor; const [formDiagnostics, setFormDiagnostics] = useState(field.diagnostics); - const { watch, formState, register } = form; + const { watch, formState, register, setValue } = form; const { errors } = formState; useEffect(() => { setFormDiagnostics(field.diagnostics); }, [field.diagnostics]); + // Sync external field value changes to the form (e.g., when a sibling field's onValueChange updates the value) + useEffect(() => { + setValue(field.key, field.value ?? ''); + }, [field.key, field.value, setValue]); + const validateIdentifierName = useCallback(debounce(async (value: string) => { const fieldValue = watch(field.key); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ModeSwitcherContext.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ModeSwitcherContext.tsx new file mode 100644 index 0000000000..9b80789912 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ModeSwitcherContext.tsx @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { createContext, useContext, ReactNode } from "react"; +import { InputType } from "@wso2/ballerina-core"; +import { InputMode } from "./MultiModeExpressionEditor/ChipExpressionEditor/types"; + +export type ModeSwitcherContextType = { + inputMode: InputMode; + onModeChange: (mode: InputMode) => void; + types: InputType[]; + isRecordTypeField: boolean; + isModeSwitcherEnabled: boolean; +}; + +const ModeSwitcherContext = createContext(undefined); + +export const useModeSwitcherContext = () => { + const context = useContext(ModeSwitcherContext); + return context; +}; + +type ModeSwitcherProviderProps = { + children: ReactNode; + inputMode: InputMode; + onModeChange: (mode: InputMode) => void; + types: InputType[]; + isRecordTypeField: boolean; + isModeSwitcherEnabled: boolean; +}; + +export const ModeSwitcherProvider = ({ + children, + inputMode, + onModeChange, + types, + isRecordTypeField, + isModeSwitcherEnabled +}: ModeSwitcherProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx index ef9156e974..eadd805b29 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx @@ -64,6 +64,7 @@ import { InputMode } from "../types"; export type ChipExpressionEditorComponentProps = { onTokenRemove?: (token: string) => void; onTokenClick?: (token: string) => void; + onBlur?: () => void; isExpandedVersion: boolean; getHelperPane?: ( value: string, @@ -207,6 +208,7 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone const handleFocusOutListner = buildOnFocusOutListner(() => { setIsTokenUpdateScheduled(true); + props.onBlur?.(); }); const waitForStateChange = (): Promise => { diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/types.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/types.ts index 2340c4f993..d82bd2c651 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/types.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/types.ts @@ -29,7 +29,6 @@ export enum InputMode { TEXT_ARRAY = "Text Array", PROMPT = "Prompt", MAP = "Map", - MAP_EXP = "Mapping", SIMPLE_TEXT = "Info" }; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/utils.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/utils.ts index f94c0cf1f1..294395724e 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/utils.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/utils.ts @@ -54,6 +54,9 @@ export const getInputModeFromTypes = (inputType: InputType): InputMode | undefin if (inputType.fieldType === "EXPRESSION") { return InputMode.EXP; } + if (inputType.fieldType === "NUMBER") { + return InputMode.NUMBER; + } if (inputType.fieldType === "SINGLE_SELECT") { return InputMode.SELECT; } @@ -63,18 +66,21 @@ export const getInputModeFromTypes = (inputType: InputType): InputMode | undefin if (inputType.fieldType === "TEXT_SET") { return InputMode.TEXT_ARRAY; } - if (inputType.fieldType === "MAPPING_EXPRESSION_SET") { - return InputMode.MAP; - } - if (inputType.fieldType === "MAPPING_EXPRESSION") { - return InputMode.MAP_EXP; - } if (inputType.fieldType === "PROMPT") { return InputMode.PROMPT; } if (inputType.fieldType === "FLAG") { return InputMode.BOOLEAN; } + if (inputType.fieldType === "RECORD_MAP_EXPRESSION") { + return InputMode.RECORD; + } + if (inputType.fieldType === "REPEATABLE_MAP") { + return InputMode.MAP; + } + if (inputType.fieldType === "REPEATABLE_LIST") { + return InputMode.ARRAY; + } //default behaviour return getInputModeFromBallerinaType(inputType.ballerinaType); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/Configurations.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/Configurations.tsx index a80b291bfc..e17be13dbc 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/Configurations.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/Configurations.tsx @@ -97,6 +97,7 @@ export class StringTemplateEditorConfig extends ChipExpressionEditorDefaultConfi } getIsValueCompatible(expValue: string) { + if (!expValue) return true; const suffix = this.getSerializationSuffix(); const prefix = this.getSerializationPrefix(); return (expValue.trim().startsWith(prefix) && expValue.trim().endsWith(suffix)) @@ -217,12 +218,13 @@ export class NumberExpressionEditorConfig extends ChipExpressionEditorDefaultCon } getIsValueCompatible(value: string): boolean { + if (!value) return true; return this.DECIMAL_INPUT_REGEX.test(value); } } export class RecordConfigExpressionEditorConfig extends ChipExpressionEditorDefaultConfiguration { - getIsToggleHelperAvailable(): boolean { + getIsToggleHelperAvailable(): boolean { return false; - } + } } diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/DynamicArrayBuilder/DynamicArrayBuilder.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/DynamicArrayBuilder/DynamicArrayBuilder.tsx index fdace4b7c5..40191ed0c5 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/DynamicArrayBuilder/DynamicArrayBuilder.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/DynamicArrayBuilder/DynamicArrayBuilder.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * Copyright (c) 2026, 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 @@ -17,8 +17,9 @@ */ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ExpressionFieldProps } from "../../ExpressionField"; -import { Codicon } from '@wso2/ui-toolkit'; +import { Codicon } from "@wso2/ui-toolkit"; + +import type { ExpressionFieldProps } from "../../ExpressionField"; import { ChipExpressionEditorComponent } from "../ChipExpressionEditor/components/ChipExpressionEditor"; import { useFormContext } from "../../../../context"; import { S } from "../styles"; @@ -45,7 +46,7 @@ export const DynamicArrayBuilder = (props: DynamicArrayBuilderProps) => { const expressionSetType = expressionFieldProps.field.types.find(t => t.fieldType === "EXPRESSION_SET" || t.fieldType === "TEXT_SET"); const minItems = expressionSetType?.minItems ?? 1; const defaultItems = expressionSetType?.defaultItems ?? 1; - + const [isInitialized, setIsInitialized] = useState(false); const currentValuesRef = useRef([]); const paddedRef = useRef(false); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/MappingConstructor/MappingConstructor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/MappingConstructor/MappingConstructor.tsx deleted file mode 100644 index c3541e1511..0000000000 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/MappingConstructor/MappingConstructor.tsx +++ /dev/null @@ -1,160 +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, { useState, useEffect, useRef } from "react"; -import { S } from '../styles'; -import { Codicon, ThemeColors } from "@wso2/ui-toolkit"; -import { ChipExpressionEditorComponent } from "../ChipExpressionEditor/components/ChipExpressionEditor"; -import { ExpressionFieldProps } from "../../ExpressionField"; -import { ChipExpressionEditorDefaultConfiguration } from "../ChipExpressionEditor/ChipExpressionDefaultConfig"; - -interface MappingConstructorProps { - label: string; - value: any[]; - onChange: (updated: any[]) => void; - expressionFieldProps: ExpressionFieldProps; -} - -const transformExternalValueToInternal = (externalValue: any[]): any[] => { - if (!externalValue) return []; - return externalValue - .filter((item) => item != null) - .map((item, index) => { - // Each item is like {someKey: "someValue"}, extract key and value - const entries = Object.entries(item); - const [key, value] = entries.length > 0 ? entries[0] : ["", ""]; - return { id: index, key: key || "", value: value || "" }; - }); -} - -const toOutputFormat = (pairs: any[]): any[] => { - return pairs.map(pair => { - if (pair.key) { - return { [pair.key]: pair.value }; - } - return {}; - }); -} - -const getNextId = (items: any[]): number => { - if (items.length === 0) { - return 0; - } - return Math.max(...items.map(item => item.id)) + 1; -} - - -export const MappingConstructor: React.FC = ({ label, value, onChange, expressionFieldProps }) => { - //used this to manually trigger rerenders when value prop changes - const [_, setManualRerenderTrigger] = useState(true); - const [hasUntouchedPairs, setHasUntouchedPairs] = useState(false); - const internalValueRef = useRef([]); - - useEffect(() => { - if (JSON.stringify(toOutputFormat(internalValueRef.current)) === JSON.stringify(value)) return; - internalValueRef.current = transformExternalValueToInternal(value); - setManualRerenderTrigger(prev => !prev); - }, [value]); - - const handleAddPair = () => { - const newPair = { id: getNextId(internalValueRef.current), key: "", value: "" }; - const updatedValue = [...internalValueRef.current, newPair]; - setHasUntouchedPairs(true); - internalValueRef.current = updatedValue; - onChange(toOutputFormat(updatedValue)); - } - - const handleDeletePair = (id: number) => { - const updatedValue = internalValueRef.current.filter(pair => pair.id !== id); - internalValueRef.current = updatedValue; - onChange(toOutputFormat(updatedValue)); - } - - const handleKeyChange = (id: number, newKey: string) => { - const updatedValue = internalValueRef.current.map(pair => - pair.id === id ? { ...pair, key: newKey } : pair - ); - setHasUntouchedPairs(newKey === ""); - internalValueRef.current = updatedValue; - onChange(toOutputFormat(updatedValue)); - } - - const handleValueChange = (id: number, newValue: string) => { - const updatedValue = internalValueRef.current.map(pair => - pair.id === id ? { ...pair, value: newValue } : pair - ); - internalValueRef.current = updatedValue; - onChange(toOutputFormat(updatedValue)); - } - - return ( - - {internalValueRef.current.map((pair) => ( - - - handleKeyChange(pair.id, e.target.value)} - placeholder="Key" - /> - - handleValueChange(pair.id, value)} - value={pair.value} - sanitizedExpression={expressionFieldProps.sanitizedExpression} - rawExpression={expressionFieldProps.rawExpression} - fileName={expressionFieldProps.fileName} - targetLineRange={expressionFieldProps.targetLineRange} - extractArgsFromFunction={expressionFieldProps.extractArgsFromFunction} - onOpenExpandedMode={expressionFieldProps.onOpenExpandedMode} - onRemove={expressionFieldProps.onRemove} - isInExpandedMode={expressionFieldProps.isInExpandedMode} - configuration={new ChipExpressionEditorDefaultConfiguration()} - placeholder={expressionFieldProps.field.placeholder} - /> - - handleDeletePair(pair.id)} - > - - - - ))} - - - Add Item - - - ); -}; - -export default MappingConstructor; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/MappingObjectConstructor/MappingObjectConstructor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/MappingObjectConstructor/MappingObjectConstructor.tsx deleted file mode 100644 index d2e387b104..0000000000 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/MappingObjectConstructor/MappingObjectConstructor.tsx +++ /dev/null @@ -1,175 +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, { useState, useEffect, useRef, useMemo } from "react"; -import { S } from '../styles'; -import { Codicon, ThemeColors } from "@wso2/ui-toolkit"; -import { ChipExpressionEditorComponent } from "../ChipExpressionEditor/components/ChipExpressionEditor"; -import { ExpressionFieldProps } from "../../ExpressionField"; -import { ChipExpressionEditorDefaultConfiguration } from "../ChipExpressionEditor/ChipExpressionDefaultConfig"; -import { isRecord } from "../../utils"; - -interface MappingObjectConstructorProps { - label: string; - value: Record; - onChange: (updated: any) => void; - expressionFieldProps: ExpressionFieldProps; -} - - -const transformExternalValueToInternal = (externalValue: any): any[] => { - if (!externalValue || !isRecord(externalValue)) return []; - return Object.entries(externalValue).map(([key, value], index) => ({ - id: index, - key: key || "", - value: value || "" - })); -} - -const toOutputFormat = (pairs: any[]): any => { - const result: any = {}; - const keyCount: Record = {}; - - pairs.forEach(pair => { - if (!pair.key) return; - - const baseKey = pair.key; - - if (keyCount[baseKey] === undefined) { - keyCount[baseKey] = 0; - result[baseKey] = pair.value; - } else { - keyCount[baseKey] += 1; - const newKey = `${baseKey}_${keyCount[baseKey]}`; - result[newKey] = pair.value; - } - }); - - return result; -}; - - -const getNextId = (items: any[]): number => { - if (items.length === 0) { - return 0; - } - return Math.max(...items.map(item => item.id)) + 1; -} - - -export const MappingObjectConstructor: React.FC = ({ label, value, onChange, expressionFieldProps }) => { - //used this to manually trigger rerenders when value prop changes - const [_, setManualRerenderTrigger] = useState(true); - const internalValueRef = useRef([]); - - useEffect(() => { - if (JSON.stringify(toOutputFormat(internalValueRef.current)) === JSON.stringify(value)) return; - internalValueRef.current = transformExternalValueToInternal(value); - setManualRerenderTrigger(prev => !prev); - }, [value]); - - const hasUntouchedPairs = useMemo(() => { - return internalValueRef.current.some(pair => pair.key === ""); - }, [internalValueRef.current]); - - - const handleAddPair = () => { - const newPair = { id: getNextId(internalValueRef.current), key: "", value: "" }; - const updatedValue = [...internalValueRef.current, newPair]; - internalValueRef.current = updatedValue; - onChange(toOutputFormat(updatedValue)); - } - - const handleDeletePair = (id: number) => { - const updatedValue = internalValueRef.current.filter(pair => pair.id !== id); - internalValueRef.current = updatedValue; - onChange(toOutputFormat(updatedValue)); - } - - const handleKeyChange = (id: number, newKey: string) => { - const updatedValue = internalValueRef.current.map(pair => - pair.id === id ? { ...pair, key: newKey } : pair - ); - internalValueRef.current = updatedValue; - onChange(toOutputFormat(updatedValue)); - } - - const handleValueChange = (id: number, newValue: string) => { - const updatedValue = internalValueRef.current.map(pair => - pair.id === id ? { ...pair, value: newValue } : pair - ); - internalValueRef.current = updatedValue; - onChange(toOutputFormat(updatedValue)); - } - - return ( - - {internalValueRef.current.map((pair) => ( - - - handleKeyChange(pair.id, e.target.value)} - placeholder="Key" - /> - - handleValueChange(pair.id, value)} - value={pair.value} - sanitizedExpression={expressionFieldProps.sanitizedExpression} - rawExpression={expressionFieldProps.rawExpression} - fileName={expressionFieldProps.fileName} - targetLineRange={expressionFieldProps.targetLineRange} - extractArgsFromFunction={expressionFieldProps.extractArgsFromFunction} - onOpenExpandedMode={expressionFieldProps.onOpenExpandedMode} - onRemove={expressionFieldProps.onRemove} - isInExpandedMode={expressionFieldProps.isInExpandedMode} - configuration={new ChipExpressionEditorDefaultConfiguration()} - placeholder={expressionFieldProps.field.placeholder} - /> - - handleDeletePair(pair.id)} - > - - - - ))} - - - Add Item - - - ); -}; - -export default MappingObjectConstructor; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/PathEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/PathEditor.tsx index 44862c43a3..0e05c696df 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/PathEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/PathEditor.tsx @@ -16,7 +16,7 @@ * under the License. */ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { FormField } from "../Form/types"; import { TextField } from "@wso2/ui-toolkit"; import { useFormContext } from "../../context"; @@ -33,11 +33,11 @@ interface PathEditorProps { export function PathEditor(props: PathEditorProps) { const { field, handleOnFieldFocus, autoFocus } = props; const { form } = useFormContext(); - const { register, setError, clearErrors } = form; + const { register, setError, clearErrors, watch } = form; const [pathErrorMsg, setPathErrorMsg] = useState(field.diagnostics?.map((diagnostic) => diagnostic.message).join("\n")); - const validatePath = useCallback(debounce(async (value: string) => { + const validatePath = useCallback(debounce((value: string) => { const response = field.type === "SERVICE_PATH" ? parseBasePath(value) : parseResourceActionPath(value); if (response.errors.length > 0) { setPathErrorMsg(response.errors[0].message); @@ -49,7 +49,16 @@ export function PathEditor(props: PathEditorProps) { setPathErrorMsg(""); clearErrors(field.key); } - }, 250), [field]); + }, 250), [field.key, field.type, setError, clearErrors]); + + // Validate on mount and when value changes (covers initial load, paste, programmatic updates) + const fieldValue = watch(field.key); + useEffect(() => { + if (fieldValue !== undefined && fieldValue !== null) { + validatePath(String(fieldValue)); + } + return () => validatePath.cancel(); + }, [fieldValue, field.key, validatePath]); return ( 1) { parentKeyForAdvanceProp = splitedAdvanceProp.slice(0, -1).join('.advanceProperties.'); } - + if (parentKeyForAdvanceProp === fieldKey) { return advancePropKey; } @@ -116,11 +116,11 @@ export const getFieldKeyForAdvanceProp = (fieldKey: string, advancePropKey: stri export const isRecord = (value: unknown): value is Record => { - return ( - typeof value === "object" && - value !== null && - !Array.isArray(value) - ); + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) + ); }; export const getValueForTextModeEditor = (value: string | any[] | Record) => { @@ -143,3 +143,215 @@ export function isExpandableMode(mode: InputMode): mode is EditorMode { export function toEditorMode(mode: InputMode): EditorMode | undefined { return isExpandableMode(mode) ? mode : undefined; } + +export const getArraySubFormFieldFromTypes = (formId: string, types: InputType[]): FormField => { + return { + key: `ar-elm-${formId}`, + label: "", + type: getPrimaryInputType(types)?.fieldType || "", + optional: false, + editable: true, + documentation: "", + value: "", + types: types, + enabled: true + } +} + +export const getMapSubFormFieldFromTypes = (formId: string, types: InputType[]): FormField[] => { + return [ + { + key: `mp-key-${formId}`, + label: "Key", + type: "IDENTIFIER", + optional: false, + editable: true, + documentation: "", + value: "", + types: [{ fieldType: "IDENTIFIER", selected: true }], + enabled: true + }, + { + key: `mp-val-${formId}`, + label: "Value", + type: getPrimaryInputType(types)?.fieldType || "", + optional: false, + editable: true, + documentation: "", + value: "", + types: types, + enabled: true + } + ] +} + +export function stringToRawArrayElements(input: string): string[] { + // remove outer [ ] + const s = input.trim().slice(1, -1); + + if (s === "") { + return [""]; + } + + const result: string[] = []; + let current = ""; + let depth = 0; + let inString = false; + + for (let i = 0; i < s.length; i++) { + const char = s[i]; + const prev = s[i - 1]; + + // handle string boundaries + if (char === '"' && prev !== "\\") { + inString = !inString; + current += char; + continue; + } + + if (!inString) { + if (char === "[" || char === "{") depth++; + if (char === "]" || char === "}") depth--; + + if (char === "," && depth === 0) { + result.push(current); + current = ""; + continue; + } + } + + current += char; + } + + // Always push the final element (even if empty) to preserve trailing empty values + result.push(current); + + return result; +} + +export function stringToRawObjectEntries( + input: string +): { key: string; value: string }[] { + + // remove outer { } + const s = input.trim().slice(1, -1); + + const result: { key: string; value: string }[] = []; + + let current = ""; + let depth = 0; + let inString = false; + + for (let i = 0; i < s.length; i++) { + const char = s[i]; + const prev = s[i - 1]; + + // toggle string state + if (char === '"' && prev !== "\\") { + inString = !inString; + current += char; + continue; + } + + if (!inString) { + if (char === "{" || char === "[") depth++; + if (char === "}" || char === "]") depth--; + + // top-level comma → end of one pair + if (char === "," && depth === 0) { + pushPair(current, result); + current = ""; + continue; + } + } + + current += char; + } + + if (current.trim()) { + pushPair(current, result); + } + + return result; +} + +function pushPair( + text: string, + result: { key: string; value: string }[] +) { + let depth = 0; + let inString = false; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + const prev = text[i - 1]; + + if (char === '"' && prev !== "\\") { + inString = !inString; + } + + if (!inString) { + if (char === "{" || char === "[") depth++; + if (char === "}" || char === "]") depth--; + + // first top-level colon + if (char === ":" && depth === 0) { + const key = text.slice(0, i).trim(); + const value = text.slice(i + 1).trim(); + + result.push({ + key: key, + value: value + }); + return; + } + } + } +} + +export function buildStringArray(elements: FormField[]): string { + if (typeof elements === "string") return elements; + const parts = elements.map(el => { + return (el.value as string).trim(); + }); + return `[${parts.join(", ")}]`; +} + +export function buildStringMap(elements: FormField[][] | string): string { + if (typeof elements === "string") return elements; + let finalString = "{"; + elements.forEach((el, index) => { + let processedValue = (el[1].value as string).trim(); + const keyValue = (el[0].value as string).trim(); + + if (index !== 0) { + finalString += `, ${keyValue}: ${processedValue}`; + } + else { + finalString += ` ${keyValue}: ${processedValue}`; + } + }); + + return finalString + "}"; +} + +export function getRecordTypeFields(fields: FormField[]): RecordTypeField[] { + return fields.filter(field => { + const types = field.types; + if (!types) return false; + return types.some( + type => + ( + type.typeMembers && + type.typeMembers.some(member => member.kind === "RECORD_TYPE") + ) + ); + }) + .map((field) => ({ + key: field.key, + property: getPropertyFromFormField(field), + recordTypeMembers: field.types + .flatMap(type => type.typeMembers || []) + .filter(member => member.kind === "RECORD_TYPE") + })); +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/resources/icons/nodes/DownloadIcon.tsx b/workspaces/ballerina/ballerina-side-panel/src/resources/icons/nodes/DownloadIcon.tsx new file mode 100644 index 0000000000..8084843219 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/resources/icons/nodes/DownloadIcon.tsx @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from "react"; + +export const DownloadIcon = () => { + return ( + + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/package.json b/workspaces/ballerina/ballerina-visualizer/package.json index d8ec149049..29aade685b 100644 --- a/workspaces/ballerina/ballerina-visualizer/package.json +++ b/workspaces/ballerina/ballerina-visualizer/package.json @@ -25,6 +25,7 @@ "@headlessui/react": "2.2.4", "@tanstack/query-core": "5.77.1", "@tanstack/react-query": "5.77.1", + "@tanstack/react-query-persist-client": "5.77.1", "@vscode/webview-ui-toolkit": "1.4.0", "framer-motion": "^11.0.0", "@wso2/ballerina-core": "workspace:*", @@ -61,6 +62,8 @@ "highlight.js": "11.11.1", "rehype-raw": "^7.0.0", "remark-breaks": "~4.0.0", + "js-yaml": "4.1.0", + "swagger-ui-react": "5.22.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", @@ -86,6 +89,8 @@ "style-loader": "4.0.0", "ts-loader": "9.5.2", "typescript": "5.8.3", + "@types/js-yaml": "4.0.5", + "@types/swagger-ui-react": "5.18.0", "webpack": "5.104.1", "@types/react-lottie": "1.2.5", "@types/lodash.debounce": "4.0.6", diff --git a/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx b/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx index 9705db3fdd..9c4d631272 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/Hooks.tsx @@ -80,5 +80,9 @@ export const useDataMapperModel = ( await refetch(); }; - return { model, isFetching, isError, refreshDMModel }; + const requestRefreshDMModel = () => { + triggerRefresh.current = true; + }; + + return { model, isFetching, isError, refreshDMModel, requestRefreshDMModel }; }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx index dbbd054050..2bc4314cca 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/MainPanel.tsx @@ -285,6 +285,8 @@ const MainPanel = () => { setNavActive(true); rpcClient.getVisualizerLocation().then(async (value) => { const configFilePath = (await rpcClient.getVisualizerRpcClient().joinProjectPath({ segments: ['config.bal'] })).filePath; + const testsFolderResult = await rpcClient.getVisualizerRpcClient().joinProjectPath({ segments: ['tests'], checkExists: true }); + const testsConfigTomlPath = testsFolderResult.exists ? (await rpcClient.getVisualizerRpcClient().joinProjectPath({ segments: ['tests', 'Config.toml'] })).filePath : undefined; let defaultFunctionsFile = (await rpcClient.getVisualizerRpcClient().joinProjectPath({ segments: ['functions.bal'] })).filePath; if (value.documentUri) { defaultFunctionsFile = value.documentUri @@ -618,6 +620,7 @@ const MainPanel = () => { ); @@ -627,6 +630,7 @@ const MainPanel = () => { diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/EntryPointTypeCreator/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/EntryPointTypeCreator/index.tsx index a431776980..306bbbd67f 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/components/EntryPointTypeCreator/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/components/EntryPointTypeCreator/index.tsx @@ -40,6 +40,7 @@ interface EntryPointTypeCreatorProps { modalHeight?: number; payloadContext?: PayloadContext; defaultTab?: 'import' | 'create-from-scratch' | 'browse-exisiting-types'; + note?: string; } interface TypeEditorState { @@ -51,7 +52,7 @@ interface TypeEditorState { export function EntryPointTypeCreator(props: EntryPointTypeCreatorProps) { - const { modalTitle, initialTypeName, modalWidth, modalHeight, payloadContext, isOpen, onClose, onTypeCreate, defaultTab } = props; + const { modalTitle, initialTypeName, modalWidth, modalHeight, payloadContext, isOpen, onClose, onTypeCreate, defaultTab, note } = props; const [typeEditorState, setTypeEditorState] = React.useState({ isTypeCreatorOpen: false, @@ -275,6 +276,7 @@ export function EntryPointTypeCreator(props: EntryPointTypeCreatorProps) { isContextTypeForm={true} payloadContext={payloadContext} defaultTab={defaultTab} + note={note} /> diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/Modal/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/Modal/index.tsx index 5debc7f5af..04c9749445 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/components/Modal/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/components/Modal/index.tsx @@ -19,7 +19,7 @@ import React, { cloneElement, isValidElement, ReactNode, ReactElement, useEffect } from "react"; import { createPortal } from "react-dom"; import styled from "@emotion/styled"; -import { Codicon, Divider, ThemeColors, Typography } from "@wso2/ui-toolkit"; +import { Icon, Divider, ThemeColors, Typography, Tooltip, Button } from "@wso2/ui-toolkit"; import { useVisualizerContext } from "../../Context"; export type DynamicModalProps = { @@ -32,6 +32,8 @@ export type DynamicModalProps = { openState: boolean; setOpenState: (state: boolean) => void; sx?: any; + closeOnBackdropClick?: boolean; + closeButtonIcon?: "close" | "minimize"; }; const ModalContainer = styled.div<{ sx?: any }>` @@ -87,6 +89,10 @@ const ModalHeaderSection = styled.header` justify-content: space-between; `; +export const CloseButton = styled(Button)` + border-radius: 5px; +`; + type TriggerProps = React.ButtonHTMLAttributes & { children: ReactNode }; const Trigger: React.FC = (props) => {props.children}; @@ -100,6 +106,8 @@ const DynamicModal: React.FC & { Trigger: typeof Trigger } = openState, setOpenState, sx, + closeOnBackdropClick = false, + closeButtonIcon = "close", }) => { const { setShowOverlay } = useVisualizerContext(); let trigger: ReactElement | null = null; @@ -121,6 +129,13 @@ const DynamicModal: React.FC & { Trigger: typeof Trigger } = onClose && onClose(); }; + const handleBackdropClick = (e: React.MouseEvent) => { + // Only close if closeOnBackdropClick is true and the click was on the backdrop itself + if (closeOnBackdropClick && e.target === e.currentTarget) { + handleClose(); + } + }; + useEffect(() => { setShowOverlay(openState === true); }); @@ -133,17 +148,32 @@ const DynamicModal: React.FC & { Trigger: typeof Trigger } = const targetEl = document.getElementById("visualizer-container"); + // Map closeButtonIcon prop to actual icon names and tooltip text + const iconName = closeButtonIcon === "minimize" ? "bi-minimize-modal" : "bi-close"; + const tooltipText = closeButtonIcon === "minimize" + ? "Minimize to return to the form" + : "Close"; + return ( <> {trigger} {openState && targetEl && createPortal( - + {title} - + + + + + {content} diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/TopNavigationBar/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/TopNavigationBar/index.tsx index 94749216ea..f777d25820 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/components/TopNavigationBar/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/components/TopNavigationBar/index.tsx @@ -18,7 +18,7 @@ import React, { useEffect, useMemo, useState } from "react"; import styled from "@emotion/styled"; -import { Codicon, Icon } from "@wso2/ui-toolkit"; +import { Button, Codicon, Icon } from "@wso2/ui-toolkit"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { HistoryEntry, MACHINE_VIEW, WorkspaceTypeResponse } from "@wso2/ballerina-core"; @@ -38,6 +38,7 @@ const BreadcrumbContainer = styled.div` gap: 8px; margin-left: 4px; color: var(--vscode-foreground); + flex: 1; `; const BreadcrumbSeparator = styled.span` @@ -223,6 +224,16 @@ export function TopNavigationBar(props: TopNavigationBarProps) { return null; })} + {/** TODO: Uncomment if want to show popup icon */} + {/* + setDevantBtnAnchor(null)} + isVisible={!!devantBtnAnchor} + projectPath={projectPath} + /> */} ); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/index.tsx index 4a90b3f110..17fdacfdda 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/index.tsx @@ -17,22 +17,12 @@ */ import React from "react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createRoot } from "react-dom/client"; import { Visualizer } from "./Visualizer"; import { VisualizerContextProvider, RpcContextProvider, ModalStackProvider } from "./Context"; +import { PlatformExtContextProvider } from "./providers/platform-ext-ctx-provider"; import { clearDiagramZoomAndPosition } from "./utils/bi"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - refetchOnWindowFocus: false, - staleTime: 1000, - gcTime: 1000, - }, - }, -}); +import { ReactQueryProvider } from "./providers/react-query-provider"; export function renderWebview(mode: string, target: HTMLElement) { // clear diagram memory @@ -43,9 +33,11 @@ export function renderWebview(mode: string, target: HTMLElement) { - - - + + + + + diff --git a/workspaces/ballerina/ballerina-visualizer/src/providers/platform-ext-ctx-provider.tsx b/workspaces/ballerina/ballerina-visualizer/src/providers/platform-ext-ctx-provider.tsx new file mode 100644 index 0000000000..71f6d73dde --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/providers/platform-ext-ctx-provider.tsx @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2026, 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 { useQueryClient, useQuery } from "@tanstack/react-query"; +import { PlatformExtState } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { PlatformExtRpcClient } from "@wso2/ballerina-rpc-client/lib/rpc-clients/platform-ext/platform-ext-client"; +import { + ConnectionListItem, + ICmdParamsBase, + ICreateDirCtxCmdParams, + CommandIds as PlatformExtCommandIds, +} from "@wso2/wso2-platform-core"; +import React, { useContext, FC, ReactNode, useEffect, useState } from "react"; + +const defaultPlatformExtContext: { + platformExtState: PlatformExtState | null; + devantConsoleUrl: string; + platformRpcClient?: PlatformExtRpcClient; + onLinkDevantProject: () => void; + loginToDevant: () => void; + importConnection: { + connection?: ConnectionListItem; + setConnection: (item?: ConnectionListItem) => void; + }; +} = { + platformExtState: { components: [], isLoggedIn: false, userInfo: null }, + onLinkDevantProject: () => {}, + loginToDevant: () => {}, + devantConsoleUrl: "", + importConnection: { setConnection: () => {} }, +}; + +const PlatformExtContext = React.createContext(defaultPlatformExtContext); + +export const usePlatformExtContext = () => { + return useContext(PlatformExtContext) || defaultPlatformExtContext; +}; + +export const PlatformExtContextProvider: FC<{ children: ReactNode }> = ({ children }) => { + const queryClient = useQueryClient(); + const { rpcClient } = useRpcContext(); + const platformRpcClient = rpcClient.getPlatformRpcClient(); + const [importingConn, setImportingConn] = useState(); + + const { data: platformExtState } = useQuery({ + queryKey: ["platform-ext-state"], + queryFn: () => platformRpcClient.getPlatformStore(), + }); + + useEffect(() => { + platformRpcClient?.onPlatformExtStoreStateChange((state) => { + queryClient.setQueryData(["platform-ext-state"], state); + }); + }, []); + + const { data: devantConsoleUrl = "" } = useQuery({ + queryKey: ["devant-url"], + queryFn: () => platformRpcClient.getDevantConsoleUrl(), + }); + + const loginToDevant = () => { + rpcClient.getCommonRpcClient().executeCommand({ + commands: [ + PlatformExtCommandIds.SignIn, + { extName: "Devant" } as ICmdParamsBase, + ], + }) + } + + const onLinkDevantProject = () => { + if (!platformExtState?.isLoggedIn && platformExtState?.hasPossibleComponent) { + rpcClient + .getCommonRpcClient() + .showInformationModal({ + message: "Please login to Devant in order to use Devant Connections", + items: ["Login"], + }) + .then((resp) => { + if (resp === "Login") { + platformRpcClient.deployIntegrationInDevant(); + } else if (resp === "Associate Project") { + loginToDevant(); + } + }); + } else { + rpcClient + .getCommonRpcClient() + .showInformationModal({ + message: + "To use Devant connections, you can either deploy your source code now or associate this directory with an existing Devant project where you plan to deploy later.", + items: ["Deploy Now", "Associate Project"], + }) + .then(async (resp) => { + if (resp === "Deploy Now") { + platformRpcClient.deployIntegrationInDevant(); + } else if (resp === "Associate Project") { + const visualizerLocation = await rpcClient.getVisualizerLocation(); + rpcClient.getCommonRpcClient().executeCommand({ + commands: [ + PlatformExtCommandIds.CreateDirectoryContext, + { + extName: "Devant", + skipComponentExistCheck: true, + fsPath: visualizerLocation.workspacePath || visualizerLocation?.projectPath, + } as ICreateDirCtxCmdParams, + ], + }); + } + }); + } + }; + + return ( + setImportingConn(item), connection: importingConn }, + }} + > + {children} + + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/providers/react-query-provider.tsx b/workspaces/ballerina/ballerina-visualizer/src/providers/react-query-provider.tsx new file mode 100644 index 0000000000..266bdf9063 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/providers/react-query-provider.tsx @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2026, 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 { QueryClient, DehydratedState } from "@tanstack/react-query"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import React from "react"; + +interface PersistedClient { + timestamp: number; + buster: string; + clientState: DehydratedState; +} + +const webviewStatePersister = (queryBaseKey: string) => { + const { rpcClient } = useRpcContext(); + return { + persistClient: async (client: PersistedClient) => { + await rpcClient.getCommonRpcClient().setWebviewCache({ cacheKey: queryBaseKey, data: client }); + }, + restoreClient: async () => { + const cache = await rpcClient.getCommonRpcClient().restoreWebviewCache(queryBaseKey); + return cache; + }, + removeClient: async () => { + await rpcClient.getCommonRpcClient().clearWebviewCache(queryBaseKey); + }, + }; +}; + +export const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx b/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx index a442940455..730e5c79d7 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx @@ -73,7 +73,7 @@ import { cloneDeep } from "lodash"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import hljs from "highlight.js"; -import { COMPLETION_ITEM_KIND, CompletionItem, CompletionItemKind, convertCompletionItemKind, FnSignatureDocumentation } from "@wso2/ui-toolkit"; +import { COMPLETION_ITEM_KIND, CompletionItem, CompletionItemKind, convertCompletionItemKind, FnSignatureDocumentation, VSCodeColors } from "@wso2/ui-toolkit"; import { FunctionDefinition, STNode } from "@wso2/syntax-tree"; import { DocSection } from "../components/ExpressionEditor"; @@ -81,6 +81,7 @@ import { DocSection } from "../components/ExpressionEditor"; import ballerina from "../languages/ballerina.js"; import { FUNCTION_REGEX } from "../resources/constants"; import { ConnectionKind, getConnectionKindConfig } from "../components/ConnectionSelector"; +import { ConnectionListItem } from "@wso2/wso2-platform-core"; hljs.registerLanguage("ballerina", ballerina); export const BALLERINA_INTEGRATOR_ISSUES_URL = "https://github.com/wso2/product-ballerina-integrator/issues"; @@ -90,7 +91,7 @@ function convertAvailableNodeToPanelNode(node: AvailableNode, functionType?: FUN if (functionType === FUNCTION_TYPE.REGULAR && (node.metadata.data as NodeMetadata)?.isDataMappedFunction) { return undefined; } - if (functionType === FUNCTION_TYPE.EXPRESSION_BODIED && !(node.metadata.data as NodeMetadata).isDataMappedFunction) { + if (functionType === FUNCTION_TYPE.EXPRESSION_BODIED && !(node.metadata.data as NodeMetadata)?.isDataMappedFunction) { return undefined; } @@ -111,19 +112,23 @@ function convertAvailableNodeToPanelNode(node: AvailableNode, functionType?: FUN } function convertDiagramCategoryToSidePanelCategory(category: Category, functionType?: FUNCTION_TYPE): PanelCategory { - if (category.metadata.label !== "Current Integration" && functionType === FUNCTION_TYPE.EXPRESSION_BODIED) { - // Skip out of scope data mapping functions - return; - } const items: PanelItem[] = category.items ?.map((item) => { if ("codedata" in item) { return convertAvailableNodeToPanelNode(item as AvailableNode, functionType); } else { - return convertDiagramCategoryToSidePanelCategory(item as Category); + return convertDiagramCategoryToSidePanelCategory(item as Category, functionType); } }) - .filter((item) => item !== undefined); + .filter((item) => { + if (item === undefined) { + return false; + } + if ((item as PanelCategory).items !== undefined) { + return (item as PanelCategory).items.length > 0; + } + return true; + }); // HACK: use the icon of the first item in the category const icon = category.items.at(0)?.metadata.icon; @@ -138,6 +143,43 @@ function convertDiagramCategoryToSidePanelCategory(category: Category, functionT }; } +/** Map devant connection details with BI connection and to figure out which Devant connection are not used */ +export function enrichCategoryWithDevant( + connections: ConnectionListItem[] = [], + panelCategories: PanelCategory[] = [], + importingConn?: ConnectionListItem +): PanelCategory[] { + const updated = panelCategories?.map((category) => { + if (category.title === "Connections") { + const usedConnIds: string[] = []; + const mappedCategoryItems = category.items?.map((categoryItem) => { + const matchingDevantConn = connections.find((conn) => conn.name?.replaceAll("-", "_").replaceAll(" ", "_") === (categoryItem as PanelCategory)?.title) + if(matchingDevantConn) { + usedConnIds.push(matchingDevantConn.groupUuid); + return { ...categoryItem, devant: matchingDevantConn, unusedDevantConn: false } + } + return categoryItem; + }); + const unusedCategoryItems: PanelCategory[] = connections + .filter((conn) => !usedConnIds.includes(conn.groupUuid)) + .map((conn) => ({ + title: conn.name?.replaceAll("-","_").replaceAll(" ","_"), + items: [] as PanelItem[], + description: "Unused Devant connection", + devant: conn, + unusedDevantConn: true, + isLoading: importingConn?.name === conn.name, + })); + return { + ...category, + items: [...mappedCategoryItems, ...unusedCategoryItems], + }; + } + return category; + }); + return updated; +} + export function convertBICategoriesToSidePanelCategories(categories: Category[]): PanelCategory[] { const panelCategories = categories.map((category) => convertDiagramCategoryToSidePanelCategory(category)); const connectorCategory = panelCategories.find((category) => category.title === "Connections"); @@ -775,6 +817,17 @@ export function convertToVisibleTypes(types: VisibleTypeItem[], isFetchingTypesF })); } +export function convertItemsToCompletionItems(items: Item[]): CompletionItem[] { + items = items.filter(item => item !== null) as Item[]; + //TODO: Need labelDetails from the LS for proper conversion + return items.map((item) => ({ + label: item.metadata.label, + value: item.metadata.label, + kind: COMPLETION_ITEM_KIND.TypeParameter, + insertText: item.metadata.label + })); +} + export function convertRecordTypeToCompletionItem(type: Type): CompletionItem { const label = type?.name ?? ""; const value = label; @@ -856,9 +909,9 @@ const isCategoryType = (item: Item): item is Category => { }; export const getFunctionItemKind = (category: string): FunctionKind => { - if (category.includes("Current")) { + if (category.toLocaleLowerCase().includes("current")) { return functionKinds.CURRENT; - } else if (category.includes("Imported")) { + } else if (category.toLocaleLowerCase().includes("imported")) { return functionKinds.IMPORTED; } else { return functionKinds.AVAILABLE; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/AIPanel.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/AIPanel.tsx index dda9ebfc2e..f530a20507 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/AIPanel.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/AIPanel.tsx @@ -78,11 +78,11 @@ const AIPanel = (props: { state: AIMachineStateValue }) => { if (subState === "determineFlow") { component = ; - } else if (["ssoFlow", "apiKeyFlow", "validatingApiKey", "awsBedrockFlow", "validatingAwsCredentials"].includes(subState)) { + } else if (["ssoFlow", "apiKeyFlow", "validatingApiKey", "awsBedrockFlow", "validatingAwsCredentials", "vertexAiFlow", "validatingVertexAiCredentials"].includes(subState)) { component = ( ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/LoginPanel/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/LoginPanel/index.tsx index a17d9a1523..7cf7e3b1f2 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/LoginPanel/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/LoginPanel/index.tsx @@ -21,7 +21,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { AIMachineEventType } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { Icon, Typography } from "@wso2/ui-toolkit"; -import React from "react"; +import React, { useEffect, useState } from "react"; const PanelWrapper = styled.div` display: flex; @@ -130,6 +130,16 @@ const LegalNotice: React.FC = () => { const LoginPanel: React.FC = () => { const { rpcClient } = useRpcContext(); + const [isPlatformAvailable, setIsPlatformAvailable] = useState(true); + + useEffect(() => { + // Check if platform extension is available on mount + rpcClient.getAiPanelRpcClient().isPlatformExtensionAvailable().then((available) => { + setIsPlatformAvailable(available); + }).catch(() => { + setIsPlatformAvailable(false); + }); + }, [rpcClient]); const handleCopilotLogin = () => { rpcClient.sendAIStateEvent(AIMachineEventType.LOGIN); @@ -143,6 +153,10 @@ const LoginPanel: React.FC = () => { rpcClient.sendAIStateEvent(AIMachineEventType.AUTH_WITH_AWS_BEDROCK); }; + const handleVertexAiClick = () => { + rpcClient.sendAIStateEvent(AIMachineEventType.AUTH_WITH_VERTEX_AI); + }; + return ( @@ -168,10 +182,13 @@ const LoginPanel: React.FC = () => { - Login to BI Copilot - or + {isPlatformAvailable && ( + Login using Devant + )} + {isPlatformAvailable && or} Enter your Anthropic API key Enter your AWS Bedrock credentials + Enter your Google Vertex AI credentials ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/SettingsPanel/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/SettingsPanel/index.tsx index b1622c1105..9961fdfce0 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/SettingsPanel/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/SettingsPanel/index.tsx @@ -21,7 +21,7 @@ import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { Button, Codicon, Typography } from "@wso2/ui-toolkit"; import { AIChatView } from "../styles"; -import { AIMachineEventType, LoginMethod } from "@wso2/ballerina-core"; +import { AIMachineEventType } from "@wso2/ballerina-core"; const Container = styled.div` display: flex; @@ -62,7 +62,6 @@ export const SettingsPanel = (props: { onClose: () => void }) => { const { rpcClient } = useRpcContext(); const [copilotAuthorized, setCopilotAuthorized] = React.useState(false); - const [shouldShowLogoutButton, setShouldShowLogoutButton] = React.useState(true); const messagesEndRef = createRef(); @@ -70,14 +69,6 @@ export const SettingsPanel = (props: { onClose: () => void }) => { isCopilotAuthorized().then((authorized) => { setCopilotAuthorized(authorized); }); - - rpcClient - .getAiPanelRpcClient() - .getLoginMethod() - .then((loginMethod) => { - console.log("Login Method: ", loginMethod); - setShouldShowLogoutButton(loginMethod !== LoginMethod.DEVANT_ENV); - }); }, []); const handleCopilotLogout = () => { @@ -109,18 +100,16 @@ export const SettingsPanel = (props: { onClose: () => void }) => { Connect to AI Platforms for Enhanced Features - {shouldShowLogoutButton && ( - - - Logout from BI Copilot - - Logging out will end your session and disconnect access to AI-powered tools like code - generation, completions, test generation, and data mappings. - - - - - )} + + + Logout from BI Copilot + + Logging out will end your session and disconnect access to AI-powered tools like code + generation, completions, test generation, and data mappings. + + + + Enable GitHub Copilot Integration diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/WaitingForLoginSection/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/WaitingForLoginSection/index.tsx index 2c031f4125..2215104403 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/WaitingForLoginSection/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/WaitingForLoginSection/index.tsx @@ -189,6 +189,14 @@ const WaitingForLogin = ({ loginMethod, isValidating = false, errorMessage }: Wa const [showAccessKey, setShowAccessKey] = useState(false); const [showSecretKey, setShowSecretKey] = useState(false); const [showSessionToken, setShowSessionToken] = useState(false); + const [vertexAiCredentials, setVertexAiCredentials] = useState({ + projectId: "", + location: "", + clientEmail: "", + privateKey: "" + }); + const [showClientEmail, setShowClientEmail] = useState(false); + const [showPrivateKey, setShowPrivateKey] = useState(false); const cancelLogin = () => { rpcClient.sendAIStateEvent(AIMachineEventType.CANCEL_LOGIN); @@ -245,6 +253,36 @@ const WaitingForLogin = ({ loginMethod, isValidating = false, errorMessage }: Wa setShowSessionToken(!showSessionToken); }; + const connectWithVertexAi = () => { + if (vertexAiCredentials.projectId.trim() && vertexAiCredentials.location.trim() && + vertexAiCredentials.clientEmail.trim() && vertexAiCredentials.privateKey.trim()) { + rpcClient.sendAIStateEvent({ + type: AIMachineEventType.SUBMIT_VERTEX_AI_CREDENTIALS, + payload: { + projectId: vertexAiCredentials.projectId.trim(), + location: vertexAiCredentials.location.trim(), + clientEmail: vertexAiCredentials.clientEmail.trim(), + privateKey: vertexAiCredentials.privateKey.trim() + }, + }); + } + }; + + const handleVertexAiCredentialChange = (field: keyof typeof vertexAiCredentials) => (e: any) => { + setVertexAiCredentials(prev => ({ + ...prev, + [field]: e.target.value + })); + }; + + const toggleClientEmailVisibility = () => { + setShowClientEmail(!showClientEmail); + }; + + const togglePrivateKeyVisibility = () => { + setShowPrivateKey(!showPrivateKey); + }; + if (loginMethod === LoginMethod.ANTHROPIC_KEY) { return ( @@ -417,6 +455,113 @@ const WaitingForLogin = ({ loginMethod, isValidating = false, errorMessage }: Wa ); } + if (loginMethod === LoginMethod.VERTEX_AI) { + const isFormValid = vertexAiCredentials.projectId.trim() && + vertexAiCredentials.location.trim() && + vertexAiCredentials.clientEmail.trim() && + vertexAiCredentials.privateKey.trim(); + + return ( + + + Connect with Google Vertex AI + + Enter your GCP service account credentials to connect to BI Copilot via Google Vertex AI. Your credentials will be securely stored + and used for authentication. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {errorMessage && ( + + + {errorMessage} + + )} + + + + {isValidating ? "Validating..." : "Connect with Vertex AI"} + + + Cancel + + + + + ); + } + // Default: BI_INTEL login method return ( diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsx index d162d30cb3..e0e7f64c54 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsx @@ -26,6 +26,7 @@ import { commandTemplates, suggestedCommandTemplates } from "../../../commandTem import { AttachmentOptions } from "../../AIChatInput/hooks/useAttachments"; import { getTemplateTextById } from "../../../commandTemplates/utils/utils"; import CodeContextCard from "../../CodeContextCard"; +import { AgentMode } from "../../AIChatInput/ModeToggle"; export const FooterContainer = styled.footer({ padding: "20px", @@ -129,6 +130,10 @@ type FooterProps = { showSuggestedCommands: boolean; codeContext?: CodeContext; onRemoveCodeContext?: () => void; + agentMode?: AgentMode; + onChangeAgentMode?: (mode: AgentMode) => void; + isAutoApproveEnabled?: boolean; + onDisableAutoApprove?: () => void; }; const Footer: React.FC = ({ @@ -142,6 +147,10 @@ const Footer: React.FC = ({ showSuggestedCommands, codeContext, onRemoveCodeContext, + agentMode, + onChangeAgentMode, + isAutoApproveEnabled, + onDisableAutoApprove, }) => { const [generatingText, setGeneratingText] = useState("Generating."); @@ -187,6 +196,10 @@ const Footer: React.FC = ({ onSend={onSend} onStop={onStop} isLoading={isLoading} + agentMode={agentMode} + onChangeAgentMode={onChangeAgentMode} + isAutoApproveEnabled={isAutoApproveEnabled} + onDisableAutoApprove={onDisableAutoApprove} /> ); 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 5bf0dfafc9..ead4f6b2d0 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 @@ -43,6 +43,7 @@ import { Button, Codicon } from "@wso2/ui-toolkit"; import { AIChatInputRef } from "../AIChatInput"; import ProgressTextSegment from "../ProgressTextSegment"; import ToolCallSegment from "../ToolCallSegment"; +import ToolCallGroupSegment, { ToolCallItem } from "../ToolCallGroupSegment"; import TodoSection from "../TodoSection"; import { ConnectorGeneratorSegment } from "../ConnectorGeneratorSegment"; import { ConfigurationCollectorSegment, ConfigurationCollectionData } from "../ConfigurationCollectorSegment"; @@ -70,6 +71,7 @@ import { SYSTEM_ERROR_SECRET } from "../AIChatInput/constants"; import { CodeSegment } from "../CodeSegment"; import AttachmentBox, { AttachmentsContainer } from "../AttachmentBox"; import Footer from "./Footer"; +import { AgentMode } from "../AIChatInput/ModeToggle"; import ApprovalFooter from "./Footer/ApprovalFooter"; import { useFooterLogic } from "./Footer/useFooterLogic"; import { SettingsPanel } from "../../SettingsPanel"; @@ -143,7 +145,7 @@ const AIChat: React.FC = () => { const [showSettings, setShowSettings] = useState(false); const [isAutoApproveEnabled, setIsAutoApproveEnabled] = useState(false); - const [isPlanModeEnabled, setIsPlanModeEnabled] = useState(false); + const [agentMode, setAgentMode] = useState(AgentMode.Edit); const [isPlanModeFeatureEnabled, setIsPlanModeFeatureEnabled] = useState(false); const [showReviewActions, setShowReviewActions] = useState(false); const [availableCheckpointIds, setAvailableCheckpointIds] = useState>(new Set()); @@ -201,7 +203,7 @@ const AIChat: React.FC = () => { // Handle plan mode for text-type prompts if (defaultPrompt.type === 'text') { - setIsPlanModeEnabled(defaultPrompt.planMode); + setAgentMode(defaultPrompt.planMode ? AgentMode.Plan : AgentMode.Edit); } } }); @@ -374,19 +376,19 @@ const AIChat: React.FC = () => { : "Searching for libraries..."; updateLastMessage((content) => - content + `\n\n${displayMessage}` + content + `\n\n${displayMessage}` ); } else if (response.toolName === "LibraryGetTool") { const toolCallId = response?.toolCallId; updateLastMessage((content) => - content + `\n\nFetching library details...` + content + `\n\nFetching library details...` ); } else if (response.toolName == "HealthcareLibraryProviderTool") { setMessages((prevMessages) => { const newMessages = [...prevMessages]; if (newMessages.length > 0) { - newMessages[newMessages.length - 1].content += `\n\nAnalyzing request & selecting healthcare libraries...`; + newMessages[newMessages.length - 1].content += `\n\nAnalyzing request & selecting healthcare libraries...`; } return newMessages; }); @@ -394,7 +396,7 @@ const AIChat: React.FC = () => { setMessages((prevMessages) => { const newMessages = [...prevMessages]; if (newMessages.length > 0) { - newMessages[newMessages.length - 1].content += `\n\nPlanning...`; + newMessages[newMessages.length - 1].content += `\n\nPlanning...`; } return newMessages; }); @@ -408,7 +410,7 @@ const AIChat: React.FC = () => { setMessages((prevMessages) => { const newMessages = [...prevMessages]; if (newMessages.length > 0) { - newMessages[newMessages.length - 1].content += `\n\n${message}`; + newMessages[newMessages.length - 1].content += `\n\n${message}`; } return newMessages; }); @@ -416,10 +418,15 @@ const AIChat: React.FC = () => { setMessages((prevMessages) => { const newMessages = [...prevMessages]; if (newMessages.length > 0) { - newMessages[newMessages.length - 1].content += `\n\nChecking for errors...`; + newMessages[newMessages.length - 1].content += `\n\nChecking for errors...`; } return newMessages; }); + } else if (response.toolName === "runTests") { + const toolCallId = response?.toolCallId; + updateLastMessage((content) => + content + `\n\nRunning tests...` + ); } } else if (type === "tool_result") { if (response.toolName === "LibrarySearchTool") { @@ -439,29 +446,29 @@ const AIChat: React.FC = () => { updateLastMessage((content) => content.replace( - `${originalMessage}`, - `${completionMessage}` + `${originalMessage}`, + `${completionMessage}` ) ); } else if (response.toolName === "LibraryGetTool") { const toolCallId = response.toolCallId; const libraryNames = response.toolOutput || []; if (toolCallId) { - const searchPattern = `Fetching library details...`; + const searchPattern = `Fetching library details...`; const resultMessage = libraryNames.length === 0 ? "No relevant libraries found" : `Fetched libraries: [${libraryNames.join(", ")}]`; - const replacement = `${resultMessage}`; + const replacement = `${resultMessage}`; updateLastMessage((content) => content.replace(searchPattern, replacement)); } } else if (response.toolName == "HealthcareLibraryProviderTool") { const libraryNames = response.toolOutput; - const searchPattern = `Analyzing request & selecting healthcare libraries...`; + const searchPattern = `Analyzing request & selecting healthcare libraries...`; const resultMessage = libraryNames.length === 0 ? "No relevant healthcare libraries found." : `Fetched healthcare libraries: [${libraryNames.join(", ")}]`; - const replacement = `${resultMessage}`; + const replacement = `${resultMessage}`; updateLastMessage((content) => content.replace(searchPattern, replacement)); } else if (response.toolName == "TaskWrite") { @@ -470,11 +477,11 @@ const AIChat: React.FC = () => { setMessages((prevMessages) => { const newMessages = [...prevMessages]; if (newMessages.length > 0) { - if (!taskOutput.success || !taskOutput.allTasks || taskOutput.allTasks.length === 0) { + if (!taskOutput.success || !taskOutput.tasks || taskOutput.tasks.length === 0) { const isInternalError = taskOutput.message && taskOutput.message.includes("ERROR: Missing"); - const indicatorPattern = /Planning\.\.\.<\/toolcall>/; + const indicatorPattern = /Planning\.\.\.<\/toolcall>/; const todoPattern = /.*?<\/todo>/s; if (isInternalError) { @@ -495,13 +502,14 @@ const AIChat: React.FC = () => { } } + // Keep tool="TaskWrite" (matching the tool_call tag written by the TaskWrite handler) newMessages[newMessages.length - 1].content = newMessages[ newMessages.length - 1 - ].content.replace(indicatorPattern, `${simplifiedMessage}`).replace(todoPattern, ""); + ].content.replace(indicatorPattern, `${simplifiedMessage}`).replace(todoPattern, ""); } } else { const todoData = { - tasks: taskOutput.allTasks, + tasks: taskOutput.tasks, message: taskOutput.message }; const todoJson = JSON.stringify(todoData); @@ -528,8 +536,8 @@ const AIChat: React.FC = () => { const newMessages = [...prevMessages]; if (newMessages.length > 0) { const lastMessageContent = newMessages[newMessages.length - 1].content; - const creatingPattern = /Creating (.+?)\.\.\.<\/toolcall>/; - const updatingPattern = /Updating (.+?)\.\.\.<\/toolcall>/; + const creatingPattern = /Creating (.+?)\.\.\.<\/toolcall>/; + const updatingPattern = /Updating (.+?)\.\.\.<\/toolcall>/; let updatedContent = lastMessageContent; @@ -539,12 +547,12 @@ const AIChat: React.FC = () => { const resultText = action === 'updated' ? 'Updated' : 'Created'; updatedContent = lastMessageContent.replace( creatingPattern, - (_match, fileName) => `${resultText} ${fileName}` + (_match, toolName, fileName) => `${resultText} ${fileName}` ); } else if (updatingPattern.test(lastMessageContent)) { updatedContent = lastMessageContent.replace( updatingPattern, - (_match, fileName) => `Updated ${fileName}` + (_match, toolName, fileName) => `Updated ${fileName}` ); } @@ -562,7 +570,8 @@ const AIChat: React.FC = () => { const newMessages = [...prevMessages]; if (newMessages.length > 0) { const lastMessageContent = newMessages[newMessages.length - 1].content; - const checkingPattern = /Checking for errors\.\.\.<\/toolcall>/; + const toolName = response.toolName; + const checkingPattern = new RegExp(`Checking for errors\\.\\.\\.<\\/toolcall>`); const message = errorCount === 0 ? "No errors found" @@ -570,15 +579,47 @@ const AIChat: React.FC = () => { const updatedContent = lastMessageContent.replace( checkingPattern, - `${message}` + `${message}` ); newMessages[newMessages.length - 1].content = updatedContent; } return newMessages; }); + } else if (response.toolName === "runTests") { + const toolCallId = response.toolCallId; + if (toolCallId) { + const searchPattern = `Running tests...`; + const resultMessage = response.toolOutput?.summary ?? "Tests completed"; + const replacement = `${resultMessage}`; + updateLastMessage((content) => content.replace(searchPattern, replacement)); + } } } else if (type === "task_approval_request") { + if (response.approvalType === "plan") { + const todoJson = JSON.stringify({ tasks: response.tasks, message: response.message }); + updateLastMessage((content) => { + const cleaned = content + .replace(/Planning\.\.\.<\/toolcall>/, '') + .replace(/.*?<\/todo>/s, ''); + return cleaned + `\n\n${todoJson}`; + }); + } else if (response.approvalType === "completion") { + const tasks = isAutoApproveEnabled + ? response.tasks.map((t: { status: string }) => + t.status === "review" ? { ...t, status: "completed" } : t) + : response.tasks; + const todoJson = JSON.stringify({ tasks, message: response.message }); + updateLastMessage((content) => + content.replace(/.*?<\/todo>/s, `${todoJson}`) + ); + } + + if (isAutoApproveEnabled && response.approvalType === "completion") { + await rpcClient.getAiPanelRpcClient().approveTask({ requestId: response.requestId }); + return; + } + setApprovalRequest({ type: "task_approval_request", requestId: response.requestId, @@ -587,46 +628,6 @@ const AIChat: React.FC = () => { taskDescription: response.taskDescription, message: response.message, }); - if (response.approvalType === "plan") { - setMessages((prevMessages) => { - const newMessages = [...prevMessages]; - if (newMessages.length > 0) { - const todoData = { - tasks: response.tasks, - message: response.message - }; - const todoJson = JSON.stringify(todoData); - let lastMessageContent = newMessages[newMessages.length - 1].content; - - const planningPattern = /Planning\.\.\.<\/toolcall>/; - const todoPattern = /.*?<\/todo>/s; - - lastMessageContent = lastMessageContent.replace(planningPattern, ''); - lastMessageContent = lastMessageContent.replace(todoPattern, ''); - - newMessages[newMessages.length - 1].content = lastMessageContent + `\n\n${todoJson}`; - } - return newMessages; - }); - } else if (response.approvalType === "completion") { - setMessages((prevMessages) => { - const newMessages = [...prevMessages]; - if (newMessages.length > 0) { - const todoData = { - tasks: response.tasks, - message: response.message - }; - const todoJson = JSON.stringify(todoData); - let lastMessageContent = newMessages[newMessages.length - 1].content; - - const todoPattern = /.*?<\/todo>/s; - lastMessageContent = lastMessageContent.replace(todoPattern, `${todoJson}`); - - newMessages[newMessages.length - 1].content = lastMessageContent; - } - return newMessages; - }); - } } else if (type === "intermediary_state") { const state = response.state; // Check if it's a documentation state by looking for specific properties @@ -1220,9 +1221,9 @@ const AIChat: React.FC = () => { content: file.content, })); - console.log("Submitting agent prompt:", { useCase, isPlanModeEnabled, codeContext, operationType, fileAttatchments }); + console.log("Submitting agent prompt:", { useCase, agentMode, codeContext, operationType, fileAttatchments }); rpcClient.getAiPanelRpcClient().generateAgent({ - usecase: useCase, isPlanMode: isPlanModeEnabled, codeContext: codeContext, operationType, fileAttachmentContents: fileAttatchments + usecase: useCase, isPlanMode: agentMode === AgentMode.Plan, codeContext: codeContext, operationType, fileAttachmentContents: fileAttatchments }) } @@ -1251,9 +1252,8 @@ const AIChat: React.FC = () => { setIsAutoApproveEnabled(newValue); }; - const handleTogglePlanMode = () => { - const newValue = !isPlanModeEnabled; - setIsPlanModeEnabled(newValue); + const handleChangeAgentMode = (mode: AgentMode) => { + setAgentMode(mode); }; const questionMessages = messages.filter((message) => message.type === "question"); @@ -1412,26 +1412,6 @@ const AIChat: React.FC = () => { {/* {`Resets in: 30 days`} */} - {isPlanModeFeatureEnabled && ( - - )} - {isPlanModeFeatureEnabled && ( - - )} + {props.testsConfigTomlPath && ( + + )} } /> {isAddConfigVariableFormOpen && } @@ -366,14 +488,14 @@ export function ViewConfigurableVariables(props?: ConfigProps) { />
- {isLoading && } + {isLoading &&
} {!isLoading && {/* Left side tree view */}
{/* Display integration category first */} - {(searchValue ? filteredCategoriesWithModules : categoriesWithModules) + {activeCategories .filter(category => category.name === integrationCategory.current) - .map((category, index) => ( + .map((category) => ( category.name !== integrationCategory.current).length > 0 && ( {/* Map all non-integration categories */} - {(searchValue ? filteredCategoriesWithModules : categoriesWithModules) + {activeCategories .filter(category => category.name !== integrationCategory.current) - .map((category, index) => ( + .map((category) => ( {!renderVariables ? - : searchValue && filteredCategoriesWithModules.length === 0 ? + : searchValue && activeCategories.length === 0 ? 0 && - selectedModule.category === integrationCategory.current && ( + (selectedModule.category === integrationCategory.current) && ( + {selectedModule.category === integrationCategory.current && ( + + )} )}
diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ConfigurationCollector/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ConfigurationCollector/index.tsx index c62bfb3cae..982b2acdf2 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ConfigurationCollector/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ConfigurationCollector/index.tsx @@ -62,14 +62,22 @@ const FieldDescription = styled.span` font-weight: normal; `; -const FieldInput = styled.input<{ hasError: boolean }>` - padding: 10px 12px; +const FieldInputWrapper = styled.div` + position: relative; + display: flex; + align-items: center; +`; + +const FieldInput = styled.input<{ hasError: boolean; hasToggle?: boolean }>` + width: 100%; + padding: 10px ${(props: { hasError: boolean; hasToggle?: boolean }) => props.hasToggle ? "36px" : "12px"} 10px 12px; background-color: ${ThemeColors.SURFACE_DIM}; color: ${ThemeColors.ON_SURFACE}; - border: 1px solid ${(props: { hasError: boolean }) => + border: 1px solid ${(props: { hasError: boolean; hasToggle?: boolean }) => props.hasError ? ThemeColors.ERROR : ThemeColors.OUTLINE_VARIANT}; border-radius: 6px; font-size: 13px; + box-sizing: border-box; &:focus { outline: none; @@ -81,6 +89,22 @@ const FieldInput = styled.input<{ hasError: boolean }>` } `; +const ToggleVisibilityButton = styled.button` + position: absolute; + right: 8px; + background: none; + border: none; + padding: 2px; + cursor: pointer; + color: ${ThemeColors.ON_SURFACE_VARIANT}; + display: flex; + align-items: center; + + &:hover { + color: ${ThemeColors.ON_SURFACE}; + } +`; + const FieldError = styled.div` font-size: 12px; color: ${ThemeColors.ERROR}; @@ -106,6 +130,11 @@ export const ConfigurationCollector: React.FC = ({ const [configValues, setConfigValues] = useState>(data?.existingValues || {}); const [errors, setErrors] = useState>({}); const [isProcessing, setIsProcessing] = useState(false); + const [visibleFields, setVisibleFields] = useState>({}); + + const toggleVisibility = (name: string) => { + setVisibleFields((prev) => ({ ...prev, [name]: !prev[name] })); + }; // Initialize configuration values when data prop changes useEffect(() => { @@ -239,27 +268,44 @@ export const ConfigurationCollector: React.FC = ({ - {data.variables?.map((variable) => ( - - - {variable.name} - {variable.description && ( - - {variable.description} - )} - - handleInputChange(variable.name, e.target.value)} - onKeyDown={handleKeyDown} - hasError={!!errors[variable.name]} - /> - {errors[variable.name] && {errors[variable.name]}} - - ))} + {data.variables?.map((variable) => { + const isSecret = variable.secret === true; + const isVisible = visibleFields[variable.name]; + const inputType = isSecret + ? (isVisible ? "text" : "password") + : (variable.type === "int" ? "number" : "text"); + return ( + + + {variable.name} + {variable.description && ( + - {variable.description} + )} + + + handleInputChange(variable.name, e.target.value)} + onKeyDown={handleKeyDown} + hasError={!!errors[variable.name]} + hasToggle={isSecret} + /> + {isSecret && ( + toggleVisibility(variable.name)} + title={isVisible ? "Hide value" : "Show value"} + > + + + )} + + {errors[variable.name] && {errors[variable.name]}} + + ); + })} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/APIConnectionPopup/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/APIConnectionPopup/index.tsx index c656ca7c1f..a958e1d32e 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/APIConnectionPopup/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/APIConnectionPopup/index.tsx @@ -44,7 +44,7 @@ const ContentContainer = styled.div<{ hasFooterButton?: boolean }>` const FooterContainer = styled.div` position: sticky; bottom: 0; - padding: 20px 32px; + padding-top: 20px; display: flex; justify-content: center; align-items: center; @@ -54,6 +54,7 @@ const FooterContainer = styled.div` const StepContent = styled.div<{ fillHeight?: boolean }>` display: flex; flex-direction: column; + flex: 1; gap: 20px; ${(props: { fillHeight?: boolean }) => props.fillHeight && ` flex: 1; @@ -89,7 +90,7 @@ const FormField = styled.div` flex-direction: column; `; -const UploadCard = styled.div<{ hasFile?: boolean }>` +const UploadCard = styled.div<{ hasFile?: boolean; disabled?: boolean }>` display: flex; align-items: center; gap: 12px; @@ -97,15 +98,17 @@ const UploadCard = styled.div<{ hasFile?: boolean }>` border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; border-radius: 14px; background: ${ThemeColors.SURFACE_DIM}; - cursor: pointer; + cursor: ${(props:{ hasFile?: boolean; disabled?: boolean }) => props.disabled ? "not-allowed" : "pointer"}; transition: all 0.2s ease; - &:hover { - border-color: ${ThemeColors.PRIMARY}; - background: ${ThemeColors.SURFACE_CONTAINER}; - } + ${(props:{ hasFile?: boolean; disabled?: boolean }) => !props.disabled && ` + &:hover { + border-color: ${ThemeColors.PRIMARY}; + background: ${ThemeColors.SURFACE_CONTAINER}; + } + `} - ${(props: { hasFile?: boolean }) => + ${(props:{ hasFile?: boolean; disabled?: boolean }) => props.hasFile ? ` border-color: ${ThemeColors.PRIMARY}; @@ -182,23 +185,6 @@ const ErrorTitle = styled(Typography)` margin: 0; `; -const SeparatorLine = styled.div` - width: 100%; - height: 1px; - background-color: ${ThemeColors.OUTLINE_VARIANT}; - opacity: 0.5; -`; - -const BrowseMoreButton = styled(Button)` - margin-top: 0; - width: 100% !important; - display: flex; - justify-content: center; - align-items: center; - background-color: var(--vscode-button-secondaryBackground, #3c3c3c) !important; - color: var(--vscode-button-secondaryForeground, #ffffff) !important; -`; - interface APIConnectionPopupProps { projectPath: string; fileName: string; @@ -208,29 +194,184 @@ interface APIConnectionPopupProps { } export function APIConnectionPopup(props: APIConnectionPopupProps) { - const { projectPath, fileName, target, onBack, onClose } = props; + const { fileName, target, onBack, onClose, projectPath } = props; const { rpcClient } = useRpcContext(); const [currentStep, setCurrentStep] = useState(0); - const [specType, setSpecType] = useState("OpenAPI"); - const [selectedFilePath, setSelectedFilePath] = useState(""); - const [connectorName, setConnectorName] = useState(""); - const [isSavingConnector, setIsSavingConnector] = useState(false); const [isSavingConnection, setIsSavingConnection] = useState(false); - const [selectedFlowNode, setSelectedFlowNode] = useState(undefined); const [updatedExpressionField, setUpdatedExpressionField] = useState(undefined); - const [connectionError, setConnectionError] = useState(null); + const [selectedFlowNode, setSelectedFlowNode] = useState(undefined); + + const steps = useMemo(() => ["Import API Specification", "Create Connection"], []); + + const handleOnFormSubmit = async (node: FlowNode, _editorConfig?: EditorConfig, options?: FormSubmitOptions) => { + console.log(">>> on form submit", node); + if (selectedFlowNode) { + setIsSavingConnection(true); + const visualizerLocation = await rpcClient.getVisualizerLocation(); + let connectionsFilePath = visualizerLocation.documentUri || visualizerLocation.projectPath; + + if (node.codedata.isGenerated && !connectionsFilePath.endsWith(".bal")) { + connectionsFilePath += "/main.bal"; + } + + if (connectionsFilePath === "") { + console.error(">>> Error updating source code. No source file found"); + setIsSavingConnection(false); + return; + } + + // node property scope is local. then use local file path and line position + if ((node.properties?.scope?.value as string)?.toLowerCase() === "local") { + node.codedata.lineRange = { + fileName: visualizerLocation.documentUri, + startLine: target, + endLine: target, + }; + } + + // Check if the node is a connector + const isConnector = node.codedata.node === "NEW_CONNECTION"; + + rpcClient + .getBIDiagramRpcClient() + .getSourceCode({ + filePath: connectionsFilePath, + flowNode: node, + isConnector: isConnector, + }) + .then((response) => { + console.log(">>> Updated source code", response); + if (response.artifacts.length > 0) { + setIsSavingConnection(false); + const newConnection = response.artifacts.find((artifact) => artifact.isNew); + onClose?.({ recentIdentifier: newConnection?.name, artifactType: DIRECTORY_MAP.CONNECTION }); + } else { + console.error(">>> Error updating source code", response); + setIsSavingConnection(false); + } + }) + .catch((error) => { + console.error(">>> Error saving connection", error); + }).finally(() => { + setIsSavingConnection(false); + }); + } + }; + + const renderStepper = () => { + return ( + <> + + + + + ); + }; - const steps = useMemo(() => ["Import API Specification", "Create Connection"], []); + const renderConnectionStep = () => { + if (selectedFlowNode) { + return ( + +
+ Connection Details + + Configure connection settings + +
+ setUpdatedExpressionField(undefined)} + isPullingConnector={isSavingConnection} + footerActionButton={true} + /> +
+ ); + } + return ( + + + + Loading connector configuration... + + + + ); + }; - const apiSpecOptions = useMemo( - () => [ - { id: "openapi", value: "OpenAPI", content: "OpenAPI" }, - { id: "wsdl", value: "WSDL", content: "WSDL" }, - ], - [] + const renderStepContent = () => { + if (currentStep === 0) { + return ( + { + setSelectedFlowNode(flowNode); + setCurrentStep(1); + }} + /> + ); + } + return renderConnectionStep(); + }; + + return ( + <> + + + + + + + + Connect via API Specification + Import an API specification file to create a connection + + onClose?.()}> + + + + {renderStepper()} + {renderStepContent()} + + ); +} + +interface APIConnectionFormProps { + onSave: (availableNode: AvailableNode, selectedFlowNode: FlowNode, type: string, name: string, filePath: string) => void; + projectPath: string; + fileName: string; + target?: LinePosition; + apiSpecOptions?: {id: string; value: string; content: string;}[]; + disabled?: boolean; + initialName?: string; + initialFilePath?: string; + actionButtonText?: string; + availableNode?: AvailableNode; +} + +const defaultOptions = [ + { id: "openapi", value: "OpenAPI", content: "OpenAPI" }, + { id: "wsdl", value: "WSDL", content: "WSDL" }, +] + +export function APIConnectionForm(props: APIConnectionFormProps) { + const { onSave, fileName, target, projectPath, apiSpecOptions = defaultOptions, disabled, initialName = "", initialFilePath = "", actionButtonText = "Save Connector", availableNode } = props; + const { rpcClient } = useRpcContext(); + + const [specType, setSpecType] = useState("OpenAPI"); + const [selectedFilePath, setSelectedFilePath] = useState(initialFilePath); + const [connectorName, setConnectorName] = useState(initialName); + const [connectionError, setConnectionError] = useState(null); + const [isSavingConnector, setIsSavingConnector] = useState(false); const supportedFileFormats = useMemo(() => { const isOpenApi = specType.toLowerCase() === "openapi"; @@ -248,12 +389,6 @@ export function APIConnectionPopup(props: APIConnectionPopupProps) { } }; - const getFileName = (filePath: string) => { - if (!filePath) return ""; - const parts = filePath.split(/[/\\]/); - return parts[parts.length - 1]; - }; - const handleOnGenerateSubmit = async (specFilePath: string, module: string, specType: string) => { if (!rpcClient) { return { success: false, errorMessage: "RPC client not available" }; @@ -351,14 +486,13 @@ export function APIConnectionPopup(props: APIConnectionPopupProps) { filePath: fileName, id: createdConnector.codedata, }); - setSelectedFlowNode(nodeTemplateResponse.flowNode); + onSave(createdConnector, nodeTemplateResponse.flowNode, specType, connectorName, selectedFilePath); } else { console.warn(">>> Created connector not found in search results"); } } catch (error) { console.error(">>> Error finding created connector", error); } - setCurrentStep(1); } else { console.error(">>> Error generating connector:", generateResponse?.errorMessage); const errorMessage = generateResponse?.errorMessage || ""; @@ -371,14 +505,6 @@ export function APIConnectionPopup(props: APIConnectionPopupProps) { setIsSavingConnector(false); }; - const handleBrowseMoreConnectors = () => { - if (onBack) { - onBack(); - } else if (onClose) { - onClose(); - } - }; - const renderErrorDisplay = () => { if (!connectionError) return null; @@ -391,224 +517,90 @@ export function APIConnectionPopup(props: APIConnectionPopupProps) { {connectionError} - - - Or try using a pre-built connector: - - - - Browse Pre-built Connectors - - ); }; - const handleOnFormSubmit = async (node: FlowNode, _editorConfig?: EditorConfig, options?: FormSubmitOptions) => { - console.log(">>> on form submit", node); - if (selectedFlowNode) { - setIsSavingConnection(true); - const visualizerLocation = await rpcClient.getVisualizerLocation(); - let connectionsFilePath = visualizerLocation.documentUri || visualizerLocation.projectPath; - - if (node.codedata.isGenerated && !connectionsFilePath.endsWith(".bal")) { - connectionsFilePath += "/main.bal"; - } - - if (connectionsFilePath === "") { - console.error(">>> Error updating source code. No source file found"); - setIsSavingConnection(false); - return; - } - - // node property scope is local. then use local file path and line position - if ((node.properties?.scope?.value as string)?.toLowerCase() === "local") { - node.codedata.lineRange = { - fileName: visualizerLocation.documentUri, - startLine: target, - endLine: target, - }; - } - - // Check if the node is a connector - const isConnector = node.codedata.node === "NEW_CONNECTION"; - - rpcClient - .getBIDiagramRpcClient() - .getSourceCode({ - filePath: connectionsFilePath, - flowNode: node, - isConnector: isConnector, - }) - .then((response) => { - console.log(">>> Updated source code", response); - if (response.artifacts.length > 0) { - setIsSavingConnection(false); - const newConnection = response.artifacts.find((artifact) => artifact.isNew); - onClose?.({ recentIdentifier: newConnection.name, artifactType: DIRECTORY_MAP.CONNECTION }); - } else { - console.error(">>> Error updating source code", response); - setIsSavingConnection(false); - } - }) - .catch((error) => { - console.error(">>> Error saving connection", error); - }).finally(() => { - setIsSavingConnection(false); - }); - } - }; - - const renderStepper = () => { - return ( - <> - - - - - ); - }; - - const renderImportStep = () => ( - - - - Connector Configuration - - - Import API specification for the connector - - - {renderErrorDisplay()} - - - { - setSpecType(value); - setConnectionError(null); - }} - /> - - - - Connector Name - - - Name of the connector module to be generated - - { - setConnectorName(value); - setConnectionError(null); - }} - placeholder="Enter connector name" - /> - - - - Import Specification File - - - - - - - - {selectedFilePath ? selectedFilePath : "Choose file to import"} - - Supports {supportedFileFormats} files - - - - - - ); - - const renderConnectionStep = () => { - if (selectedFlowNode) { - return ( - -
- Connection Details - - Configure connection settings - -
- setUpdatedExpressionField(undefined)} - isPullingConnector={isSavingConnection} - footerActionButton={true} - /> -
- ); - } - return ( + return ( + <> - + + + Connector Configuration + - Loading connector configuration... + Import API specification for the connector + + {renderErrorDisplay()} + + + { + setSpecType(value); + setConnectionError(null); + }} + /> + + + + Connector Name + + + Name of the connector module to be generated + + { + setConnectorName(value); + setConnectionError(null); + }} + placeholder="Enter connector name" + disabled={disabled} + /> + + + + Import Specification File + + + + + + + + {selectedFilePath ? selectedFilePath : "Choose file to import"} + + Supports {supportedFileFormats} files + + + - ); - }; - - const renderStepContent = () => { - if (currentStep === 0) { - return renderImportStep(); - } - return renderConnectionStep(); - }; - - return ( - <> - - - - - - - - Connect via API Specification - Import an API specification file to create a connection - - onClose?.()}> - - - - {renderStepper()} - {renderStepContent()} - {currentStep === 0 && ( - - - {isSavingConnector ? "Saving..." : "Save Connector"} - - - )} - + + + {isSavingConnector ? "Saving..." : actionButtonText} + + ); + } export default APIConnectionPopup; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/AddConnectionPopupContent.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/AddConnectionPopupContent.tsx new file mode 100644 index 0000000000..aa6f3d0e29 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/AddConnectionPopupContent.tsx @@ -0,0 +1,473 @@ +/** + * 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, useMemo, useCallback } from "react"; +import { AvailableNode, Category, Item, LinePosition } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { Codicon, Icon, ThemeColors, Typography, ProgressRing } from "@wso2/ui-toolkit"; +import { cloneDeep, debounce } from "lodash"; +import ButtonCard from "../../../../components/ButtonCard"; +import { ConnectorIcon } from "@wso2/bi-diagram"; +import { BodyTinyInfo } from "../../../styles"; +import { ArrowIcon, ConnectorOptionButtons, ConnectorOptionCard, ConnectorOptionContent, ConnectorOptionDescription, ConnectorOptionIcon, ConnectorOptionTitle, ConnectorOptionTitleContainer, ConnectorsGrid, ConnectorTypeLabel, CreateConnectorOptions, FilterButton, FilterButtons, IntroText, SearchContainer, Section, SectionHeader, SectionTitle, StyledSearchBox } from "./styles"; +import { AddConnectionPopupProps } from "./index"; +import { usePlatformExtContext } from "../../../../providers/platform-ext-ctx-provider"; +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; + +interface Props extends AddConnectionPopupProps { + handleDatabaseConnection?: () => void; + handleApiSpecConnection?: () => void; + handleSelectConnector: (connector: AvailableNode, filteredCategories: Category[]) => void; + DevantServicesSection?: React.ComponentType<{ searchText: string }>; +} + +export function AddConnectionPopupContent(props: Props) { + const { fileName, target, handleDatabaseConnection, handleApiSpecConnection, handleSelectConnector, DevantServicesSection } = props; + const { rpcClient } = useRpcContext(); + const { platformExtState, loginToDevant } = usePlatformExtContext(); + + const [searchText, setSearchText] = useState(""); + const [connectors, setConnectors] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [fetchingInfo, setFetchingInfo] = useState(false); + const [filterType, setFilterType] = useState<"All" | "Standard" | "Organization">("All"); + + const fetchConnectors = useCallback((filter?: boolean) => { + setFetchingInfo(true); + const defaultPosition: LinePosition = { line: 0, offset: 0 }; + const position = target || defaultPosition; + rpcClient + .getBIDiagramRpcClient() + .search({ + position: { + startLine: position, + endLine: position, + }, + filePath: fileName, + queryMap: { + limit: 60, + filterByCurrentOrg: filter ?? filterType === "Organization", + }, + searchKind: "CONNECTOR", + }) + .then(async (model) => { + console.log(">>> bi connectors", model); + console.log(">>> bi filtered connectors", model.categories); + setConnectors(model.categories); + }) + .finally(() => { + setIsSearching(false); + setFetchingInfo(false); + }); + }, [rpcClient, target, fileName, filterType]); + + useEffect(() => { + setIsSearching(true); + fetchConnectors(); + }, []); + + const handleSearch = useCallback((text: string) => { + const defaultPosition: LinePosition = { line: 0, offset: 0 }; + const position = target || defaultPosition; + rpcClient + .getBIDiagramRpcClient() + .search({ + position: { + startLine: position, + endLine: position, + }, + filePath: fileName, + queryMap: { + q: text, + limit: 60, + filterByCurrentOrg: filterType === "Organization" ? true : false, + }, + searchKind: "CONNECTOR", + }) + .then(async (model) => { + console.log(">>> bi searched connectors", model); + console.log(">>> bi filtered connectors", model.categories); + + // When searching, the API might return a flat array of connectors instead of categories + // Check if categories exist and have the proper structure (with items arrays) + let normalizedCategories: Category[] = []; + + if (model.categories && Array.isArray(model.categories)) { + // Check if the first item is a category (has items) or a connector (has codedata) + const firstItem = model.categories[0]; + if (firstItem && "items" in firstItem && Array.isArray(firstItem.items)) { + // Proper category structure - use as is + normalizedCategories = model.categories; + } else if (firstItem && "codedata" in firstItem) { + // Flat array of connectors - wrap in a category + normalizedCategories = [{ + metadata: { + label: "Search Results", + description: "" + }, + items: model.categories as unknown as AvailableNode[] + }]; + } + } + + console.log(">>> normalized categories for search", normalizedCategories); + setConnectors(normalizedCategories); + }) + .finally(() => { + setIsSearching(false); + }); + }, [rpcClient, target, fileName, filterType]); + + const debouncedSearch = useMemo( + () => debounce(handleSearch, 1100), + [handleSearch] + ); + + useEffect(() => { + setIsSearching(true); + debouncedSearch(searchText); + return () => debouncedSearch.cancel(); + }, [searchText, debouncedSearch]); + + useEffect(() => { + setIsSearching(true); + fetchConnectors(); + }, [filterType, fetchConnectors]); + + useEffect(() => { + rpcClient?.onProjectContentUpdated((state: boolean) => { + if (state) { + fetchConnectors(); + } + }); + }, [rpcClient, fetchConnectors]); + + const handleOnSearch = (text: string) => { + setSearchText(text); + }; + + const filterItems = (items: Item[]): Item[] => { + return items + .map((item) => { + if ("items" in item) { + const filteredItems = filterItems(item.items); + return { + ...item, + items: filteredItems, + }; + } else { + const lowerCaseTitle = item.metadata.label.toLowerCase(); + const lowerCaseDescription = item.metadata.description?.toLowerCase() || ""; + const lowerCaseSearchText = searchText.toLowerCase(); + if ( + lowerCaseTitle.includes(lowerCaseSearchText) || + lowerCaseDescription.includes(lowerCaseSearchText) + ) { + return item; + } + } + }) + .filter(Boolean); + }; + + const filteredCategories = cloneDeep(connectors).map((category) => { + if (!category || !category.items) { + return category; + } + // Only apply client-side filtering if there's no search text (backend already filtered) + if (searchText) { + // When searching, show all items from backend results + return category; + } + category.items = filterItems(category.items); + return category; + }).filter((category) => { + if (!category) { + return false; + } + // When searching, show all categories that have items + if (searchText) { + return category.items && category.items.length > 0; + } + // Map filterType to category labels similar to ConnectorView + // "Standard" maps to "StandardLibrary" (exclude Local and CurrentOrg) + // "Organization" maps to "CurrentOrg" + if (filterType === "Standard") { + return category.metadata.label !== "Local" && category.metadata.label !== "CurrentOrg"; + } else if (filterType === "Organization") { + return category.metadata.label === "CurrentOrg"; + } + // "All" shows all categories except Local (which is handled separately) + return category.metadata.label !== "Local"; + }); + + const isLoading = isSearching || fetchingInfo; + + const openLearnMoreURL = () => { + rpcClient.getCommonRpcClient().openExternalUrl({ + url: 'https://ballerina.io/learn/publish-packages-to-ballerina-central/' + }) + }; + + const getConnectorCreationOptions = () => { + if (!searchText || searchText.trim() === "") { + // No search - show both options + return { showApiSpec: true, showDatabase: true }; + } + + const lowerSearchText = searchText.toLowerCase().trim(); + + // Database-related keywords + const databaseKeywords = [ + "database", "db", "mysql", "postgresql", "postgres", "mssql", "sql server", + "sqlserver", "oracle", "sqlite", "mariadb", "mongodb", "cassandra", + "redis", "dynamodb", "table", "schema", "query", "sql" + ]; + + // API-related keywords + const apiKeywords = [ + "api", "http", "https", "rest", "graphql", "soap", "wsdl", "openapi", + "swagger", "endpoint", "service", "client", "request", "response", + "json", "xml", "yaml", "websocket", "rpc" + ]; + + const isDatabaseSearch = databaseKeywords.some(keyword => lowerSearchText.includes(keyword)); + const isApiSearch = apiKeywords.some(keyword => lowerSearchText.includes(keyword)); + + // If search matches database keywords, show only database option + if (isDatabaseSearch && !isApiSearch) { + return { showApiSpec: false, showDatabase: true }; + } + + // If search matches API keywords, show only API spec option + if (isApiSearch && !isDatabaseSearch) { + return { showApiSpec: true, showDatabase: false }; + } + + // If both or neither match, show both options + return { showApiSpec: true, showDatabase: true }; + }; + + const connectorOptions = getConnectorCreationOptions(); + + return ( + <> + {(platformExtState?.hasPossibleComponent && !platformExtState?.isLoggedIn) && ( + + + Login + {" "} + to Devant in order to connect with Devant dependencies + + )} + {platformExtState?.selectedContext?.project ? ( + + To establish your connection, first define a connector. You may create a custom connector using an + API specification. Alternatively, you can select one of the pre-built + connectors below or connect to services running in Devant or services configured in Devant. You will + then be guided to provide the required details to complete the connection setup. + + ) : ( + + To establish your connection, first define a connector. You may create a custom connector using an + API specification or by introspecting a database. Alternatively, you can select one of the pre-built + connectors below. You will then be guided to provide the required details to complete the connection + setup. + + )} + + + + + + {(connectorOptions.showApiSpec || connectorOptions.showDatabase) && ( +
+ Create New Connector + + {connectorOptions.showApiSpec && handleApiSpecConnection && ( + + + + + + Connect via API Specification + + Import an OpenAPI or WSDL file to create a connector + + + + OpenAPI + + + WSDL + + + + + + + + )} + {/* Database connection option */} + {connectorOptions.showDatabase && handleDatabaseConnection && ( + + + + + + + Connect to a Database + + + Enter credentials to introspect and discover database tables + + + + MySQL + + + MSSQL + + + PostgreSQL + + + + + + + + )} + +
+ )} + + {DevantServicesSection && } + +
+ + Pre-built Connectors + + setFilterType("All")} + > + All + + setFilterType("Standard")} + > + Standard + + setFilterType("Organization")} + > + Organization + + + + {isLoading && ( +
+ +
+ )} + {!isLoading && filteredCategories && filteredCategories.length > 0 && ( + + {filteredCategories.map((category, index) => { + if (!category.items || category.items.length === 0) { + return null; + } + + return ( + + {category.items.map((connector, connectorIndex) => { + const availableNode = connector as AvailableNode; + if (!("codedata" in connector)) { + return null; + } + return ( + + ) : ( + + ) + } + onClick={() => handleSelectConnector(availableNode, filteredCategories)} + /> + ); + })} + + ); + })} + + )} + {!isLoading && (!filteredCategories || filteredCategories.length === 0) && ( +
+ {filterType === "Organization" ? ( + <> + + No connectors found in your organization. You can create and publish connectors to Ballerina Central. + + + Learn how to{' '} + { + openLearnMoreURL(); + }} + > + publish packages to Ballerina Central + + + + ) : ( + + No connectors found. + + )} +
+ )} +
+ + ); +} + diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/index.tsx index 2dc46ae574..446652d8cd 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/index.tsx @@ -16,198 +16,23 @@ * under the License. */ -import React, { useEffect, useState, useMemo, useCallback } from "react"; -import styled from "@emotion/styled"; -import { AvailableNode, Category, Item, LinePosition, MACHINE_VIEW, ParentPopupData } from "@wso2/ballerina-core"; +import React, { useEffect, useState } from "react"; +import { AvailableNode, Category, LinePosition, MACHINE_VIEW, ParentPopupData } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; -import { Codicon, Icon, SearchBox, ThemeColors, Typography, ProgressRing, Tooltip } from "@wso2/ui-toolkit"; +import { Codicon, Icon, SearchBox, ThemeColors, Typography, ProgressRing } from "@wso2/ui-toolkit"; import { cloneDeep, debounce } from "lodash"; import ButtonCard from "../../../../components/ButtonCard"; import { ConnectorIcon } from "@wso2/bi-diagram"; import APIConnectionPopup from "../APIConnectionPopup"; import ConnectionConfigurationPopup from "../ConnectionConfigurationPopup"; import DatabaseConnectionPopup from "../DatabaseConnectionPopup"; -import { BodyTinyInfo } from "../../../styles"; import { PopupOverlay, PopupContainer, PopupHeader, PopupTitle, CloseButton } from "../styles"; +import { usePlatformExtContext } from "../../../../providers/platform-ext-ctx-provider"; +import { DevantConnectorPopup } from "../DevantConnections/DevantConnectorPopup"; +import { PopupContent } from "./styles"; +import { AddConnectionPopupContent } from "./AddConnectionPopupContent"; -const PopupContent = styled.div` - flex: 1; - overflow-y: auto; - padding: 16px 20px; - display: flex; - flex-direction: column; - gap: 16px; -`; - -const IntroText = styled(Typography)` - font-size: 12px; - color: ${ThemeColors.ON_SURFACE_VARIANT}; - line-height: 1.5; - margin: 0; -`; - -const SearchContainer = styled.div` - width: 100%; -`; - -const StyledSearchBox = styled(SearchBox)` - width: 100%; -`; - -const Section = styled.div` - display: flex; - flex-direction: column; - gap: 12px; -`; - -const SectionTitle = styled(Typography)` - font-size: 14px; - font-weight: 600; - color: ${ThemeColors.ON_SURFACE}; - margin: 0; -`; - -const CreateConnectorOptions = styled.div` - display: flex; - flex-direction: column; - gap: 12px; -`; - -const ConnectorOptionCard = styled.div<{ disabled?: boolean }>` - position: relative; - display: flex; - align-items: center; - gap: 12px; - padding: 12px; - border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; - border-radius: 8px; - background-color: ${ThemeColors.SURFACE_DIM}; - cursor: ${(props: { disabled?: boolean }) => (props.disabled ? "not-allowed" : "pointer")}; - transition: all 0.2s ease; - opacity: ${(props: { disabled?: boolean }) => (props.disabled ? 0.5 : 1)}; - - &:hover { - background-color: ${(props: { disabled?: boolean }) => - props.disabled ? ThemeColors.SURFACE_DIM : ThemeColors.PRIMARY_CONTAINER}; - border-color: ${(props: { disabled?: boolean }) => - props.disabled ? ThemeColors.OUTLINE_VARIANT : ThemeColors.PRIMARY}; - } -`; - -const ConnectorOptionIcon = styled.div` - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 8px; - background-color: ${ThemeColors.SURFACE_CONTAINER}; - flex-shrink: 0; -`; - -const ConnectorOptionContent = styled.div` - flex: 1; - display: flex; - flex-direction: column; - gap: 6px; -`; - -const ConnectorOptionTitleContainer = styled.div` - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - justify-content: space-between; -`; - -const ConnectorOptionTitle = styled(Typography)` - font-size: 14px; - font-weight: 600; - color: ${ThemeColors.ON_SURFACE}; - margin: 0; -`; - -const ExperimentalBadge = styled(Typography)` - font-size: 12px; - color: ${ThemeColors.ON_SURFACE_VARIANT}; - padding: 4px; - border-radius: 4px; - background-color: ${ThemeColors.SURFACE_CONTAINER}; - margin: 0; - display: inline-block; -`; - -const ConnectorOptionDescription = styled(Typography)` - font-size: 12px; - color: ${ThemeColors.ON_SURFACE_VARIANT}; - margin: 0; -`; - -const ConnectorOptionButtons = styled.div` - display: flex; - gap: 8px; - flex-wrap: wrap; -`; - -const ConnectorTypeLabel = styled(Typography)` - font-size: 12px; - color: ${ThemeColors.ON_SURFACE_VARIANT}; - padding: 6px; - border-radius: 4px; - background-color: ${ThemeColors.SURFACE_CONTAINER}; - margin: 0; - display: inline-block; -`; - -const ArrowIcon = styled.div` - display: flex; - align-items: center; - color: ${ThemeColors.ON_SURFACE_VARIANT}; -`; - -const SectionHeader = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -`; - -const FilterButtons = styled.div` - display: flex; - gap: 4px; - align-items: center; -`; - -const FilterButton = styled.button<{ active?: boolean }>` - font-size: 12px; - padding: 6px 12px; - height: 28px; - border-radius: 4px; - border: none; - cursor: pointer; - font-weight: ${(props: { active?: boolean }) => (props.active ? 600 : 400)}; - background-color: ${(props: { active?: boolean }) => - props.active ? ThemeColors.PRIMARY : "transparent"}; - color: ${(props: { active?: boolean }) => - props.active ? ThemeColors.ON_PRIMARY : ThemeColors.ON_SURFACE_VARIANT}; - transition: all 0.2s ease; - - &:hover { - background-color: ${(props: { active?: boolean }) => - props.active ? ThemeColors.PRIMARY : ThemeColors.SURFACE_CONTAINER}; - color: ${(props: { active?: boolean }) => - props.active ? ThemeColors.ON_PRIMARY : ThemeColors.ON_SURFACE}; - } -`; - -const ConnectorsGrid = styled.div` - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 12px; - margin-top: 8px; -`; - -interface AddConnectionPopupProps { +export interface AddConnectionPopupProps { projectPath: string; fileName: string; target?: LinePosition; @@ -217,169 +42,31 @@ interface AddConnectionPopupProps { } export function AddConnectionPopup(props: AddConnectionPopupProps) { + const { onClose, onNavigateToOverview, isPopup, target, fileName, projectPath } = props; + const { platformExtState } = usePlatformExtContext(); + + if(platformExtState?.isLoggedIn && platformExtState?.selectedContext?.project){ + return ( + + ); + } + + return +} + +function AddBIConnectionPopup(props: AddConnectionPopupProps) { const { projectPath, fileName, target, onClose, onNavigateToOverview, isPopup } = props; const { rpcClient } = useRpcContext(); - - const [searchText, setSearchText] = useState(""); - const [connectors, setConnectors] = useState([]); - const [isSearching, setIsSearching] = useState(false); - const [fetchingInfo, setFetchingInfo] = useState(false); - const [filterType, setFilterType] = useState<"All" | "Standard" | "Organization">("All"); const [wizardStep, setWizardStep] = useState<"database" | "api" | "connector" | null>(null); const [selectedConnector, setSelectedConnector] = useState(null); - const [experimentalEnabled, setExperimentalEnabled] = useState(false); - const [hasPersistConnection, setHasPersistConnection] = useState(false); - - useEffect(() => { - rpcClient - ?.getCommonRpcClient() - .experimentalEnabled() - .then((enabled) => setExperimentalEnabled(enabled)) - .catch((err) => { - console.error(">>> error checking experimental flag", err); - setExperimentalEnabled(false); - }); - }, [rpcClient]); - - // Temporary fix to check for existing database Persist connection till the backend is updated to support this. - useEffect(() => { - const checkExistingDatabaseConnection = async () => { - if (!rpcClient || !experimentalEnabled) { - return; - } - try { - const res = await rpcClient.getBIDiagramRpcClient().getModuleNodes(); - - const hasDatabaseConnection = res.flowModel.connections?.some((connection) => { - const metadataData = connection.metadata?.data as any; - return metadataData?.connectorType === "persist"; - }); - - setHasPersistConnection(hasDatabaseConnection || false); - } catch (error) { - console.error(">>> Error checking for existing database connection", error); - setHasPersistConnection(false); - } - }; - - if (experimentalEnabled) { - checkExistingDatabaseConnection(); - } - }, [rpcClient, experimentalEnabled]); - - const fetchConnectors = useCallback((filter?: boolean) => { - setFetchingInfo(true); - const defaultPosition: LinePosition = { line: 0, offset: 0 }; - const position = target || defaultPosition; - rpcClient - .getBIDiagramRpcClient() - .search({ - position: { - startLine: position, - endLine: position, - }, - filePath: fileName, - queryMap: { - limit: 60, - filterByCurrentOrg: filter ?? filterType === "Organization", - }, - searchKind: "CONNECTOR", - }) - .then(async (model) => { - console.log(">>> bi connectors", model); - console.log(">>> bi filtered connectors", model.categories); - setConnectors(model.categories); - }) - .finally(() => { - setIsSearching(false); - setFetchingInfo(false); - }); - }, [rpcClient, target, fileName, filterType]); - - useEffect(() => { - setIsSearching(true); - fetchConnectors(); - }, []); - - const handleSearch = useCallback((text: string) => { - const defaultPosition: LinePosition = { line: 0, offset: 0 }; - const position = target || defaultPosition; - rpcClient - .getBIDiagramRpcClient() - .search({ - position: { - startLine: position, - endLine: position, - }, - filePath: fileName, - queryMap: { - q: text, - limit: 60, - filterByCurrentOrg: filterType === "Organization" ? true : false, - }, - searchKind: "CONNECTOR", - }) - .then(async (model) => { - console.log(">>> bi searched connectors", model); - console.log(">>> bi filtered connectors", model.categories); - - // When searching, the API might return a flat array of connectors instead of categories - // Check if categories exist and have the proper structure (with items arrays) - let normalizedCategories: Category[] = []; - - if (model.categories && Array.isArray(model.categories)) { - // Check if the first item is a category (has items) or a connector (has codedata) - const firstItem = model.categories[0]; - if (firstItem && "items" in firstItem && Array.isArray(firstItem.items)) { - // Proper category structure - use as is - normalizedCategories = model.categories; - } else if (firstItem && "codedata" in firstItem) { - // Flat array of connectors - wrap in a category - normalizedCategories = [{ - metadata: { - label: "Search Results", - description: "" - }, - items: model.categories as unknown as AvailableNode[] - }]; - } - } - - console.log(">>> normalized categories for search", normalizedCategories); - setConnectors(normalizedCategories); - }) - .finally(() => { - setIsSearching(false); - }); - }, [rpcClient, target, fileName, filterType]); - - const debouncedSearch = useMemo( - () => debounce(handleSearch, 1100), - [handleSearch] - ); - - useEffect(() => { - setIsSearching(true); - debouncedSearch(searchText); - return () => debouncedSearch.cancel(); - }, [searchText, debouncedSearch]); - - useEffect(() => { - setIsSearching(true); - fetchConnectors(); - }, [filterType, fetchConnectors]); - - useEffect(() => { - rpcClient?.onProjectContentUpdated((state: boolean) => { - if (state) { - fetchConnectors(); - } - }); - }, [rpcClient, fetchConnectors]); - - const handleOnSearch = (text: string) => { - setSearchText(text); - }; + const [filteredCategories, setFilteredCategories] = useState([]); const handleDatabaseConnection = () => { // Navigate to database connection wizard @@ -430,62 +117,6 @@ export function AddConnectionPopup(props: AddConnectionPopupProps) { } }; - const filterItems = (items: Item[]): Item[] => { - return items - .map((item) => { - if ("items" in item) { - const filteredItems = filterItems(item.items); - return { - ...item, - items: filteredItems, - }; - } else { - const lowerCaseTitle = item.metadata.label.toLowerCase(); - const lowerCaseDescription = item.metadata.description?.toLowerCase() || ""; - const lowerCaseSearchText = searchText.toLowerCase(); - if ( - lowerCaseTitle.includes(lowerCaseSearchText) || - lowerCaseDescription.includes(lowerCaseSearchText) - ) { - return item; - } - } - }) - .filter(Boolean); - }; - - const filteredCategories = cloneDeep(connectors).map((category) => { - if (!category || !category.items) { - return category; - } - // Only apply client-side filtering if there's no search text (backend already filtered) - if (searchText) { - // When searching, show all items from backend results - return category; - } - category.items = filterItems(category.items); - return category; - }).filter((category) => { - if (!category) { - return false; - } - // When searching, show all categories that have items - if (searchText) { - return category.items && category.items.length > 0; - } - // Map filterType to category labels similar to ConnectorView - // "Standard" maps to "StandardLibrary" (exclude Local and CurrentOrg) - // "Organization" maps to "CurrentOrg" - if (filterType === "Standard") { - return category.metadata.label !== "Local" && category.metadata.label !== "CurrentOrg"; - } else if (filterType === "Organization") { - return category.metadata.label === "CurrentOrg"; - } - // "All" shows all categories except Local (which is handled separately) - return category.metadata.label !== "Local"; - }); - - const isLoading = isSearching || fetchingInfo; // Show configuration form when connector is selected if (wizardStep === "connector" && selectedConnector) { @@ -536,53 +167,6 @@ export function AddConnectionPopup(props: AddConnectionPopupProps) { } }; - const openLearnMoreURL = () => { - rpcClient.getCommonRpcClient().openExternalUrl({ - url: 'https://ballerina.io/learn/publish-packages-to-ballerina-central/' - }) - }; - - const getConnectorCreationOptions = () => { - if (!searchText || searchText.trim() === "") { - // No search - show both options (database shown disabled if hasPersistConnection) - return { showApiSpec: true, showDatabase: experimentalEnabled }; - } - - const lowerSearchText = searchText.toLowerCase().trim(); - - // Database-related keywords - const databaseKeywords = [ - "database", "db", "mysql", "postgresql", "postgres", "mssql", "sql server", - "sqlserver", "oracle", "sqlite", "mariadb", "mongodb", "cassandra", - "redis", "dynamodb", "table", "schema", "query", "sql" - ]; - - // API-related keywords - const apiKeywords = [ - "api", "http", "https", "rest", "graphql", "soap", "wsdl", "openapi", - "swagger", "endpoint", "service", "client", "request", "response", - "json", "xml", "yaml", "websocket", "rpc" - ]; - - const isDatabaseSearch = databaseKeywords.some(keyword => lowerSearchText.includes(keyword)); - const isApiSearch = apiKeywords.some(keyword => lowerSearchText.includes(keyword)); - - // If search matches database keywords, show only database option - if (isDatabaseSearch && !isApiSearch) { - return { showApiSpec: false, showDatabase: experimentalEnabled }; - } - - // If search matches API keywords, show only API spec option - if (isApiSearch && !isDatabaseSearch) { - return { showApiSpec: true, showDatabase: false }; - } - - // If both or neither match, show both options - return { showApiSpec: true, showDatabase: experimentalEnabled }; - }; - - const connectorOptions = getConnectorCreationOptions(); - return ( <> @@ -594,228 +178,15 @@ export function AddConnectionPopup(props: AddConnectionPopupProps) { - - {experimentalEnabled ? ( - <> - To establish your connection, first define a connector. You may create a custom connector using - an API specification or by introspecting a database. Alternatively, you can select one of the - pre-built connectors below. You will then be guided to provide the required details to complete - the connection setup. - - ) : ( - <> - To establish your connection, first define a connector. You may create a custom connector using - an API specification. Alternatively, you can select one of the pre-built connectors below. You will then be guided to provide the required details to complete - the connection setup. - - )} - - - - - - - - {(connectorOptions.showApiSpec || connectorOptions.showDatabase) && ( -
- Create New Connector - - {connectorOptions.showApiSpec && ( - - - - - - Connect via API Specification - - Import an OpenAPI or WSDL file to create a connector - - - - OpenAPI - - - WSDL - - - - - - - - )} - {/* Temporary disable DB connection option if persist connection exists */} - {connectorOptions.showDatabase && (() => { - const databaseCardContent = ( - <> - - - - - - Connect to a Database - Experimental - - - Enter credentials to introspect and discover database tables - - - - MySQL - - - MSSQL - - - PostgreSQL - - - - - - - - ); - - const databaseCard = ( - { - if (hasPersistConnection) { - e.preventDefault(); - e.stopPropagation(); - return; - } - handleDatabaseConnection(); - }} - > - {databaseCardContent} - - ); - - return hasPersistConnection ? ( - - {databaseCard} - - ) : ( - databaseCard - ); - })()} - -
- )} - -
- - Pre-built Connectors - - setFilterType("All")} - > - All - - setFilterType("Standard")} - > - Standard - - setFilterType("Organization")} - > - Organization - - - - {isLoading && ( -
- -
- )} - {!isLoading && filteredCategories && filteredCategories.length > 0 && ( - - {filteredCategories.map((category, index) => { - if (!category.items || category.items.length === 0) { - return null; - } - - return ( - - {category.items.map((connector, connectorIndex) => { - const availableNode = connector as AvailableNode; - if (!("codedata" in connector)) { - return null; - } - return ( - - ) : ( - - ) - } - onClick={() => handleSelectConnector(availableNode)} - /> - ); - })} - - ); - })} - - )} - {!isLoading && (!filteredCategories || filteredCategories.length === 0) && ( -
- {filterType === "Organization" ? ( - <> - - No connectors found in your organization. You can create and publish connectors to Ballerina Central. - - - Learn how to{' '} - { - openLearnMoreURL(); - }} - > - publish packages to Ballerina Central - - - - ) : ( - - No connectors found. - - )} -
- )} -
+ { + handleSelectConnector(connector); + setFilteredCategories(filteredCategories); + }} + />
diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/styles.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/styles.ts new file mode 100644 index 0000000000..9437ff166c --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/styles.ts @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import styled from "@emotion/styled"; +import { Typography, ThemeColors, SearchBox, Button } from "@wso2/ui-toolkit"; + +export const PopupContent = styled.div` + flex: 1; + overflow-y: auto; + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 16px; +`; + +export const IntroText = styled(Typography)` + font-size: 12px; + color: ${ThemeColors.ON_SURFACE_VARIANT}; + line-height: 1.5; + margin: 0; +`; + +export const SearchContainer = styled.div` + width: 100%; +`; + +export const StyledSearchBox = styled(SearchBox)` + width: 100%; +`; + +export const Section = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const SectionTitle = styled(Typography)` + font-size: 14px; + font-weight: 600; + color: ${ThemeColors.ON_SURFACE}; + margin: 0; +`; + +export const CreateConnectorOptions = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const ConnectorOptionCard = styled.div` + position: relative; + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-radius: 8px; + background-color: ${ThemeColors.SURFACE_DIM}; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: ${ThemeColors.PRIMARY_CONTAINER}; + border-color: ${ThemeColors.PRIMARY}; + } +`; + +export const ConnectorOptionIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 8px; + background-color: ${ThemeColors.SURFACE_CONTAINER}; + flex-shrink: 0; +`; + +export const ConnectorOptionContent = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; +`; + +export const ConnectorOptionTitleContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + justify-content: space-between; +`; + +export const ConnectorOptionTitle = styled(Typography)` + font-size: 14px; + font-weight: 600; + color: ${ThemeColors.ON_SURFACE}; + margin: 0; +`; + +export const ConnectorOptionDescription = styled(Typography)` + font-size: 12px; + color: ${ThemeColors.ON_SURFACE_VARIANT}; + margin: 0; +`; + +export const ConnectorOptionButtons = styled.div` + display: flex; + gap: 8px; + flex-wrap: wrap; +`; + +export const ConnectorTypeLabel = styled(Typography)` + font-size: 12px; + color: ${ThemeColors.ON_SURFACE_VARIANT}; + padding: 6px; + border-radius: 4px; + background-color: ${ThemeColors.SURFACE_CONTAINER}; + margin: 0; + display: inline-block; +`; + +export const ArrowIcon = styled.div` + display: flex; + align-items: center; + color: ${ThemeColors.ON_SURFACE_VARIANT}; +`; + +export const SectionHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +`; + +export const FilterButtons = styled.div` + display: flex; + gap: 4px; + align-items: center; +`; + +export const FilterButton = styled.button<{ active?: boolean }>` + font-size: 12px; + padding: 6px 12px; + height: 28px; + border-radius: 4px; + border: none; + cursor: pointer; + font-weight: ${(props: { active?: boolean }) => (props.active ? 600 : 400)}; + background-color: ${(props: { active?: boolean }) => + props.active ? ThemeColors.PRIMARY : "transparent"}; + color: ${(props: { active?: boolean }) => + props.active ? ThemeColors.ON_PRIMARY : ThemeColors.ON_SURFACE_VARIANT}; + transition: all 0.2s ease; + + &:hover { + background-color: ${(props: { active?: boolean }) => + props.active ? ThemeColors.PRIMARY : ThemeColors.SURFACE_CONTAINER}; + color: ${(props: { active?: boolean }) => + props.active ? ThemeColors.ON_PRIMARY : ThemeColors.ON_SURFACE}; + } +`; + +export const ConnectorsGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; + margin-top: 8px; +`; + +export const BackButton = styled(Button)` + min-width: auto; + padding: 4px; +`; + +export const StepperContainer = styled.div` + padding: 24px 32px; + border-bottom: 1px solid ${ThemeColors.OUTLINE_VARIANT}; +`; + +export const ConnectorDetailCard = styled.div` + position: relative; + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-radius: 8px; + background-color: ${ThemeColors.SURFACE_DIM}; + transition: all 0.2s ease; + opacity: ${(props: { disabled?: boolean }) => (props.disabled ? 0.5 : 1)}; +`; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigView/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigView/index.tsx index f474bccfeb..33abe6a9bd 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigView/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigView/index.tsx @@ -18,7 +18,7 @@ import React, { ReactNode, useEffect, useState } from "react"; import styled from "@emotion/styled"; -import { ExpressionFormField } from "@wso2/ballerina-side-panel"; +import { ExpressionEditorDevantProps, ExpressionFormField, FormValues } from "@wso2/ballerina-side-panel"; import { EditorConfig, FlowNode, LineRange, SubPanel } from "@wso2/ballerina-core"; import FormGenerator from "../../Forms/FormGenerator"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; @@ -66,6 +66,8 @@ interface ConnectionConfigViewProps { isPullingConnector?: boolean; navigateToPanel?: (targetPanel: SidePanelView, connectionKind?: ConnectionKind) => void; footerActionButton?: boolean; // Render save button as footer action button + devantExpressionEditor?: ExpressionEditorDevantProps; + customValidator?: (fieldKey: string, value: any, allValues: FormValues) => string | undefined; } export function ConnectionConfigView(props: ConnectionConfigViewProps) { @@ -81,6 +83,8 @@ export function ConnectionConfigView(props: ConnectionConfigViewProps) { isSaving, navigateToPanel, footerActionButton, + devantExpressionEditor, + customValidator, } = props; const { rpcClient } = useRpcContext(); const [targetLineRange, setTargetLineRange] = useState(); @@ -126,6 +130,8 @@ export function ConnectionConfigView(props: ConnectionConfigViewProps) { footerActionButton={footerActionButton} navigateToPanel={navigateToPanel} handleOnFormSubmit={onSubmit} + devantExpressionEditor={devantExpressionEditor} + customValidator={customValidator} /> )} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigurationPopup/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigurationPopup/index.tsx index 0c5e94a270..d1e2922093 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigurationPopup/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/ConnectionConfigurationPopup/index.tsx @@ -34,7 +34,7 @@ import { Codicon, Icon, ThemeColors, Typography } from "@wso2/ui-toolkit"; import { ConnectorIcon } from "@wso2/bi-diagram"; import ConnectionConfigView from "../ConnectionConfigView"; import { getFormProperties } from "../../../../utils/bi"; -import { ExpressionFormField } from "@wso2/ballerina-side-panel"; +import { ExpressionEditorDevantProps, ExpressionFormField, FormValues } from "@wso2/ballerina-side-panel"; import { RelativeLoader } from "../../../../components/RelativeLoader"; import { HelperView } from "../../HelperView"; import { DownloadIcon } from "../../../../components/DownloadIcon"; @@ -196,17 +196,52 @@ enum SavingFormStatus { ERROR = "error", } -interface ConnectionConfigurationPopupProps { +export interface ConnectionConfigurationPopupProps { selectedConnector: AvailableNode; fileName: string; target?: LinePosition; onClose: (parent?: ParentPopupData) => void; onBack: () => void; filteredCategories?: Category[]; + customValidator?: (fieldKey: string, value: any, allValues: FormValues) => string | undefined; + overrideFlowNode?: (node: FlowNode) => FlowNode; } export function ConnectionConfigurationPopup(props: ConnectionConfigurationPopupProps) { - const { selectedConnector, fileName, target, onClose, onBack, filteredCategories = [] } = props; + const { selectedConnector, onClose, onBack } = props; + + return ( + <> + + + + + + + + Configure {selectedConnector.metadata.label} + + Configure connection settings for this connector + + + onClose()}> + + + + + + + + ); +} + +export interface ConnectionConfigurationFormProps extends Omit { + loading?: boolean; + devantExpressionEditor?: ExpressionEditorDevantProps; +} + +export function ConnectionConfigurationForm(props: ConnectionConfigurationFormProps) { + const { selectedConnector, fileName, target, onClose, filteredCategories = [], loading, devantExpressionEditor, customValidator, overrideFlowNode } = props; const { rpcClient } = useRpcContext(); const [pullingStatus, setPullingStatus] = useState(undefined); @@ -247,7 +282,7 @@ export function ConnectionConfigurationPopup(props: ConnectionConfigurationPopup }); // Wait for either the timer or the request to finish - const response = await Promise.race([ + let response = await Promise.race([ nodeTemplatePromise.then((res) => { if (timer) { clearTimeout(timer); @@ -264,6 +299,9 @@ export function ConnectionConfigurationPopup(props: ConnectionConfigurationPopup } console.log(">>> FlowNode template", response); + if (overrideFlowNode) { + response.flowNode = overrideFlowNode(response.flowNode); + } selectedNodeRef.current = response.flowNode; const formProperties = getFormProperties(response.flowNode); console.log(">>> Form properties", formProperties); @@ -374,109 +412,93 @@ export function ConnectionConfigurationPopup(props: ConnectionConfigurationPopup return ( <> - - - - - - - - Configure {selectedConnector.metadata.label} - - Configure connection settings for this connector - - - onClose()}> - - - - - - - {selectedConnector.metadata.icon ? ( - - - - ) : ( - - )} - - - {selectedConnector.metadata.label} - - {selectedConnector.metadata.description || ""} - - - - {getConnectorTag()} - - - - - {pullingStatus && ( - - {pullingStatus === PullingStatus.FETCHING && ( - - )} - {pullingStatus === PullingStatus.PULLING && ( - - - - Please wait while the connector is being pulled. - - - )} - {pullingStatus === PullingStatus.SUCCESS && ( - - - Connector pulled successfully. - - )} - {pullingStatus === PullingStatus.ERROR && ( - - - - Failed to pull the connector. Please try again. - - - )} - + + + {selectedConnector.metadata.icon ? ( + + + + ) : ( + )} - {!pullingStatus && selectedNodeRef.current && ( - <> - - + + {selectedConnector.metadata.label} + + {selectedConnector.metadata.description || ""} + + + + {getConnectorTag()} + + + + + {pullingStatus && ( + + {pullingStatus === PullingStatus.FETCHING && ( + + )} + {pullingStatus === PullingStatus.PULLING && ( + + + + Please wait while the connector is being pulled. + + + )} + {pullingStatus === PullingStatus.SUCCESS && ( + + - - - )} - - + Connector pulled successfully. + + )} + {pullingStatus === PullingStatus.ERROR && ( + + + + Failed to pull the connector. Please try again. + + + )} + + )} + {!pullingStatus && selectedNodeRef.current && ( + <> + + + + + )} + ); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantBIConnectorInitForm.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantBIConnectorInitForm.tsx new file mode 100644 index 0000000000..425b7951dc --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantBIConnectorInitForm.tsx @@ -0,0 +1,300 @@ +/** + * 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 { + ConnectionListItem, + getTypeForDisplayType, + ServiceInfoVisibilityEnum, + type MarketplaceItem, +} from "@wso2/wso2-platform-core"; +import React, { useEffect, type FC } from "react"; +import { usePlatformExtContext } from "../../../../providers/platform-ext-ctx-provider"; +import { useMutation } from "@tanstack/react-query"; +import { DevantConnectionFlow, DevantTempConfig } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { ConnectionConfigurationForm, ConnectionConfigurationFormProps } from "../ConnectionConfigurationPopup"; +import { DIRECTORY_MAP } from "@wso2/ballerina-core"; +import { generateInitialConnectionName, isValidDevantConnName } from "./utils"; +import { getInitialVisibility, getPossibleVisibilities } from "./DevantConnectorCreateForm"; + +interface Props extends Omit { + importedConnection?: ConnectionListItem; + selectedMarketplaceItem: MarketplaceItem; + selectedFlow: DevantConnectionFlow; + devantConfigs: DevantTempConfig[]; + resetDevantConfigs: () => void; + onAddDevantConfig: (name: string, value: string, isSecret: boolean) => Promise; + IDLFilePath?: string; + biConnectionNames: string[]; + existingDevantConnNames: string[]; + onFlowChange: (flow: DevantConnectionFlow | null) => void; + projectPath: string; +} + +export const DevantBIConnectorCreateForm: FC = (props) => { + const { + selectedMarketplaceItem, + selectedFlow, + devantConfigs, + onAddDevantConfig, + IDLFilePath, + biConnectionNames, + onClose, + resetDevantConfigs, + onFlowChange, + importedConnection, + projectPath, + existingDevantConnNames, + } = props; + const { platformExtState, platformRpcClient } = usePlatformExtContext(); + + let initialNameCandidate = + selectedMarketplaceItem?.name?.replaceAll(" ", "_")?.replaceAll("-", "_") || "my_connection"; + if ( + [ + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR, + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS, + ].includes(selectedFlow) + ) { + initialNameCandidate = props.selectedConnector?.codedata?.module; + } + if (importedConnection) { + initialNameCandidate = importedConnection?.name?.replaceAll(" ", "_")?.replaceAll("-", "_") || "my_connection"; + } + + useEffect(() => { + if (selectedMarketplaceItem && !props.selectedConnector) { + if (importedConnection) { + onFlowChange( + selectedMarketplaceItem.isThirdParty + ? DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR + : DevantConnectionFlow.IMPORT_INTERNAL_OTHER_SELECT_BI_CONNECTOR, + ); + } else { + onFlowChange( + selectedMarketplaceItem.isThirdParty + ? DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR + : DevantConnectionFlow.CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR, + ); + } + } + }, [props.selectedConnector]); + + const { mutate: createDevantInternalConnNonOAS, isPending: isCreating } = useMutation({ + mutationFn: async ({ recentIdentifier }: { recentIdentifier: string }) => { + if (importedConnection) { + const connectionDetailed = await platformRpcClient.getConnection({ + connectionGroupId: importedConnection.groupUuid, + orgId: platformExtState?.selectedContext?.org?.id?.toString(), + }); + await platformRpcClient.replaceDevantTempConfigValues({ + configs: devantConfigs, + createdConnection: connectionDetailed, + }); + + let visibility: ServiceInfoVisibilityEnum = ServiceInfoVisibilityEnum.Public; + if (connectionDetailed?.schemaName?.toLowerCase()?.includes("organization")) { + visibility = ServiceInfoVisibilityEnum.Organization; + } else if (connectionDetailed?.schemaName?.toLowerCase()?.includes("project")) { + visibility = ServiceInfoVisibilityEnum.Project; + } + + await platformRpcClient.createConnectionConfig({ + marketplaceItem: selectedMarketplaceItem, + name: importedConnection.name, + visibility, + componentDir: projectPath, + }); + } else if ( + [ + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR, + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS, + ].includes(selectedFlow) && + devantConfigs?.length > 0 + ) { + const marketplaceService = await platformRpcClient.registerDevantMarketplaceService({ + name: recentIdentifier, + configs: devantConfigs?.map((item) => ({ ...item, id: item.name })) || [], + idlFilePath: IDLFilePath || "", + idlType: + selectedFlow === DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS ? "OpenAPI" : "TCP", + serviceType: "REST", + }); + + const isProjectLevel = !!!platformExtState?.selectedComponent?.metadata?.id; + + const createdConnection = await platformRpcClient?.createThirdPartyConnection({ + componentId: isProjectLevel ? "" : platformExtState?.selectedComponent?.metadata?.id, + name: recentIdentifier, + orgId: platformExtState?.selectedContext?.org.id?.toString(), + orgUuid: platformExtState?.selectedContext?.org?.uuid, + projectId: platformExtState?.selectedContext?.project.id, + serviceSchemaId: marketplaceService.connectionSchemas[0]?.id, + serviceId: marketplaceService.serviceId, + endpointRefs: marketplaceService.endpointRefs, + sensitiveKeys: marketplaceService.connectionSchemas[0].entries + ?.filter((item) => item.isSensitive) + .map((item) => item.name), + }); + + await platformRpcClient.replaceDevantTempConfigValues({ + configs: devantConfigs, + createdConnection: createdConnection, + }); + + await platformRpcClient.createConnectionConfig({ + marketplaceItem: marketplaceService, + name: recentIdentifier, + visibility: "PUBLIC", + componentDir: projectPath, + }); + } else if ( + [ + DevantConnectionFlow.CREATE_THIRD_PARTY_OAS, + DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER, + DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR, + ].includes(selectedFlow) + ) { + const isProjectLevel = !!!platformExtState?.selectedComponent?.metadata?.id; + const createdConnection = await platformRpcClient?.createThirdPartyConnection({ + componentId: isProjectLevel ? "" : platformExtState?.selectedComponent?.metadata?.id, + name: recentIdentifier, + orgId: platformExtState?.selectedContext?.org.id?.toString(), + orgUuid: platformExtState?.selectedContext?.org?.uuid, + projectId: platformExtState?.selectedContext?.project.id, + serviceSchemaId: selectedMarketplaceItem.connectionSchemas[0]?.id, + serviceId: selectedMarketplaceItem.serviceId, + endpointRefs: selectedMarketplaceItem.endpointRefs, + sensitiveKeys: selectedMarketplaceItem.connectionSchemas[0].entries + ?.filter((item) => item.isSensitive) + .map((item) => item.name), + }); + + await platformRpcClient.replaceDevantTempConfigValues({ + configs: devantConfigs, + createdConnection: createdConnection, + }); + + await platformRpcClient.createConnectionConfig({ + marketplaceItem: selectedMarketplaceItem, + name: recentIdentifier, + visibility: "PUBLIC", + componentDir: projectPath, + }); + } else if ( + [ + DevantConnectionFlow.CREATE_INTERNAL_OTHER, + DevantConnectionFlow.CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR, + ].includes(selectedFlow) + ) { + const isProjectLevel = !!!platformExtState?.selectedComponent?.metadata?.id; + const visibilities = getPossibleVisibilities( + selectedMarketplaceItem, + platformExtState?.selectedContext?.project, + ); + const createdConnection = await platformRpcClient?.createInternalConnection({ + componentId: isProjectLevel ? "" : platformExtState.selectedComponent?.metadata?.id, + name: recentIdentifier, + orgId: platformExtState.selectedContext?.org.id?.toString(), + orgUuid: platformExtState.selectedContext?.org?.uuid, + projectId: platformExtState.selectedContext?.project.id, + serviceSchemaId: selectedMarketplaceItem.connectionSchemas[0]?.id || "", + serviceId: selectedMarketplaceItem.serviceId, + serviceVisibility: getInitialVisibility(selectedMarketplaceItem, visibilities), + componentType: isProjectLevel + ? "non-component" + : getTypeForDisplayType(platformExtState.selectedComponent?.spec?.type), + componentPath: projectPath, + generateCreds: true, + }); + + await platformRpcClient.replaceDevantTempConfigValues({ + configs: devantConfigs, + createdConnection: createdConnection, + }); + + await platformRpcClient.createConnectionConfig({ + marketplaceItem: selectedMarketplaceItem, + name: recentIdentifier, + visibility: getInitialVisibility(selectedMarketplaceItem, visibilities), + componentDir: projectPath, + }); + } + }, + onError: (error) => { + console.error(">>> Error creating Devant connection", error); + }, + onSuccess: (_, { recentIdentifier }) => { + platformRpcClient.refreshConnectionList(); + resetDevantConfigs(); + onClose({ recentIdentifier, artifactType: DIRECTORY_MAP.CONNECTION }); + }, + }); + + if (!props.selectedConnector) { + return null; + } + + if (!platformExtState?.isLoggedIn) { + return ; + } + + return ( + config.name), + onAddDevantConfig: [ + // Only allow users to create devant configs via these flows + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR, + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS, + ].includes(selectedFlow) + ? onAddDevantConfig + : undefined, + }} + onClose={(params) => { + if (params.recentIdentifier) { + createDevantInternalConnNonOAS({ recentIdentifier: params.recentIdentifier }); + } else { + onClose(); + } + }} + loading={isCreating} + customValidator={(fieldKey, value) => { + if (fieldKey === "variable") { + return isValidDevantConnName(value, existingDevantConnNames, biConnectionNames); + } + return undefined; + }} + overrideFlowNode={(node) => { + if (node.properties.variable) { + node.properties.variable.value = importedConnection + ? initialNameCandidate + : generateInitialConnectionName( + biConnectionNames, + existingDevantConnNames, + initialNameCandidate, + ); + if (importedConnection) { + node.properties.variable.editable = false; + } + } + return node; + }} + /> + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantBIConnectorSelect.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantBIConnectorSelect.tsx new file mode 100644 index 0000000000..6a28ffff53 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantBIConnectorSelect.tsx @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2026, 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, { useCallback, type FC } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { AvailableNode, LinePosition } from "@wso2/ballerina-core"; +import { debounce } from "lodash"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { ConnectorsGrid, SearchContainer, StyledSearchBox } from "../AddConnectionPopup/styles"; +import { Codicon, ProgressRing } from "@wso2/ui-toolkit"; +import ButtonCard from "../../../../components/ButtonCard"; +import { ConnectorIcon } from "@wso2/bi-diagram"; +import { BodyTinyInfo } from "../../../styles"; + +interface Props { + target: LinePosition | null; + fileName: string; + onItemSelect: (availableNode: AvailableNode | undefined) => void; +} + +export const DevantBIConnectorSelect: FC = (props) => { + const { target, fileName, onItemSelect } = props; + const { rpcClient } = useRpcContext(); + const [searchText, setSearchText] = React.useState(""); + + const debouncedSetSearchText = useCallback( + debounce((value: string) => setSearchText(value), 500), + [], + ); + + const { data: connectorList, isLoading: loadingConnectors } = useQuery({ + queryKey: ["searchConnectorsToInit", fileName, target, searchText], + queryFn: () => + rpcClient.getBIDiagramRpcClient().search({ + filePath: fileName, + queryMap: { limit: 60, q: searchText?.toLowerCase() ?? "" }, + searchKind: "CONNECTOR", + }), + select: (data) => { + let resp: AvailableNode[] = []; + if (data.categories && data.categories.length > 0) { + if (data.categories[0]?.items) { + data.categories?.forEach((cat) => { + cat.items?.forEach((item) => { + if ((item as AvailableNode)?.codedata) { + resp.push(item as AvailableNode); + } + }); + }); + } else { + data.categories?.forEach((cat) => resp.push(cat as unknown as AvailableNode)); + } + } + return resp; + }, + }); + + return ( + <> + + + + + {loadingConnectors ? ( +
+ +
+ ) : ( + <> + {connectorList.length === 0 ? ( + <> + {searchText ? ( + + No connectors matching with "{searchText}" + + ) : ( + No connectors available + )} + + ) : ( + + {connectorList.map((availableNode, connectorIndex) => ( + + ) : ( + + ) + } + onClick={() => onItemSelect(availableNode)} + /> + ))} + + )} + + )} + + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorCreateForm.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorCreateForm.tsx new file mode 100644 index 0000000000..4be601ae00 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorCreateForm.tsx @@ -0,0 +1,415 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + type MarketplaceItem, + type MarketplaceItemSchema, + type Project, + ServiceInfoVisibilityEnum, + capitalizeFirstLetter, + getTypeForDisplayType, +} from "@wso2/wso2-platform-core"; +import React, { ReactNode, useEffect, useState, type FC } from "react"; +import { useForm, SubmitHandler } from "react-hook-form"; +import { FormStyles } from "../../Forms/styles"; +import { Dropdown, TextField, Codicon, LinkButton, ThemeColors, CheckBox, CheckBoxGroup } from "@wso2/ui-toolkit"; +import styled from "@emotion/styled"; +import { usePlatformExtContext } from "../../../../providers/platform-ext-ctx-provider"; +import { useMutation } from "@tanstack/react-query"; +import { ActionButton, ConnectorContentContainer, ConnectorInfoContainer, FooterContainer } from "../styles"; +import { DevantConnectionFlow, DevantTempConfig } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { generateInitialConnectionName, isValidDevantConnName } from "./utils"; + +const Row = styled.div<{}>` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; +`; + +export const ButtonContainer = styled.div<{}>` + display: flex; + flex-direction: row; + flex-grow: 1; + justify-content: flex-end; +`; + +const BoxGroup = styled.div` + display: flex; + width: 100%; + flex-wrap: wrap; +`; + +const RowTitle = styled.div` + display: flex; + gap: 2px; + align-items: center; +`; + +export const getPossibleVisibilities = (marketplaceItem: MarketplaceItem, project: Project) => { + const { connectionSchemas = [], visibility: visibilities = [] } = marketplaceItem ?? {}; + const filteredVisibilities = visibilities.filter((item) => { + if (item === ServiceInfoVisibilityEnum.Project) { + return marketplaceItem.projectId === project.id; + } + return item; + }); + /** + * + * There can be services with multiple visibilities but only with one schema. + * [PROJECT, ORGANIZATION] => Default OAuth Connection - Organization + * + * In this case, the visibilities should be filtered to only include the one that mathces the schema. + * + * If the schema is Unsecured, the visibilities should be filtered to include only Organization and Public + * else, the visibilities should be filtered to include only the visibilities that match the schema name. + */ + if (connectionSchemas.length === 1 && filteredVisibilities.length > 1) { + return filteredVisibilities.filter((v) => { + const connectionSchemaName = connectionSchemas[0].name.toLowerCase(); + if (connectionSchemaName.includes("Unsecured".toLowerCase())) { + return v === ServiceInfoVisibilityEnum.Organization || v === ServiceInfoVisibilityEnum.Public; + } + return connectionSchemaName.includes(v.toLowerCase()); + }); + } + return filteredVisibilities; +}; + +export const getInitialVisibility = (item: MarketplaceItem, visibilities: string[] = []) => { + if (item?.isThirdParty) { + return ServiceInfoVisibilityEnum.Public; + } + if (visibilities.includes(ServiceInfoVisibilityEnum.Project)) { + return ServiceInfoVisibilityEnum.Project; + } + if (visibilities.includes(ServiceInfoVisibilityEnum.Organization)) { + return ServiceInfoVisibilityEnum.Organization; + } + return ServiceInfoVisibilityEnum.Public; +}; + +const getPossibleSchemas = ( + item: MarketplaceItem, + selectedVisibility: string, + connectionSchemas: MarketplaceItemSchema[] = [], +) => { + if (!item) { + return []; + } + // If third party, return schemas without filtering + if (item.isThirdParty) { + return item.connectionSchemas; + } + // Set the filtered schemas based on the selected visibility + // organization and public visibilities can have + // Oauth2, api key or unauthenaticated + // project visibility can have only project + const schemasFiltered = connectionSchemas.filter((schema) => { + if ( + selectedVisibility.toLowerCase().includes("organization") || + selectedVisibility.toLowerCase().includes("public") + ) { + return ( + schema.name.toLowerCase().includes(selectedVisibility.toLowerCase()) || + schema.name.toLowerCase().includes("unsecured") + ); + } + return schema.name.toLowerCase().includes("project"); + }); + return schemasFiltered; +}; + +interface CreateConnectionForm { + name?: string; + visibility?: string; + schemaId?: string; + isProjectLevel?: boolean; + envKeys?: string[]; +} + +interface DevantConnectorCreateFormProps { + marketplaceItem: MarketplaceItem | undefined; + projectPath: string; + devantConfigs: DevantTempConfig[]; + devantFlow: DevantConnectionFlow; + existingDevantConnNames?: string[]; + biConnectionNames?: string[]; + onSuccess?: (data: { connectionNode?: any; connectionName?: string }) => void; +} + +export const DevantConnectorCreateForm: FC = ({ + biConnectionNames, + marketplaceItem, + existingDevantConnNames = [], + projectPath, + onSuccess, + devantConfigs = [], +}) => { + const { platformExtState, platformRpcClient } = usePlatformExtContext(); + const [showAdvancedSection, setShowAdvancedSection] = useState(false); + + const visibilities = getPossibleVisibilities(marketplaceItem, platformExtState?.selectedContext?.project); + + const form = useForm({ + mode: "all", + defaultValues: { + name: generateInitialConnectionName( + biConnectionNames, + existingDevantConnNames, + marketplaceItem?.name || "", + ), + visibility: getInitialVisibility(marketplaceItem, visibilities), + schemaId: "", + isProjectLevel: false, + envKeys: [], + }, + }); + + useEffect(() => { + form.reset({ + name: generateInitialConnectionName( + biConnectionNames, + existingDevantConnNames, + marketplaceItem?.name || "", + ), + visibility: getInitialVisibility(marketplaceItem, visibilities), + schemaId: "", + isProjectLevel: false, + }); + }, [marketplaceItem]); + + const { mutate: createConnection, isPending: isCreatingConnection } = useMutation({ + mutationFn: async (data: CreateConnectionForm) => { + const createdConnection = await platformRpcClient?.createInternalConnection({ + componentId: isProjectLevel ? "" : platformExtState.selectedComponent?.metadata?.id, + name: data.name, + orgId: platformExtState.selectedContext?.org.id?.toString(), + orgUuid: platformExtState.selectedContext?.org?.uuid, + projectId: platformExtState.selectedContext?.project.id, + serviceSchemaId: marketplaceItem?.connectionSchemas[0]?.id || "", + serviceId: marketplaceItem?.serviceId, + serviceVisibility: getInitialVisibility(marketplaceItem, visibilities), + componentType: isProjectLevel + ? "non-component" + : getTypeForDisplayType(platformExtState.selectedComponent?.spec?.type), + componentPath: projectPath, + generateCreds: true, + }); + + const securityType = createdConnection?.schemaName?.toLowerCase()?.includes("oauth") ? "oauth" : "apikey"; + + const initializeResp = await platformRpcClient?.initializeDevantOASConnection({ + devantConfigs, + marketplaceItem: marketplaceItem!, + configurations: createdConnection.configurations, + name: data.name, + securityType, + visibility: data.visibility, + }); + + await platformRpcClient.createConnectionConfig({ + marketplaceItem: marketplaceItem, + name: createdConnection.name, + visibility: data.visibility, + componentDir: projectPath, + }); + + return initializeResp; + }, + onSuccess: (data) => { + platformRpcClient.refreshConnectionList(); + if (onSuccess) { + onSuccess(data); + } + }, + }); + + const createDevantConnection: SubmitHandler = (data) => createConnection(data); + + const selectedVisibility = form.watch("visibility"); + + const schemas = getPossibleSchemas(marketplaceItem, selectedVisibility, marketplaceItem?.connectionSchemas); + + useEffect(() => { + if (!schemas.some((item) => item.id === form.getValues("schemaId")) && schemas.length > 0) { + form.setValue("schemaId", schemas[0].id); + } + }, [schemas]); + + const isProjectLevel = form.watch("isProjectLevel"); + const selectedSchemaId = form.watch("schemaId"); + const selectedKeys = form.watch("envKeys"); + const selectedSchema = schemas?.find((schema) => schema.id === selectedSchemaId); + + useEffect(() => { + form.setValue("envKeys", selectedSchema?.entries?.map((entry) => entry.name) || []); + }, [selectedSchema]); + + const advancedConfigItems: ReactNode[] = []; + if (!marketplaceItem.isThirdParty) { + advancedConfigItems.push( + + ({ + value: item, + content: capitalizeFirstLetter(item.toLowerCase()), + }))} + {...form.register("visibility", { + validate: (value) => { + if (!value) { + return "Required"; + } + }, + })} + required + disabled={visibilities?.length === 0} + errorMsg={form.formState.errors.visibility?.message} + /> + , + + ({ value: item.id, content: item.name }))} + {...form.register("schemaId", { + validate: (value) => { + if (!value) { + return "Required"; + } + }, + })} + required + disabled={schemas?.length === 0} + errorMsg={form.formState.errors.schemaId?.message} + /> + , + ); + } + + if (platformExtState.selectedComponent) { + advancedConfigItems.push( + + { + form.setValue("isProjectLevel", checked); + }} + /> + , + ); + } + + return ( + + + + + + isValidDevantConnName(value, existingDevantConnNames, biConnectionNames), + })} + errorMsg={form.formState.errors.name?.message} + /> + + + {advancedConfigItems.length > 0 && ( + + Advanced Configurations + + {!showAdvancedSection && ( + setShowAdvancedSection(true)} + sx={{ fontSize: 12, padding: 8, color: ThemeColors.PRIMARY, gap: 4 }} + > + + Expand + + )} + {showAdvancedSection && ( + setShowAdvancedSection(false)} + sx={{ fontSize: 12, padding: 8, color: ThemeColors.PRIMARY, gap: 4 }} + > + + Collapsed + + )} + + + )} + + {showAdvancedSection && advancedConfigItems} + + {marketplaceItem?.isThirdParty && selectedSchema && ( + + + + Environment Variables{" "} + + + + {selectedSchema?.entries?.map((entry) => ( + { + form.setValue( + "envKeys", + checked + ? [...selectedKeys, entry.name] + : selectedKeys.filter((key) => key !== entry.name), + ); + }} + /> + ))} + + + + )} + + + + + {isCreatingConnection ? "Creating..." : "Create Connection"} + + + + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorList.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorList.tsx new file mode 100644 index 0000000000..4d71664877 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorList.tsx @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2026, 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, useEffect, useMemo } from "react"; +import { AvailableNode, LinePosition } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { Codicon, ProgressRing } from "@wso2/ui-toolkit"; +import { debounce } from "lodash"; +import ButtonCard from "../../../../components/ButtonCard"; +import { BodyTinyInfo } from "../../../styles"; +import { usePlatformExtContext } from "../../../../providers/platform-ext-ctx-provider"; +import { useQuery } from "@tanstack/react-query"; +import { GetMarketplaceItemsParams, MarketplaceItem } from "@wso2/wso2-platform-core"; +import { + ConnectorsGrid, + FilterButton, + FilterButtons, + Section, + SectionHeader, + SectionTitle, +} from "../AddConnectionPopup/styles"; +import { DevantConnectionFlow } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { DevantConnectionType, getKnownAvailableNode, ProgressWrap } from "./utils"; + +interface DevantConnectorListProps { + onItemSelect: ( + flow: DevantConnectionFlow | null, + item: MarketplaceItem, + availableNode: AvailableNode | undefined, + ) => void; + fileName: string; + target?: LinePosition; + searchText?: string; +} + +export function DevantConnectorList(props: DevantConnectorListProps) { + const { onItemSelect, fileName, target, searchText } = props; + const { platformExtState, platformRpcClient } = usePlatformExtContext(); + const { rpcClient } = useRpcContext(); + const [filterType, setFilterType] = useState<"all" | "internal" | "thirdParty">("all"); + + const [debouncedSearchText, setDebouncedSearchText] = useState(searchText || ""); + + const debouncedSetSearch = useMemo(() => debounce((text: string) => setDebouncedSearchText(text), 500), []); + + useEffect(() => { + debouncedSetSearch(searchText || ""); + return () => debouncedSetSearch.cancel(); + }, [searchText, debouncedSetSearch]); + + const { data: balOrgConnectors, isLoading: loadingBalOrgConnectors } = useQuery({ + queryKey: ["searchConnectors", fileName, target], + queryFn: () => + rpcClient + .getBIDiagramRpcClient() + .search({ filePath: fileName, queryMap: { limit: 60, orgName: "ballerina" }, searchKind: "CONNECTOR" }), + }); + + const handleMarketplaceItemClick = (item: MarketplaceItem) => { + // TODO: once we store the connector info in Devant side, + // we should be able to open the correct form + let availableNode: AvailableNode | undefined; + if (item.serviceType === "REST") { + availableNode = getKnownAvailableNode(balOrgConnectors?.categories, "ballerina", "http"); + } else if (item.serviceType === "GRAPHQL") { + availableNode = getKnownAvailableNode(balOrgConnectors?.categories, "ballerina", "graphql"); + } else if (item.serviceType === "SOAP") { + availableNode = getKnownAvailableNode(balOrgConnectors?.categories, "ballerina", "soap"); + } else if (item.serviceType === "GRPC") { + availableNode = getKnownAvailableNode(balOrgConnectors?.categories, "ballerina", "grpc"); + } + + if (item.isThirdParty) { + if (item.serviceType === "REST") { + onItemSelect(DevantConnectionFlow.CREATE_THIRD_PARTY_OAS, item, availableNode); + } else if (availableNode) { + onItemSelect(DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER, item, availableNode); + } else { + onItemSelect(DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR, item, availableNode); + } + } else { + if (item.serviceType === "REST") { + onItemSelect(DevantConnectionFlow.CREATE_INTERNAL_OAS, item, availableNode); + } else if (availableNode) { + onItemSelect(DevantConnectionFlow.CREATE_INTERNAL_OTHER, item, availableNode); + } else { + onItemSelect(DevantConnectionFlow.CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR, item, availableNode); + } + } + }; + + const reactQueryKey = { + org: platformExtState?.selectedContext?.org?.uuid, + project: platformExtState?.selectedContext?.project?.id, + debouncedSearch: debouncedSearchText, + isLoggedIn: platformExtState.isLoggedIn, + component: platformExtState?.selectedComponent?.metadata?.id, + }; + + const getMarketPlaceParams: GetMarketplaceItemsParams = { + limit: 24, + offset: 0, + networkVisibilityFilter: "all", + networkVisibilityprojectId: platformExtState?.selectedContext?.project?.id, + sortBy: "createdTime", + query: debouncedSearchText || undefined, + searchContent: false, + }; + + if(filterType === "internal") { + getMarketPlaceParams.isThirdParty = false; + } + if(filterType === "thirdParty") { + getMarketPlaceParams.isThirdParty = true; + getMarketPlaceParams.networkVisibilityFilter = "org,project,public"; + } + + const { data: marketplaceServices, isLoading: isLoadingMarketplace } = useQuery({ + queryKey: ["marketplace-services", filterType, reactQueryKey], + queryFn: () => + platformRpcClient?.getMarketplaceItems({ + orgId: platformExtState?.selectedContext?.org?.id?.toString(), + request: getMarketPlaceParams, + }), + enabled: platformExtState.isLoggedIn && !!platformExtState?.selectedContext?.project, + select: (data) => ({ + ...data, + data: data.data.filter( + (item) => { + if (filterType === "internal") { + return item.component?.componentId !== platformExtState?.selectedComponent?.metadata?.id + } + return true + }, + ), + }), + }); + + let emptyText = "No services running or configured in Devant"; + if (filterType === "internal") { + emptyText = "No services running in Devant"; + } else if (filterType === "thirdParty") { + emptyText = "No third party services configured in Devant"; + } + + return ( + <> +
+ + Devant Services + e.stopPropagation()}> + setFilterType("all")} + > + All + + setFilterType("internal")} + > + Internal + + setFilterType("thirdParty")} + > + Third Party + + + + handleMarketplaceItemClick(item)} + disableItems={loadingBalOrgConnectors} + /> +
+ + ); +} + +const ConnectionSection = ({ + emptyText, + loading, + data, + searchText, + onItemClick, + disableItems = false, +}: { + emptyText?: string; + loading: boolean; + data: MarketplaceItem[]; + searchText: string; + onItemClick: (item: MarketplaceItem) => void; + disableItems?: boolean; +}) => { + return ( + <> + {loading ? ( + + + + ) : ( + <> + {data?.length === 0 ? ( + <> + {searchText ? ( + + {emptyText} in your Devant organization matching with "{searchText}" + + ) : ( + + {emptyText} in your Devant organization + + )} + + ) : ( + + {data?.map((item) => { + return ( + } + onClick={() => onItemClick(item)} + disabled={disableItems} + /> + ); + })} + + )} + + )} + + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorMarketplaceInfo.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorMarketplaceInfo.tsx new file mode 100644 index 0000000000..061acb1858 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorMarketplaceInfo.tsx @@ -0,0 +1,307 @@ +/** + * Copyright (c) 2026, 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 { useQuery } from "@tanstack/react-query"; +import { VSCodePanelTab, VSCodePanelView, VSCodePanels } from "@vscode/webview-ui-toolkit/react"; +import type { ConnectionListItem, MarketplaceItem } from "@wso2/wso2-platform-core"; +import { useEffect, type FC, type ReactNode } from "react"; +import styled from "@emotion/styled"; +import { Badge, ProgressRing, Icon } from "@wso2/ui-toolkit"; +import ReactMarkdown from "react-markdown"; +import SwaggerUIReact from "swagger-ui-react"; +import "@wso2/ui-toolkit/src/styles/swagger/styles.css"; +import type SwaggerUIProps from "swagger-ui-react/swagger-ui-react"; +import { Banner } from "../../../../components/Banner"; +import { usePlatformExtContext } from "../../../../providers/platform-ext-ctx-provider"; +import { + ConnectorDetailCard, + ConnectorOptionButtons, + ConnectorOptionContent, + ConnectorOptionDescription, + ConnectorOptionIcon, + ConnectorOptionTitle, + ConnectorTypeLabel, +} from "../AddConnectionPopup/styles"; +import { + ActionButton, + ConnectorContentContainer, + ConnectorInfoContainer, + ConnectorProgressContainer, + FooterContainer, +} from "../styles"; +import { DevantConnectionFlow } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; + +const StyledSummary = styled.p` + margin-top: 1rem; + font-size: 0.75rem; +`; + +const StyledTagsContainer = styled.div` + margin-top: 0.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + opacity: 0.8; +`; + +const StyledPanelsContainer = styled.div` + margin-top: 1.25rem; +`; + +const StyledApiDefinitionContainer = styled.div` + width: 100%; +`; + +const StyledNoPreviewContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 2.5rem 1rem; + text-align: center; +`; + +const StyledNoPreviewTitle = styled.h4` + font-weight: 600; + font-size: 1.125rem; + opacity: 0.7; +`; + +const StyledNoPreviewText = styled.p` + opacity: 0.5; +`; + +export const SwaggerUI: FC = (props) => { + return ; +}; + +type Props = { + item?: MarketplaceItem; + onFlowChange: (flow: DevantConnectionFlow | null) => void; + onNextClick: () => void; + loading: boolean; + importedConnection?: ConnectionListItem; + saveButtonText?: string; +}; + +const disableAuthorizeAndInfoPlugin = () => ({ + wrapComponents: { info: () => (): any => null, authorizeBtn: () => (): any => null }, +}); + +const disableTryItOutPlugin = () => ({ + statePlugins: { + spec: { + wrapSelectors: { + servers: () => (): any[] => [], + securityDefinitions: () => (): any => null, + schemes: () => (): any[] => [], + allowTryItOutFor: () => () => false, + }, + }, + }, +}); + +export const DevantConnectorMarketplaceInfo: FC = ({ + item, + onNextClick, + onFlowChange, + loading, + importedConnection, + saveButtonText = "Continue", +}) => { + const { platformRpcClient, platformExtState } = usePlatformExtContext(); + + const { + data: serviceIdl, + error: serviceIdlError, + isLoading: isLoadingIdl, + } = useQuery({ + queryKey: [ + "marketplace_idl", + { + orgId: platformExtState?.selectedContext?.org.id, + resourceId: item?.resourceId, + serviceId: item?.serviceId, + type: item?.serviceType, + }, + ], + queryFn: () => + platformRpcClient?.getMarketplaceIdl({ + serviceId: item?.isThirdParty ? item.resourceId : item?.serviceId, + orgId: platformExtState?.selectedContext?.org?.id?.toString(), + }), + enabled: !!item, + }); + + useEffect(() => { + if (serviceIdlError || (serviceIdl && (serviceIdl?.idlType !== "OpenAPI" || !serviceIdl?.content))) { + let newFlow: DevantConnectionFlow; + if (importedConnection) { + if (item.isThirdParty) { + if (serviceIdl?.idlType === "TCP") { + newFlow = DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR; + } else { + newFlow = DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER; + } + } else { + newFlow = DevantConnectionFlow.IMPORT_INTERNAL_OTHER; + } + } else { + if (item.isThirdParty) { + if (serviceIdl?.idlType === "TCP") { + newFlow = DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR; + } else { + newFlow = DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER; + } + } else { + newFlow = DevantConnectionFlow.CREATE_INTERNAL_OTHER; + } + } + onFlowChange(newFlow); + } + }, [serviceIdl, serviceIdlError]); + + const panelTabs: { key: string; title: string; view: ReactNode }[] = [ + { + key: "api-definition", + title: "API Definition", + view: ( + + {serviceIdl?.content ? ( + <> + {serviceIdl?.idlType === "OpenAPI" ? ( + + ) : ( + + No preview available + + The IDL for this service is not available for preview. Please download the IDL + to view it. + + + )} + + ) : ( + <> + {isLoadingIdl && ( + + + + )} + {serviceIdlError && ( + + )} + + )} + + ), + }, + ]; + + if (item?.description?.trim()) { + panelTabs.unshift({ + key: "overview", + title: "Overview", + view: {item?.description?.trim()}, + }); + } + + return ( + + + + {item?.summary?.trim() && {item?.summary?.trim()}} + {(item?.tags?.length ?? 0) > 0 && ( + + {item?.tags?.map((tagItem) => ( + {tagItem} + ))} + + )} + + + {panelTabs.map((item) => ( + + {item?.title} + + ))} + {panelTabs.map((item) => ( + + {item?.view} + + ))} + + + + + + {loading ? "Loading..." : saveButtonText} + + + + ); +}; + +export const ConnectorDetailCardItem = ({ item }: { item: MarketplaceItem }) => { + return ( + + + + + + {item?.name} + {item?.description} + + {item?.serviceType && {item?.serviceType}} + {item?.version && {item?.version}} + {item?.status && {item?.status}} + + + + ); +}; + +const getYamlString = (yamlString: string) => { + try { + if (/%[0-9A-Fa-f]{2}/.test(yamlString)) { + const decoded = decodeURIComponent(yamlString); + // Basic heuristic to ensure decoding produced YAML-like content + if ( + decoded !== yamlString && + (decoded.includes("\n") || decoded.includes(":") || /openapi/i.test(decoded)) + ) { + return decoded; + } + } + return yamlString; + } catch { + return yamlString; + } +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorPopup.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorPopup.tsx new file mode 100644 index 0000000000..7a63db8589 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorPopup.tsx @@ -0,0 +1,555 @@ +/** + * Copyright (c) 2026, 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 { AvailableNode, ConfigVariable, DIRECTORY_MAP, LinePosition, ParentPopupData } from "@wso2/ballerina-core"; +import { Codicon, ProgressRing, Stepper, ThemeColors } from "@wso2/ui-toolkit"; +import { + PopupOverlay, + PopupContainer, + PopupHeader, + PopupTitle, + CloseButton, + BackButton, + HeaderTitleContainer, + PopupSubtitle, +} from "../styles"; +import { PopupContent, StepperContainer } from "../AddConnectionPopup/styles"; +import { DevantConnectorList } from "./DevantConnectorList"; +import React, { useEffect, useState } from "react"; +import { DevantConnectorMarketplaceInfo } from "./DevantConnectorMarketplaceInfo"; +import { ConnectionListItem, MarketplaceItem, ServiceInfoVisibilityEnum } from "@wso2/wso2-platform-core"; +import { DevantConnectorCreateForm } from "./DevantConnectorCreateForm"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { usePlatformExtContext } from "../../../../providers/platform-ext-ctx-provider"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { DevantConnectionFlow, DevantTempConfig } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { DevantBIConnectorCreateForm } from "./DevantBIConnectorInitForm"; +import { DevantBIConnectorSelect } from "./DevantBIConnectorSelect"; +import { AddConnectionPopupContent } from "../AddConnectionPopup/AddConnectionPopupContent"; +import { APIConnectionForm } from "../APIConnectionPopup"; +import { + DEVANT_CONNECTION_FLOWS_STEPS, + DevantConnectionFlowStep, + DevantConnectionFlowSubTitles, + DevantConnectionFlowTitles, + generateInitialConnectionName, + getKnownAvailableNode, + ProgressWrap, +} from "./utils"; +import { ModulePart, STKindChecker } from "@wso2/syntax-tree"; +import { URI } from "vscode-uri"; + +interface DevantConnectorPopupProps { + onClose?: (parent?: ParentPopupData) => void; + onNavigateToOverview: () => void; + isPopup?: boolean; + fileName: string; + target?: LinePosition; + projectPath: string; +} + +export function DevantConnectorPopup(props: DevantConnectorPopupProps) { + const { onClose, onNavigateToOverview, isPopup, fileName, target, projectPath } = props; + const { platformRpcClient, platformExtState, importConnection } = usePlatformExtContext(); + const [isCreating, setIsCreating] = useState(true); + const { rpcClient } = useRpcContext(); + const [selectedFlow, setSelectedFlow] = useState(null); + const [selectedMarketplaceItem, setSelectedMarketplaceItem] = useState(null); + const [steps, setSteps] = useState([]); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [devantConfigs, setDevantConfigs] = useState([]); + const [availableNode, setAvailableNode] = useState(); + const [IDLFilePath, setIDLFilePath] = useState(""); + const [oasConnectorName, setOasConnectorName] = useState(""); + const [importingConn, setImportingConn] = useState(); + + const goToNextStep = () => { + if (currentStepIndex < steps.length - 1) { + setCurrentStepIndex((prev) => prev + 1); + } + }; + + const goToPreviousStep = () => { + if (currentStepIndex > 0) { + setCurrentStepIndex((prev) => prev - 1); + } + }; + + useEffect(() => { + if (importConnection?.connection) { + setImportingConn(importConnection.connection); + handleInitImportConnector(importConnection.connection); + importConnection.setConnection(undefined); + setIsCreating(false); + } + }, [importConnection]); + + const { mutate: handleInitImportConnector, isPending: isLoadingImportConnectorData } = useMutation({ + mutationFn: async (connection: ConnectionListItem) => { + const balOrgConnectors = await rpcClient + .getBIDiagramRpcClient() + .search({ filePath: fileName, queryMap: { limit: 60, orgName: "ballerina" }, searchKind: "CONNECTOR" }); + const service = await platformRpcClient.getMarketplaceItem({ + orgId: platformExtState?.selectedContext?.org?.id?.toString(), + serviceId: connection.serviceId, + }); + + let availableNode: AvailableNode | undefined; + if (service.serviceType === "REST") { + availableNode = getKnownAvailableNode(balOrgConnectors?.categories, "ballerina", "http"); + } else if (service.serviceType === "GRAPHQL") { + availableNode = getKnownAvailableNode(balOrgConnectors?.categories, "ballerina", "graphql"); + } else if (service.serviceType === "SOAP") { + availableNode = getKnownAvailableNode(balOrgConnectors?.categories, "ballerina", "soap"); + } else if (service.serviceType === "GRPC") { + availableNode = getKnownAvailableNode(balOrgConnectors?.categories, "ballerina", "grpc"); + } + + setSelectedMarketplaceItem(service); + setAvailableNode(availableNode); + + if (service.isThirdParty) { + if (service.serviceType === "REST") { + setSelectedFlow(DevantConnectionFlow.IMPORT_THIRD_PARTY_OAS); + } else if (availableNode) { + setSelectedFlow(DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER); + } else { + setSelectedFlow(DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR); + } + } else { + // internal + if (service.serviceType === "REST") { + setSelectedFlow(DevantConnectionFlow.IMPORT_INTERNAL_OAS); + } else if (availableNode) { + setSelectedFlow(DevantConnectionFlow.IMPORT_INTERNAL_OTHER); + } else { + setSelectedFlow(DevantConnectionFlow.IMPORT_INTERNAL_OTHER_SELECT_BI_CONNECTOR); + } + } + }, + }); + + useEffect(() => { + if (selectedFlow) { + const flowSteps = DEVANT_CONNECTION_FLOWS_STEPS[selectedFlow] || []; + setSteps(flowSteps); + } + }, [selectedFlow]); + + const { mutate: createTempConfigs } = useMutation({ + mutationFn: async (item: MarketplaceItem) => { + const configResp = await rpcClient.getBIDiagramRpcClient().getConfigVariablesV2({ + projectPath, + includeLibraries: false, + }); + const existingConfigs = new Set(); + + const projectToml = await rpcClient.getCommonRpcClient().getCurrentProjectTomlValues(); + const configVars = (configResp.configVariables as any)?.[ + `${projectToml?.package?.org}/${projectToml?.package?.name}` + ]?.[""] as ConfigVariable[]; + configVars?.forEach((configVar) => + existingConfigs.add(configVar?.properties?.variable?.value?.toString() || ""), + ); + + const allEntries = item.connectionSchemas?.[0]?.entries || []; + const configs: DevantTempConfig[] = allEntries.map((entry) => { + let uniqueName = entry.name; + let counter = 1; + + // Check if name conflicts with existing configs or already used names + while (existingConfigs.has(uniqueName)) { + uniqueName = `${entry.name}${counter}`; + counter++; + } + + return { + id: entry.name, + name: uniqueName, + value: "", + isSecret: entry.isSensitive, + description: entry.description, + type: entry.type, + selected: true, + }; + }); + for (const [index, config] of configs.entries()) { + const resp = await platformRpcClient.addDevantTempConfig({ name: config.name, newLine: index === 0 }); + config.node = resp.configNode; + } + setDevantConfigs(configs); + }, + }); + + useEffect(() => { + if (selectedMarketplaceItem) { + createTempConfigs(selectedMarketplaceItem); + } + }, [selectedMarketplaceItem]); + + const handleClosePopup = () => { + deleteTempConfig(); + if (isPopup) { + onClose?.(); + } else { + onNavigateToOverview(); + } + }; + + const handleBackButtonClick = () => { + if (currentStepIndex > 0) { + goToPreviousStep(); + } else if (currentStepIndex === 0) { + setSelectedFlow(null); + setSelectedMarketplaceItem(null); + deleteTempConfig(); + setAvailableNode(undefined); + setOasConnectorName(""); + setIDLFilePath(""); + + // if (showDevantMarketplace && !selectedFlow) { + // setShowDevantMarketplace(false); + // } + } + }; + + const { data: biConnectionNames = [] } = useQuery({ + queryKey: ["bi-connectionNames", projectPath], + queryFn: async () => { + const biConnectionNames = new Set(); + const joinedPath = await rpcClient + .getVisualizerRpcClient() + .joinProjectPath({ segments: ["connections.bal"] }); + const stResp = await rpcClient.getLangClientRpcClient().getST({ + documentIdentifier: { uri: URI.file(joinedPath.filePath).toString() }, + }); + + for (const member of (stResp?.syntaxTree as ModulePart)?.members) { + if (STKindChecker.isModuleVarDecl(member)) { + if (STKindChecker.isCaptureBindingPattern(member.typedBindingPattern?.bindingPattern)) { + if (STKindChecker.isIdentifierToken(member.typedBindingPattern?.bindingPattern.variableName)) { + biConnectionNames.add(member.typedBindingPattern?.bindingPattern.variableName.value); + } + } + } + } + return Array.from(biConnectionNames); + }, + }); + + const { data: existingDevantConnNames = devantConfigs?.map((conn) => conn.name) || [] } = useQuery({ + queryKey: ["devant-connection-names-in-project", platformExtState?.selectedContext?.project.id], + queryFn: async () => { + const allConnNamesSet = new Set(); + const connections = await platformRpcClient?.getConnections({ + projectId: platformExtState?.selectedContext?.project.id, + orgId: platformExtState?.selectedContext?.org.id?.toString(), + componentId: "", + }); + if (connections && connections.length > 0) { + connections.forEach((conn) => allConnNamesSet.add(conn.name)); + } + const components = await platformRpcClient?.getComponentList({ + projectId: platformExtState?.selectedContext?.project.id, + orgId: platformExtState?.selectedContext?.org.id?.toString(), + orgHandle: platformExtState?.selectedContext?.org.handle || "", + projectHandle: platformExtState?.selectedContext?.project.handler || "", + }); + const allConns = await Promise.all( + components.map((comp) => + platformRpcClient.getConnections({ + projectId: platformExtState?.selectedContext?.project.id, + orgId: platformExtState?.selectedContext?.org.id?.toString(), + componentId: comp.metadata.id || "", + }), + ), + ); + allConns.forEach((conns) => conns.forEach((conn) => allConnNamesSet.add(conn.name))); + return Array.from(allConnNamesSet); + }, + enabled: !!platformExtState?.selectedContext?.project?.id, + }); + + const { mutateAsync: importInternalOASConnection, isPending: initializingOASConn } = useMutation({ + mutationFn: async () => { + const connectionDetailed = await platformRpcClient.getConnection({ + connectionGroupId: importingConn?.groupUuid, + orgId: platformExtState?.selectedContext?.org?.id?.toString(), + }); + + let visibility: ServiceInfoVisibilityEnum = ServiceInfoVisibilityEnum.Public; + if (connectionDetailed?.schemaName?.toLowerCase()?.includes("organization")) { + visibility = ServiceInfoVisibilityEnum.Organization; + } else if (connectionDetailed?.schemaName?.toLowerCase()?.includes("project")) { + visibility = ServiceInfoVisibilityEnum.Project; + } + + await platformRpcClient.createConnectionConfig({ + marketplaceItem: selectedMarketplaceItem, + name: connectionDetailed.name, + visibility: visibility, + componentDir: projectPath, + }); + + const securityType = connectionDetailed?.schemaName?.toLowerCase()?.includes("oauth") ? "oauth" : "apikey"; + const configurations = connectionDetailed?.configurations; + + const resp = await platformRpcClient.initializeDevantOASConnection({ + name: connectionDetailed.name, + marketplaceItem: selectedMarketplaceItem, + visibility, + configurations, + securityType, + devantConfigs: devantConfigs, + }); + return resp; + }, + onSuccess: (data) => { + platformRpcClient.refreshConnectionList(); + if (onClose) { + onClose({ + recentIdentifier: data.connectionName, + artifactType: DIRECTORY_MAP.CONNECTION, + }); + } + }, + }); + + const { mutate: generateCustomConnectorFromOAS, isPending: generatingCustomConnectorFromOAS } = useMutation({ + mutationFn: async () => { + if (selectedFlow === DevantConnectionFlow.IMPORT_INTERNAL_OAS) { + await importInternalOASConnection(); + } else { + const resp = await platformRpcClient?.generateCustomConnectorFromOAS({ + marketplaceItem: selectedMarketplaceItem!, + connectionName: generateInitialConnectionName( + biConnectionNames, + existingDevantConnNames, + selectedMarketplaceItem?.name, + ), + }); + return resp; + } + }, + onSuccess: (data) => { + setAvailableNode(data?.connectionNode); + goToNextStep(); + }, + }); + + const { mutate: deleteTempConfig } = useMutation({ + mutationFn: async () => { + if (devantConfigs.length > 0) { + await platformRpcClient?.deleteDevantTempConfigs({ + nodes: devantConfigs.map((config) => config.node!), + }); + } + }, + onSettled: () => setDevantConfigs([]), + }); + + let title: string = isCreating ? "Add Connection" : "Import Connection"; + + let subTitle: string = ""; + if (selectedFlow && DevantConnectionFlowTitles[selectedFlow]) { + title = DevantConnectionFlowTitles[selectedFlow]; + subTitle = DevantConnectionFlowSubTitles[selectedFlow] || ""; + } + + const isRootLoading = isLoadingImportConnectorData; + + return ( + <> + + + + {(isCreating ? selectedFlow : currentStepIndex > 0) && ( + + + + )} + + {title} + {subTitle && {subTitle}} + + handleClosePopup()}> + + + + {selectedFlow && steps.length > 1 && ( + + + + )} + + {isRootLoading ? ( + + + + ) : ( + <> + {selectedFlow ? ( + <> + {steps.length > 0 && steps[currentStepIndex].length > 0 && ( + <> + {steps[currentStepIndex] === DevantConnectionFlowStep.VIEW_SWAGGER && ( + { + if ( + [ + DevantConnectionFlow.IMPORT_INTERNAL_OAS, + DevantConnectionFlow.CREATE_THIRD_PARTY_OAS, + ].includes(selectedFlow) + ) { + generateCustomConnectorFromOAS(); + } else { + goToNextStep(); + } + }} + onFlowChange={(flow) => setSelectedFlow(flow)} + loading={generatingCustomConnectorFromOAS || initializingOASConn} + importedConnection={importingConn} + saveButtonText={ + selectedFlow === DevantConnectionFlow.IMPORT_INTERNAL_OAS + ? "Save" + : "Continue" + } + /> + )} + {steps[currentStepIndex] === + DevantConnectionFlowStep.INIT_DEVANT_INTERNAL_OAS_CONNECTOR && ( + { + if (data.connectionName && onClose) { + onClose({ + recentIdentifier: data.connectionName, + artifactType: DIRECTORY_MAP.CONNECTION, + }); + } + }} + /> + )} + {steps[currentStepIndex] === DevantConnectionFlowStep.INIT_CONNECTOR && ( + setDevantConfigs([])} + selectedFlow={selectedFlow} + selectedMarketplaceItem={selectedMarketplaceItem} + selectedConnector={availableNode} + IDLFilePath={IDLFilePath} + biConnectionNames={biConnectionNames} + existingDevantConnNames={existingDevantConnNames} + onFlowChange={(flow) => setSelectedFlow(flow)} + importedConnection={importingConn} + projectPath={projectPath} + onAddDevantConfig={async (name, value, isSecret) => { + const resp = await platformRpcClient.addDevantTempConfig({ + name, + newLine: devantConfigs.length === 0, + }); + const newDevantConfig: DevantTempConfig = { + id: name, + name: name, + value: value, + isSecret: isSecret, + type: "string", + node: resp.configNode, + }; + setDevantConfigs([...devantConfigs, newDevantConfig]); + }} + /> + )} + {steps[currentStepIndex] === + DevantConnectionFlowStep.SELECT_BI_CONNECTOR && ( + { + setAvailableNode(availableNode); + goToNextStep(); + }} + /> + )} + {steps[currentStepIndex] === DevantConnectionFlowStep.UPLOAD_OAS && ( + { + setAvailableNode(availableNode); + goToNextStep(); + setOasConnectorName(name); + setIDLFilePath(filePath); + }} + /> + )} + + )} + + ) : ( + { + setAvailableNode(availableNode); + setSelectedFlow( + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR, + ); + }} + handleApiSpecConnection={() => { + setSelectedFlow( + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS, + ); + }} + DevantServicesSection={({ searchText }) => ( + { + setSelectedFlow(flow); + setSelectedMarketplaceItem(item); + setAvailableNode(availableNode); + }} + searchText={searchText} + /> + )} + /> + )} + + )} + + + + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/utils.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/utils.ts new file mode 100644 index 0000000000..1c69c8f6e6 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/utils.ts @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2026, 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 { AvailableNode, Category } from "@wso2/ballerina-core"; +import { DevantConnectionFlow } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import styled from "@emotion/styled"; + +export const ProgressWrap = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px; +`; + +export const DevantConnectionFlowTitles: Partial> = { + // Create related flow titles + [DevantConnectionFlow.CREATE_INTERNAL_OAS]: "Connect to Devant service", + [DevantConnectionFlow.CREATE_INTERNAL_OTHER]: "Connect to Devant service", + [DevantConnectionFlow.CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR]: "Connect to Devant service", + [DevantConnectionFlow.CREATE_THIRD_PARTY_OAS]: "Connect via API Specification", + [DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER]: "Connect to Third-Party service", + [DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR]: "Connect to Third-Party service", + [DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR]: "Connect to Third-Party service", + [DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS]: "Connect to Third-Party service", + // Import related flow titles + [DevantConnectionFlow.IMPORT_INTERNAL_OAS]: "Connect to Devant service", + [DevantConnectionFlow.IMPORT_INTERNAL_OTHER]: "Connect to Devant service", + [DevantConnectionFlow.IMPORT_INTERNAL_OTHER_SELECT_BI_CONNECTOR]: "Connect to Devant service", + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OAS]: "Use registered third party connection via API Specification", + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER]: "Use registered third party connection", + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR]: "Use registered third party connection", +}; + +export const DevantConnectionFlowSubTitles: Partial> = { + // Create related flow subtitles + [DevantConnectionFlow.CREATE_INTERNAL_OAS]: "Connect to REST API service running in Devant", + [DevantConnectionFlow.CREATE_INTERNAL_OTHER]: "Connect to service running in Devant by configuring your connector", + [DevantConnectionFlow.CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR]: + "Connect to service running in Devant by configuring your connector", + [DevantConnectionFlow.CREATE_THIRD_PARTY_OAS]: + "Connect to Third-Party REST API service by creating and mapping configurations", + [DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER]: + "Connect to Third-Party service by creating and mapping configurations", + [DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR]: + "Connect to Third-Party service by configuring your connector", + [DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR]: + "Connect to Third-Party service by configuring your Ballerina connector", + [DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS]: + "Connect to Third-Party service from API Specification", + // Import related flow subtitles + [DevantConnectionFlow.IMPORT_INTERNAL_OAS]: "Connect to REST API service running in Devant", + [DevantConnectionFlow.IMPORT_INTERNAL_OTHER]: "Connect to service running in Devant by configuring your connector", + [DevantConnectionFlow.IMPORT_INTERNAL_OTHER_SELECT_BI_CONNECTOR]: + "Connect to service running in Devant by configuring your connector", + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OAS]: + "Connect to Third-Party REST API service by creating and mapping configurations", + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER]: + "Connect to Third-Party service by creating and mapping configurations", + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR]: + "Connect to Third-Party service by configuring your connector", +}; + +export enum DevantConnectionFlowStep { + VIEW_SWAGGER = "Connection Details", + INIT_DEVANT_INTERNAL_OAS_CONNECTOR = "Create Connection", + SELECT_BI_CONNECTOR = "Select Connector", + INIT_CONNECTOR = "Initialize Connector", + SELECT_OR_CREATE_BI_CONNECTOR = "Select or Create Connector", + UPLOAD_OAS = "Upload Specification", +} + +export const DEVANT_CONNECTION_FLOWS_STEPS: Partial> = { + // Connection creation flow steps + [DevantConnectionFlow.CREATE_INTERNAL_OAS]: [ + DevantConnectionFlowStep.VIEW_SWAGGER, + DevantConnectionFlowStep.INIT_DEVANT_INTERNAL_OAS_CONNECTOR, + ], + [DevantConnectionFlow.CREATE_INTERNAL_OTHER]: [DevantConnectionFlowStep.INIT_CONNECTOR], + [DevantConnectionFlow.CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR]: [ + DevantConnectionFlowStep.SELECT_BI_CONNECTOR, + DevantConnectionFlowStep.INIT_CONNECTOR, + ], + [DevantConnectionFlow.CREATE_THIRD_PARTY_OAS]: [ + DevantConnectionFlowStep.VIEW_SWAGGER, + DevantConnectionFlowStep.INIT_CONNECTOR, + ], + [DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER]: [DevantConnectionFlowStep.INIT_CONNECTOR], + [DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR]: [ + DevantConnectionFlowStep.SELECT_BI_CONNECTOR, + DevantConnectionFlowStep.INIT_CONNECTOR, + ], + [DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR]: [DevantConnectionFlowStep.INIT_CONNECTOR], + [DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS]: [ + DevantConnectionFlowStep.UPLOAD_OAS, + DevantConnectionFlowStep.INIT_CONNECTOR, + ], + // Connection importing flow steps + [DevantConnectionFlow.IMPORT_INTERNAL_OAS]: [DevantConnectionFlowStep.VIEW_SWAGGER], + [DevantConnectionFlow.IMPORT_INTERNAL_OTHER]: [DevantConnectionFlowStep.INIT_CONNECTOR], + [DevantConnectionFlow.IMPORT_INTERNAL_OTHER_SELECT_BI_CONNECTOR]: [ + DevantConnectionFlowStep.SELECT_BI_CONNECTOR, + DevantConnectionFlowStep.INIT_CONNECTOR, + ], + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OAS]: [ + DevantConnectionFlowStep.VIEW_SWAGGER, + DevantConnectionFlowStep.INIT_CONNECTOR, + ], + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER]: [DevantConnectionFlowStep.INIT_CONNECTOR], + [DevantConnectionFlow.IMPORT_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR]: [ + DevantConnectionFlowStep.SELECT_BI_CONNECTOR, + DevantConnectionFlowStep.INIT_CONNECTOR, + ], +}; + +export enum DevantConnectionType { + INTERNAL = "INTERNAL", + THIRD_PARTY = "THIRD_PARTY", + DATABASE = "DATABASE", +} + +/** + * Generates a unique name that doesn't exist in either biConnectorNames or devantConnectorNames. + * If the candidate name exists, appends a numeric suffix and tries again. + * + * @param biConnectorNames - Array of existing BI connector names + * @param devantConnectorNames - Array of existing Devant connector names + * @param candidateName - The initial name to try + * @returns A unique name that doesn't conflict with existing names + */ +export const generateInitialConnectionName = ( + biConnectorNames: string[], + devantConnectorNames: string[], + candidateName: string, +): string => { + // Create a Set of all existing names (case-insensitive) for O(1) lookup + const existingNames = new Set([ + ...biConnectorNames, + ...devantConnectorNames, + ...devantConnectorNames.map((name) => name.replaceAll("-", "_")), + ]); + + const newCandidateName = candidateName?.replaceAll(" ", "_").replaceAll("-", "_") || "my_connection"; + let uniqueName = newCandidateName; + let counter = 1; + + // Keep incrementing counter until we find a unique name + while (existingNames.has(uniqueName)) { + uniqueName = `${newCandidateName}${counter}`; + counter++; + } + + return uniqueName; +}; + +export const isValidDevantConnName = (value: string, devantConnNames: string[], biConnNames: string[]) => { + // Check minimum length + if (!value) { + return "Connection name is required"; + } + if (value.length < 3) { + return "Connection name must be at least 3 characters long"; + } + + if (value.length > 50) { + return "Connection Name is too long"; + } + + // Check for valid format: alphanumeric and underscores only, can't start with number + const validNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + if (!validNameRegex.test(value)) { + if (/^[0-9]/.test(value)) { + return "Connection name cannot start with a number"; + } + return "Connection name can only contain letters, numbers, and underscores"; + } + + // Check for duplicates in Devant connections + if (devantConnNames?.some((conn) => conn === value)) { + return "A Devant connection with this name already exists"; + } + + // Check for duplicates in BI connections + if (biConnNames?.some((conn) => conn === value)) { + return "Duplicate connection name"; + } +}; + +export const getKnownAvailableNode = (categories: Category[], org: string, module: string) => { + const networkConnectors = categories?.find((item) => item.metadata.label === "Network"); + const matchingNode = networkConnectors?.items?.find( + (item) => (item as AvailableNode).codedata?.org === org && (item as AvailableNode).codedata?.module === module, + ); + return matchingNode as AvailableNode | undefined; +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/styles.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/styles.ts index a4b1c31711..3536fcc6e8 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/styles.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/styles.ts @@ -90,6 +90,48 @@ export const PopupContent = styled.div` gap: 16px; `; +export const FooterContainer = styled.div` + position: sticky; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 10; +`; + +export const ActionButton = styled(Button)` + width: 100% !important; + min-width: 0 !important; + display: flex !important; + justify-content: center; + align-items: center; +`; + +export const ConnectorInfoContainer = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + height: 100%; + min-height: 0; +`; + +export const ConnectorContentContainer = styled.div<{ hasFooterButton?: boolean }>` + flex: 1; + display: flex; + flex-direction: column; + overflow: auto; + padding-bottom: ${(props: { hasFooterButton?: boolean }) => props.hasFooterButton ? "0" : "24px"}; + min-height: 0; +`; + +export const ConnectorProgressContainer = styled.p` + display: flex; + padding: 50px; + justify-content: center; + align-items: center; +`; + export const PopupFooter = styled.div` padding: 16px 20px; display: flex; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/DiagramWrapper/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/DiagramWrapper/index.tsx index 2acbacb6ee..7c6ac4dc8b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/DiagramWrapper/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/DiagramWrapper/index.tsx @@ -107,10 +107,13 @@ export function DiagramWrapper(param: DiagramWrapperProps) { const [listener, setListener] = useState(""); const [parentMetadata, setParentMetadata] = useState(); const [currentPosition, setCurrentPosition] = useState(); + const [parentCodedata, setParentCodedata] = useState(); const [functionModel, setFunctionModel] = useState(); const [servicePosition, setServicePosition] = useState(); const [isSaving, setIsSaving] = useState(false); + const [isTracingEnabled, setIsTracingEnabled] = useState(false); + const [isToggling, setIsToggling] = useState(false); useEffect(() => { rpcClient.getVisualizerLocation().then((location) => { @@ -164,6 +167,37 @@ export function DiagramWrapper(param: DiagramWrapperProps) { }, [rpcClient]); + useEffect(() => { + checkTracingStatus(); + }, []); + + const checkTracingStatus = async () => { + try { + const status = await rpcClient.getAgentChatRpcClient().getTracingStatus(); + setIsTracingEnabled(status.enabled); + } catch (error) { + setIsTracingEnabled(false); + } + }; + + const handleToggleTracing = async () => { + if (isToggling) { + return; + } + + setIsToggling(true); + try { + const command = isTracingEnabled ? "ballerina.disableTracing" : "ballerina.enableTracing"; + await rpcClient.getCommonRpcClient().executeCommand({ commands: [command] }); + await checkTracingStatus(); + } catch (error) { + console.error("Failed to toggle tracing:", error); + throw error; + } finally { + setIsToggling(false); + } + }; + const handleFunctionClose = () => { setFunctionModel(undefined); }; @@ -176,7 +210,7 @@ export function DiagramWrapper(param: DiagramWrapperProps) { setLoadingDiagram(true); }; - const handleReadyDiagram = (fileName?: string, parentMetadata?: ParentMetadata, position?: NodePosition) => { + const handleReadyDiagram = (fileName?: string, parentMetadata?: ParentMetadata, position?: NodePosition, parentCodedata?: CodeData) => { setLoadingDiagram(false); if (fileName) { setFileName(fileName); @@ -187,6 +221,9 @@ export function DiagramWrapper(param: DiagramWrapperProps) { if (position) { setCurrentPosition(position); } + if (parentCodedata) { + setParentCodedata(parentCodedata); + } }; const getFunctionModel = async () => { @@ -227,6 +264,35 @@ export function DiagramWrapper(param: DiagramWrapperProps) { }; const handleEdit = (fileUri?: string, position?: NodePosition) => { + const isTestFunction = parentCodedata?.sourceCode.includes("@test:Config"); + const isAIEvaluation = isTestFunction && parentCodedata?.sourceCode.includes('"evaluations"'); + + if (isAIEvaluation) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { + view: MACHINE_VIEW.BIAIEvaluationForm, + identifier: parentMetadata?.label || "", + documentUri: fileUri, + serviceType: 'UPDATE_TEST', + } + }); + return; + } + + if (isTestFunction) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { + view: MACHINE_VIEW.BITestFunctionForm, + identifier: parentMetadata?.label || "", + documentUri: fileUri, + serviceType: 'UPDATE_TEST', + } + }); + return; + } + const context: VisualizerLocation = { view: view === FOCUS_FLOW_DIAGRAM_VIEW.NP_FUNCTION @@ -256,6 +322,9 @@ export function DiagramWrapper(param: DiagramWrapperProps) { const getTitle = () => { if (isNPFunction) return "Natural Function"; if (isAutomation) return "Automation"; + if (parentCodedata?.sourceCode.includes("@ai:AgentTool")) return "Agent Tool"; + if ((parentCodedata?.sourceCode.includes("@test:Config")) && parentCodedata?.sourceCode.includes("\"evaluations\"")) return "AI Evaluation"; + if (parentCodedata?.sourceCode.includes("@test:Config")) return "Test"; return parentMetadata?.kind || ""; }; @@ -269,19 +338,37 @@ export function DiagramWrapper(param: DiagramWrapperProps) { // Calculate actions based on conditions const getActions = () => { + const tracingButton = ( + + + {isTracingEnabled ? "Tracing: On" : "Tracing: Off"} + + ); + if (isAgent) { return ( - handleResourceTryIt(parentMetadata?.accessor || "", parentMetadata?.label || "")} - > - - Chat - + <> + {tracingButton} + handleResourceTryIt(parentMetadata?.accessor || "", parentMetadata?.label || "")} + > + + Chat + + ); } 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 c88f89cd86..13efcf13f4 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/PanelManager.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/PanelManager.tsx @@ -40,6 +40,7 @@ import { FormSubmitOptions } from "."; import { ConnectionConfig, ConnectionCreator, ConnectionSelectionList, ConnectionKind } from "../../../components/ConnectionSelector"; import { RelativeLoader } from "../../../components/RelativeLoader"; import { LoaderContainer } from "../../../components/RelativeLoader/styles"; +import { ConnectionListItem } from "@wso2/wso2-platform-core"; const Container = styled.div` display: flex; @@ -145,6 +146,11 @@ interface PanelManagerProps { onAddMcpServer?: (node: FlowNode) => void; onSelectNewConnection?: (nodeId: string, metadata?: any) => void; onUpdateNodeWithConnection?: (selectedNode: FlowNode) => void; + + // Devant handlers + onImportDevantConn?: (devantConn: ConnectionListItem) => void + onLinkDevantProject?: () => void; + onRefreshDevantConnections?: () => void; } export function PanelManager(props: PanelManagerProps) { @@ -200,7 +206,9 @@ export function PanelManager(props: PanelManagerProps) { onSelectNewConnection, onUpdateNodeWithConnection, onNavigateToPanel, - onChangeSelectedNode + onImportDevantConn, + onLinkDevantProject, + onRefreshDevantConnections, } = props; const handleOnBackToAddTool = () => { @@ -246,6 +254,9 @@ export function PanelManager(props: PanelManagerProps) { onSelect={onSelectNode} onAddConnection={onAddConnection} onClose={onClose} + onImportDevantConn={onImportDevantConn} + onLinkDevantProject={onLinkDevantProject} + onRefreshDevantConnections={onRefreshDevantConnections} /> ); 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 d2c64f5cc0..71c84191d6 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx @@ -61,6 +61,7 @@ import { convertEmbeddingProviderCategoriesToSidePanelCategories, convertDataLoaderCategoriesToSidePanelCategories, convertChunkerCategoriesToSidePanelCategories, + enrichCategoryWithDevant, convertKnowledgeBaseCategoriesToSidePanelCategories, } from "../../../utils/bi"; import { useDraftNodeManager } from "./hooks/useDraftNodeManager"; @@ -77,11 +78,12 @@ import { ConnectionKind } from "../../../components/ConnectionSelector"; import { findFlowNodeByModuleVarName, getAgentFilePath, - removeAgentNode, removeToolFromAgentNode, } from "../AIChatAgent/utils"; import { DiagramSkeleton } from "../../../components/Skeletons"; import { AI_COMPONENT_PROGRESS_MESSAGE, AI_COMPONENT_PROGRESS_MESSAGE_TIMEOUT, GET_DEFAULT_MODEL_PROVIDER, LOADING_MESSAGE } from "../../../constants"; +import { ConnectionListItem } from "@wso2/wso2-platform-core"; +import { usePlatformExtContext } from "../../../providers/platform-ext-ctx-provider"; const Container = styled.div` width: 100%; @@ -93,7 +95,7 @@ export interface BIFlowDiagramProps { breakpointState?: number; syntaxTree?: STNode; onUpdate: () => void; - onReady: (fileName: string, parentMetadata?: ParentMetadata, position?: NodePosition) => void; + onReady: (fileName: string, parentMetadata?: ParentMetadata, position?: NodePosition, parentCodedata?: CodeData) => void; onSave?: () => void; } @@ -119,7 +121,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { const [suggestedModel, setSuggestedModel] = useState(); const [showSidePanel, setShowSidePanel] = useState(false); const [sidePanelView, setSidePanelView] = useState(SidePanelView.NODE_LIST); - const [categories, setCategories] = useState([]); + const [categories, setCategories] = useState([]); // const [fetchingAiSuggestions, setFetchingAiSuggestions] = useState(false); const [showProgressIndicator, setShowProgressIndicator] = useState(false); const [showProgressSpinner, setShowProgressSpinner] = useState(false); @@ -130,6 +132,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { const [selectedMcpToolkitName, setSelectedMcpToolkitName] = useState(undefined); const [selectedConnectionKind, setSelectedConnectionKind] = useState(); const [selectedNodeId, setSelectedNodeId] = useState(); + const [importingConn, setImportingConn] = useState(); const [projectOrg, setProjectOrg] = useState(""); const [isUserAuthenticated, setIsUserAuthenticated] = useState(false); @@ -169,6 +172,25 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { const isCreatingNewDataLoader = useRef(false); const isCreatingNewChunker = useRef(false); + const { platformExtState, platformRpcClient, onLinkDevantProject, importConnection: importDevantConn } = usePlatformExtContext() + + const enrichedCategories = useMemo(()=>{ + return enrichCategoryWithDevant(platformExtState?.devantConns?.list, categories, importingConn) + },[platformExtState, categories, importingConn]) + + const handleClickImportDevantConn = (data: ConnectionListItem) => { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { + view: MACHINE_VIEW.AddConnectionWizard, + documentUri: model.fileName, + metadata: { target: targetRef.current.startLine }, + }, + isPopup: true, + }); + importDevantConn.setConnection(data) + } + useEffect(() => { debouncedGetFlowModelForBreakpoints(); }, [breakpointState]); @@ -200,6 +222,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { setShowProgressIndicator(true); if (parent.artifactType === DIRECTORY_MAP.CONNECTION) { updateConnectionWithNewItem(parent.recentIdentifier); + platformRpcClient?.refreshConnectionList(); } fetchNodesAndAISuggestions(topNodeRef.current, targetRef.current, false, false); } @@ -519,9 +542,9 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { } updateAgentModelTypes(model?.flowModel); setModel(model.flowModel); - const parentMetadata = model.flowModel.nodes.find( - (node) => node.codedata.node === "EVENT_START" - )?.metadata.data as ParentMetadata | undefined; + const eventStartNode = model.flowModel.nodes.find((node) => node.codedata.node === "EVENT_START"); + const parentMetadata = eventStartNode?.metadata.data as ParentMetadata | undefined; + const parentCodedata = eventStartNode?.codedata; if (shouldUpdateLineRangeRef.current) { const varName = typeof updatedNodeRef.current?.properties?.variable?.value === "string" ? updatedNodeRef.current.properties.variable.value @@ -535,7 +558,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { // Get visualizer location and pass position to onReady rpcClient.getVisualizerLocation().then((location: VisualizerLocation) => { console.log(">>> Visualizer location", location?.position); - onReady(model.flowModel.fileName, parentMetadata, location?.position); + onReady(model.flowModel.fileName, parentMetadata, location?.position, parentCodedata); }); } }) @@ -2513,7 +2536,7 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { showSidePanel={showSidePanel} sidePanelView={sidePanelView} subPanel={subPanel} - categories={categories} + categories={enrichedCategories} selectedNode={selectedNodeRef.current} parentNode={parentNodeRef.current} nodeFormTemplate={nodeTemplateRef.current} @@ -2570,6 +2593,14 @@ export function BIFlowDiagram(props: BIFlowDiagramProps) { onSelectNewConnection={handleOnSelectNewConnection} selectedMcpToolkitName={selectedMcpToolkitName} onNavigateToPanel={handleOnNavigateToPanel} + // Devant specific callbacks + onImportDevantConn={handleClickImportDevantConn} + onLinkDevantProject={!platformExtState?.selectedContext?.project ? onLinkDevantProject : undefined} + onRefreshDevantConnections={ + platformExtState?.selectedContext?.project && !platformExtState?.devantConns?.loading + ? () => platformRpcClient?.refreshConnectionList() + : undefined + } /> diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/ForkForm/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/ForkForm/index.tsx index 0f049c0e91..4dca219d93 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/ForkForm/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/ForkForm/index.tsx @@ -19,6 +19,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { Button, Codicon, FormExpressionEditorRef, LinkButton, ThemeColors, Typography } from "@wso2/ui-toolkit"; +import styled from "@emotion/styled"; import { FlowNode, @@ -29,14 +30,33 @@ import { FormDiagnostics, Diagnostic, ExpressionProperty, + Property, + NodeProperties } from "@wso2/ballerina-core"; import { FormValues, ExpressionEditor, ExpressionFormField, FormExpressionEditorProps, + Form, + TypeEditor, + EditorFactory, + Provider, + FormField, } from "@wso2/ballerina-side-panel"; import { FormStyles } from "../styles"; + +const FieldGroup = styled.div` + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + padding: 10px; + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + border-radius: 6px; + position: relative; + padding-right: 10px; +`; import { convertNodePropertyToFormField, removeDuplicateDiagnostics } from "../../../../utils/bi"; import { cloneDeep, debounce } from "lodash"; import { RemoveEmptyNodesVisitor, traverseNode } from "@wso2/bi-diagram"; @@ -45,6 +65,7 @@ import { useRpcContext } from "@wso2/ballerina-rpc-client"; interface ForkFormProps { fileName: string; node: FlowNode; + openRecordEditor: (isOpen: boolean, f: FormValues, editingField?: FormField, newType?: string | NodeProperties) => void; targetLineRange: LineRange; expressionEditor: FormExpressionEditorProps; onSubmit: (node?: FlowNode) => void; @@ -61,6 +82,7 @@ export function ForkForm(props: ForkFormProps) { node, targetLineRange, expressionEditor, + openRecordEditor, onSubmit, openSubPanel, updatedExpressionField, @@ -76,7 +98,9 @@ export function ForkForm(props: ForkFormProps) { handleSubmit, setError, clearErrors, - formState: { isValidating }, + register, + unregister, + formState: { isValidating, errors }, } = useForm(); const { rpcClient } = useRpcContext(); @@ -129,6 +153,10 @@ export function ForkForm(props: ForkFormProps) { const variableValue = branch.properties.variable.value; setValue(`branch-${index}`, variableValue || ""); } + if (branch.properties?.type) { + const typeValue = branch.properties.type.value; + setValue(`branch-${index}-type`, typeValue || ""); + } }); return () => { @@ -158,6 +186,10 @@ export function ForkForm(props: ForkFormProps) { if (variableValue) { branch.properties.variable.value = variableValue; } + const typeValue = data[`branch-${index}-type`]?.trim(); + if (typeValue !== undefined) { + branch.properties.type.value = typeValue; + } }); updatedNode.branches = branches; @@ -209,6 +241,7 @@ export function ForkForm(props: ForkFormProps) { }; setValue(`branch-${branches.length}`, "worker" + (branches.length + 1)); + setValue(`branch-${branches.length}-type`, ""); // add new branch to end of the current branches setBranches([...branches, newBranch]); }; @@ -226,6 +259,8 @@ export function ForkForm(props: ForkFormProps) { for (let i = index + 1; i < branches.length; i++) { const value = getValues(`branch-${i}`); setValue(`branch-${i - 1}`, value); + const typeValue = getValues(`branch-${i}-type`); + setValue(`branch-${i - 1}-type`, typeValue); } }; @@ -299,56 +334,115 @@ export function ForkForm(props: ForkFormProps) { return hasDiagnostics; }, [diagnosticsInfo]); + const handleGetExpressionDiagnostics = async ( + showDiagnostics: boolean, + expression: string, + key: string, + property: ExpressionProperty + ) => { + await expressionEditor?.getExpressionFormDiagnostics?.( + showDiagnostics, + expression, + key, + property, + handleSetDiagnosticsInfo + ); + }; + + const getFormValues = useCallback(() => { + const formValues: FormValues = { ...getValues() }; + branches.forEach((branch, index) => { + formValues[`branch-${index}`] = getValues(`branch-${index}`); + formValues[`branch-${index}-type`] = getValues(`branch-${index}-type`); + }); + return formValues; + }, [getValues, branches]); + const disableSaveButton = !isValid || isValidating || showProgressIndicator; // TODO: support multiple type fields return ( - - {branches.map((branch, index) => { - if (branch.properties?.variable) { - const field = convertNodePropertyToFormField(`branch-${index}`, branch.properties.variable); - field.label = "Worker " + (index + 1); // TODO: remove this - return ( - - 2 ? () => removeWorker(index) : undefined} - completions={activeEditor === index ? expressionEditor.completions : []} - triggerCharacters={expressionEditor.triggerCharacters} - retrieveCompletions={expressionEditor.retrieveCompletions} - extractArgsFromFunction={expressionEditor.extractArgsFromFunction} - getExpressionEditorDiagnostics={handleExpressionEditorDiagnostics} - onFocus={() => handleEditorFocus(index)} - onCompletionItemSelect={expressionEditor.onCompletionItemSelect} - onCancel={expressionEditor.onCancel} - onBlur={expressionEditor.onBlur} - /> - - ); - } - })} - - - - Add Worker - - - {onSubmit && ( - - - - )} - + { }, + removeLastPopup: () => { }, + closePopup: () => { }, + }} + nodeInfo={{ kind: node.codedata.node }} + > + + {branches.map((branch, index) => { + if (branch.properties?.variable) { + const variableField = convertNodePropertyToFormField(`branch-${index}`, branch.properties.variable); + variableField.types = [{ fieldType: "IDENTIFIER", selected: false }] + const typeField = convertNodePropertyToFormField(`branch-${index}-type`, branch.properties.type); + typeField.types = [{ fieldType: "TYPE", selected: false }] + variableField.label = "Worker " + (index + 1); + return ( + + +
+ +
+
+ + openRecordEditor && openRecordEditor(open, getFormValues(), typeField, newType) + } + /> +
+ +
+ {branches.length > 2 && ( + { + if (branches.length > 2) { + removeWorker(index); + } + }} + /> + )} +
+
+
+ ); + } + })} + + + + Add Worker + + + {onSubmit && ( + + + + )} +
+
); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx index b0c63ebb0d..851f5148b8 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx @@ -41,7 +41,9 @@ import { NodeKind, EditorConfig, InputType, - getPrimaryInputType + getPrimaryInputType, + functionKinds, + NodeProperties } from "@wso2/ballerina-core"; import { FieldDerivation, @@ -54,6 +56,7 @@ import { FormImports, HelperpaneOnChangeOptions, InputMode, + ExpressionEditorDevantProps, } from "@wso2/ballerina-side-panel"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { @@ -79,6 +82,7 @@ import { removeDuplicateDiagnostics, updateLineRange, convertRecordTypeToCompletionItem, + convertItemsToCompletionItems, } from "../../../../utils/bi"; import IfForm from "../IfForm"; import { cloneDeep, debounce } from "lodash"; @@ -103,7 +107,7 @@ import DynamicModal from "../../../../components/Modal"; import React from "react"; import { SidePanelView } from "../../FlowDiagram/PanelManager"; import { ConnectionKind } from "../../../../components/ConnectionSelector"; -import { getImportedTypes } from "../../TypeEditor/utils"; +import { getFilteredTypesByKind } from "../../TypeEditor/utils"; import { useModalStack } from "../../../../Context"; interface TypeEditorState { @@ -148,6 +152,8 @@ interface FormProps { fieldOverrides?: Record>; footerActionButton?: boolean; // Render save button as footer action button derivedFields?: FieldDerivation[]; // Configuration for auto-deriving field values from other fields + devantExpressionEditor?: ExpressionEditorDevantProps; + customValidator?: (fieldKey: string, value: any, allValues: FormValues) => string | undefined; // Custom validation function for form fields } // Styled component for the action button description @@ -223,6 +229,7 @@ export const FormGenerator = forwardRef(func injectedComponents, fieldPriority, footerActionButton, + customValidator, } = props; const { rpcClient } = useRpcContext(); @@ -269,7 +276,7 @@ export const FormGenerator = forwardRef(func ); const isDataMapperCreationNode = node && node.codedata.node === "DATA_MAPPER_CREATION"; const isFunctionCreationNode = node && node.codedata.node === "FUNCTION_CREATION"; - + return isAgentNode || isDataMapperCreationNode || isFunctionCreationNode; }, [node]); @@ -488,6 +495,15 @@ export const FormGenerator = forwardRef(func } else { updatedField.diagnostics = []; } + if (customValidator) { + const customValidationMessage = customValidator(field.key, data[field.key], data); + if (customValidationMessage) { + updatedField.diagnostics = [...updatedField.diagnostics, { + message: customValidationMessage, + severity: "ERROR" + }] + } + } return updatedField; }); setBaseFields(updatedFields); @@ -533,7 +549,7 @@ export const FormGenerator = forwardRef(func await rpcClient.getVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: context }); }; - const handleOpenTypeEditor = (isOpen: boolean, f: FormValues, editingField?: FormField) => { + const handleOpenTypeEditor = (isOpen: boolean, f: FormValues, editingField?: FormField, newType?: string | NodeProperties) => { // Get f.value and assign that value to field value const updatedFields = fields.map((field) => { const updatedField = { ...field }; @@ -543,7 +559,8 @@ export const FormGenerator = forwardRef(func return updatedField; }); setBaseFields(updatedFields); - setTypeEditorState({ isOpen, fieldKey: editingField?.key, newTypeValue: f[editingField?.key] }); + const newTypeValue = typeof newType === 'string' ? newType : f[editingField?.key]; + setTypeEditorState({ isOpen, fieldKey: editingField?.key, newTypeValue }); }; const handleTypeEditorStateChange = (state: boolean) => { @@ -681,14 +698,38 @@ export const FormGenerator = forwardRef(func ) => { let visibleTypes: CompletionItem[] = types; if (!types.length) { - const types = await rpcClient.getBIDiagramRpcClient().getVisibleTypes({ + const searchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: fileName, + position: targetLineRange, + queryMap: { + q: '', + offset: 0, + limit: 1000 + }, + searchKind: 'TYPE' + }); + + const allItems = searchResponse.categories.flatMap(category => + category.items.flatMap(item => { + if ('codedata' in item) { + return [item]; + } else if ('items' in item) { + return item.items; + } + return []; + }) + ); + + const basicTypes = await rpcClient.getBIDiagramRpcClient().getVisibleTypes({ filePath: fileName, position: updateLineRange(targetLineRange, expressionOffsetRef.current).startLine, ...(valueTypeConstraint && { typeConstraint: valueTypeConstraint }) }); const isFetchingTypesForDM = valueTypeConstraint === "json"; - visibleTypes = convertToVisibleTypes(types, isFetchingTypesForDM); + const visibleSearchTypes = convertItemsToCompletionItems(allItems); + const visibleBasicTypes = convertToVisibleTypes(basicTypes, isFetchingTypesForDM); + visibleTypes = [...visibleSearchTypes, ...visibleBasicTypes]; setTypes(visibleTypes); } @@ -757,13 +798,26 @@ export const FormGenerator = forwardRef(func } }; + const hasPropertyDiagnosticMessages = (nodeWithDiagnostics: FlowNode | null): boolean => { + const nodeProperties = nodeWithDiagnostics?.properties; + if (!nodeProperties) { + return false; + } + + return Object.values(nodeProperties).some((property) => { + const diagnostics = property?.diagnostics?.diagnostics; + return Array.isArray(diagnostics) && diagnostics.some((diagnostic) => Boolean(diagnostic?.message?.trim())); + }); + }; + const handleFormValidation = async (data: FormValues, dirtyFields?: any): Promise => { if (node && targetLineRange && !skipFormValidation) { const updatedNode = mergeFormDataWithFlowNode(data, targetLineRange, dirtyFields); const nodeWithDiagnostics = await getFormWithDiagnostics(updatedNode); setDiagnosticsToFields(data, nodeWithDiagnostics!); - if (nodeWithDiagnostics?.diagnostics?.hasDiagnostics) { + // HACK: Ignore top-level hasDiagnostics when LS does not send property-level diagnostic messages. + if (nodeWithDiagnostics?.diagnostics?.hasDiagnostics && hasPropertyDiagnosticMessages(nodeWithDiagnostics)) { return false } } @@ -949,6 +1003,7 @@ export const FormGenerator = forwardRef(func forcedValueTypeConstraint: valueTypeConstraints, handleValueTypeConstChange: handleValueTypeConstChange, inputMode: inputMode, + devantExpressionEditor: props.devantExpressionEditor, }); }; @@ -1237,13 +1292,16 @@ export const FormGenerator = forwardRef(func searchKind: 'TYPE' }) .then((response) => { - return getImportedTypes(response.categories); + return getFilteredTypesByKind(response.categories, functionKinds.IMPORTED); }) .finally(() => { }); let type: TypeHelperItem | undefined; + if (!newTypes.length || !newTypes[0].subCategory) { + return undefined; + } for (const category of newTypes[0].subCategory) { const matchedType = findMatchedType(category.items, typeName); if (matchedType) { @@ -1363,18 +1421,58 @@ export const FormGenerator = forwardRef(func // handle fork node form if (node?.codedata.node === "FORK") { return ( - + + + { + stack.map((item, i) => + {stack.slice(0, i + 1).length > 1 && ( + + {stack.slice(0, i + 1).map((stackItem, index) => ( + + {index > 0 && /} + + {stackItem?.type?.name || "New Type"} + + + ))} + + )} +
+ { }} + isPopupTypeForm={true} + getNewTypeCreateForm={getNewTypeCreateForm} + refetchTypes={refetchStates[i]} + /> +
+
) + } +
); } @@ -1514,6 +1612,8 @@ export const FormGenerator = forwardRef(func closeRecordConfigPage(); } }} + closeOnBackdropClick={true} + closeButtonIcon="minimize" > (func closeRecordConfigPage(); } }} + closeOnBackdropClick={true} + closeButtonIcon="minimize" > void; hideSaveButton?: boolean; customDiagnosticFilter?: (diagnostics: Diagnostic[]) => Diagnostic[]; + onValidityChange?: (isValid: boolean) => void; } export function FormGeneratorNew(props: FormProps) { @@ -152,7 +153,8 @@ export function FormGeneratorNew(props: FormProps) { changeOptionalFieldTitle, onChange, hideSaveButton, - customDiagnosticFilter + customDiagnosticFilter, + onValidityChange } = props; const { rpcClient } = useRpcContext(); @@ -174,6 +176,7 @@ export function FormGeneratorNew(props: FormProps) { const importsCodedataRef = useRef(null); // To store codeData for getVisualizableFields const [fieldsValues, setFields] = useState(fields); + const fieldsRef = useRef(fields); const [formImports, setFormImports] = useState({}); const [selectedType, setSelectedType] = useState(null); const [refetchStates, setRefetchStates] = useState([false]); @@ -390,6 +393,7 @@ export function FormGeneratorNew(props: FormProps) { useEffect(() => { if (fields) { setFields(fields); + fieldsRef.current = fields; setFormImports(getImportsForFormFields(fields)); } }, [fields]); @@ -640,7 +644,7 @@ export function FormGeneratorNew(props: FormProps) { } try { - const field = fields.find(f => f.key === key); + const field = fieldsRef.current.find(f => f.key === key); if (field) { const response = await rpcClient.getBIDiagramRpcClient().getExpressionDiagnostics({ filePath: fileName, @@ -1016,6 +1020,7 @@ export function FormGeneratorNew(props: FormProps) { changeOptionalFieldTitle={changeOptionalFieldTitle} onChange={onChange} hideSaveButton={hideSaveButton} + onValidityChange={onValidityChange} /> )} { @@ -1071,6 +1076,8 @@ export function FormGeneratorNew(props: FormProps) { closeRecordConfigPage(); } }} + closeOnBackdropClick={true} + closeButtonIcon="minimize" > void; isInModal?: boolean; anchorRef: React.RefObject; @@ -54,15 +54,18 @@ type ConfigurablesPageProps = { targetLineRange: LineRange; onClose?: () => void; inputMode?: InputMode; + excludedConfigs?: string[]; + onAddNewConfigurable?: (refreshConfigVariables: () => Promise) => void; + showAddNew?: boolean; } export const Configurables = (props: ConfigurablesPageProps) => { - const { onChange, onClose, fileName, targetLineRange, inputMode } = props; + const { onChange, onClose, fileName, targetLineRange, excludedConfigs = [], onAddNewConfigurable, showAddNew = true } = props; const { rpcClient } = useRpcContext(); const { breadCrumbSteps, navigateToNext, navigateToBreadcrumb, isAtRoot } = useHelperPaneNavigation("Configurables"); - const [configVariables, setConfigVariables] = useState({}); + const [configVariables, setConfigVariables] = useState([]); const [errorMessage, setErrorMessage] = useState(''); const [configVarNode, setCofigVarNode] = useState(); const [isSaving, setIsSaving] = useState(false); @@ -81,6 +84,12 @@ export const Configurables = (props: ConfigurablesPageProps) => { isNew: true, isEnvVariable: isImportEnv }); + if (fileName.includes("/tests/")) { + if (!node.flowNode.codedata.data) { + node.flowNode.codedata.data = {}; + } + node.flowNode.codedata.data["filePath"] = fileName; + } setCofigVarNode(node.flowNode); }; @@ -88,6 +97,7 @@ export const Configurables = (props: ConfigurablesPageProps) => { }, [isImportEnv]); useEffect(() => { + setConfigVariables([]); getConfigVariables() getProjectInfo() const fetchTomlValues = async () => { @@ -106,7 +116,7 @@ export const Configurables = (props: ConfigurablesPageProps) => { }; fetchTomlValues(); - }, []) + }, [excludedConfigs]) const getProjectInfo = async () => { const visualizerContext = await rpcClient.getVisualizerLocation(); @@ -140,7 +150,24 @@ export const Configurables = (props: ConfigurablesPageProps) => { setShowContent(true); }); - setConfigVariables(data); + let configVariablesArr = translateToArrayFormat(data).filter(data => + Array.isArray(data.items) && + data.items.some(sub => Array.isArray(sub.items) && sub.items.length > 0) + ); + + configVariablesArr = configVariablesArr.map(category => ({ + ...category, + items: category.items.map(subCategory => ({ + ...subCategory, + items: subCategory.items.filter((item: ConfigVariable) => { + const value = item?.properties?.variable?.value as string; + return !excludedConfigs.includes(value); + }) + })).filter(subCategory => subCategory.items.length > 0) + })).filter(category => category.items.length > 0); + + + setConfigVariables(configVariablesArr); setErrorMessage(errorMsg); }; @@ -182,6 +209,12 @@ export const Configurables = (props: ConfigurablesPageProps) => { }; const handleAddNewConfigurable = () => { + // Use override if provided + if (onAddNewConfigurable) { + onAddNewConfigurable(getConfigVariables); + return; + } + addModal( { ) : ( <> {(() => { - let filteredCategories = translateToArrayFormat(configVariables) - .filter(category => - Array.isArray(category.items) && - category.items.some(sub => Array.isArray(sub.items) && sub.items.length > 0) - ); - + let filteredCategories = configVariables; // Apply search filter if search value exists if (searchValue && searchValue.trim()) { filteredCategories = filteredCategories.map(category => ({ @@ -324,10 +352,14 @@ export const Configurables = (props: ConfigurablesPageProps) => { )} - -
- -
+ {showAddNew && ( + <> + +
+ +
+ + )}
) } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/DevantConfigurables.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/DevantConfigurables.tsx new file mode 100644 index 0000000000..6e0a16bf17 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/DevantConfigurables.tsx @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2026, 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 { ConfigVariable } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { useState } from "react"; +import { Button, CheckBox, TextField, Typography } from "@wso2/ui-toolkit"; +import { POPUP_IDS, useModalStack } from "../../../../Context"; +import { ExpressionEditorDevantProps } from "@wso2/ballerina-side-panel"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { usePlatformExtContext } from "../../../../providers/platform-ext-ctx-provider"; +import { FooterContainer } from "../../Connection/styles"; +import { FormStyles } from "../../Forms/styles"; +import { Configurables, ConfigurablesPageProps } from "./Configurables"; + +interface DevantConfigurablesProps extends Omit { + devantExpressionEditor?: ExpressionEditorDevantProps; +} + +export const DevantConfigurables = (props: DevantConfigurablesProps) => { + const { devantExpressionEditor, onClose } = props; + const { rpcClient } = useRpcContext(); + const { addModal, closeModal } = useModalStack(); + + const { data: existingConfigVariables = [] } = useQuery({ + queryFn: async () => { + const visualizerLocation = await rpcClient.getVisualizerLocation(); + const data = await rpcClient.getBIDiagramRpcClient().getConfigVariablesV2({ + projectPath: visualizerLocation?.projectPath || "", + includeLibraries: false, + }); + const configNames: string[] = []; + const projectToml = await rpcClient.getCommonRpcClient().getCurrentProjectTomlValues(); + const configVars = (data.configVariables as any)?.[ + `${projectToml?.package?.org}/${projectToml?.package?.name}` + ]?.[""] as ConfigVariable[]; + configVars?.forEach((configVar) => + configNames.push(configVar?.properties?.variable?.value?.toString() || ""), + ); + return configNames; + }, + queryKey: ["config-variables"], + }); + + const onAddNewConfigurable = (refreshConfigVariables: () => Promise) => { + addModal( + { + props.onChange(name, false); + closeModal(POPUP_IDS.CONFIGURABLES); + refreshConfigVariables(); + }} + existingNames={[...existingConfigVariables, ...(devantExpressionEditor?.devantConfigs || [])]} + />, + POPUP_IDS.CONFIGURABLES, + "New Devant Configurable", + 400, + ); + + if (onClose) { + onClose(); + } + }; + + return ( + !(devantExpressionEditor?.devantConfigs || []).includes(config), + )} + onAddNewConfigurable={onAddNewConfigurable} + showAddNew={!!devantExpressionEditor?.onAddDevantConfig} + /> + ); +}; + +interface DevantNewConfigurableData { + name: string; + value: string; + isSecret: boolean; +} + +interface DevantNewConfigurableFormProps { + onAddDevantConfig?: (name: string, value: string, isSecret: boolean) => Promise; + onSuccess: (name: string) => void; + existingNames?: string[]; +} + +const DevantNewConfigurableForm: React.FC = ({ + onAddDevantConfig, + onSuccess, + existingNames = [], +}) => { + const [name, setName] = useState(""); + const [value, setValue] = useState(""); + const [isSecret, setIsSecret] = useState(false); + const [errors, setErrors] = useState<{ name?: string; value?: string }>({}); + + const { mutate, isPending } = useMutation({ + mutationFn: (data: DevantNewConfigurableData) => { + if (onAddDevantConfig) { + return onAddDevantConfig(data.name, data.value, data.isSecret); + } + return Promise.resolve(); + }, + onSuccess: () => { + onSuccess(name); + }, + }); + + const validateName = (nameValue: string): string | undefined => { + if (!nameValue.trim()) { + return "Name is required"; + } + // Name cannot have spaces or special characters, cannot start with a number + const validNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + if (!validNameRegex.test(nameValue)) { + return "Name must start with a letter or underscore, and contain only letters, numbers, and underscores"; + } + // Check for duplicates + if (existingNames.some((existing) => existing.toLowerCase() === nameValue.toLowerCase())) { + return "A configurable with this name already exists"; + } + return undefined; + }; + + const validateValue = (valueStr: string): string | undefined => { + if (!valueStr.trim()) { + return "Value is required"; + } + return undefined; + }; + + const handleNameChange = (newName: string) => { + setName(newName); + if (errors.name) { + setErrors((prev) => ({ ...prev, name: undefined })); + } + }; + + const handleValueChange = (newValue: string) => { + setValue(newValue); + if (errors.value) { + setErrors((prev) => ({ ...prev, value: undefined })); + } + }; + + const handleSave = () => { + const nameError = validateName(name); + const valueError = validateValue(value); + + if (nameError || valueError) { + setErrors({ name: nameError, value: valueError }); + return; + } + + mutate({ + name: name.trim(), + value: value.trim(), + isSecret, + }); + }; + + return ( + + + Create a new configurable that will be used when your integration is running in Devant + + + handleNameChange(e.target.value)} + placeholder="Enter configurable name" + errorMsg={errors.name} + sx={{ width: "100%" }} + /> + + + + handleValueChange(e.target.value)} + placeholder="Enter configurable value" + type={isSecret ? "password" : "text"} + errorMsg={errors.value} + sx={{ width: "100%" }} + /> + + + + setIsSecret(!isSecret)} /> + + + + + + + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Inputs.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Inputs.tsx index d45e6b0f05..65b141df78 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Inputs.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Inputs.tsx @@ -19,13 +19,14 @@ import { ExpandableList } from "../Components/ExpandableList" import { TypeIndicator } from "../Components/TypeIndicator" import { ExpressionProperty, LineRange } from "@wso2/ballerina-core" -import { Codicon, CompletionItem, HelperPaneCustom, SearchBox, ThemeColors, Tooltip, Typography } from "@wso2/ui-toolkit" -import { useEffect, useMemo, useState } from "react" +import { Codicon, CompletionItem, HelperPaneCustom, SearchBox, Tooltip, Typography } from "@wso2/ui-toolkit" +import { ArrayIndexBadge, ArrayIndexControls, ArrayIndexLabel, ArrayIndexRow, ArrayIndexStepButton } from "../styles/ArrayIndexStepper" +import { useEffect, useMemo, useRef, useState } from "react" import { getPropertyFromFormField, useFieldContext, InputMode } from "@wso2/ballerina-side-panel" import { ScrollableContainer } from "../Components/ScrollableContainer" import { HelperPaneIconType, getHelperPaneIcon } from "../utils/iconUtils" import { EmptyItemsPlaceHolder } from "../Components/EmptyItemsPlaceHolder" -import { shouldShowNavigationArrow } from "../utils/types" +import { isArrayOfObjectsType, shouldShowNavigationArrow } from "../utils/types" import { HelperPaneListItem } from "../Components/HelperPaneListItem" import { useHelperPaneNavigation, BreadCrumbStep } from "../hooks/useHelperPaneNavigation" import { BreadcrumbNavigation } from "../Components/BreadcrumbNavigation" @@ -44,7 +45,7 @@ type InputsPageProps = { type InputItemProps = { item: CompletionItem; onItemSelect: (value: string) => void; - onMoreIconClick: (value: string) => void; + onMoreIconClick: (item: CompletionItem) => void; } const InputItem = ({ item, onItemSelect, onMoreIconClick }: InputItemProps) => { @@ -74,7 +75,7 @@ const InputItem = ({ item, onItemSelect, onMoreIconClick }: InputItemProps) => { onItemSelect(item.label)} endAction={endAction} - onClickEndAction={() => onMoreIconClick(item.label)} + onClickEndAction={() => onMoreIconClick(item)} > {mainContent} @@ -86,7 +87,27 @@ export const Inputs = (props: InputsPageProps) => { const [searchValue, setSearchValue] = useState(""); const [isLoading, setIsLoading] = useState(true); const [showContent, setShowContent] = useState(false); - const { breadCrumbSteps, navigateToNext, navigateToBreadcrumb, isAtRoot, getCurrentNavigationPath } = useHelperPaneNavigation("Inputs"); + const { breadCrumbSteps, navigateToNext, navigateToNextArray, updateLastStepArrayIndex, navigateToBreadcrumb, isAtRoot, getCurrentNavigationPath } = useHelperPaneNavigation("Inputs"); + + const currentStep = breadCrumbSteps[breadCrumbSteps.length - 1]; + const isInArrayContext = !isAtRoot() && currentStep?.isArrayAccess === true; + const currentArrayIndex = currentStep?.arrayIndex ?? 0; + + // Local index state for immediate UI feedback; synced to navigation after debounce + const [localArrayIndex, setLocalArrayIndex] = useState(0); + const localIndexRef = useRef(0); + const indexDebounceRef = useRef | null>(null); + + // Sync local index when navigating to a new array step + useEffect(() => { + localIndexRef.current = currentArrayIndex; + setLocalArrayIndex(currentArrayIndex); + }, [currentStep?.replaceText]); + + // Cleanup debounce timer on unmount + useEffect(() => { + return () => { if (indexDebounceRef.current) clearTimeout(indexDebounceRef.current); }; + }, []); const { field, triggerCharacters } = useFieldContext(); @@ -156,8 +177,24 @@ export const Inputs = (props: InputsPageProps) => { onChange(fullPath, false); } - const handleInputsMoreIconClick = (value: string) => { - navigateToNext(value, navigationPath); + const handleInputsMoreIconClick = (item: CompletionItem) => { + const typeDetail = item?.labelDetails?.detail || item?.description; + if (isArrayOfObjectsType(typeDetail)) { + navigateToNextArray(item.label, navigationPath, 0); + } else { + navigateToNext(item.label, navigationPath); + } + } + + const handleArrayIndexStep = (delta: number) => { + const newIndex = Math.max(0, localIndexRef.current + delta); + localIndexRef.current = newIndex; + setLocalArrayIndex(newIndex); + + if (indexDebounceRef.current) clearTimeout(indexDebounceRef.current); + indexDebounceRef.current = setTimeout(() => { + updateLastStepArrayIndex(newIndex); + }, 400); } const handleBreadCrumbItemClicked = (step: BreadCrumbStep) => { @@ -193,6 +230,20 @@ export const Inputs = (props: InputsPageProps) => { breadCrumbSteps={breadCrumbSteps} onNavigateToBreadcrumb={handleBreadCrumbItemClicked} /> + {isInArrayContext && ( + + Index + + handleArrayIndexStep(-1)} disabled={localArrayIndex === 0}> + - + + {localArrayIndex} + handleArrayIndexStep(1)}> + + + + + + )} {dropdownItems.length >= 6 && (
diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/RecordConfigModal.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/RecordConfigModal.tsx index f8dba95470..6eeb372e3f 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/RecordConfigModal.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/RecordConfigModal.tsx @@ -17,7 +17,7 @@ */ import { GetRecordConfigResponse, GetRecordConfigRequest, LineRange, RecordTypeField, TypeField, RecordSourceGenRequest, RecordSourceGenResponse, GetRecordModelFromSourceRequest, GetRecordModelFromSourceResponse, ExpressionProperty, NodeKind, getPrimaryInputType, InputType } from "@wso2/ballerina-core"; -import { Dropdown, HelperPane, Typography, Button, HelperPaneHeight, FormExpressionEditorRef, ErrorBanner, ProgressRing, ThemeColors } from "@wso2/ui-toolkit"; +import { Dropdown, HelperPane, Typography, HelperPaneHeight, FormExpressionEditorRef, ErrorBanner, ProgressRing, ThemeColors } from "@wso2/ui-toolkit"; import styled from "@emotion/styled"; import { useEffect, useRef, useState, RefObject } from "react"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; @@ -74,7 +74,8 @@ export const LabelContainer = styled.div({ export const TwoColumnLayout = styled.div({ display: 'flex', gap: '16px', - height: '500px' + height: '100%', + overflow: 'hidden' }); export const LeftColumn = styled.div({ @@ -111,7 +112,9 @@ export const ExpressionEditorContainer = styled.div({ flex: '1', display: 'flex', flexDirection: 'column', - gap: '8px' + gap: '8px', + minHeight: 0, + overflow: 'hidden' }); export const ExpressionEditorLabel = styled.div({ @@ -132,13 +135,6 @@ export const ExpressionEditorDocumentation = styled.div({ } }); -export const ButtonContainer = styled.div({ - display: 'flex', - gap: '8px', - justifyContent: 'flex-end', - marginTop: '16px' -}); - export function ConfigureRecordPage(props: ConfigureRecordPageProps) { const { fileName, onChange, currentValue, recordTypeField, onClose, targetLineRange, getHelperPane, field, triggerCharacters, formContext } = props; const { rpcClient } = useRpcContext(); @@ -147,6 +143,8 @@ export function ConfigureRecordPage(props: ConfigureRecordPageProps) { const recordModelRef = useRef([]); const [selectedMemberName, setSelectedMemberName] = useState(""); const firstRender = useRef(true); + const initialMountRef = useRef(true); + const onChangeRef = useRef(onChange); const sourceCode = useRef(currentValue); const [isLoading, setIsLoading] = useState(false); // Local state for expression value - only update form on save/close @@ -223,6 +221,21 @@ export function ConfigureRecordPage(props: ConfigureRecordPageProps) { } }, [currentValue]); + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + // Auto-propagate localExpressionValue changes to parent form (remove need for Save button) + useEffect(() => { + // Skip the first render to avoid calling onChange with initial value + if (initialMountRef.current) { + initialMountRef.current = false; + return; + } + + onChangeRef.current(localExpressionValue, true); + }, [localExpressionValue]); + const fetchRecordModelFromSource = async (currentValue: string) => { setIsLoading(true); let org = ""; @@ -417,12 +430,6 @@ export function ConfigureRecordPage(props: ConfigureRecordPageProps) { } } - const handleSave = () => { - // Update the form with the current local expression value - onChange(localExpressionValue, true); - onClose(); - } - // Debounced function to fetch diagnostics const fetchDiagnostics = useRef( debounce(async (value: string) => { @@ -694,7 +701,13 @@ export function ConfigureRecordPage(props: ConfigureRecordPageProps) { }} triggerCharacters={triggerCharacters} > -
+
{formDiagnostics && formDiagnostics.length > 0 && ( d.message).join(', ')} /> @@ -714,11 +732,6 @@ export function ConfigureRecordPage(props: ConfigureRecordPageProps) { - - - diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/hooks/useHelperPaneNavigation.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/hooks/useHelperPaneNavigation.tsx index 7ebd8753e2..71962a41e1 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/hooks/useHelperPaneNavigation.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/hooks/useHelperPaneNavigation.tsx @@ -21,6 +21,9 @@ import { useState } from "react"; export type BreadCrumbStep = { label: string; replaceText: string; + isArrayAccess?: boolean; + arrayIndex?: number; + fieldName?: string; } export const useHelperPaneNavigation = (initialLabel: string) => { @@ -38,6 +41,39 @@ export const useHelperPaneNavigation = (initialLabel: string) => { setBreadCrumbSteps(newBreadCrumSteps); }; + const navigateToNextArray = (value: string, currentValue: string, index: number) => { + const separator = currentValue ? '.' : ''; + const indexedValue = `${value}[${index}]`; + const newBreadCrumSteps = [...breadCrumbSteps, { + label: indexedValue, + replaceText: currentValue + separator + indexedValue, + isArrayAccess: true, + arrayIndex: index, + fieldName: value + }]; + setBreadCrumbSteps(newBreadCrumSteps); + }; + + const updateLastStepArrayIndex = (index: number) => { + if (breadCrumbSteps.length <= 1) return; + const steps = [...breadCrumbSteps]; + const lastStep = steps[steps.length - 1]; + if (!lastStep.isArrayAccess || !lastStep.fieldName) return; + + const parentStep = steps[steps.length - 2]; + const parentPath = parentStep.replaceText; + const separator = parentPath ? '.' : ''; + const newReplaceText = parentPath + separator + lastStep.fieldName + '[' + index + ']'; + + steps[steps.length - 1] = { + ...lastStep, + arrayIndex: index, + label: lastStep.fieldName + '[' + index + ']', + replaceText: newReplaceText + }; + setBreadCrumbSteps(steps); + }; + const navigateToBreadcrumb = (step: BreadCrumbStep) => { const index = breadCrumbSteps.findIndex(item => item.label === step.label); const newSteps = index !== -1 ? breadCrumbSteps.slice(0, index + 1) : breadCrumbSteps; @@ -56,6 +92,8 @@ export const useHelperPaneNavigation = (initialLabel: string) => { return { breadCrumbSteps, navigateToNext, + navigateToNextArray, + updateLastStepArrayIndex, navigateToBreadcrumb, isAtRoot, getCurrentPath, diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/index.tsx index db23e35454..a0bd5ae542 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/index.tsx @@ -30,11 +30,12 @@ import { CreateValue } from './Views/CreateValue'; import { FunctionsPage } from './Views/Functions'; import { FormSubmitOptions } from '../FlowDiagram'; import { Configurables } from './Views/Configurables'; +import { DevantConfigurables } from './Views/DevantConfigurables'; import styled from '@emotion/styled'; import { useModalStack } from '../../../Context'; import { getDefaultValue } from './utils/types'; import { HelperPaneIconType, getHelperPaneIcon } from './utils/iconUtils'; -import { HelperpaneOnChangeOptions, InputMode } from '@wso2/ballerina-side-panel'; +import { ExpressionEditorDevantProps, HelperpaneOnChangeOptions, InputMode } from '@wso2/ballerina-side-panel'; const AI_PROMPT_TYPE = "ai:Prompt"; @@ -68,6 +69,7 @@ export type HelperPaneNewProps = { handleRetrieveCompletions: (value: string, property: ExpressionProperty, offset: number, triggerCharacter?: string) => Promise; handleValueTypeConstChange: (valueTypeConstraint: string) => void; inputMode?: InputMode; + devantExpressionEditor?: ExpressionEditorDevantProps; }; const TitleContainer = styled.div` @@ -94,7 +96,8 @@ const HelperPaneNewEl = ({ handleRetrieveCompletions, forcedValueTypeConstraint, handleValueTypeConstChange, - inputMode + inputMode, + devantExpressionEditor, }: HelperPaneNewProps) => { const [selectedItem, setSelectedItem] = useState(); const currentMenuItemCount = types ? @@ -256,8 +259,21 @@ const HelperPaneNewEl = ({
+ {devantExpressionEditor && ( + menuItemRefs.current[0] = el} + to="DEVANT_CONFIGS" + > + + {getHelperPaneIcon(HelperPaneIconType.CONFIGURABLE)} + + Devant Configs + + + + )} menuItemRefs.current[2] = el} + ref={el => menuItemRefs.current[3] = el} to="INPUTS" > @@ -268,7 +284,7 @@ const HelperPaneNewEl = ({ menuItemRefs.current[1] = el} + ref={el => menuItemRefs.current[2] = el} to="VARIABLES" > @@ -279,7 +295,7 @@ const HelperPaneNewEl = ({ menuItemRefs.current[3] = el} + ref={el => menuItemRefs.current[4] = el} to="CONFIGURABLES" > @@ -292,7 +308,7 @@ const HelperPaneNewEl = ({ menuItemRefs.current[4] = el} + ref={el => menuItemRefs.current[5] = el} to="FUNCTIONS" > @@ -304,7 +320,7 @@ const HelperPaneNewEl = ({ {forcedValueTypeConstraint?.includes(AI_PROMPT_TYPE) && ( menuItemRefs.current[5] = el} + ref={el => menuItemRefs.current[6] = el} to="DOCUMENTS" > @@ -359,6 +375,22 @@ const HelperPaneNewEl = ({ /> + {devantExpressionEditor && ( + + Devant Configs + + + )} + Create Value @@ -501,6 +534,7 @@ export const getHelperPaneNew = (props: HelperPaneNewProps) => { forcedValueTypeConstraint={forcedValueTypeConstraint} handleValueTypeConstChange={handleValueTypeConstChange} inputMode={props.inputMode} + devantExpressionEditor={props.devantExpressionEditor} /> ); }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/styles/ArrayIndexStepper.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/styles/ArrayIndexStepper.tsx new file mode 100644 index 0000000000..cde3c48778 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/styles/ArrayIndexStepper.tsx @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import styled from "@emotion/styled"; +import { ThemeColors } from "@wso2/ui-toolkit"; + +export const ArrayIndexRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin: 0px 8px 4px; + padding: 4px 10px; + background-color: ${ThemeColors.SURFACE_DIM_2}; + border-radius: 6px; +`; + +export const ArrayIndexLabel = styled.span` + font-size: 11px; + color: ${ThemeColors.ON_SURFACE}; + opacity: 0.7; +`; + +export const ArrayIndexControls = styled.div` + display: inline-flex; + align-items: center; + gap: 4px; +`; + +export const ArrayIndexStepButton = styled.button` + background: none; + border: none; + cursor: pointer; + color: ${ThemeColors.HIGHLIGHT}; + font-size: 16px; + line-height: 1; + padding: 0 2px; + user-select: none; + + &:disabled { + cursor: not-allowed; + opacity: 0.3; + color: ${ThemeColors.ON_SURFACE}; + } +`; + +export const ArrayIndexBadge = styled.span` + font-size: 11px; + min-width: 22px; + text-align: center; + padding: 2px 6px; + border-radius: 4px; + background-color: var(--vscode-chat-slashCommandBackground); + color: var(--vscode-chat-slashCommandForeground); + font-variant-numeric: tabular-nums; +`; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/utils/types.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/utils/types.ts index 09fb8e0936..bf46424ebf 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/utils/types.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/utils/types.ts @@ -90,3 +90,10 @@ export const shouldShowNavigationArrow = (item: CompletionItem): boolean => { const typeDetail = item?.labelDetails?.detail || item?.description; return !isPrimitiveType(typeDetail) || item?.labelDetails?.description === "Record"; }; + +// Determines if a type is an array of non-primitive (object) types +export const isArrayOfObjectsType = (typeDetail: string): boolean => { + if (!typeDetail) return false; + const cleanType = typeDetail.trim(); + return cleanType.endsWith('[]') && !isPrimitiveType(cleanType); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/LibraryOverview.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/LibraryOverview.tsx new file mode 100644 index 0000000000..4c4610c39b --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/LibraryOverview.tsx @@ -0,0 +1,475 @@ +/** + * Copyright (c) 2026, 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 { useMemo, useState } from "react"; +import { + DIRECTORY_MAP, + EVENT_TYPE, + MACHINE_VIEW, + ProjectStructure, + ProjectStructureArtifactResponse, +} from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { Button, Codicon, Icon, ThemeColors } from "@wso2/ui-toolkit"; +import styled from "@emotion/styled"; + +// ── Layout ────────────────────────────────────────────────────────────── + +const SectionsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + padding: 16px; + width: 100%; +`; + +const ColumnsLayout = styled.div` + display: flex; + gap: 12px; + align-items: stretch; +`; + +const PrimaryColumn = styled.div` + flex: 2; + position: relative; + min-width: 0; +`; + +const PrimaryColumnInner = styled.div` + position: absolute; + inset: 0; + display: flex; + flex-direction: row; + gap: 12px; + align-items: stretch; +`; + +const SecondaryColumn = styled.div` + flex: 3; + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +`; + +// ── Section ───────────────────────────────────────────────────────────── + +const Section = styled.div<{ vertical?: boolean }>` + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-radius: 4px; + overflow: hidden; + background: var(--vscode-sideBar-background); + ${(props: { vertical?: boolean }) => props.vertical ? "flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column;" : ""} +`; + +const SectionHeader = styled.div<{ accentColor: string }>` + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 12px; + user-select: none; + background-color: color-mix(in srgb, ${(props: { accentColor: string }) => props.accentColor} 6%, transparent); +`; + +const SectionHeaderLeft = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const SectionTitle = styled.span` + font-size: 14px; + font-weight: 500; + color: ${ThemeColors.ON_SURFACE}; +`; + +const ItemCount = styled.span` + font-size: 11px; + color: ${ThemeColors.ON_SURFACE}; + opacity: 0.6; +`; + +const SectionContent = styled.div<{ vertical?: boolean }>` + display: flex; + flex-wrap: ${(props: { vertical?: boolean }) => props.vertical ? "nowrap" : "wrap"}; + flex-direction: ${(props: { vertical?: boolean }) => props.vertical ? "column" : "row"}; + gap: 6px; + padding: 12px 12px; + ${(props: { vertical?: boolean }) => props.vertical ? "flex: 1; overflow-y: auto;" : ""} +`; + +// ── Empty state label ──────────────────────────────────────────────────── + +const EmptySectionLabel = styled.span` + font-size: 12px; + color: ${ThemeColors.ON_SURFACE}; + opacity: 0.7; +`; + + +// ── Construct chip ────────────────────────────────────────────────────── + +const ConstructItem = styled.div<{ accentColor: string; fullWidth?: boolean }>` + display: ${(props: { fullWidth?: boolean }) => props.fullWidth ? "flex" : "inline-flex"}; + align-items: center; + gap: 12px; + padding: 8px 10px; + border: 1px solid color-mix(in srgb, ${(props: { accentColor: string }) => props.accentColor} 25%, transparent); + border-radius: 4px; + cursor: pointer; + font-size: 12px; + color: ${(props: { accentColor: string }) => props.accentColor}; + background: color-mix(in srgb, ${(props: { accentColor: string }) => props.accentColor} 12%, transparent); + &:hover { + background: color-mix(in srgb, ${(props: { accentColor: string }) => props.accentColor} 20%, transparent); + border-color: color-mix(in srgb, ${(props: { accentColor: string }) => props.accentColor} 40%, transparent); + } + &:hover .delete-btn { + display: flex; + } +`; + +const ConstructItemIcon = styled.div` + flex-shrink: 0; + display: flex; + align-items: center; + > div:first-child { + width: 14px; + height: 14px; + font-size: 14px; + } +`; + +const ConstructItemName = styled.span<{ flex?: boolean }>` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + ${(props: { flex?: boolean }) => props.flex ? "flex: 1; min-width: 0;" : "max-width: 160px;"} +`; + +const HighlightMatch = styled.span` + font-weight: 700; + text-decoration: underline; + text-decoration-color: ${ThemeColors.PRIMARY}; + text-underline-offset: 2px; +`; + +const DeleteButton = styled.div` + display: none; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 50%; + cursor: pointer; + color: ${ThemeColors.ON_SURFACE}; + opacity: 0.7; + &:hover { + color: ${ThemeColors.ERROR}; + opacity: 1; + } +`; + +// ── Config ────────────────────────────────────────────────────────────── + +interface SectionConfig { + key: DIRECTORY_MAP; + title: string; + icon: string; + emptyMessage: string; + accentColor: string; + addTooltip: string; + column: "primary" | "secondary"; +} + +const SECTIONS: SectionConfig[] = [ + { + key: DIRECTORY_MAP.TYPE, + title: "Types", + icon: "bi-type", + emptyMessage: "No types yet", + accentColor: "var(--vscode-charts-purple)", + addTooltip: "Add New Type", + column: "primary", + }, + { + key: DIRECTORY_MAP.CONFIGURABLE, + title: "Configurations", + icon: "bi-config", + emptyMessage: "No configurations yet", + accentColor: "var(--vscode-charts-yellow)", + addTooltip: "Add New Configuration", + column: "primary", + }, + { + key: DIRECTORY_MAP.CONNECTION, + title: "Connections", + icon: "bi-connection", + emptyMessage: "No connections yet", + accentColor: "var(--vscode-charts-blue)", + addTooltip: "Add New Connection", + column: "secondary", + }, + { + key: DIRECTORY_MAP.FUNCTION, + title: "Functions", + icon: "bi-function", + emptyMessage: "No functions yet", + accentColor: "var(--vscode-charts-green)", + addTooltip: "Add New Function", + column: "secondary", + }, + { + key: DIRECTORY_MAP.NP_FUNCTION, + title: "Natural Functions", + icon: "bi-ai-function", + emptyMessage: "No natural functions yet", + accentColor: "var(--vscode-charts-orange)", + addTooltip: "Add New Natural Function", + column: "secondary", + }, + { + key: DIRECTORY_MAP.DATA_MAPPER, + title: "Data Mappers", + icon: "dataMapper", + emptyMessage: "No data mappers yet", + accentColor: "var(--vscode-charts-lines)", + addTooltip: "Add New Data Mapper", + column: "secondary", + }, +]; + +// ── Helpers ───────────────────────────────────────────────────────────── + +function highlightName(name: string, query: string) { + if (!query) { + return <>{name}; + } + const idx = name.toLowerCase().indexOf(query); + if (idx === -1) { + return <>{name}; + } + return ( + <> + {name.slice(0, idx)} + {name.slice(idx, idx + query.length)} + {name.slice(idx + query.length)} + + ); +} + +// ── Component ─────────────────────────────────────────────────────────── + +interface LibraryOverviewProps { + projectStructure: ProjectStructure; + searchQuery: string; +} + +export function LibraryOverview(props: LibraryOverviewProps) { + const { projectStructure, searchQuery } = props; + const { rpcClient } = useRpcContext(); + + const isSearching = searchQuery.trim().length > 0; + + const handleAdd = (key: DIRECTORY_MAP) => { + if (key === DIRECTORY_MAP.CONNECTION) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { view: MACHINE_VIEW.AddConnectionWizard }, + isPopup: true, + }); + } else if (key === DIRECTORY_MAP.TYPE) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { view: MACHINE_VIEW.TypeDiagram, addType: true }, + }); + } else if (key === DIRECTORY_MAP.FUNCTION) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { view: MACHINE_VIEW.BIFunctionForm }, + }); + } else if (key === DIRECTORY_MAP.NP_FUNCTION) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { view: MACHINE_VIEW.BINPFunctionForm }, + }); + } else if (key === DIRECTORY_MAP.DATA_MAPPER) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { view: MACHINE_VIEW.BIDataMapperForm }, + }); + } else if (key === DIRECTORY_MAP.CONFIGURABLE) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { view: MACHINE_VIEW.AddConfigVariables }, + }); + } + }; + + const handleItemClick = (key: DIRECTORY_MAP, item: ProjectStructureArtifactResponse) => { + if (key === DIRECTORY_MAP.CONNECTION) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { + view: MACHINE_VIEW.EditConnectionWizard, + identifier: item.name, + }, + isPopup: true, + }); + } else if (key === DIRECTORY_MAP.TYPE) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { + view: MACHINE_VIEW.TypeDiagram, + documentUri: item.path, + position: item.position, + }, + }); + } else if (key === DIRECTORY_MAP.CONFIGURABLE) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { view: MACHINE_VIEW.ViewConfigVariables }, + }); + } else if (item.position && item.path) { + rpcClient.getVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { documentUri: item.path, position: item.position }, + }); + } + }; + + const handleDelete = (item: ProjectStructureArtifactResponse) => { + if (!item.position || !item.path) { + return; + } + rpcClient.getBIDiagramRpcClient().deleteByComponentInfo({ + filePath: item.path, + component: { + name: item.name, + filePath: item.path, + startLine: item.position.startLine, + startColumn: item.position.startColumn, + endLine: item.position.endLine, + endColumn: item.position.endColumn, + }, + }); + }; + + const dirMap = projectStructure.directoryMap as Record; + const query = searchQuery.trim().toLowerCase(); + + const sectionsWithItems = useMemo(() => { + return SECTIONS.map((section) => { + const allItems: ProjectStructureArtifactResponse[] = dirMap[section.key] ?? []; + const filteredItems = isSearching + ? allItems.filter((item) => item.name.toLowerCase().includes(query)) + : allItems; + return { section, allItems, filteredItems }; + }); + }, [dirMap, query, isSearching]); + + const primarySections = sectionsWithItems.filter((s) => s.section.column === "primary"); + const secondarySections = sectionsWithItems.filter((s) => s.section.column === "secondary"); + + const renderSection = ( + { section, allItems, filteredItems }: typeof sectionsWithItems[number], + vertical?: boolean, + ) => { + const displayItems = isSearching ? filteredItems : allItems; + const hasItems = displayItems.length > 0; + + return ( +
+ + + + {section.title} + {hasItems && ( + + ({isSearching ? `${filteredItems.length}/${allItems.length}` : allItems.length}) + + )} + + + + + + {hasItems && ( + + {displayItems.map((item) => ( + handleItemClick(section.key, item)} + > + + + + + {highlightName(item.name, query)} + + {item.position && item.path && ( + { + e.stopPropagation(); + handleDelete(item); + }} + > + + + )} + + ))} + + )} + {!hasItems && ( + + + {isSearching ? `No matching ${section.title.toLowerCase()}` : section.emptyMessage} + + + )} +
+ ); + }; + + return ( + + + + + {primarySections.map((s) => renderSection(s, true))} + + + + {secondarySections.map((s) => renderSection(s))} + + + + ); +} + +export default LibraryOverview; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/PublishToCentralButton.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/PublishToCentralButton.tsx new file mode 100644 index 0000000000..4b1ebba3a5 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/PublishToCentralButton.tsx @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2026, 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 { Icon } from "@wso2/ui-toolkit"; +import { VSCodeLink, VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { useHoverWithDelay } from "./useHoverWithDelay"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { useQuery } from "@tanstack/react-query"; + +const Wrapper = styled.div` + position: relative; + display: inline-flex; +`; + +const StyledButton = styled(VSCodeButton)` + padding: 4px 8px; +`; + +const TooltipBubble = styled.div` + position: absolute; + top: 100%; + right: 0; + left: auto; + transform: translateY(4px); + margin-top: 4px; + padding: 10px 12px; + background: var(--vscode-editorWidget-background); + border: 1px solid var(--vscode-widget-border); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + white-space: nowrap; + display: flex; + flex-direction: column; + gap: 6px; + z-index: 1000; + pointer-events: auto; + font-size: 12px; + color: var(--vscode-editor-foreground); + & a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + & a:hover { + text-decoration: underline; + } +`; + +export interface PublishToCentralButtonProps { + disabled?: boolean; +} + +export function PublishToCentralButton({ + disabled = false +}: PublishToCentralButtonProps) { + const { rpcClient } = useRpcContext(); + const [isTooltipVisible, hoverHandlers] = useHoverWithDelay(200); + const [isPublishing, setIsPublishing] = useState(false); + + const handlePublishToCentral = async () => { + setIsPublishing(true); + try { + await rpcClient.getCommonRpcClient().publishToCentral(); + } finally { + setIsPublishing(false); + } + }; + + const handlePublishLearnMore = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const url = hasCentralPATConfigured + ? "https://ballerina.io/learn/publish-packages-to-ballerina-central/" + : "https://ballerina.io/learn/publish-packages-to-ballerina-central/#obtain-an-access-token"; + rpcClient.getCommonRpcClient().openExternalUrl({ + url: url, + }); + }; + + const { data: hasCentralPATConfigured } = useQuery({ + queryKey: ["has-central-pat-configured"], + queryFn: () => rpcClient.getCommonRpcClient().hasCentralPATConfigured(), + refetchInterval: 10000 + }); + + const tooltipMessage = hasCentralPATConfigured + ? "Publish this library to Ballerina Central." + : "No Ballerina Central PAT configured. Please try again after configuring the PAT."; + const learnMoreLabel = "Learn more"; + const publishingLabel = isPublishing ? "Publishing..." : "Publish"; + + const isDisabled = disabled || !hasCentralPATConfigured || isPublishing; + + return ( + + + {publishingLabel} + + {isTooltipVisible && ( + + {tooltipMessage} + + {learnMoreLabel} + + + )} + + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx index c3f4523ade..df57f05b0b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx @@ -16,31 +16,32 @@ * under the License. */ -import React, { useEffect, useMemo, useState } from "react"; +import React, { ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { ProjectStructure, EVENT_TYPE, MACHINE_VIEW, BuildMode, BI_COMMANDS, - DevantMetadata, SHARED_COMMANDS, - DIRECTORY_MAP + DIRECTORY_MAP, } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; -import { Typography, Codicon, ProgressRing, Button, Icon, Divider, CheckBox } from "@wso2/ui-toolkit"; +import { Typography, Codicon, ProgressRing, Button, Icon, Divider, CheckBox, ProgressIndicator, Overlay, Dropdown } from "@wso2/ui-toolkit"; import styled from "@emotion/styled"; import { ThemeColors } from "@wso2/ui-toolkit"; import ComponentDiagram from "../ComponentDiagram"; import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; import ReactMarkdown from "react-markdown"; -import { useQuery } from '@tanstack/react-query' import { IOpenInConsoleCmdParams, CommandIds as PlatformExtCommandIds } from "@wso2/wso2-platform-core"; import { AlertBoxWithClose } from "../../AIPanel/AlertBoxWithClose"; import { getIntegrationTypes } from "./utils"; import { UndoRedoGroup } from "../../../components/UndoRedoGroup"; +import { usePlatformExtContext } from "../../../providers/platform-ext-ctx-provider"; import { TopNavigationBar } from "../../../components/TopNavigationBar"; import { TitleBar } from "../../../components/TitleBar"; +import { PublishToCentralButton } from "./PublishToCentralButton"; +import { LibraryOverview } from "./LibraryOverview"; const SpinnerContainer = styled.div` display: flex; @@ -99,6 +100,7 @@ const HeaderControls = styled.div` display: flex; gap: 8px; margin-right: 16px; + align-items: center; `; const MainContent = styled.div<{ fullWidth?: boolean }>` @@ -153,14 +155,60 @@ const EmptyReadmeContainer = styled.div` height: 100%; `; -const DiagramHeaderContainer = styled.div<{ withPadding?: boolean }>` +const DiagramHeaderContainer = styled.div<{ withPadding?: boolean, isLibrary?: boolean }>` display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; + margin-bottom: ${(props: { isLibrary?: boolean }) => props.isLibrary ? "0" : "16px"}; padding: ${(props: { withPadding: boolean; }) => (props.withPadding ? "16px 16px 0 16px" : "0")}; `; +const LibrarySearchBar = styled.div` + position: relative; + display: flex; + align-items: center; + width: clamp(160px, 35vw, 400px); +`; + +const LibrarySearchInput = styled.input` + width: 100%; + padding: 6px 24px 6px 28px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + color: var(--vscode-input-foreground); + font-size: 12px; + font-family: var(--vscode-font-family); + &:focus { + outline: none; + border-color: var(--vscode-focusBorder); + } + &::placeholder { + color: var(--vscode-input-placeholderForeground); + } +`; + +const LibrarySearchIcon = styled.div` + position: absolute; + left: 8px; + color: var(--vscode-input-placeholderForeground); + pointer-events: none; + display: flex; + align-items: center; +`; + +const LibrarySearchClearButton = styled.div` + position: absolute; + right: 6px; + display: flex; + align-items: center; + cursor: pointer; + color: var(--vscode-input-placeholderForeground); + &:hover { + color: var(--vscode-input-foreground); + } +`; + const DiagramContent = styled.div` flex: 1; min-height: 0; // Prevents flex blowout @@ -296,9 +344,16 @@ const DeploymentHeader = styled.div` font-size: 13px; font-weight: 600; margin: 0; + width: 100%; } `; +const DevantHeaderWrap = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + interface DeploymentBodyProps { isExpanded: boolean; } @@ -311,7 +366,7 @@ const DeploymentBody = styled.div` `; interface DeploymentOptionProps { - title: string; + title: ReactNode; description: string; buttonText: string; isExpanded: boolean; @@ -403,7 +458,6 @@ interface DeploymentOptionsProps { handleJarBuild: () => void; handleDeploy: () => Promise; goToDevant: () => void; - devantMetadata: DevantMetadata | undefined; hasDeployableIntegration: boolean; } @@ -412,11 +466,11 @@ function DeploymentOptions({ handleJarBuild, handleDeploy, goToDevant, - devantMetadata, hasDeployableIntegration }: DeploymentOptionsProps) { const [expandedOptions, setExpandedOptions] = useState>(new Set(['cloud', 'devant'])); const { rpcClient } = useRpcContext(); + const { platformExtState } = usePlatformExtContext(); const toggleOption = (option: string) => { setExpandedOptions(prev => { @@ -430,6 +484,7 @@ function DeploymentOptions({ }); }; + const isDeployed = platformExtState?.isLoggedIn ? !!platformExtState?.selectedComponent : platformExtState?.hasPossibleComponent; return ( <> @@ -437,20 +492,39 @@ function DeploymentOptions({ Deployment Options + Deployed in Devant + + + ) : ( + "Deploy to Devant" + ) + } description={ - devantMetadata?.hasComponent + isDeployed ? "This integration is already deployed in Devant." : "Deploy your integration to the cloud using Devant by WSO2." } - buttonText={devantMetadata?.hasComponent ? "View in Devant" : "Deploy"} + buttonText={isDeployed ? "View in Devant" : "Deploy"} isExpanded={expandedOptions.has("devant")} onToggle={() => toggleOption("devant")} - onDeploy={devantMetadata?.hasComponent ? () => goToDevant() : handleDeploy} + onDeploy={isDeployed? () => goToDevant() : handleDeploy} learnMoreLink={"https://wso2.com/devant/docs"} hasDeployableIntegration={hasDeployableIntegration} secondaryAction={ - devantMetadata?.hasComponent && devantMetadata?.hasLocalChanges + isDeployed && platformExtState?.hasLocalChanges ? { description: "To redeploy in Devant, please commit and push your changes.", buttonText: "Open Source Control", @@ -518,8 +592,9 @@ function IntegrationControlPlane({ enabled, handleICP }: IntegrationControlPlane ); } -function DevantDashboard({ projectStructure, handleDeploy, goToDevant, devantMetadata }: { projectStructure: ProjectStructure, handleDeploy: () => void, goToDevant: () => void, devantMetadata: DevantMetadata }) { +function DevantDashboard({ projectStructure, handleDeploy, goToDevant }: { projectStructure: ProjectStructure, handleDeploy: () => void, goToDevant: () => void }) { const { rpcClient } = useRpcContext(); + const { platformExtState } = usePlatformExtContext(); const handleSaveAndDeployToDevant = () => { handleDeploy(); @@ -535,25 +610,23 @@ function DevantDashboard({ projectStructure, handleDeploy, goToDevant, devantMet (projectStructure.directoryMap.SERVICE && projectStructure.directoryMap.SERVICE.length > 0) ); - console.log(">>> devantMetadata", devantMetadata); - return ( - {devantMetadata?.hasComponent ? Deployed in Devant : Deploy to Devant} + {platformExtState?.selectedComponent ? Deployed in Devant : Deploy to Devant} {!hasAutomationOrService ? ( Before you can deploy your integration to Devant, please add an artifact (such as a Service or Automation) to your project. ) : ( <> - {devantMetadata?.hasComponent ? ( + {platformExtState?.selectedComponent ? ( <> This integration is deployed in Devant. )} + {isLibrary && ( + + )} ); @@ -885,9 +960,9 @@ export function PackageOverview(props: PackageOverviewProps) { btn2Id="Close" /> )} - - Design - {!isEmptyProject() && ( + + {isLibrary ? "Artifacts" : "Design"} + {!isEmptyProject() && !isLibrary && ( @@ -895,32 +970,61 @@ export function PackageOverview(props: PackageOverviewProps) { Add Artifact )} - - - {isEmptyProject() ? ( - - - {isLibrary ? "Your library is empty" : "Your integration is empty"} - - - Start by adding artifacts or use AI to generate your {isLibrary ? "shared logic and utilities" : "integration structure"} - - - - - - - ) : ( - + {isLibrary && ( + + + + + + setLibrarySearchQuery(e.target.value)} + /> + {librarySearchQuery.trim().length > 0 && ( + { + setLibrarySearchQuery(""); + librarySearchRef.current?.focus(); + }} + > + + + )} + + )} - + + {isLibrary && } + {!isLibrary && ( + + {isEmptyProject() ? ( + + + Your integration is empty + + + Start by adding artifacts or use AI to generate your integration structure + + + + + + + ) : ( + + )} + + )} @@ -959,7 +1063,6 @@ export function PackageOverview(props: PackageOverviewProps) { handleJarBuild={handleJarBuild} handleDeploy={handleDeploy} goToDevant={goToDevant} - devantMetadata={devantMetadata} hasDeployableIntegration={deployableIntegrationTypes.length > 0} /> @@ -971,7 +1074,6 @@ export function PackageOverview(props: PackageOverviewProps) { projectStructure={projectStructure} handleDeploy={handleDeploy} goToDevant={goToDevant} - devantMetadata={devantMetadata} /> } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/useHoverWithDelay.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/useHoverWithDelay.ts new file mode 100644 index 0000000000..c8f8a2c881 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/useHoverWithDelay.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useCallback, useEffect, useRef, useState } from "react"; + +const DEFAULT_HIDE_DELAY_MS = 200; + +/** + * Hook for hover-revealed UI (e.g. tooltips) that stay visible when moving the cursor + * from the trigger to the content. Hiding is delayed so the user can reach the tooltip. + * + * @param hideDelayMs - Delay in ms before hiding after pointer leaves (default 200) + * @returns [isVisible, hoverHandlers] - spread hoverHandlers on both trigger and tooltip + */ +export function useHoverWithDelay(hideDelayMs: number = DEFAULT_HIDE_DELAY_MS) { + const [isVisible, setIsVisible] = useState(false); + const hideTimeoutRef = useRef | null>(null); + + const clearHideTimeout = useCallback(() => { + if (hideTimeoutRef.current !== null) { + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + } + }, []); + + const scheduleHide = useCallback(() => { + clearHideTimeout(); + hideTimeoutRef.current = setTimeout(() => setIsVisible(false), hideDelayMs); + }, [clearHideTimeout, hideDelayMs]); + + const show = useCallback(() => { + clearHideTimeout(); + setIsVisible(true); + }, [clearHideTimeout]); + + useEffect(() => () => clearHideTimeout(), [clearHideTimeout]); + + const hoverHandlers = { + onMouseEnter: show, + onMouseLeave: scheduleHide, + }; + + return [isVisible, hoverHandlers] as const; +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/utils.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/utils.ts index 59774ead64..093a8889db 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/utils.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/utils.ts @@ -15,7 +15,13 @@ * specific language governing permissions and limitations * under the License. */ -import { SCOPE, DIRECTORY_MAP, ProjectStructure } from "@wso2/ballerina-core"; +import { + SCOPE, + DIRECTORY_MAP, + ProjectStructure, + ProjectStructureResponse, + ProjectScopeMapping +} from "@wso2/ballerina-core"; const INTEGRATION_API_MODULES = ["http", "graphql", "tcp"]; const EVENT_INTEGRATION_MODULES = ["kafka", "rabbitmq", "salesforce", "trigger.github", "mqtt", "asb"]; @@ -64,5 +70,40 @@ export function getIntegrationTypes(projectStructure: ProjectStructure | undefin scopes.push(SCOPE.AUTOMATION); } + // Add library scope if the project is a library + if (projectStructure.isLibrary) { + scopes.push(SCOPE.LIBRARY); + } + return scopes; } + +/** + * Builds a list of deployable integration scopes per project for a workspace. + * + * @param workspaceStructure - Workspace structure containing the projects list. + * @returns A list of project-to-scope mappings used for workspace-level deployment. + */ +export function getWorkspaceProjectScopes( + workspaceStructure: ProjectStructureResponse | undefined +): ProjectScopeMapping[] { + if (!workspaceStructure || !workspaceStructure.projects) { + return []; + } + + const mapProjectToScope = (project: ProjectStructure): ProjectScopeMapping | undefined => { + const integrationTypes = getIntegrationTypes(project); + if (integrationTypes.length > 0) { + return { + projectPath: project.projectPath!, + projectTitle: project.projectTitle || project.projectName, + integrationTypes + }; + } + return undefined; + }; + + return workspaceStructure.projects + .map(mapProjectToScope) + .filter((scopeMapping): scopeMapping is ProjectScopeMapping => scopeMapping !== undefined); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PlatformExtPopover/PlatformExtPopover.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PlatformExtPopover/PlatformExtPopover.tsx new file mode 100644 index 0000000000..4af65e2e9e --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PlatformExtPopover/PlatformExtPopover.tsx @@ -0,0 +1,356 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import styled from "@emotion/styled"; +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; +import { Codicon, Dropdown, Popover, ThemeColors, VSCodeColors, Button } from "@wso2/ui-toolkit"; +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { usePlatformExtContext } from "../../../providers/platform-ext-ctx-provider"; +import { + ICmdParamsBase, + ICreateDirCtxCmdParams, + IManageDirContextCmdParams, + IOpenInConsoleCmdParams, + CommandIds as PlatformExtCommandIds, +} from "@wso2/wso2-platform-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { QuickPickItem } from "vscode"; + +const PopupContainer = styled.div` + min-width: 200px; + font-family: "GilmerRegular"; + font-size: 12px; + text-overflow: ellipsis; + color: ${ThemeColors.ON_SURFACE}; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; + ul { + padding: 0 12px; + margin: 0; + } +`; + +const PanelItem = styled.div` + display: flex; + align-items: flex-start; + gap: 8px; +`; + +const PanelItemContent = styled.div` + flex: 1; +`; + +const PanelItemTitle = styled.div` + font-size: 10px; + opacity: 60%; + line-height: 10px; + margin-bottom: 2px; +`; + +const PanelItemVal = styled.div` + font-size: 12px; + line-height: 12px; +`; + +const PanelItemValButton = styled(PanelItemVal)` + cursor: pointer; + &:hover { + text-decoration: underline; + } +`; + +const ButtonGroup = styled.div` + display: flex; + align-items: center; + gap: 2px; + padding-top: 8px; +`; + +const PanelItemVSCodeLink = styled(VSCodeLink)` + font-size: 11px; + line-height: 11px; +`; + +export interface DiagnosticsPopUpProps { + isVisible: boolean; + anchorEl: HTMLElement; + onClose: () => void; + projectPath?: string; +} + +export function PlatformExtPopover(props: DiagnosticsPopUpProps) { + const { isVisible, onClose, anchorEl, projectPath } = props; + const { platformExtState, platformRpcClient, loginToDevant } = usePlatformExtContext(); + const { rpcClient } = useRpcContext(); + + const handleSignOut = () => { + rpcClient + .getCommonRpcClient() + .showInformationModal({ + message: "Are you sure you want to sign out of your Devant account?", + items: ["Yes"], + }) + .then((res) => { + if (res === "Yes") { + rpcClient.getCommonRpcClient().executeCommand({ + commands: [PlatformExtCommandIds.SignOut], + }); + } + }); + }; + + const handleSwitchProject = () => { + rpcClient.getCommonRpcClient().executeCommand({ + commands: [ + PlatformExtCommandIds.ManageDirectoryContext, + { + extName: "Devant", + onlyShowSwitchProject: true, + } as IManageDirContextCmdParams, + ], + }); + }; + + const handleLinkWorkspace = async () => { + const visualizerLocation = await rpcClient.getVisualizerLocation(); + rpcClient.getCommonRpcClient().executeCommand({ + commands: [ + PlatformExtCommandIds.CreateDirectoryContext, + { + extName: "Devant", + skipComponentExistCheck: true, + fsPath: visualizerLocation?.workspacePath || visualizerLocation?.projectPath || "", + } as ICreateDirCtxCmdParams, + ], + }); + }; + + const nonCriticalEnvs = platformExtState?.envs?.filter((env) => !env.critical) || []; + + const handleEnvSelect = () => { + rpcClient + .getCommonRpcClient() + .showQuickPick({ + items: nonCriticalEnvs.map((env) => ({ label: env.name })) || [], + options: { title: "Select Environment to Connect" }, + }) + .then((resp) => { + const selectedEnv = nonCriticalEnvs.find((env) => env.name === resp?.label); + if (selectedEnv) { + platformRpcClient.setSelectedEnv(selectedEnv.id); + } + }); + }; + + const openIntegrationInConsole = () => { + rpcClient.getCommonRpcClient().executeCommand({ + commands: [ + PlatformExtCommandIds.OpenInConsole, + { + extName: "Devant", + componentFsPath: projectPath, + component: platformExtState?.selectedComponent, + newComponentParams: { buildPackLang: "ballerina" }, + } as IOpenInConsoleCmdParams, + ], + }); + }; + + const handleIntegrationSelect = () => { + if (platformExtState?.components?.length === 0) { + return; + } + if (platformExtState?.components?.length === 1) { + openIntegrationInConsole(); + return; + } + + const quickPickOptions: QuickPickItem[] = [ + { + label: "View in Console", + detail: "Open the integration in Devant Console", + }, + { kind: -1, label: "Associated Integrations" }, + ...platformExtState?.components.map((item) => ({ + label: item?.metadata?.name, + description: + item.metadata?.id === platformExtState?.selectedComponent?.metadata?.id ? "Selected" : undefined, + })), + ]; + rpcClient + .getCommonRpcClient() + .showQuickPick({ + items: quickPickOptions, + options: { title: "Select Integration" }, + }) + .then((resp) => { + if (resp?.label === "View in Console") { + openIntegrationInConsole(); + return; + } + const selectedIntegration = platformExtState?.components.find( + (env) => env.metadata.name === resp?.label, + ); + if (selectedIntegration) { + platformRpcClient.setSelectedComponent(selectedIntegration.metadata?.id || ""); + } + }); + }; + + return ( + <> + + + {platformExtState?.userInfo ? ( + <> + + + Account + {platformExtState?.userInfo?.userEmail} + + + + + + {platformExtState?.selectedContext ? ( + <> + + + Organization + {platformExtState?.selectedContext?.org?.name} + + + + + Project + + {platformExtState?.selectedContext?.project?.name} + + + + {projectPath && ( + <> + {platformExtState?.selectedComponent && ( + + + Integration + + {platformExtState?.selectedComponent?.metadata?.name} + + + + )} + {platformExtState?.devantConns?.list?.length > 0 && + platformExtState?.selectedEnv && ( + + + Connected Environment + {nonCriticalEnvs?.length > 1 ? ( + + {platformExtState?.selectedEnv?.name} + + ) : ( + + {platformExtState?.selectedEnv?.name} + + )} + + + )} + {platformExtState?.devantConns?.list?.length > 0 && ( + + + + Using {platformExtState?.devantConns?.list?.length} Devant{" "} + {platformExtState?.devantConns?.list?.length < 2 + ? "Connection" + : "Connections"} + + + + Connect to Devant
+ while running or debugging +
+
+ + { + platformRpcClient.setConnectedToDevant( + !!!platformExtState?.devantConns?.connectedToDevant, + ); + }} + /> + +
+ )} + + )} + + ) : ( + + + + Link workspace + {" "} + with a Devant project to activate Devant features + + + )} + + ) : ( + + + Login to your Devant + account +
to manage your project in the cloud +
+
+ )} +
+
+ + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PlatformExtPopover/index.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PlatformExtPopover/index.ts new file mode 100644 index 0000000000..21f265200f --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PlatformExtPopover/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { PlatformExtPopover } from "./PlatformExtPopover"; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/AddProjectFormFields.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/AddProjectFormFields.tsx index 054bdfd137..009f4bd812 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/AddProjectFormFields.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/AddProjectFormFields.tsx @@ -26,7 +26,7 @@ import { } from "./styles"; import { ProjectTypeSelector, PackageInfoSection } from "./components"; import { AddProjectFormData } from "./types"; -import { sanitizePackageName, validatePackageName } from "./utils"; +import { sanitizePackageName, validatePackageName, validateOrgName } from "./utils"; // Re-export for backwards compatibility export type { AddProjectFormData } from "./types"; @@ -47,6 +47,7 @@ export function AddProjectFormFields({ const [packageNameTouched, setPackageNameTouched] = useState(false); const [isPackageInfoExpanded, setIsPackageInfoExpanded] = useState(false); const [packageNameError, setPackageNameError] = useState(null); + const [orgNameError, setOrgNameError] = useState(null); const handleIntegrationName = (value: string) => { onFormDataChange({ integrationName: value }); @@ -72,6 +73,12 @@ export function AddProjectFormFields({ setPackageNameError(error); }, [formData.packageName]); + // Real-time validation for organization name + useEffect(() => { + const error = validateOrgName(formData.orgName); + setOrgNameError(error); + }, [formData.orgName]); + return ( <> {!isInWorkspace && ( @@ -121,6 +128,7 @@ export function AddProjectFormFields({ onToggle={() => setIsPackageInfoExpanded(!isPackageInfoExpanded)} data={{ orgName: formData.orgName, version: formData.version }} onChange={(data) => onFormDataChange(data)} + orgNameError={orgNameError} /> ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/ProjectFormFields.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/ProjectFormFields.tsx index 44046fd276..a378d69d04 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/ProjectFormFields.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/ProjectFormFields.tsx @@ -28,7 +28,7 @@ import { } from "./styles"; import { CollapsibleSection, ProjectTypeSelector, PackageInfoSection } from "./components"; import { ProjectFormData } from "./types"; -import { sanitizePackageName, validatePackageName } from "./utils"; +import { sanitizePackageName, validatePackageName, validateOrgName } from "./utils"; // Re-export for backwards compatibility export type { ProjectFormData } from "./types"; @@ -45,6 +45,7 @@ export function ProjectFormFields({ formData, onFormDataChange, integrationNameE const { rpcClient } = useRpcContext(); const [packageNameTouched, setPackageNameTouched] = useState(false); const [packageNameError, setPackageNameError] = useState(null); + const [orgNameError, setOrgNameError] = useState(null); const [isWorkspaceSupported, setIsWorkspaceSupported] = useState(false); const [isProjectStructureExpanded, setIsProjectStructureExpanded] = useState(false); const [isPackageInfoExpanded, setIsPackageInfoExpanded] = useState(false); @@ -62,6 +63,9 @@ export function ProjectFormFields({ formData, onFormDataChange, integrationNameE const sanitized = sanitizePackageName(value); onFormDataChange({ packageName: sanitized }); setPackageNameTouched(value.length > 0); + if (packageNameError) { + setPackageNameError(null); + } }; const handleProjectDirSelection = async () => { @@ -73,12 +77,30 @@ export function ProjectFormFields({ formData, onFormDataChange, integrationNameE setIsProjectStructureExpanded(!isProjectStructureExpanded); }; + const projectTypeNote = formData.createAsWorkspace + ? "This sets the type for your first project. You can add more projects or libraries to this workspace later." + : undefined; + useEffect(() => { (async () => { + const commonRpcClient = rpcClient.getCommonRpcClient(); + + // Set default path if not already set if (!formData.path) { - const currentDir = await rpcClient.getCommonRpcClient().getWorkspaceRoot(); + const currentDir = await commonRpcClient.getWorkspaceRoot(); onFormDataChange({ path: currentDir.path }); } + + // Set default org name if not already set + if (!formData.orgName) { + try { + const { orgName } = await commonRpcClient.getDefaultOrgName(); + onFormDataChange({ orgName }); + } catch (error) { + console.error("Failed to fetch default org name:", error); + } + } + const isWorkspaceSupported = await rpcClient .getLangClientRpcClient() .isSupportedSLVersion({ major: 2201, minor: 13, patch: 0 }) @@ -90,6 +112,17 @@ export function ProjectFormFields({ formData, onFormDataChange, integrationNameE })(); }, []); + useEffect(() => { + const error = validatePackageName(formData.packageName, formData.integrationName); + setPackageNameError(error); + }, [formData.packageName, formData.integrationName]); + + // Validation effect for org name + useEffect(() => { + const orgError = validateOrgName(formData.orgName); + setOrgNameError(orgError); + }, [formData.orgName]); + return ( <> {/* Primary Fields - Always Visible */} @@ -111,7 +144,7 @@ export function ProjectFormFields({ formData, onFormDataChange, integrationNameE value={formData.packageName} label="Package Name" description="This will be used as the Ballerina package name for the integration." - errorMsg={packageNameValidationError || ""} + errorMsg={packageNameValidationError || packageNameError || ""} /> @@ -136,16 +169,24 @@ export function ProjectFormFields({ formData, onFormDataChange, integrationNameE + + onFormDataChange({ isLibrary })} + note={projectTypeNote} + /> + + Optional Configurations - {/* Project Structure Section */} + {/* Workspace Section */} {isWorkspaceSupported && ( onFormDataChange({ createAsWorkspace: checked })} /> - Include this integration in a new workspace for multi-project management. + Enable Workspace mode to manage multiple integrations within a single repository with shared dependencies. {formData.createAsWorkspace && ( - <> - - onFormDataChange({ workspaceName: value })} - value={formData.workspaceName} - label="Workspace Name" - placeholder="Enter workspace name" - required={true} - /> - - - onFormDataChange({ isLibrary })} - note="This sets the type for your first project. You can add more projects or libraries to this workspace later." + + onFormDataChange({ workspaceName: value })} + value={formData.workspaceName} + label="Workspace Name" + placeholder="Enter workspace name" + required={true} /> - + )} )} @@ -185,6 +218,7 @@ export function ProjectFormFields({ formData, onFormDataChange, integrationNameE onToggle={() => setIsPackageInfoExpanded(!isPackageInfoExpanded)} data={{ orgName: formData.orgName, version: formData.version }} onChange={(data) => onFormDataChange(data)} + orgNameError={orgNameError} /> ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/components/PackageInfoSection.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/components/PackageInfoSection.tsx index 73f551cbe1..95460df65e 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/components/PackageInfoSection.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/components/PackageInfoSection.tsx @@ -17,7 +17,7 @@ */ import { TextField } from "@wso2/ui-toolkit"; -import { FieldGroup } from "../styles"; +import { FieldGroup, Note } from "../styles"; import { CollapsibleSection } from "./CollapsibleSection"; export interface PackageInfoData { @@ -34,6 +34,8 @@ export interface PackageInfoSectionProps { data: PackageInfoData; /** Callback when the package info changes */ onChange: (data: Partial) => void; + /** Error message for org name validation */ + orgNameError?: string | null; } export function PackageInfoSection({ @@ -41,6 +43,7 @@ export function PackageInfoSection({ onToggle, data, onChange, + orgNameError, }: PackageInfoSectionProps) { return ( + + This integration is generated as a Ballerina package. Define the organization and version that will be assigned to it. + onChange({ orgName: value })} value={data.orgName} label="Organization Name" description="The organization that owns this Ballerina package." + errorMsg={orgNameError || undefined} /> diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/components/ProjectTypeSelector.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/components/ProjectTypeSelector.tsx index 34f7a904dc..516fa75d8a 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/components/ProjectTypeSelector.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/components/ProjectTypeSelector.tsx @@ -26,7 +26,7 @@ import { RadioContent, RadioTitle, RadioDescription, - ProjectTypeNote, + Note, } from "../styles"; export interface ProjectTypeOption { @@ -53,7 +53,7 @@ const PROJECT_TYPE_OPTIONS: ProjectTypeOption[] = [ { value: "library", title: "Library Project", - description: "Shared logic and utilities that can be reused across multiple projects in the workspace.", + description: "Shared logic and utilities that can be reused across multiple integrations.", }, ]; @@ -87,7 +87,7 @@ export function ProjectTypeSelector({ ); })} - {note && {note}} + {note && {note}} ); } 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 27da43ae79..48d01c40d9 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/index.tsx @@ -19,7 +19,7 @@ import { useState } from "react"; import { Button, Icon, Typography } from "@wso2/ui-toolkit"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; -import { EVENT_TYPE, MACHINE_VIEW } from "@wso2/ballerina-core"; +import { EVENT_TYPE, MACHINE_VIEW, ValidateProjectFormErrorField } from "@wso2/ballerina-core"; import { PageWrapper, FormContainer, @@ -30,6 +30,7 @@ import { } from "./styles"; import { ProjectFormFields } from "./ProjectFormFields"; import { ProjectFormData } from "./types"; +import { validatePackageName } from "./utils"; export function ProjectForm() { const { rpcClient } = useRpcContext(); @@ -63,18 +64,72 @@ export function ProjectForm() { } }; - const handleCreateProject = () => { - rpcClient.getBIDiagramRpcClient().createProject({ - projectName: formData.integrationName, - packageName: formData.packageName, - projectPath: formData.path, - createDirectory: formData.createDirectory, - createAsWorkspace: formData.createAsWorkspace, - workspaceName: formData.workspaceName, - orgName: formData.orgName || undefined, - version: formData.version || undefined, - isLibrary: formData.isLibrary, - }); + const handleCreateProject = async () => { + setIsValidating(true); + setIntegrationNameError(null); + setPathError(null); + setPackageNameValidationError(null); + + let hasError = false; + + if (formData.integrationName.length < 2) { + setIntegrationNameError("Integration name must be at least 2 characters"); + hasError = true; + } + + if (formData.packageName.length < 2) { + setPackageNameValidationError("Package name must be at least 2 characters"); + hasError = true; + } else { + const packageNameError = validatePackageName(formData.packageName, formData.integrationName); + if (packageNameError) { + setPackageNameValidationError(packageNameError); + hasError = true; + } + } + + if (formData.path.length < 2) { + setPathError("Please select a path for your project"); + hasError = true; + } + + if (hasError) { + setIsValidating(false); + return; + } + + try { + const validationResult = await rpcClient.getBIDiagramRpcClient().validateProjectPath({ + projectPath: formData.path, + projectName: formData.createAsWorkspace ? formData.workspaceName : formData.packageName, + createDirectory: formData.createDirectory, + }); + + if (!validationResult.isValid) { + if (validationResult.errorField === ValidateProjectFormErrorField.PATH) { + setPathError(validationResult.errorMessage || "Invalid project path"); + } else if (validationResult.errorField === ValidateProjectFormErrorField.NAME) { + setPackageNameValidationError(validationResult.errorMessage || "Invalid project name"); + } + setIsValidating(false); + return; + } + + rpcClient.getBIDiagramRpcClient().createProject({ + projectName: formData.integrationName, + packageName: formData.packageName, + projectPath: formData.path, + createDirectory: formData.createDirectory, + createAsWorkspace: formData.createAsWorkspace, + workspaceName: formData.workspaceName, + orgName: formData.orgName || undefined, + version: formData.version || undefined, + isLibrary: formData.isLibrary, + }); + } catch (error) { + setPathError("An error occurred during validation"); + setIsValidating(false); + } }; const gotToWelcome = () => { diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/styles/form.styles.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/styles/form.styles.ts index 244bd1e1cf..9277957272 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/styles/form.styles.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/styles/form.styles.ts @@ -258,7 +258,7 @@ export const RadioDescription = styled.span` line-height: 1.4; `; -export const ProjectTypeNote = styled.div` +export const Note = styled.div` font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 12px; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/utils.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/utils.ts index 2a4c37c040..8f42b6a538 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/utils.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ProjectForm/utils.ts @@ -67,7 +67,8 @@ export const isFormValidAddProject = (formData: AddProjectFormData, isInWorkspac formData.integrationName.length >= 2 && formData.packageName.length >= 2 && (isInWorkspace || (formData.workspaceName?.length ?? 0) >= 1) && - validatePackageName(formData.packageName, formData.integrationName) === null + validatePackageName(formData.packageName, formData.integrationName) === null && + validateOrgName(formData.orgName) === null ); }; @@ -79,3 +80,44 @@ export const sanitizePackageName = (name: string): string => { .replace(/\.{2,}/g, ".") // Convert multiple consecutive dots to single dot .replace(/_{2,}/g, "_"); // Convert multiple consecutive underscores to single underscore }; + +// Reserved organization names +const RESERVED_ORG_NAMES = ["ballerina", "ballerinax", "wso2"]; + +// Org name pattern (based on Ballerina language specification for RestrictedIdentifier) +// RestrictedIdentifier := AsciiLetter RestrictedFollowingChar* RestrictedIdentifierWord* +// RestrictedIdentifierWord := _ RestrictedFollowingChar+ +// RestrictedFollowingChar := AsciiLetter | Digit +// AsciiLetter := A .. Z | a .. z +const RESTRICTED_IDENTIFIER_REGEX = /^[a-zA-Z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$/; + +export const validateOrgName = (orgName: string): string | null => { + // Empty org name is allowed (optional field) + if (!orgName || orgName.length === 0) { + return null; + } + + // Check for reserved org names (case-insensitive) + if (RESERVED_ORG_NAMES.includes(orgName.toLowerCase())) { + return `"${orgName}" is a reserved organization name`; + } + + // Validate against RestrictedIdentifier pattern + if (!RESTRICTED_IDENTIFIER_REGEX.test(orgName)) { + if (!/^[a-zA-Z]/.test(orgName)) { + return "Organization name must start with a letter (a-z, A-Z)"; + } + if (orgName.includes("__")) { + return "Organization name cannot have consecutive underscores"; + } + if (orgName.endsWith("_")) { + return "Organization name cannot end with an underscore"; + } + if (/_[^a-zA-Z0-9]/.test(orgName)) { + return "Underscore must be followed by at least one letter or digit"; + } + return "Organization name can only contain letters (a-z, A-Z), digits (0-9), and underscores"; + } + + return null; +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/FTPConfigForm.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/FTPConfigForm.tsx index 6d63856d69..f63866ec1b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/FTPConfigForm.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/FTPConfigForm.tsx @@ -36,7 +36,8 @@ export function FunctionConfigForm(props: FunctionConfigFormProps) { const events = [ { name: 'onCreate', description: 'Triggered when a new file is created' }, - { name: 'onDelete', description: 'Triggered when a file is deleted' } + { name: 'onDelete', description: 'Triggered when a file is deleted' }, + { name: 'onError', description: 'Triggered when an error occurs during file processing' } ]; // Check if all functions with a specific metadata.label are enabled diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/Parameters/ParamEditor.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/Parameters/ParamEditor.tsx new file mode 100644 index 0000000000..37d48d6934 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/Parameters/ParamEditor.tsx @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) + * + * 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 { Divider, Typography } from '@wso2/ui-toolkit'; +import { EditorContainer } from '../../../styles'; +import { getPrimaryInputType, LineRange, ParameterModel } from '@wso2/ballerina-core'; +import { FormField, FormImports } from '@wso2/ballerina-side-panel'; +import FormGeneratorNew from '../../../../Forms/FormGeneratorNew'; +import { useRpcContext } from '@wso2/ballerina-rpc-client'; +import { getImportsForProperty } from '../../../../../../utils/bi'; + +export interface ParamEditorProps { + param: ParameterModel; + onChange: (param: ParameterModel) => void; + onSave?: (param: ParameterModel) => void; + onCancel?: (param?: ParameterModel) => void; +} + +export function ParamEditor(props: ParamEditorProps) { + const { param, onChange, onSave, onCancel } = props; + + const { rpcClient } = useRpcContext(); + const [currentFields, setCurrentFields] = useState([]); + const [filePath, setFilePath] = useState(''); + const [targetLineRange, setTargetLineRange] = useState(); + + const handleOnCancel = () => { + onCancel?.(param); + }; + + useEffect(() => { + rpcClient.getVisualizerRpcClient().joinProjectPath({ segments: ['main.bal'] }).then((response) => { + setFilePath(response.filePath); + }); + updateFormFields(); + }, []); + + const updateFormFields = () => { + const fields: FormField[] = []; + const nameFieldType = getPrimaryInputType(param.name?.types)?.fieldType || "TEXT"; + const typeFieldType = getPrimaryInputType(param.type?.types)?.fieldType || "TEXT"; + + // Add name field + fields.push({ + key: `name`, + label: 'Name', + type: nameFieldType, + optional: false, + editable: true, + documentation: '', + enabled: param.name?.enabled ?? true, + value: param.name?.value ?? '', + types: [{ fieldType: nameFieldType, selected: false }] + }); + + // Add type field + fields.push({ + key: `type`, + label: 'Type', + type: typeFieldType, + optional: false, + editable: true, + documentation: param?.type?.metadata?.description || '', + enabled: param.type?.enabled ?? true, + value: param.type?.value || "json", + defaultValue: "json", + types: [{ fieldType: typeFieldType, selected: false }] + }); + + setCurrentFields(fields); + }; + + useEffect(() => { + updateFormFields(); + }, [param.name, param.type]); + + const onParameterSubmit = (dataValues: Record, formImports: FormImports) => { + const updatedParam = { + ...param, + type: { + ...param.type, + value: dataValues['type'] ?? param.type?.value ?? "json", + imports: getImportsForProperty('type', formImports) + }, + name: { ...param.name, value: dataValues['name'] ?? param.name?.value ?? "" } + }; + + // Update the parent component's state first + onChange(updatedParam); + + // Then call onSave if provided + if (onSave) { + onSave(updatedParam); + } + }; + + useEffect(() => { + if (filePath && rpcClient) { + rpcClient + .getBIDiagramRpcClient() + .getEndOfFile({ filePath }) + .then((res) => { + setTargetLineRange({ + startLine: res, + endLine: res, + }); + }); + } + }, [filePath, rpcClient]); + + return ( + + + Content Schema + + + <> + {filePath && targetLineRange && + + } + + + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/Parameters/Parameters.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/Parameters/Parameters.tsx index a818a98eef..2cafa9f0f8 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/Parameters/Parameters.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/Parameters/Parameters.tsx @@ -16,6 +16,7 @@ * under the License. */ +import React, { useState } from "react"; import styled, { CSSObject } from "@emotion/styled"; import { ParameterModel } from "@wso2/ballerina-core"; import { Codicon } from "@wso2/ui-toolkit"; @@ -27,11 +28,12 @@ import { disabledHeaderLabel, headerLabelStyles, } from "../../../styles"; +import { ParamEditor } from "./ParamEditor"; export interface ParametersProps { parameters: ParameterModel[]; onChange: (parameters: ParameterModel[]) => void; - onEditClick: (param: ParameterModel) => void; + onEditClick?: (param: ParameterModel) => void; showPayload: boolean; streamEnabled?: boolean; } @@ -41,20 +43,25 @@ const ParamLabelContainer = styled.div` align-items: center; gap: 12px; font-family: var(--vscode-font-family); + flex: 1; `; +const ParamName = styled.span` + color: var(--vscode-editor-foreground, #222); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--vscode-font-family); +`; const ParamType = styled.span` - font-size: 12px; + font-size: 13px; color: var(--vscode-descriptionForeground, #888); background: var(--vscode-editorWidget-background, #f5f5f5); border-radius: 4px; padding: 2px 8px; letter-spacing: 0.1px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; `; const HeaderLabel = styled.div` @@ -69,13 +76,62 @@ const HeaderLabel = styled.div` `; export function Parameters(props: ParametersProps) { - const { parameters, onChange, onEditClick, showPayload, streamEnabled } = props; + const { parameters, onChange, onEditClick, showPayload } = props; + + const [editModel, setEditModel] = useState(undefined); + const [editingIndex, setEditingIndex] = useState(-1); const onDelete = (param: ParameterModel) => { const updatedParameters = parameters.filter( (p) => p.metadata.label !== param.metadata.label || p.name.value !== param.name.value ); onChange(updatedParameters); + setEditModel(undefined); + setEditingIndex(-1); + }; + + const handleEdit = (param: ParameterModel) => { + if (param.editable === false) { + return; + } + + if (onEditClick) { + onEditClick(param); + return; + } + + setEditModel(param); + const index = parameters.findIndex(p => + p.metadata?.label === param.metadata?.label && + p.name?.value === param.name?.value + ); + setEditingIndex(index); + }; + + const onChangeParam = (param: ParameterModel) => { + setEditModel(param); + // Update the parameters array in real-time for existing parameters + if (editingIndex >= 0) { + const updatedParameters = [...parameters]; + updatedParameters[editingIndex] = param; + onChange(updatedParameters); + } + }; + + const onSaveParam = (param: ParameterModel) => { + const updatedParam = { ...param, enabled: true }; + if (editingIndex >= 0) { + const updatedParameters = [...parameters]; + updatedParameters[editingIndex] = updatedParam; + onChange(updatedParameters); + } + setEditModel(undefined); + setEditingIndex(-1); + }; + + const onParamEditCancel = () => { + setEditModel(undefined); + setEditingIndex(-1); }; return ( @@ -88,7 +144,12 @@ export function Parameters(props: ParametersProps) { const label = ( - {formattedTypeValue} + + {formattedTypeValue} + + {param.name?.value && ( + {param.name.value} + )} ); @@ -98,24 +159,32 @@ export function Parameters(props: ParametersProps) {
!readonly && onEditClick(param)} + onClick={() => handleEdit(param)} > {label}
- - {!readonly && ( + {!readonly && ( + - onEditClick(param)} /> + handleEdit(param)} /> - )} - - onDelete(param)} /> - - + + onDelete(param)} /> + + + )} ); })} + {editModel && !onEditClick && ( + + )} )}
diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/index.tsx index dbdd9b0e5a..a40fd8600e 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/FTPForm/index.tsx @@ -16,10 +16,10 @@ * under the License. */ -import { useEffect, useState } from 'react'; -import { ActionButtons, Divider, SidePanelBody, ProgressIndicator, Tooltip, CheckBoxGroup, CheckBox, Codicon, LinkButton, Dropdown, Typography } from '@wso2/ui-toolkit'; +import { useEffect, useMemo, useState } from 'react'; +import { ActionButtons, Divider, SidePanelBody, ProgressIndicator, Tooltip, CheckBoxGroup, CheckBox, Codicon, LinkButton, Dropdown, Typography, RadioButtonGroup, HeaderExpressionEditor } from '@wso2/ui-toolkit'; import styled from '@emotion/styled'; -import { FunctionModel, ParameterModel, GeneralPayloadContext, Type, ServiceModel, Protocol, Imports } from '@wso2/ballerina-core'; +import { FunctionModel, ParameterModel, GeneralPayloadContext, Type, ServiceModel, Protocol, Imports, PropertyModel } from '@wso2/ballerina-core'; import { EntryPointTypeCreator } from '../../../../../components/EntryPointTypeCreator'; import { Parameters } from './Parameters/Parameters'; @@ -40,6 +40,113 @@ const AddButtonWrapper = styled.div` margin: 8px 0; `; +const PostProcessSection = styled.div` + margin: 0; +`; + +const SectionHeader = styled.div` + display: flex; + align-items: center; + padding: 8px 0; +`; + +const SectionContent = styled.div` + padding-left: 8px; + margin-top: 8px; +`; + +const PostProcessContent = styled(SectionContent)` + display: flex; + flex-direction: column; + gap: 20px; +`; + +const PostProcessChoiceContainer = styled.div` + margin-top: 4px; + margin-left: 16px; +`; + +const NestedFields = styled.div` + margin-left: 24px; + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 12px; +`; + +const AdvancedConfigsHeader = styled(SectionHeader)` + cursor: pointer; + user-select: none; + &:hover { + opacity: 0.8; + } +`; + +const AdvancedConfigsContent = styled(SectionContent)<{ isExpanded: boolean }>` + display: ${({ isExpanded }: { isExpanded: boolean }) => (isExpanded ? 'block' : 'none')}; +`; + +const InfoBanner = styled.div` + display: flex; + gap: 8px; + padding: 8px 12px; + border-left: 3px solid var(--vscode-focusBorder); + background: var(--vscode-inputValidation-infoBackground); + border-radius: 4px; + align-items: flex-start; +`; + +const POST_PROCESS_RADIO_GROUP_SX = { + "& vscode-radio-group": { + display: "flex", + flexDirection: "column", + gap: "2px" + }, + "& vscode-radio": { + margin: 0 + } +}; +/** + * Converts a PascalCase or camelCase type name to a camelCase parameter name. + * For CSV format, pluralizes the name since it represents an array of rows. + */ +const typeNameToParamName = (typeName: string, pluralize: boolean = false): string => { + if (!typeName) return "content"; + + let baseName = typeName.trim(); + if (!baseName) return "content"; + + // Remove module qualifier and array suffix + if (baseName.includes(":")) { + baseName = baseName.split(":").pop() || baseName; + } + if (baseName.endsWith("[]")) { + baseName = baseName.slice(0, -2); + } + // Remove non-identifier characters + baseName = baseName.replace(/[^A-Za-z0-9_]/g, ""); + if (!baseName || /^\d/.test(baseName)) return "content"; + + // Convert to camelCase (lowercase first letter) + const camelCase = baseName.charAt(0).toLowerCase() + baseName.slice(1); + + if (!pluralize) return camelCase; + + // Simple pluralization rules + const lastChar = camelCase.slice(-1); + const lastTwoChars = camelCase.slice(-2); + + if (lastTwoChars === 'ss' || lastTwoChars === 'sh' || lastTwoChars === 'ch' || lastChar === 'x' || lastChar === 'z') { + return camelCase + 'es'; + } + if (lastChar === 'y' && !['a', 'e', 'i', 'o', 'u'].includes(camelCase.slice(-2, -1))) { + return camelCase.slice(0, -1) + 'ies'; + } + if (lastChar === 's') { + return camelCase; + } + return camelCase + 's'; +}; export const EditorContentColumn = styled.div` display: flex; flex-direction: column; @@ -75,40 +182,41 @@ export function FTPForm(props: FTPFormProps) { const [isTypeEditorOpen, setIsTypeEditorOpen] = useState(false); // Filter non-enabled functions for dropdown options based on selected handler - const nonEnabledFunctions = serviceModel.functions?.filter(fn => { - if (!fn.enabled && selectedHandler && fn.metadata?.label === selectedHandler) { - return true; - } - // If no selectedHandler is provided, default to onCreate for backward compatibility - if (!fn.enabled && !selectedHandler && fn.metadata?.label === "onCreate") { - return true; - } - return false; - }) || []; + const nonEnabledFunctions = useMemo(() => { + return serviceModel.functions?.filter(fn => { + if (!fn.enabled && selectedHandler && fn.metadata?.label === selectedHandler) { + return true; + } + // If no selectedHandler is provided, default to onCreate for backward compatibility + if (!fn.enabled && !selectedHandler && fn.metadata?.label === "onCreate") { + return true; + } + return false; + }) || []; + }, [serviceModel.functions, selectedHandler]); - // Reset form state when model prop changes useEffect(() => { setServiceModel(model); - // Set initial function model based on first non-enabled function matching the selected handler - if (isNew && nonEnabledFunctions.length > 0) { - const initialFunction = nonEnabledFunctions[0]; - setFunctionModel(initialFunction); - setSelectedFileFormat(initialFunction.name?.metadata?.label || ''); - } - if (!isNew) { - setFunctionModel(props.functionModel); - setSelectedFileFormat(props.functionModel?.name?.metadata?.label || ''); + }, [model]); + + // Initialize add-handler state from currently available FTP handler templates. + useEffect(() => { + if (!isNew || nonEnabledFunctions.length === 0) { + return; } - }, [model, selectedHandler]); + const initialFunction = nonEnabledFunctions[0]; + setFunctionModel(initialFunction); + setSelectedFileFormat(initialFunction.name?.metadata?.label || ''); + }, [isNew, nonEnabledFunctions]); - // Update function model when selectedHandler changes + // Initialize edit state from the selected function model only. useEffect(() => { - if (isNew && selectedHandler && nonEnabledFunctions.length > 0) { - const initialFunction = nonEnabledFunctions[0]; - setFunctionModel(initialFunction); - setSelectedFileFormat(initialFunction.name?.metadata?.label || ''); + if (isNew) { + return; } - }, [selectedHandler]); + setFunctionModel(props.functionModel || null); + setSelectedFileFormat(props.functionModel?.name?.metadata?.label || ''); + }, [isNew, props.functionModel]); const handleParamChange = (params: ParameterModel[]) => { if (functionModel) { @@ -160,7 +268,7 @@ export function FTPForm(props: FTPFormProps) { if ( selectedFileFormat === 'RAW'){ if (isStreamEnabled){ - return `stream`; + return `stream`; } else { return `byte[]`; } @@ -171,7 +279,9 @@ export function FTPForm(props: FTPFormProps) { baseType = baseType.slice(0, -2); } else if (baseType.startsWith("stream<")) { - if (baseType.endsWith(", error>")) { + if (baseType.endsWith(", error?>")) { + baseType = baseType.slice(7, -9); + } else if (baseType.endsWith(", error>")) { baseType = baseType.slice(7, -8); } else if (baseType.endsWith(">")) { baseType = baseType.slice(7, -1); @@ -180,7 +290,7 @@ export function FTPForm(props: FTPFormProps) { // Apply the correct wrapper based on stream state if (isStreamEnabled) { - return `stream<${baseType}, error>`; + return `stream<${baseType}, error?>`; } else { return `${baseType}[]`; } @@ -201,7 +311,9 @@ export function FTPForm(props: FTPFormProps) { baseType = baseType.slice(0, -2); } else if (baseType.startsWith("stream<")) { - if (baseType.endsWith(", error>")) { + if (baseType.endsWith(", error?>")) { + baseType = baseType.slice(7, -9); + } else if (baseType.endsWith(", error>")) { baseType = baseType.slice(7, -8); } else if (baseType.endsWith(">")) { baseType = baseType.slice(7, -1); @@ -217,6 +329,10 @@ export function FTPForm(props: FTPFormProps) { if (payloadParam) { const typeValue = typeof type === 'string' ? type : type.name; + // Derive param name from type name (pluralize for CSV since it's an array of rows) + const shouldPluralize = selectedFileFormat === 'CSV'; + const paramName = typeNameToParamName(typeValue, shouldPluralize); + // Update all parameters in one pass const updatedParameters = functionModel.parameters.map(param => { // Enable DATA_BINDING parameter with new type @@ -230,7 +346,7 @@ export function FTPForm(props: FTPFormProps) { } return { ...param, - name: { ...param.name, value: "content" }, + name: { ...param.name, value: paramName }, type: updatedType, enabled: true }; @@ -285,9 +401,6 @@ export function FTPForm(props: FTPFormProps) { setFunctionModel(updatedFunctionModel); }; - const handleEditContentSchema = () => { - setIsTypeEditorOpen(true); - }; // Define parameter configuration from frontend const parameterConfig = { @@ -326,15 +439,293 @@ export function FTPForm(props: FTPFormProps) { ); // Check if properties exist for conditional rendering const hasStreamProperty = functionModel?.properties?.stream !== undefined; + const isOnCreateHandler = selectedHandler === 'onCreate' || functionModel?.metadata?.label === 'onCreate'; + const isOnErrorHandler = selectedHandler === 'onError' || functionModel?.metadata?.label === 'onError'; + const showOnErrorInfo = !!isNew && isOnErrorHandler; + + // Post-processing action handling - nested structure under postProcessAction + const postProcessAction = functionModel?.properties?.postProcessAction as PropertyModel | undefined; + const postProcessActionOnSuccess = postProcessAction?.properties?.onSuccess as PropertyModel | undefined; + const postProcessActionOnError = postProcessAction?.properties?.onError as PropertyModel | undefined; + + // Check if we have the two-action format + const hasSuccessAction = postProcessActionOnSuccess !== undefined && postProcessActionOnSuccess.choices !== undefined; + const hasErrorAction = postProcessActionOnError !== undefined && postProcessActionOnError.choices !== undefined; + + const shouldShowAdvancedConfigsDivider = isOnCreateHandler || hasSuccessAction || hasErrorAction; + + // State for Advanced Configs section + const [isAdvancedConfigsExpanded, setIsAdvancedConfigsExpanded] = useState(false); + + const getSelectedActionValue = (action: PropertyModel | undefined): string => { + if (!action?.choices || action.choices.length === 0) { + return ''; + } + const enabledChoice = action.choices.find((choice: PropertyModel) => choice.enabled); + if (enabledChoice?.value) { + return enabledChoice.value; + } + const moveChoice = action.choices.find((choice: PropertyModel) => choice.value === 'MOVE'); + return (moveChoice?.value || action.choices[0]?.value || ''); + }; + + const getSelectedChoice = (action: PropertyModel | undefined, selectedValue?: string): PropertyModel | undefined => { + if (!action?.choices || action.choices.length === 0) { + return undefined; + } + const enabledChoice = action.choices.find((choice: PropertyModel) => choice.enabled); + if (enabledChoice) { + return enabledChoice; + } + if (selectedValue) { + const selectedChoice = action.choices.find((choice: PropertyModel) => choice.value === selectedValue); + if (selectedChoice) { + return selectedChoice; + } + } + return action.choices.find((choice: PropertyModel) => choice.value === 'MOVE') || action.choices[0]; + }; + + const isMoveToRequiredAndEmpty = (action: PropertyModel | undefined, selectedValue?: string): boolean => { + if (!action?.choices) { + return false; + } + const isActionEnabled = action.enabled ?? true; + if (!isActionEnabled) { + return false; + } + const selectedChoice = getSelectedChoice(action, selectedValue); + if (!selectedChoice || selectedChoice.value !== 'MOVE') { + return false; + } + const moveToValue = selectedChoice.properties?.moveTo?.value || ""; + const trimmedMoveTo = moveToValue.trim(); + return !trimmedMoveTo || trimmedMoveTo === '""' || trimmedMoveTo === "''"; + }; + + const selectedSuccessAction = getSelectedActionValue(postProcessActionOnSuccess); + const selectedErrorAction = getSelectedActionValue(postProcessActionOnError); + const hasInvalidMoveTo = + isMoveToRequiredAndEmpty(postProcessActionOnSuccess, selectedSuccessAction) || + isMoveToRequiredAndEmpty(postProcessActionOnError, selectedErrorAction); + + // Generic handler for post-process action change (nested under postProcessAction) + const handleActionChange = (propertyKey: string, action: PropertyModel | undefined, value: string) => { + if (!functionModel || !action?.choices || !postProcessAction) return; + + const updatedChoices = action.choices.map((choice: PropertyModel) => ({ + ...choice, + enabled: choice.value === value + })); + + setFunctionModel({ + ...functionModel, + properties: { + ...functionModel.properties, + postProcessAction: { + ...postProcessAction, + properties: { + ...postProcessAction.properties, + [propertyKey]: { + ...action, + enabled: true, + choices: updatedChoices + } + } + } + } + }); + }; + + const handleActionToggle = ( + propertyKey: string, + action: PropertyModel | undefined, + checked: boolean, + selectedValue?: string + ) => { + if (!functionModel || !action?.choices || !postProcessAction) return; + + let updatedChoices = action.choices; + if (checked) { + const selectedIndex = selectedValue + ? action.choices.findIndex((choice: PropertyModel) => choice.value === selectedValue) + : -1; + const moveIndex = action.choices.findIndex((choice: PropertyModel) => choice.value === 'MOVE'); + const existingEnabledIndex = action.choices.findIndex((choice: PropertyModel) => choice.enabled); + const indexToEnable = existingEnabledIndex !== -1 + ? existingEnabledIndex + : (selectedIndex >= 0 && selectedIndex < action.choices.length) + ? selectedIndex + : (moveIndex !== -1 ? moveIndex : 0); + + updatedChoices = action.choices.map((choice: PropertyModel, index: number) => ({ + ...choice, + enabled: index === indexToEnable + })); + } + + setFunctionModel({ + ...functionModel, + properties: { + ...functionModel.properties, + postProcessAction: { + ...postProcessAction, + properties: { + ...postProcessAction.properties, + [propertyKey]: { + ...action, + enabled: checked, + choices: updatedChoices + } + } + } + } + }); + }; + + // Generic handler for Move To change (nested under postProcessAction) + const handleMoveToChangeGeneric = (propertyKey: string, action: PropertyModel | undefined, value: string) => { + if (!functionModel || !action?.choices || !postProcessAction) return; + + const moveChoiceIndex = action.choices.findIndex((choice: PropertyModel) => choice.value === 'MOVE'); + if (moveChoiceIndex === -1) return; + + const updatedChoices = [...action.choices]; + updatedChoices[moveChoiceIndex] = { + ...updatedChoices[moveChoiceIndex], + properties: { + ...updatedChoices[moveChoiceIndex].properties, + moveTo: { + ...updatedChoices[moveChoiceIndex].properties?.moveTo, + value: value + } + } + }; + + setFunctionModel({ + ...functionModel, + properties: { + ...functionModel.properties, + postProcessAction: { + ...postProcessAction, + properties: { + ...postProcessAction.properties, + [propertyKey]: { + ...action, + choices: updatedChoices + } + } + } + } + }); + }; + + // Get the current MOVE choice properties for any action + const getMoveChoicePropertiesGeneric = (action: PropertyModel | undefined) => { + if (!action?.choices) return { moveTo: "" }; + const moveChoice = action.choices.find((choice: PropertyModel) => choice.value === 'MOVE'); + if (!moveChoice?.properties) return { moveTo: "" }; + + const moveToValue = moveChoice.properties.moveTo?.value || ""; + + return { + moveTo: moveToValue + }; + }; + + // Render a post-processing action section + const renderPostProcessActionSection = ( + subtitle: string, + propertyKey: string, + action: PropertyModel | undefined, + selectedValue: string + ) => { + if (!action?.choices) return null; + + const isActionEnabled = action.enabled ?? true; + const moveProperties = getMoveChoicePropertiesGeneric(action); + const trimmedMoveTo = moveProperties.moveTo?.trim(); + const isMoveToEmpty = !trimmedMoveTo || trimmedMoveTo === '""' || trimmedMoveTo === "''"; + + return ( + + + handleActionToggle(propertyKey, action, checked, selectedValue)} + sx={{ + marginTop: 0, + description: action.metadata?.description || '' + }} + /> + + {isActionEnabled && ( + + ({ + id: `${propertyKey}-${index}`, + value: choice.value, + content: choice.metadata?.label || choice.value + })) || []} + onChange={(e) => { + handleActionChange(propertyKey, action, e.target.value); + }} + /> + + )} + + {/* Show nested fields for MOVE action */} + {isActionEnabled && selectedValue === 'MOVE' && ( + +
+ + Move To + + handleMoveToChangeGeneric(propertyKey, action, value)} + onSave={() => {}} + onCancel={() => {}} + /> + + Destination path expression to move the file + + {isMoveToEmpty && ( + + Move To is required + + )} +
+
+ )} +
+ ); + }; return ( <> {isSaving && } + {showOnErrorInfo && ( + + + + On Error runs only for errors when file content cannot be mapped to the Content Schema. + + + )} {/* File Configuration Section - Only show for onCreate handler */} - {(selectedHandler === 'onCreate'|| functionModel?.metadata?.label === 'onCreate') && ( + {isOnCreateHandler && ( @@ -376,7 +767,6 @@ export function FTPForm(props: FTPFormProps) { handleParamChange(params); } }} - onEditClick={handleEditContentSchema} showPayload={true} streamEnabled={hasStreamProperty ? functionModel.properties.stream.enabled : undefined} /> @@ -437,50 +827,79 @@ export function FTPForm(props: FTPFormProps) { )} - {(fileInfoParameter || callerParameter) && (selectedHandler === 'onCreate' || functionModel?.metadata?.label === 'onCreate') ? : null} - - {/* File Metadata Section */} - {fileInfoParameter && ( + {/* Post-Processing Actions Section - Show two actions for all handlers */} + {(hasSuccessAction || hasErrorAction) && ( <> - - - { - const updatedParameters = functionModel.parameters.map((p) => { - if (p === fileInfoParameter) { - return { ...p, enabled: checked }; - } - return p; - }); - handleParamChange(updatedParameters); - }} - sx={{ marginTop: 0, description: parameterConfig.fileInfo.description}} - /> - + {isOnCreateHandler && } + + After File Processing + + + {hasSuccessAction && renderPostProcessActionSection( + "Success", + "onSuccess", + postProcessActionOnSuccess, + selectedSuccessAction + )} + {hasErrorAction && renderPostProcessActionSection( + "Error", + "onError", + postProcessActionOnError, + selectedErrorAction + )} + )} - {/* FTP Connection Section */} - {callerParameter && ( + {/* Advanced Configs Section - Contains File Metadata and FTP Connection */} + {(fileInfoParameter || callerParameter) && ( <> - - { - const updatedParameters = functionModel.parameters.map((p) => { - if (p === callerParameter) { - return { ...p, enabled: checked }; - } - return p; - }); - handleParamChange(updatedParameters); - }} - sx={{ marginTop: 0, description: parameterConfig.caller.description }} - /> - + {shouldShowAdvancedConfigsDivider && } + setIsAdvancedConfigsExpanded(!isAdvancedConfigsExpanded)}> + + Advanced Parameters + + + {/* File Metadata Section */} + {fileInfoParameter && ( + + { + const updatedParameters = functionModel.parameters.map((p) => { + if (p === fileInfoParameter) { + return { ...p, enabled: checked }; + } + return p; + }); + handleParamChange(updatedParameters); + }} + sx={{ marginTop: 0, description: parameterConfig.fileInfo.description}} + /> + + )} + + {/* FTP Connection Section */} + {callerParameter && ( + + { + const updatedParameters = functionModel.parameters.map((p) => { + if (p === callerParameter) { + return { ...p, enabled: checked }; + } + return p; + }); + handleParamChange(updatedParameters); + }} + sx={{ marginTop: 8, description: parameterConfig.caller.description }} + /> + + )} + )} @@ -488,8 +907,8 @@ export function FTPForm(props: FTPFormProps) { primaryButton={{ text: isSaving ? "Saving..." : "Save", onClick: handleSave, - tooltip: isSaving ? "Saving..." : "Save", - disabled: isSaving, + tooltip: isSaving ? "Saving..." : hasInvalidMoveTo ? "Move To is required" : "Save", + disabled: isSaving || hasInvalidMoveTo, loading: isSaving, }} secondaryButton={{ @@ -507,12 +926,15 @@ export function FTPForm(props: FTPFormProps) { isOpen={isTypeEditorOpen} onClose={handleTypeEditorClose} onTypeCreate={handleTypeCreated} - initialTypeName={"ContentSchema"} + initialTypeName={"Content"} modalTitle={"Define Content Schema"} payloadContext={payloadContext} defaultTab="create-from-scratch" modalWidth={650} modalHeight={600} + note={selectedFileFormat === 'CSV' + ? "Define schema for one row -- file content will be array of row schema." + : undefined} /> ); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx index 6f5d8e6136..d5a8888000 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx @@ -16,14 +16,15 @@ * under the License. */ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; import { Typography, ProgressRing } from "@wso2/ui-toolkit"; -import { FormField, FormImports, FormValues } from "@wso2/ballerina-side-panel"; +import { FormField, FormImports, FormValues, StringTemplateEditorConfig } from "@wso2/ballerina-side-panel"; import { ListenerModel, LineRange, RecordTypeField, Property, getPrimaryInputType } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import FormGeneratorNew from "../../Forms/FormGeneratorNew"; import { getImportsForProperty } from "../../../../utils/bi"; +import { isValueEqual } from "../utils"; const Container = styled.div` /* padding: 0 20px 20px; */ @@ -56,18 +57,23 @@ interface ListenerConfigFormProps { onBack?: () => void; formSubmitText?: string; onChange?: (data: ListenerModel) => void; + onDirtyChange?: (isDirty: boolean) => void; + onValidityChange?: (isValid: boolean) => void; + filePath?: string; } export function ListenerConfigForm(props: ListenerConfigFormProps) { const { rpcClient } = useRpcContext(); const [listenerFields, setListenerFields] = useState([]); - const { listenerModel, onSubmit, onBack, formSubmitText = "Next", isSaving, onChange } = props; + const { listenerModel, onSubmit, onBack, formSubmitText = "Next", isSaving, onChange, onDirtyChange, onValidityChange, filePath: targetFilePath } = props; const [filePath, setFilePath] = useState(''); const [targetLineRange, setTargetLineRange] = useState(); const [recordTypeFields, setRecordTypeFields] = useState([]); + const initialFieldValuesRef = useRef>({}); useEffect(() => { + let cancelled = false; const recordTypeFields: RecordTypeField[] = Object.entries(listenerModel.properties) .filter(([_, property]) => getPrimaryInputType(property.types)?.typeMembers && @@ -89,18 +95,34 @@ export function ListenerConfigForm(props: ListenerConfigFormProps) { } as Property, recordTypeMembers: getPrimaryInputType(property.types)?.typeMembers.filter(member => member.kind === "RECORD_TYPE") })); - console.log(">>> recordTypeFields", recordTypeFields); setRecordTypeFields(recordTypeFields); - listenerModel && setListenerFields(convertConfig(listenerModel)); - rpcClient.getVisualizerRpcClient().joinProjectPath({ segments: ['main.bal'] }).then((response) => { - setFilePath(response.filePath); - }); - }, [listenerModel]); + if (listenerModel) { + const convertedFields = convertConfig(listenerModel); + setListenerFields(convertedFields); + initialFieldValuesRef.current = convertedFields.reduce((acc, field) => { + acc[field.key] = field.value; + return acc; + }, {} as Record); + onDirtyChange?.(false); + } + if (targetFilePath) { + setFilePath(targetFilePath); + } else { + rpcClient.getVisualizerRpcClient().joinProjectPath({ segments: ['main.bal'] }).then((response) => { + if (!cancelled && !targetFilePath) { + setFilePath(response.filePath); + } + }); + } + return () => { + cancelled = true; + }; + }, [listenerModel, rpcClient, targetFilePath]); const handleListenerSubmit = async (data: FormValues, formImports: FormImports) => { listenerFields.forEach(val => { - if (data[val.key]) { + if (data[val.key] !== undefined) { val.value = data[val.key] } val.imports = getImportsForProperty(val.key, formImports); @@ -109,18 +131,18 @@ export function ListenerConfigForm(props: ListenerConfigFormProps) { onSubmit(response); }; - const handleListenerChange = (fieldKey: string, value: any, allValues: FormValues) => { + const handleListenerChange = (_fieldKey: string, _value: any, allValues: FormValues) => { if (onChange && !allValues["defaultListener"]) { let hasChanges = false; - console.log("Listener change: ", fieldKey, value, allValues); listenerFields.forEach(val => { - if (allValues[val.key] !== undefined && allValues[val.key] !== val.value) { + if (allValues[val.key] !== undefined && !isValueEqual(allValues[val.key], initialFieldValuesRef.current[val.key])) { hasChanges = true; } - if (allValues[val.key]) { + if (allValues[val.key] !== undefined) { val.value = allValues[val.key] } }) + onDirtyChange?.(hasChanges); if (!hasChanges) { return; } @@ -173,6 +195,7 @@ export function ListenerConfigForm(props: ListenerConfigFormProps) { recordTypeFields={recordTypeFields} onChange={handleListenerChange} hideSaveButton={onChange ? true : false} + onValidityChange={onValidityChange} /> } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/ParamEditor.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/ParamEditor.tsx index 5457e8ac85..b2ce62ff8c 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/ParamEditor.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Parameters/ParamEditor.tsx @@ -189,7 +189,7 @@ export function ParamEditor(props: ParamProps) { documentation: '', enabled: param.name?.enabled, value: param.name.value, - types: [{fieldType: getPrimaryInputType(param.name.types)?.fieldType, selected: false}] + types: [{ fieldType: getPrimaryInputType(param.name.types)?.fieldType, selected: false }] }); fields.push({ key: `type`, @@ -203,7 +203,7 @@ export function ParamEditor(props: ParamProps) { defaultValue: "string", value: param.type.value, items: ["string", "int", "float", "decimal", "boolean"], - types: [{fieldType: getPrimaryInputType(param.type.types)?.fieldType, selected: false}] + types: [{ fieldType: getPrimaryInputType(param.type.types)?.fieldType, selected: false }] }); break; case "HEADER": @@ -217,7 +217,7 @@ export function ParamEditor(props: ParamProps) { documentation: '', enabled: true, value: (param.headerName?.value || "Content-Type").replace(/"/g, ""), - types: [{fieldType: getPrimaryInputType(param.headerName?.types)?.fieldType, selected: false}], + types: [{ fieldType: "AUTOCOMPLETE", selected: false }], // TODO: Need to come up with a better way to handle this onValueChange: (value: string | boolean) => { const sanitizeValue = (value as string) .replace(/-([a-zA-Z])/g, (_, c) => c ? c.toUpperCase() : '') @@ -227,9 +227,10 @@ export function ParamEditor(props: ParamProps) { // Set the sanitized value to the variable name field // When the header name changes, auto-update the variable name field (param.name.value) to a sanitized version if (param.name && typeof param.name === 'object') { - param.name.value = sanitizeValue; - param.headerName.value = `"${value}"`; - onChange({ ...param, name: { ...param.name, value: sanitizedValueWithLowerFirst } }); + if (isNew) { + param.name.value = sanitizedValueWithLowerFirst; + } + onChange({ ...param, name: { ...param.name }, headerName: { ...param.headerName, value: `"${value}"` } }); } } }); @@ -243,7 +244,7 @@ export function ParamEditor(props: ParamProps) { documentation: '', enabled: param.name?.enabled, value: param.name.value || "contentType", - types: [{fieldType: getPrimaryInputType(param.name.types)?.fieldType, selected: false}] + types: [{ fieldType: getPrimaryInputType(param.name.types)?.fieldType, selected: false }] }); fields.push({ key: `type`, @@ -257,7 +258,7 @@ export function ParamEditor(props: ParamProps) { defaultValue: "string", value: param.type.value, items: ["string", "int", "float", "decimal", "boolean"], - types: [{fieldType: getPrimaryInputType(param.type.types)?.fieldType, selected: false}] + types: [{ fieldType: getPrimaryInputType(param.type.types)?.fieldType, selected: false }] }); break; case "PAYLOAD": @@ -270,7 +271,7 @@ export function ParamEditor(props: ParamProps) { documentation: '', enabled: param.name?.enabled, value: param.name.value, - types: [{fieldType: getPrimaryInputType(param.name.types)?.fieldType, selected: false}] + types: [{ fieldType: getPrimaryInputType(param.name.types)?.fieldType, selected: false }] }); fields.push({ key: `type`, @@ -282,7 +283,7 @@ export function ParamEditor(props: ParamProps) { enabled: param.type?.enabled, value: param.type.value || "json", defaultValue: "json", - types: [{fieldType: getPrimaryInputType(param.type.types)?.fieldType, selected: false}], + types: [{ fieldType: getPrimaryInputType(param.type.types)?.fieldType, selected: false }], // isContextTypeSupported: true // Enable this to support context typeEditor }); break; @@ -300,7 +301,7 @@ export function ParamEditor(props: ParamProps) { documentation: '', enabled: true, value: (param.defaultValue as PropertyModel)?.value, - types: [{fieldType: getPrimaryInputType((param.defaultValue as PropertyModel).types)?.fieldType, selected: false}] + types: [{ fieldType: getPrimaryInputType((param.defaultValue as PropertyModel).types)?.fieldType, selected: false }] }); } setCurrentFields(fields); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourcePath/ResourcePath.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourcePath/ResourcePath.tsx index 3ada78f08f..c416ed80ec 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourcePath/ResourcePath.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/ResourcePath/ResourcePath.tsx @@ -243,8 +243,7 @@ export function ResourcePath(props: ResourcePathProps) { label="Resource Path" size={70} onTextChange={(input) => { - const trimmedInput = input.startsWith('/') ? input.slice(1) : input; - handlePathChange(trimmedInput); + handlePathChange(input); }} disabled={readonly} onKeyUp={handleBlur} 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 2da3a63bdf..a32c6670f3 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 @@ -86,7 +86,6 @@ export function ResponseEditor(props: ParamProps) { const updateNewFields = (res: StatusCodeResponse, hasBody: boolean = true) => { const NO_BODY_TYPES = ["http:Response", "http:NoContent", "error"]; const defaultItems = [ - "", "string", "int", "boolean", @@ -169,6 +168,7 @@ export function ResponseEditor(props: ParamProps) { type: "AUTOCOMPLETE", items: ["application/json", "application/xml", "application/x-www-form-urlencoded", "multipart/form-data", "text/plain"], key: `mediaType`, + types: [{ fieldType: "AUTOCOMPLETE", selected: true }], defaultValue: res.mediaType.value, }); } @@ -183,6 +183,8 @@ export function ResponseEditor(props: ParamProps) { ...convertPropertyToFormField(res.name), key: `check`, type: "FLAG", + optional: true, + types: [{ fieldType: "FLAG", selected: false }], label: "Make this response reusable", documentation: "Check this option to make this response reusable", onValueChange: (value: string | boolean) => { diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Utils/ResourcePathParser.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Utils/ResourcePathParser.tsx index 86ca3a202a..136b7b725a 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Utils/ResourcePathParser.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/Utils/ResourcePathParser.tsx @@ -26,6 +26,12 @@ export function parseResourcePath(input: string): ParseResult { segments: [] }; + // Path cannot start with a / character + if (input.startsWith('/')) { + result.errors.push({ position: 0, message: 'path cannot start with a slash (/)' }); + return result; + } + if (!input || input === '') { result.valid = false; result.errors.push({ position: 0, message: 'path cannot be empty' }); @@ -36,7 +42,7 @@ export function parseResourcePath(input: string): ParseResult { result.segments.push({ type: 'dot', start: 0, end: 0 }); result.valid = result.errors.length === 0; if (!result.valid) { - result.errors.push({ position: 0, message: 'cannot have charcaters after dot (.)' }); + result.errors.push({ position: 0, message: 'cannot have characters after dot (.)' }); } return result; } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/index.tsx index 369f7864b7..2654edf159 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ResourceForm/index.tsx @@ -173,7 +173,9 @@ export function ResourceForm(props: ResourceFormProps) { if (createMore) { closeMethod(); } - functionModel.name.value = sanitizedHttpPath(functionModel.name.value as string); + if (functionModel.name.value !== ".") { + functionModel.name.value = sanitizedHttpPath(functionModel.name.value as string); + } onSave(functionModel, !createMore); } 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 4c9a26d3f6..1315efa691 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 @@ -16,16 +16,16 @@ * under the License. */ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; -import { FormField, FormImports, FormValues } from "@wso2/ballerina-side-panel"; +import { FormField, FormImports, FormValues, StringTemplateEditorConfig } from "@wso2/ballerina-side-panel"; import { getPrimaryInputType, LineRange, Property, RecordTypeField, ServiceModel, SubPanel, SubPanelView } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { URI, Utils } from "vscode-uri"; import { FormGeneratorNew } from "../../Forms/FormGeneratorNew"; import { FormHeader } from "../../../../components/FormHeader"; import { getImportsForProperty } from "../../../../utils/bi"; -import { removeForwardSlashes, sanitizedHttpPath } from "../utils"; +import { isValueEqual, removeForwardSlashes, sanitizedHttpPath } from "../utils"; const Container = styled.div` /* padding: 0 20px 20px; */ @@ -68,16 +68,19 @@ interface ServiceConfigFormProps { onBack?: () => void; formSubmitText?: string; onChange?: (data: ServiceModel) => void; + onDirtyChange?: (isDirty: boolean) => void; + onValidityChange?: (isValid: boolean) => void; } export function ServiceConfigForm(props: ServiceConfigFormProps) { const { rpcClient } = useRpcContext(); const [serviceFields, setServiceFields] = useState([]); - const { serviceModel, onSubmit, onBack, openListenerForm, formSubmitText = "Next", isSaving, onChange } = props; + const { serviceModel, onSubmit, onBack, openListenerForm, formSubmitText = "Next", isSaving, onChange, onDirtyChange, onValidityChange } = props; const [filePath, setFilePath] = useState(''); const [targetLineRange, setTargetLineRange] = useState(); const [recordTypeFields, setRecordTypeFields] = useState([]); + const initialFieldValuesRef = useRef>({}); useEffect(() => { // Check if the service is HTTP protocol and any properties with choices @@ -142,7 +145,15 @@ export function ServiceConfigForm(props: ServiceConfigFormProps) { setRecordTypeFields(recordTypeFields); } - serviceModel && setServiceFields(convertConfig(serviceModel)); + if (serviceModel) { + const convertedFields = convertConfig(serviceModel); + setServiceFields(convertedFields); + initialFieldValuesRef.current = convertedFields.reduce((acc, field) => { + acc[field.key] = field.value; + return acc; + }, {} as Record); + onDirtyChange?.(false); + } rpcClient.getVisualizerRpcClient().joinProjectPath({ segments: ['main.bal'] }).then((response) => { setFilePath(response.filePath); }); @@ -177,11 +188,12 @@ export function ServiceConfigForm(props: ServiceConfigFormProps) { // First, check if any changes exist before modifying serviceFields let hasChanges = false; for (let val of serviceFields) { - if (allValues[val.key] !== undefined && allValues[val.key] !== val.value) { + if (allValues[val.key] !== undefined && !isValueEqual(allValues[val.key], initialFieldValuesRef.current[val.key])) { hasChanges = true; break; } } + onDirtyChange?.(hasChanges); if (!hasChanges) { return; } @@ -240,6 +252,7 @@ export function ServiceConfigForm(props: ServiceConfigFormProps) { preserveFieldOrder={true} onChange={handleServiceChange} hideSaveButton={onChange ? true : false} + onValidityChange={onValidityChange} /> } diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/ServiceConfigureView.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/ServiceConfigureView.tsx index 1bac8837c8..d34907103c 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/ServiceConfigureView.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/ServiceConfigureView.tsx @@ -16,9 +16,9 @@ * under the License. */ -import { useEffect, useState, useRef } from "react"; +import { useCallback, useEffect, useState, useRef } from "react"; import styled from "@emotion/styled"; -import { ConfigProperties, ConfigVariable, DIRECTORY_MAP, getPrimaryInputType, LineRange, ListenerModel, NodePosition, ProjectStructureArtifactResponse, ServiceModel } from "@wso2/ballerina-core"; +import { ConfigProperties, ConfigVariable, DIRECTORY_MAP, getPrimaryInputType, LineRange, ListenerModel, NodePosition, ProjectStructureArtifactResponse, PropertyModel, ServiceModel } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { Button, Codicon, Icon, LinkButton, ProgressRing, SidePanelBody, SplitView, TabPanel, ThemeColors, TreeView, TreeViewItem, Typography, View, ViewContent } from "@wso2/ui-toolkit"; import { TopNavigationBar } from "../../../components/TopNavigationBar"; @@ -186,6 +186,17 @@ interface ChangeMap { filePath: string; } +function getDisplayServiceName(service?: ServiceModel): string { + const serviceName = service?.name || ""; + if (!serviceName) { + return serviceName; + } + if (service?.moduleName !== "ftp") { + return serviceName; + } + return serviceName.replace(/\s*-\s*\/$/, ""); +} + const Overlay = styled.div` position: fixed; width: 100vw; @@ -200,25 +211,39 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { const [serviceModel, setServiceModel] = useState(undefined); const [listeners, setListeners] = useState([]); + const [currentIdentifier, setCurrentIdentifier] = useState(null); + const [isSaving, setIsSaving] = useState(false); - const [hasChanges, setHasChanges] = useState(false); const [existingListenerType, setExistingListenerType] = useState(""); // Example: "Listener", "CdcListener" const [selectedListener, setSelectedListener] = useState(null); const [changeMap, setChangeMap] = useState<{ [key: string]: ChangeMap }>({}); + const [dirtyFormMap, setDirtyFormMap] = useState<{ [key: string]: boolean }>({}); const { addModal, closeModal } = useModalStack() // Helper function to create key from filePath and position - const getChangeKey = (filePath: string, position: NodePosition) => { - return `${filePath}:${position.startLine}:${position.startColumn}:${position.endLine}:${position.endColumn}`; + const getChangeKey = (filePath: string, position: NodePosition, isService: boolean) => { + const modelType = isService ? "service" : "listener"; + return `${modelType}:${filePath}:${position.startLine}:${position.startColumn}:${position.endLine}:${position.endColumn}`; }; // Helper function to add a change to the map const addChangeToMap = (filePath: string, position: NodePosition, data: ServiceModel | ListenerModel, isService: boolean) => { - const key = getChangeKey(filePath, position); + const key = getChangeKey(filePath, position, isService); setChangeMap(prev => ({ ...prev, [key]: { data, isService, filePath } })); }; + const removeChangeFromMap = (filePath: string, position: NodePosition, isService: boolean) => { + const key = getChangeKey(filePath, position, isService); + setChangeMap(prev => { + if (!prev[key]) { + return prev; + } + const next = { ...prev }; + delete next[key]; + return next; + }); + }; const [configTitle, setConfigTitle] = useState(""); @@ -245,6 +270,31 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { const [listenerType, setListenerType] = useState<"SINGLE" | "MULTIPLE">("MULTIPLE"); const [haveServiceConfigs, setHaveServiceConfigs] = useState(true); + // Track validity of each form (service form + listener forms) to disable Save when diagnostics exist + const [formValidityMap, setFormValidityMap] = useState<{ [key: string]: boolean }>({}); + const hasDiagnostics = Object.values(formValidityMap).some(isValid => !isValid); + const hasDirtyChanges = Object.values(dirtyFormMap).some(isDirty => isDirty); + + const handleFormValidityChange = useCallback((formKey: string, isValid: boolean) => { + setFormValidityMap(prev => { + if (prev[formKey] === isValid) return prev; + return { ...prev, [formKey]: isValid }; + }); + }, []); + + const handleFormDirtyChange = useCallback((filePath: string, position: NodePosition, isService: boolean, isDirty: boolean) => { + const key = getChangeKey(filePath, position, isService); + setDirtyFormMap(prev => { + if (prev[key] === isDirty) { + return prev; + } + return { ...prev, [key]: isDirty }; + }); + if (!isDirty) { + removeChangeFromMap(filePath, position, isService); + } + }, []); + useEffect(() => { fetchService(props.position); }, [props.position]); @@ -315,7 +365,7 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { const serviceSection = visibleSections.find(s => s.isService); if (serviceSection && serviceSection.ratio > 0.05) { newVisibleSection = 'service'; - newTitle = `${serviceModel.name} Configuration`; + newTitle = `${getDisplayServiceName(serviceModel)} Configuration`; } else { // Get all visible listeners const visibleListenerIds = visibleSections @@ -424,11 +474,14 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { console.log("Service Model: ", res.service); // Set the service model setServiceModel(res.service); - setConfigTitle(`${res.service.name} Configuration`); + setConfigTitle(`${getDisplayServiceName(res.service)} Configuration`); // Set the service listeners setServiceListeners(res.service); // Find the listener type findListenerType(res.service); + // Reset change state on load - Save button should be disabled until user makes changes + setChangeMap({}); + setDirtyFormMap({}); }); } catch (error) { console.log("Error fetching service model: ", error); @@ -456,59 +509,157 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { setListenerType(detectedType); } + const getAttachedListenerNames = (listenerProperty?: PropertyModel): string[] => { + if (!listenerProperty) { + return []; + } + const names: string[] = []; + if (Array.isArray(listenerProperty.values)) { + names.push(...listenerProperty.values.filter(Boolean)); + } + if (listenerProperty.value && !names.includes(listenerProperty.value)) { + names.unshift(listenerProperty.value); + } + return names; + }; + const setServiceListeners = (service: ServiceModel) => { rpcClient.getVisualizerLocation().then((location) => { const projectPath = location.projectPath; rpcClient.getBIDiagramRpcClient().getProjectStructure().then((res) => { const project = res.projects.find(project => project.projectPath === projectPath); - const listeners = project?.directoryMap[DIRECTORY_MAP.LISTENER]; - if (service?.properties?.listener) { - const listenerProperty = service.properties.listener.properties; - const listenersToSet: ProjectStructureArtifactResponse[] = []; - Object.keys(listenerProperty).forEach((listener) => { - const listenerItem = listeners?.find((l) => l.name === listener); - if (listenerItem) { - listenersToSet.push(listenerItem); - } else { - const property = listenerProperty[listener]; - listenersToSet.push({ - id: listener, - name: listener, - path: props.filePath, - type: "TYPE", - position: { - startLine: property.codedata.lineRange.startLine.line, - startColumn: property.codedata.lineRange.startLine.offset, - endLine: property.codedata.lineRange.endLine.line, - endColumn: property.codedata.lineRange.endLine.offset, - }, - }); - } - }); + const projectListeners = project?.directoryMap[DIRECTORY_MAP.LISTENER] || []; + const listenersToSet: ProjectStructureArtifactResponse[] = []; + const listenerPropertyModel = service?.properties?.listener; + if (!listenerPropertyModel) { setListeners(listenersToSet); + return; } + + const attachedListenerNames = getAttachedListenerNames(listenerPropertyModel); + const listenerProperties = listenerPropertyModel.properties || {}; + attachedListenerNames.forEach((listenerName) => { + const listenerItem = projectListeners.find((l) => l.name === listenerName); + if (listenerItem) { + listenersToSet.push(listenerItem); + return; + } + + const property = listenerProperties[listenerName]; + if (property?.codedata?.lineRange) { + listenersToSet.push({ + id: listenerName, + name: listenerName, + path: props.filePath, + type: "TYPE", + position: { + startLine: property.codedata.lineRange.startLine.line, + startColumn: property.codedata.lineRange.startLine.offset, + endLine: property.codedata.lineRange.endLine.line, + endColumn: property.codedata.lineRange.endLine.offset, + }, + }); + } else { + listenersToSet.push({ + id: listenerName, + name: listenerName, + path: props.filePath, + type: DIRECTORY_MAP.LISTENER, + position: { + startLine: props.position.startLine, + startColumn: props.position.startColumn, + endLine: props.position.endLine, + endColumn: props.position.endColumn, + }, + }); + } + }); + + setListeners(listenersToSet); }); }); }; const handleOnAttachListener = async (listenerName: string) => { - if (serviceModel.properties['listener'].value && serviceModel.properties['listener'].values.length === 0) { - serviceModel.properties['listener'].values = [serviceModel.properties['listener'].value]; + const listenerProperty = serviceModel?.properties?.listener; + if (!listenerProperty) { + return; } - serviceModel.properties['listener'].values.push(listenerName); - const res = await rpcClient.getServiceDesignerRpcClient().updateServiceSourceCode({ filePath: props.filePath, service: serviceModel }); + + const existingNames = getAttachedListenerNames(listenerProperty); + if (existingNames.includes(listenerName)) { + closeModal(POPUP_IDS.ATTACH_LISTENER); + return; + } + + const updatedListeners = [...existingNames, listenerName]; + const updatedService = { + ...serviceModel, + properties: { + ...serviceModel.properties, + listener: { + ...listenerProperty, + values: updatedListeners, + value: updatedListeners[0] || listenerProperty.value || "" + } + } + }; + + setServiceModel(updatedService); + const res = await rpcClient.getServiceDesignerRpcClient().updateServiceSourceCode({ + filePath: props.filePath, + service: updatedService + }); const updatedArtifact = res.artifacts.at(0); + if (!updatedArtifact) { + console.error("No artifact returned after attaching listener"); + return; + } + setCurrentIdentifier(updatedArtifact.name); await fetchService(updatedArtifact.position); closeModal(POPUP_IDS.ATTACH_LISTENER); setChangeMap({}); + setDirtyFormMap({}); } const handleOnDetachListener = async (listenerName: string) => { - serviceModel.properties['listener'].values = serviceModel.properties['listener'].values.filter(listener => listener !== listenerName); - const res = await rpcClient.getServiceDesignerRpcClient().updateServiceSourceCode({ filePath: props.filePath, service: serviceModel }); + const listenerProperty = serviceModel?.properties?.listener; + if (!listenerProperty) { + return; + } + const existingNames = getAttachedListenerNames(listenerProperty); + const updatedListeners = existingNames.filter(listener => listener !== listenerName); + + if (updatedListeners.length === 0) { + return; + } + + const updatedService = { + ...serviceModel, + properties: { + ...serviceModel.properties, + listener: { + ...listenerProperty, + values: updatedListeners, + value: updatedListeners[0] + } + } + }; + + setServiceModel(updatedService); + const res = await rpcClient.getServiceDesignerRpcClient().updateServiceSourceCode({ + filePath: props.filePath, + service: updatedService + }); const updatedArtifact = res.artifacts.at(0); + if (!updatedArtifact) { + console.error("No artifact returned after detaching listener"); + return; + } + setCurrentIdentifier(updatedArtifact.name); await fetchService(updatedArtifact.position); setChangeMap({}); + setDirtyFormMap({}); } const handleOnListenerClick = (listenerId: string) => { @@ -532,30 +683,35 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { const handleListenerChange = async (data: ListenerModel, filePath: string, position: NodePosition) => { addChangeToMap(filePath, position, data, false); - setHasChanges(true); setIsSaving(false); } const handleServiceChange = async (data: ServiceModel, filePath: string, position: NodePosition) => { addChangeToMap(filePath, position, data, true); - setHasChanges(true); setIsSaving(false); } const handleSave = async () => { setIsSaving(true); const changes = Object.values(changeMap); - for (const change of changes) { - if (change.isService) { - const res = await rpcClient.getServiceDesignerRpcClient().updateServiceSourceCode({ filePath: change.filePath, service: change.data as ServiceModel }); - const updatedArtifact = res.artifacts.at(0); - await fetchService(updatedArtifact.position); - } else { - await rpcClient.getServiceDesignerRpcClient().updateListenerSourceCode({ filePath: change.filePath, listener: change.data as ListenerModel }); + const listenerChanges = changes.filter((c) => !c.isService); + const serviceChanges = changes.filter((c) => c.isService); + // Listeners first, then service last + for (const change of listenerChanges) { + await rpcClient.getServiceDesignerRpcClient().updateListenerSourceCode({ filePath: change.filePath, listener: change.data as ListenerModel }); + } + for (const change of serviceChanges) { + const res = await rpcClient.getServiceDesignerRpcClient().updateServiceSourceCode({ filePath: change.filePath, service: change.data as ServiceModel }); + const updatedArtifact = res.artifacts.at(0); + if (!updatedArtifact) { + console.error("No artifact returned after saving service changes"); + continue; } + setCurrentIdentifier(updatedArtifact.name); + await fetchService(updatedArtifact.position); } setChangeMap({}); - setHasChanges(false); + setDirtyFormMap({}); setIsSaving(false); } @@ -565,6 +721,10 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { } } + const handleGoBack = () => { + rpcClient.getVisualizerRpcClient().goBack({ identifier: currentIdentifier }); + } + return ( @@ -576,7 +736,7 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { { serviceModel && ( <> - +
@@ -600,7 +760,7 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { fontWeight: !selectedListener ? 'bold' : 'normal' }} - >{serviceModel.name} + >{getDisplayServiceName(serviceModel)} )} @@ -694,7 +854,7 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { {configTitle} - @@ -711,7 +871,13 @@ export function ServiceConfigureView(props: ServiceConfigureProps) {
{haveServiceConfigs && (
- + handleFormDirtyChange(filePath, position, true, isDirty)} + onValidityChange={(isValid) => handleFormValidityChange('service', isValid)} + />
)} {listeners.map((listener) => ( @@ -740,7 +906,11 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { filePath={listener.path} position={listener.position} onChange={handleListenerChange} + onDirtyChange={(isDirty, filePath, position) => handleFormDirtyChange(filePath, position, false, isDirty)} setListenerType={handleSetListenerType} + onValidityChange={(isValid) => handleFormValidityChange(listener.id, isValid)} + isAttachedListener={listeners.indexOf(listener) > 0} + listenerName={listener.name} />
@@ -756,8 +926,9 @@ export function ServiceConfigureView(props: ServiceConfigureProps) { version={serviceModel.version} moduleName={serviceModel.moduleName} type={existingListenerType} + removeDeprecated={serviceModel.moduleName === "ftp" && !!serviceModel.properties?.annotServiceConfig} onAttachListener={handleOnAttachListener} - attachedListeners={listeners.map(listener => listener.name)} + attachedListeners={getAttachedListenerNames(serviceModel.properties?.listener)} /> , POPUP_IDS.ATTACH_LISTENER, "Attach Listener", 600, 500); }}> Attach Listener @@ -785,11 +956,15 @@ interface ServiceConfigureListenerEditViewProps { filePath: string; position: NodePosition; onChange?: (data: ListenerModel, filePath: string, position: NodePosition) => void; + onDirtyChange?: (isDirty: boolean, filePath: string, position: NodePosition) => void; setListenerType?: (type: string) => void; + onValidityChange?: (isValid: boolean) => void; + isAttachedListener?: boolean; // True if this is an attached listener (not the first/primary one) + listenerName?: string; // Name of the listener for display purposes } function ServiceConfigureListenerEditView(props: ServiceConfigureListenerEditViewProps) { - const { filePath, position, onChange, setListenerType } = props; + const { filePath, position, onChange, onDirtyChange, setListenerType, onValidityChange, isAttachedListener = false, listenerName } = props; const { rpcClient } = useRpcContext(); const [listenerModel, setListenerModel] = useState(undefined); @@ -814,7 +989,7 @@ function ServiceConfigureListenerEditView(props: ServiceConfigureListenerEditVie (properties[key] as any).name === "listenerType" || (properties[key] as any).metadata?.label === "Listener Type" ); - if (listenerTypeKey && properties[listenerTypeKey]?.value) { + if (listenerTypeKey && properties[listenerTypeKey]?.value && setListenerType) { setListenerType(properties[listenerTypeKey].value); } }; @@ -834,6 +1009,18 @@ function ServiceConfigureListenerEditView(props: ServiceConfigureListenerEditVie onChange(data, filePath, position); } + const handleListenerDirtyChange = (isDirty: boolean) => { + onDirtyChange?.(isDirty, filePath, position); + } + + // Check if this is a legacy listener (legacy FTP listeners carry path/folderPath in listener properties) + const isLegacyListener = + listenerModel?.properties?.folderPath !== undefined || + listenerModel?.properties?.path !== undefined; + + // For attached listeners in new system (no folderPath in listener), show only monitoring path + const showMinimalConfig = isAttachedListener && !isLegacyListener; + return ( {!listenerModel && @@ -842,13 +1029,50 @@ function ServiceConfigureListenerEditView(props: ServiceConfigureListenerEditVie Loading... } - {listenerModel && - + {listenerModel && !showMinimalConfig && + + } + {listenerModel && showMinimalConfig && + } ); }; +// Minimal config component for attached listeners in new system (no folderPath in listener) +interface AttachedListenerMinimalConfigProps { + listenerName?: string; + onSave?: (value: ListenerModel) => void; + isSaving?: boolean; + savingText?: string; +} + +function AttachedListenerMinimalConfig(props: AttachedListenerMinimalConfigProps) { + const { listenerName } = props; + + return ( +
+ + This service is attached to the existing listener {listenerName}. + The monitoring path is configured at the service level. + +
+ ); +} namespace S { @@ -883,8 +1107,9 @@ interface AttachListenerModalProps { packageName: string; version: string; type: string; + removeDeprecated?: boolean; attachedListeners: string[]; - onAttachListener: (listenerName: string) => void; + onAttachListener: (listenerName: string) => Promise; } function AttachListenerModal(props: AttachListenerModalProps) { @@ -903,13 +1128,16 @@ function AttachListenerModal(props: AttachListenerModalProps) { useEffect(() => { setIsLoading(true); - rpcClient.getServiceDesignerRpcClient().getListeners({ filePath: props.filePath, moduleName: props.moduleName }).then(res => { - console.log("Existing listeners: ", res.listeners); + rpcClient.getServiceDesignerRpcClient().getListeners({ + filePath: props.filePath, + moduleName: props.moduleName, + removeDeprecated: props.removeDeprecated + }).then(res => { setExistingListeners(res.listeners.filter(listener => !props.attachedListeners.includes(listener)).filter(listener => !listener.includes("+"))); }).finally(() => { setIsLoading(false); }); - }, [props.filePath, props.moduleName]); + }, [props.filePath, props.moduleName, props.removeDeprecated]); const handleTabChange = (tabId: string) => { setActiveTab(tabId as "existing" | "new"); @@ -922,29 +1150,45 @@ function AttachListenerModal(props: AttachListenerModalProps) { version: props.version, type: props.type, }, - filePath: props.filePath + filePath: props.filePath, + removeDeprecated: props.removeDeprecated }; if (tabId === "new") { + setIsLoading(true); + setListenerModel(undefined); rpcClient.getServiceDesignerRpcClient().getListenerModel(payload).then(res => { - console.log("New listener model: ", res.listener) setListenerModel(res.listener); - }) + }).finally(() => { + setIsLoading(false); + }); } } - const handleListenerSelect = (listenerName: string) => { - console.log("Listener selected: ", listenerName); + const handleListenerSelect = async (listenerName: string) => { setAttachingListener(listenerName); - props.onAttachListener(listenerName); + try { + await props.onAttachListener(listenerName); + } catch (error) { + console.error("Failed to attach listener", error); + } finally { + setAttachingListener(undefined); + } } const onCreateNewListener = async (value?: ListenerModel) => { - if (value) { - const listenerName = value.properties['variableNameKey'].value; - setAttachingListener(listenerName); + if (!value) { + return; + } + const listenerName = value.properties['variableNameKey'].value; + setAttachingListener(listenerName); + try { await rpcClient.getServiceDesignerRpcClient().addListenerSourceCode({ filePath: "", listener: value }); - handleListenerSelect(listenerName); + await props.onAttachListener(listenerName); + } catch (error) { + console.error("Failed to create and attach listener", error); + } finally { + setAttachingListener(undefined); } }; @@ -1001,8 +1245,13 @@ function AttachListenerModal(props: AttachListenerModalProps) { {existingListeners.map((listener) => ( handleListenerSelect(listener)} + enabled={!attachingListener} + onClick={() => { + if (attachingListener) { + return; + } + void handleListenerSelect(listener); + }} > {} @@ -1027,7 +1276,13 @@ function AttachListenerModal(props: AttachListenerModalProps) { )} {!isLoading && listenerModel && ( - + )} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/ServiceEditView.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/ServiceEditView.tsx index 796265838d..5f8b1e58d8 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/ServiceEditView.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/ServiceEditView.tsx @@ -85,10 +85,12 @@ export interface ServiceEditViewProps { filePath: string; position: NodePosition; onChange?: (data: ServiceModel, filePath: string, position: NodePosition) => void; + onDirtyChange?: (isDirty: boolean, filePath: string, position: NodePosition) => void; + onValidityChange?: (isValid: boolean) => void; } export function ServiceEditView(props: ServiceEditViewProps) { - const { filePath, position, onChange } = props; + const { filePath, position, onChange, onDirtyChange, onValidityChange } = props; const { rpcClient } = useRpcContext(); const [serviceModel, setServiceModel] = useState(undefined); @@ -119,6 +121,10 @@ export function ServiceEditView(props: ServiceEditViewProps) { } } + const handleServiceDirtyChange = (isDirty: boolean) => { + onDirtyChange?.(isDirty, filePath, position); + } + const handleListenerSubmit = async (value?: ListenerModel) => { setSaving(true); let listenerName; @@ -145,7 +151,7 @@ export function ServiceEditView(props: ServiceEditViewProps) { return ( <> - {serviceModel && } + {serviceModel && } ); }; 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 2edc88460b..29cee2aef4 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/index.tsx @@ -199,6 +199,8 @@ export function ServiceDesigner(props: ServiceDesignerProps) { const [showFunctionConfigForm, setShowFunctionConfigForm] = useState(false); const [projectListeners, setProjectListeners] = useState([]); const prevPosition = useRef(position); + const positionRef = useRef(position); + const isMountedRef = useRef(true); const [resources, setResources] = useState([]); const [searchValue, setSearchValue] = useState(""); @@ -242,30 +244,40 @@ export function ServiceDesigner(props: ServiceDesignerProps) { } } - // Check if there are any available FTP handlers (onCreate or onDelete) that are not yet enabled + // Check if there are any available FTP handlers (onCreate, onDelete, onError) that are not yet enabled const hasAvailableFTPHandlers = () => { if (!serviceModel?.functions) return false; const onCreateFunctions = serviceModel.functions.filter(fn => fn.metadata?.label === 'onCreate'); const onDeleteFunctions = serviceModel.functions.filter(fn => fn.metadata?.label === 'onDelete'); + const onErrorFunctions = serviceModel.functions.filter(fn => fn.metadata?.label === 'onError'); const deprecatedFunctions = serviceModel.functions.filter(fn => fn.metadata?.label === 'EVENT'); const hasAvailableOnCreate = onCreateFunctions.length > 0 && onCreateFunctions.some(fn => !fn.enabled); const hasAvailableOnDelete = onDeleteFunctions.length > 0 && onDeleteFunctions.some(fn => !fn.enabled); + const hasAvailableOnError = onErrorFunctions.length > 0 && onErrorFunctions.some(fn => !fn.enabled); const hasDeprecatedFunctions = deprecatedFunctions.length > 0 && deprecatedFunctions.some(fn => fn.enabled); // Remove the add handler option if deprecated APIs present - return (hasAvailableOnCreate || hasAvailableOnDelete) && !hasDeprecatedFunctions; + return (hasAvailableOnCreate || hasAvailableOnDelete || hasAvailableOnError) && !hasDeprecatedFunctions; }; useEffect(() => { + positionRef.current = position; + isMountedRef.current = true; + if (!serviceModel || isPositionChanged(prevPosition.current, position)) { fetchService(position); } rpcClient.onProjectContentUpdated(() => { - fetchService(position); + if (!isMountedRef.current) return; + fetchService(positionRef.current); }); + + return () => { + isMountedRef.current = false; + }; }, [position]); const fetchService = (targetPosition: NodePosition, addMore?: boolean) => { @@ -278,6 +290,7 @@ export function ServiceDesigner(props: ServiceDesignerProps) { .getServiceDesignerRpcClient() .getServiceModelFromCode({ filePath, codedata: { lineRange } }) .then((res) => { + if (!isMountedRef.current) return; console.log("Service Model: ", res.service); if (addMore) { handleNewResourceFunction(); @@ -550,6 +563,7 @@ export function ServiceDesigner(props: ServiceDesignerProps) { const handleNewFunctionClose = () => { setShowForm(false); + setSelectedFTPHandler(undefined); // If a handler was selected, also close the FunctionConfigForm if (selectedHandler) { setShowFunctionConfigForm(false); @@ -560,6 +574,7 @@ export function ServiceDesigner(props: ServiceDesignerProps) { const handleFunctionEdit = (value: FunctionModel) => { setFunctionModel(value); setIsNew(false); + setSelectedFTPHandler(undefined); setShowForm(true); }; @@ -752,6 +767,21 @@ export function ServiceDesigner(props: ServiceDesignerProps) { }; const haveServiceTypeName = serviceModel?.properties["serviceTypeName"]?.value; + const displayServiceName = isFtpService + ? (serviceModel?.name || "").replace(/\s*-\s*\/$/, "") + : serviceModel?.name; + + const getFtpHandlerTitle = () => { + const handlerKey = (selectedFTPHandler || functionModel?.metadata?.label || "").toLowerCase(); + const handlerLabelMap: Record = { + "oncreate": "On Create", + "ondelete": "On Delete", + "onerror": "On Error" + }; + const handlerLabel = handlerLabelMap[handlerKey] || "Handler"; + const prefix = isNew ? "New " : ""; + return `${prefix}${handlerLabel} Handler Configuration`; + }; const openInit = async (resource: ProjectStructureArtifactResponse) => { await rpcClient @@ -806,7 +836,7 @@ export function ServiceDesigner(props: ServiceDesignerProps) { serviceModel && ( <> @@ -1346,7 +1376,7 @@ export function ServiceDesigner(props: ServiceDesignerProps) { {isFtpService && serviceModel && ( { + if (value === null || value === undefined) return value; + if (Array.isArray(value) || (typeof value === "object" && value.constructor === Object)) { + return value; + } + if (typeof value === "string") { + const trimmed = value.trim(); + if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) { + try { + return JSON.parse(trimmed); + } catch { + // Not valid JSON, treat as plain string + } + } + const serialized = serializeValue.serializeValue(value).trim().replace(/^"|"$/g, ""); + return serialized.replace(/\s+/g, " ").trim(); + } + return value; + }; + + const stableStringify = (obj: any): string => { + if (obj === null || obj === undefined) return JSON.stringify(obj); + if (Array.isArray(obj)) { + return "[" + obj.map(stableStringify).join(",") + "]"; + } + if (typeof obj === "object") { + const keys = Object.keys(obj).sort(); + return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}"; + } + return JSON.stringify(obj); + }; + + const normalizedCurrent = normalizeForComparison(currentValue); + const normalizedInitial = normalizeForComparison(initialValue); + + const currentIsObj = typeof normalizedCurrent === "object" && normalizedCurrent !== null; + const initialIsObj = typeof normalizedInitial === "object" && normalizedInitial !== null; + + if (currentIsObj && initialIsObj) { + return stableStringify(normalizedCurrent) === stableStringify(normalizedInitial); + } + if (currentIsObj !== initialIsObj) return false; + + const strCurrent = String(normalizedCurrent).replace(/\s+/g, " ").trim(); + const strInitial = String(normalizedInitial).replace(/\s+/g, " ").trim(); + return strCurrent === strInitial; +}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TestFunctionForm/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TestFunctionForm/index.tsx index d4693ab63a..830d5a3b1f 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TestFunctionForm/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TestFunctionForm/index.tsx @@ -37,7 +37,6 @@ const FormContainer = styled.div` .side-panel-body { overflow: visible; - overflow-y: hidden; } `; 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 87d52ea4c4..dbdc1b02c0 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/index.tsx @@ -18,11 +18,11 @@ import { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react'; import { debounce } from 'lodash'; -import { Imports, LineRange, PayloadContext, Type, Protocol } from '@wso2/ballerina-core'; +import { Imports, LineRange, PayloadContext, Type, Protocol, functionKinds } from '@wso2/ballerina-core'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; -import { ContextTypeEditor, EditorContext, StackItem, TypeEditor, TypeHelperCategory, TypeHelperItem, TypeHelperOperator } from '@wso2/type-editor'; +import { ContextTypeEditor, TypeEditor, TypeHelperCategory, TypeHelperItem, TypeHelperOperator } from '@wso2/type-editor'; import { TYPE_HELPER_OPERATORS } from './constants'; -import { filterOperators, filterTypes, getImportedTypes, getTypeBrowserTypes, getTypes } from './utils'; +import { filterOperators, filterTypes, getFilteredTypesByKind, getTypeBrowserTypes, getTypes } from './utils'; import { useMutation } from '@tanstack/react-query'; import { Overlay, ThemeColors } from '@wso2/ui-toolkit'; import { createPortal } from 'react-dom'; @@ -55,10 +55,11 @@ type FormTypeEditorProps = { payloadContext?: PayloadContext; simpleType?: string; defaultTab?: 'import' | 'create-from-scratch' | 'browse-exisiting-types'; + note?: string; }; export const FormTypeEditor = (props: FormTypeEditorProps) => { - const { type, onTypeChange, newType, newTypeValue, onCloseCompletions, getNewTypeCreateForm, onSaveType, refetchTypes, isPopupTypeForm, isContextTypeForm, simpleType, payloadContext, defaultTab, isGraphql } = props; + const { type, onTypeChange, newType, newTypeValue, onCloseCompletions, getNewTypeCreateForm, onSaveType, refetchTypes, isPopupTypeForm, isContextTypeForm, simpleType, payloadContext, defaultTab, isGraphql, note } = props; const { rpcClient } = useRpcContext(); const isCdcService = payloadContext?.protocol === Protocol.CDC; @@ -70,6 +71,7 @@ export const FormTypeEditor = (props: FormTypeEditorProps) => { const [basicTypes, setBasicTypes] = useState([]); const [importedTypes, setImportedTypes] = useState([]); + const [workspaceTypes, setWorkspaceTypes] = useState([]); const [filteredBasicTypes, setFilteredBasicTypes] = useState([]); const [filteredOperators, setFilteredOperators] = useState([]); const [filteredTypeBrowserTypes, setFilteredTypeBrowserTypes] = useState([]); @@ -106,88 +108,93 @@ export const FormTypeEditor = (props: FormTypeEditorProps) => { }, [refetchTypes]); const debouncedSearchTypeHelper = useCallback( - debounce(async (searchText: string, isType: boolean) => { - if (!rpcClient) return; - - if (isType && (!fetchedInitialTypes.current || refetchTypes)) { - try { - let types; - if (isGraphql) { - const context = type?.codedata?.node === "CLASS" - ? TypeHelperContext.GRAPHQL_FIELD_TYPE - : TypeHelperContext.GRAPHQL_INPUT_TYPE; - types = await rpcClient.getServiceDesignerRpcClient().getResourceReturnTypes({ - filePath: filePath, - context: context, - }); - } else { - types = await rpcClient.getBIDiagramRpcClient().getVisibleTypes({ - filePath: filePath, - position: { - line: targetLineRange.startLine.line, - offset: targetLineRange.startLine.offset - }, - }); - } - const basicTypes = getTypes(types, false, payloadContext); - setBasicTypes(basicTypes); - setFilteredBasicTypes(basicTypes); - fetchedInitialTypes.current = true; + debounce(async (searchText: string, isType: boolean) => { + if (!rpcClient) return; - if (!isGraphql && !isCdcService) { - const searchResponse = await rpcClient.getBIDiagramRpcClient().search({ - filePath: filePath, - position: targetLineRange, - queryMap: { - q: '', - offset: 0, - limit: 1000 - }, - searchKind: 'TYPE' - }); + if (isType && (!fetchedInitialTypes.current || refetchTypes)) { + try { + let types; + if (isGraphql) { + const context = type?.codedata?.node === "CLASS" + ? TypeHelperContext.GRAPHQL_FIELD_TYPE + : TypeHelperContext.GRAPHQL_INPUT_TYPE; + types = await rpcClient.getServiceDesignerRpcClient().getResourceReturnTypes({ + filePath: filePath, + context: context, + }); + } else { + types = await rpcClient.getBIDiagramRpcClient().getVisibleTypes({ + filePath: filePath, + position: { + line: targetLineRange.startLine.line, + offset: targetLineRange.startLine.offset + }, + }); + } + const basicTypes = getTypes(types, false, payloadContext); + setBasicTypes(basicTypes); + setFilteredBasicTypes(basicTypes); + fetchedInitialTypes.current = true; - const importedTypes = getImportedTypes(searchResponse.categories); - setImportedTypes(importedTypes); - } - - } catch (error) { - console.error(error); - } finally { - setLoading(false); + const searchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: filePath, + position: targetLineRange, + queryMap: { + q: '', + offset: 0, + limit: 1000 + }, + searchKind: 'TYPE' + }); + + const workspaceTypes = getFilteredTypesByKind(searchResponse.categories, functionKinds.CURRENT); + setWorkspaceTypes(workspaceTypes); + + if (!isGraphql && !isCdcService) { + const importedTypes = getFilteredTypesByKind(searchResponse.categories, functionKinds.IMPORTED); + setImportedTypes(importedTypes); } - } else if (isType) { - setFilteredBasicTypes(filterTypes(basicTypes, searchText)); - if (!isCdcService) { - try { - const response = await rpcClient.getBIDiagramRpcClient().search({ - filePath: filePath, - position: targetLineRange, - queryMap: { - q: searchText, - offset: 0, - limit: 1000 - }, - searchKind: 'TYPE' - }); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + } else if (isType) { + setFilteredBasicTypes(filterTypes(basicTypes, searchText)); - const importedTypes = getImportedTypes(response.categories); - setImportedTypes(importedTypes); - } catch (error) { - console.error(error); - } finally { - setLoading(false); - } - } else { + if (!isCdcService) { + try { + const response = await rpcClient.getBIDiagramRpcClient().search({ + filePath: filePath, + position: targetLineRange, + queryMap: { + q: searchText, + offset: 0, + limit: 1000 + }, + searchKind: 'TYPE' + }); + + const importedTypes = getFilteredTypesByKind(response.categories, functionKinds.IMPORTED); + const workspaceTypes = getFilteredTypesByKind(response.categories, functionKinds.CURRENT); + setImportedTypes(importedTypes); + setWorkspaceTypes(workspaceTypes); + } catch (error) { + console.error(error); + } finally { setLoading(false); } } else { - setFilteredOperators(filterOperators(TYPE_HELPER_OPERATORS, searchText)); setLoading(false); } - }, 150), - [basicTypes, filePath, targetLineRange, rpcClient] - ); + } else { + setFilteredOperators(filterOperators(TYPE_HELPER_OPERATORS, searchText)); + setLoading(false); + } + }, 150), + [basicTypes, filePath, targetLineRange, rpcClient] + ); const handleSearchTypeHelper = useCallback( (searchText: string, isType: boolean) => { setLoading(true); @@ -264,12 +271,14 @@ export const FormTypeEditor = (props: FormTypeEditorProps) => { simpleType={simpleType} payloadContext={payloadContext} defaultTab={defaultTab} + note={note} typeHelper={{ loading, loadingTypeBrowser, referenceTypes: basicTypes, basicTypes: filteredBasicTypes, importedTypes, + workspaceTypes, operators: filteredOperators, typeBrowserTypes: filteredTypeBrowserTypes, onSearchTypeHelper: handleSearchTypeHelper, @@ -299,6 +308,7 @@ export const FormTypeEditor = (props: FormTypeEditorProps) => { referenceTypes: basicTypes, basicTypes: filteredBasicTypes, importedTypes, + workspaceTypes, operators: filteredOperators, typeBrowserTypes: filteredTypeBrowserTypes, onSearchTypeHelper: handleSearchTypeHelper, diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/utils.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/utils.tsx index 430762d586..f61dffede5 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/utils.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeEditor/utils.tsx @@ -16,7 +16,7 @@ * under the License. */ -import { AvailableNode, Category, functionKinds, Item, VisibleTypeItem, GeneralPayloadContext, Protocol } from '@wso2/ballerina-core'; +import { AvailableNode, Category, functionKinds, Item, VisibleTypeItem, GeneralPayloadContext, Protocol, FunctionKind } from '@wso2/ballerina-core'; import type { TypeHelperCategory, TypeHelperItem, TypeHelperOperator } from '@wso2/type-editor'; import { COMPLETION_ITEM_KIND, convertCompletionItemKind } from '@wso2/ui-toolkit'; import { getFunctionItemKind, isDMSupportedType } from '../../../utils/bi'; @@ -57,6 +57,12 @@ export const getTypes = (types: VisibleTypeItem[], filterDMTypes?: boolean, payl continue; } + // Skip User-Defined types since they will again fetched + // from search API align with other integrations types + if (type.labelDetails?.detail === "User-Defined") { + continue; + } + if (!categoryRecord[type.labelDetails.detail]) { categoryRecord[type.labelDetails.detail] = []; } @@ -111,7 +117,43 @@ const isCategoryType = (item: Item): item is Category => { return !(item as AvailableNode)?.codedata; } -export const getImportedTypes = (types: Category[]) => { +export const transformTypesFromSearchToHelperCategory = (types: Category[]): TypeHelperCategory[] => { + return types.map((category) => { + const items: TypeHelperItem[] = []; + const subCategories: TypeHelperCategory[] = []; + const categoryKind = getFunctionItemKind(category.metadata.label); + for (const categoryItem of category.items) { + if (isCategoryType(categoryItem)) { + subCategories.push({ + category: categoryItem.metadata.label, + items: categoryItem.items.map((item) => ({ + name: item.metadata.label, + insertText: item.metadata.label, + type: COMPLETION_ITEM_KIND.TypeParameter, + codedata: (item as AvailableNode).codedata, + kind: categoryKind + })) + }); + } else { + items.push({ + name: categoryItem.metadata.label, + insertText: categoryItem.metadata.label, + type: COMPLETION_ITEM_KIND.TypeParameter, + codedata: categoryItem.codedata, + kind: categoryKind + }); + } + } + + return { + category: category.metadata.label, + subCategory: subCategories, + items: items + } + }); +} + +export const getFilteredTypesByKind = (types: Category[], kind: FunctionKind) => { const categories: TypeHelperCategory[] = []; for (const category of types) { @@ -120,7 +162,7 @@ export const getImportedTypes = (types: Category[]) => { } const categoryKind = getFunctionItemKind(category.metadata.label); - if (categoryKind !== functionKinds.IMPORTED) { + if (categoryKind !== kind) { continue; } @@ -132,6 +174,17 @@ export const getImportedTypes = (types: Category[]) => { continue; } + let subCategoryKind = categoryKind; + if (kind === functionKinds.CURRENT) { + // HACK: If item is under the current workspace category, + // but it is not in the current integration, then + // treat is as an imported item. + subCategoryKind = getFunctionItemKind(categoryItem.metadata.label); + if (subCategoryKind !== functionKinds.CURRENT) { + subCategoryKind = functionKinds.IMPORTED + } + } + subCategories.push({ category: categoryItem.metadata.label, items: categoryItem.items.map((item) => ({ @@ -139,7 +192,7 @@ export const getImportedTypes = (types: Category[]) => { insertText: item.metadata.label, type: COMPLETION_ITEM_KIND.TypeParameter, codedata: (item as AvailableNode).codedata, - kind: categoryKind + kind: subCategoryKind })) }); } else { diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeHelper/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeHelper/index.tsx index 2f4ace2040..42a8cf26fc 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeHelper/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/TypeHelper/index.tsx @@ -22,7 +22,7 @@ import { RefObject, useRef } from 'react'; import { debounce } from 'lodash'; import { useCallback, useState } from 'react'; -import { CodeData, InputType, LineRange, getPrimaryInputType } from '@wso2/ballerina-core'; +import { CodeData, InputType, LineRange, functionKinds, getPrimaryInputType } from '@wso2/ballerina-core'; import { TypeHelperCategory, TypeHelperComponent, @@ -30,7 +30,7 @@ import { TypeHelperOperator } from '@wso2/type-editor'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; -import { filterOperators, filterTypes, getImportedTypes, getTypeBrowserTypes, getTypes } from '../TypeEditor/utils'; +import { filterOperators, filterTypes, getFilteredTypesByKind, getTypeBrowserTypes, getTypes } from '../TypeEditor/utils'; import { TYPE_HELPER_OPERATORS } from '../TypeEditor/constants'; import { useMutation } from "@tanstack/react-query"; import { createPortal } from "react-dom"; @@ -60,7 +60,7 @@ type TypeHelperProps = { typeHelperState: boolean; onChange: (newType: string, newCursorPosition: number) => void; changeTypeHelperState: (isOpen: boolean) => void; - updateImports: (key: string, imports: {[key: string]: string}, codedata?: CodeData) => void; + updateImports: (key: string, imports: { [key: string]: string }, codedata?: CodeData) => void; onTypeCreate: (typeName: string) => void; onCloseCompletions?: () => void; typeHelperContext?: TypeHelperContext; @@ -83,7 +83,7 @@ const TypeHelperEl = (props: TypeHelperProps) => { onTypeCreate, onCloseCompletions, exprRef, - typeHelperContext + typeHelperContext } = props; const { rpcClient } = useRpcContext(); @@ -93,6 +93,7 @@ const TypeHelperEl = (props: TypeHelperProps) => { const [basicTypes, setBasicTypes] = useState([]); const [importedTypes, setImportedTypes] = useState([]); + const [workspaceTypes, setWorkspaceTypes] = useState([]); const [filteredBasicTypes, setFilteredBasicTypes] = useState([]); const [filteredOperators, setFilteredOperators] = useState([]); const [filteredTypeBrowserTypes, setFilteredTypeBrowserTypes] = useState([]); @@ -108,7 +109,8 @@ const TypeHelperEl = (props: TypeHelperProps) => { if (isType && !fetchedInitialTypes.current) { try { const isFetchingTypesForDM = primaryBallerinaType === "json"; - const types = (typeHelperContext === TypeHelperContext.GRAPHQL_FIELD_TYPE || typeHelperContext === TypeHelperContext.GRAPHQL_INPUT_TYPE) + const isGraphQLContext = typeHelperContext === TypeHelperContext.GRAPHQL_FIELD_TYPE || typeHelperContext === TypeHelperContext.GRAPHQL_INPUT_TYPE; + const types = isGraphQLContext ? await rpcClient.getServiceDesignerRpcClient().getResourceReturnTypes({ filePath: filePath, context: typeHelperContext, @@ -127,19 +129,23 @@ const TypeHelperEl = (props: TypeHelperProps) => { setFilteredBasicTypes(basicTypes); fetchedInitialTypes.current = true; - if (typeHelperContext === TypeHelperContext.HTTP_STATUS_CODE) { - const searchResponse = await rpcClient.getBIDiagramRpcClient().search({ - filePath: filePath, - position: targetLineRange, - queryMap: { - q: '', - offset: 0, - limit: 1000 - }, - searchKind: 'TYPE' - }); - const importedTypes = getImportedTypes(searchResponse.categories); + const searchResponse = await rpcClient.getBIDiagramRpcClient().search({ + filePath: filePath, + position: targetLineRange, + queryMap: { + q: '', + offset: 0, + limit: 1000 + }, + searchKind: 'TYPE' + }); + + const workspaceTypes = getFilteredTypesByKind(searchResponse.categories, functionKinds.CURRENT); + setWorkspaceTypes(workspaceTypes); + // Additionally fetch imported types + if (!isGraphQLContext) { + const importedTypes = getFilteredTypesByKind(searchResponse.categories, functionKinds.IMPORTED); setImportedTypes(importedTypes); } @@ -163,8 +169,10 @@ const TypeHelperEl = (props: TypeHelperProps) => { searchKind: 'TYPE' }); - const importedTypes = getImportedTypes(response.categories); + const importedTypes = getFilteredTypesByKind(response.categories, functionKinds.IMPORTED); + const workspaceTypes = getFilteredTypesByKind(response.categories, functionKinds.CURRENT); setImportedTypes(importedTypes); + setWorkspaceTypes(workspaceTypes); } catch (error) { console.error(error); } finally { @@ -200,7 +208,7 @@ const TypeHelperEl = (props: TypeHelperProps) => { limit: 1000 }, searchKind: 'TYPE' - }) + }) .then((response) => { setFilteredTypeBrowserTypes(getTypeBrowserTypes(response.categories)); }) @@ -220,8 +228,8 @@ const TypeHelperEl = (props: TypeHelperProps) => { [debouncedSearchTypeBrowser] ); - const { mutateAsync: addFunction, isPending: isAddingType } = useMutation({ - mutationFn: (item: TypeHelperItem) => + const { mutateAsync: addFunction, isPending: isAddingType } = useMutation({ + mutationFn: (item: TypeHelperItem) => rpcClient.getBIDiagramRpcClient().addFunction({ filePath: filePath, codedata: item.codedata, @@ -264,6 +272,7 @@ const TypeHelperEl = (props: TypeHelperProps) => { referenceTypes={basicTypes} basicTypes={filteredBasicTypes} importedTypes={importedTypes} + workspaceTypes={workspaceTypes} operators={filteredOperators} typeBrowserTypes={filteredTypeBrowserTypes} typeBrowserRef={typeBrowserRef} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/WorkspaceOverview/PackageListView.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/WorkspaceOverview/PackageListView.tsx index d6fc86c65d..5d9fe32b82 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/WorkspaceOverview/PackageListView.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/WorkspaceOverview/PackageListView.tsx @@ -18,7 +18,7 @@ import React, { useMemo } from 'react'; import styled from '@emotion/styled'; -import { Codicon, Icon, Typography } from '@wso2/ui-toolkit'; +import { Codicon, Icon, Tooltip, Typography } from '@wso2/ui-toolkit'; import { EVENT_TYPE, MACHINE_VIEW, ProjectStructureResponse, SCOPE } from '@wso2/ballerina-core'; import { useRpcContext } from '@wso2/ballerina-rpc-client'; import { getIntegrationTypes } from '../PackageOverview/utils'; @@ -136,6 +136,13 @@ const ChipContainer = styled.div` min-height: 28px; `; +const MetaRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +`; + const Chip = styled.div<{ color: string }>` display: inline-flex; align-items: center; @@ -151,8 +158,43 @@ const Chip = styled.div<{ color: string }>` white-space: nowrap; `; +const ICPBadge = styled.div` + --icp-badge-green: #00b894; + display: inline-flex; + align-items: center; + gap: 4px; + height: 20px; + padding: 0 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + line-height: 1; + letter-spacing: 0.03em; + color: var(--icp-badge-green); + background: color-mix(in srgb, var(--icp-badge-green) 14%, transparent); + border: 1px solid color-mix(in srgb, var(--icp-badge-green) 55%, transparent); + white-space: nowrap; +`; + +const ICPBadgeIcon = styled.span` + display: inline-flex; + align-items: center; + justify-content: center; + height: 100%; + line-height: 1; +`; + +const ICPBadgeText = styled.span` + display: inline-flex; + align-items: center; + height: 100%; + line-height: 1; +`; + export interface PackageListViewProps { workspaceStructure: ProjectStructureResponse; + icpStatusByProjectPath?: Record; + showICPBadge?: boolean; } const getTypeColor = (type: SCOPE): string => { @@ -162,6 +204,7 @@ const getTypeColor = (type: SCOPE): string => { [SCOPE.EVENT_INTEGRATION]: 'var(--vscode-charts-orange)', [SCOPE.FILE_INTEGRATION]: 'var(--vscode-charts-purple)', [SCOPE.AI_AGENT]: 'var(--vscode-charts-red)', + [SCOPE.LIBRARY]: 'var(--vscode-charts-yellow)', [SCOPE.ANY]: 'var(--vscode-charts-gray)' }; return colors[type]; @@ -174,6 +217,7 @@ const getTypeIcon = (type: SCOPE): { name: string; source: 'icon' | 'codicon' } [SCOPE.EVENT_INTEGRATION]: { name: 'Event', source: 'icon' }, [SCOPE.FILE_INTEGRATION]: { name: 'file', source: 'icon' }, [SCOPE.AI_AGENT]: { name: 'bi-ai-agent', source: 'icon' }, + [SCOPE.LIBRARY]: { name: 'package', source: 'codicon' }, [SCOPE.ANY]: { name: 'project', source: 'codicon' } }; return icons[type]; @@ -186,6 +230,7 @@ const getTypeLabel = (type: SCOPE): string => { [SCOPE.EVENT_INTEGRATION]: 'Event Integration', [SCOPE.FILE_INTEGRATION]: 'File Integration', [SCOPE.AI_AGENT]: 'AI Agent', + [SCOPE.LIBRARY]: 'Library', [SCOPE.ANY]: '' }; return labels[type]; @@ -204,10 +249,8 @@ const renderIcon = (iconConfig: { name: string; source: 'icon' | 'codicon' }) => ); }; -const renderPackageIcon = (types: SCOPE[], isLibrary: boolean) => { - if (isLibrary) { - return renderIcon({ name: 'package', source: 'codicon' }); - } else if (types.length > 0) { +const renderPackageIcon = (types: SCOPE[]) => { + if (types.length > 0) { const iconConfig = getTypeIcon(types[0]); return renderIcon(iconConfig); } @@ -218,6 +261,8 @@ const renderPackageIcon = (types: SCOPE[], isLibrary: boolean) => { export function PackageListView(props: PackageListViewProps) { const { rpcClient } = useRpcContext(); const workspaceStructure = props.workspaceStructure; + const icpStatusByProjectPath = props.icpStatusByProjectPath ?? {}; + const showICPBadge = props.showICPBadge ?? false; const packages = useMemo(() => { return workspaceStructure.projects.map((project) => { @@ -225,7 +270,7 @@ export function PackageListView(props: PackageListViewProps) { id: project.projectName, name: project.projectTitle, projectPath: project.projectPath, - isLibrary: project?.isLibrary ?? false, + isLibrary: project.isLibrary ?? false, types: getIntegrationTypes(project) } }); @@ -262,7 +307,7 @@ export function PackageListView(props: PackageListViewProps) { - {renderPackageIcon(pkg.types, pkg.isLibrary)} + {renderPackageIcon(pkg.types)} {pkg.name} @@ -277,18 +322,25 @@ export function PackageListView(props: PackageListViewProps) { - - {pkg.types.length > 0 && pkg.types.map((type) => ( - - {type !== SCOPE.ANY ? getTypeLabel(type) : ''} - - ))} - {pkg.isLibrary && ( - - Library - + + + {pkg.types.length > 0 && pkg.types.map((type) => ( + + {type !== SCOPE.ANY ? getTypeLabel(type) : ''} + + ))} + + {showICPBadge && !pkg.isLibrary && icpStatusByProjectPath[pkg.projectPath] && ( + + + + + + ICP + + )} - + ))} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/WorkspaceOverview/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/WorkspaceOverview/index.tsx index 17c22ce150..4116d39167 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/WorkspaceOverview/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/WorkspaceOverview/index.tsx @@ -16,14 +16,18 @@ * under the License. */ -import React, { useEffect, useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { ProjectStructureResponse, SHARED_COMMANDS, - BI_COMMANDS + BI_COMMANDS, + BuildMode, + WorkspaceDevantMetadata } from "@wso2/ballerina-core"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; -import { Typography, Codicon, ProgressRing, Button, Icon } from "@wso2/ui-toolkit"; +import { useQuery } from "@tanstack/react-query"; +import { IOpenInConsoleCmdParams, CommandIds as PlatformExtCommandIds } from "@wso2/wso2-platform-core"; +import { Typography, Codicon, ProgressRing, Button, Icon, Divider } from "@wso2/ui-toolkit"; import styled from "@emotion/styled"; import { ThemeColors } from "@wso2/ui-toolkit"; import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; @@ -31,6 +35,7 @@ import ReactMarkdown from "react-markdown"; import { AlertBoxWithClose } from "../../AIPanel/AlertBoxWithClose"; import { UndoRedoGroup } from "../../../components/UndoRedoGroup"; import { PackageListView } from "./PackageListView"; +import { getWorkspaceProjectScopes } from "../PackageOverview/utils"; const SpinnerContainer = styled.div` display: flex; @@ -83,14 +88,20 @@ const HeaderControls = styled.div` align-items: center; `; -const MainContent = styled.div` +const MainContent = styled.div<{ hasDeployment?: boolean }>` flex: 1; overflow-y: auto; overflow-x: hidden; padding: 24px; - display: flex; - flex-direction: column; - gap: 24px; + display: ${(props: { hasDeployment?: boolean }) => props.hasDeployment ? 'grid' : 'flex'}; + ${(props: { hasDeployment?: boolean }) => props.hasDeployment && ` + grid-template-columns: 3fr 1fr; + gap: 24px; + `} + ${(props: { hasDeployment?: boolean }) => !props.hasDeployment && ` + flex-direction: column; + gap: 24px; + `} `; const Section = styled.section` @@ -208,13 +219,404 @@ const ProjectSubtitle = styled.h2` } `; +const LeftContent = styled.div` + display: flex; + flex-direction: column; + gap: 24px; + min-height: 0; +`; + +const SidePanel = styled.div` + padding: 0px 10px 10px 10px; +`; + +const Title = styled(Typography)` + margin: 8px 0; +`; + +interface DeploymentOptionContainerProps { + isExpanded: boolean; +} + +const DeploymentOptionContainer = styled.div` + cursor: pointer; + border: ${(props: DeploymentOptionContainerProps) => props.isExpanded ? '1px solid var(--vscode-welcomePage-tileBorder)' : 'none'}; + background: ${(props: DeploymentOptionContainerProps) => props.isExpanded ? 'var(--vscode-welcomePage-tileBackground)' : 'transparent'}; + border-radius: 6px; + display: flex; + overflow: hidden; + width: 100%; + padding: 10px; + flex-direction: column; + margin-bottom: 8px; + + &:hover { + background: var(--vscode-welcomePage-tileHoverBackground); + } +`; + +const DeploymentHeader = styled.div` + display: flex; + align-items: center; + gap: 8px; + h3 { + font-size: 13px; + font-weight: 600; + margin: 0; + } +`; + +interface DeploymentBodyProps { + isExpanded: boolean; +} + +const DeploymentBody = styled.div` + max-height: ${(props: DeploymentBodyProps) => props.isExpanded ? '200px' : '0'}; + overflow: hidden; + transition: max-height 0.3s ease-in-out; + margin-top: ${(props: DeploymentBodyProps) => props.isExpanded ? '8px' : '0'}; +`; + +interface DeploymentOptionProps { + title: string; + description: string; + buttonText: string; + isExpanded: boolean; + onToggle: () => void; + onDeploy: () => void; + learnMoreLink?: string; + hasDeployableIntegration?: boolean; + secondaryAction?: { + description: string; + buttonText: string; + onClick: () => void; + }; +} + +function DeploymentOption({ + title, + description, + buttonText, + isExpanded, + onToggle, + onDeploy, + learnMoreLink, + hasDeployableIntegration, + secondaryAction +}: DeploymentOptionProps) { + const { rpcClient } = useRpcContext(); + + const openLearnMoreURL = () => { + rpcClient.getCommonRpcClient().openExternalUrl({ + url: learnMoreLink + }) + }; + + return ( + + + {isExpanded ? ( + + ) : ( + + )} +

{title}

+
+ +

+ {description} + {learnMoreLink && ( + Learn more + )} +

+ + {secondaryAction && ( + <> +

{secondaryAction.description}

+ + + )} +
+
+ ); +} + +interface DeploymentOptionsProps { + handleDockerBuild: () => void; + handleJarBuild: () => void; + handleDeploy: () => Promise; + goToDevant: () => void; + devantMetadata: WorkspaceDevantMetadata | undefined; + hasDeployableIntegration: boolean; + hasUndeployedIntegrations: boolean; + deployableProjectPaths: Set; +} + +function DeploymentOptions({ + handleDockerBuild, + handleJarBuild, + handleDeploy, + goToDevant, + devantMetadata, + hasDeployableIntegration, + hasUndeployedIntegrations, + deployableProjectPaths +}: DeploymentOptionsProps) { + const [expandedOptions, setExpandedOptions] = useState>(new Set(['cloud'])); + const { rpcClient } = useRpcContext(); + + const toggleOption = (option: string) => { + setExpandedOptions(prev => { + const newSet = new Set(prev); + if (newSet.has(option)) { + newSet.delete(option); + } else { + newSet.add(option); + } + return newSet; + }); + }; + + // Calculate deployment states + const deployedProjects = devantMetadata?.projectsMetadata?.filter(p => p.hasComponent) || []; + const undeployedProjects = devantMetadata?.projectsMetadata?.filter(p => !p.hasComponent) || []; + const deployedWithChanges = deployedProjects.filter(p => p.hasLocalChanges); + + const hasDeployedProjects = deployedProjects.length > 0; + const hasUndeployedProjects = undeployedProjects.length > 0; + const hasDeployedWithChanges = deployedWithChanges.length > 0; + + // Determine title, description, button text, and whether deployment is allowed + let title = "Deploy to Devant"; + let description = "Deploy your workspace integrations to the cloud using Devant by WSO2."; + let buttonText = "Deploy"; + let primaryAction: () => void | Promise = handleDeploy; + let secondaryAction = undefined; + let isDeploymentDisabled = false; + let disabledTooltip = ""; + + if (hasDeployedProjects && !hasUndeployedProjects) { + // All projects are deployed - disable deployment button + title = "Deployed in Devant"; + description = "All workspace integrations are deployed in Devant."; + buttonText = "View in Devant"; + primaryAction = goToDevant; + isDeploymentDisabled = false; // View action is always enabled + + if (hasDeployedWithChanges) { + secondaryAction = { + description: "To redeploy in Devant, please commit and push your changes.", + buttonText: "Open Source Control", + onClick: () => rpcClient.getCommonRpcClient().executeCommand({ commands: ["workbench.scm.focus"] }) + }; + } + } else if (hasDeployedProjects && hasUndeployedProjects) { + // Mixed state: some deployed, some not - show clear message about remaining + title = "Partially Deployed in Devant"; + + // Separate deployable and non-deployable undeployed projects + const deployableUndeployed = undeployedProjects.filter(p => deployableProjectPaths.has(p.projectPath)); + const nonDeployableUndeployed = undeployedProjects.filter(p => !deployableProjectPaths.has(p.projectPath)); + + const deployableNames = deployableUndeployed.map(p => p.projectName).filter(Boolean).join(", "); + const nonDeployableNames = nonDeployableUndeployed.map(p => p.projectName).filter(Boolean).join(", "); + + if (hasUndeployedIntegrations) { + // There are undeployed projects that CAN be deployed + const baseMessage = deployableUndeployed.length === 1 ? "You have an undeployed integration" : "You have undeployed integrations"; + description = `${baseMessage}: ${deployableNames || deployableUndeployed.length + " integration(s)"}`; + buttonText = "Deploy Remaining"; + } else { + // There are undeployed projects but they CANNOT be deployed (no entry point found) + description = `Some integration(s) (${nonDeployableNames || nonDeployableUndeployed.length + " integration(s)"}) cannot be deployed. No entry point found within these integration(s).`; + buttonText = "Deploy Remaining"; + } + + primaryAction = handleDeploy; + isDeploymentDisabled = !hasUndeployedIntegrations; + disabledTooltip = hasUndeployedIntegrations ? "" : "No entry point found in the remaining integration(s)"; + + if (hasDeployedWithChanges) { + const baseMessage = deployedWithChanges.length === 1 + ? `A deployed integration has uncommitted changes (${deployedWithChanges[0].projectName})` + : `Some deployed integrations have uncommitted changes (${deployedWithChanges.map(p => p.projectName).filter(Boolean).join(", ")})`; + secondaryAction = { + description: `${baseMessage}. Commit and push to redeploy.`, + buttonText: "Open Source Control", + onClick: () => rpcClient.getCommonRpcClient().executeCommand({ commands: ["workbench.scm.focus"] }) + }; + } + } else { + // No deployments yet + isDeploymentDisabled = !hasDeployableIntegration; + disabledTooltip = hasDeployableIntegration ? "" : "No deployable integration(s) found"; + } + + return ( + <> +
+ Deployment Options + + toggleOption("cloud")} + onDeploy={primaryAction} + learnMoreLink={"https://wso2.com/devant/docs"} + hasDeployableIntegration={!isDeploymentDisabled} + secondaryAction={secondaryAction} + /> + + toggleOption('docker')} + onDeploy={handleDockerBuild} + hasDeployableIntegration={hasDeployableIntegration} + /> + + toggleOption('vm')} + onDeploy={handleJarBuild} + hasDeployableIntegration={hasDeployableIntegration} + /> +
+ + ); +} + +interface IntegrationControlPlaneProps { + icpState: "all" | "partial" | "none"; + enabledCount: number; + totalCount: number; + onEnableAll: () => void; + onDisableAll: () => void; + onEnableRemaining: () => void; +} + +function IntegrationControlPlane({ + icpState, + enabledCount, + totalCount, + onEnableAll, + onDisableAll, + onEnableRemaining +}: IntegrationControlPlaneProps) { + const { rpcClient } = useRpcContext(); + + const openLearnMoreURL = () => { + rpcClient.getCommonRpcClient().openExternalUrl({ + url: "https://wso2.com/integrator/integration-control-plane/" + }) + }; + + return ( +
+ Integration Control Plane +

+ {"Monitor the deployment runtime using WSO2 Integration Control Plane."} + Learn More +

+ + {totalCount > 0 ? `${enabledCount}/${totalCount} packages are ICP-enabled` : "No ICP-eligible packages found"} + + {icpState === "all" && ( + + )} + {icpState === "none" && ( + + )} + {icpState === "partial" && ( + + )} +
+ ); +} + export function WorkspaceOverview() { const { rpcClient } = useRpcContext(); const [readmeContent, setReadmeContent] = React.useState(""); const [workspaceStructure, setWorkspaceStructure] = React.useState(); + const [icpStatusByProjectPath, setIcpStatusByProjectPath] = React.useState>({}); const [showAlert, setShowAlert] = React.useState(false); + const { data: devantMetadata } = useQuery({ + queryKey: ["workspace-devant-metadata"], + queryFn: () => rpcClient.getBIDiagramRpcClient().getWorkspaceDevantMetadata(), + refetchInterval: 5000 + }); + + const getICPProjectPaths = (projects: ProjectStructureResponse["projects"]) => { + return projects + .filter((project) => !(project?.isLibrary ?? false)) + .map((project) => project.projectPath); + }; + + const syncWorkspaceICPStatus = async (projectPaths: string[]) => { + if (projectPaths.length === 0) { + setIcpStatusByProjectPath({}); + return; + } + + try { + const icpStatus = await Promise.all( + projectPaths.map((projectPath) => + rpcClient.getICPRpcClient().isIcpEnabled({ projectPath }) + ) + ); + const nextStatusMap = projectPaths.reduce>((acc, projectPath, index) => { + acc[projectPath] = Boolean(icpStatus[index]?.enabled); + return acc; + }, {}); + setIcpStatusByProjectPath(nextStatusMap); + } catch (error) { + console.error("Failed to sync ICP status:", error); + } + }; + const fetchContext = () => { rpcClient .getBIDiagramRpcClient() @@ -235,6 +637,8 @@ export function WorkspaceOverview() { .then((res) => { setReadmeContent(res.content); }); + + syncWorkspaceICPStatus(getICPProjectPaths(res.projects)); }); }; @@ -255,6 +659,51 @@ export function WorkspaceOverview() { return workspaceStructure?.projects.length === 0; }, [workspaceStructure]); + const hasStandardIntegrations = useMemo(() => { + return (workspaceStructure?.projects ?? []).some((project) => !(project?.isLibrary ?? false)); + }, [workspaceStructure?.projects]); + + const icpProjectPaths = useMemo(() => { + return workspaceStructure ? getICPProjectPaths(workspaceStructure.projects) : []; + }, [workspaceStructure]); + + const icpEnabledCount = useMemo(() => { + return icpProjectPaths.filter((projectPath) => Boolean(icpStatusByProjectPath[projectPath])).length; + }, [icpProjectPaths, icpStatusByProjectPath]); + + const icpState = useMemo<"all" | "partial" | "none">(() => { + if (icpProjectPaths.length === 0 || icpEnabledCount === 0) { + return "none"; + } + if (icpEnabledCount === icpProjectPaths.length) { + return "all"; + } + return "partial"; + }, [icpProjectPaths, icpEnabledCount]); + + const projectScopes = useMemo(() => { + return getWorkspaceProjectScopes(workspaceStructure); + }, [workspaceStructure]); + + // Calculate which projects need deployment + const undeployedProjectScopes = useMemo(() => { + if (!devantMetadata?.projectsMetadata || !workspaceStructure) { + return projectScopes; + } + + const deployedPaths = new Set( + devantMetadata.projectsMetadata + .filter(p => p.hasComponent) + .map(p => p.projectPath) + ); + + return projectScopes.filter(scope => !deployedPaths.has(scope.projectPath)); + }, [projectScopes, devantMetadata, workspaceStructure]); + + const deployableProjectPaths = useMemo(() => { + return new Set(projectScopes.map(scope => scope.projectPath)); + }, [projectScopes]); + if (!workspaceStructure) { return ( @@ -303,6 +752,84 @@ export function WorkspaceOverview() { return resp; } + const handleDeploy = async () => { + // Only deploy undeployed projects + await rpcClient.getBIDiagramRpcClient().deployWorkspace({ + projectScopes: undeployedProjectScopes, + rootDirectory: workspaceStructure?.workspacePath || '' + }); + }; + + const handleDockerBuild = () => { + rpcClient.getBIDiagramRpcClient().buildProject(BuildMode.DOCKER); + }; + + const handleJarBuild = () => { + rpcClient.getBIDiagramRpcClient().buildProject(BuildMode.JAR); + }; + + const updateICPForProjectPaths = async (projectPaths: string[], enableICP: boolean) => { + if (projectPaths.length === 0) { + return; + } + + for (const projectPath of projectPaths) { + try { + if (enableICP) { + await rpcClient.getICPRpcClient().addICP({ projectPath }); + } else { + await rpcClient.getICPRpcClient().disableICP({ projectPath }); + } + } catch (error) { + console.error("Failed to update ICP for project:", projectPath, error); + } + } + }; + + const handleEnableAllICP = async () => { + await updateICPForProjectPaths(icpProjectPaths, true); + await syncWorkspaceICPStatus(icpProjectPaths); + }; + + const handleDisableAllICP = async () => { + await updateICPForProjectPaths(icpProjectPaths, false); + await syncWorkspaceICPStatus(icpProjectPaths); + }; + + const handleEnableRemainingICP = async () => { + const remainingProjectPaths = icpProjectPaths.filter((projectPath) => !icpStatusByProjectPath[projectPath]); + await updateICPForProjectPaths(remainingProjectPaths, true); + await syncWorkspaceICPStatus(icpProjectPaths); + }; + + const goToDevant = () => { + // For workspace, open the Devant console at the project level + // If there are deployed projects, open the first one + if (devantMetadata?.projectsMetadata && devantMetadata.projectsMetadata.length > 0) { + const firstDeployedProject = devantMetadata.projectsMetadata.find(p => p.hasComponent); + if (firstDeployedProject) { + rpcClient.getCommonRpcClient().executeCommand({ + commands: [ + PlatformExtCommandIds.OpenInConsole, + { + extName: "Devant", + componentFsPath: firstDeployedProject.projectPath, + newComponentParams: { buildPackLang: "ballerina" } + } as IOpenInConsoleCmdParams + ] + }); + return; + } + } + // Fallback: open console without specific component + rpcClient.getCommonRpcClient().executeCommand({ + commands: [PlatformExtCommandIds.OpenInConsole, { + extName: "Devant", + newComponentParams: { buildPackLang: "ballerina" } + } as IOpenInConsoleCmdParams] + }); + }; + return ( @@ -319,96 +846,125 @@ export function WorkspaceOverview() { - - {showAlert && ( - handleSettings()} - btn1Id="settings" - - btn2Title="Close" - btn2IconName="close" - btn2OnClick={() => handleClose()} - btn2Id="Close" - /> - )} - -
- - - Integrations - {/* TODO: Add generate with AI button once AI is implemented (https://github.com/wso2/product-ballerina-integrator/issues/1899) */} - {/* {!isEmptyWorkspace && ( + + + {showAlert && ( + handleSettings()} + btn1Id="settings" + + btn2Title="Close" + btn2IconName="close" + btn2OnClick={() => handleClose()} + btn2Id="Close" + /> + )} + +
+ + + Integrations + {/* TODO: Add generate with AI button once AI is implemented (https://github.com/wso2/product-ballerina-integrator/issues/1899) */} + {/* {!isEmptyWorkspace && ( + + + + )} */} + + {isEmptyWorkspace ? ( + + + Your workspace is empty + + + Start by adding integrations to your workspace + + + + {/* TODO: Add generate with AI button once AI is implemented (https://github.com/wso2/product-ballerina-integrator/issues/1899) */} + {/* */} + + + ) : ( + + )} + +
+ +
+ + + README - - - )} */} - - {isEmptyWorkspace ? ( - - - Your workspace is empty - - - Start by adding integrations to your workspace - - - {/* TODO: Add generate with AI button once AI is implemented (https://github.com/wso2/product-ballerina-integrator/issues/1899) */} - {/* */} - - - ) : ( - - )} - -
- -
- - - README - - {/* TODO: Add generate with AI button once AI is implemented (https://github.com/wso2/product-ballerina-integrator/issues/1899) */} - {/* {readmeContent && isEmptyWorkspace && ( - + )} */} + - )} */} - - - - {readmeContent ? ( - - {readmeContent} - - ) : ( - - - Document your workspace and integrations - - Add a README - - )} - -
+ +
+ {readmeContent ? ( + + {readmeContent} + + ) : ( + + + Document your workspace and integrations + + Add a README + + )} +
+
+ + {hasStandardIntegrations && ( + + 0} + hasUndeployedIntegrations={undeployedProjectScopes.length > 0} + deployableProjectPaths={deployableProjectPaths} + /> + + + + )}
); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx index f5946375f9..1add954b47 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx @@ -46,7 +46,9 @@ import { IORoot, IntermediateClauseType, TriggerKind, - TypeKind + TypeKind, + Type, + Imports } from "@wso2/ballerina-core"; import { CompletionItem, ProgressIndicator } from "@wso2/ui-toolkit"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; @@ -60,6 +62,7 @@ import { calculateExpressionOffsets, convertBalCompletion, updateLineRange } fro import { createAddSubMappingRequest } from "./utils"; import { FunctionForm } from "../BI/FunctionForm"; import { UndoRedoGroup } from "../../components/UndoRedoGroup"; +import { EntryPointTypeCreator } from "../../components/EntryPointTypeCreator"; // Types for model comparison interface ModelSignature { @@ -97,6 +100,11 @@ export function DataMapperView(props: DataMapperViewProps) { const expressionOffsetRef = useRef(0); // To track the expression offset on adding import statements const [isUpdatingSource, setIsUpdatingSource] = useState(false); + /* Type Editor related */ + const [isTypeEditorOpen, setIsTypeEditorOpen] = useState(false); + const onTypeCreateRef = useRef<(type: Type | string, imports?: Imports) => void>(() => {}); + const initialTypeNameRef = useRef(""); + // Keep track of previous inputs/outputs and sub mappings for comparison const prevSignatureRef = useRef(null); @@ -105,7 +113,8 @@ export function DataMapperView(props: DataMapperViewProps) { model, isFetching, isError, - refreshDMModel + refreshDMModel, + requestRefreshDMModel } = useDataMapperModel(filePath, viewState, position); const prevPositionRef = useRef(position); @@ -593,10 +602,11 @@ export function DataMapperView(props: DataMapperViewProps) { } }); - let i = 2; + let i = 1; let uniqueName = name; + const separator = /\d$/.test(name) ? "_" : ""; while (completions.some(c => c.insertText === uniqueName)) { - uniqueName = name + (i++); + uniqueName = name + separator + (i++); } return uniqueName; @@ -616,6 +626,40 @@ export function DataMapperView(props: DataMapperViewProps) { } }; + const createConvertedVariable = async (variableName: string, isInput: boolean, typeName?: string, parentTypeName?: string) => { + const initialTypeName = typeName || variableName.charAt(0).toUpperCase() + variableName.slice(1); + initialTypeNameRef.current = await genUniqueName(initialTypeName, viewState.viewId); + + onTypeCreateRef.current = (type: Type | string, imports?: Imports) => { + const newTypeName = typeof type === 'string' ? type : (type as Type).name; + requestRefreshDMModel(); + rpcClient + .getDataMapperRpcClient() + .createConvertedVariable({ + filePath, + codedata: { + ...viewState.codedata, + isNew: !typeName + }, + varName: name, + targetField: viewState.viewId, + subMappingName: viewState.subMappingName, + typeName: newTypeName, + isInput, + variableName, + parentTypeName, + imports + }).then(res => { + console.log(">>> [Data Mapper] createConvertedVariable response:", res); + }).catch(error => { + console.error(error); + setIsFileUpdateError(true); + }); + }; + + setIsTypeEditorOpen(true); + }; + const onDMClose = () => { onClose ? onClose() : rpcClient.getVisualizerRpcClient()?.goBack(); }; @@ -768,6 +812,7 @@ export function DataMapperView(props: DataMapperViewProps) { deleteClause={deleteClause} getClausePosition={getClausePosition} getConvertedExpression={getConvertedExpression} + createConvertedVariable={createConvertedVariable} addSubMapping={addSubMapping} deleteMapping={deleteMapping} deleteSubMapping={deleteSubMapping} @@ -789,6 +834,18 @@ export function DataMapperView(props: DataMapperViewProps) { }} /> )} + {isTypeEditorOpen && + setIsTypeEditorOpen(false)} + onTypeCreate={onTypeCreateRef.current} + initialTypeName={initialTypeNameRef.current} + modalTitle={"Define Type for converted variable"} + + modalWidth={650} + modalHeight={600} + /> + } )} diff --git a/workspaces/ballerina/ballerina-visualizer/webpack.config.js b/workspaces/ballerina/ballerina-visualizer/webpack.config.js index d558ef4b77..3dc5d8c153 100644 --- a/workspaces/ballerina/ballerina-visualizer/webpack.config.js +++ b/workspaces/ballerina/ballerina-visualizer/webpack.config.js @@ -45,7 +45,10 @@ module.exports = { enforce: "pre", test: /\.js$/, loader: "source-map-loader", - exclude: /node_modules\/parse5/, + exclude: [ + /node_modules\/parse5/, + /node_modules\/autolinker/ + ], }, { test: /\.css$/, diff --git a/workspaces/ballerina/bi-diagram/src/components/ConnectorIcon/index.tsx b/workspaces/ballerina/bi-diagram/src/components/ConnectorIcon/index.tsx index 942a1ee136..51f412696b 100644 --- a/workspaces/ballerina/bi-diagram/src/components/ConnectorIcon/index.tsx +++ b/workspaces/ballerina/bi-diagram/src/components/ConnectorIcon/index.tsx @@ -59,8 +59,9 @@ export function ConnectorIcon(props: ConnectorIconProps): React.ReactElement { return getLlmModelIcons(selectedModule); } - if (codedata && (codedata.node === "AGENT_CALL" || codedata.node === "AGENT_RUN")) { - return ; + // use custom icon for mcp + if (url?.includes("mcp")) { + return ; } // use custom icon for wso2 module @@ -68,11 +69,6 @@ export function ConnectorIcon(props: ConnectorIconProps): React.ReactElement { return ; } - // use custom icon for mcp - if (url?.includes("mcp")) { - return ; - } - // use custom icon for ai module if (url?.includes("ballerinax_ai_") || url?.includes("ballerina_ai")) { return ; @@ -82,6 +78,10 @@ export function ConnectorIcon(props: ConnectorIconProps): React.ReactElement { return setImageError(true)} style={{ ...style }} />; } + if (codedata && (codedata.node === "AGENT_CALL" || codedata.node === "AGENT_RUN")) { + return ; + } + if (fallbackIcon) { return
{fallbackIcon}
; } diff --git a/workspaces/ballerina/bi-diagram/src/components/nodes/BaseNode/BaseNodeWidget.tsx b/workspaces/ballerina/bi-diagram/src/components/nodes/BaseNode/BaseNodeWidget.tsx index 1b01c6f169..da06ce3e6b 100644 --- a/workspaces/ballerina/bi-diagram/src/components/nodes/BaseNode/BaseNodeWidget.tsx +++ b/workspaces/ballerina/bi-diagram/src/components/nodes/BaseNode/BaseNodeWidget.tsx @@ -26,7 +26,7 @@ import { NODE_PADDING, NODE_WIDTH, } from "../../../resources/constants"; -import { Button, Item, Menu, MenuItem, Popover, ThemeColors } from "@wso2/ui-toolkit"; +import { Button, Icon, Item, Menu, MenuItem, Popover, ThemeColors, Tooltip } from "@wso2/ui-toolkit"; import { MoreVertIcon } from "../../../resources"; import NodeIcon from "../../NodeIcon"; import { useDiagramContext } from "../../DiagramContext"; @@ -195,6 +195,10 @@ export function BaseNodeWidget(props: BaseNodeWidgetProps) { const isMenuOpen = Boolean(menuAnchorEl); const hasBreakpoint = model.hasBreakpoint(); const isActiveBreakpoint = model.isActiveBreakpoint(); + const canViewFunction = + model.node.codedata.node === "FUNCTION_CALL" && + model.node.codedata.org === project?.org && + Boolean(model.node.properties?.view?.value); const handleOnClick = async (event: React.MouseEvent) => { if (readOnly) { @@ -257,6 +261,13 @@ export function BaseNodeWidget(props: BaseNodeWidgetProps) { setIsHovered(false); }; + const handleOnViewFunctionClick = () => { + if (readOnly) { + return; + } + viewFunction(); + }; + const openDataMapper = async () => { if (!model.node.properties?.view?.value) { return; @@ -315,7 +326,7 @@ export function BaseNodeWidget(props: BaseNodeWidgetProps) { }); } - if (model.node.codedata.node === "FUNCTION_CALL" && model.node.codedata.org === project?.org) { + if (canViewFunction) { menuItems.splice(1, 0, { id: "viewFunction", label: "View", @@ -383,6 +394,21 @@ export function BaseNodeWidget(props: BaseNodeWidgetProps) { {hasError && } + {canViewFunction && ( + + + + + + )} /calendars/[string `abc-calender`]/events.post({\n attachments: [],\n attendees: [],\n created: \"\",\n description: \"\"\n });\n if address.houseNo == \"\" {\n while address.houseNo != \"\" {\n address.houseNo = getRandomHome();\n }\n } else if address.street == \"\" {\n match name {\n \"john\" => {\n io:println(\"name list\");\n }\n \"wick\" => {\n log:printInfo(string `${address.street} + ${name}`);\n }\n _ => {\n }\n }\n } else {\n }\n string agentRes = check aiAgent.run(string `${name} user asking following questions`);\n string sampleRes = check aiAgentResult.run(\"sample query\");\n fork {\n worker worker1 {\n do {\n lock {\n }\n } on fail error err {\n }\n }\n worker worker2 {\n }\n }\n map waitResult = wait {worker1, worker2};\n foreach string var1 in [] {\n if agentRes == \"\" {\n return;\n } else {\n break;\n }\n if var1 {\n continue;\n }\n }\n\n } on fail error e {\n log:printError(\"Error occurred\", 'error = e);\n return e;\n }\n}" + }, + "returning": false, + "diagnostics": { + "hasDiagnostics": true + }, + "flags": 0 + }, + { + "id": "38879", + "metadata": { + "label": "ErrorHandler", + "description": "Catch and handle errors" + }, + "codedata": { + "node": "ERROR_HANDLER", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 5, + "offset": 4 + }, + "endLine": { + "line": 71, + "offset": 5 + } + }, + "sourceCode": "do {\n\n string name = getName(\"a\");\n\n User user = {\n id: \"1234\",\n name: name,\n Address: {\n houseNo: \"\",\n street: \"\",\n city: \"Colombo\"\n }\n };\n name = user.name;\n Address address = transform(user);\n gcalendar:Event gcalendarEvent = check gcalendarClient->/calendars/[string `abc-calender`]/events.post({\n attachments: [],\n attendees: [],\n created: \"\",\n description: \"\"\n });\n if address.houseNo == \"\" {\n while address.houseNo != \"\" {\n address.houseNo = getRandomHome();\n }\n } else if address.street == \"\" {\n match name {\n \"john\" => {\n io:println(\"name list\");\n }\n \"wick\" => {\n log:printInfo(string `${address.street} + ${name}`);\n }\n _ => {\n }\n }\n } else {\n }\n string agentRes = check aiAgent.run(string `${name} user asking following questions`);\n string sampleRes = check aiAgentResult.run(\"sample query\");\n fork {\n worker worker1 {\n do {\n lock {\n }\n } on fail error err {\n }\n }\n worker worker2 {\n }\n }\n map waitResult = wait {worker1, worker2};\n foreach string var1 in [] {\n if agentRes == \"\" {\n return;\n } else {\n break;\n }\n if var1 {\n continue;\n }\n }\n\n } on fail error e {\n log:printError(\"Error occurred\", 'error = e);\n return e;\n }" + }, + "returning": false, + "branches": [ + { + "label": "Body", + "kind": "BLOCK", + "codedata": { + "node": "BODY", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 5, + "offset": 7 + }, + "endLine": { + "line": 68, + "offset": 5 + } + }, + "sourceCode": "{\n\n string name = getName(\"a\");\n\n User user = {\n id: \"1234\",\n name: name,\n Address: {\n houseNo: \"\",\n street: \"\",\n city: \"Colombo\"\n }\n };\n name = user.name;\n Address address = transform(user);\n gcalendar:Event gcalendarEvent = check gcalendarClient->/calendars/[string `abc-calender`]/events.post({\n attachments: [],\n attendees: [],\n created: \"\",\n description: \"\"\n });\n if address.houseNo == \"\" {\n while address.houseNo != \"\" {\n address.houseNo = getRandomHome();\n }\n } else if address.street == \"\" {\n match name {\n \"john\" => {\n io:println(\"name list\");\n }\n \"wick\" => {\n log:printInfo(string `${address.street} + ${name}`);\n }\n _ => {\n }\n }\n } else {\n }\n string agentRes = check aiAgent.run(string `${name} user asking following questions`);\n string sampleRes = check aiAgentResult.run(\"sample query\");\n fork {\n worker worker1 {\n do {\n lock {\n }\n } on fail error err {\n }\n }\n worker worker2 {\n }\n }\n map waitResult = wait {worker1, worker2};\n foreach string var1 in [] {\n if agentRes == \"\" {\n return;\n } else {\n break;\n }\n if var1 {\n continue;\n }\n }\n\n }" + }, + "repeatable": "ONE", + "children": [ + { + "id": "38971", + "metadata": { + "label": "getName", + "description": "" + }, + "codedata": { + "node": "FUNCTION_CALL", + "org": "gayanka", + "module": "test_2_3_5", + "packageName": "test_2_3_5", + "symbol": "getName", + "version": "0.1.0", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 7, + "offset": 8 + }, + "endLine": { + "line": 7, + "offset": 35 + } + }, + "sourceCode": "string name = getName(\"a\");" + }, + "returning": false, + "properties": { + "view": { + "metadata": { + "label": "View", + "description": "Function definition location" + }, + "types": [ + { + "fieldType": "VIEW", + "selected": true + } + ], + "value": { + "fileName": "automation.bal", + "startLine": { + "line": 74, + "offset": 0 + }, + "endLine": { + "line": 76, + "offset": 1 + } + }, + "optional": false, + "editable": false, + "advanced": false, + "hidden": false + }, + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Trigger error flow" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": false, + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "variable": { + "metadata": { + "label": "Result", + "description": "Name of the result variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "name", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 7, + "offset": 15 + }, + "endLine": { + "line": 7, + "offset": 19 + } + } + } + }, + "type": { + "metadata": { + "label": "Result Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "string", + "placeholder": "var", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": {} + } + }, + "diagnostics": { + "hasDiagnostics": true + }, + "flags": 0 + }, + { + "id": "41178", + "metadata": { + "label": "Declare Variable", + "description": "Create a new variable" + }, + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 9, + "offset": 8 + }, + "endLine": { + "line": 17, + "offset": 10 + } + }, + "sourceCode": "User user = {\n id: \"1234\",\n name: name,\n Address: {\n houseNo: \"\",\n street: \"\",\n city: \"Colombo\"\n }\n };" + }, + "returning": false, + "properties": { + "expression": { + "metadata": { + "label": "Expression", + "description": "Initialize with value" + }, + "types": [ + { + "fieldType": "ACTION_OR_EXPRESSION", + "selected": true + } + ], + "value": "{\n id: \"1234\",\n name: name,\n Address: {\n houseNo: \"\",\n street: \"\",\n city: \"Colombo\"\n }\n }", + "optional": true, + "editable": true, + "advanced": false, + "hidden": false + }, + "variable": { + "metadata": { + "label": "Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "user", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 9, + "offset": 13 + }, + "endLine": { + "line": 9, + "offset": 17 + } + } + } + }, + "type": { + "metadata": { + "label": "Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "User", + "placeholder": "var", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": {} + } + }, + "flags": 0 + }, + { + "id": "49873", + "metadata": { + "label": "Update Variable", + "description": "Update the value of an existing variable" + }, + "codedata": { + "node": "ASSIGN", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 18, + "offset": 8 + }, + "endLine": { + "line": 18, + "offset": 25 + } + }, + "sourceCode": "name = user.name;" + }, + "returning": false, + "properties": { + "expression": { + "metadata": { + "label": "Expression", + "description": "Update value" + }, + "types": [ + { + "fieldType": "ACTION_OR_EXPRESSION", + "selected": true + } + ], + "value": "user.name", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + }, + "variable": { + "metadata": { + "label": "Variable", + "description": "Name of the variable/field" + }, + "types": [ + { + "fieldType": "LV_EXPRESSION", + "selected": false + } + ], + "value": "name", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "flags": 0 + }, + { + "id": "50882", + "metadata": { + "label": "Map Data", + "description": "" + }, + "codedata": { + "node": "DATA_MAPPER_CALL", + "org": "gayanka", + "module": "test_2_3_5", + "packageName": "test_2_3_5", + "symbol": "transform", + "version": "0.1.0", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 19, + "offset": 8 + }, + "endLine": { + "line": 19, + "offset": 42 + } + }, + "sourceCode": "Address address = transform(user);" + }, + "returning": false, + "properties": { + "view": { + "metadata": { + "label": "View", + "description": "Function definition location" + }, + "types": [ + { + "fieldType": "VIEW", + "selected": true + } + ], + "value": { + "fileName": "data_mappings.bal", + "startLine": { + "line": 0, + "offset": 0 + }, + "endLine": { + "line": 3, + "offset": 2 + } + }, + "optional": false, + "editable": false, + "advanced": false, + "hidden": false + }, + "mt": { + "metadata": { + "label": "Mt" + }, + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "User", + "typeMembers": [ + { + "type": "User", + "packageInfo": "gayanka:test_2_3_5:0.1.0", + "packageName": "test_2_3_5", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "User", + "selected": true + } + ], + "value": "user", + "placeholder": "{id: \"\", name: \"\", Address: {houseNo: \"\", street: \"\", city: \"\"}}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED", + "originalName": "mt" + } + }, + "variable": { + "metadata": { + "label": "Result", + "description": "Name of the result variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "address", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 19, + "offset": 16 + }, + "endLine": { + "line": 19, + "offset": 23 + } + } + } + }, + "type": { + "metadata": { + "label": "Result Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "Address", + "placeholder": "var", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": {} + } + }, + "flags": 0 + }, + { + "id": "51998", + "metadata": { + "label": "post", + "description": "Creates an event.\n", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerinax_googleapis.gcalendar_4.0.1.png" + }, + "codedata": { + "node": "RESOURCE_ACTION_CALL", + "org": "ballerinax", + "module": "googleapis.gcalendar", + "packageName": "googleapis.gcalendar", + "object": "Client", + "symbol": "post", + "version": "4.0.1", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 20, + "offset": 8 + }, + "endLine": { + "line": 25, + "offset": 11 + } + }, + "sourceCode": "gcalendar:Event gcalendarEvent = check gcalendarClient->/calendars/[string `abc-calender`]/events.post({\n attachments: [],\n attendees: [],\n created: \"\",\n description: \"\"\n });", + "resourcePath": "/calendars/[calendarId]/events" + }, + "returning": false, + "properties": { + "resourcePath": { + "metadata": { + "label": "Resource Path", + "description": "Resource Path" + }, + "types": [ + { + "fieldType": "ACTION_PATH", + "selected": false + } + ], + "value": "/calendars/[calendarId]/events", + "optional": false, + "editable": false, + "advanced": false, + "hidden": true, + "codedata": { + "originalName": "/calendars/[calendarId]/events" + } + }, + "calendarId": { + "metadata": { + "label": "calendarId", + "description": "Calendar identifier. To retrieve calendar IDs call the calendarList.list method. If you want to access the primary calendar of the currently logged in user, use the \"primary\" keyword" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "value": "string `abc-calender`", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "PATH_PARAM", + "originalName": "calendarId" + } + }, + "connection": { + "metadata": { + "label": "Connection", + "description": "Connection to use" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "gcalendarClient", + "optional": false, + "editable": false, + "advanced": false, + "hidden": true + }, + "variable": { + "metadata": { + "label": "Result", + "description": "Name of the result variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "gcalendarEvent", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 20, + "offset": 24 + }, + "endLine": { + "line": 20, + "offset": 38 + } + } + } + }, + "payload": { + "metadata": { + "label": "Payload", + "description": "Data required to create an event" + }, + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "gcalendar:Event", + "typeMembers": [ + { + "type": "Event", + "packageInfo": "ballerinax:googleapis.gcalendar:4.0.1", + "packageName": "googleapis.gcalendar", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "gcalendar:Event", + "selected": false + } + ], + "value": "{\n attachments: [],\n attendees: [],\n created: \"\",\n description: \"\"\n }", + "placeholder": "{}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED", + "originalName": "payload" + } + }, + "alt": { + "metadata": { + "label": "Alt", + "description": "Data format for the response" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "\"json\"?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "alt" + }, + "defaultValue": "()" + }, + "fields": { + "metadata": { + "label": "Fields", + "description": "Selector specifying which fields to include in a partial response" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "fields" + }, + "defaultValue": "()" + }, + "key": { + "metadata": { + "label": "Key" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "key" + }, + "defaultValue": "()" + }, + "oauth_token": { + "metadata": { + "label": "Oauth Token", + "description": "OAuth 2.0 token for the current user" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "oauth_token" + }, + "defaultValue": "()" + }, + "prettyPrint": { + "metadata": { + "label": "Pretty Print", + "description": "Returns response with indentations and line breaks" + }, + "types": [ + { + "fieldType": "FLAG", + "ballerinaType": "boolean", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "prettyPrint" + }, + "defaultValue": "()" + }, + "quotaUser": { + "metadata": { + "label": "Quota User", + "description": "An opaque string that represents a user for quota purposes. Must not exceed 40 characters" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "quotaUser" + }, + "defaultValue": "()" + }, + "conferenceDataVersion": { + "metadata": { + "label": "Conference Data Version", + "description": "Version number of conference data supported by the API client. Version 0 assumes no conference data support and ignores conference data in the event's body. Version 1 enables support for copying of ConferenceData as well as for creating new conferences using the createRequest field of conferenceData. The default is 0" + }, + "types": [ + { + "fieldType": "NUMBER", + "ballerinaType": "int", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "int?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "conferenceDataVersion" + }, + "defaultValue": "()" + }, + "maxAttendees": { + "metadata": { + "label": "Max Attendees", + "description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional" + }, + "types": [ + { + "fieldType": "NUMBER", + "ballerinaType": "int", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "int?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "maxAttendees" + }, + "defaultValue": "()" + }, + "sendUpdates": { + "metadata": { + "label": "Send Updates", + "description": "Whether to send notifications about the creation of the new event. Note that some emails might still be sent. The default is false" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "\"none\"?|\"all\"|\"externalOnly\"", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "sendUpdates" + }, + "defaultValue": "()" + }, + "supportsAttachments": { + "metadata": { + "label": "Supports Attachments", + "description": "Whether API client performing operation supports event attachments. Optional. The default is False" + }, + "types": [ + { + "fieldType": "FLAG", + "ballerinaType": "boolean", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "supportsAttachments" + }, + "defaultValue": "()" + }, + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Trigger error flow" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "type": { + "metadata": { + "label": "Result Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "gcalendar:Event", + "placeholder": "var", + "optional": false, + "editable": false, + "advanced": false, + "hidden": true, + "codedata": {} + } + }, + "flags": 1 + }, + { + "id": "58289", + "metadata": { + "label": "If", + "description": "Add conditional branch to the integration flow." + }, + "codedata": { + "node": "IF", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 26, + "offset": 8 + }, + "endLine": { + "line": 42, + "offset": 9 + } + }, + "sourceCode": "if address.houseNo == \"\" {\n while address.houseNo != \"\" {\n address.houseNo = getRandomHome();\n }\n } else if address.street == \"\" {\n match name {\n \"john\" => {\n io:println(\"name list\");\n }\n \"wick\" => {\n log:printInfo(string `${address.street} + ${name}`);\n }\n _ => {\n }\n }\n } else {\n }" + }, + "returning": false, + "branches": [ + { + "label": "Then", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 26, + "offset": 33 + }, + "endLine": { + "line": 30, + "offset": 9 + } + }, + "sourceCode": "{\n while address.houseNo != \"\" {\n address.houseNo = getRandomHome();\n }\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "condition": { + "metadata": { + "label": "Condition", + "description": "Boolean Condition" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": true + } + ], + "value": "address.houseNo == \"\" ", + "placeholder": "true", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [ + { + "id": "58975", + "metadata": { + "label": "While", + "description": "Loop over a block of code." + }, + "codedata": { + "node": "WHILE", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 27, + "offset": 12 + }, + "endLine": { + "line": 29, + "offset": 13 + } + }, + "sourceCode": "while address.houseNo != \"\" {\n address.houseNo = getRandomHome();\n }" + }, + "returning": false, + "branches": [ + { + "label": "Body", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 27, + "offset": 40 + }, + "endLine": { + "line": 29, + "offset": 13 + } + }, + "sourceCode": "{\n address.houseNo = getRandomHome();\n }" + }, + "repeatable": "ONE", + "children": [ + { + "id": "60066", + "metadata": { + "label": "getRandomHome", + "description": "" + }, + "codedata": { + "node": "FUNCTION_CALL", + "org": "gayanka", + "module": "test_2_3_5", + "packageName": "test_2_3_5", + "symbol": "getRandomHome", + "version": "0.1.0", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 28, + "offset": 16 + }, + "endLine": { + "line": 28, + "offset": 50 + } + }, + "sourceCode": "address.houseNo = getRandomHome();" + }, + "returning": false, + "properties": { + "view": { + "metadata": { + "label": "View", + "description": "Function definition location" + }, + "types": [ + { + "fieldType": "VIEW", + "selected": true + } + ], + "value": { + "fileName": "functions.bal", + "startLine": { + "line": 0, + "offset": 0 + }, + "endLine": { + "line": 1, + "offset": 1 + } + }, + "optional": false, + "editable": false, + "advanced": false, + "hidden": false + } + }, + "flags": 0 + } + ] + } + ], + "properties": { + "condition": { + "metadata": { + "label": "Condition", + "description": "Boolean Condition" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": true + } + ], + "value": "address.houseNo != \"\" ", + "placeholder": "true", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "flags": 0 + } + ] + }, + { + "label": "address.street == \"\"", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 30, + "offset": 39 + }, + "endLine": { + "line": 41, + "offset": 9 + } + }, + "sourceCode": "{\n match name {\n \"john\" => {\n io:println(\"name list\");\n }\n \"wick\" => {\n log:printInfo(string `${address.street} + ${name}`);\n }\n _ => {\n }\n }\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "condition": { + "metadata": { + "label": "Condition", + "description": "Boolean Condition" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": true + } + ], + "value": "address.street == \"\" ", + "placeholder": "true", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [ + { + "id": "63160", + "metadata": { + "label": "Match", + "description": "Switches the data flow based on the value of an expression." + }, + "codedata": { + "node": "MATCH", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 31, + "offset": 12 + }, + "endLine": { + "line": 40, + "offset": 13 + } + }, + "sourceCode": "match name {\n \"john\" => {\n io:println(\"name list\");\n }\n \"wick\" => {\n log:printInfo(string `${address.street} + ${name}`);\n }\n _ => {\n }\n }" + }, + "returning": false, + "branches": [ + { + "label": "\"john\"", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 32, + "offset": 26 + }, + "endLine": { + "line": 34, + "offset": 17 + } + }, + "sourceCode": "{\n io:println(\"name list\");\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "patterns": { + "metadata": { + "label": "Patterns", + "description": "List of binding patterns" + }, + "types": [ + { + "fieldType": "SINGLE_SELECT", + "selected": true + } + ], + "value": [ + { + "metadata": { + "label": "Pattern", + "description": "Binding pattern" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "\"john\"", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "comment": {} + } + ], + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [ + { + "id": "65144", + "metadata": { + "label": "println", + "description": "Prints `any`, `error` or string templates(such as `The respective int value is ${val}`) value(s) to the STDOUT\nfollowed by a new line.\n```ballerina\nio:println(\"Start processing the CSV file from \", srcFileName);\n```\n", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_io_1.8.0.png" + }, + "codedata": { + "node": "FUNCTION_CALL", + "org": "ballerina", + "module": "io", + "packageName": "io", + "symbol": "println", + "version": "1.8.0", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 33, + "offset": 20 + }, + "endLine": { + "line": 33, + "offset": 44 + } + }, + "sourceCode": "io:println(\"name list\");" + }, + "returning": false, + "properties": { + "values": { + "metadata": { + "label": "values", + "description": "The value(s) to be printed" + }, + "types": [ + { + "fieldType": "EXPRESSION_SET", + "selected": false + } + ], + "value": ["\"name list\""], + "placeholder": "()", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REST_PARAMETER", + "originalName": "values" + } + } + }, + "flags": 0 + } + ] + }, + { + "label": "\"wick\"", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 35, + "offset": 26 + }, + "endLine": { + "line": 37, + "offset": 17 + } + }, + "sourceCode": "{\n log:printInfo(string `${address.street} + ${name}`);\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "patterns": { + "metadata": { + "label": "Patterns", + "description": "List of binding patterns" + }, + "types": [ + { + "fieldType": "SINGLE_SELECT", + "selected": true + } + ], + "value": [ + { + "metadata": { + "label": "Pattern", + "description": "Binding pattern" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "\"wick\"", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "comment": {} + } + ], + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [ + { + "id": "68148", + "metadata": { + "label": "printInfo", + "description": "Prints info logs.\n```ballerina\nlog:printInfo(\"info message\", id = 845315)\n```\n", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_log_2.15.0.png" + }, + "codedata": { + "node": "FUNCTION_CALL", + "org": "ballerina", + "module": "log", + "packageName": "log", + "symbol": "printInfo", + "version": "2.15.0", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 36, + "offset": 20 + }, + "endLine": { + "line": 36, + "offset": 72 + } + }, + "sourceCode": "log:printInfo(string `${address.street} + ${name}`);" + }, + "returning": false, + "properties": { + "msg": { + "metadata": { + "label": "Msg", + "description": "The message to be logged" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string|log:PrintableRawTemplate", + "selected": false + } + ], + "value": "string `${address.street} + ${name}`", + "placeholder": "\"\"", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED", + "originalName": "msg" + } + }, + "error": { + "metadata": { + "label": "Error" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "error?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "error" + }, + "defaultValue": "()" + }, + "stackTrace": { + "metadata": { + "label": "Stack Trace", + "description": "The error stack trace to be logged" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "error:StackFrame[]?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "stackTrace" + }, + "defaultValue": "()" + }, + "additionalValues": { + "metadata": { + "label": "Additional Values", + "description": "Capture key value pairs" + }, + "types": [ + { + "fieldType": "MAPPING_EXPRESSION_SET", + "selected": false + } + ], + "value": [], + "placeholder": "{}", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_RECORD_REST", + "originalName": "Additional Values" + } + } + }, + "flags": 0 + } + ] + }, + { + "label": "_", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 38, + "offset": 21 + }, + "endLine": { + "line": 39, + "offset": 17 + } + }, + "sourceCode": "{\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "patterns": { + "metadata": { + "label": "Patterns", + "description": "List of binding patterns" + }, + "types": [ + { + "fieldType": "SINGLE_SELECT", + "selected": true + } + ], + "value": [ + { + "metadata": { + "label": "Pattern", + "description": "Binding pattern" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "_", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "comment": {} + } + ], + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [] + } + ], + "properties": { + "matchTarget": { + "metadata": { + "label": "Target", + "description": "Match target expression" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "any|error", + "selected": true + } + ], + "value": "name ", + "placeholder": "true", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "flags": 0 + } + ] + }, + { + "label": "Else", + "kind": "BLOCK", + "codedata": { + "node": "ELSE", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 41, + "offset": 15 + }, + "endLine": { + "line": 42, + "offset": 9 + } + }, + "sourceCode": "{\n }" + }, + "repeatable": "ZERO_OR_ONE", + "children": [] + } + ], + "flags": 0 + }, + { + "id": "74742", + "metadata": { + "label": "run", + "description": "Executes the agent for a given user query.", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_ai_1.9.0.png", + "data": { + "agent": { + "systemPrompt": "{role: string `sample role`, instructions: string `sample instructions`}", + "instructions": "sample instructions", + "role": "sample role", + "scope": "Global", + "model": "aiWso2modelprovider" + }, + "model": { + "name": "aiWso2modelprovider", + "path": "https://bcentral-packageicons.azureedge.net/images/test_2_3_5_test_2_3_5_0.1.0.png", + "type": "Wso2ModelProvider" + } + } + }, + "codedata": { + "node": "AGENT_CALL", + "org": "ballerina", + "module": "ai", + "packageName": "ai", + "object": "Agent", + "symbol": "run", + "version": "1.9.0", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 43, + "offset": 8 + }, + "endLine": { + "line": 43, + "offset": 94 + } + }, + "sourceCode": "string agentRes = check aiAgent.run(string `${name} user asking following questions`);", + "inferredReturnType": "td", + "data": { + "agentCodedata": { + "node": "AGENT", + "org": "ballerina", + "module": "ai", + "packageName": "ai", + "object": "Agent", + "symbol": "init", + "version": "1.9.0", + "lineRange": { + "fileName": "agents.bal", + "startLine": { + "line": 4, + "offset": 0 + }, + "endLine": { + "line": 6, + "offset": 2 + } + }, + "sourceCode": "final ai:Agent aiAgent = check new (\n systemPrompt = {role: string `sample role`, instructions: string `sample instructions`}, model = aiWso2modelprovider\n);", + "isNew": false, + "data": { + "scope": "Global" + } + } + } + }, + "returning": false, + "properties": { + "systemPrompt": { + "metadata": { + "label": "System Prompt", + "description": "The system prompt assigned to the agent" + }, + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "ai:SystemPrompt", + "typeMembers": [ + { + "type": "SystemPrompt", + "packageInfo": "ballerina:ai:1.9.0", + "packageName": "ai", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:SystemPrompt", + "selected": false + } + ], + "value": "{role: string `sample role`, instructions: string `sample instructions`}", + "placeholder": "{role: \"\", instructions: \"\"}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": true, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "{role: \"\", instructions: \"\"}" + }, + "model": { + "metadata": { + "label": "Model", + "description": "The model used by the agent" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:ModelProvider", + "selected": false + } + ], + "value": "aiWso2modelprovider", + "placeholder": "object {}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": true, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "object {}" + }, + "tools": { + "metadata": { + "label": "Tools", + "description": "The tools available for the agent" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "(ai:BaseToolKit|ai:ToolConfig|ai:FunctionTool)[]", + "selected": false + } + ], + "placeholder": "[]", + "optional": true, + "editable": true, + "advanced": true, + "hidden": true, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "[]" + }, + "maxIter": { + "metadata": { + "label": "Maximum Iterations", + "description": "The maximum number of iterations the agent performs to complete the task.\nBy default, it is set to the number of tools + 1." + }, + "types": [ + { + "fieldType": "NUMBER", + "ballerinaType": "int", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "\"INFER_TOOL_COUNT\"|int", + "selected": false + } + ], + "placeholder": "\"INFER_TOOL_COUNT\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "\"INFER_TOOL_COUNT\"" + }, + "verbose": { + "metadata": { + "label": "Verbose", + "description": "Specifies whether verbose logging is enabled" + }, + "types": [ + { + "fieldType": "FLAG", + "ballerinaType": "boolean", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": false + } + ], + "placeholder": "false", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "false" + }, + "memory": { + "metadata": { + "label": "Memory", + "description": "The memory used by the agent to store and manage conversation history.\nDefaults to use an in-memory message store that trims on overflow, if unspecified." + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:Memory?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": true, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "()" + }, + "toolLoadingStrategy": { + "metadata": { + "label": "Tool Loading Strategy", + "description": "Defines the strategies for loading tool schemas into an Agent. \nBy default, all tools are loaded without any filtering." + }, + "types": [ + { + "fieldType": "SINGLE_SELECT", + "options": [ + { + "label": "LLM_FILTER", + "value": "\"LLM_FILTER\"" + }, + { + "label": "NO_FILTER", + "value": "\"NO_FILTER\"" + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:ToolLoadingStrategy", + "selected": false + } + ], + "placeholder": "\"LLM_FILTER\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "\"LLM_FILTER\"" + }, + "type": { + "metadata": { + "label": "Variable Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "string", + "placeholder": "var", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": {} + }, + "variable": { + "metadata": { + "label": "Variable Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "agentRes", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 43, + "offset": 15 + }, + "endLine": { + "line": 43, + "offset": 23 + } + } + } + }, + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Trigger error flow" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "scope": { + "metadata": { + "label": "Connection Scope", + "description": "Scope of the connection, Global or Local" + }, + "types": [ + { + "fieldType": "ENUM", + "selected": true + } + ], + "value": "Global", + "optional": false, + "editable": true, + "advanced": true, + "hidden": true, + "codedata": { + "kind": "" + } + }, + "role": { + "metadata": { + "label": "Role", + "description": "Define the agent's primary function" + }, + "types": [ + { + "fieldType": "PROMPT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "value": "sample role", + "placeholder": "e.g., Customer Support Assistant, Sales Advisor, Data Analyst", + "optional": true, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED" + }, + "defaultValue": "" + }, + "instructions": { + "metadata": { + "label": "Instructions", + "description": "Detailed instructions for the agent" + }, + "types": [ + { + "fieldType": "PROMPT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "value": "sample instructions", + "placeholder": "e.g., You are a friendly assistant. Your goal is to...", + "optional": true, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED" + }, + "defaultValue": "" + }, + "connection": { + "metadata": { + "label": "Object", + "description": "The object which you want to call the method on" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "aiAgent", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false + }, + "query": { + "metadata": { + "label": "Query", + "description": "The natural language input provided to the agent" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "value": "string `${name} user asking following questions`", + "placeholder": "\"\"", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED", + "originalName": "query" + } + }, + "sessionId": { + "metadata": { + "label": "Session ID", + "description": "The ID associated with the agent memory" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "placeholder": "\"\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "sessionId" + }, + "defaultValue": "\"\"" + }, + "context": { + "metadata": { + "label": "Context", + "description": "The additional context that can be used during agent tool execution" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:Context", + "selected": false + } + ], + "placeholder": "new ()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "context" + }, + "defaultValue": "new ()" + }, + "td": { + "metadata": { + "label": "Td", + "description": "Type descriptor specifying the expected return type format" + }, + "types": [ + { + "fieldType": "TYPE", + "ballerinaType": "ai:Trace|string", + "selected": false + } + ], + "value": "string", + "placeholder": "ai:Trace|string", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "PARAM_FOR_TYPE_INFER", + "originalName": "td" + }, + "defaultValue": "ai:Trace|string" + } + }, + "flags": 1 + }, + { + "id": "75707", + "metadata": { + "label": "run", + "description": "Executes the agent for a given user query.", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_ai_1.9.0.png", + "data": { + "tools": [ + { + "name": "createEventTool", + "path": "https://bcentral-packageicons.azureedge.net/images/ballerinax_googleapis.gcalendar_4.0.1.png", + "description": "create event tool description" + }, + { + "name": "getRandomName", + "path": "", + "description": "Define a function" + }, + { + "name": "aiMcpbasetoolkit", + "path": "https://bcentral-packageicons.azureedge.net/images/ballerina_mcp_0.4.2.png", + "description": "", + "type": "MCP Server" + } + ], + "agent": { + "systemPrompt": "{role: string `sample role 2`, instructions: string `sample instruction 2`}", + "instructions": "sample instruction 2", + "role": "sample role 2", + "scope": "Global", + "model": "aiWso2modelproviderResult", + "tools": "[createEventTool, getRandomName, aiMcpbasetoolkit]" + }, + "model": { + "name": "aiWso2modelproviderResult", + "path": "https://bcentral-packageicons.azureedge.net/images/test_2_3_5_test_2_3_5_0.1.0.png", + "type": "Wso2ModelProvider" + } + } + }, + "codedata": { + "node": "AGENT_CALL", + "org": "ballerina", + "module": "ai", + "packageName": "ai", + "object": "Agent", + "symbol": "run", + "version": "1.9.0", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 44, + "offset": 8 + }, + "endLine": { + "line": 44, + "offset": 67 + } + }, + "sourceCode": "string sampleRes = check aiAgentResult.run(\"sample query\");", + "inferredReturnType": "td", + "data": { + "agentCodedata": { + "node": "AGENT", + "org": "ballerina", + "module": "ai", + "packageName": "ai", + "object": "Agent", + "symbol": "init", + "version": "1.9.0", + "lineRange": { + "fileName": "agents.bal", + "startLine": { + "line": 7, + "offset": 0 + }, + "endLine": { + "line": 9, + "offset": 2 + } + }, + "sourceCode": "final ai:Agent aiAgentResult = check new (\n systemPrompt = {role: string `sample role 2`, instructions: string `sample instruction 2`}, model = aiWso2modelproviderResult, tools = [createEventTool, getRandomName, aiMcpbasetoolkit]\n);", + "isNew": false, + "data": { + "scope": "Global" + } + } + } + }, + "returning": false, + "properties": { + "systemPrompt": { + "metadata": { + "label": "System Prompt", + "description": "The system prompt assigned to the agent" + }, + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "ai:SystemPrompt", + "typeMembers": [ + { + "type": "SystemPrompt", + "packageInfo": "ballerina:ai:1.9.0", + "packageName": "ai", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:SystemPrompt", + "selected": false + } + ], + "value": "{role: string `sample role 2`, instructions: string `sample instruction 2`}", + "placeholder": "{role: \"\", instructions: \"\"}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": true, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "{role: \"\", instructions: \"\"}" + }, + "model": { + "metadata": { + "label": "Model", + "description": "The model used by the agent" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:ModelProvider", + "selected": false + } + ], + "value": "aiWso2modelproviderResult", + "placeholder": "object {}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": true, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "object {}" + }, + "tools": { + "metadata": { + "label": "Tools", + "description": "The tools available for the agent" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "(ai:BaseToolKit|ai:ToolConfig|ai:FunctionTool)[]", + "selected": false + } + ], + "value": "[createEventTool, getRandomName, aiMcpbasetoolkit]", + "placeholder": "[]", + "optional": true, + "editable": true, + "advanced": true, + "hidden": true, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "[]" + }, + "maxIter": { + "metadata": { + "label": "Maximum Iterations", + "description": "The maximum number of iterations the agent performs to complete the task.\nBy default, it is set to the number of tools + 1." + }, + "types": [ + { + "fieldType": "NUMBER", + "ballerinaType": "int", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "\"INFER_TOOL_COUNT\"|int", + "selected": false + } + ], + "placeholder": "\"INFER_TOOL_COUNT\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "\"INFER_TOOL_COUNT\"" + }, + "verbose": { + "metadata": { + "label": "Verbose", + "description": "Specifies whether verbose logging is enabled" + }, + "types": [ + { + "fieldType": "FLAG", + "ballerinaType": "boolean", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": false + } + ], + "placeholder": "false", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "false" + }, + "memory": { + "metadata": { + "label": "Memory", + "description": "The memory used by the agent to store and manage conversation history.\nDefaults to use an in-memory message store that trims on overflow, if unspecified." + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:Memory?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": true, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "()" + }, + "toolLoadingStrategy": { + "metadata": { + "label": "Tool Loading Strategy", + "description": "Defines the strategies for loading tool schemas into an Agent. \nBy default, all tools are loaded without any filtering." + }, + "types": [ + { + "fieldType": "SINGLE_SELECT", + "options": [ + { + "label": "LLM_FILTER", + "value": "\"LLM_FILTER\"" + }, + { + "label": "NO_FILTER", + "value": "\"NO_FILTER\"" + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:ToolLoadingStrategy", + "selected": false + } + ], + "placeholder": "\"LLM_FILTER\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD" + }, + "defaultValue": "\"LLM_FILTER\"" + }, + "type": { + "metadata": { + "label": "Variable Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "string", + "placeholder": "var", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": {} + }, + "variable": { + "metadata": { + "label": "Variable Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "sampleRes", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 44, + "offset": 15 + }, + "endLine": { + "line": 44, + "offset": 24 + } + } + } + }, + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Trigger error flow" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "scope": { + "metadata": { + "label": "Connection Scope", + "description": "Scope of the connection, Global or Local" + }, + "types": [ + { + "fieldType": "ENUM", + "selected": true + } + ], + "value": "Global", + "optional": false, + "editable": true, + "advanced": true, + "hidden": true, + "codedata": { + "kind": "" + } + }, + "role": { + "metadata": { + "label": "Role", + "description": "Define the agent's primary function" + }, + "types": [ + { + "fieldType": "PROMPT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "value": "sample role 2", + "placeholder": "e.g., Customer Support Assistant, Sales Advisor, Data Analyst", + "optional": true, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED" + }, + "defaultValue": "" + }, + "instructions": { + "metadata": { + "label": "Instructions", + "description": "Detailed instructions for the agent" + }, + "types": [ + { + "fieldType": "PROMPT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "value": "sample instruction 2", + "placeholder": "e.g., You are a friendly assistant. Your goal is to...", + "optional": true, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED" + }, + "defaultValue": "" + }, + "connection": { + "metadata": { + "label": "Object", + "description": "The object which you want to call the method on" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "aiAgentResult", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false + }, + "query": { + "metadata": { + "label": "Query", + "description": "The natural language input provided to the agent" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "value": "\"sample query\"", + "placeholder": "\"\"", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED", + "originalName": "query" + } + }, + "sessionId": { + "metadata": { + "label": "Session ID", + "description": "The ID associated with the agent memory" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "placeholder": "\"\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "sessionId" + }, + "defaultValue": "\"\"" + }, + "context": { + "metadata": { + "label": "Context", + "description": "The additional context that can be used during agent tool execution" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:Context", + "selected": false + } + ], + "placeholder": "new ()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "context" + }, + "defaultValue": "new ()" + }, + "td": { + "metadata": { + "label": "Td", + "description": "Type descriptor specifying the expected return type format" + }, + "types": [ + { + "fieldType": "TYPE", + "ballerinaType": "ai:Trace|string", + "selected": false + } + ], + "value": "string", + "placeholder": "ai:Trace|string", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "PARAM_FOR_TYPE_INFER", + "originalName": "td" + }, + "defaultValue": "ai:Trace|string" + } + }, + "flags": 1 + }, + { + "id": "76951", + "metadata": { + "label": "Fork", + "description": "Create parallel workers" + }, + "codedata": { + "node": "FORK", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 45, + "offset": 8 + }, + "endLine": { + "line": 55, + "offset": 9 + } + }, + "sourceCode": "fork {\n worker worker1 {\n do {\n lock {\n }\n } on fail error err {\n }\n }\n worker worker2 {\n }\n }" + }, + "returning": false, + "branches": [ + { + "label": "worker1", + "kind": "WORKER", + "codedata": { + "node": "WORKER", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 46, + "offset": 12 + }, + "endLine": { + "line": 52, + "offset": 13 + } + }, + "sourceCode": "worker worker1 {\n do {\n lock {\n }\n } on fail error err {\n }\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "variable": { + "metadata": { + "label": "Worker Name", + "description": "Name of the worker" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "worker1", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 46, + "offset": 19 + }, + "endLine": { + "line": 46, + "offset": 26 + } + } + } + }, + "type": { + "metadata": { + "label": "Return Type", + "description": "Type of the return value" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "", + "optional": true, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [ + { + "id": "79005", + "metadata": { + "label": "ErrorHandler", + "description": "Catch and handle errors" + }, + "codedata": { + "node": "ERROR_HANDLER", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 47, + "offset": 16 + }, + "endLine": { + "line": 51, + "offset": 17 + } + }, + "sourceCode": "do {\n lock {\n }\n } on fail error err {\n }" + }, + "returning": false, + "branches": [ + { + "label": "Body", + "kind": "BLOCK", + "codedata": { + "node": "BODY", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 47, + "offset": 19 + }, + "endLine": { + "line": 50, + "offset": 17 + } + }, + "sourceCode": "{\n lock {\n }\n }" + }, + "repeatable": "ONE", + "children": [ + { + "id": "80032", + "metadata": { + "label": "Lock", + "description": "Allow to access mutable states safely" + }, + "codedata": { + "node": "LOCK", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 48, + "offset": 20 + }, + "endLine": { + "line": 49, + "offset": 21 + } + }, + "sourceCode": "lock {\n }" + }, + "returning": false, + "branches": [ + { + "label": "Body", + "kind": "BLOCK", + "codedata": { + "node": "BODY", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 48, + "offset": 25 + }, + "endLine": { + "line": 49, + "offset": 21 + } + }, + "sourceCode": "{\n }" + }, + "repeatable": "ONE", + "children": [] + } + ], + "flags": 0 + } + ] + }, + { + "label": "On Failure", + "kind": "BLOCK", + "codedata": { + "node": "ON_FAILURE", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 50, + "offset": 36 + }, + "endLine": { + "line": 51, + "offset": 17 + } + }, + "sourceCode": "{\n }" + }, + "repeatable": "ZERO_OR_ONE", + "properties": { + "ignore": { + "metadata": { + "label": "Ignore", + "description": "Ignore the error value" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "false", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + }, + "errorVariable": { + "metadata": { + "label": "Error Variable", + "description": "Name of the error variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "err ", + "placeholder": "err", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + }, + "errorType": { + "metadata": { + "label": "Error Type", + "description": "Type of the error" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "error", + "placeholder": "error", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [] + } + ], + "flags": 0 + } + ] + }, + { + "label": "worker2", + "kind": "WORKER", + "codedata": { + "node": "WORKER", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 53, + "offset": 12 + }, + "endLine": { + "line": 54, + "offset": 13 + } + }, + "sourceCode": "worker worker2 {\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "variable": { + "metadata": { + "label": "Worker Name", + "description": "Name of the worker" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "worker2", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 53, + "offset": 19 + }, + "endLine": { + "line": 53, + "offset": 26 + } + } + } + }, + "type": { + "metadata": { + "label": "Return Type", + "description": "Type of the return value" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "", + "optional": true, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [] + } + ], + "flags": 0 + }, + { + "id": "87604", + "metadata": { + "label": "Wait", + "description": "Wait for a set of futures to complete" + }, + "codedata": { + "node": "WAIT", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 56, + "offset": 8 + }, + "endLine": { + "line": 56, + "offset": 60 + } + }, + "sourceCode": "map waitResult = wait {worker1, worker2};" + }, + "returning": false, + "properties": { + "waitAll": { + "metadata": { + "label": "Wait All", + "description": "Wait for all tasks to complete" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + }, + "futures": { + "metadata": { + "label": "Futures", + "description": "The futures to wait for" + }, + "types": [ + { + "fieldType": "REPEATABLE_PROPERTY", + "selected": false + } + ], + "value": { + "future1": { + "metadata": { + "label": "Future", + "description": "The worker/async function to wait for" + }, + "types": [ + { + "fieldType": "FIXED_PROPERTY", + "selected": false + } + ], + "value": { + "variable": { + "metadata": { + "label": "Variable Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "dependentProperty": "waitAll" + } + }, + "expression": { + "metadata": { + "label": "Expression", + "description": "Expression" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "worker1", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "optional": false, + "editable": false, + "advanced": false, + "hidden": false + }, + "future2": { + "metadata": { + "label": "Future", + "description": "The worker/async function to wait for" + }, + "types": [ + { + "fieldType": "FIXED_PROPERTY", + "selected": false + } + ], + "value": { + "variable": { + "metadata": { + "label": "Variable Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "dependentProperty": "waitAll" + } + }, + "expression": { + "metadata": { + "label": "Expression", + "description": "Expression" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "worker2", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "optional": false, + "editable": false, + "advanced": false, + "hidden": false + } + }, + "optional": false, + "editable": false, + "advanced": false, + "hidden": false + }, + "variable": { + "metadata": { + "label": "Variable Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "waitResult", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 56, + "offset": 23 + }, + "endLine": { + "line": 56, + "offset": 33 + } + } + } + }, + "type": { + "metadata": { + "label": "Variable Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "map", + "placeholder": "var", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": {} + } + }, + "flags": 0 + }, + { + "id": "88824", + "metadata": { + "label": "Foreach", + "description": "Iterate over a block of code." + }, + "codedata": { + "node": "FOREACH", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 57, + "offset": 8 + }, + "endLine": { + "line": 66, + "offset": 9 + } + }, + "sourceCode": "foreach string var1 in [] {\n if agentRes == \"\" {\n return;\n } else {\n break;\n }\n if var1 {\n continue;\n }\n }" + }, + "returning": false, + "branches": [ + { + "label": "Body", + "kind": "BLOCK", + "codedata": { + "node": "BODY", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 57, + "offset": 34 + }, + "endLine": { + "line": 66, + "offset": 9 + } + }, + "sourceCode": "{\n if agentRes == \"\" {\n return;\n } else {\n break;\n }\n if var1 {\n continue;\n }\n }" + }, + "repeatable": "ONE", + "children": [ + { + "id": "89789", + "metadata": { + "label": "If", + "description": "Add conditional branch to the integration flow." + }, + "codedata": { + "node": "IF", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 58, + "offset": 12 + }, + "endLine": { + "line": 62, + "offset": 13 + } + }, + "sourceCode": "if agentRes == \"\" {\n return;\n } else {\n break;\n }" + }, + "returning": false, + "branches": [ + { + "label": "Then", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 58, + "offset": 30 + }, + "endLine": { + "line": 60, + "offset": 13 + } + }, + "sourceCode": "{\n return;\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "condition": { + "metadata": { + "label": "Condition", + "description": "Boolean Condition" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": true + } + ], + "value": "agentRes == \"\" ", + "placeholder": "true", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [ + { + "id": "90791", + "metadata": { + "label": "Return" + }, + "codedata": { + "node": "RETURN", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 59, + "offset": 16 + }, + "endLine": { + "line": 59, + "offset": 23 + } + }, + "sourceCode": "return;" + }, + "returning": true, + "flags": 0 + } + ] + }, + { + "label": "Else", + "kind": "BLOCK", + "codedata": { + "node": "ELSE", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 60, + "offset": 19 + }, + "endLine": { + "line": 62, + "offset": 13 + } + }, + "sourceCode": "{\n break;\n }" + }, + "repeatable": "ZERO_OR_ONE", + "children": [ + { + "id": "92774", + "metadata": { + "label": "Break", + "description": "Exit the current loop" + }, + "codedata": { + "node": "BREAK", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 61, + "offset": 16 + }, + "endLine": { + "line": 61, + "offset": 22 + } + }, + "sourceCode": "break;" + }, + "returning": false, + "flags": 0 + } + ] + } + ], + "flags": 0 + }, + { + "id": "94687", + "metadata": { + "label": "If", + "description": "Add conditional branch to the integration flow." + }, + "codedata": { + "node": "IF", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 63, + "offset": 12 + }, + "endLine": { + "line": 65, + "offset": 13 + } + }, + "sourceCode": "if var1 {\n continue;\n }" + }, + "returning": false, + "branches": [ + { + "label": "Then", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 63, + "offset": 20 + }, + "endLine": { + "line": 65, + "offset": 13 + } + }, + "sourceCode": "{\n continue;\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "condition": { + "metadata": { + "label": "Condition", + "description": "Boolean Condition" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": true + } + ], + "value": "var1 ", + "placeholder": "true", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [ + { + "id": "95753", + "metadata": { + "label": "Continue", + "description": "Skip the current iteration and continue with the next one" + }, + "codedata": { + "node": "CONTINUE", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 64, + "offset": 16 + }, + "endLine": { + "line": 64, + "offset": 25 + } + }, + "sourceCode": "continue;" + }, + "returning": false, + "flags": 0 + } + ] + } + ], + "flags": 0 + } + ] + } + ], + "properties": { + "variable": { + "metadata": { + "label": "Variable Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "var1", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 57, + "offset": 23 + }, + "endLine": { + "line": 57, + "offset": 27 + } + } + } + }, + "type": { + "metadata": { + "label": "Variable Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "string", + "placeholder": "var", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": {} + }, + "collection": { + "metadata": { + "label": "Collection", + "description": "Collection to iterate" + }, + "types": [ + { + "fieldType": "ACTION_OR_EXPRESSION", + "ballerinaType": "(any|error)[]|stream|string|map|json", + "selected": true + } + ], + "value": "[] ", + "placeholder": "[]", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "flags": 0 + } + ] + }, + { + "label": "On Failure", + "kind": "BLOCK", + "codedata": { + "node": "ON_FAILURE", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 68, + "offset": 22 + }, + "endLine": { + "line": 71, + "offset": 5 + } + }, + "sourceCode": "{\n log:printError(\"Error occurred\", 'error = e);\n return e;\n }" + }, + "repeatable": "ZERO_OR_ONE", + "properties": { + "ignore": { + "metadata": { + "label": "Ignore", + "description": "Ignore the error value" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "selected": true + } + ], + "value": "false", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + }, + "errorVariable": { + "metadata": { + "label": "Error Variable", + "description": "Name of the error variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "e ", + "placeholder": "err", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + }, + "errorType": { + "metadata": { + "label": "Error Type", + "description": "Type of the error" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "gcalendar:Error|ai:error", + "placeholder": "error", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "children": [ + { + "id": "100493", + "metadata": { + "label": "printError", + "description": "Prints error logs.\n```ballerina\nerror e = error(\"error occurred\");\nlog:printError(\"error log with cause\", 'error = e, id = 845315);\n```\n", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_log_2.15.0.png" + }, + "codedata": { + "node": "FUNCTION_CALL", + "org": "ballerina", + "module": "log", + "packageName": "log", + "symbol": "printError", + "version": "2.15.0", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 69, + "offset": 8 + }, + "endLine": { + "line": 69, + "offset": 53 + } + }, + "sourceCode": "log:printError(\"Error occurred\", 'error = e);" + }, + "returning": false, + "properties": { + "msg": { + "metadata": { + "label": "Msg", + "description": "The message to be logged" + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string|log:PrintableRawTemplate", + "selected": false + } + ], + "value": "\"Error occurred\"", + "placeholder": "\"\"", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED", + "originalName": "msg" + } + }, + "error": { + "metadata": { + "label": "Error" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "error?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "error" + }, + "defaultValue": "()" + }, + "stackTrace": { + "metadata": { + "label": "Stack Trace", + "description": "The error stack trace to be logged" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "error:StackFrame[]?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "stackTrace" + }, + "defaultValue": "()" + }, + "additionalValues": { + "metadata": { + "label": "Additional Values", + "description": "Capture key value pairs" + }, + "types": [ + { + "fieldType": "MAPPING_EXPRESSION_SET", + "selected": false + } + ], + "value": [ + { + "'error": "e" + } + ], + "placeholder": "{}", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_RECORD_REST", + "originalName": "Additional Values" + } + } + }, + "flags": 0 + }, + { + "id": "101449", + "metadata": { + "label": "Return", + "description": "Value of 'e'" + }, + "codedata": { + "node": "RETURN", + "lineRange": { + "fileName": "automation.bal", + "startLine": { + "line": 70, + "offset": 8 + }, + "endLine": { + "line": 70, + "offset": 17 + } + }, + "sourceCode": "return e;" + }, + "returning": true, + "properties": { + "expression": { + "metadata": { + "label": "Expression", + "description": "Return value" + }, + "types": [ + { + "fieldType": "ACTION_OR_EXPRESSION", + "selected": true + } + ], + "value": "e", + "optional": true, + "editable": true, + "advanced": false, + "hidden": false + } + }, + "flags": 0 + } + ] + } + ], + "diagnostics": { + "hasDiagnostics": true + }, + "flags": 0 + } + ], + "connections": [ + { + "id": "35776", + "metadata": { + "label": "Agent", + "description": "Represents an agent.", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_ai_1.9.0.png" + }, + "codedata": { + "node": "AGENT", + "org": "ballerina", + "module": "ai", + "object": "Agent", + "symbol": "init", + "lineRange": { + "fileName": "agents.bal", + "startLine": { + "line": 4, + "offset": 0 + }, + "endLine": { + "line": 6, + "offset": 2 + } + }, + "sourceCode": "final ai:Agent aiAgent = check new (\n systemPrompt = {role: string `sample role`, instructions: string `sample instructions`}, model = aiWso2modelprovider\n);" + }, + "returning": false, + "properties": { + "systemPrompt": { + "metadata": { + "label": "System Prompt", + "description": "The system prompt assigned to the agent" + }, + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "ai:SystemPrompt", + "typeMembers": [ + { + "type": "SystemPrompt", + "packageInfo": "ballerina:ai:1.9.0", + "packageName": "ai", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:SystemPrompt", + "selected": false + } + ], + "value": "{role: string `sample role`, instructions: string `sample instructions`}", + "placeholder": "{role: \"\", instructions: \"\"}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "systemPrompt" + }, + "defaultValue": "{role: \"\", instructions: \"\"}" + }, + "model": { + "metadata": { + "label": "Model", + "description": "The model used by the agent" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:ModelProvider", + "selected": true + } + ], + "value": "aiWso2modelprovider", + "placeholder": "object {}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "model" + }, + "defaultValue": "object {}" + }, + "tools": { + "metadata": { + "label": "Tools", + "description": "The tools available for the agent" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "(ai:BaseToolKit|ai:ToolConfig|ai:FunctionTool)[]", + "selected": false + } + ], + "placeholder": "[]", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "tools" + }, + "defaultValue": "[]" + }, + "maxIter": { + "metadata": { + "label": "Maximum Iterations", + "description": "The maximum number of iterations the agent performs to complete the task.\nBy default, it is set to the number of tools + 1." + }, + "types": [ + { + "fieldType": "NUMBER", + "ballerinaType": "int", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "\"INFER_TOOL_COUNT\"|int", + "selected": false + } + ], + "placeholder": "\"INFER_TOOL_COUNT\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "maxIter" + }, + "defaultValue": "\"INFER_TOOL_COUNT\"" + }, + "verbose": { + "metadata": { + "label": "Verbose", + "description": "Specifies whether verbose logging is enabled" + }, + "types": [ + { + "fieldType": "FLAG", + "ballerinaType": "boolean", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": false + } + ], + "placeholder": "false", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "verbose" + }, + "defaultValue": "false" + }, + "memory": { + "metadata": { + "label": "Memory", + "description": "The memory used by the agent to store and manage conversation history.\nDefaults to use an in-memory message store that trims on overflow, if unspecified." + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:Memory?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "memory" + }, + "defaultValue": "()" + }, + "toolLoadingStrategy": { + "metadata": { + "label": "Tool Loading Strategy", + "description": "Defines the strategies for loading tool schemas into an Agent. \nBy default, all tools are loaded without any filtering." + }, + "types": [ + { + "fieldType": "SINGLE_SELECT", + "options": [ + { + "label": "LLM_FILTER", + "value": "\"LLM_FILTER\"" + }, + { + "label": "NO_FILTER", + "value": "\"NO_FILTER\"" + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:ToolLoadingStrategy", + "selected": false + } + ], + "placeholder": "\"LLM_FILTER\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "toolLoadingStrategy" + }, + "defaultValue": "\"LLM_FILTER\"" + }, + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Terminate on error" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": false, + "advanced": true, + "hidden": true + }, + "scope": { + "metadata": { + "label": "Connection Scope", + "description": "Scope of the connection, Global or Local" + }, + "types": [ + { + "fieldType": "ENUM", + "selected": true + } + ], + "value": "Global", + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "variable": { + "metadata": { + "label": "Variable Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "aiAgent", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "agents.bal", + "startLine": { + "line": 4, + "offset": 15 + }, + "endLine": { + "line": 4, + "offset": 22 + } + } + } + }, + "type": { + "metadata": { + "label": "Variable Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "ai:Agent", + "placeholder": "var", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": {} + } + }, + "flags": 1 + }, + { + "id": "38752", + "metadata": { + "label": "Agent", + "description": "Represents an agent.", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_ai_1.9.0.png" + }, + "codedata": { + "node": "AGENT", + "org": "ballerina", + "module": "ai", + "object": "Agent", + "symbol": "init", + "lineRange": { + "fileName": "agents.bal", + "startLine": { + "line": 7, + "offset": 0 + }, + "endLine": { + "line": 9, + "offset": 2 + } + }, + "sourceCode": "final ai:Agent aiAgentResult = check new (\n systemPrompt = {role: string `sample role 2`, instructions: string `sample instruction 2`}, model = aiWso2modelproviderResult, tools = [createEventTool, getRandomName, aiMcpbasetoolkit]\n);" + }, + "returning": false, + "properties": { + "systemPrompt": { + "metadata": { + "label": "System Prompt", + "description": "The system prompt assigned to the agent" + }, + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "ai:SystemPrompt", + "typeMembers": [ + { + "type": "SystemPrompt", + "packageInfo": "ballerina:ai:1.9.0", + "packageName": "ai", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:SystemPrompt", + "selected": false + } + ], + "value": "{role: string `sample role 2`, instructions: string `sample instruction 2`}", + "placeholder": "{role: \"\", instructions: \"\"}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "systemPrompt" + }, + "defaultValue": "{role: \"\", instructions: \"\"}" + }, + "model": { + "metadata": { + "label": "Model", + "description": "The model used by the agent" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:ModelProvider", + "selected": true + } + ], + "value": "aiWso2modelproviderResult", + "placeholder": "object {}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "model" + }, + "defaultValue": "object {}" + }, + "tools": { + "metadata": { + "label": "Tools", + "description": "The tools available for the agent" + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "(ai:BaseToolKit|ai:ToolConfig|ai:FunctionTool)[]", + "selected": true + } + ], + "value": "[createEventTool, getRandomName, aiMcpbasetoolkit]", + "placeholder": "[]", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "tools" + }, + "defaultValue": "[]" + }, + "maxIter": { + "metadata": { + "label": "Maximum Iterations", + "description": "The maximum number of iterations the agent performs to complete the task.\nBy default, it is set to the number of tools + 1." + }, + "types": [ + { + "fieldType": "NUMBER", + "ballerinaType": "int", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "\"INFER_TOOL_COUNT\"|int", + "selected": false + } + ], + "placeholder": "\"INFER_TOOL_COUNT\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "maxIter" + }, + "defaultValue": "\"INFER_TOOL_COUNT\"" + }, + "verbose": { + "metadata": { + "label": "Verbose", + "description": "Specifies whether verbose logging is enabled" + }, + "types": [ + { + "fieldType": "FLAG", + "ballerinaType": "boolean", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "boolean", + "selected": false + } + ], + "placeholder": "false", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "verbose" + }, + "defaultValue": "false" + }, + "memory": { + "metadata": { + "label": "Memory", + "description": "The memory used by the agent to store and manage conversation history.\nDefaults to use an in-memory message store that trims on overflow, if unspecified." + }, + "types": [ + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:Memory?", + "selected": false + } + ], + "placeholder": "()", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "memory" + }, + "defaultValue": "()" + }, + "toolLoadingStrategy": { + "metadata": { + "label": "Tool Loading Strategy", + "description": "Defines the strategies for loading tool schemas into an Agent. \nBy default, all tools are loaded without any filtering." + }, + "types": [ + { + "fieldType": "SINGLE_SELECT", + "options": [ + { + "label": "LLM_FILTER", + "value": "\"LLM_FILTER\"" + }, + { + "label": "NO_FILTER", + "value": "\"NO_FILTER\"" + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "ai:ToolLoadingStrategy", + "selected": false + } + ], + "placeholder": "\"LLM_FILTER\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "INCLUDED_FIELD", + "originalName": "toolLoadingStrategy" + }, + "defaultValue": "\"LLM_FILTER\"" + }, + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Terminate on error" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": false, + "advanced": true, + "hidden": true + }, + "scope": { + "metadata": { + "label": "Connection Scope", + "description": "Scope of the connection, Global or Local" + }, + "types": [ + { + "fieldType": "ENUM", + "selected": true + } + ], + "value": "Global", + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "variable": { + "metadata": { + "label": "Variable Name", + "description": "Name of the variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "aiAgentResult", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "agents.bal", + "startLine": { + "line": 7, + "offset": 15 + }, + "endLine": { + "line": 7, + "offset": 28 + } + } + } + }, + "type": { + "metadata": { + "label": "Variable Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "ai:Agent", + "placeholder": "var", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": {} + } + }, + "flags": 1 + }, + { + "id": "35796", + "metadata": { + "label": "getDefaultModelProvider", + "description": "Creates a default model provider based on the provided `wso2ProviderConfig`.", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_ai_1.9.0.png" + }, + "codedata": { + "node": "FUNCTION_CALL", + "org": "ballerina", + "module": "ai", + "packageName": "ai", + "symbol": "getDefaultModelProvider", + "version": "1.9.0", + "lineRange": { + "fileName": "connections.bal", + "startLine": { + "line": 4, + "offset": 0 + }, + "endLine": { + "line": 4, + "offset": 84 + } + }, + "sourceCode": "final ai:Wso2ModelProvider aiWso2modelprovider = check ai:getDefaultModelProvider();" + }, + "returning": false, + "properties": { + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Trigger error flow" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "variable": { + "metadata": { + "label": "Result", + "description": "Name of the result variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "aiWso2modelprovider", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "connections.bal", + "startLine": { + "line": 4, + "offset": 27 + }, + "endLine": { + "line": 4, + "offset": 46 + } + } + } + }, + "type": { + "metadata": { + "label": "Result Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "ai:Wso2ModelProvider", + "placeholder": "var", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": {} + } + }, + "flags": 1 + }, + { + "id": "36794", + "metadata": { + "label": "getDefaultModelProvider", + "description": "Creates a default model provider based on the provided `wso2ProviderConfig`.", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_ai_1.9.0.png" + }, + "codedata": { + "node": "FUNCTION_CALL", + "org": "ballerina", + "module": "ai", + "packageName": "ai", + "symbol": "getDefaultModelProvider", + "version": "1.9.0", + "lineRange": { + "fileName": "connections.bal", + "startLine": { + "line": 5, + "offset": 0 + }, + "endLine": { + "line": 5, + "offset": 90 + } + }, + "sourceCode": "final ai:Wso2ModelProvider aiWso2modelproviderResult = check ai:getDefaultModelProvider();" + }, + "returning": false, + "properties": { + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Trigger error flow" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "variable": { + "metadata": { + "label": "Result", + "description": "Name of the result variable" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "aiWso2modelproviderResult", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "connections.bal", + "startLine": { + "line": 5, + "offset": 27 + }, + "endLine": { + "line": 5, + "offset": 52 + } + } + } + }, + "type": { + "metadata": { + "label": "Result Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "ai:Wso2ModelProvider", + "placeholder": "var", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": {} + } + }, + "flags": 1 + }, + { + "id": "34793", + "metadata": { + "label": "Gcalendar", + "description": "Manipulates events and other calendar data.", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerinax_googleapis.gcalendar_4.0.1.png" + }, + "codedata": { + "node": "NEW_CONNECTION", + "org": "ballerinax", + "module": "googleapis.gcalendar", + "object": "Client", + "symbol": "init", + "lineRange": { + "fileName": "connections.bal", + "startLine": { + "line": 3, + "offset": 0 + }, + "endLine": { + "line": 3, + "offset": 73 + } + }, + "sourceCode": "final gcalendar:Client gcalendarClient = check new ({auth: {token: \"\"}});" + }, + "returning": false, + "properties": { + "config": { + "metadata": { + "label": "Config", + "description": "The configurations to be used when initializing the `connector` " + }, + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "gcalendar:ConnectionConfig", + "typeMembers": [ + { + "type": "ConnectionConfig", + "packageInfo": "ballerinax:googleapis.gcalendar:4.0.1", + "packageName": "googleapis.gcalendar", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "gcalendar:ConnectionConfig", + "selected": false + } + ], + "value": "{auth: {token: \"\"}}", + "placeholder": "{auth: {token: \"\"}}", + "optional": false, + "editable": true, + "advanced": false, + "hidden": false, + "codedata": { + "kind": "REQUIRED", + "originalName": "config" + } + }, + "serviceUrl": { + "metadata": { + "label": "Service Url", + "description": "URL of the target service " + }, + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": false + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "placeholder": "\"\"", + "optional": true, + "editable": true, + "advanced": true, + "hidden": false, + "codedata": { + "kind": "DEFAULTABLE", + "originalName": "serviceUrl" + }, + "defaultValue": "\"\"" + }, + "checkError": { + "metadata": { + "label": "Check Error", + "description": "Terminate on error" + }, + "types": [ + { + "fieldType": "FLAG", + "selected": true + } + ], + "value": true, + "optional": false, + "editable": false, + "advanced": true, + "hidden": true + }, + "scope": { + "metadata": { + "label": "Connection Scope", + "description": "Scope of the connection, Global or Local" + }, + "types": [ + { + "fieldType": "ENUM", + "selected": true + } + ], + "value": "Global", + "optional": false, + "editable": true, + "advanced": true, + "hidden": true + }, + "variable": { + "metadata": { + "label": "Connection Name", + "description": "Name of the connection" + }, + "types": [ + { + "fieldType": "IDENTIFIER", + "selected": true + } + ], + "value": "gcalendarClient", + "optional": false, + "editable": false, + "advanced": false, + "hidden": false, + "codedata": { + "lineRange": { + "fileName": "connections.bal", + "startLine": { + "line": 3, + "offset": 23 + }, + "endLine": { + "line": 3, + "offset": 38 + } + } + } + }, + "type": { + "metadata": { + "label": "Connection Type", + "description": "Type of the variable" + }, + "types": [ + { + "fieldType": "TYPE", + "selected": true + } + ], + "value": "gcalendar:Client", + "placeholder": "var", + "optional": false, + "editable": false, + "advanced": false, + "hidden": true, + "codedata": {} + } + }, + "flags": 1 + } + ] +} diff --git a/workspaces/ballerina/bi-diagram/src/test/Diagram.test.tsx b/workspaces/ballerina/bi-diagram/src/test/Diagram.test.tsx index 0fa4666f34..117497bc27 100644 --- a/workspaces/ballerina/bi-diagram/src/test/Diagram.test.tsx +++ b/workspaces/ballerina/bi-diagram/src/test/Diagram.test.tsx @@ -30,8 +30,77 @@ import model3 from "../stories/3-suggestions.json"; import model4 from "../stories/4-with-diagnostics.json"; import model5 from "../stories/5-complex-1.json"; import model6 from "../stories/6-ai-agent.json"; +import model7 from "../stories/7-all-nodes.json"; -async function renderAndCheckSnapshot(model: Flow, testName: string) { +// --- Emotion Style Snapshot Helpers --- + +/** + * Extract Emotion CSS rules from