diff --git a/.gitignore b/.gitignore index f2518547c7b..9a1eb305707 100644 --- a/.gitignore +++ b/.gitignore @@ -83,5 +83,7 @@ package.json.backup pnpm-lock.yaml +TODO.md + # AI evaluation results **/results diff --git a/.vscode/launch.json b/.vscode/launch.json index 2f9a4a62441..54837af4444 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -43,6 +43,34 @@ ], "preLaunchTask": "watch-all", "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-all", + "envFile": "${workspaceFolder}/workspaces/ballerina/ballerina-extension/.env" }, { "name": "BI Extension", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 369f3fa849a..49c1155c0fb 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -391,6 +391,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 @@ -578,6 +581,9 @@ importers: zod: specifier: ^4.1.8 version: 4.1.11 + zustand: + specifier: ^5.0.5 + version: 5.0.9(@types/react@18.2.0)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)) devDependencies: '@sentry/webpack-plugin': specifier: ^1.20.1 @@ -585,6 +591,9 @@ importers: '@types/chai': specifier: ^4.3.9 version: 4.3.20 + '@types/js-yaml': + specifier: ~4.0.9 + version: 4.0.9 '@types/mocha': specifier: ~10.0.3 version: 10.0.10 @@ -672,9 +681,9 @@ importers: webpack-cli: specifier: ^6.0.1 version: 6.0.1(webpack@5.104.1) - yarn: - specifier: ^1.22.19 - version: 1.22.22 + webpack-merge-and-include-globally: + specifier: ^2.3.4 + version: 2.3.4(webpack@5.104.1) ../../workspaces/ballerina/ballerina-low-code-diagram: dependencies: @@ -931,6 +940,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 @@ -1025,6 +1037,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 @@ -1125,6 +1140,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.90.10(@tanstack/react-query@5.77.1(react@18.2.0))(react@18.2.0) '@types/lodash': specifier: ~4.17.16 version: 4.17.17 @@ -1188,6 +1206,9 @@ importers: highlight.js: specifier: ^11.11.1 version: 11.11.1 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 lodash: specifier: 4.17.23 version: 4.17.23 @@ -1221,6 +1242,9 @@ importers: remark-breaks: specifier: ~4.0.0 version: 4.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 @@ -1228,6 +1252,9 @@ importers: specifier: ~1.6.1 version: 1.6.1 devDependencies: + '@types/js-yaml': + specifier: ^4.0.5 + version: 4.0.9 '@types/lodash.debounce': specifier: ^4.0.6 version: 4.0.9 @@ -1246,6 +1273,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 @@ -9719,14 +9749,26 @@ packages: '@tanstack/query-core@5.90.17': resolution: {integrity: sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ==} + '@tanstack/query-core@5.90.8': + resolution: {integrity: sha512-4E0RP/0GJCxSNiRF2kAqE/LQkTJVlL/QNU7gIJSptaseV9HP6kOuA+N11y4bZKZxa3QopK3ZuewwutHx6DqDXQ==} + '@tanstack/query-persist-client-core@4.27.0': resolution: {integrity: sha512-A+dPA7zG0MJOMDeBc/2WcKXW4wV2JMkeBVydobPW9G02M4q0yAj7vI+7SmM2dFuXyIvxXp4KulCywN6abRKDSQ==} + '@tanstack/query-persist-client-core@5.91.7': + resolution: {integrity: sha512-MKJmuHl4LIl3Zs91fL9CQx1XJ8//1bnq8keY5V+cv3SLkPM6Wqx/HYFBcevfFFrzo9TDHob9ivuM4UOGTyQ7Mg==} + '@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.90.10': + resolution: {integrity: sha512-xLAB3W01LGGWLPFD9dKNcl0dWyBU6mPJGyZt6SbkwyuCgHSaKz8sGQ83ryWfn1u/h3nkCOBSXnOTFCLYtMvWeg==} + peerDependencies: + '@tanstack/react-query': ^5.90.8 + react: ^18 || ^19 + '@tanstack/react-query@4.0.10': resolution: {integrity: sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==} peerDependencies: @@ -13900,6 +13942,9 @@ packages: es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + es6-promisify@6.1.1: + resolution: {integrity: sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==} + es6-shim@0.35.8: resolution: {integrity: sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==} @@ -20349,6 +20394,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rev-hash@3.0.0: + resolution: {integrity: sha512-s+87HfEKAu95TaTxnbCobn0/BkbzR23LHSwVdYvr8mn5+PPjzy+hTWyh92b5oaLgig9TKPe5d6ZcubsVBtUrZg==} + engines: {node: '>=8'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -22914,6 +22963,11 @@ packages: peerDependencies: webpack: 1 || 2 || 3 + webpack-merge-and-include-globally@2.3.4: + resolution: {integrity: sha512-s5dd7m3ycVBlC7C6GAx91JQzbjhxC/NJRuT2sCkg8WCcF8CE1x/7xwVXqgmt0Fr/H/0sX5C5HE2RdU6+vCY5yg==} + peerDependencies: + webpack: '>=1.0.0' + webpack-merge@5.10.0: resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} engines: {node: '>=10.0.0'} @@ -23292,11 +23346,6 @@ packages: yargs@7.1.2: resolution: {integrity: sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==} - yarn@1.22.22: - resolution: {integrity: sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==} - engines: {node: '>=4.0.0'} - hasBin: true - yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} @@ -35532,15 +35581,27 @@ snapshots: '@tanstack/query-core@5.90.17': {} + '@tanstack/query-core@5.90.8': {} + '@tanstack/query-persist-client-core@4.27.0': dependencies: '@tanstack/query-core': 4.27.0 + '@tanstack/query-persist-client-core@5.91.7': + dependencies: + '@tanstack/query-core': 5.90.8 + '@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.90.10(@tanstack/react-query@5.77.1(react@18.2.0))(react@18.2.0)': + dependencies: + '@tanstack/query-persist-client-core': 5.91.7 + '@tanstack/react-query': 5.77.1(react@18.2.0) + react: 18.2.0 + '@tanstack/react-query@4.0.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@tanstack/query-core': 4.41.0 @@ -40904,6 +40965,8 @@ snapshots: es6-promise@4.2.8: {} + es6-promisify@6.1.1: {} + es6-shim@0.35.8: {} es6-symbol@3.1.4: @@ -44650,10 +44713,7 @@ snapshots: pretty-format: 25.5.0 throat: 5.0.0 transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - utf-8-validate jest-leak-detector@25.5.0: dependencies: @@ -50003,6 +50063,8 @@ snapshots: reusify@1.1.0: {} + rev-hash@3.0.0: {} + rfdc@1.4.1: {} rimraf@2.6.3: @@ -53615,6 +53677,13 @@ snapshots: lodash: 4.17.23 webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(webpack-cli@6.0.1) + webpack-merge-and-include-globally@2.3.4(webpack@5.104.1): + dependencies: + es6-promisify: 6.1.1 + glob: 7.2.3 + rev-hash: 3.0.0 + webpack: 5.104.1(webpack-cli@6.0.1) + webpack-merge@5.10.0: dependencies: clone-deep: 4.0.1 @@ -54280,8 +54349,6 @@ snapshots: y18n: 3.2.2 yargs-parser: 5.0.1 - yarn@1.22.22: {} - yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13 diff --git a/workspaces/ballerina/ballerina-core/package.json b/workspaces/ballerina/ballerina-core/package.json index bdec8603660..84e607f3407 100644 --- a/workspaces/ballerina/ballerina-core/package.json +++ b/workspaces/ballerina/ballerina-core/package.json @@ -27,7 +27,8 @@ "tree-kill": "^1.2.2", "vscode-uri": "^3.0.8", "@types/mousetrap": "~1.6.11", - "@types/ws": "^8.2.1" + "@types/ws": "^8.2.1", + "@wso2/wso2-platform-core": "workspace:*" }, "devDependencies": { "@types/node": "^22.15.21", diff --git a/workspaces/ballerina/ballerina-core/src/index.ts b/workspaces/ballerina/ballerina-core/src/index.ts index df051da1cae..78ad207855a 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/extended-lang-client.ts b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts index 4cd2cfe64c1..8d423c7cd55 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts @@ -866,6 +866,7 @@ export type BISourceCodeResponse = { export type BIDeleteByComponentInfoRequest = { filePath: string; component: ComponentInfo; + nodeType?: string; } export type BIDeleteByComponentInfoResponse = { @@ -1858,17 +1859,6 @@ export type OpenAPIClientDeleteResponse = { deleteData: OpenAPIClientDeleteData } -// <-------- Deployment Related -------> - -export interface DeploymentRequest { - integrationTypes: SCOPE[]; -} - -export interface DeploymentResponse { - isCompleted: boolean; -} - - // 2201.12.3 -> New Project Component Artifacts Tree export interface BaseArtifact { 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 4e452c463ae..1057386562f 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 @@ -72,8 +72,6 @@ import { GetRecordModelFromSourceRequest, UpdateTypesRequest, UpdateTypesResponse, - DeploymentRequest, - DeploymentResponse, OpenAPIClientGenerationRequest, OpenAPIGeneratedModulesRequest, OpenAPIGeneratedModulesResponse, @@ -117,7 +115,6 @@ import { EndOfFileRequest, RecordsInWorkspaceMentions, BuildMode, - DevantMetadata, GeneratedClientSaveResponse, AddProjectToWorkspaceRequest, DeleteProjectRequest, @@ -158,7 +155,6 @@ export interface BIDiagramAPI { getReadmeContent: (params: ReadmeContentRequest) => Promise; openReadme: (params: OpenReadmeRequest) => void; renameIdentifier: (params: RenameIdentifierRequest) => Promise; - deployProject: (params: DeploymentRequest) => Promise; openAIChat: (params: AIChatRequest) => void; getSignatureHelp: (params: SignatureHelpRequest) => Promise; buildProject: (mode: BuildMode) => void; @@ -196,8 +192,7 @@ export interface BIDiagramAPI { searchNodes: (params: BISearchNodesRequest) => Promise; getRecordNames: () => Promise; getFunctionNames: () => Promise; - getDevantMetadata: () => Promise; - generateOpenApiClient: (params: OpenAPIClientGenerationRequest) => Promise; + generateOpenApiClient: (params: OpenAPIClientGenerationRequest) => Promise;// getOpenApiGeneratedModules: (params: OpenAPIGeneratedModulesRequest) => Promise; deleteOpenApiGeneratedModules: (params: OpenAPIClientDeleteRequest) => Promise; OpenConfigTomlRequest: (params: OpenConfigTomlRequest) => 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 104f7b71d08..e0cc0450af0 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 @@ -186,12 +186,6 @@ export enum BuildMode { DOCKER = "docker" } -export interface DevantMetadata { - isLoggedIn?: boolean; - 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 1da53727308..845ba3ad569 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 @@ -73,8 +73,6 @@ import { GetRecordModelFromSourceRequest, UpdateTypesRequest, UpdateTypesResponse, - DeploymentRequest, - DeploymentResponse, OpenAPIClientGenerationRequest, OpenAPIGeneratedModulesRequest, OpenAPIGeneratedModulesResponse, @@ -119,7 +117,6 @@ import { EndOfFileRequest, RecordsInWorkspaceMentions, BuildMode, - DevantMetadata, GeneratedClientSaveResponse, AddProjectToWorkspaceRequest, DeleteProjectRequest, @@ -161,7 +158,6 @@ export const getModuleNodes: RequestType = { method export const getReadmeContent: RequestType = { method: `${_preFix}/getReadmeContent` }; export const openReadme: NotificationType = { method: `${_preFix}/openReadme` }; export const renameIdentifier: NotificationType = { method: `${_preFix}/renameIdentifier` }; -export const deployProject: RequestType = { method: `${_preFix}/deployProject` }; export const openAIChat: NotificationType = { method: `${_preFix}/openAIChat` }; export const getSignatureHelp: RequestType = { method: `${_preFix}/getSignatureHelp` }; export const buildProject: NotificationType = { method: `${_preFix}/buildProject` }; @@ -199,7 +195,6 @@ export const search: RequestType = { method: export const searchNodes: RequestType = { method: `${_preFix}/searchNodes` }; export const getRecordNames: RequestType = { method: `${_preFix}/getRecordNames` }; export const getFunctionNames: RequestType = { method: `${_preFix}/getFunctionNames` }; -export const getDevantMetadata: RequestType = { method: `${_preFix}/getDevantMetadata` }; 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 a98708821c9..4db0d951b11 100644 --- a/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/common/index.ts @@ -33,7 +33,10 @@ import { WorkspaceRootResponse, ShowErrorMessageRequest, WorkspaceTypeResponse, - SampleDownloadRequest + SetWebviewCacheRequestParam, + ShowInfoModalRequest, + SampleDownloadRequest, + ShowQuickPickRequest } from "./interfaces"; export interface CommonRPCAPI { @@ -50,7 +53,12 @@ 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; } 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 680777cceaf..c1831e9e3b6 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 { 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: string[]; + options?: QuickPickOptions; +} + export interface TomlWorkspace { packages: string[]; } @@ -109,12 +120,23 @@ export interface WorkspaceTomlValues { export interface PackageTomlValues { package: TomlPackage; + tool?: { + openapi?: { + id: string; + targetModule: string; + filePath: 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; } 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 9b7379b10d7..f34c642dcdb 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 @@ -34,7 +34,10 @@ import { WorkspaceRootResponse, ShowErrorMessageRequest, WorkspaceTypeResponse, - SampleDownloadRequest + SetWebviewCacheRequestParam, + ShowInfoModalRequest, + SampleDownloadRequest, + ShowQuickPickRequest } from "./interfaces"; import { RequestType, NotificationType } from "vscode-messenger-common"; @@ -52,6 +55,11 @@ 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` }; 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 00000000000..74898278848 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/index.ts @@ -0,0 +1,49 @@ +/** + * 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 { GetMarketplaceListReq,MarketplaceListResp, GetMarketplaceIdlReq, MarketplaceIdlResp, ConnectionListItem, GetConnectionsReq, DeleteLocalConnectionsConfigReq, GetMarketplaceItemReq, MarketplaceItem, GetConnectionItemReq, ConnectionDetailed, RegisterMarketplaceConnectionReq, CreateLocalConnectionsConfigReq } from "@wso2/wso2-platform-core" +import { CreateDevantConnectionResp, CreateDevantConnectionV2Req, DeleteDevantTempConfigReq, GenerateCustomConnectorFromOASReq, GenerateCustomConnectorFromOASResp, ImportDevantConnectionReq, ImportDevantConnectionResp, RegisterAndCreateDevantConnectionReq, AddDevantTempConfigReq, AddDevantTempConfigResp, ReplaceDevantTempConfigValuesReq } 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 + createDevantComponentConnectionV2: (params: CreateDevantConnectionV2Req) => Promise + importDevantComponentConnection: (params: ImportDevantConnectionReq) => Promise + registerAndCreateDevantComponentConnection: (params: RegisterAndCreateDevantConnectionReq) => Promise + addDevantTempConfig: (params: AddDevantTempConfigReq) => Promise + deleteDevantTempConfigs: (params: DeleteDevantTempConfigReq) => Promise + replaceDevantTempConfigValues: (params: ReplaceDevantTempConfigValuesReq) => Promise + // Platform ext proxies + getMarketplaceItems: (params: GetMarketplaceListReq) => Promise; + getMarketplaceItem: (params: GetMarketplaceItemReq) => Promise; + getMarketplaceIdl: (params: GetMarketplaceIdlReq) => Promise; + getConnections: (params: GetConnectionsReq) => Promise; + getConnection: (params: GetConnectionItemReq) => Promise; + deleteLocalConnectionsConfig: (params: DeleteLocalConnectionsConfigReq) => void; + getDevantConsoleUrl: () => Promise; + refreshConnectionList: () => Promise; + setConnectedToDevant: (connected: boolean) => void; + setSelectedComponent: (componentId: string) => void; + setSelectedEnv: (envId: string) => void; + deployIntegrationInDevant: () => void; + registerMarketplaceConnection: (params: RegisterMarketplaceConnectionReq) => Promise; + createConnectionConfig: (params: CreateLocalConnectionsConfigReq) => Promise; +} \ No newline at end of file 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 00000000000..58103f9ec73 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/interfaces.ts @@ -0,0 +1,141 @@ +/** + * 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 { ComponentKind, ConnectionDetailed, ConnectionListItem, ContextItemEnriched, Environment, MarketplaceIdlTypes, MarketplaceItem, MarketplaceServiceTypes, UserInfo } from "@wso2/wso2-platform-core"; +import { AvailableNode, NodePosition } from "../../interfaces/bi"; +import { ModuleVarDecl } from "@wso2/syntax-tree/lib/syntax-tree-interfaces"; + + +export interface GenerateCustomConnectorFromOASReq { + connectionName: string; + marketplaceItem: MarketplaceItem; +} + +export interface GenerateCustomConnectorFromOASResp { + connectionNode?: AvailableNode; +} + +export interface CreateDevantConnectionV2Req { + flow: DevantConnectionFlow; + createInternalConnectionParams?: { + name: string; + visibility: string; + schemaId: string; + isProjectLevel?: boolean; + devantTempConfigs?: DevantTempConfig[]; + } + // todo: rename as createThirdPartyConnectionParams + importThirdPartyConnectionParams?: { + name: string; + schemaId: string; + isProjectLevel?: boolean; + devantTempConfigs?: DevantTempConfig[]; + } + importInternalConnectionParams?: { + connection: ConnectionDetailed; + } + marketplaceItem: MarketplaceItem; +} + +export interface ImportDevantConnectionReq { + connectionListItem: ConnectionListItem; +} + +export interface RegisterAndCreateDevantConnectionReq { + 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 CreateDevantConnectionResp { + connectionName?: string; + connectionNode?: AvailableNode; +} + +export interface ImportDevantConnectionResp { + connectionName?: string; + connectionNode?: AvailableNode; +} + +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; +} \ No newline at end of file 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 00000000000..eaf733aa4d4 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/rpc-type.ts @@ -0,0 +1,51 @@ +/** + * 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 { ConnectionDetailed, ConnectionListItem, CreateLocalConnectionsConfigReq, DeleteLocalConnectionsConfigReq, GetConnectionItemReq, GetConnectionsReq, GetMarketplaceIdlReq, GetMarketplaceItemReq, GetMarketplaceListReq,MarketplaceIdlResp,MarketplaceItem,MarketplaceListResp, RegisterMarketplaceConnectionReq } from "@wso2/wso2-platform-core" +import { NotificationType, RequestType } from "vscode-messenger-common"; +import { AddDevantTempConfigReq, AddDevantTempConfigResp, CreateDevantConnectionResp, CreateDevantConnectionV2Req, DeleteDevantTempConfigReq, GenerateCustomConnectorFromOASReq, GenerateCustomConnectorFromOASResp, ImportDevantConnectionReq, ImportDevantConnectionResp, PlatformExtState, RegisterAndCreateDevantConnectionReq, ReplaceDevantTempConfigValuesReq } from "./interfaces"; + +const _preFix = "platform-ext"; +// BI ext handlers +export const generateCustomConnectorFromOAS: RequestType = { method: `${_preFix}/generateCustomConnectorFromOAS` }; +export const createDevantComponentConnectionV2: RequestType = { method: `${_preFix}/createDevantComponentConnectionV2` }; +export const importDevantComponentConnection: RequestType = { method: `${_preFix}/importDevantComponentConnection` }; +export const registerAndCreateDevantComponentConnection: RequestType = { method: `${_preFix}/registerAndCreateDevantComponentConnection` }; +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 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 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 registerMarketplaceConnection: RequestType = { method: `${_preFix}/registerMarketplaceConnection` }; +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 00000000000..744e1111810 --- /dev/null +++ b/workspaces/ballerina/ballerina-core/src/rpc-types/platform-ext/utils.ts @@ -0,0 +1,18 @@ +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-extension/package.json b/workspaces/ballerina/ballerina-extension/package.json index 05f8c580398..d9285e5c732 100644 --- a/workspaces/ballerina/ballerina-extension/package.json +++ b/workspaces/ballerina/ballerina-extension/package.json @@ -329,6 +329,9 @@ ], "default": "integrated", "description": "Indicates the terminal kind to launch the debugging process in." + }, + "choreoConnect": { + "description": "Connect with Choreo/Devant when launching the app" } } }, @@ -1260,6 +1263,7 @@ "vscode-uri": "^3.0.8", "xml-js": "^1.6.11", "xstate": "^4.38.3", + "zustand": "^5.0.5", "zod": "^4.1.8" }, "devDependencies": { @@ -1296,7 +1300,8 @@ "vscode-messenger": "^0.4.4", "webpack": "^5.89.0", "webpack-cli": "^6.0.1", - "yarn": "^1.22.19" + "webpack-merge-and-include-globally": "^2.3.4", + "@types/js-yaml": "~4.0.9" }, "extensionPack": [ "be5invis.toml" diff --git a/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts b/workspaces/ballerina/ballerina-extension/src/RPCLayer.ts index a43b3b1cfeb..22bdc2bba11 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/features/debugger/config-provider.ts b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts index 44b15bd0046..fbe1b8472e6 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 29450ad8354..a79a8a70bfb 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/devant/activator.ts @@ -20,7 +20,7 @@ import { BI_COMMANDS, DIRECTORY_MAP, EVENT_TYPE, MACHINE_VIEW, SCOPE, findScopeB import { CommandIds as PlatformCommandIds, IWso2PlatformExtensionAPI, - ICommitAndPuhCmdParams, + ICommitAndPushCmdParams, ICreateComponentCmdParams, } from "@wso2/wso2-platform-core"; import { BallerinaExtension } from "../../core"; @@ -58,7 +58,7 @@ const handleComponentPushToDevant = async () => { // 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 +69,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) { 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 bdd91ec0b50..f05364049ba 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 @@ -55,8 +55,6 @@ import { DeleteProjectRequest, deleteType, DeleteTypeRequest, - DeploymentRequest, - deployProject, EndOfFileRequest, ExpressionCompletionsRequest, ExpressionDiagnosticsRequest, @@ -82,7 +80,6 @@ import { getConfigVariablesV2, getDataMapperCompletions, getDesignModel, - getDevantMetadata, getEnclosedFunction, getEndOfFile, getExpressionCompletions, @@ -192,7 +189,6 @@ export function registerBiDiagramRpcHandlers(messenger: Messenger) { messenger.onRequest(getReadmeContent, (args: ReadmeContentRequest) => rpcManger.getReadmeContent(args)); 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.onNotification(openAIChat, (args: AIChatRequest) => rpcManger.openAIChat(args)); messenger.onRequest(getSignatureHelp, (args: SignatureHelpRequest) => rpcManger.getSignatureHelp(args)); messenger.onNotification(buildProject, (args: BuildMode) => rpcManger.buildProject(args)); @@ -231,7 +227,6 @@ export function registerBiDiagramRpcHandlers(messenger: Messenger) { messenger.onRequest(searchNodes, (args: BISearchNodesRequest) => rpcManger.searchNodes(args)); messenger.onRequest(getRecordNames, () => rpcManger.getRecordNames()); messenger.onRequest(getFunctionNames, () => rpcManger.getFunctionNames()); - messenger.onRequest(getDevantMetadata, () => rpcManger.getDevantMetadata()); 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 dca33238b36..d840cabed75 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 @@ -67,9 +67,6 @@ import { DeleteProjectRequest, DeleteTypeRequest, DeleteTypeResponse, - DeploymentRequest, - DeploymentResponse, - DevantMetadata, Diagnostics, EndOfFileRequest, ExpressionCompletionsRequest, @@ -143,13 +140,13 @@ 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 { ShellExecution, Task, @@ -173,10 +170,13 @@ import { README_FILE, addProjectToExistingWorkspace, convertProjectToWorkspace, 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"; export class BiDiagramRpcManager implements BIDiagramAPI { OpenConfigTomlRequest: (params: OpenConfigTomlRequest) => Promise; @@ -1095,36 +1095,6 @@ 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; - } - - if (!integrationType) { - return { isCompleted: true }; - } - - const deployementParams: ICreateComponentCmdParams = { - integrationType: integrationType as any, - buildPackLang: "ballerina", // Example language - name: path.basename(StateMachine.context().projectPath), - componentDir: StateMachine.context().projectPath, - extName: "Devant" - }; - commands.executeCommand(PlatformExtCommandIds.CreateNewComponent, deployementParams); - - return { isCompleted: true }; - } openAIChat(params: AIChatRequest): void { if (params.readme) { @@ -1319,6 +1289,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) => { @@ -1826,40 +1805,6 @@ export class BiDiagramRpcManager implements BIDiagramAPI { return { mentions: recordNames }; } - async getDevantMetadata(): Promise { - let hasContextYaml = false; - let isLoggedIn = false; - let hasComponent = false; - let hasLocalChanges = false; - try { - const projectPath = StateMachine.context().projectPath; - const repoRoot = getRepoRoot(projectPath); - if (repoRoot) { - const contextYamlPath = path.join(repoRoot, ".choreo", "context.yaml"); - if (fs.existsSync(contextYamlPath)) { - hasContextYaml = true; - } - } - - const platformExt = extensions.getExtension("wso2.wso2-platform"); - if (!platformExt) { - return { hasComponent: hasContextYaml, isLoggedIn: false }; - } - const platformExtAPI: IWso2PlatformExtensionAPI = await platformExt.activate(); - hasLocalChanges = await platformExtAPI.localRepoHasChanges(projectPath); - isLoggedIn = platformExtAPI.isLoggedIn(); - if (isLoggedIn) { - const components = platformExtAPI.getDirectoryComponents(projectPath); - hasComponent = components.length > 0; - return { isLoggedIn, hasComponent, hasLocalChanges }; - } - return { isLoggedIn, hasComponent: hasContextYaml, hasLocalChanges }; - } catch (err) { - console.error("failed to call getDevantMetadata: ", err); - return { hasComponent: hasComponent || hasContextYaml, isLoggedIn, hasLocalChanges }; - } - } - async getRecordConfig(params: GetRecordConfigRequest): Promise { return new Promise((resolve, reject) => { StateMachine.langClient().getRecordConfig(params).then((res) => { @@ -1950,14 +1895,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")); @@ -1979,13 +1920,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); - }); + } }); } @@ -2105,19 +2068,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/common/rpc-handler.ts b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/rpc-handler.ts index f618654ce4d..700ceea07f3 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,11 +19,21 @@ */ import { BallerinaDiagnosticsRequest, + ClearWebviewCache, CommandsRequest, + FileOrDirRequest, + GoToSourceRequest, + OpenExternalUrlRequest, + RestoreWebviewCache, + RunExternalCommandRequest, + SetWebviewCache, + SetWebviewCacheRequestParam, + ShowErrorMessageRequest, + showInformationModal, + WorkspaceFileRequest, downloadSelectedSampleFromGithub, executeCommand, experimentalEnabled, - FileOrDirRequest, getBallerinaDiagnostics, getCurrentProjectTomlValues, getTypeCompletions, @@ -31,18 +41,16 @@ import { getWorkspaceRoot, getWorkspaceType, goToSource, - GoToSourceRequest, isNPSupported, openExternalUrl, - OpenExternalUrlRequest, 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 +70,12 @@ 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)); } 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 9dc94adda13..075a160fe89 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 @@ -41,7 +41,10 @@ import { WorkspaceFileRequest, WorkspaceRootResponse, WorkspacesFileResponse, - WorkspaceTypeResponse + WorkspaceTypeResponse, + SetWebviewCacheRequestParam, + ShowInfoModalRequest, + ShowQuickPickRequest, } from "@wso2/ballerina-core"; import child_process from 'child_process'; import path from "path"; @@ -190,6 +193,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,6 +283,14 @@ 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; } @@ -415,4 +448,16 @@ 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); + } } 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 d8505f53153..013a5c9234e 100644 --- a/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/common/utils.ts @@ -18,6 +18,7 @@ import * as os from 'os'; import { NodePosition } from "@wso2/syntax-tree"; +import { StateMachine } from "../../stateMachine"; import { Position, Progress, Range, Uri, window, workspace, WorkspaceEdit } from "vscode"; import { PROJECT_KIND, ProjectInfo, TextEdit, WorkspaceTypeResponse } from "@wso2/ballerina-core"; import axios from 'axios'; @@ -90,7 +91,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'] }, 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 00000000000..d5a7b8bfa58 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/platform-store.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +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"), + ), +); \ No newline at end of file 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 00000000000..4c92beecc71 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/platform-utils.ts @@ -0,0 +1,584 @@ +import { SyntaxTree, PackageTomlValues, AvailableNode } from "@wso2/ballerina-core"; +import { ModulePart, STKindChecker, CaptureBindingPattern, TypeDefinition, RecordField } 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, + ConnectionConfigurations, + ServiceInfoVisibilityEnum, + IWso2PlatformExtensionAPI, +} from "@wso2/wso2-platform-core"; +import { BiDiagramRpcManager } from "../bi-diagram/rpc-manager"; +import { CommonRpcManager } from "../common/rpc-manager"; +import * as toml from "@iarna/toml"; +import { extension } from "../../BalExtensionContext"; +import { PersistOptions, createJSONStorage } from "zustand/middleware"; +import { platformExtStore } from "./platform-store"; +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 getInjectedEnvVarNames = (key: string): string => { + const parts = key.split("_"); + if (parts.length > 1) { + let lastPart = parts[parts.length - 1]; + if (lastPart.startsWith("CHOREO")) { + lastPart = lastPart.slice("CHOREO".length); + } + parts[parts.length - 1] = lastPart; + } + return parts.join("_"); +}; + +export const initializeDevantConnection = async (params: { + name: string; + visibility: string; + securityType: "" | "oauth" | "apikey"; + marketplaceItem: MarketplaceItem; + configurations: ConnectionConfigurations; + platformExt: IWso2PlatformExtensionAPI; +}): Promise<{ connectionName?: string; connectionNode?: AvailableNode }> => { + const projectPath = StateMachine.context().projectPath; + + + const serviceIdl = await params.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.name.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"); + } else { + // todo: show button to open up devant connection documentation UI + vscode.window.showErrorMessage( + "Client creation for connection is only supported for REST APIs with valid openAPI spec" + ); + return { connectionName: params?.name }; + } + + // Generate Bal client + const diagram = new BiDiagramRpcManager(); + await diagram.generateOpenApiClient({ + module: moduleName, + openApiContractPath: filePath, + projectPath, + }); + + 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 syntaxTree = (await StateMachine.context().langClient.getSyntaxTree({ + documentIdentifier: { uri: configFileUri.toString() }, + })) as SyntaxTree; + for (const entry in connectionKeys) { + let baseName = connectionKeys[entry].key?.toLowerCase(); + 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++; + } + + keys[entry] = { + keyname: candidate, + envName: getInjectedEnvVarNames(connectionKeys[entry].envVariableName), + }; + } + + 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); + } + + if (params.marketplaceItem?.isThirdParty) { + // find the connector node, if its a 3rd party connector + 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 }; + } + } + } + + 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, + }); + + return { connectionName: resp.connName }; +}; + +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; +}; \ No newline at end of file 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 00000000000..85ef2584eba --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/rpc-handler.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getMarketplaceItems, getMarketplaceIdl, getConnections, deleteLocalConnectionsConfig, getDevantConsoleUrl, importDevantComponentConnection, getMarketplaceItem, getConnection, onPlatformExtStoreStateChange, refreshConnectionList, getPlatformStore, setConnectedToDevant, setSelectedComponent, deployIntegrationInDevant, registerMarketplaceConnection, registerAndCreateDevantComponentConnection, deleteDevantTempConfigs, createDevantComponentConnectionV2, generateCustomConnectorFromOAS, addDevantTempConfig, setSelectedEnv, createConnectionConfig, replaceDevantTempConfigValues } from "@wso2/ballerina-core"; +import { Messenger } from "vscode-messenger"; +import { PlatformExtRpcManager } from "./rpc-manager"; +import { CreateLocalConnectionsConfigReq, DeleteLocalConnectionsConfigReq, GetConnectionItemReq, GetConnectionsReq, GetMarketplaceIdlReq, GetMarketplaceItemReq, GetMarketplaceListReq, RegisterMarketplaceConnectionReq, } from "@wso2/wso2-platform-core"; +import { AddDevantTempConfigReq, CreateDevantConnectionV2Req, DeleteDevantTempConfigReq, GenerateCustomConnectorFromOASReq, ImportDevantConnectionReq, RegisterAndCreateDevantConnectionReq, 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}`); + }); + + messenger.onRequest(getPlatformStore, () => platformExtStore.getState().state); + 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(createDevantComponentConnectionV2, (params: CreateDevantConnectionV2Req) => rpcManger.createDevantComponentConnectionV2(params)); + messenger.onRequest(generateCustomConnectorFromOAS, (params: GenerateCustomConnectorFromOASReq) => rpcManger.generateCustomConnectorFromOAS(params)); + messenger.onRequest(importDevantComponentConnection, (params: ImportDevantConnectionReq) => rpcManger.importDevantComponentConnection(params)); + messenger.onRequest(registerAndCreateDevantComponentConnection, (params: RegisterAndCreateDevantConnectionReq) => rpcManger.registerAndCreateDevantComponentConnection(params)); + messenger.onRequest(replaceDevantTempConfigValues, (params: ReplaceDevantTempConfigValuesReq) => rpcManger.replaceDevantTempConfigValues(params)); + messenger.onRequest(addDevantTempConfig, (params: AddDevantTempConfigReq) => rpcManger.addDevantTempConfig(params)); + messenger.onRequest(deleteDevantTempConfigs, (params: DeleteDevantTempConfigReq) => rpcManger.deleteDevantTempConfigs(params)); + messenger.onRequest(registerMarketplaceConnection, (params: RegisterMarketplaceConnectionReq) => rpcManger.registerMarketplaceConnection(params)); + messenger.onRequest(getConnections, (params: GetConnectionsReq) => rpcManger.getConnections(params)); + messenger.onRequest(getConnection, (params: GetConnectionItemReq) => rpcManger.getConnection(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, (componentId: string) => rpcManger.setSelectedComponent(componentId)); + messenger.onRequest(setSelectedEnv, (envId: string) => rpcManger.setSelectedEnv(envId)); + messenger.onRequest(deployIntegrationInDevant, () => rpcManger.deployIntegrationInDevant()); + messenger.onRequest(createConnectionConfig, (params: CreateLocalConnectionsConfigReq) => rpcManger.createConnectionConfig(params)); +} 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 00000000000..63abb25c766 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/rpc-manager.ts @@ -0,0 +1,1215 @@ +/** + * 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 { + onPlatformExtStoreStateChange, + PlatformExtAPI, + SyntaxTree, + PackageTomlValues, + DIRECTORY_MAP, + findDevantScopeByModule, + VisualizerLocation, + AvailableNode, +} from "@wso2/ballerina-core"; +import { extensions, Uri, window, WorkspaceEdit } from "vscode"; +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { + ComponentDisplayType, + ConnectionListItem, + DeleteLocalConnectionsConfigReq, + GetConnectionsReq, + GetMarketplaceIdlReq, + GetMarketplaceItemReq, + GetMarketplaceListReq, + getTypeForDisplayType, + IWso2PlatformExtensionAPI, + MarketplaceIdlResp, + MarketplaceItem, + MarketplaceListResp, + ServiceInfoVisibilityEnum, + GetConnectionItemReq, + StartProxyServerResp, + StopProxyServerReq, + ConnectionDetailed, + CommandIds as PlatformExtCommandIds, + DevantScopes, + ICreateComponentCmdParams, + ComponentKind, + ICmdParamsBase, + ConnectionConfigurations, + RegisterMarketplaceConnectionReq, + RegisterMarketplaceConfigMap, + Project, + Organization, + CreateLocalConnectionsConfigReq, +} from "@wso2/wso2-platform-core"; +import { log } from "../../utils/logger"; +import { + AddDevantTempConfigReq, + AddDevantTempConfigResp, + CreateDevantConnectionResp, + CreateDevantConnectionV2Req, + DeleteDevantTempConfigReq, + DevantConnectionFlow, + DevantTempConfig, + GenerateCustomConnectorFromOASReq, + GenerateCustomConnectorFromOASResp, + ImportDevantConnectionReq, + ImportDevantConnectionResp, + RegisterAndCreateDevantConnectionReq, + ReplaceDevantTempConfigValuesReq, +} from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import * as toml from "@iarna/toml"; +import { StateMachine } from "../../stateMachine"; +import { CommonRpcManager } from "../common/rpc-manager"; +import { CaptureBindingPattern, ModulePart, ModuleVarDecl, STKindChecker } from "@wso2/syntax-tree"; +import { DeleteBiDevantConnectionReq, OpenAPIDefinition } from "./types"; +import { platformExtStore } from "./platform-store"; +import { Messenger } from "vscode-messenger"; +import { VisualizerWebview } from "../../views/visualizer/webview"; +import { + findUniqueConnectionName, + getConfigFileUri, + getDomain, + getInjectedEnvVarNames, + getYamlString, + hasContextYaml, + initializeDevantConnection, + Templates, +} from "./platform-utils"; +import { debounce } from "lodash"; +import { BiDiagramRpcManager } from "../bi-diagram/rpc-manager"; +import { updateSourceCode } from "../../utils"; +import * as yaml from "js-yaml"; + +export class PlatformExtRpcManager implements PlatformExtAPI { + static platformExtAPI: IWso2PlatformExtensionAPI; + private async getPlatformExt() { + if (PlatformExtRpcManager.platformExtAPI) { + return PlatformExtRpcManager.platformExtAPI; + } + const platformExt = extensions.getExtension("wso2.wso2-platform"); + if (!platformExt) { + throw new Error("platform ext not installed"); + } + if (!platformExt.isActive) { + await platformExt.activate(); + } + const platformExtAPI: IWso2PlatformExtensionAPI = platformExt.exports; + 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); + } + + // todo: check and delete unused rpc functions + 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 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 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 registerMarketplaceConnection(params: RegisterMarketplaceConnectionReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + return platformExt?.registerMarketplaceConnection(params); + } catch (err) { + log(`Failed to register create marketplace connection: ${err}`); + } + } + + 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"); + log(`Failed to invoke deleteDevantConnection: ${err}`); + } + } + + async importDevantComponentConnection(params: ImportDevantConnectionReq): Promise { + try { + const platformExt = await this.getPlatformExt(); + StateMachine.setEditMode(); + + let visibility: ServiceInfoVisibilityEnum = ServiceInfoVisibilityEnum.Public; + if (params.connectionListItem?.schemaName?.toLowerCase()?.includes("organization")) { + visibility = ServiceInfoVisibilityEnum.Organization; + } else if (params.connectionListItem?.schemaName?.toLowerCase()?.includes("project")) { + visibility = ServiceInfoVisibilityEnum.Project; + } + + const connectionItem = await this.getConnection({ + orgId: platformExtStore.getState().state?.selectedContext?.org?.id?.toString(), + connectionGroupId: params.connectionListItem?.groupUuid, + }); + + const marketplaceItem = await this.getMarketplaceItem({ + orgId: platformExtStore.getState().state?.selectedContext?.org?.id?.toString(), + serviceId: params?.connectionListItem?.serviceId, + }); + + let securityType: "" | "oauth" | "apikey"; + if (marketplaceItem?.isThirdParty) { + securityType = ""; + } else { + securityType = params.connectionListItem?.schemaName?.toLowerCase()?.includes("oauth") + ? "oauth" + : "apikey"; + } + + const resp = await initializeDevantConnection({ + platformExt, + name: params.connectionListItem.name, + marketplaceItem: marketplaceItem, + visibility: visibility, + configurations: connectionItem?.configurations, + // todo: handle third party + securityType, + }); + + StateMachine.setReadyMode(); + this.refreshConnectionList(); + return resp; + } catch (err) { + StateMachine.setReadyMode(); + window.showErrorMessage("Failed to import Devant connection"); + log(`Failed to invoke importDevantComponentConnection: ${err}`); + } + } + + async createDevantComponentConnectionV2(params: CreateDevantConnectionV2Req): Promise { + try { + const platformExt = await this.getPlatformExt(); + StateMachine.setEditMode(); + const projectPath = StateMachine.context().projectPath; + + // todo: check if this logic is valid + let visibility: ServiceInfoVisibilityEnum = ServiceInfoVisibilityEnum.Public; + if(params.importInternalConnectionParams?.connection?.visibilities?.some(item => item.componentUuid)){ + visibility = ServiceInfoVisibilityEnum.Project; + } else if(params.importInternalConnectionParams?.connection?.visibilities?.some(item => item.projectUuid)){ + visibility = ServiceInfoVisibilityEnum.Organization; + } + + if ([ DevantConnectionFlow.IMPORT_INTERNAL_OAS].includes(params.flow)) { + await platformExt?.createConnectionConfig({ + componentDir: projectPath, + marketplaceItem: params.marketplaceItem, + name: params.importInternalConnectionParams.connection.name, + visibility: visibility, + }); + + const securityType = params.importInternalConnectionParams?.connection?.schemaName?.toLowerCase()?.includes("oauth") + ? "oauth" + : "apikey"; + const configurations = params.importInternalConnectionParams?.connection?.configurations; + + const resp = await initializeDevantConnection({ + platformExt, + name: params.importInternalConnectionParams.connection.name, + marketplaceItem: params.marketplaceItem, + visibility, + configurations, + securityType, + }); + + return resp; + } + if ( + [ + DevantConnectionFlow.CREATE_INTERNAL_OAS, + DevantConnectionFlow.CREATE_INTERNAL_OTHER, + DevantConnectionFlow.CREATE_INTERNAL_OTHER_SELECT_BI_CONNECTOR, + ].includes(params.flow) + ) { + const isProjectLevel = + !!!platformExtStore.getState().state?.selectedComponent?.metadata?.id || + params?.createInternalConnectionParams?.isProjectLevel; + const createdConnection = await platformExt?.createComponentConnection({ + componentId: isProjectLevel + ? "" + : platformExtStore.getState().state?.selectedComponent?.metadata?.id, + name: params.createInternalConnectionParams.name, + orgId: platformExtStore.getState().state?.selectedContext?.org.id?.toString(), + orgUuid: platformExtStore.getState().state?.selectedContext?.org?.uuid, + projectId: platformExtStore.getState().state?.selectedContext?.project.id, + serviceSchemaId: params.createInternalConnectionParams.schemaId, + serviceId: params.marketplaceItem.serviceId, + serviceVisibility: params.createInternalConnectionParams.visibility!, + componentType: isProjectLevel + ? "non-component" + : getTypeForDisplayType(platformExtStore.getState().state?.selectedComponent?.spec?.type), + componentPath: projectPath, + generateCreds: true, + }); + + await platformExt?.createConnectionConfig({ + componentDir: projectPath, + marketplaceItem: params.marketplaceItem, + name: params.createInternalConnectionParams.name, + visibility: params.createInternalConnectionParams.visibility, + }); + + if (params.flow === DevantConnectionFlow.CREATE_INTERNAL_OAS) { + const securityType = createdConnection?.schemaName?.toLowerCase()?.includes("oauth") + ? "oauth" + : "apikey"; + const configurations = createdConnection.configurations; + + const resp = await initializeDevantConnection({ + platformExt, + name: params.createInternalConnectionParams.name, + marketplaceItem: params.marketplaceItem, + visibility: params.createInternalConnectionParams.visibility!, + configurations, + securityType, + }); + + return resp; + } else { + await this.replaceDevantTempConfigValues({ createdConnection, configs: params.createInternalConnectionParams?.devantTempConfigs, }); + return {}; + } + } + if ( + [ + DevantConnectionFlow.CREATE_THIRD_PARTY_OAS, + DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER, + DevantConnectionFlow.CREATE_THIRD_PARTY_OTHER_SELECT_BI_CONNECTOR, + ].includes(params.flow) + ) { + const isProjectLevel = + !!!platformExtStore.getState().state?.selectedComponent?.metadata?.id || + params?.importThirdPartyConnectionParams?.isProjectLevel; + const matchingSchema = params.marketplaceItem.connectionSchemas?.find( + (item) => item.id === params.importThirdPartyConnectionParams?.schemaId, + ); + if (!matchingSchema) { + throw new Error(`No matching schemes found in marketplace item`); + } + if ( + !params?.marketplaceItem?.endpointRefs || + Object.keys(params?.marketplaceItem?.endpointRefs).length === 0 + ) { + throw new Error(`No endpoints found in the third party API item`); + } + + const createdConnection = await platformExt?.createThirdPartyConnection({ + componentId: isProjectLevel + ? "" + : platformExtStore.getState().state?.selectedComponent?.metadata?.id, + name: params.importThirdPartyConnectionParams?.name, + orgId: platformExtStore.getState().state?.selectedContext?.org.id?.toString(), + orgUuid: platformExtStore.getState().state?.selectedContext?.org?.uuid, + projectId: platformExtStore.getState().state?.selectedContext?.project.id, + serviceSchemaId: matchingSchema.id, + serviceId: params.marketplaceItem.serviceId, + endpointRefs: params?.marketplaceItem?.endpointRefs, + sensitiveKeys: matchingSchema.entries?.filter((item) => item.isSensitive).map((item) => item.name), + }); + await this.replaceDevantTempConfigValues({ createdConnection, configs: params.importThirdPartyConnectionParams?.devantTempConfigs, }); + + await platformExt?.createConnectionConfig({ + componentDir: projectPath, + marketplaceItem: params.marketplaceItem, + name: params.createInternalConnectionParams.name, + visibility: params.createInternalConnectionParams.visibility, + }); + } + + StateMachine.setReadyMode(); + this.refreshConnectionList(); + return { connectionName: "", connectionNode: null }; + } catch (err) { + StateMachine.setReadyMode(); + window.showErrorMessage("Failed to create Devant connection"); + log(`Failed to invoke createDevantComponentConnectionV2: ${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) { + fs.writeFileSync(filePath, yaml.dump(yaml.load(getYamlString(serviceIdl.content))), "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"); + 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}`); + } + } + + // todo: break this down into separate functions + async registerAndCreateDevantComponentConnection( + params: RegisterAndCreateDevantConnectionReq, + ): Promise { + try { + const platformExt = await this.getPlatformExt(); + StateMachine.setEditMode(); + const projectPath = StateMachine.context().projectPath; + + 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, + }); + + const isProjectLevel = !!!platformExtStore.getState().state?.selectedComponent?.metadata?.id; + + const createdConnection = await platformExt?.createThirdPartyConnection({ + componentId: isProjectLevel ? "" : platformExtStore.getState().state?.selectedComponent?.metadata?.id, + name: params.name, + orgId: platformExtStore.getState().state?.selectedContext?.org.id?.toString(), + orgUuid: platformExtStore.getState().state?.selectedContext?.org?.uuid, + projectId: platformExtStore.getState().state?.selectedContext?.project.id, + serviceSchemaId: registeredMarketplaceItem.connectionSchemas[0]?.id, + serviceId: registeredMarketplaceItem.serviceId, + endpointRefs: marketplaceService.endpointRefs, + sensitiveKeys: registeredMarketplaceItem.connectionSchemas[0].entries + ?.filter((item) => item.isSensitive) + .map((item) => item.name), + }); + + await this.replaceDevantTempConfigValues({ createdConnection, configs: params.configs }); + + await platformExt?.createConnectionConfig({ + componentDir: projectPath, + marketplaceItem: registeredMarketplaceItem, + name: params.name, + visibility: "PUBLIC", + }); + + StateMachine.setReadyMode(); + this.refreshConnectionList(); + return { connectionName: "", connectionNode: null }; + } catch (err) { + StateMachine.setReadyMode(); + window.showErrorMessage("Failed to create Devant connection"); + log(`Failed to invoke registerAndCreateDevantComponentConnection: ${err}`); + } + } + + async replaceDevantTempConfigValues(params: ReplaceDevantTempConfigValuesReq): Promise { + 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("${getInjectedEnvVarNames(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, + }); + } + } + + 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 }); + + // WIP: 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 + */ + /* + 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 lowestEnv = envs.find(item=>!item.critical) + if(!lowestEnv){ + throw new Error("failed to find env when refreshing devant connection list") + } + + const secureHosts = new Set() + const envMap = new Map() + + for(const connItem of connections){ + const connectionDetailedItem = await platformExt.getConnection({ + connectionGroupId: connItem.groupUuid, + orgId: platformExtStore.getState().state?.selectedContext?.org?.id?.toString() + }); + const matchingConfig = connectionDetailedItem.configurations[lowestEnv.templateId]; + if(matchingConfig){ + for(const entryName in matchingConfig.entries ){ + if(matchingConfig.entries[entryName].value){ + if(connItem.schemaName?.toLowerCase().includes("organization") && entryName==="ServiceURL" && matchingConfig.entries[entryName].value.startsWith("https://")){ + const domain = getDomain(matchingConfig.entries[entryName].value) + secureHosts.add(domain) + envMap.set(entryName, matchingConfig.entries[entryName].value.replace("https://", "http://")) + }else{ + envMap.set(entryName, matchingConfig.entries[entryName].value) + } + if((envMap.get(entryName).startsWith("https://") || envMap.get(entryName).startsWith("http://")) && envMap.get(entryName).endsWith("/")){ + envMap.set(entryName, envMap.get(entryName.slice(0, -1))) + } + }else if(matchingConfig.entries[entryName].isSensitive && !matchingConfig.entries[entryName].isFile){ + /////////// + // todo: // + /////////// + } + } + } + } + */ + } 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 00000000000..9c1f01e54c4 --- /dev/null +++ b/workspaces/ballerina/ballerina-extension/src/rpc-managers/platform-ext/types.ts @@ -0,0 +1,53 @@ + +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/stateMachine.ts b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts index 87e89eef813..84486f05e12 100644 --- a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts @@ -846,6 +846,9 @@ export function openView(type: EVENT_TYPE, viewLocation: VisualizerLocation, res } export function updateView(refreshTreeView?: boolean) { + 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")) { diff --git a/workspaces/ballerina/ballerina-rpc-client/package.json b/workspaces/ballerina/ballerina-rpc-client/package.json index 9f0c2f0ec52..289c2201e5b 100644 --- a/workspaces/ballerina/ballerina-rpc-client/package.json +++ b/workspaces/ballerina/ballerina-rpc-client/package.json @@ -21,7 +21,8 @@ "react-dom": "18.2.0", "vscode-messenger-common": "^0.4.5", "vscode-messenger-webview": "^0.5.1", - "vscode-languageserver-types": "^3.17.5" + "vscode-languageserver-types": "^3.17.5", + "@wso2/wso2-platform-core": "workspace:*" }, "devDependencies": { "@types/react": "18.2.0", diff --git a/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts b/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts index f4cba7d1927..45e0d190c10 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts @@ -73,6 +73,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 { @@ -95,6 +96,7 @@ export class BallerinaRpcClient { private _aiAgent: AiAgentRpcClient; private _icpManager: ICPServiceRpcClient; private _agentChat: AgentChatRpcClient; + private _platformExt: PlatformExtRpcClient; constructor() { this.messenger = new Messenger(vscode); @@ -117,6 +119,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 { @@ -187,6 +190,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/bi-diagram/rpc-client.ts b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/bi-diagram/rpc-client.ts index 1e45c2e40b0..b6b774e7f13 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 @@ -59,9 +59,6 @@ import { DeleteProjectRequest, DeleteTypeRequest, DeleteTypeResponse, - DeploymentRequest, - DeploymentResponse, - DevantMetadata, EndOfFileRequest, ExpressionCompletionsRequest, ExpressionCompletionsResponse, @@ -138,7 +135,6 @@ import { deleteOpenApiGeneratedModules, deleteProject, deleteType, - deployProject, formDidClose, formDidOpen, generateOpenApiClient, @@ -155,7 +151,6 @@ import { getConfigVariablesV2, getDataMapperCompletions, getDesignModel, - getDevantMetadata, getEnclosedFunction, getEndOfFile, getExpressionCompletions, @@ -348,10 +343,6 @@ export class BiDiagramRpcClient implements BIDiagramAPI { return this._messenger.sendRequest(renameIdentifier, HOST_EXTENSION, params); } - deployProject(params: DeploymentRequest): Promise { - return this._messenger.sendRequest(deployProject, HOST_EXTENSION, params); - } - openAIChat(params: AIChatRequest): void { return this._messenger.sendNotification(openAIChat, HOST_EXTENSION, params); } @@ -496,10 +487,6 @@ export class BiDiagramRpcClient implements BIDiagramAPI { return this._messenger.sendRequest(getFunctionNames, HOST_EXTENSION); } - getDevantMetadata(): Promise { - return this._messenger.sendRequest(getDevantMetadata, 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 a7731461765..2d1ae7a83e2 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 @@ -52,7 +52,15 @@ import { runBackgroundTerminalCommand, selectFileOrDirPath, selectFileOrFolderPath, - showErrorMessage + showErrorMessage, + SetWebviewCacheRequestParam, + SetWebviewCache, + RestoreWebviewCache, + ClearWebviewCache, + ShowInfoModalRequest, + showInformationModal, + ShowQuickPickRequest, + showQuickPick } from "@wso2/ballerina-core"; import { HOST_EXTENSION } from "vscode-messenger-common"; import { Messenger } from "vscode-messenger-webview"; @@ -115,6 +123,14 @@ export class CommonRpcClient implements CommonRPCAPI { showErrorMessage(params: ShowErrorMessageRequest): void { 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,6 +140,18 @@ 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); } 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 00000000000..057fc2dc448 --- /dev/null +++ b/workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/platform-ext/platform-ext-client.ts @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PlatformExtAPI, getMarketplaceItems, getMarketplaceItem, getMarketplaceIdl, getConnections, deleteLocalConnectionsConfig, getDevantConsoleUrl, importDevantComponentConnection, getConnection, onPlatformExtStoreStateChange, refreshConnectionList, getPlatformStore, setConnectedToDevant, setSelectedComponent, deployIntegrationInDevant, registerMarketplaceConnection, registerAndCreateDevantComponentConnection, deleteDevantTempConfigs, createDevantComponentConnectionV2, generateCustomConnectorFromOAS, addDevantTempConfig, setSelectedEnv, createConnectionConfig, replaceDevantTempConfigValues } from "@wso2/ballerina-core"; +import { HOST_EXTENSION } from "vscode-messenger-common"; +import { Messenger } from "vscode-messenger-webview"; +import { ContextItemEnriched, GetMarketplaceListReq,MarketplaceListResp, ComponentKind, GetMarketplaceIdlReq, MarketplaceIdlResp, ConnectionListItem, GetConnectionsReq, DeleteLocalConnectionsConfigReq, GetMarketplaceItemReq, MarketplaceItem, GetConnectionItemReq, ConnectionDetailed, RegisterMarketplaceConnectionReq, CreateLocalConnectionsConfigReq } from "@wso2/wso2-platform-core" +import { AddDevantTempConfigReq, AddDevantTempConfigResp, CreateDevantConnectionResp, CreateDevantConnectionV2Req, DeleteDevantTempConfigReq, GenerateCustomConnectorFromOASReq, GenerateCustomConnectorFromOASResp, ImportDevantConnectionReq, ImportDevantConnectionResp, PlatformExtState, RegisterAndCreateDevantConnectionReq, 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); + } + + createDevantComponentConnectionV2(params: CreateDevantConnectionV2Req): Promise { + return this._messenger.sendRequest(createDevantComponentConnectionV2, HOST_EXTENSION, params); + } + + importDevantComponentConnection(params: ImportDevantConnectionReq): Promise { + return this._messenger.sendRequest(importDevantComponentConnection, HOST_EXTENSION, params); + } + + registerAndCreateDevantComponentConnection(params: RegisterAndCreateDevantConnectionReq): Promise { + return this._messenger.sendRequest(registerAndCreateDevantComponentConnection, 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); + } + + registerMarketplaceConnection(params: RegisterMarketplaceConnectionReq): Promise { + return this._messenger.sendRequest(registerMarketplaceConnection, 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); + } + + 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-side-panel/package.json b/workspaces/ballerina/ballerina-side-panel/package.json index c88cd97430d..ada7cc84679 100644 --- a/workspaces/ballerina/ballerina-side-panel/package.json +++ b/workspaces/ballerina/ballerina-side-panel/package.json @@ -46,6 +46,7 @@ "react-dom": "18.2.0", "react-hook-form": "7.56.4", "react-markdown": "~10.1.0", + "@wso2/wso2-platform-core": "workspace:*", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "@github/markdown-toolbar-element": "^2.2.3", diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts b/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts index 593f601a19e..33a3fdaf03a 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/Form/types.ts @@ -248,13 +248,19 @@ type SanitizedExpressionEditorProps = { sanitizedExpression?: (expression: string) => 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 555826f913e..0540a66d7bf 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; `; @@ -153,10 +163,11 @@ interface GroupListProps { category: Category; expand?: boolean; onSelect: (node: Node, category: string) => void; + onImportDevantConn?: (devantConn: ConnectionListItem) => void; } export function GroupList(props: GroupListProps) { - const { category, expand, onSelect } = props; + const { category, expand, onSelect, onImportDevantConn } = props; const [showList, setShowList] = useState(expand ?? false); const [expandedTitleIndex, setExpandedTitleIndex] = useState(null); @@ -176,6 +187,16 @@ export function GroupList(props: GroupListProps) { setExpandedTitleIndex(null); }; + if (category.devant && category.unusedDevantConn) { + return ( + + ); + } + if (nodes.length === 0) { return null; } @@ -185,6 +206,13 @@ export function GroupList(props: GroupListProps) { {category.icon || } {category.title} + {category.tooltip && ( + + )} {openList ? : } @@ -224,6 +252,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/NodeList/categoryConfig.ts b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/categoryConfig.ts index da37cedf5bd..874a9118ed8 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 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 895d8b29108..7f5c7fc0278 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/NodeList/index.tsx @@ -36,6 +36,7 @@ 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"; namespace S { @@ -319,6 +320,9 @@ interface NodeListProps { onBack?: () => void; onClose?: () => void; searchPlaceholder?: string; + onImportDevantConn?: (devantConn: ConnectionListItem) => void; + onLinkDevantProject?: () => void; + onRefreshDevantConnections?: () => void; } export function NodeList(props: NodeListProps) { @@ -335,6 +339,9 @@ export function NodeList(props: NodeListProps) { onBack, onClose, searchPlaceholder, + onImportDevantConn, + onLinkDevantProject, + onRefreshDevantConnections, } = props; const [searchText, setSearchText] = useState(""); @@ -434,6 +441,18 @@ export function NodeList(props: NodeListProps) { } }; + const handleOnLinkDevantProject = () => { + if (onLinkDevantProject){ + onLinkDevantProject(); + } + } + + const handleOnRefreshDevantConnections = () => { + if (onRefreshDevantConnections){ + onRefreshDevantConnections(); + } + } + 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)); @@ -535,6 +554,7 @@ export function NodeList(props: NodeListProps) { category={category} expand={searchText?.length > 0} onSelect={handleAddNode} + onImportDevantConn={onImportDevantConn} /> )) } @@ -613,7 +633,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]; @@ -633,7 +655,7 @@ export function NodeList(props: NodeListProps) { handler(); }} > - + ); @@ -666,14 +688,16 @@ export function NodeList(props: NodeListProps) { 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"; @@ -682,7 +706,7 @@ export function NodeList(props: NodeListProps) { key={`empty-${group.title}-${actionIndex}`} onClick={handler} > - + {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 2decc078280..a921abc7737 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/resources/icons/nodes/DownloadIcon.tsx b/workspaces/ballerina/ballerina-side-panel/src/resources/icons/nodes/DownloadIcon.tsx new file mode 100644 index 00000000000..fbbb12dd7d5 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/resources/icons/nodes/DownloadIcon.tsx @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from "react"; + +export const DownloadIcon = () => { + return ( + + ); +}; diff --git a/workspaces/ballerina/ballerina-visualizer/package.json b/workspaces/ballerina/ballerina-visualizer/package.json index c9579aabb91..cb51c49d7eb 100644 --- a/workspaces/ballerina/ballerina-visualizer/package.json +++ b/workspaces/ballerina/ballerina-visualizer/package.json @@ -22,6 +22,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", "@wso2/ballerina-core": "workspace:*", "@wso2/ballerina-graphql-design-diagram": "workspace:*", @@ -56,7 +57,9 @@ "@hookform/resolvers": "~5.0.1", "highlight.js": "^11.11.1", "rehype-raw": "^7.0.0", - "remark-breaks": "~4.0.0" + "remark-breaks": "~4.0.0", + "js-yaml": "^4.1.0", + "swagger-ui-react": "5.22.0" }, "devDependencies": { "@types/react": "18.2.0", @@ -81,6 +84,8 @@ "webpack": "^5.99.8", "@types/react-lottie": "^1.2.5", "@types/lodash.debounce": "^4.0.6", + "@types/js-yaml": "^4.0.5", + "@types/swagger-ui-react": "5.18.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.2.1" }, diff --git a/workspaces/ballerina/ballerina-visualizer/src/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/index.tsx index 4a90b3f110a..17fdacfddad 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 00000000000..3462c389d41 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/providers/platform-ext-ctx-provider.tsx @@ -0,0 +1,186 @@ +/** + * 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 { useQueryClient, useQuery, useMutation } from "@tanstack/react-query"; +import { AvailableNode, DIRECTORY_MAP, EVENT_TYPE, findDevantScopeByModule, MACHINE_VIEW, PackageTomlValues } from "@wso2/ballerina-core"; +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; + refetchProjectInfo: () => void; + devantConsoleUrl: string; + projectPath: string; + workspacePath: string; + // todo: check if we need to refresh project toml? + projectToml?: { values: Partial; refresh: () => void }; + platformRpcClient?: PlatformExtRpcClient; + deployableArtifacts?: { exists: boolean; refetch: () => void }; + onLinkDevantProject: () => void; + importConnection: { + connection?: ConnectionListItem; + setConnection: (item?: ConnectionListItem) => void; + } +} = { + platformExtState: { components: [], isLoggedIn: false, userInfo: null }, + refetchProjectInfo: () => {}, + onLinkDevantProject: () => {}, + devantConsoleUrl: "", + projectPath: "", + workspacePath: "", + 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: visualizerLocation = { projectPath: "", workspacePath: "" }, refetch: refetchProjectInfo } = useQuery( + { + queryKey: ["project-info"], + queryFn: () => rpcClient.getVisualizerLocation(), + select: (data) => ({ projectPath: data.projectPath, workspacePath: data.workspacePath }), + refetchOnWindowFocus: true, + } + ); + + const { data: projectToml, refetch: refetchToml } = useQuery({ + queryKey: ["project-toml", visualizerLocation.projectPath], + queryFn: () => rpcClient.getCommonRpcClient().getCurrentProjectTomlValues(), + refetchOnWindowFocus: true, + }); + + const { data: hasArtifacts, refetch: refetchHasArtifacts } = useQuery({ + queryKey: ["has-artifacts", visualizerLocation.projectPath], + queryFn: () => rpcClient.getBIDiagramRpcClient().getProjectStructure(), + select: (projectStructure) => { + if (!projectStructure) return false; + + const project = projectStructure.projects.find( + (project) => project.projectPath === visualizerLocation.projectPath + ); + if (!project) return false; + + const services = project.directoryMap[DIRECTORY_MAP.SERVICE] ?? []; + const automation = project.directoryMap[DIRECTORY_MAP.AUTOMATION] ?? []; + + const hasAutomation = automation.length > 0; + const hasServiceScopes = services.map((s) => findDevantScopeByModule(s?.moduleName)).some(Boolean); + + return hasAutomation || hasServiceScopes; + }, + refetchOnWindowFocus: true, + }); + + 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 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") { + rpcClient.getCommonRpcClient().executeCommand({ + commands: [PlatformExtCommandIds.SignIn, { extName: "Devant" } as ICmdParamsBase], + }); + } + }); + } 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((resp) => { + if (resp === "Deploy Now") { + platformRpcClient.deployIntegrationInDevant(); + } else if (resp === "Associate Project") { + rpcClient.getCommonRpcClient().executeCommand({ + commands: [ + PlatformExtCommandIds.CreateDirectoryContext, + { + extName: "Devant", + skipComponentExistCheck: true, + fsPath: visualizerLocation.workspacePath || visualizerLocation?.projectPath, + } as ICreateDirCtxCmdParams, + ], + }); + } + }); + } + }; + + // todo: avoid passing refetch functions via context + 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 00000000000..60bf2c1f1ed --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/providers/react-query-provider.tsx @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { 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} + + ); +}; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx b/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx index 88fafda780d..6fd4f63269b 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/utils/bi.tsx @@ -24,6 +24,7 @@ import { ParameterValue, Parameter, FormImports, + Node, } from "@wso2/ballerina-side-panel"; import { AddNodeVisitor, RemoveNodeVisitor, NodeIcon, traverseFlow, ConnectorIcon, AIModelIcon } from "@wso2/bi-diagram"; import { @@ -56,6 +57,7 @@ import { SubPanel, SubPanelView, NodeMetadata, + PackageTomlValues, Type, getPrimaryInputType, isTemplateType, @@ -73,7 +75,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 +83,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, ContextItemEnriched } from "@wso2/wso2-platform-core"; hljs.registerLanguage("ballerina", ballerina); export const BALLERINA_INTEGRATOR_ISSUES_URL = "https://github.com/wso2/product-ballerina-integrator/issues"; @@ -138,6 +141,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"); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentDiagram/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentDiagram/index.tsx index e7239fd1a86..33f3427f0bd 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentDiagram/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ComponentDiagram/index.tsx @@ -139,7 +139,7 @@ export function ComponentDiagram(props: ComponentDiagramProps) { }); }; - const handleDeleteComponent = async (component: CDListener | CDService | CDAutomation | CDConnection) => { + const handleDeleteComponent = async (component: CDListener | CDService | CDAutomation | CDConnection, nodeType?: string) => { console.log(">>> delete component", component); setIsDeleting(true); rpcClient @@ -154,6 +154,7 @@ export function ComponentDiagram(props: ComponentDiagramProps) { endLine: component.location.endLine.line, endColumn: component.location.endLine.offset, }, + nodeType }) .then((response) => { console.log(">>> Updated source code after delete", response); 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 3490eb155d6..ca6dbc06c41 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 apiSpecOptions = useMemo( - () => [ - { id: "openapi", value: "OpenAPI", content: "OpenAPI" }, - { id: "wsdl", value: "WSDL", content: "WSDL" }, - ], - [] + const handleOnFormSubmit = async (node: FlowNode, _dataMapperMode?: DataMapperDisplayMode, 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 renderConnectionStep = () => { + if (selectedFlowNode) { + return ( + +
+ Connection Details + + Configure connection settings + +
+ setUpdatedExpressionField(undefined)} + isPullingConnector={isSavingConnection} + footerActionButton={true} + /> +
+ ); + } + return ( + + + + Loading connector configuration... + + + + ); + }; + + 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" }; @@ -285,6 +420,7 @@ export function APIConnectionPopup(props: APIConnectionPopupProps) { } }; + const findConnectorByModule = (categories: Category[], moduleName: string): AvailableNode | null => { for (const category of categories) { if (category.items) { @@ -301,12 +437,17 @@ export function APIConnectionPopup(props: APIConnectionPopupProps) { return null; }; + const handleSaveConnector = async () => { if (!selectedFilePath || !connectorName || !rpcClient) { return; } setIsSavingConnector(true); setConnectionError(null); + if (availableNode) { + onSave(availableNode, null, specType, connectorName, selectedFilePath); + return; + } const generateResponse = await handleOnGenerateSubmit(selectedFilePath, connectorName, specType); // Only proceed if there's no error message @@ -351,14 +492,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 +511,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 +523,90 @@ export function APIConnectionPopup(props: APIConnectionPopupProps) { {connectionError} - - - Or try using a pre-built connector: - - - - Browse Pre-built Connectors - - ); }; - const handleOnFormSubmit = async (node: FlowNode, _dataMapperMode?: DataMapperDisplayMode, 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 00000000000..c2b2073bc57 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/AddConnectionPopupContent.tsx @@ -0,0 +1,532 @@ +/** + * 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 styled from "@emotion/styled"; +import { AvailableNode, Category, Item, LinePosition } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { Codicon, Icon, ThemeColors, Typography, ProgressRing, Tooltip } 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, ExperimentalBadge, FilterButton, FilterButtons, IntroText, SearchContainer, Section, SectionHeader, SectionTitle, StyledSearchBox } from "./styles"; +import { AddConnectionPopupProps } from "./index"; + +interface Props extends AddConnectionPopupProps { + handleDatabaseConnection?: () => void; + handleApiSpecConnection?: () => void; + handleSelectConnector?: (connector: AvailableNode, filteredCategories: Category[]) => void; +} + +export function AddConnectionPopupContent(props: Props) { + const { fileName, target, handleDatabaseConnection, handleApiSpecConnection, handleSelectConnector } = 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 [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 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 (database shown disabled if hasPersistConnection) + return { showApiSpec: !!handleApiSpecConnection, showDatabase: experimentalEnabled && !!handleDatabaseConnection }; + } + + 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 ( + <> + + {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, 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 2dc46ae5748..53a17cf1acb 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,21 @@ * 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 { cloneDeep, debounce } from "lodash"; -import ButtonCard from "../../../../components/ButtonCard"; -import { ConnectorIcon } from "@wso2/bi-diagram"; +import { Codicon, ThemeColors } from "@wso2/ui-toolkit"; 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 +40,30 @@ interface AddConnectionPopupProps { } export function AddConnectionPopup(props: AddConnectionPopupProps) { + const { onClose, onNavigateToOverview, isPopup, target, fileName } = props; + const { platformExtState } = usePlatformExtContext(); + + if((platformExtState?.hasPossibleComponent && !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 +114,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 +164,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 +175,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 00000000000..4699566a00e --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionPopup/styles.ts @@ -0,0 +1,202 @@ +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<{ 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}; + } +`; + +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)}; +`; + +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 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; +`; + +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}; +`; \ 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 0e1b636ce1d..0844b6d59a9 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 { DataMapperDisplayMode, 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 966c385116b..efc7c3a5543 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"; @@ -48,7 +48,7 @@ const ConnectorInfoCard = styled.div` align-items: center; gap: 12px; padding: 12px; - margin: 16px 20px; + margin: 16px 0; border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; border-radius: 8px; background-color: ${ThemeColors.SURFACE_DIM}; @@ -143,7 +143,7 @@ const ConfigContent = styled.div<{ hasFooterButton?: boolean }>` display: flex; flex-direction: column; overflow: ${(props: { hasFooterButton?: boolean }) => props.hasFooterButton ? "hidden" : "auto"}; - padding: 0 16px ${(props: { hasFooterButton?: boolean }) => props.hasFooterButton ? "0" : "24px"} 16px; + padding: 0 0 ${(props: { hasFooterButton?: boolean }) => props.hasFooterButton ? "0" : "24px"} 0; min-height: 0; `; @@ -183,6 +183,11 @@ const StatusText = styled(Typography)` text-align: center; `; +const FormWrap = styled.div` + padding: 16px; +`; + + enum PullingStatus { FETCHING = "fetching", PULLING = "pulling", @@ -196,17 +201,54 @@ 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 +289,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 +306,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 +419,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 00000000000..76638afabe4 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantBIConnectorInitForm.tsx @@ -0,0 +1,178 @@ +/** + * 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, 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"; + +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[]; + onFlowChange: (flow: DevantConnectionFlow | null) => void; + projectPath: string; +} + +export const DevantBIConnectorCreateForm: FC = (props) => { + const { selectedMarketplaceItem, selectedFlow, devantConfigs, onAddDevantConfig, IDLFilePath, biConnectionNames, onClose, resetDevantConfigs, onFlowChange, importedConnection, projectPath } = 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}Connection` + } + 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, + }) + await platformRpcClient.createConnectionConfig({ + marketplaceItem: selectedMarketplaceItem, + name: importedConnection.name, + visibility: "PUBLIC", + componentDir: projectPath + }) + } else if (devantConfigs?.length > 0) { + if ( + [ + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR, + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS, + ].includes(selectedFlow) + ) { + return platformRpcClient.registerAndCreateDevantComponentConnection({ + 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", + }); + } + return platformRpcClient.createDevantComponentConnectionV2({ + flow: selectedFlow, + marketplaceItem: selectedMarketplaceItem, + createInternalConnectionParams: { + devantTempConfigs: devantConfigs || [], + name: recentIdentifier, + schemaId: selectedMarketplaceItem.connectionSchemas[0]?.id || "", + visibility: "PUBLIC", + }, + importThirdPartyConnectionParams: { + devantTempConfigs: devantConfigs || [], + name: recentIdentifier, + schemaId: selectedMarketplaceItem.connectionSchemas[0]?.id || "", + }, + }); + } + }, + onError: (error) => { + console.error(">>> Error creating Devant connection", error); + }, + onSuccess: (_, { recentIdentifier }) => { + resetDevantConfigs(); + onClose({ recentIdentifier, artifactType: DIRECTORY_MAP.CONNECTION }); + }, + }); + + if(!props.selectedConnector){ + return null; + } + + 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, devantConfigs?.map((conn) => conn.name) || [], biConnectionNames); + } + return undefined; + }} + overrideFlowNode={(node) => { + if(node.properties.variable){ + node.properties.variable.value = importedConnection ? initialNameCandidate : generateInitialConnectionName( + biConnectionNames, + platformExtState?.devantConns?.list?.map((conn) => conn.name) || [], + 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 00000000000..c32b6b845ed --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantBIConnectorSelect.tsx @@ -0,0 +1,136 @@ +/** + * 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, { 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 00000000000..80ec6def599 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorCreateForm.tsx @@ -0,0 +1,387 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + type MarketplaceItem, + type MarketplaceItemSchema, + type Project, + ServiceInfoVisibilityEnum, + capitalizeFirstLetter, +} 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 } 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; +`; + +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; +}; + +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, + devantFlow: DevantConnectionFlow, + biConnectionNames?: string[], + onSuccess?: (data: { connectionNode?: any; connectionName?: string }) => void, +} + +export const DevantConnectorCreateForm: FC = ({ + biConnectionNames, + marketplaceItem, + devantFlow, + onSuccess, +}) => { + 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, + platformExtState?.devantConns?.list?.map((conn) => conn.name) || [], + marketplaceItem?.name || "" + ), + visibility: getInitialVisibility(marketplaceItem, visibilities), + schemaId: "", + isProjectLevel: false, + envKeys: [], + }, + }); + + useEffect(() => { + form.reset({ + name: generateInitialConnectionName( + biConnectionNames, + platformExtState?.devantConns?.list?.map((conn) => conn.name) || [], + marketplaceItem?.name || "" + ), + visibility: getInitialVisibility(marketplaceItem, visibilities), + schemaId: "", + isProjectLevel: false, + }); + }, [marketplaceItem]); + + const { mutate: createConnection, isPending: isCreatingConnection } = useMutation({ + mutationFn: (data: CreateConnectionForm) => + platformRpcClient?.createDevantComponentConnectionV2({ + flow: devantFlow, + marketplaceItem: marketplaceItem, + createInternalConnectionParams: { + name: data.name, + schemaId: data.schemaId, + visibility: data.visibility, + isProjectLevel: data.isProjectLevel, + }, + }), + onSuccess: (data) => { + 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, + platformExtState?.devantConns?.list?.map((conn) => conn.name) || [], + 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 00000000000..b087574cd85 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorList.tsx @@ -0,0 +1,336 @@ +/** + * 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, useCallback } from "react"; +import { AvailableNode, LinePosition, ParentPopupData } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { Codicon, Icon, ThemeColors, ProgressRing, Button } 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, + CommandIds as PlatformExtCommandIds, + ICmdParamsBase as PlatformExtICmdParamsBase, +} from "@wso2/wso2-platform-core"; +import { + ArrowIcon, + ConnectorOptionButtons, + ConnectorOptionCard, + ConnectorOptionContent, + ConnectorOptionDescription, + ConnectorOptionIcon, + ConnectorOptionTitle, + ConnectorsGrid, + ConnectorTypeLabel, + CreateConnectorOptions, + FilterButton, + FilterButtons, + IntroText, + SearchContainer, + Section, + SectionHeader, + SectionTitle, + StyledSearchBox, +} from "../AddConnectionPopup/styles"; +import { DevantConnectionFlow } from "@wso2/ballerina-core/lib/rpc-types/platform-ext/interfaces"; +import { DevantConnectionType, getKnownAvailableNode, ProgressWrap } from "./utils"; + +interface DevantConnectorListProps { + showBiConnectors: () => void; + onItemSelect: ( + flow: DevantConnectionFlow | null, + item: MarketplaceItem, + availableNode: AvailableNode | undefined, + ) => void; + fileName: string; + target?: LinePosition; +} + +export function DevantConnectorList(props: DevantConnectorListProps) { + const { showBiConnectors, onItemSelect, fileName, target } = props; + const { platformExtState, platformRpcClient } = usePlatformExtContext(); + const [searchText, setSearchText] = useState(""); + const { rpcClient } = useRpcContext(); + + const debouncedSetSearchText = useCallback( + debounce((value: string) => setSearchText(value), 500), + [], + ); + + 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, type: DevantConnectionType) => { + // 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 (type === DevantConnectionType.THIRD_PARTY) { + 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 (type === DevantConnectionType.INTERNAL) { + 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: searchText, + 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: searchText || undefined, + searchContent: false, + isThirdParty: false, + }; + + const { data: internalApisResp, isLoading: internalApisLoading } = useQuery({ + queryKey: ["devant-internal-services", 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) => item.component?.componentId !== platformExtState?.selectedComponent?.metadata?.id, + ), + }), + }); + + const { data: thirdPartyApisResp, isLoading: thirdPartyApisLoading } = useQuery({ + queryKey: ["third-party-services", reactQueryKey], + queryFn: () => + platformRpcClient?.getMarketplaceItems({ + orgId: platformExtState?.selectedContext?.org?.id?.toString(), + request: { + ...getMarketPlaceParams, + isThirdParty: true, + networkVisibilityFilter: "org,project,public", + }, + }), + enabled: platformExtState.isLoggedIn && !!platformExtState?.selectedContext?.project, + }); + + return ( + <> + + Connect to API services running in Devant, use existing third-party connections, or create a new + connection. + + + + + + +
+ + showBiConnectors()}> + + + + + Create New Connection + + Create new connection using pre-built ballerina connectors or using API specifications + + + Ballerina Connectors + OpenAPI + Databases + + + + + + + +
+ + {platformExtState?.isLoggedIn ? ( + <> + {loadingBalOrgConnectors && ( + + + + )} + handleMarketplaceItemClick(item, DevantConnectionType.INTERNAL)} + /> + handleMarketplaceItemClick(item, DevantConnectionType.THIRD_PARTY)} + /> + + ) : ( +
+ + Devant Dependencies + + + You need to be signed into Devant in order connect with dependencies managed by Devant + + +
+ )} + + ); +} + +const ConnectionSection = ({ + emptyText, + title, + loading, + data, + searchText, + onItemClick, +}: { + emptyText?: string; + title: string; + loading: boolean; + data: MarketplaceItem[]; + searchText: string; + onItemClick: (item: MarketplaceItem) => void; +}) => { + const { platformExtState } = usePlatformExtContext(); + const [filterType, setFilterType] = useState<"Project" | "Organization">("Organization"); + const filteredData = data.filter((item) => { + if (filterType === "Project") { + return item.projectId === platformExtState?.selectedContext?.project?.id; + } + return true; + }); + return ( +
+ + {title} + + setFilterType("Organization")}> + All + + setFilterType("Project")}> + Project + + + + {loading ? ( + + + + ) : ( + <> + {filteredData?.length === 0 ? ( + <> + {searchText ? ( + + {emptyText} in your Devant {filterType === "Project" ? "project" : "organization"}{" "} + matching with "{searchText}" + + ) : ( + + {emptyText} in your Devant {filterType === "Project" ? "project" : "organization"} + + )} + + ) : ( + + {filteredData?.map((item) => { + return ( + } + onClick={() => onItemClick(item)} + /> + ); + })} + + )} + + )} +
+ ); +}; 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 00000000000..ac32a220d11 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorMarketplaceInfo.tsx @@ -0,0 +1,301 @@ +/** + * 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 { useQuery } from "@tanstack/react-query"; +import { VSCodePanelTab, VSCodePanelView, VSCodePanels } from "@vscode/webview-ui-toolkit/react"; +import type { ConnectionListItem, MarketplaceItem, Organization } from "@wso2/wso2-platform-core"; +import { useEffect, type FC, type ReactNode } from "react"; +import styled from "@emotion/styled"; +import { Button, Badge, ProgressRing, Icon, Codicon, ThemeColors, Typography } 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, + ConnectorOptionCard, + 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 00000000000..b120beb1606 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/DevantConnectorPopup.tsx @@ -0,0 +1,511 @@ +/** + * 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 { + AvailableNode, + ConfigVariable, + DataMapperDisplayMode, + DIRECTORY_MAP, + FlowNode, + 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 } 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"; +import { set } from "lodash"; + +interface AddConnectionPopupProps { + onClose?: (parent?: ParentPopupData) => void; + onNavigateToOverview: () => void; + isPopup?: boolean; + fileName: string; + target?: LinePosition; +} + +export function DevantConnectorPopup(props: AddConnectionPopupProps) { + const { onClose, onNavigateToOverview, isPopup, fileName, target } = props; + const { platformRpcClient, projectPath, projectToml, platformExtState, importConnection } = usePlatformExtContext(); + const [isCreating, setIsCreating] = useState(false); + 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 [showBiConnectorSelection, setShowBiConnectorSelection] = useState(false); + 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); // todo: use mutation + 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 configVars = (configResp.configVariables as any)?.[ + `${projectToml?.values?.package?.org}/${projectToml?.values?.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 (showBiConnectorSelection && !selectedFlow) { + setShowBiConnectorSelection(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 { mutate: initializeOASConn, isPending: initializingOASConn } = useMutation({ + mutationFn: async () => { + const connectionDetailed = await platformRpcClient.getConnection({ + connectionGroupId: importingConn?.groupUuid, + orgId: platformExtState?.selectedContext?.org?.id?.toString() + }) + return platformRpcClient?.createDevantComponentConnectionV2({ + flow: selectedFlow, + marketplaceItem: selectedMarketplaceItem!, + importInternalConnectionParams: { + connection: connectionDetailed + }, + }) + }, + onSuccess: (data) => { + if (onClose) { + onClose({ + recentIdentifier: data.connectionName, + artifactType: DIRECTORY_MAP.CONNECTION, + }); + } + }, + }); + + const { mutate: generateCustomConnectorFromOAS, isPending: generatingCustomConnectorFromOAS } = useMutation({ + mutationFn: async () => { + const resp = await platformRpcClient?.generateCustomConnectorFromOAS({ + marketplaceItem: selectedMarketplaceItem!, + connectionName: generateInitialConnectionName( + biConnectionNames, + platformExtState?.devantConns?.list?.map((conn) => conn.name) || [], + selectedMarketplaceItem?.name, + ), + }); + if(selectedFlow === DevantConnectionFlow.IMPORT_INTERNAL_OAS) { + initializeOASConn(); + } + 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 || showBiConnectorSelection : 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.connectionNode) { + rpcClient + .getBIDiagramRpcClient() + .getNodeTemplate({ + position: target || null, + filePath: fileName, + id: data.connectionNode.codedata, + }) + .then((nodeTemplatePromise) => { + // todo: check this flow + // init connector flow + }); + } else if (data.connectionName) { + if (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} + 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); + }} + /> + )} + + )} + + ) : ( + <> + {showBiConnectorSelection ? ( + { + setAvailableNode(availableNode); + setSelectedFlow( + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_BI_CONNECTOR, + ); + }} + handleApiSpecConnection={() => { + setSelectedFlow( + DevantConnectionFlow.REGISTER_CREATE_THIRD_PARTY_FROM_OAS, + ); + }} + /> + ) : ( + setShowBiConnectorSelection(true)} + onItemSelect={(flow, item, availableNode) => { + setSelectedFlow(flow); + setSelectedMarketplaceItem(item); + setAvailableNode(availableNode); + }} + /> + )} + + )} + + )} + + + + ); +} 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 00000000000..92e421358cf --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/DevantConnections/utils.ts @@ -0,0 +1,191 @@ +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 9533291f0f1..2c39e7b5827 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/styles.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/styles.ts @@ -82,3 +82,44 @@ export const CloseButton = styled(Button)` `; +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; +`; \ No newline at end of file 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 04ee0f4069f..fc92c565bcc 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; @@ -143,6 +144,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) { @@ -197,7 +203,9 @@ export function PanelManager(props: PanelManagerProps) { onSelectNewConnection, onUpdateNodeWithConnection, onNavigateToPanel, - onChangeSelectedNode + onImportDevantConn, + onLinkDevantProject, + onRefreshDevantConnections, } = props; const handleOnBackToAddTool = () => { @@ -243,6 +251,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 60ca86db666..0cbe870cdee 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/FlowDiagram/index.tsx @@ -60,6 +60,7 @@ import { convertEmbeddingProviderCategoriesToSidePanelCategories, convertDataLoaderCategoriesToSidePanelCategories, convertChunkerCategoriesToSidePanelCategories, + enrichCategoryWithDevant, convertKnowledgeBaseCategoriesToSidePanelCategories, } from "../../../utils/bi"; import { useDraftNodeManager } from "./hooks/useDraftNodeManager"; @@ -81,6 +82,9 @@ import { } 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 { useMutation } from "@tanstack/react-query"; +import { ConnectionListItem } from "@wso2/wso2-platform-core"; +import { usePlatformExtContext } from "../../../providers/platform-ext-ctx-provider"; const Container = styled.div` width: 100%; @@ -118,7 +122,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); @@ -129,6 +133,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); @@ -167,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]); @@ -198,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); } @@ -2420,7 +2445,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} @@ -2476,6 +2501,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/FormGenerator/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/FormGenerator/index.tsx index ff669f4be60..647c5ed3ac1 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 @@ -54,6 +54,7 @@ import { FormImports, HelperpaneOnChangeOptions, InputMode, + ExpressionEditorDevantProps, } from "@wso2/ballerina-side-panel"; import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { @@ -148,6 +149,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 +226,7 @@ export const FormGenerator = forwardRef(func injectedComponents, fieldPriority, footerActionButton, + customValidator, } = props; const { rpcClient } = useRpcContext(); @@ -481,6 +485,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); @@ -942,6 +955,7 @@ export const FormGenerator = forwardRef(func forcedValueTypeConstraint: valueTypeConstraints, handleValueTypeConstChange: handleValueTypeConstChange, inputMode: inputMode, + devantExpressionEditor: props.devantExpressionEditor, }); }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/styles.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/styles.ts index 274ecedc182..f1215949b09 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/styles.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Forms/styles.ts @@ -24,7 +24,7 @@ export namespace FormStyles { flex-direction: column; gap: 18px; height: calc(100vh - 100px); - overflow-y: scroll; + overflow-y: auto; padding: 16px; `; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx index 4d75c62829c..51f4ae77fc3 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx @@ -20,7 +20,7 @@ import { CompletionInsertText, ConfigVariable, FlowNode, LineRange, TomlPackage import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { ReactNode, useEffect, useState } from "react"; import ExpandableList from "../Components/ExpandableList"; -import { Divider, SearchBox, Typography } from "@wso2/ui-toolkit"; +import { Button, CheckBox, Divider, SearchBox, TextField, Typography } from "@wso2/ui-toolkit"; import { ScrollableContainer } from "../Components/ScrollableContainer"; import FooterButtons from "../Components/FooterButtons"; import FormGenerator from "../../Forms/FormGenerator"; @@ -46,7 +46,7 @@ type ListItem = { items: any[] } -type ConfigurablesPageProps = { +export type ConfigurablesPageProps = { onChange: (insertText: string | CompletionInsertText, isRecordConfigureChange?: boolean) => 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); @@ -88,6 +91,7 @@ export const Configurables = (props: ConfigurablesPageProps) => { }, [isImportEnv]); useEffect(() => { + setConfigVariables([]); getConfigVariables() getProjectInfo() const fetchTomlValues = async () => { @@ -140,7 +144,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 +203,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 +346,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 00000000000..66627f80b57 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/DevantConfigurables.tsx @@ -0,0 +1,218 @@ +/** + * 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 { 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 { projectPath, projectToml } = usePlatformExtContext(); + const { addModal, closeModal } = useModalStack(); + + const { data: existingConfigVariables = [] } = useQuery({ + queryFn: () => + rpcClient.getBIDiagramRpcClient().getConfigVariablesV2({ + projectPath, + includeLibraries: false, + }), + queryKey: ["config-variables", projectPath], + select: (data) => { + const configNames: string[] = []; + const configVars = (data.configVariables as any)?.[ + `${projectToml?.values?.package?.org}/${projectToml?.values?.package?.name}` + ]?.[""] as ConfigVariable[]; + configVars.forEach((configVar) => + configNames.push(configVar?.properties?.variable?.value?.toString() || ""), + ); + return configNames; + }, + }); + + + const { mutate, isPending } = useMutation({ + mutationFn: (data: { + name: string; + value: string; + isSecret: boolean; + refreshConfigVariables: () => Promise + }) => { + if(devantExpressionEditor?.onAddDevantConfig){ + return devantExpressionEditor.onAddDevantConfig(data.name, data.value, data.isSecret) + } + }, + onSuccess: (_, input) => { + props.onChange(input.name, false); + closeModal(POPUP_IDS.CONFIGURABLES); + input.refreshConfigVariables(); + }, + }); + + const onAddNewConfigurable = (refreshConfigVariables: () => Promise) => { + addModal( + mutate({...data, refreshConfigVariables })} + existingNames={[ ...existingConfigVariables, ...(devantExpressionEditor?.devantConfigs || [] )]} + isSaving={isPending} + />, 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 { + onSave: (data: DevantNewConfigurableData) => void; + existingNames?: string[]; + isSaving?: boolean; +} + +const DevantNewConfigurableForm: React.FC = ({ + onSave, + existingNames = [], + isSaving = false, +}) => { + const [name, setName] = useState(""); + const [value, setValue] = useState(""); + const [isSecret, setIsSecret] = useState(false); + const [errors, setErrors] = useState<{ name?: string; value?: string }>({}); + + 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; + } + + onSave({ + 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/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/index.tsx index b7d8701783a..ba8aaada826 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 ? @@ -266,11 +269,24 @@ const HelperPaneNewEl = ({
+ {devantExpressionEditor && ( + menuItemRefs.current[0] = el} + to="DEVANT_CONFIGS" + > + + {getHelperPaneIcon(HelperPaneIconType.CONFIGURABLE)} + + Devant Configs + + + + )} {((forcedValueTypeConstraint && forcedValueTypeConstraint.length > 0)) && ( <> {valueCreationOptions.length > 0 && ( menuItemRefs.current[0] = el} + ref={el => menuItemRefs.current[1] = el} to="CREATE_VALUE" data={recordTypeField} > @@ -284,7 +300,7 @@ const HelperPaneNewEl = ({ )} )} menuItemRefs.current[2] = el} + ref={el => menuItemRefs.current[3] = el} to="INPUTS" > @@ -295,7 +311,7 @@ const HelperPaneNewEl = ({ menuItemRefs.current[1] = el} + ref={el => menuItemRefs.current[2] = el} to="VARIABLES" > @@ -306,7 +322,7 @@ const HelperPaneNewEl = ({ menuItemRefs.current[3] = el} + ref={el => menuItemRefs.current[4] = el} to="CONFIGURABLES" > @@ -386,6 +402,22 @@ const HelperPaneNewEl = ({ /> + {devantExpressionEditor && ( + + Devant Configs + + + )} + Create Value @@ -528,6 +561,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/PackageOverview/PlatformExtPopover.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/PlatformExtPopover.tsx new file mode 100644 index 00000000000..49a29be694a --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/PlatformExtPopover.tsx @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import 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, + IManageDirContextCmdParams, + CommandIds as PlatformExtCommandIds, +} from "@wso2/wso2-platform-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; + +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; +} + +export function PlatformExtPopover(props: DiagnosticsPopUpProps) { + const { isVisible, onClose, anchorEl } = props; + const { platformExtState, platformRpcClient } = 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 handleLogin = () => { + rpcClient.getCommonRpcClient().executeCommand({ + commands: [PlatformExtCommandIds.SignIn, { extName: "Devant" } as ICmdParamsBase], + }); + }; + + const nonCriticalEnvs = platformExtState?.envs?.filter((env) => !env.critical) || []; + + const handleEnvSelect = () => { + rpcClient + .getCommonRpcClient() + .showQuickPick({ + items: nonCriticalEnvs.map((env) => env.name) || [], + options: { title: "Select Environment to Connect" }, + }) + .then((resp) => { + const selectedEnv = nonCriticalEnvs.find((env) => env.name === resp); + if (selectedEnv) { + platformRpcClient.setSelectedEnv(selectedEnv.id); + } + }); + }; + + const handleIntegrationSelect = () => { + rpcClient + .getCommonRpcClient() + .showQuickPick({ + items: platformExtState?.components.map((item) => item?.metadata?.name) || [], + options: { title: "Select Integration" }, + }) + .then((resp) => { + const selectedIntegration = platformExtState?.components.find((env) => env.metadata.name === resp); + if (selectedIntegration) { + platformRpcClient.setSelectedComponent(selectedIntegration.metadata?.id || ""); + } + }); + }; + + return ( + <> + + + {platformExtState?.userInfo ? ( + <> + + + Account + {platformExtState?.userInfo?.userEmail} + + + + + + + + Organization + {platformExtState?.selectedContext?.org?.name} + + + + + Project + + {platformExtState?.selectedContext?.project?.name} + + + + {platformExtState?.selectedComponent && ( + + + Integration + {platformExtState?.components?.length > 1 ? ( + + {platformExtState?.selectedComponent?.metadata?.name} + + ) : ( + + {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, + ); + }} + /> + +
+ )} + + ) : ( + + + Login to your Devant + account +
to manage your project in the cloud +
+
+ )} +
+
+ + ); +} 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 c3f4523ade2..48bd7c1542a 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/PackageOverview/index.tsx @@ -16,19 +16,18 @@ * under the License. */ -import React, { useEffect, useMemo, useState } from "react"; +import React, { ReactNode, useEffect, useMemo, 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"; @@ -37,10 +36,11 @@ 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 { PlatformExtPopover } from "./PlatformExtPopover"; const SpinnerContainer = styled.div` display: flex; @@ -99,6 +99,7 @@ const HeaderControls = styled.div` display: flex; gap: 8px; margin-right: 16px; + align-items: center; `; const MainContent = styled.div<{ fullWidth?: boolean }>` @@ -296,9 +297,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,14 +319,13 @@ const DeploymentBody = styled.div` `; interface DeploymentOptionProps { - title: string; + title: ReactNode; description: string; buttonText: string; isExpanded: boolean; onToggle: () => void; onDeploy: () => void; learnMoreLink?: string; - hasDeployableIntegration?: boolean; secondaryAction?: { description: string; buttonText: string; @@ -335,9 +342,9 @@ function DeploymentOption({ onDeploy, learnMoreLink, secondaryAction, - hasDeployableIntegration }: DeploymentOptionProps) { const { rpcClient } = useRpcContext(); + const { deployableArtifacts } = usePlatformExtContext(); const openLearnMoreURL = () => { rpcClient.getCommonRpcClient().openExternalUrl({ @@ -377,8 +384,8 @@ function DeploymentOption({ e.stopPropagation(); onDeploy(); }} - disabled={!hasDeployableIntegration} - tooltip={hasDeployableIntegration ? "" : "No deployable integration found"} + disabled={!deployableArtifacts?.exists} + tooltip={deployableArtifacts?.exists ? "" : "No deployable integration found"} > {buttonText} @@ -403,8 +410,6 @@ interface DeploymentOptionsProps { handleJarBuild: () => void; handleDeploy: () => Promise; goToDevant: () => void; - devantMetadata: DevantMetadata | undefined; - hasDeployableIntegration: boolean; } function DeploymentOptions({ @@ -412,11 +417,10 @@ 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 +434,7 @@ function DeploymentOptions({ }); }; + const isDeployed = platformExtState?.isLoggedIn ? !!platformExtState?.selectedComponent : platformExtState?.hasPossibleComponent; return ( <> @@ -437,20 +442,38 @@ 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", @@ -471,7 +494,6 @@ function DeploymentOptions({ isExpanded={expandedOptions.has('docker')} onToggle={() => toggleOption('docker')} onDeploy={handleDockerBuild} - hasDeployableIntegration={hasDeployableIntegration} /> toggleOption('vm')} onDeploy={handleJarBuild} - hasDeployableIntegration={hasDeployableIntegration} />
@@ -518,8 +539,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 +557,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 && ( <> )} + + + setDevantBtnAnchor(null)} isVisible={!!devantBtnAnchor} /> ); @@ -951,7 +971,15 @@ export function PackageOverview(props: PackageOverviewProps) { {!isLibrary && ( - + + {/* + + */} {!isInDevant && <> 0} /> @@ -971,7 +997,6 @@ export function PackageOverview(props: PackageOverviewProps) { projectStructure={projectStructure} handleDeploy={handleDeploy} goToDevant={goToDevant} - devantMetadata={devantMetadata} /> } diff --git a/workspaces/ballerina/ballerina-visualizer/webpack.config.js b/workspaces/ballerina/ballerina-visualizer/webpack.config.js index 41959d7e77f..86534597884 100644 --- a/workspaces/ballerina/ballerina-visualizer/webpack.config.js +++ b/workspaces/ballerina/ballerina-visualizer/webpack.config.js @@ -38,7 +38,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/component-diagram/src/components/Diagram.tsx b/workspaces/ballerina/component-diagram/src/components/Diagram.tsx index eec59c26dc4..0ceb7c7852b 100644 --- a/workspaces/ballerina/component-diagram/src/components/Diagram.tsx +++ b/workspaces/ballerina/component-diagram/src/components/Diagram.tsx @@ -51,7 +51,7 @@ export interface DiagramProps { onFunctionSelect: (func: CDFunction | CDResourceFunction) => void; onAutomationSelect: (automation: CDAutomation) => void; onConnectionSelect: (connection: CDConnection) => void; - onDeleteComponent: (component: CDListener | CDService | CDAutomation | CDConnection) => void; + onDeleteComponent: (component: CDListener | CDService | CDAutomation | CDConnection, nodeType?: string) => void; } export type GQLFuncListType = Record>; diff --git a/workspaces/ballerina/component-diagram/src/components/DiagramContext.tsx b/workspaces/ballerina/component-diagram/src/components/DiagramContext.tsx index 0db903eeb2b..829e3f776cf 100644 --- a/workspaces/ballerina/component-diagram/src/components/DiagramContext.tsx +++ b/workspaces/ballerina/component-diagram/src/components/DiagramContext.tsx @@ -31,7 +31,7 @@ export interface DiagramContextState { onFunctionSelect: (func: CDFunction | CDResourceFunction) => void; onAutomationSelect: (automation: CDAutomation) => void; onConnectionSelect: (connection: CDConnection) => void; - onDeleteComponent: (component: CDListener | CDService | CDAutomation | CDConnection) => void; + onDeleteComponent: (component: CDListener | CDService | CDAutomation | CDConnection, nodeType?: string) => void; onToggleNodeExpansion: (nodeId: string) => void; // Toggle expansion state of a node onToggleGraphQLGroup?: (serviceUuid: string, group: "Query" | "Subscription" | "Mutation") => void; } diff --git a/workspaces/ballerina/component-diagram/src/components/nodes/ConnectionNode/ConnectionNodeWidget.tsx b/workspaces/ballerina/component-diagram/src/components/nodes/ConnectionNode/ConnectionNodeWidget.tsx index 0bc3afedeee..81dee634aeb 100644 --- a/workspaces/ballerina/component-diagram/src/components/nodes/ConnectionNode/ConnectionNodeWidget.tsx +++ b/workspaces/ballerina/component-diagram/src/components/nodes/ConnectionNode/ConnectionNodeWidget.tsx @@ -185,7 +185,7 @@ export function ConnectionNodeWidget(props: ConnectionNodeWidgetProps) { const menuItems: Item[] = [ { id: "edit", label: "Edit", onClick: () => handleOnClick() }, - { id: "delete", label: "Delete", onClick: () => onDeleteComponent(model.node) }, + { id: "delete", label: "Delete", onClick: () => onDeleteComponent(model.node, model.getType()) }, ]; return ( diff --git a/workspaces/common-libs/font-wso2-vscode/src/icons/Devant.svg b/workspaces/common-libs/font-wso2-vscode/src/icons/Devant.svg new file mode 100644 index 00000000000..f1ccc95b08e --- /dev/null +++ b/workspaces/common-libs/font-wso2-vscode/src/icons/Devant.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx b/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx index e6f54da7e95..48f92ccd866 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/SidePanel/SidePanel.tsx @@ -69,7 +69,7 @@ const SubPanelContainer = styled.div` box-shadow: 0 5px 10px 0 var(--vscode-badge-background); background-color: var(--vscode-editor-background); color: var(--vscode-editor-foreground); - z-index: 1500; + z-index: 1900; opacity: ${(props: SidePanelProps) => props.isSubPanelOpen ? 1 : 0}; transform: translateX(${(props: SidePanelProps) => props.alignment === 'left' ? (props.isSubPanelOpen ? '0%' : '-100%') diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts index 653ee327623..a2ea5b6db47 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts @@ -258,6 +258,11 @@ export interface GetMarketplaceListReq { request: GetMarketplaceItemsParams; } +export interface GetMarketplaceItemReq { + orgId: string; + serviceId: string; +} + export interface GetMarketplaceIdlReq { orgId: string; serviceId: string; @@ -288,6 +293,53 @@ export interface CreateComponentConnectionReq { generateCreds: boolean; } +export interface CreateThirdPartyConnectionReq { + orgId: string; + orgUuid: string; + projectId: string; + componentId: string; + name: string; + serviceId: string; + serviceSchemaId: string; + endpointRefs: Record; + sensitiveKeys: string[]; +} + +export type MarketplaceIdlTypes = 'UDP' | 'TCP' | 'WSDL' | 'Proto3' | 'GraphQL_SDL' | 'OpenAPI' | 'AsyncAPI'; + +export type MarketplaceServiceTypes = 'ASYNC_API' | 'GRPC' | 'GRAPHQL' | 'SOAP' | 'REST'; + +export type RegisterMarketplaceConfigMap = Record< + string, + { + environmentTemplateIds: string[]; + values: { + key: string; + value: string; + isOptional?: boolean; + }[]; + name: string; + } +>; + +export interface RegisterMarketplaceConnectionReq { + orgId: string; + orgUuid: string; + projectId: string; + name: string; + serviceType: MarketplaceServiceTypes; + idlType: MarketplaceIdlTypes; + idlContent: string; + schemaEntries: { + name: string; + type: string; + description?: string; + isSensitive: boolean; + isOptional?: boolean; + }[]; + configs: RegisterMarketplaceConfigMap; +} + export interface DeleteConnectionReq { orgId: string; connectionId: string; @@ -503,7 +555,7 @@ export interface IChoreoRPCClient { getMarketplaceItems(params: GetMarketplaceListReq): Promise; getMarketplaceIdl(params: GetMarketplaceIdlReq): Promise; getConnections(params: GetConnectionsReq): Promise; - getConnectionItem(params: GetConnectionItemReq): Promise; + getConnectionItem(params: GetConnectionItemReq): Promise; createComponentConnection(params: CreateComponentConnectionReq): Promise; deleteConnection(params: DeleteConnectionReq): Promise; getConnectionGuide(params: GetConnectionGuideReq): Promise; @@ -600,7 +652,7 @@ export class ChoreoRpcWebview implements IChoreoRPCClient { getConnections(params: GetConnectionsReq): Promise { return this._messenger.sendRequest(ChoreoRpcGetConnections, HOST_EXTENSION, params); } - getConnectionItem(params: GetConnectionItemReq): Promise { + getConnectionItem(params: GetConnectionItemReq): Promise { return this._messenger.sendRequest(ChoreoRpcGetConnectionItem, HOST_EXTENSION, params); } createComponentConnection(params: CreateComponentConnectionReq): Promise { @@ -690,7 +742,7 @@ export const ChoreoRpcGetMarketplaceItemIdl: RequestType = { method: "rpc/connections/getConnections", }; -export const ChoreoRpcGetConnectionItem: RequestType = { +export const ChoreoRpcGetConnectionItem: RequestType = { method: "rpc/connections/getConnectionItem", }; export const ChoreoRpcCreateComponentConnection: RequestType = { diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts index a7b50c84f86..bb2820ab355 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts @@ -5,6 +5,11 @@ export interface ICmdParamsBase { extName?: ExtensionName; } +export interface ICreateDirCtxCmdParams extends ICmdParamsBase { + skipComponentExistCheck?: boolean; + fsPath?: string; +} + export interface ICloneProjectCmdParams extends ICmdParamsBase { organization: Organization; project: Project; @@ -15,7 +20,7 @@ export interface ICloneProjectCmdParams extends ICmdParamsBase { integrationDisplayType: string; } -export interface ICommitAndPuhCmdParams extends ICmdParamsBase { +export interface ICommitAndPushCmdParams extends ICmdParamsBase { componentPath: string; } diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts index e0cef78c396..445ae11bec6 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts @@ -17,6 +17,8 @@ */ import type { DeploymentStatus } from "../enums"; +import { GetMarketplaceListReq, MarketplaceListResp, GetMarketplaceIdlReq, MarketplaceIdlResp, CreateComponentConnectionReq, GetConnectionsReq, DeleteConnectionReq, GetMarketplaceItemReq, GetConnectionItemReq, GetProjectEnvsReq, CreateThirdPartyConnectionReq, RegisterMarketplaceConnectionReq } from "./cli-rpc.types"; +import { CreateLocalConnectionsConfigReq, DeleteLocalConnectionsConfigReq } from "./messenger-rpc.types"; import type { AuthState, ContextItemEnriched, ContextStoreState, WebviewState } from "./store.types"; export type ExtensionName = "WSO2" | "Choreo" | "Devant"; @@ -30,8 +32,22 @@ export interface IWso2PlatformExtensionAPI { getContextStateStore(): ContextStoreState; openClonedDir(params: openClonedDirReq): Promise; getStsToken(): Promise; + getMarketplaceItems(params: GetMarketplaceListReq): Promise; + getMarketplaceItem(params: GetMarketplaceItemReq): Promise; getSelectedContext(): ContextItemEnriched | null; + getMarketplaceIdl(params: GetMarketplaceIdlReq): Promise; + createComponentConnection(params: CreateComponentConnectionReq): Promise; + createThirdPartyConnection(params: CreateThirdPartyConnectionReq): Promise; + createConnectionConfig: (params: CreateLocalConnectionsConfigReq) => Promise; + registerMarketplaceConnection(params: RegisterMarketplaceConnectionReq): Promise; + getConnections: (params: GetConnectionsReq) => Promise; + getConnection: (params: GetConnectionItemReq) => Promise; + deleteConnection: (params: DeleteConnectionReq) => Promise; + deleteLocalConnectionsConfig: (params: DeleteLocalConnectionsConfigReq) => void; getDevantConsoleUrl: () => Promise; + getProjectEnvs: (params: GetProjectEnvsReq)=> Promise + startProxyServer: (params: StartProxyServerReq) => Promise; + stopProxyServer: (params: StopProxyServerReq) => Promise; // Auth Subscription subscribeAuthState(callback: (state: AuthState)=>void): () => void; @@ -42,6 +58,24 @@ export interface IWso2PlatformExtensionAPI { subscribeContextState(callback: (state: ContextItemEnriched | undefined)=>void): () => void; } +export interface StartProxyServerReq { + orgId: string; + project: string; + component?: string; + env?: string; + skipConnection?: string[]; +} + + +export interface StartProxyServerResp { + proxyServerPort: number; + envVars: { [key: string]: string }; +} + +export interface StopProxyServerReq { + proxyPort: number; +} + export interface openClonedDirReq { orgHandle: string; projectHandle: string; @@ -279,6 +313,7 @@ export interface Environment { apimSandboxEnvId?: string; apimEnvId?: string; isMigrating: boolean; + templateId: string; } export interface ComponentEP { @@ -407,6 +442,8 @@ export interface MarketplaceItem { tags?: string[]; categories?: string[]; visibility: ("PUBLIC" | "ORGANIZATION" | "PROJECT")[]; + isThirdParty?: boolean; + endpointRefs?: Record; } export interface ConnectionStatus { @@ -436,22 +473,25 @@ export interface ConnectionListItem extends ConnectionBase { resourceType: string; } -export interface ConnectionDetailed { - configurations: { - [id: string]: { - environmentUuid: string; - entries: { - [entryName: string]: { - key: string; - keyUuid: string; - value: string; - isSensitive: boolean; - isFile: boolean; - }; +export interface ConnectionConfigurations { + [id: string]: { + environmentUuid: string; + entries: { + [entryName: string]: { + key: string; + keyUuid: string; + value: string; + isSensitive: boolean; + isFile: boolean; + envVariableName: string; }; }; }; - envMapping: object; +} + +export interface ConnectionDetailed extends ConnectionListItem { + configurations: ConnectionConfigurations; + envMapping: Record; visibilities: { organizationUuid: string; projectUuid: string; diff --git a/workspaces/wso2-platform/wso2-platform-extension/.vscode/launch.json b/workspaces/wso2-platform/wso2-platform-extension/.vscode/launch.json index 68346bd2a1b..8699e506271 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/.vscode/launch.json +++ b/workspaces/wso2-platform/wso2-platform-extension/.vscode/launch.json @@ -10,8 +10,8 @@ "type": "extensionHost", "request": "launch", "env": { - "WEB_VIEW_DEV_MODE": "true", - "WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", + "PLATFORM_WEB_VIEW_DEV_MODE": "true", + "PLATFORM_WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", "REQUEST_TRACE_ENABLED": "true" }, "args": [ @@ -38,8 +38,8 @@ ], "envFile": "${workspaceFolder}/.env", "env": { - "WEB_VIEW_DEV_MODE": "true", - "WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", + "PLATFORM_WEB_VIEW_DEV_MODE": "true", + "PLATFORM_WEB_VIEW_DEV_HOST": "http://localhost:3000/main.js", }, "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", diff --git a/workspaces/wso2-platform/wso2-platform-extension/package.json b/workspaces/wso2-platform/wso2-platform-extension/package.json index b568c4f4d9d..789f7ec40db 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/package.json +++ b/workspaces/wso2-platform/wso2-platform-extension/package.json @@ -4,7 +4,7 @@ "description": "Manage WSO2 Choreo and Devant projects in VS Code.", "license": "Apache-2.0", "version": "1.0.18", - "cliVersion": "v1.2.212509091800", + "cliVersion": "v1.2.212512011930", "publisher": "wso2", "bugs": { "url": "https://github.com/wso2/choreo-vscode/issues" @@ -26,6 +26,12 @@ ], "main": "./dist/extension.js", "contributes": { + "jsonValidation": [ + { + "fileMatch": "launch.json", + "url": "./schemas/launch-config-schema.json" + } + ], "commands": [ { "command": "wso2.wso2-platform.sign.in", diff --git a/workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-config-yaml-schema.json b/workspaces/wso2-platform/wso2-platform-extension/schemas/component-config-yaml-schema.json similarity index 100% rename from workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-config-yaml-schema.json rename to workspaces/wso2-platform/wso2-platform-extension/schemas/component-config-yaml-schema.json diff --git a/workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-yaml-schema-1_0.json b/workspaces/wso2-platform/wso2-platform-extension/schemas/component-yaml-schema-1_0.json similarity index 100% rename from workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-yaml-schema-1_0.json rename to workspaces/wso2-platform/wso2-platform-extension/schemas/component-yaml-schema-1_0.json diff --git a/workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-yaml-schema-1_1.json b/workspaces/wso2-platform/wso2-platform-extension/schemas/component-yaml-schema-1_1.json similarity index 100% rename from workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-yaml-schema-1_1.json rename to workspaces/wso2-platform/wso2-platform-extension/schemas/component-yaml-schema-1_1.json diff --git a/workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-yaml-schema-1_2.json b/workspaces/wso2-platform/wso2-platform-extension/schemas/component-yaml-schema-1_2.json similarity index 100% rename from workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-yaml-schema-1_2.json rename to workspaces/wso2-platform/wso2-platform-extension/schemas/component-yaml-schema-1_2.json diff --git a/workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-yaml-schema-init.json b/workspaces/wso2-platform/wso2-platform-extension/schemas/component-yaml-schema-init.json similarity index 100% rename from workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/component-yaml-schema-init.json rename to workspaces/wso2-platform/wso2-platform-extension/schemas/component-yaml-schema-init.json diff --git a/workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/endpoints-yaml-schema.json b/workspaces/wso2-platform/wso2-platform-extension/schemas/endpoints-yaml-schema.json similarity index 100% rename from workspaces/wso2-platform/wso2-platform-extension/yaml-schemas/endpoints-yaml-schema.json rename to workspaces/wso2-platform/wso2-platform-extension/schemas/endpoints-yaml-schema.json diff --git a/workspaces/wso2-platform/wso2-platform-extension/schemas/launch-config-schema.json b/workspaces/wso2-platform/wso2-platform-extension/schemas/launch-config-schema.json new file mode 100644 index 00000000000..c3c7f3ca9bc --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/schemas/launch-config-schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "choreoConnect": { + "description": "Connect with Choreo/Devant when launching the app", + "oneOf": [ + { + "type": "boolean", + "const": true, + "description": "Connect with Choreo/Devant when launching the app" + }, + { + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "Choreo project name" + }, + "component": { + "type": "string", + "description": "Choreo component name" + }, + "env": { + "type": "string", + "description": "Non-critical deployed Choreo environment to connect with" + }, + "skipConnection": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of Choreo connections to skip (if you want to point to local or other instance instead of the one running in Choreo)" + } + }, + "additionalProperties": false + } + ] + } + }, + "type": "object", + "properties": { + "configurations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "choreoConnect": { + "$ref": "#/definitions/choreoConnect" + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts b/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts index a3a2ae96902..305bf5d0a47 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts @@ -16,13 +16,13 @@ * under the License. */ -import type { AuthState, ComponentKind, ContextItemEnriched, ContextStoreComponentState, IWso2PlatformExtensionAPI, openClonedDirReq } from "@wso2/wso2-platform-core"; +import type { ComponentKind, GetMarketplaceListReq, IWso2PlatformExtensionAPI, openClonedDirReq, GetMarketplaceIdlReq, CreateComponentConnectionReq, CreateLocalConnectionsConfigReq, GetConnectionsReq, DeleteConnectionReq, DeleteLocalConnectionsConfigReq, GetMarketplaceItemReq, GetConnectionItemReq, StartProxyServerReq, StopProxyServerReq, AuthState, ContextStoreComponentState, ContextItemEnriched, GetProjectEnvsReq, CreateThirdPartyConnectionReq, RegisterMarketplaceConnectionReq } from "@wso2/wso2-platform-core"; import { ext } from "./extensionVariables"; import { hasDirtyRepo } from "./git/util"; import { contextStore } from "./stores/context-store"; import { webviewStateStore } from "./stores/webview-state-store"; import { openClonedDir } from "./uri-handlers"; -import { isSamePath } from "./utils"; +import { createConnectionConfig, deleteLocalConnectionConfig, isSamePath } from "./utils"; export class PlatformExtensionApi implements IWso2PlatformExtensionAPI { private getComponentsOfDir = (fsPath: string, components?: ContextStoreComponentState[]) => { @@ -39,8 +39,22 @@ export class PlatformExtensionApi implements IWso2PlatformExtensionAPI { public getContextStateStore = () => contextStore.getState().state; public openClonedDir = (params: openClonedDirReq) => openClonedDir(params); public getStsToken = () => ext.clients.rpcClient.getStsToken(); + public getMarketplaceItems = (params: GetMarketplaceListReq) => ext.clients.rpcClient.getMarketplaceItems(params); + public getMarketplaceItem = (params: GetMarketplaceItemReq) => ext.clients.rpcClient.getMarketplaceItem(params); public getSelectedContext = () => contextStore.getState().state?.selected || null; + public getMarketplaceIdl = (params: GetMarketplaceIdlReq) => ext.clients.rpcClient.getMarketplaceIdl(params); + public createComponentConnection = (params: CreateComponentConnectionReq) => ext.clients.rpcClient.createComponentConnection(params); + public createThirdPartyConnection = (params: CreateThirdPartyConnectionReq) => ext.clients.rpcClient.createThirdPartyConnection(params); + public createConnectionConfig = (params: CreateLocalConnectionsConfigReq) => createConnectionConfig(params); + public registerMarketplaceConnection = (params: RegisterMarketplaceConnectionReq) => ext.clients.rpcClient.registerMarketplaceConnection(params); + public getConnections = (params: GetConnectionsReq) => ext.clients.rpcClient.getConnections(params); + public getConnection = (params: GetConnectionItemReq) => ext.clients.rpcClient.getConnectionItem(params); + public deleteConnection = (params: DeleteConnectionReq) => ext.clients.rpcClient.deleteConnection(params); + public deleteLocalConnectionsConfig = (params: DeleteLocalConnectionsConfigReq) => deleteLocalConnectionConfig(params); public getDevantConsoleUrl = async() => (await ext.clients.rpcClient.getConfigFromCli()).devantConsoleUrl; + public getProjectEnvs = async(params: GetProjectEnvsReq) => ext.clients.rpcClient.getEnvs(params); + public startProxyServer = async(params: StartProxyServerReq) => ext.clients.rpcClient.startProxyServer(params); + public stopProxyServer = async(params: StopProxyServerReq) => ext.clients.rpcClient.stopProxyServer(params); // Auth state subscriptions public subscribeAuthState = (callback: (state: AuthState)=>void) => ext.authProvider?.subscribe((state)=>callback(state.state)) ?? (() => {}); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts index 2391ba75ab2..2df05f6935b 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts @@ -35,6 +35,7 @@ import type { CreateConfigYamlReq, CreateDeploymentReq, CreateProjectReq, + CreateThirdPartyConnectionReq, CredentialItem, DeleteCompReq, DeleteConnectionReq, @@ -66,6 +67,7 @@ import type { GetGitTokenForRepositoryReq, GetGitTokenForRepositoryResp, GetMarketplaceIdlReq, + GetMarketplaceItemReq, GetMarketplaceListReq, GetProjectEnvsReq, GetProxyDeploymentInfoReq, @@ -78,12 +80,17 @@ import type { IsRepoAuthorizedReq, IsRepoAuthorizedResp, MarketplaceIdlResp, + MarketplaceItem, MarketplaceListResp, Project, ProjectBuildLogsData, PromoteProxyDeploymentReq, ProxyDeploymentInfo, + RegisterMarketplaceConnectionReq, RequestPromoteApprovalReq, + StartProxyServerReq, + StartProxyServerResp, + StopProxyServerReq, SubscriptionsResp, ToggleAutoBuildReq, ToggleAutoBuildResp, @@ -474,6 +481,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response; } + async getMarketplaceItem(params: GetMarketplaceItemReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: MarketplaceItem = await this.client.sendRequest("connections/getMarketplaceItem", params); + return response; + } + async getMarketplaceIdl(params: GetMarketplaceIdlReq): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -490,11 +505,11 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response; } - async getConnectionItem(params: GetConnectionItemReq): Promise { + async getConnectionItem(params: GetConnectionItemReq): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); } - const response: ConnectionListItem = await this.client.sendRequest("connections/getConnectionItem", params); + const response: ConnectionDetailed = await this.client.sendRequest("connections/getConnectionItem", params); return response; } @@ -506,6 +521,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response; } + async createThirdPartyConnection(params: CreateThirdPartyConnectionReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: ConnectionDetailed = await this.client.sendRequest("connections/createThirdPartyConnection", params); + return response; + } + async deleteConnection(params: DeleteConnectionReq): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -521,6 +544,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response; } + async registerMarketplaceConnection(params: RegisterMarketplaceConnectionReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: { service: MarketplaceItem } = await this.client.sendRequest("connections/registerMarketplaceConnection", params); + return response.service; + } + async getAutoBuildStatus(params: GetAutoBuildStatusReq): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -620,6 +651,21 @@ export class ChoreoRPCClient implements IChoreoRPCClient { const response: GetCliRpcResp = await this.client.sendRequest("auth/getConfigs", {}); return response; } + + async startProxyServer(params: StartProxyServerReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: StartProxyServerResp = await this.client.sendRequest("connect/startProxyServer", params); + return response; + } + + async stopProxyServer(params: StopProxyServerReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + await this.client.sendRequest("connect/stopProxyServer", params); + } } export class ChoreoTracer implements Tracer { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts index dc0f7ca16f8..dfb25940e90 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts @@ -21,7 +21,7 @@ import { ComponentKind, type ContextStoreComponentState, GitProvider, - type ICommitAndPuhCmdParams, + type ICommitAndPushCmdParams, type Organization, parseGitURL, } from "@wso2/wso2-platform-core"; @@ -37,7 +37,7 @@ import { getUserInfoForCmd, isRpcActive, setExtensionName } from "./cmd-utils"; export function commitAndPushToGitCommand(context: ExtensionContext) { context.subscriptions.push( - commands.registerCommand(CommandIds.CommitAndPushToGit, async (params: ICommitAndPuhCmdParams) => { + commands.registerCommand(CommandIds.CommitAndPushToGit, async (params: ICommitAndPushCmdParams) => { setExtensionName(params?.extName); const extensionName = webviewStateStore.getState().state.extensionName; try { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts index a65bd5732ed..399531bc4c9 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts @@ -22,7 +22,6 @@ import * as path from "path"; import { ChoreoComponentType, CommandIds, - type ComponentKind, DevantScopes, type ExtensionName, type ICreateComponentCmdParams, diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-directory-context-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-directory-context-cmd.ts index 35e6d7ac820..fb97c37a254 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-directory-context-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-directory-context-cmd.ts @@ -24,6 +24,7 @@ import { CommandIds, type ContextItem, type ICmdParamsBase, + ICreateDirCtxCmdParams, type Organization, type Project, type UserInfo, @@ -38,10 +39,11 @@ import { contextStore, getContextKey, waitForContextStoreToLoad } from "../store import { webviewStateStore } from "../stores/webview-state-store"; import { convertFsPathToUriPath, isSubpath, openDirectory } from "../utils"; import { getUserInfoForCmd, isRpcActive, selectOrg, selectProjectWithCreateNew, setExtensionName } from "./cmd-utils"; +import { initGit } from "../git/main"; export function createDirectoryContextCommand(context: ExtensionContext) { context.subscriptions.push( - commands.registerCommand(CommandIds.CreateDirectoryContext, async (params: ICmdParamsBase) => { + commands.registerCommand(CommandIds.CreateDirectoryContext, async (params: ICreateDirCtxCmdParams) => { setExtensionName(params?.extName); const extensionName = webviewStateStore.getState().state.extensionName; try { @@ -60,6 +62,8 @@ export function createDirectoryContextCommand(context: ExtensionContext) { if (gitRoot) { directoryUrl = Uri.parse(convertFsPathToUriPath(gitRoot)); + } else if(params?.fsPath){ + directoryUrl = Uri.parse(convertFsPathToUriPath(params.fsPath)) } else { const componentDir = await window.showOpenDialog({ canSelectFolders: true, @@ -76,14 +80,19 @@ export function createDirectoryContextCommand(context: ExtensionContext) { directoryUrl = componentDir[0]; } - gitRoot = await getGitRoot(context, directoryUrl.fsPath); - if (!gitRoot) { - throw new Error("Selected directory is not within a git repository"); + try { + gitRoot = await getGitRoot(context, directoryUrl.fsPath); + }catch{ + // ignore error } - - const remotes = await getGitRemotes(context, gitRoot); - if (remotes.length === 0) { - throw new Error("The Selected directory does not have any Git remotes"); + if (!gitRoot) { + if(params?.fsPath){ + const git = await initGit(context); + await git?.init(params?.fsPath) + gitRoot = params?.fsPath; + }else { + throw new Error("Selected directory is not within a git repository"); + } } const selectedOrg = await selectOrg(userInfo, "Select organization"); @@ -98,7 +107,12 @@ export function createDirectoryContextCommand(context: ExtensionContext) { ext?.clients?.rpcClient?.changeOrgContext(selectedOrg?.id?.toString()!), ); - const components = await window.withProgress( + if (!params || !params?.skipComponentExistCheck) { + const remotes = await getGitRemotes(context, gitRoot); + if (remotes.length === 0) { + throw new Error("The Selected directory does not have any Git remotes"); + } + const components = await window.withProgress( { title: `Fetching ${extensionName === "Devant" ? "integrations" : "components"} of project ${selectedProject.name}...`, location: ProgressLocation.Notification, @@ -110,41 +124,41 @@ export function createDirectoryContextCommand(context: ExtensionContext) { projectHandle: selectedProject.handler, projectId: selectedProject.id, }), - ); - - if (components.length > 0) { - // Check if user is trying to link with the correct Git directory - const hasMatchingRemote = components.some((componentItem) => { - const repoUrl = getComponentKindRepoSource(componentItem.spec.source).repo; - const parsedRepoUrl = parseGitURL(repoUrl); - if (parsedRepoUrl) { - const [repoOrg, repoName, repoProvider] = parsedRepoUrl; - return remotes.some((remoteItem) => { - const parsedRemoteUrl = parseGitURL(remoteItem.fetchUrl); - if (parsedRemoteUrl) { - const [repoRemoteOrg, repoRemoteName, repoRemoteProvider] = parsedRemoteUrl; - return repoOrg === repoRemoteOrg && repoName === repoRemoteName && repoRemoteProvider === repoProvider; - } - }); - } - }); - - if (!hasMatchingRemote) { - const resp = await window.showInformationMessage( - "The selected directory does not have any Git remotes that match with the repositories associated with the selected project. Do you wish to continue?", - { modal: true }, - "Continue", - ); - if (resp !== "Continue") { - return; + ); + + if (components.length > 0) { + // Check if user is trying to link with the correct Git directory + const hasMatchingRemote = components.some((componentItem) => { + const repoUrl = getComponentKindRepoSource(componentItem.spec.source).repo; + const parsedRepoUrl = parseGitURL(repoUrl); + if (parsedRepoUrl) { + const [repoOrg, repoName, repoProvider] = parsedRepoUrl; + return remotes.some((remoteItem) => { + const parsedRemoteUrl = parseGitURL(remoteItem.fetchUrl); + if (parsedRemoteUrl) { + const [repoRemoteOrg, repoRemoteName, repoRemoteProvider] = parsedRemoteUrl; + return repoOrg === repoRemoteOrg && repoName === repoRemoteName && repoRemoteProvider === repoProvider; + } + }); + } + }); + + if (!hasMatchingRemote) { + const resp = await window.showInformationMessage( + "The selected directory does not have any Git remotes that match with the repositories associated with the selected project. Do you wish to continue?", + { modal: true }, + "Continue", + ); + if (resp !== "Continue") { + return; + } } } } const contextFilePath = updateContextFile(gitRoot, userInfo, selectedProject, selectedOrg, projectList); - // todo: check this in windows - const isWithinWorkspace = workspace.workspaceFolders?.some((item) => isSubpath(gitRoot!, item.uri?.fsPath)); + const isWithinWorkspace = workspace.workspaceFolders?.some((item) => isSubpath(item.uri?.fsPath, gitRoot!)); if (isWithinWorkspace) { contextStore.getState().refreshState(); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts index 48e63bf667e..b2379f2b33f 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts @@ -22,30 +22,32 @@ import { initGit } from "./git/main"; import { getLogger } from "./logger/logger"; export const activateDevantFeatures = () => { - autoRefetchDevantStsToken(); - showRepoSyncNotification(); + if (process.env.CLOUD_STS_TOKEN) { + autoRefetchDevantStsToken(); + showRepoSyncNotification(); + } }; const autoRefetchDevantStsToken = () => { - const intervalTime = 20 * 60 * 1000; // 20 minutes - const intervalId = setInterval(async () => { - try { - await ext.clients.rpcClient.getStsToken(); - } catch { - getLogger().error("Failed to refresh STS token"); - if (intervalId) { - clearInterval(intervalId); - } - } - }, intervalTime); + const intervalTime = 20 * 60 * 1000; // 20 minutes + const intervalId = setInterval(async () => { + try { + await ext.clients.rpcClient.getStsToken(); + } catch { + getLogger().error("Failed to refresh STS token"); + if (intervalId) { + clearInterval(intervalId); + } + } + }, intervalTime); - ext.context.subscriptions.push({ - dispose: () => { - if (intervalId) { - clearTimeout(intervalId); - } - }, - }); + ext.context.subscriptions.push({ + dispose: () => { + if (intervalId) { + clearInterval(intervalId); + } + }, + }); }; const showRepoSyncNotification = async () => { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/git/util.ts b/workspaces/wso2-platform/wso2-platform-extension/src/git/util.ts index d8d39b015bf..3b03d199812 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/git/util.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/git/util.ts @@ -596,7 +596,7 @@ export const getGitRoot = async (context: ExtensionContext, directoryPath: strin } }; -export const hasDirtyRepo = async (directoryPath: string, context: ExtensionContext, ignoredFileNames: string[] = []): Promise => { +export const hasDirtyRepo = async (directoryPath: string = "", context: ExtensionContext, ignoredFileNames: string[] = []): Promise => { try{ const git = await initGit(context); const repoRoot = await git?.getRepositoryRoot(directoryPath) diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/tarminal-handlers.ts b/workspaces/wso2-platform/wso2-platform-extension/src/tarminal-handlers.ts index f1763451667..f41be1a6dde 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/tarminal-handlers.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/tarminal-handlers.ts @@ -112,4 +112,4 @@ export function addTerminalHandlers() { } } }); -} +} \ No newline at end of file diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts b/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts index 3aa77705b58..4b133803894 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts @@ -80,9 +80,9 @@ export function activateURIHandlers() { contextStore.getState().resetState(); } } - const region = await ext.clients.rpcClient.getCurrentRegion(); - await ext.authProvider?.getState().loginSuccess(userInfo, region); - window.showInformationMessage(`Successfully signed into ${extName}`); + const region = await ext.clients.rpcClient.getCurrentRegion(); + await ext.authProvider?.getState().loginSuccess(userInfo, region); + window.showInformationMessage(`Successfully signed into ${extName}`); } } catch (error: any) { if (!(error instanceof ResponseError) || ![ErrorCode.NoOrgsAvailable, ErrorCode.NoAccountAvailable].includes(error.code)) { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts index 6fe5a7b6682..2c423761d43 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts @@ -16,7 +16,7 @@ * under the License. */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import * as os from "os"; import { join } from "path"; import * as path from "path"; @@ -24,19 +24,25 @@ import { ChoreoComponentType, type ComponentConfigYamlContent, type ComponentYamlContent, + CreateLocalConnectionsConfigReq, + DeleteLocalConnectionsConfigReq, type Endpoint, type EndpointYamlContent, type ReadLocalEndpointsConfigResp, type ReadLocalProxyConfigResp, + deepEqual, getRandomNumber, parseGitURL, } from "@wso2/wso2-platform-core"; import * as yaml from "js-yaml"; -import { type ExtensionContext, Uri, commands, window, workspace } from "vscode"; +import { type ExtensionContext, ProgressLocation, Uri, commands, window, workspace } from "vscode"; import type { IFileStatus } from "./git/git"; import { initGit } from "./git/main"; import { getGitRemotes } from "./git/util"; import { getLogger } from "./logger/logger"; +import { ext } from "./extensionVariables"; +import { dataCacheStore } from "./stores/data-cache-store"; +import { webviewStateStore } from "./stores/webview-state-store"; export const readLocalEndpointsConfig = (componentPath: string): ReadLocalEndpointsConfigResp => { const filterEndpointSchemaPath = (eps: Endpoint[] = []) => @@ -442,3 +448,108 @@ export const getExtVersion = (context: ExtensionContext): string => { const packageJson = JSON.parse(readFileSync(path.join(context?.extensionPath, "package.json"), "utf8")); return packageJson?.version; }; + +export const deleteLocalConnectionConfig = (params: DeleteLocalConnectionsConfigReq) => { + const componentYamlPath = join(params.componentDir, ".choreo", "component.yaml"); + if (existsSync(componentYamlPath)) { + const componentYamlFileContent: ComponentYamlContent = yaml.load(readFileSync(componentYamlPath, "utf8")) as any; + if (componentYamlFileContent.dependencies?.connectionReferences) { + componentYamlFileContent.dependencies.connectionReferences = componentYamlFileContent.dependencies.connectionReferences.filter( + (item) => item.name !== params.connectionName, + ); + } + if (componentYamlFileContent.dependencies?.serviceReferences) { + componentYamlFileContent.dependencies.serviceReferences = componentYamlFileContent.dependencies.serviceReferences.filter( + (item) => item.name !== params.connectionName, + ); + } + writeFileSync(componentYamlPath, yaml.dump(componentYamlFileContent)); + } +} + +export const createConnectionConfig = async (params: CreateLocalConnectionsConfigReq):Promise=>{ + if (existsSync(join(params.componentDir, ".choreo", "endpoints.yaml"))) { + rmSync(join(params.componentDir, ".choreo", "endpoints.yaml")); + } + if (existsSync(join(params.componentDir, ".choreo", "component-config.yaml"))) { + rmSync(join(params.componentDir, ".choreo", "component-config.yaml")); + } + + const org = ext.authProvider?.getUserInfo()?.organizations?.find((item) => item.uuid === params.marketplaceItem?.organizationId); + if (!org) { + return ""; + } + const componentYamlPath = join(params.componentDir, ".choreo", "component.yaml"); + + + let resourceRef = ``; + if(params.marketplaceItem?.isThirdParty){ + resourceRef = `thirdparty:${params.marketplaceItem?.name}/${params.marketplaceItem?.version}`; + }else{ + let project = dataCacheStore + .getState() + .getProjects(org.handle) + ?.find((item) => item.id === params.marketplaceItem?.projectId); + if (!project) { + const projects = await window.withProgress( + { title: `Fetching projects of organization ${org.name}...`, location: ProgressLocation.Notification }, + () => ext.clients.rpcClient.getProjects(org.id.toString()), + ); + project = projects?.find((item) => item.id === params.marketplaceItem?.projectId); + if (!project) { + return ""; + } + } + + let component = dataCacheStore + .getState() + .getComponents(org.handle, project.handler) + ?.find((item) => item.metadata?.id === params.marketplaceItem?.component?.componentId); + if (!component) { + const extName = webviewStateStore.getState().state?.extensionName; + const components = await window.withProgress( + { + title: `Fetching ${extName === "Devant" ? "integrations" : "components"} of project ${project.name}...`, + location: ProgressLocation.Notification, + }, + () => + ext.clients.rpcClient.getComponentList({ + orgHandle: org.handle, + orgId: org.id.toString(), + projectHandle: project?.handler!, + projectId: project?.id!, + }), + ); + component = components?.find((item) => item.metadata?.id === params.marketplaceItem?.component?.componentId); + if(!component){ + return "" + } + } + resourceRef = `service:/${project.handler}/${component?.metadata?.handler}/v1/${params?.marketplaceItem?.component?.endpointId}/${params.visibility}`; + } + if (existsSync(componentYamlPath)) { + const componentYamlFileContent: ComponentYamlContent = yaml.load(readFileSync(componentYamlPath, "utf8")) as any; + const schemaVersion = Number(componentYamlFileContent.schemaVersion); + if (schemaVersion < 1.2) { + componentYamlFileContent.schemaVersion = "1.2"; + } + componentYamlFileContent.dependencies = { + ...componentYamlFileContent.dependencies, + connectionReferences: [...(componentYamlFileContent.dependencies?.connectionReferences ?? []), { name: params?.name, resourceRef }], + }; + const originalContent: ComponentYamlContent = yaml.load(readFileSync(componentYamlPath, "utf8")) as any; + if (!deepEqual(originalContent, componentYamlFileContent)) { + writeFileSync(componentYamlPath, yaml.dump(componentYamlFileContent)); + } + } else { + if (!existsSync(join(params.componentDir, ".choreo"))) { + mkdirSync(join(params.componentDir, ".choreo")); + } + const endpointFileContent: ComponentYamlContent = { + schemaVersion: "1.2", + dependencies: { connectionReferences: [{ name: params?.name, resourceRef }] }, + }; + writeFileSync(componentYamlPath, yaml.dump(endpointFileContent)); + } + return componentYamlPath; +} diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts index 3e181a7a8ea..8bc66170161 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts @@ -127,7 +127,7 @@ import { contextStore } from "../stores/context-store"; import { dataCacheStore } from "../stores/data-cache-store"; import { webviewStateStore } from "../stores/webview-state-store"; import { sendTelemetryEvent, sendTelemetryException } from "../telemetry/utils"; -import { getConfigFileDrifts, getNormalizedPath, getSubPath, goTosource, readLocalEndpointsConfig, readLocalProxyConfig, saveFile } from "../utils"; +import { createConnectionConfig, deleteLocalConnectionConfig, getConfigFileDrifts, getNormalizedPath, getSubPath, goTosource, readLocalEndpointsConfig, readLocalProxyConfig, saveFile } from "../utils"; // Register handlers function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | WebviewView) { @@ -391,112 +391,22 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W } }); messenger.onRequest(CreateLocalConnectionsConfig, async (params: CreateLocalConnectionsConfigReq) => { - if (existsSync(join(params.componentDir, ".choreo", "endpoints.yaml"))) { - rmSync(join(params.componentDir, ".choreo", "endpoints.yaml")); - } - if (existsSync(join(params.componentDir, ".choreo", "component-config.yaml"))) { - rmSync(join(params.componentDir, ".choreo", "component-config.yaml")); - } - - const org = ext.authProvider?.getState().state?.userInfo?.organizations?.find((item) => item.uuid === params.marketplaceItem?.organizationId); - if (!org) { - return; - } - - let project = dataCacheStore - .getState() - .getProjects(org.handle) - ?.find((item) => item.id === params.marketplaceItem?.projectId); - if (!project) { - const projects = await window.withProgress( - { title: `Fetching projects of organization ${org.name}...`, location: ProgressLocation.Notification }, - () => ext.clients.rpcClient.getProjects(org.id.toString()), - ); - project = projects?.find((item) => item.id === params.marketplaceItem?.projectId); - if (!project) { - return; - } - } - - let component = dataCacheStore - .getState() - .getComponents(org.handle, project.handler) - ?.find((item) => item.metadata?.id === params.marketplaceItem?.component?.componentId); - if (!component) { - const extName = webviewStateStore.getState().state?.extensionName; - const components = await window.withProgress( - { - title: `Fetching ${extName === "Devant" ? "integrations" : "components"} of project ${project.name}...`, - location: ProgressLocation.Notification, - }, - () => - ext.clients.rpcClient.getComponentList({ - orgHandle: org.handle, - orgId: org.id.toString(), - projectHandle: project?.handler!, - projectId: project?.id!, - }), - ); - component = components?.find((item) => item.metadata?.id === params.marketplaceItem?.component?.componentId); - if (!component) { - return; - } - } - - const componentYamlPath = join(params.componentDir, ".choreo", "component.yaml"); - const resourceRef = `service:/${project.handler}/${component.metadata?.handler}/v1/${params?.marketplaceItem?.component?.endpointId}/${params.visibility}`; - if (existsSync(componentYamlPath)) { - const componentYamlFileContent: ComponentYamlContent = yaml.load(readFileSync(componentYamlPath, "utf8")) as any; - const schemaVersion = Number(componentYamlFileContent.schemaVersion); - if (schemaVersion < 1.2) { - componentYamlFileContent.schemaVersion = "1.2"; - } - componentYamlFileContent.dependencies = { - ...componentYamlFileContent.dependencies, - connectionReferences: [...(componentYamlFileContent.dependencies?.connectionReferences ?? []), { name: params?.name, resourceRef }], - }; - const originalContent: ComponentYamlContent = yaml.load(readFileSync(componentYamlPath, "utf8")) as any; - if (!deepEqual(originalContent, componentYamlFileContent)) { - writeFileSync(componentYamlPath, yaml.dump(componentYamlFileContent)); - } - } else { - if (!existsSync(join(params.componentDir, ".choreo"))) { - mkdirSync(join(params.componentDir, ".choreo")); - } - const endpointFileContent: ComponentYamlContent = { - schemaVersion: "1.2", - dependencies: { connectionReferences: [{ name: params?.name, resourceRef }] }, - }; - writeFileSync(componentYamlPath, yaml.dump(endpointFileContent)); + const componentYamlPath = await createConnectionConfig(params); + if(componentYamlPath){ + window + .showInformationMessage( + `Connection ${params.name} created and component.yaml updated. Follow the developer guide to finish integration. Once done, commit and push your changes.`, + "View Configurations", + ) + .then((res) => { + if (res === "View Configurations") { + goTosource(componentYamlPath); + } + }); } - - window - .showInformationMessage( - `Connection ${params.name} created and component.yaml updated. Follow the developer guide to finish integration. Once done, commit and push your changes.`, - "View Configurations", - ) - .then((res) => { - if (res === "View Configurations") { - goTosource(componentYamlPath); - } - }); }); messenger.onRequest(DeleteLocalConnectionsConfig, async (params: DeleteLocalConnectionsConfigReq) => { - const componentYamlPath = join(params.componentDir, ".choreo", "component.yaml"); - if (existsSync(componentYamlPath)) { - const componentYamlFileContent: ComponentYamlContent = yaml.load(readFileSync(componentYamlPath, "utf8")) as any; - if (componentYamlFileContent.dependencies?.connectionReferences) { - componentYamlFileContent.dependencies.connectionReferences = componentYamlFileContent.dependencies.connectionReferences.filter( - (item) => item.name !== params.connectionName, - ); - } - if (componentYamlFileContent.dependencies?.serviceReferences) { - componentYamlFileContent.dependencies.serviceReferences = componentYamlFileContent.dependencies.serviceReferences.filter( - (item) => item.name !== params.connectionName, - ); - } - writeFileSync(componentYamlPath, yaml.dump(componentYamlFileContent)); - } + deleteLocalConnectionConfig(params) }); messenger.onRequest(FileExists, (filePath: string) => existsSync(getNormalizedPath(filePath))); messenger.onRequest(ReadFile, (filePath: string) => { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/utils.ts index 54f877444ac..42f96c62485 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/utils.ts @@ -20,11 +20,11 @@ import { Uri, type Webview } from "vscode"; export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { if (shouldUseWebViewDevMode(pathList)) { - return process.env.WEB_VIEW_DEV_HOST; + return process.env.PLATFORM_WEB_VIEW_DEV_HOST; } return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); } function shouldUseWebViewDevMode(pathList: string[]): boolean { - return pathList[pathList.length - 1] === "main.js" && process.env.WEB_VIEW_DEV_MODE === "true" && process.env.WEB_VIEW_DEV_HOST !== undefined; + return pathList[pathList.length - 1] === "main.js" && process.env.PLATFORM_WEB_VIEW_DEV_MODE === "true" && process.env.PLATFORM_WEB_VIEW_DEV_HOST !== undefined; } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/yaml-ls.ts b/workspaces/wso2-platform/wso2-platform-extension/src/yaml-ls.ts index e737c9650c0..a29337457aa 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/yaml-ls.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/yaml-ls.ts @@ -32,7 +32,7 @@ export async function registerYamlLanguageServer(): Promise { return; } const yamlExtensionAPI = await yamlExtension.activate(); - const schemaBasePath = path.join(ext.context.extensionPath, "yaml-schemas"); + const schemaBasePath = path.join(ext.context.extensionPath, "schemas"); const schemas2 = { endpointsYaml: `${SCHEMA}://schema/endpoints`, componentConfigYaml: `${SCHEMA}://schema/component-config`, diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/Markdown/Markdown.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/Markdown/Markdown.tsx index 1eeca98d067..433c1c8a4ce 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/Markdown/Markdown.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/Markdown/Markdown.tsx @@ -83,7 +83,7 @@ const markDownOverrides: { [key: string]: FC> } = { ), // TODO: move into separate component code: ({ children, className, node }) => { - const isInline = !className && node?.position?.end?.line === node?.position?.start?.line + const isInline = !className && node?.position?.end?.line === node?.position?.start?.line; // Extract language from className const match = /language-(\w+)/.exec(className || ""); const language: any = match != null ? match[1] : "markdown"; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx index 1d38c681b30..eb1dd4a51ea 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx @@ -198,11 +198,7 @@ export const ComponentFormGenDetailsSection: FC = ({ onNextClick, organiz if (!invalidRepoMsg && repoUrl && !isRepoAuthorizedResp?.isAccessible && provider) { if (provider === GitProvider.GITHUB) { if (isRepoAuthorizedResp?.retrievedRepos) { - invalidRepoMsg = ( - - {extensionName} lacks access to the selected repository. - - ); + invalidRepoMsg = {extensionName} lacks access to the selected repository.; invalidRepoAction = "Grant Access"; onInvalidRepoActionClick = () => ChoreoWebViewAPI.getInstance().triggerGithubInstallFlow(organization.id?.toString()); } else { @@ -214,9 +210,7 @@ export const ComponentFormGenDetailsSection: FC = ({ onNextClick, organiz } else { onInvalidRepoActionClick = () => ChoreoWebViewAPI.getInstance().openExternalChoreo(`organizations/${organization.handle}/settings/credentials`); if (isRepoAuthorizedResp?.retrievedRepos) { - invalidRepoMsg = ( - Selected Credential does not have sufficient permissions to access the repository. - ); + invalidRepoMsg = Selected Credential does not have sufficient permissions to access the repository.; invalidRepoAction = "Manage Credentials"; } else { invalidRepoMsg = `Failed to retrieve ${toSentenceCase(provider)} repositories using the selected credential.`; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx index ae34163f5e1..5ddb1065a41 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx @@ -18,7 +18,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { RequiredFormInput } from "@wso2/ui-toolkit"; import { GitProvider, type NewComponentWebviewProps, buildGitURL } from "@wso2/wso2-platform-core"; import classNames from "classnames"; import debounce from "lodash.debounce"; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormSummarySection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormSummarySection.tsx index 40ba4391f9e..f8bc7e202ee 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormSummarySection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormSummarySection.tsx @@ -155,12 +155,26 @@ export const ComponentFormSummarySection: FC = ({ } if (type === ChoreoComponentType.Service && endpointDetails?.endpoints?.length) { - items.push( - 1 ? "s" : ""}`} - />, - ); + if ([ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildDetails?.buildPackLang as ChoreoBuildPackNames)) { + // if ballerina or MI + if (!buildDetails?.useDefaultEndpoints) { + // if not using default endpoints + items.push( + 1 ? "s" : ""}`} + />, + ); + } + } else { + // if not using ballerina or MI + items.push( + 1 ? "s" : ""}`} + />, + ); + } } }