diff --git a/workspaces/wso2-platform/wso2-platform-core/src/constants.ts b/workspaces/wso2-platform/wso2-platform-core/src/constants.ts index 678ea7b0980..06f83669ce6 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/constants.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/constants.ts @@ -35,6 +35,7 @@ export const CommandIds = { CreateComponentDependency: "wso2.wso2-platform.component.create.dependency", ViewDependency: "wso2.wso2-platform.component.view.dependency", OpenCompSrcDir: "wso2.wso2-platform.open.component.src", + CommitAndPushToGit: "wso2.wso2-platform.push-to-git", // TODO: add command & code lens to delete dependency }; diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts index 60981cb8b68..653ee327623 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/cli-rpc.types.ts @@ -31,11 +31,14 @@ import type { DeploymentLogsData, DeploymentTrack, Environment, + GitRepoMetadata, + GithubOrganization, MarketplaceItem, Pagination, Project, ProjectBuildLogsData, ProxyDeploymentInfo, + SubscriptionItem, } from "./common.types"; import type { InboundConfig } from "./config-file.types"; @@ -53,6 +56,11 @@ export interface GetCredentialsReq { orgId: string; orgUuid: string; } +export interface GetCredentialDetailsReq { + orgId: string; + orgUuid: string; + credentialId: string; +} export interface IsRepoAuthorizedReq { orgId: string; repoUrl: string; @@ -385,6 +393,10 @@ export interface GetBuildLogsReq { displayType: string; projectId: string; buildId: number; + orgUuid: string; + buildRef: string; + deploymentTrackId: string; + clusterId: string; } export interface GetBuildLogsForTypeReq { @@ -394,6 +406,76 @@ export interface GetBuildLogsForTypeReq { buildId: number; } +export interface GetSubscriptionsReq { + orgId: string; + cloudType?: string; +} + +export interface UpdateCodeServerReq { + orgId: string; + orgUuid: string; + orgHandle: string; + projectId: string; + componentId: string; + sourceCommitHash: string; +} + +export interface GetGitTokenForRepositoryReq { + orgId: string; + gitOrg: string; + gitRepo: string; + secretRef: string; +} + +export interface GetGitTokenForRepositoryResp { + token: string; + gitOrganization: string; + gitRepository: string; + vendor: string; + username: string; + serverUrl: string; +} + +export interface GetGitMetadataReq { + orgId: string; + gitOrgName: string; + gitRepoName: string; + branch: string; + relativePath: string; + secretRef: string; +} + +export interface GetGitMetadataResp { + metadata: GitRepoMetadata; +} + +export interface SubscriptionsResp { + count: number; + list: SubscriptionItem[]; + cloudType: string; + emailType: string; +} + +export interface GetAuthorizedGitOrgsReq { + orgId: string; + credRef: string; +} + +export interface GetAuthorizedGitOrgsResp { + gitOrgs: GithubOrganization[]; +} + +export interface GetCliRpcResp { + billingConsoleUrl: string; + choreoConsoleUrl: string; + devantConsoleUrl: string; + ghApp: { + installUrl: string; + authUrl: string; + clientId: string; + }; +} + export interface IChoreoRPCClient { getComponentItem(params: GetComponentItemReq): Promise; getDeploymentTracks(params: GetDeploymentTracksReq): Promise; @@ -405,7 +487,9 @@ export interface IChoreoRPCClient { getBuildPacks(params: BuildPackReq): Promise; getRepoBranches(params: GetBranchesReq): Promise; isRepoAuthorized(params: IsRepoAuthorizedReq): Promise; + getAuthorizedGitOrgs(params: GetAuthorizedGitOrgsReq): Promise; getCredentials(params: GetCredentialsReq): Promise; + getCredentialDetails(params: GetCredentialDetailsReq): Promise; deleteComponent(params: DeleteCompReq): Promise; getBuilds(params: GetBuildsReq): Promise; createBuild(params: CreateBuildReq): Promise; @@ -433,6 +517,9 @@ export interface IChoreoRPCClient { cancelApprovalRequest(params: CancelApprovalReq): Promise; requestPromoteApproval(params: RequestPromoteApprovalReq): Promise; promoteProxyDeployment(params: PromoteProxyDeploymentReq): Promise; + getSubscriptions(params: GetSubscriptionsReq): Promise; + getGitTokenForRepository(params: GetGitTokenForRepositoryReq): Promise; + getGitRepoMetadata(params: GetGitMetadataReq): Promise; } export class ChoreoRpcWebview implements IChoreoRPCClient { @@ -462,9 +549,15 @@ export class ChoreoRpcWebview implements IChoreoRPCClient { isRepoAuthorized(params: IsRepoAuthorizedReq): Promise { return this._messenger.sendRequest(ChoreoRpcIsRepoAuthorizedRequest, HOST_EXTENSION, params); } + getAuthorizedGitOrgs(params: GetAuthorizedGitOrgsReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetAuthorizedGitOrgsRequest, HOST_EXTENSION, params); + } getCredentials(params: GetCredentialsReq): Promise { return this._messenger.sendRequest(ChoreoRpcGetCredentialsRequest, HOST_EXTENSION, params); } + getCredentialDetails(params: GetCredentialDetailsReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetCredentialDetailsRequest, HOST_EXTENSION, params); + } deleteComponent(params: DeleteCompReq): Promise { return this._messenger.sendRequest(ChoreoRpcDeleteComponentRequest, HOST_EXTENSION, params); } @@ -549,6 +642,15 @@ export class ChoreoRpcWebview implements IChoreoRPCClient { promoteProxyDeployment(params: PromoteProxyDeploymentReq): Promise { return this._messenger.sendRequest(ChoreoRpcPromoteProxyDeployment, HOST_EXTENSION, params); } + getSubscriptions(params: GetSubscriptionsReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetSubscriptions, HOST_EXTENSION, params); + } + getGitTokenForRepository(params: GetGitTokenForRepositoryReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetGitTokenForRepository, HOST_EXTENSION, params); + } + getGitRepoMetadata(params: GetGitMetadataReq): Promise { + return this._messenger.sendRequest(ChoreoRpcGetGitRepoMetadata, HOST_EXTENSION, params); + } } export const ChoreoRpcGetProjectsRequest: RequestType = { method: "rpc/project/getProjects" }; @@ -559,7 +661,11 @@ export const ChoreoRpcCreateComponentRequest: RequestType = { method: "rpc/component/getBuildPacks" }; export const ChoreoRpcGetBranchesRequest: RequestType = { method: "rpc/repo/getBranches" }; export const ChoreoRpcIsRepoAuthorizedRequest: RequestType = { method: "rpc/repo/isRepoAuthorized" }; +export const ChoreoRpcGetAuthorizedGitOrgsRequest: RequestType = { + method: "rpc/repo/getAuthorizedGitOrgs", +}; export const ChoreoRpcGetCredentialsRequest: RequestType = { method: "rpc/repo/getCredentials" }; +export const ChoreoRpcGetCredentialDetailsRequest: RequestType = { method: "rpc/repo/getCredentialDetails" }; export const ChoreoRpcDeleteComponentRequest: RequestType = { method: "rpc/component/delete" }; export const ChoreoRpcCreateBuildRequest: RequestType = { method: "rpc/build/create" }; export const ChoreoRpcGetDeploymentTracksRequest: RequestType = { @@ -612,3 +718,12 @@ export const ChoreoRpcRequestPromoteApproval: RequestType = { method: "rpc/deployment/promoteProxy", }; +export const ChoreoRpcGetSubscriptions: RequestType = { + method: "rpc/auth/getSubscriptions", +}; +export const ChoreoRpcGetGitTokenForRepository: RequestType = { + method: "rpc/repo/gitTokenForRepository", +}; +export const ChoreoRpcGetGitRepoMetadata: RequestType = { + method: "rpc/repo/getRepoMetadata", +}; diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts index 4f62c0fa3a2..a7b50c84f86 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/cmd-params.ts @@ -15,6 +15,10 @@ export interface ICloneProjectCmdParams extends ICmdParamsBase { integrationDisplayType: string; } +export interface ICommitAndPuhCmdParams extends ICmdParamsBase { + componentPath: string; +} + export interface ICreateDependencyParams extends ICmdParamsBase { componentFsPath?: string; isCodeLens?: boolean; diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/common.types.ts index 98630317e03..52e4cf42a5c 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 @@ -28,6 +28,7 @@ export interface IWso2PlatformExtensionAPI { getWebviewStateStore(): WebviewState; getContextStateStore(): ContextStoreState; openClonedDir(params: openClonedDirReq): Promise; + getStsToken(): Promise; } export interface openClonedDirReq { @@ -67,6 +68,7 @@ export interface ComponentKindSource { bitbucket?: ComponentKindGitProviderSource; github?: ComponentKindGitProviderSource; gitlab?: ComponentKindGitProviderSource; + secretRef?: string; } export interface ComponentKindBuildDocker { @@ -172,6 +174,8 @@ export interface BuildKind { completedAt: string; images: { id: string; createdAt: string; updatedAt: string }[]; gitCommit: { message: string; author: string; date: string; email: string }; + clusterId: string; + buildRef: string; }; } @@ -548,4 +552,45 @@ export interface CredentialItem { organizationUuid: string; type: string; referenceToken: string; + serverUrl: string; +} + +export interface SubscriptionItem { + subscriptionId: string; + tierId: string; + supportPlanId: string; + cloudType: string; + subscriptionType: string; + subscriptionBillingProvider: string; + subscriptionBillingProviderStatus: string; +} + +export interface GithubRepository { + name: string; +} + +export interface GithubOrganization { + orgName: string; + orgHandler: string; + repositories: GithubRepository[]; +} + +export interface GitRepoMetadata { + isBareRepo: boolean; + isSubPathEmpty: boolean; + isSubPathValid: boolean; + isValidRepo: boolean; + hasBallerinaTomlInPath: boolean; + hasBallerinaTomlInRoot: boolean; + isDockerfilePathValid: boolean; + hasDockerfileInPath: boolean; + isDockerContextPathValid: boolean; + isOpenApiFilePathValid: boolean; + hasOpenApiFileInPath: boolean; + hasPomXmlInPath: boolean; + hasPomXmlInRoot: boolean; + isBuildpackPathValid: boolean; + isTestRunnerPathValid: boolean; + isProcfileExists: boolean; + isEndpointYamlExists: boolean; } diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/messenger-rpc.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/messenger-rpc.types.ts index b0785c461ec..32c61097855 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/messenger-rpc.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/messenger-rpc.types.ts @@ -76,6 +76,8 @@ export const CreateLocalEndpointsConfig: RequestType = { method: "createLocalProxyConfig" }; export const CreateLocalConnectionsConfig: RequestType = { method: "createLocalConnectionsConfig" }; export const DeleteLocalConnectionsConfig: RequestType = { method: "deleteLocalConnectionsConfig" }; +export const CloneRepositoryIntoCompDir: RequestType = { method: "cloneRepositoryIntoCompDir" }; +export const PushEverythingToRemoteRepo: RequestType = { method: "pushEverythingToRemoteRepo" }; const NotificationMethods = { onAuthStateChanged: "onAuthStateChanged", @@ -103,6 +105,28 @@ export interface OpenTestViewReq { endpoints: ComponentEP[]; } +export interface PushEverythingToRemoteRepoReq { + dirPath: string; + componentName: string; +} + +export interface CloneRepositoryIntoCompDirReq { + cwd: string; + subpath: string; + org: Organization; + componentName: string; + repo: { + provider: string; + orgName: string; + orgHandler: string; + repo: string; + serverUrl?: string; + branch: string; + secretRef: string; + isBareRepo: boolean; + }; +} + export interface SubmitComponentCreateReq { org: Organization; project: Project; diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/store.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/store.types.ts index 612f7cb9e6f..3311ecb3bd9 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/store.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/store.types.ts @@ -20,7 +20,7 @@ import type { CommitHistory, ComponentKind, Environment, ExtensionName, Organiza export interface DataCacheState { orgs?: { - [orgHandle: string]: { + [orgRegionHandle: string]: { projects?: { [projectHandle: string]: { data?: Project; @@ -41,6 +41,7 @@ export interface DataCacheState { export interface AuthState { userInfo: UserInfo | null; + region: "US" | "EU"; } export interface WebviewState { diff --git a/workspaces/wso2-platform/wso2-platform-core/src/types/webview-prop.types.ts b/workspaces/wso2-platform/wso2-platform-core/src/types/webview-prop.types.ts index 045cc9b28c7..b9bf3bf4ec3 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/types/webview-prop.types.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/types/webview-prop.types.ts @@ -30,6 +30,7 @@ export interface NewComponentWebviewProps { existingComponents: ComponentKind[]; initialValues?: { type?: string; subType?: string; buildPackLang?: string; name?: string }; extensionName?: string; + isNewCodeServerComp?: boolean; } export interface ComponentsDetailsWebviewProps { @@ -39,6 +40,7 @@ export interface ComponentsDetailsWebviewProps { component: ComponentKind; directoryFsPath?: string; initialEnvs: Environment[]; + isNewComponent?: boolean; } export interface ComponentsListActivityViewProps { diff --git a/workspaces/wso2-platform/wso2-platform-core/src/utils.ts b/workspaces/wso2-platform/wso2-platform-core/src/utils.ts index 9f8d10fa74f..4ac9bcfa06f 100644 --- a/workspaces/wso2-platform/wso2-platform-core/src/utils.ts +++ b/workspaces/wso2-platform/wso2-platform-core/src/utils.ts @@ -285,6 +285,23 @@ export const parseGitURL = (url?: string): null | [string, string, string] => { return [org, repoName, provider]; }; +export const buildGitURL = (org: string, repoName: string, provider: string, withDotGitSuffix?: boolean, serverUrl?: string): string | null => { + switch (provider) { + case GitProvider.GITHUB: + return `https://github.com/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}`; + case GitProvider.BITBUCKET: + return serverUrl + ? `${serverUrl}/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}` + : `https://bitbucket.org/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}`; + case GitProvider.GITLAB_SERVER: + return serverUrl + ? `${serverUrl}/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}` + : `https://gitlab.com/${org}/${repoName}${withDotGitSuffix ? ".git" : ""}`; + default: + return null; + } +}; + export const getComponentKindRepoSource = (source: ComponentKindSource) => { return { repo: source?.github?.repository || source?.bitbucket?.repository || source?.gitlab?.repository || "", diff --git a/workspaces/wso2-platform/wso2-platform-extension/.env.example b/workspaces/wso2-platform/wso2-platform-extension/.env.example index 89dd6a06942..eff573cc0f2 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/.env.example +++ b/workspaces/wso2-platform/wso2-platform-extension/.env.example @@ -1,35 +1,2 @@ -#DEFAULT -PLATFORM_DEFAULT_GHAPP_INSTALL_URL=https://github.com/apps/wso2-cloud-app/installations/new -PLATFORM_DEFAULT_GHAPP_AUTH_URL=https://github.com/login/oauth/authorize -PLATFORM_DEFAULT_GHAPP_CLIENT_ID= -PLATFORM_DEFAULT_GHAPP_REDIRECT_URL=https://console.choreowww.dev/ghapp -PLATFORM_DEFAULT_GHAPP_DEVANT_REDIRECT_URL=https://console.devant.dev/ghapp -PLATFORM_DEFAULT_CHOREO_CONSOLE_BASE_URL=https://console.choreo.dev -PLATFORM_DEFAULT_BILLING_CONSOLE_BASE_URL=https://subscriptions.wso2.com -PLATFORM_DEFAULT_DEVANT_CONSOLE_BASE_URL=https://console.devant.dev -PLATFORM_DEFAULT_DEVANT_ASGARDEO_CLIENT_ID= - -# STAGE -PLATFORM_STAGE_GHAPP_INSTALL_URL=https://github.com/apps/wso2-cloud-app-stage/installations/new -PLATFORM_STAGE_GHAPP_AUTH_URL=https://github.com/login/oauth/authorize -PLATFORM_STAGE_GHAPP_CLIENT_ID= -PLATFORM_STAGE_GHAPP_REDIRECT_URL=https://console.st.choreo.dev/ghapp -PLATFORM_STAGE_GHAPP_DEVANT_REDIRECT_URL=https://preview-st.devant.dev/ghapp -PLATFORM_STAGE_CHOREO_CONSOLE_BASE_URL=https://console.st.choreo.dev -PLATFORM_STAGE_BILLING_CONSOLE_BASE_URL=https://subscriptions.st.wso2.com -PLATFORM_STAGE_DEVANT_CONSOLE_BASE_URL=https://preview-st.devant.dev -PLATFORM_STAGE_DEVANT_ASGARDEO_CLIENT_ID= - -# DEV -PLATFORM_DEV_GHAPP_INSTALL_URL=https://github.com/apps/wso2-cloud-app-dev/installations/new -PLATFORM_DEV_GHAPP_AUTH_URL=https://github.com/login/oauth/authorize -PLATFORM_DEV_GHAPP_CLIENT_ID= -PLATFORM_DEV_GHAPP_REDIRECT_URL=https://consolev2.preview-dv.choreo.dev/ghapp -PLATFORM_DEV_GHAPP_DEVANT_REDIRECT_URL=https://preview-dv.devant.dev/ghapp -PLATFORM_DEV_CHOREO_CONSOLE_BASE_URL=https://consolev2.preview-dv.choreo.dev -PLATFORM_DEV_BILLING_CONSOLE_BASE_URL=https://subscriptions.dv.wso2.com -PLATFORM_DEV_DEVANT_CONSOLE_BASE_URL=https://preview-dv.devant.dev -PLATFORM_DEV_DEVANT_ASGARDEO_CLIENT_ID= - # Common PLATFORM_CHOREO_CLI_RELEASES_BASE_URL=https://github.com/wso2/choreo-cli/releases/download/ \ No newline at end of file diff --git a/workspaces/wso2-platform/wso2-platform-extension/package.json b/workspaces/wso2-platform/wso2-platform-extension/package.json index 3a479682516..74e916ed11c 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/package.json +++ b/workspaces/wso2-platform/wso2-platform-extension/package.json @@ -3,8 +3,8 @@ "displayName": "WSO2 Platform", "description": "Manage WSO2 Choreo and Devant projects in VS Code.", "license": "Apache-2.0", - "version": "1.0.13-sts-12", - "cliVersion": "v1.2.182507031200", + "version": "1.0.14-code-server-16", + "cliVersion": "v1.2.212509091800", "publisher": "wso2", "bugs": { "url": "https://github.com/wso2/choreo-vscode/issues" @@ -18,7 +18,8 @@ "Other" ], "activationEvents": [ - "onStartupFinished" + "onStartupFinished", + "onLanguageModel:agent" ], "extensionDependencies": [ "redhat.vscode-yaml" @@ -123,7 +124,14 @@ "shortTitle": "Open component source", "category": "WSO2", "icon": "$(repo-clone)" + }, + { + "command": "wso2.wso2-platform.push-to-git", + "title": "Commit & push component to remote repo", + "category": "WSO2", + "icon": "$(repo-push)" } + ], "configuration": { "type": "object", @@ -160,20 +168,14 @@ "dev" ], "default": "prod", - "description": "The WSO2 Platform Enviornment to use", - "scope": "window" + "description": "The WSO2 Platform Environment to use", + "scope": "machine" }, "WSO2.WSO2-Platform.Advanced.RpcPath": { "type": "string", "default": "", "description": "The path to Choreo RPC server", "scope": "window" - }, - "WSO2.WSO2-Platform.Advanced.StsToken": { - "type": "string", - "default": "", - "description": "User STS token", - "scope": "window" } } }, @@ -185,7 +187,13 @@ "fontCharacter": "\\f147" } } - } + }, + "mcpServerDefinitionProviders": [ + { + "id": "choreo", + "label": "Choreo MCP Server" + } + ] }, "scripts": { "clean": "del-cli ./dist ./out ./resources/jslibs ./platform-*.vsix ./coverage ./.nyc_output", @@ -249,6 +257,7 @@ "file-type": "^18.2.1", "js-yaml": "^4.1.0", "yaml": "^2.8.0", + "@iarna/toml": "^2.2.5", "jschardet": "^3.1.4", "vscode-messenger": "^0.5.1", "vscode-messenger-common": "^0.5.1", diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts b/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts index 82ecd87c2a7..6e1946724b8 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/PlatformExtensionApi.ts @@ -36,4 +36,5 @@ export class PlatformExtensionApi implements IWso2PlatformExtensionAPI { public getWebviewStateStore = () => webviewStateStore.getState().state; public getContextStateStore = () => contextStore.getState().state; public openClonedDir = (params: openClonedDirReq) => openClonedDir(params); + public getStsToken = () => ext.clients.rpcClient.getStsToken(); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts index aaa642d32b1..2391ba75ab2 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/client.ts @@ -41,12 +41,14 @@ import type { DeploymentLogsData, DeploymentTrack, Environment, + GetAuthorizedGitOrgsReq, GetAutoBuildStatusReq, GetAutoBuildStatusResp, GetBranchesReq, GetBuildLogsForTypeReq, GetBuildLogsReq, GetBuildsReq, + GetCliRpcResp, GetCommitsReq, GetComponentEndpointsReq, GetComponentItemReq, @@ -55,16 +57,23 @@ import type { GetConnectionGuideResp, GetConnectionItemReq, GetConnectionsReq, + GetCredentialDetailsReq, GetCredentialsReq, GetDeploymentStatusReq, GetDeploymentTracksReq, + GetGitMetadataReq, + GetGitMetadataResp, + GetGitTokenForRepositoryReq, + GetGitTokenForRepositoryResp, GetMarketplaceIdlReq, GetMarketplaceListReq, GetProjectEnvsReq, GetProxyDeploymentInfoReq, + GetSubscriptionsReq, GetSwaggerSpecReq, GetTestKeyReq, GetTestKeyResp, + GithubOrganization, IChoreoRPCClient, IsRepoAuthorizedReq, IsRepoAuthorizedResp, @@ -75,8 +84,10 @@ import type { PromoteProxyDeploymentReq, ProxyDeploymentInfo, RequestPromoteApprovalReq, + SubscriptionsResp, ToggleAutoBuildReq, ToggleAutoBuildResp, + UpdateCodeServerReq, UserInfo, } from "@wso2/wso2-platform-core"; import { workspace } from "vscode"; @@ -107,7 +118,7 @@ export class RPCClient { const resp = await this._conn.sendRequest<{}>("initialize", { clientName: "vscode", clientVersion: "1.0.0", - cloudStsToken: workspace.getConfiguration().get("WSO2.WSO2-Platform.Advanced.StsToken") || process.env.CLOUD_STS_TOKEN || "", + cloudStsToken: process.env.CLOUD_STS_TOKEN || "", }); console.log("Initialized RPC server", resp); } catch (e) { @@ -137,7 +148,6 @@ export class RPCClient { await this.init(); return this.sendRequest(method, params, timeout, true); } - getLogger().error("Error sending request", e); handlerError(e); throw e; } @@ -246,6 +256,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response; } + async getAuthorizedGitOrgs(params: GetAuthorizedGitOrgsReq) { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response = await this.client.sendRequest<{ gitOrgs: GithubOrganization[] }>("repo/getAuthorizedGitOrgs", params); + return { gitOrgs: response.gitOrgs }; + } + async getCredentials(params: GetCredentialsReq) { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -254,6 +272,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response?.credentials; } + async getCredentialDetails(params: GetCredentialDetailsReq) { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: CredentialItem = await this.client.sendRequest("repo/getCredentialDetails", params); + return response; + } + async getUserInfo(): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -262,26 +288,23 @@ export class ChoreoRPCClient implements IChoreoRPCClient { return response.userInfo; } - async getSignInUrl({ - baseUrl, - callbackUrl, - clientId, - isSignUp, - }: { callbackUrl: string; baseUrl?: string; clientId?: string; isSignUp?: boolean }): Promise { + async getSignInUrl({ callbackUrl }: { callbackUrl: string }): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response = await this.client.sendRequest<{ loginUrl: string }>("auth/getSignInUrl", { callbackUrl }, 2000); + return response.loginUrl; + } + + async getDevantSignInUrl({ callbackUrl }: { callbackUrl: string }): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); } - const response = await this.client.sendRequest<{ loginUrl: string }>("auth/getSignInUrl", { callbackUrl, baseUrl, clientId, isSignUp }, 2000); + const response = await this.client.sendRequest<{ loginUrl: string }>("auth/getDevantSignInUrl", { callbackUrl }, 2000); return response.loginUrl; } - async signInWithAuthCode( - authCode: string, - region?: string, - orgId?: string, - redirectUrl?: string, - clientId?: string, - ): Promise { + async signInWithAuthCode(authCode: string, region?: string, orgId?: string): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); } @@ -289,8 +312,18 @@ export class ChoreoRPCClient implements IChoreoRPCClient { authCode, region, orgId, - redirectUrl, - clientId, + }); + return response.userInfo; + } + + async signInDevantWithAuthCode(authCode: string, region?: string, orgId?: string): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response = await this.client.sendRequest<{ userInfo: UserInfo }>("auth/signInDevantWithAuthCode", { + authCode, + region, + orgId, }); return response.userInfo; } @@ -302,6 +335,14 @@ export class ChoreoRPCClient implements IChoreoRPCClient { await this.client.sendRequest("auth/signOut", undefined, 2000); } + async getCurrentRegion(): Promise<"US" | "EU"> { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const resp: { region: "US" | "EU" } = await this.client.sendRequest("auth/getCurrentRegion"); + return resp.region; + } + async changeOrgContext(orgId: string): Promise { if (!this.client) { throw new Error("RPC client is not initialized"); @@ -532,6 +573,53 @@ export class ChoreoRPCClient implements IChoreoRPCClient { } await this.client.sendRequest("deployment/promoteProxy", params); } + + async getSubscriptions(params: GetSubscriptionsReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: SubscriptionsResp = await this.client.sendRequest("auth/getSubscriptions", params); + return response; + } + + async getStsToken(): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: { token: string } = await this.client.sendRequest("auth/getStsToken", {}); + return response?.token; + } + + async getGitTokenForRepository(params: GetGitTokenForRepositoryReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: GetGitTokenForRepositoryResp = await this.client.sendRequest("repo/gitTokenForRepository", params); + return response; + } + + async getGitRepoMetadata(params: GetGitMetadataReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: GetGitMetadataResp = await this.client.sendRequest("repo/getRepoMetadata", params); + return response; + } + + async updateCodeServer(params: UpdateCodeServerReq): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + await this.client.sendRequest("component/updateCodeServer", params); + } + + async getConfigFromCli(): Promise { + if (!this.client) { + throw new Error("RPC client is not initialized"); + } + const response: GetCliRpcResp = await this.client.sendRequest("auth/getConfigs", {}); + return response; + } } export class ChoreoTracer implements Tracer { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/connection.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/connection.ts index bde9ee67ae0..02d6ba9e93e 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/connection.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/connection.ts @@ -17,10 +17,11 @@ */ import { type ChildProcessWithoutNullStreams, spawn } from "child_process"; -import { workspace } from "vscode"; import { type MessageConnection, StreamMessageReader, StreamMessageWriter, createMessageConnection } from "vscode-jsonrpc/node"; +import { ext } from "../extensionVariables"; import { getLogger } from "../logger/logger"; -import { getChoreoEnv, getChoreoExecPath } from "./cli-install"; +import { parseJwt } from "../utils"; +import { getChoreoExecPath } from "./cli-install"; export class StdioConnection { private _connection: MessageConnection; @@ -29,11 +30,16 @@ export class StdioConnection { const executablePath = getChoreoExecPath(); console.log("Starting RPC server, path:", executablePath); getLogger().debug(`Starting RPC server${executablePath}`); + let region = process.env.CLOUD_REGION; + if (!region && process.env.CLOUD_STS_TOKEN && parseJwt(process.env.CLOUD_STS_TOKEN)?.iss?.includes(".eu.")) { + region = "EU"; + } this._serverProcess = spawn(executablePath, ["start-rpc-server"], { env: { ...process.env, - SKIP_KEYRING: workspace.getConfiguration().get("WSO2.WSO2-Platform.Advanced.StsToken") || process.env.CLOUD_STS_TOKEN ? "true" : "", - CHOREO_ENV: getChoreoEnv(), + SKIP_KEYRING: process.env.CLOUD_STS_TOKEN ? "true" : "", + CHOREO_ENV: ext.choreoEnv, + CHOREO_REGION: region, }, }); this._connection = createMessageConnection( diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/rpc-resolver.ts b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/rpc-resolver.ts index f44e7424f6b..6577050bdcb 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/rpc-resolver.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/choreo-rpc/rpc-resolver.ts @@ -31,6 +31,7 @@ import { ChoreoRpcDeleteConnection, ChoreoRpcDisableAutoBuild, ChoreoRpcEnableAutoBuild, + ChoreoRpcGetAuthorizedGitOrgsRequest, ChoreoRpcGetAutoBuildStatus, ChoreoRpcGetBranchesRequest, ChoreoRpcGetBuildLogs, @@ -43,20 +44,25 @@ import { ChoreoRpcGetConnectionGuide, ChoreoRpcGetConnectionItem, ChoreoRpcGetConnections, + ChoreoRpcGetCredentialDetailsRequest, ChoreoRpcGetCredentialsRequest, ChoreoRpcGetDeploymentStatusRequest, ChoreoRpcGetDeploymentTracksRequest, ChoreoRpcGetEndpointsRequest, ChoreoRpcGetEnvsRequest, + ChoreoRpcGetGitRepoMetadata, + ChoreoRpcGetGitTokenForRepository, ChoreoRpcGetMarketplaceItemIdl, ChoreoRpcGetMarketplaceItems, ChoreoRpcGetProjectsRequest, ChoreoRpcGetProxyDeploymentInfo, + ChoreoRpcGetSubscriptions, ChoreoRpcGetSwaggerRequest, ChoreoRpcGetTestKeyRequest, ChoreoRpcIsRepoAuthorizedRequest, ChoreoRpcPromoteProxyDeployment, ChoreoRpcRequestPromoteApproval, + type GetAuthorizedGitOrgsReq, type GetAutoBuildStatusReq, type GetBranchesReq, type GetBuildLogsForTypeReq, @@ -69,13 +75,17 @@ import { type GetConnectionGuideReq, type GetConnectionItemReq, type GetConnectionsReq, + type GetCredentialDetailsReq, type GetCredentialsReq, type GetDeploymentStatusReq, type GetDeploymentTracksReq, + type GetGitMetadataReq, + type GetGitTokenForRepositoryReq, type GetMarketplaceIdlReq, type GetMarketplaceListReq, type GetProjectEnvsReq, type GetProxyDeploymentInfoReq, + type GetSubscriptionsReq, type GetSwaggerSpecReq, type GetTestKeyReq, type IChoreoRPCClient, @@ -105,7 +115,9 @@ export function registerChoreoRpcResolver(messenger: Messenger, rpcClient: IChor messenger.onRequest(ChoreoRpcGetBuildPacksRequest, (params: BuildPackReq) => rpcClient.getBuildPacks(params)); messenger.onRequest(ChoreoRpcGetBranchesRequest, (params: GetBranchesReq) => rpcClient.getRepoBranches(params)); messenger.onRequest(ChoreoRpcIsRepoAuthorizedRequest, (params: IsRepoAuthorizedReq) => rpcClient.isRepoAuthorized(params)); + messenger.onRequest(ChoreoRpcGetAuthorizedGitOrgsRequest, (params: GetAuthorizedGitOrgsReq) => rpcClient.getAuthorizedGitOrgs(params)); messenger.onRequest(ChoreoRpcGetCredentialsRequest, (params: GetCredentialsReq) => rpcClient.getCredentials(params)); + messenger.onRequest(ChoreoRpcGetCredentialDetailsRequest, (params: GetCredentialDetailsReq) => rpcClient.getCredentialDetails(params)); messenger.onRequest(ChoreoRpcDeleteComponentRequest, async (params: Parameters[0]) => { const extName = webviewStateStore.getState().state.extensionName; return window.withProgress( @@ -174,4 +186,11 @@ export function registerChoreoRpcResolver(messenger: Messenger, rpcClient: IChor rpcClient.promoteProxyDeployment(params), ); }); + messenger.onRequest(ChoreoRpcGetSubscriptions, (params: GetSubscriptionsReq) => rpcClient.getSubscriptions(params)); + messenger.onRequest(ChoreoRpcGetGitTokenForRepository, (params: GetGitTokenForRepositoryReq) => rpcClient.getGitTokenForRepository(params)); + messenger.onRequest(ChoreoRpcGetGitRepoMetadata, async (params: GetGitMetadataReq) => { + return window.withProgress({ title: "Fetching repo metadata...", location: ProgressLocation.Notification }, () => + rpcClient.getGitRepoMetadata(params), + ); + }); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts new file mode 100644 index 00000000000..be2f87da3eb --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/commit-and-push-to-git-cmd.ts @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CommandIds, ComponentKind, type ContextStoreComponentState, GitProvider, type ICommitAndPuhCmdParams, parseGitURL } from "@wso2/wso2-platform-core"; +import { type ExtensionContext, ProgressLocation, type QuickPickItem, Uri, commands, env, window, workspace } from "vscode"; +import { ext } from "../extensionVariables"; +import { initGit } from "../git/main"; +import { hasDirtyRepo } from "../git/util"; +import { getLogger } from "../logger/logger"; +import { contextStore } from "../stores/context-store"; +import { webviewStateStore } from "../stores/webview-state-store"; +import { delay, isSamePath } from "../utils"; +import { getUserInfoForCmd, isRpcActive, setExtensionName } from "./cmd-utils"; + +export function commitAndPushToGitCommand(context: ExtensionContext) { + context.subscriptions.push( + commands.registerCommand(CommandIds.CommitAndPushToGit, async (params: ICommitAndPuhCmdParams) => { + setExtensionName(params?.extName); + const extensionName = webviewStateStore.getState().state.extensionName; + try { + isRpcActive(ext); + const userInfo = await getUserInfoForCmd("commit and push changes to Git"); + if (userInfo) { + const selected = contextStore.getState().state.selected; + if (!selected) { + throw new Error("project is not associated with a component directory"); + } + + let selectedComp: ContextStoreComponentState | undefined; + const getSelectedComponent = async (items: ContextStoreComponentState[]) => { + const componentItems: (QuickPickItem & { item?: ContextStoreComponentState })[] = items.map((item) => ({ + label: item?.component?.metadata?.displayName!, + item: item, + })); + const selectedComp = await window.showQuickPick(componentItems, { + title: `Multiple ${extensionName === "Devant" ? "integrations" : "components"} detected. Please select ${extensionName === "Devant" ? "an integration" : "a component"} to push`, + }); + return selectedComp?.item; + }; + + if (contextStore.getState().state?.components?.length === 0) { + throw new Error("No components in this workspace"); + } + + if (params?.componentPath) { + const matchingComponent = contextStore + .getState() + .state?.components?.filter((item) => isSamePath(item.componentFsPath, params?.componentPath)); + if (matchingComponent?.length === 0) { + selectedComp = await getSelectedComponent(contextStore.getState().state?.components!); + } else if (matchingComponent?.length === 1) { + selectedComp = matchingComponent[0]; + } else if (matchingComponent && matchingComponent?.length > 1) { + selectedComp = await getSelectedComponent(matchingComponent); + } + } else { + selectedComp = await getSelectedComponent(contextStore.getState().state?.components!); + } + + if (!selectedComp) { + throw new Error("Failed to select component fo be pushed to remote"); + } + + const haveChanges = await hasDirtyRepo(selectedComp.componentFsPath, ext.context, ["context.yaml"]); + if (!haveChanges) { + window.showErrorMessage("There are no new changes to push to cloud"); + return; + } + + const newGit = await initGit(ext.context); + if (!newGit) { + throw new Error("failed to initGit"); + } + const dotGit = await newGit?.getRepositoryDotGit(selectedComp.componentFsPath); + const repoRoot = await newGit?.getRepositoryRoot(selectedComp.componentFsPath); + const repo = newGit.open(repoRoot, dotGit); + + const remotes = await window.withProgress({ title: "Fetching remotes of the repo...", location: ProgressLocation.Notification }, () => + repo.getRemotes(), + ); + + if (remotes.length === 0) { + window.showErrorMessage("No remotes found within the directory"); + return; + } + + let matchingRemote = remotes.find((item) => { + if (item.pushUrl) { + const urlObj = new URL(item.pushUrl); + if (urlObj.password) { + return true; + } + } + }); + + if (!matchingRemote && process.env.CLOUD_STS_TOKEN && remotes[0].fetchUrl) { + try { + const repoUrl = remotes[0].fetchUrl; + const parsed = parseGitURL(repoUrl); + if (parsed && parsed[2] === GitProvider.GITHUB) { + const [repoOrg, repoName] = parsed; + const urlObj = new URL(repoUrl); + getLogger().debug(`Fetching PAT for org ${repoOrg} and repo ${repoName}`); + const gitPat = await window.withProgress( + { title: `Accessing the repository ${repoUrl}...`, location: ProgressLocation.Notification }, + () => + ext.clients.rpcClient.getGitTokenForRepository({ + orgId: selected.org?.id?.toString()!, + gitOrg: repoOrg, + gitRepo: repoName, + secretRef: selectedComp.component?.spec?.source?.secretRef || "", + }), + ); + urlObj.username = gitPat.username || "x-access-token"; + urlObj.password = gitPat.token; + await window.withProgress({ title: "Setting new remote...", location: ProgressLocation.Notification }, async () => { + await repo.addRemote("cloud-editor-remote", urlObj.href); + const remotes = await repo.getRemotes(); + matchingRemote = remotes.find((item) => item.name === "cloud-editor-remote"); + }); + } + } catch { + getLogger().debug(`Failed to get token for ${remotes[0].fetchUrl}`); + } + } + + await window.withProgress({ title: "Adding changes to be committed...", location: ProgressLocation.Notification }, async () => { + await repo.add(["."]); + }); + + const commitMessage = await window.showInputBox({ + placeHolder: "Message to describe the changes done to your integration", + title: "Enter commit message", + validateInput: (val) => { + if (!val) { + return "Commit message is required"; + } + return null; + }, + }); + + if (!commitMessage) { + window.showErrorMessage("Commit message is required in order to proceed"); + return; + } + + const headRef = await window.withProgress( + { title: "Fetching remote repo metadata...", location: ProgressLocation.Notification }, + async () => { + await repo.fetch({ silent: true, remote: matchingRemote?.name }); + await repo.commit(commitMessage); + await delay(500); + return repo.getHEADRef(); + }, + ); + + if (headRef?.ahead && (headRef?.behind === 0 || headRef?.behind === undefined)) { + await window.withProgress({ title: "Pushing changes to remote repository...", location: ProgressLocation.Notification }, () => + repo.push(matchingRemote?.name), + ); + window.showInformationMessage("Your changes have been successfully pushed to cloud"); + } else { + await commands.executeCommand("git.sync"); + } + } + } catch (err: any) { + console.error("Failed to push to remote", err); + window.showErrorMessage(err?.message || "Failed to push to remote"); + } + }), + ); +} diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/create-component-cmd.ts index 698eafe0c37..b83f59dcefe 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 @@ -23,9 +23,12 @@ import { ChoreoBuildPackNames, ChoreoComponentType, CommandIds, + type ComponentKind, DevantScopes, type ExtensionName, type ICreateComponentCmdParams, + type Organization, + type Project, type SubmitComponentCreateReq, type WorkspaceConfig, getComponentKindRepoSource, @@ -34,15 +37,16 @@ import { getTypeOfIntegrationType, parseGitURL, } from "@wso2/wso2-platform-core"; -import { type ExtensionContext, ProgressLocation, type QuickPickItem, Uri, commands, window, workspace } from "vscode"; -import { choreoEnvConfig } from "../config"; +import { type ExtensionContext, ProgressLocation, type QuickPickItem, Uri, commands, env, window, workspace } from "vscode"; import { ext } from "../extensionVariables"; +import { initGit } from "../git/main"; import { getGitRemotes, getGitRoot } from "../git/util"; +import { getLogger } from "../logger/logger"; import { authStore } from "../stores/auth-store"; -import { contextStore } from "../stores/context-store"; +import { contextStore, waitForContextStoreToLoad } from "../stores/context-store"; import { dataCacheStore } from "../stores/data-cache-store"; import { webviewStateStore } from "../stores/webview-state-store"; -import { convertFsPathToUriPath, isSamePath, isSubpath, openDirectory } from "../utils"; +import { convertFsPathToUriPath, delay, isSamePath, isSubpath, openDirectory } from "../utils"; import { showComponentDetailsView } from "../webviews/ComponentDetailsView"; import { ComponentFormView, type IComponentCreateFormParams } from "../webviews/ComponentFormView"; import { getUserInfoForCmd, isRpcActive, selectOrg, selectProjectWithCreateNew, setExtensionName } from "./cmd-utils"; @@ -59,6 +63,7 @@ export function createNewComponentCommand(context: ExtensionContext) { isRpcActive(ext); const userInfo = await getUserInfoForCmd(`create ${extName === "Devant" ? "an integration" : "a component"}`); if (userInfo) { + await waitForContextStoreToLoad(); const selected = contextStore.getState().state.selected; let selectedProject = selected?.project; let selectedOrg = selected?.org; @@ -172,8 +177,10 @@ export function createNewComponentCommand(context: ExtensionContext) { dataCacheStore.getState().setComponents(selectedOrg.handle, selectedProject.handler, components); let gitRoot: string | undefined; + let isGitInitialized = false; try { gitRoot = await getGitRoot(context, selectedUri.fsPath); + isGitInitialized = true; } catch (err) { // ignore error } @@ -222,6 +229,15 @@ export function createNewComponentCommand(context: ExtensionContext) { const isWithinWorkspace = workspace.workspaceFolders?.some((item) => isSubpath(item.uri?.fsPath, selectedUri?.fsPath)); + let compInitialName = params?.name || dirName || selectedType; + const existingNames = components.map((c) => c.metadata?.name?.toLowerCase?.()); + const baseName = compInitialName; + let counter = 1; + while (existingNames.includes(compInitialName.toLowerCase())) { + compInitialName = `${baseName}-${counter}`; + counter++; + } + const createCompParams: IComponentCreateFormParams = { directoryUriPath: selectedUri.path, directoryFsPath: selectedUri.fsPath, @@ -229,11 +245,12 @@ export function createNewComponentCommand(context: ExtensionContext) { organization: selectedOrg!, project: selectedProject!, extensionName: webviewStateStore.getState().state.extensionName, + isNewCodeServerComp: isGitInitialized === false && !!process.env.CLOUD_STS_TOKEN, initialValues: { type: selectedType, subType: selectedSubType, buildPackLang: params?.buildPackLang, - name: params?.name || dirName || "", + name: compInitialName, }, }; @@ -304,39 +321,67 @@ export const submitCreateComponentHandler = async ({ createParams, org, project } */ - if (extensionName !== "Devant") { - showComponentDetailsView(org, project, createdComponent, createParams?.componentDir); - } - - window - .showInformationMessage( - `${extensionName === "Devant" ? "Integration" : "Component"} '${createdComponent.metadata.name}' was successfully created`, - `Open in ${extensionName}`, - ) - .then(async (resp) => { - if (resp === `Open in ${extensionName}`) { - commands.executeCommand( - "vscode.open", - `${extensionName === "Devant" ? choreoEnvConfig.getDevantUrl() : choreoEnvConfig.getConsoleUrl()}/organizations/${org.handle}/projects/${project.id}/components/${createdComponent.metadata.handler}/overview`, - ); - } - }); - const compCache = dataCacheStore.getState().getComponents(org.handle, project.handler); dataCacheStore.getState().setComponents(org.handle, project.handler, [createdComponent, ...compCache]); // update the context file if needed try { - const gitRoot = await getGitRoot(ext.context, createParams.componentDir); + const newGit = await initGit(ext.context); + const gitRoot = await newGit?.getRepositoryRoot(createParams.componentDir); + const dotGit = await newGit?.getRepositoryDotGit(createParams.componentDir); const projectCache = dataCacheStore.getState().getProjects(org.handle); - if (gitRoot) { - updateContextFile(gitRoot, authStore.getState().state.userInfo!, project, org, projectCache); - contextStore.getState().refreshState(); + if (newGit && gitRoot && dotGit) { + if (process.env.CLOUD_STS_TOKEN) { + // update the code server, to attach itself to the created component + const repo = newGit.open(gitRoot, dotGit); + const head = await repo.getHEAD(); + if (head.name) { + const commit = await repo.getCommit(head.name); + try { + await window.withProgress( + { title: "Updating cloud editor with newly created component...", location: ProgressLocation.Notification }, + () => + ext.clients.rpcClient.updateCodeServer({ + componentId: createdComponent.metadata.id, + orgHandle: org.handle, + orgId: org.id.toString(), + orgUuid: org.uuid, + projectId: project.id, + sourceCommitHash: commit.hash, + }), + ); + } catch (err) { + getLogger().error("Failed to updated code server after creating the component", err); + } + + // Clear code server local storage data data + try { + await commands.executeCommand("devantEditor.clearLocalStorage"); + } catch (err) { + getLogger().error(`Failed to execute devantEditor.clearLocalStorage command: ${err}`); + } + } + } else { + updateContextFile(gitRoot, authStore.getState().state.userInfo!, project, org, projectCache); + contextStore.getState().refreshState(); + } } } catch (err) { console.error("Failed to get git details of ", createParams.componentDir); } + if (extensionName !== "Devant") { + showComponentDetailsView(org, project, createdComponent, createParams?.componentDir, undefined, true); + } + + const successMessage = `${extensionName === "Devant" ? "Integration" : "Component"} '${createdComponent.metadata.name}' was successfully created.`; + + const isWithinWorkspace = workspace.workspaceFolders?.some((item) => isSubpath(item.uri?.fsPath, createParams.componentDir)); + + if (process.env.CLOUD_STS_TOKEN) { + await ext.context.globalState.update("code-server-component-id", createdComponent.metadata?.id); + } + if (workspace.workspaceFile) { const workspaceContent: WorkspaceConfig = JSON.parse(readFileSync(workspace.workspaceFile.fsPath, "utf8")); workspaceContent.folders = [ @@ -346,9 +391,24 @@ export const submitCreateComponentHandler = async ({ createParams, org, project path: path.normalize(path.relative(path.dirname(workspace.workspaceFile.fsPath), createParams.componentDir)), }, ]; + } else if (isWithinWorkspace) { + window.showInformationMessage(successMessage, `Open in ${extensionName}`).then(async (resp) => { + if (resp === `Open in ${extensionName}`) { + commands.executeCommand( + "vscode.open", + `${extensionName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl}/organizations/${org.handle}/projects/${extensionName === "Devant" ? project.id : project.handler}/components/${createdComponent.metadata.handler}/overview`, + ); + } + }); } else { - contextStore.getState().refreshState(); + window.showInformationMessage(`${successMessage} Reload workspace to continue`, { modal: true }, "Continue").then(async (resp) => { + if (resp === "Continue") { + commands.executeCommand("vscode.openFolder", Uri.file(createParams.componentDir), { forceNewWindow: false }); + } + }); } + + contextStore.getState().refreshState(); } return createdComponent; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/index.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/index.ts index c07d8d22ee9..d38ba829a65 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/index.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/index.ts @@ -18,6 +18,7 @@ import type { ExtensionContext } from "vscode"; import { cloneRepoCommand } from "./clone-project-cmd"; +import { commitAndPushToGitCommand } from "./commit-and-push-to-git-cmd"; import { createComponentDependencyCommand } from "./create-comp-dependency-cmd"; import { createNewComponentCommand } from "./create-component-cmd"; import { createDirectoryContextCommand } from "./create-directory-context-cmd"; @@ -49,4 +50,5 @@ export function activateCmds(context: ExtensionContext) { createComponentDependencyCommand(context); viewComponentDependencyCommand(context); openCompSrcCommand(context); + commitAndPushToGitCommand(context); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/open-in-console-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/open-in-console-cmd.ts index 71dea4d7872..7161b376e19 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/open-in-console-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/open-in-console-cmd.ts @@ -16,16 +16,8 @@ * under the License. */ -import { - CommandIds, - type ComponentKind, - type ICreateComponentCmdParams, - type IOpenInConsoleCmdParams, - type Organization, - type Project, -} from "@wso2/wso2-platform-core"; +import { CommandIds, type ComponentKind, type ICreateComponentCmdParams, type IOpenInConsoleCmdParams } from "@wso2/wso2-platform-core"; import { type ExtensionContext, ProgressLocation, type QuickPickItem, QuickPickItemKind, Uri, commands, env, window } from "vscode"; -import { choreoEnvConfig } from "../config"; import { ext } from "../extensionVariables"; import { contextStore } from "../stores/context-store"; import { dataCacheStore } from "../stores/data-cache-store"; @@ -66,10 +58,7 @@ export function openInConsoleCommand(context: ExtensionContext) { } } - let projectBaseUrl = `${choreoEnvConfig.getConsoleUrl()}/organizations/${selectedOrg?.handle}/projects/${selectedProject.handler}`; - if (extensionName === "Devant") { - projectBaseUrl = `${choreoEnvConfig.getDevantUrl()}/organizations/${selectedOrg?.handle}/projects/${selectedProject.id}`; - } + let projectBaseUrl = `${ext.config?.choreoConsoleUrl}/organizations/${selectedOrg?.handle}/projects/${selectedProject.handler}`; if (params?.component) { env.openExternal(Uri.parse(`${projectBaseUrl}/components/${params?.component.metadata.handler}/overview`)); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-cmd.ts index 74953c79337..589900bb5b1 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-cmd.ts @@ -19,7 +19,6 @@ import { CommandIds, type ICmdParamsBase } from "@wso2/wso2-platform-core"; import { type ExtensionContext, ProgressLocation, commands, window } from "vscode"; import * as vscode from "vscode"; -import { choreoEnvConfig } from "../config"; import { ext } from "../extensionVariables"; import { getLogger } from "../logger/logger"; import { webviewStateStore } from "../stores/webview-state-store"; @@ -34,17 +33,12 @@ export function signInCommand(context: ExtensionContext) { getLogger().debug("Signing in to WSO2 Platform"); const callbackUrl = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://wso2.wso2-platform/signin`)); - let baseUrl: string | undefined; - if (webviewStateStore.getState().state?.extensionName === "Devant") { - baseUrl = `${choreoEnvConfig.getDevantUrl()}/login`; - } - let clientId: string | undefined; - if (webviewStateStore.getState().state?.extensionName === "Devant") { - clientId = choreoEnvConfig.getDevantAsgardeoClientId(); - } console.log("Generating WSO2 Platform login URL for ", callbackUrl.toString()); const loginUrl = await window.withProgress({ title: "Generating Login URL...", location: ProgressLocation.Notification }, async () => { - return ext.clients.rpcClient.getSignInUrl({ callbackUrl: callbackUrl.toString(), baseUrl, clientId }); + if (webviewStateStore.getState().state?.extensionName === "Devant") { + return ext.clients.rpcClient.getDevantSignInUrl({ callbackUrl: callbackUrl.toString() }); + } + return ext.clients.rpcClient.getSignInUrl({ callbackUrl: callbackUrl.toString() }); }); if (loginUrl) { @@ -54,7 +48,7 @@ export function signInCommand(context: ExtensionContext) { window.showErrorMessage("Unable to open external link for authentication."); } } catch (error: any) { - getLogger().error(`Error while signing in to WSO2 Platofmr. ${error?.message}${error?.cause ? `\nCause: ${error.cause.message}` : ""}`); + getLogger().error(`Error while signing in to WSO2 Platform. ${error?.message}${error?.cause ? `\nCause: ${error.cause.message}` : ""}`); if (error instanceof Error) { window.showErrorMessage(error.message); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-with-code-cmd.ts b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-with-code-cmd.ts index b59704676f9..1825dba26d6 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-with-code-cmd.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/cmds/sign-in-with-code-cmd.ts @@ -42,9 +42,10 @@ export function signInWithAuthCodeCommand(context: ExtensionContext) { }); if (authCode) { - ext.clients.rpcClient.signInWithAuthCode(authCode).then((userInfo) => { + ext.clients.rpcClient.signInWithAuthCode(authCode).then(async (userInfo) => { if (userInfo) { - authStore.getState().loginSuccess(userInfo); + const region = await ext.clients.rpcClient.getCurrentRegion(); + authStore.getState().loginSuccess(userInfo, region); } }); } else { diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/config.ts b/workspaces/wso2-platform/wso2-platform-extension/src/config.ts index f407dd1eca0..72c597d3e40 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/config.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/config.ts @@ -18,23 +18,9 @@ import { window } from "vscode"; import { z } from "zod"; -import { getChoreoEnv } from "./choreo-rpc/cli-install"; +import { ext } from "./extensionVariables"; -const ghAppSchema = z.object({ - installUrl: z.string().min(1), - authUrl: z.string().min(1), - clientId: z.string().min(1), - redirectUrl: z.string().min(1), - devantRedirectUrl: z.string().min(1), -}); - -const envSchemaItem = z.object({ - ghApp: ghAppSchema, - choreoConsoleBaseUrl: z.string().min(1), - billingConsoleBaseUrl: z.string().min(1), - devantConsoleBaseUrl: z.string().min(1), - devantAsgardeoClientId: z.string().min(1), -}); +const envSchemaItem = z.object({}); const envSchema = z.object({ CLI_RELEASES_BASE_URL: z.string().min(1), @@ -45,45 +31,9 @@ const envSchema = z.object({ const _env = envSchema.safeParse({ CLI_RELEASES_BASE_URL: process.env.PLATFORM_CHOREO_CLI_RELEASES_BASE_URL, - defaultEnvs: { - ghApp: { - installUrl: process.env.PLATFORM_DEFAULT_GHAPP_INSTALL_URL ?? "", - authUrl: process.env.PLATFORM_DEFAULT_GHAPP_AUTH_URL ?? "", - clientId: process.env.PLATFORM_DEFAULT_GHAPP_CLIENT_ID ?? "", - redirectUrl: process.env.PLATFORM_DEFAULT_GHAPP_REDIRECT_URL ?? "", - devantRedirectUrl: process.env.PLATFORM_DEFAULT_GHAPP_DEVANT_REDIRECT_URL ?? "", - }, - choreoConsoleBaseUrl: process.env.PLATFORM_DEFAULT_CHOREO_CONSOLE_BASE_URL ?? "", - billingConsoleBaseUrl: process.env.PLATFORM_DEFAULT_BILLING_CONSOLE_BASE_URL ?? "", - devantConsoleBaseUrl: process.env.PLATFORM_DEFAULT_DEVANT_CONSOLE_BASE_URL ?? "", - devantAsgardeoClientId: process.env.PLATFORM_DEFAULT_DEVANT_ASGARDEO_CLIENT_ID ?? "", - }, - stageEnvs: { - ghApp: { - installUrl: process.env.PLATFORM_STAGE_GHAPP_INSTALL_URL ?? "", - authUrl: process.env.PLATFORM_STAGE_GHAPP_AUTH_URL ?? "", - clientId: process.env.PLATFORM_STAGE_GHAPP_CLIENT_ID ?? "", - redirectUrl: process.env.PLATFORM_STAGE_GHAPP_REDIRECT_URL ?? "", - devantRedirectUrl: process.env.PLATFORM_STAGE_GHAPP_DEVANT_REDIRECT_URL ?? "", - }, - choreoConsoleBaseUrl: process.env.PLATFORM_STAGE_CHOREO_CONSOLE_BASE_URL ?? "", - billingConsoleBaseUrl: process.env.PLATFORM_STAGE_BILLING_CONSOLE_BASE_URL ?? "", - devantConsoleBaseUrl: process.env.PLATFORM_STAGE_DEVANT_CONSOLE_BASE_URL ?? "", - devantAsgardeoClientId: process.env.PLATFORM_STAGE_DEVANT_ASGARDEO_CLIENT_ID ?? "", - }, - devEnvs: { - ghApp: { - installUrl: process.env.PLATFORM_DEV_GHAPP_INSTALL_URL ?? "", - authUrl: process.env.PLATFORM_DEV_GHAPP_AUTH_URL ?? "", - clientId: process.env.PLATFORM_DEV_GHAPP_CLIENT_ID ?? "", - redirectUrl: process.env.PLATFORM_DEV_GHAPP_REDIRECT_URL ?? "", - devantRedirectUrl: process.env.PLATFORM_DEV_GHAPP_DEVANT_REDIRECT_URL ?? "", - }, - choreoConsoleBaseUrl: process.env.PLATFORM_DEV_CHOREO_CONSOLE_BASE_URL ?? "", - billingConsoleBaseUrl: process.env.PLATFORM_DEV_BILLING_CONSOLE_BASE_URL ?? "", - devantConsoleBaseUrl: process.env.PLATFORM_DEV_DEVANT_CONSOLE_BASE_URL ?? "", - devantAsgardeoClientId: process.env.PLATFORM_DEV_DEVANT_ASGARDEO_CLIENT_ID ?? "", - }, + defaultEnvs: {}, + stageEnvs: {}, + devEnvs: {}, } as z.infer); if (!_env.success) { @@ -97,33 +47,11 @@ class ChoreoEnvConfig { public getCliInstallUrl() { return _env.data?.CLI_RELEASES_BASE_URL; } - - public getGHAppConfig() { - return this._config.ghApp; - } - - public getConsoleUrl(): string { - return this._config.choreoConsoleBaseUrl; - } - - public getBillingUrl(): string { - return this._config.billingConsoleBaseUrl; - } - - public getDevantUrl(): string { - return this._config.devantConsoleBaseUrl; - } - - public getDevantAsgardeoClientId(): string { - return this._config.devantAsgardeoClientId; - } } -const choreoEnv = getChoreoEnv(); - let pickedEnvConfig: z.infer; -switch (choreoEnv) { +switch (ext.choreoEnv) { case "prod": pickedEnvConfig = _env.data!.defaultEnvs; break; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts new file mode 100644 index 00000000000..196c3677926 --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/src/devant-utils.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Uri, commands, window, workspace } from "vscode"; +import { ext } from "./extensionVariables"; +import { initGit } from "./git/main"; +import { getLogger } from "./logger/logger"; + +export const activateDevantFeatures = () => { + 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); + + ext.context.subscriptions.push({ + dispose: () => { + if (intervalId) { + clearTimeout(intervalId); + } + }, + }); +}; + +const showRepoSyncNotification = async () => { + if (workspace.workspaceFolders && workspace.workspaceFolders?.length > 0) { + try { + const componentPath = Uri.from(workspace.workspaceFolders[0].uri).fsPath; + const newGit = await initGit(ext.context); + if (!newGit) { + throw new Error("failed to initGit"); + } + const dotGit = await newGit?.getRepositoryDotGit(componentPath); + const repoRoot = await newGit?.getRepositoryRoot(componentPath); + const repo = newGit.open(repoRoot, dotGit); + await repo.fetch(); + const head = await repo.getHEADRef(); + if (head?.behind) { + window.showInformationMessage(`Your remote Git repository has ${head.behind} new changes`, "Sync Repository").then((res) => { + if (res === "Sync Repository") { + commands.executeCommand("git.sync"); + } + }); + } + } catch (err) { + getLogger().error(`Failed to check if the Git head is behind: ${(err as Error)?.message}`); + } + } +}; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/error-utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/error-utils.ts index cad5720e8ba..cde75f4fc3b 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/error-utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/error-utils.ts @@ -16,11 +16,10 @@ * under the License. */ -import { CommandIds } from "@wso2/wso2-platform-core"; import { commands, window as w } from "vscode"; import { ResponseError } from "vscode-jsonrpc"; import { ErrorCode } from "./choreo-rpc/constants"; -import { choreoEnvConfig } from "./config"; +import { ext } from "./extensionVariables"; import { getLogger } from "./logger/logger"; import { authStore } from "./stores/auth-store"; import { webviewStateStore } from "./stores/webview-state-store"; @@ -84,7 +83,10 @@ export function handlerError(err: any) { w.showErrorMessage("Failed to create project due to reaching maximum number of projects allowed within the free tier.", "Upgrade").then( (res) => { if (res === "Upgrade") { - commands.executeCommand("vscode.open", `${choreoEnvConfig.getBillingUrl()}/cloud/choreo/upgrade`); + commands.executeCommand( + "vscode.open", + `${ext.config?.billingConsoleUrl}/cloud/${extensionName === "Devant" ? "devant" : "choreo"}/upgrade`, + ); } }, ); @@ -95,7 +97,10 @@ export function handlerError(err: any) { "Upgrade", ).then((res) => { if (res === "Upgrade") { - commands.executeCommand("vscode.open", `${choreoEnvConfig.getBillingUrl()}/cloud/choreo/upgrade`); + commands.executeCommand( + "vscode.open", + `${ext.config?.billingConsoleUrl}/cloud/${extensionName === "Devant" ? "devant" : "choreo"}/upgrade`, + ); } }); break; @@ -117,11 +122,10 @@ export function handlerError(err: any) { case ErrorCode.NoAccountAvailable: w.showErrorMessage(`It looks like you don't have an account yet. Please sign up before logging in.`, "Sign Up").then((res) => { if (res === "Sign Up") { - if (extensionName === "Devant") { - commands.executeCommand("vscode.open", `${choreoEnvConfig.getDevantUrl()}/signup`); - } else { - commands.executeCommand("vscode.open", `${choreoEnvConfig.getConsoleUrl()}/signup`); - } + commands.executeCommand( + "vscode.open", + ` ${extensionName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl}/signup`, + ); } }); break; @@ -131,11 +135,7 @@ export function handlerError(err: any) { `Open ${extensionName} Console`, ).then((res) => { if (res === `Open ${extensionName} Console`) { - if (extensionName === "Devant") { - commands.executeCommand("vscode.open", choreoEnvConfig.getDevantUrl()); - } else { - commands.executeCommand("vscode.open", choreoEnvConfig.getConsoleUrl()); - } + commands.executeCommand("vscode.open", extensionName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl); } }); break; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/extension.ts b/workspaces/wso2-platform/wso2-platform-extension/src/extension.ts index 69bdbcfa660..cf172b01c65 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/extension.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/extension.ts @@ -24,8 +24,10 @@ import { initRPCServer } from "./choreo-rpc/activate"; import { activateCmds } from "./cmds"; import { continueCreateComponent } from "./cmds/create-component-cmd"; import { activateCodeLenses } from "./code-lens"; +import { activateDevantFeatures } from "./devant-utils"; import { ext } from "./extensionVariables"; import { getLogger, initLogger } from "./logger/logger"; +import { activateMcp } from "./mcp"; import { activateStatusbar } from "./status-bar"; import { authStore } from "./stores/auth-store"; import { contextStore } from "./stores/context-store"; @@ -35,14 +37,20 @@ import { ChoreoConfigurationProvider, addTerminalHandlers } from "./tarminal-han import { activateTelemetry } from "./telemetry/telemetry"; import { activateURIHandlers } from "./uri-handlers"; import { registerYamlLanguageServer } from "./yaml-ls"; +import { getCliVersion } from "./choreo-rpc/cli-install"; +import { getExtVersion } from "./utils"; export async function activate(context: vscode.ExtensionContext) { activateTelemetry(context); await initLogger(context); - getLogger().debug("Activating WSO2 Platform Extension"); + ext.context = context; ext.api = new PlatformExtensionApi(); - setInitialEnv(); + ext.choreoEnv = getChoreoEnv(); + + getLogger().info("Activating WSO2 Platform Extension"); + getLogger().info(`Extension version: ${getExtVersion(context)}`); + getLogger().info(`CLI version: ${getCliVersion()}`); // Initialize stores await authStore.persist.rehydrate(); @@ -73,12 +81,14 @@ export async function activate(context: vscode.ExtensionContext) { continueCreateComponent(); addTerminalHandlers(); context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("*", new ChoreoConfigurationProvider())); + activateMcp(context); + activateDevantFeatures(); getLogger().debug("WSO2 Platform Extension activated"); + ext.config = await ext.clients.rpcClient.getConfigFromCli(); }) .catch((e) => { getLogger().error("Failed to initialize rpc client", e); }); - activateCmds(context); activateURIHandlers(); activateCodeLenses(context); @@ -88,24 +98,19 @@ export async function activate(context: vscode.ExtensionContext) { return ext.api; } -function setInitialEnv() { - const choreoEnv = process.env.CHOREO_ENV || process.env.CLOUD_ENV; - if ( - choreoEnv && - ["dev", "stage", "prod"].includes(choreoEnv) && - workspace.getConfiguration().get("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment") !== choreoEnv - ) { - workspace.getConfiguration().update("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment", choreoEnv); - } -} +const getChoreoEnv = (): string => { + return ( + process.env.CHOREO_ENV || + process.env.CLOUD_ENV || + workspace.getConfiguration().get("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment") || + "prod" + ); +}; function registerPreInitHandlers(): any { workspace.onDidChangeConfiguration(async ({ affectsConfiguration }: ConfigurationChangeEvent) => { - if ( - affectsConfiguration("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment") || - affectsConfiguration("WSO2.WSO2-Platform.Advanced.RpcPath") || - affectsConfiguration("WSO2.WSO2-Platform.Advanced.StsToken") - ) { + if (affectsConfiguration("WSO2.WSO2-Platform.Advanced.ChoreoEnvironment") || affectsConfiguration("WSO2.WSO2-Platform.Advanced.RpcPath")) { + // skip showing this if cloud sts env is available const selection = await window.showInformationMessage( "WSO2 Platform extension configuration changed. Please restart vscode for changes to take effect.", "Restart Now", diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/extensionVariables.ts b/workspaces/wso2-platform/wso2-platform-extension/src/extensionVariables.ts index 31d6b0d9ef5..6bdddd2c01c 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/extensionVariables.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/extensionVariables.ts @@ -16,6 +16,7 @@ * under the License. */ +import type { GetCliRpcResp } from "@wso2/wso2-platform-core"; import type { ExtensionContext, StatusBarItem } from "vscode"; import type { PlatformExtensionApi } from "./PlatformExtensionApi"; import type { ChoreoRPCClient } from "./choreo-rpc"; @@ -25,6 +26,12 @@ export class ExtensionVariables { public context!: ExtensionContext; public api!: PlatformExtensionApi; public statusBarItem!: StatusBarItem; + public config?: GetCliRpcResp; + public choreoEnv: string; + + public constructor() { + this.choreoEnv = "prod"; + } public clients!: { rpcClient: ChoreoRPCClient; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/git/git.ts b/workspaces/wso2-platform/wso2-platform-extension/src/git/git.ts index 40ce0fdf45a..b869afd6a77 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/git/git.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/git/git.ts @@ -414,6 +414,7 @@ const COMMIT_FORMAT = "%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B"; export interface ICloneOptions { readonly parentPath: string; + readonly skipCreateSubPath?: boolean; readonly progress: Progress<{ increment: number }>; readonly recursive?: boolean; readonly ref?: string; @@ -518,7 +519,13 @@ export class Git { }; try { - const command = ["clone", url.includes(" ") ? encodeURI(url) : url, folderPath, "--progress"]; + const command = ["clone", url.includes(" ") ? encodeURI(url) : url]; + if(options.skipCreateSubPath){ + command.push(".") + }else{ + command.push(folderPath) + } + command.push("--progress") if (options.recursive) { command.push("--recursive"); } diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/mcp.ts b/workspaces/wso2-platform/wso2-platform-extension/src/mcp.ts new file mode 100644 index 00000000000..3aaa7f02c6a --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-extension/src/mcp.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 * as vscode from "vscode"; +import { getChoreoExecPath } from "./choreo-rpc/cli-install"; +import { getUserInfoForCmd } from "./cmds/cmd-utils"; +import { ext } from "./extensionVariables"; + +export function activateMcp(context: vscode.ExtensionContext) { + const didChangeEmitter = new vscode.EventEmitter(); + context.subscriptions.push( + vscode.lm.registerMcpServerDefinitionProvider("choreo", { + onDidChangeMcpServerDefinitions: didChangeEmitter.event, + provideMcpServerDefinitions: async () => { + const servers: vscode.McpServerDefinition[] = []; + servers.push( + new vscode.McpStdioServerDefinition( + "Choreo MCP Server", + getChoreoExecPath(), + ["start-mcp-server"], + { CHOREO_ENV: ext.choreoEnv, CHOREO_REGION: process.env.CLOUD_REGION || "" }, + "1.0.0", + ), + ); + return servers; + }, + resolveMcpServerDefinition: async (def, _token) => { + const userInfo = await getUserInfoForCmd("connect with Choreo MCP server"); + if (userInfo) { + return def; + } + return undefined; + }, + }), + ); +} diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/auth-store.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/auth-store.ts index 8801204ea5f..923cbdda230 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/auth-store.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/auth-store.ts @@ -27,21 +27,21 @@ import { getGlobalStateStore } from "./store-utils"; interface AuthStore { state: AuthState; resetState: () => void; - loginSuccess: (userInfo: UserInfo) => void; + loginSuccess: (userInfo: UserInfo, region: "US" | "EU") => void; logout: () => Promise; initAuth: () => Promise; } -const initialState: AuthState = { userInfo: null }; +const initialState: AuthState = { userInfo: null, region: "US" }; export const authStore = createStore( persist( (set, get) => ({ state: initialState, resetState: () => set(() => ({ state: initialState })), - loginSuccess: (userInfo) => { + loginSuccess: (userInfo, region) => { dataCacheStore.getState().setOrgs(userInfo.organizations); - set(({ state }) => ({ state: { ...state, userInfo } })); + set(({ state }) => ({ state: { ...state, userInfo, region } })); contextStore.getState().refreshState(); }, logout: async () => { @@ -54,7 +54,8 @@ export const authStore = createStore( try { const userInfo = await ext.clients.rpcClient.getUserInfo(); if (userInfo) { - get().loginSuccess(userInfo); + const region = await ext.clients.rpcClient.getCurrentRegion(); + get().loginSuccess(userInfo, region); const contextStoreState = contextStore.getState().state; if (contextStoreState.selected?.org) { ext?.clients?.rpcClient?.changeOrgContext(contextStoreState.selected?.org?.id?.toString()); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/context-store.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/context-store.ts index 527fb108d0e..8a01fb4ea16 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/context-store.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/context-store.ts @@ -36,7 +36,7 @@ import { createStore } from "zustand"; import { persist } from "zustand/middleware"; import { ext } from "../extensionVariables"; import { getGitRemotes, getGitRoot } from "../git/util"; -import { isSubpath } from "../utils"; +import { isSamePath, isSubpath } from "../utils"; import { authStore } from "./auth-store"; import { dataCacheStore } from "./data-cache-store"; import { locationStore } from "./location-store"; @@ -63,11 +63,12 @@ export const contextStore = createStore( if (authStore.getState().state?.userInfo) { set(({ state }) => ({ state: { ...state, loading: true } })); let items = await getAllContexts(get().state?.items); - let selected = getSelected(items, get().state?.selected); + let selected = await getSelected(items, get().state?.selected); + set(({ state }) => ({ state: { ...state, items, selected } })); let components = await getComponentsInfoCache(selected); set(({ state }) => ({ state: { ...state, items, selected, components } })); items = await getEnrichedContexts(get().state?.items); - selected = getSelected(items, selected); + selected = await getSelected(items, selected); components = await getComponentsInfoCache(selected); set(({ state }) => ({ state: { ...state, items, selected, components } })); components = await getComponentsInfo(selected); @@ -195,7 +196,44 @@ const getAllContexts = async (previousItems: { [key: string]: ContextItemEnriche return contextItems; }; -const getSelected = (items: { [key: string]: ContextItemEnriched }, prevSelected?: ContextItemEnriched) => { +const getSelected = async (items: { [key: string]: ContextItemEnriched }, prevSelected?: ContextItemEnriched) => { + if (process.env.CLOUD_STS_TOKEN && process.env.CLOUD_INITIAL_ORG_ID && process.env.CLOUD_INITIAL_PROJECT_ID) { + // Give priority to project provided as env variable, when running in the cloud editor + const userOrgs = authStore.getState().state.userInfo?.organizations; + const matchingOrg = userOrgs?.find( + (item) => item.uuid === process.env.CLOUD_INITIAL_ORG_ID || item.id?.toString() === process.env.CLOUD_INITIAL_ORG_ID, + ); + if (matchingOrg) { + let projectsCache = dataCacheStore.getState().getProjects(matchingOrg.handle); + if (projectsCache.length === 0) { + const projects = await ext.clients.rpcClient.getProjects(matchingOrg.id.toString()); + dataCacheStore.getState().setProjects(matchingOrg.handle, projects); + projectsCache = projects; + } + const matchingProject = projectsCache.find((item) => item.id === process.env.CLOUD_INITIAL_PROJECT_ID); + if (matchingProject) { + return { + orgHandle: matchingOrg.handle, + projectHandle: matchingProject.handler, + org: matchingOrg, + project: matchingProject, + contextDirs: + workspace.workspaceFolders?.map((item) => ({ + workspaceName: item.name, + projectRootFsPath: item.uri.fsPath, + dirFsPath: item.uri.fsPath, + })) ?? [], + } as ContextItemEnriched; + } + } + + const globalCompId: string | null | undefined = ext.context.globalState.get("code-server-component-id"); + if (globalCompId) { + await ext.context.globalState.update("code-server-component-id", null); + await ext.context.workspaceState.update("code-server-component-id", globalCompId); + } + } + let selected: ContextItemEnriched | undefined = undefined; const matchingItem = Object.values(items).find( (item) => @@ -300,9 +338,20 @@ const getComponentsInfo = async (selected?: ContextItemEnriched): Promise { + const workspaceCompId: string | null | undefined = ext.context.workspaceState.get("code-server-component-id") || process.env.SOURCE_COMPONENT_ID; // + if (process.env.CLOUD_STS_TOKEN && process.env.CLOUD_INITIAL_ORG_ID && process.env.CLOUD_INITIAL_PROJECT_ID && workspaceCompId) { + const filteredComps = components.filter((item) => item.metadata?.id === workspaceCompId); + if (filteredComps.length === 1) { + return filteredComps; + } + } + return components; +}; + const mapComponentList = async (components: ComponentKind[], selected?: ContextItemEnriched): Promise => { const comps: ContextStoreComponentState[] = []; - for (const componentItem of components) { + for (const componentItem of getFilteredComponents(components)) { if (selected?.contextDirs) { // biome-ignore lint/correctness/noUnsafeOptionalChaining: for (const item of selected?.contextDirs) { @@ -324,7 +373,12 @@ const mapComponentList = async (components: ComponentKind[], selected?: ContextI if (hasMatchingRemote) { const subPathDir = path.join(gitRoot, getComponentKindRepoSource(componentItem.spec.source)?.path); const isSubPath = isSubpath(item.dirFsPath, subPathDir); - if (isSubPath && existsSync(subPathDir) && !comps.some((item) => item.component?.metadata?.id === componentItem.metadata?.id)) { + const isPathSame = isSamePath(item.dirFsPath, subPathDir); + if ( + (isPathSame || isSubPath) && + existsSync(subPathDir) && + !comps.some((item) => item.component?.metadata?.id === componentItem.metadata?.id) + ) { comps.push({ component: componentItem, workspaceName: item.workspaceName, diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/data-cache-store.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/data-cache-store.ts index 3751dfecb86..d57723e2db3 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/data-cache-store.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/data-cache-store.ts @@ -19,6 +19,8 @@ import type { CommitHistory, ComponentKind, DataCacheState, Environment, Organization, Project } from "@wso2/wso2-platform-core"; import { createStore } from "zustand"; import { persist } from "zustand/middleware"; +import { ext } from "../extensionVariables"; +import { authStore } from "./auth-store"; import { getGlobalStateStore } from "./store-utils"; interface DataCacheStore { @@ -54,14 +56,14 @@ export const dataCacheStore = createStore( } = {}; projects.forEach((item) => { updatedProjects[item.handler] = { - components: get().state?.orgs?.[orgHandle]?.projects?.[item.handler]?.components || {}, + components: get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[item.handler]?.components || {}, data: item, }; }); const updatedOrgs = { ...(get().state?.orgs ?? {}), - [orgHandle]: { ...(get().state?.orgs?.[orgHandle] ?? {}), projects: updatedProjects }, + [getRootKey(orgHandle)]: { ...(get().state?.orgs?.[getRootKey(orgHandle)] ?? {}), projects: updatedProjects }, }; set(({ state }) => ({ state: { ...state, orgs: updatedOrgs } })); @@ -72,11 +74,11 @@ export const dataCacheStore = createStore( ...state, orgs: { ...(get().state?.orgs ?? {}), - [orgHandle]: { - ...(get().state?.orgs?.[orgHandle] ?? {}), + [getRootKey(orgHandle)]: { + ...(get().state?.orgs?.[getRootKey(orgHandle)] ?? {}), projects: { - ...(get().state?.orgs?.[orgHandle]?.projects ?? {}), - [projectHandle]: { ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle] ?? {}), envs }, + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects ?? {}), + [projectHandle]: { ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle] ?? {}), envs }, }, }, }, @@ -84,17 +86,17 @@ export const dataCacheStore = createStore( })); }, getEnvs: (orgHandle, projectHandle) => { - return get().state.orgs?.[orgHandle]?.projects?.[projectHandle]?.envs || []; + return get().state.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.envs || []; }, getProjects: (orgHandle) => { - const projectList = Object.values(get().state.orgs?.[orgHandle]?.projects ?? {}) + const projectList = Object.values(get().state.orgs?.[getRootKey(orgHandle)]?.projects ?? {}) .filter((item) => item.data) .map((item) => item.data); return projectList as Project[]; }, setComponents: (orgHandle, projectHandle, components) => { const newComponents: { [componentHandle: string]: { data?: ComponentKind; commits?: { [branch: string]: CommitHistory[] } } } = {}; - const prevComponents = get().state.orgs?.[orgHandle]?.projects?.[projectHandle]?.components ?? {}; + const prevComponents = get().state.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components ?? {}; components.forEach((item) => { const matchingItem = prevComponents[item.metadata.name]; newComponents[item.metadata.name] = { ...matchingItem, data: item }; @@ -102,12 +104,12 @@ export const dataCacheStore = createStore( const updatedOrgs = { ...(get().state?.orgs ?? {}), - [orgHandle]: { - ...(get().state?.orgs?.[orgHandle] ?? {}), + [getRootKey(orgHandle)]: { + ...(get().state?.orgs?.[getRootKey(orgHandle)] ?? {}), projects: { - ...(get().state?.orgs?.[orgHandle]?.projects ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects ?? {}), [projectHandle]: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle] ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle] ?? {}), components: newComponents, }, }, @@ -117,7 +119,7 @@ export const dataCacheStore = createStore( set(({ state }) => ({ state: { ...state, orgs: updatedOrgs } })); }, getComponents: (orgHandle, projectHandle) => { - const componentList = Object.values(get().state.orgs?.[orgHandle]?.projects?.[projectHandle]?.components ?? {}) + const componentList = Object.values(get().state.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components ?? {}) .filter((item) => item.data) .map((item) => item.data); return componentList as ComponentKind[]; @@ -125,18 +127,18 @@ export const dataCacheStore = createStore( setCommits: (orgHandle, projectHandle, componentHandle, branch, commits) => { const updatedOrgs = { ...(get().state?.orgs ?? {}), - [orgHandle]: { - ...(get().state?.orgs?.[orgHandle] ?? {}), + [getRootKey(orgHandle)]: { + ...(get().state?.orgs?.[getRootKey(orgHandle)] ?? {}), projects: { - ...(get().state?.orgs?.[orgHandle]?.projects ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects ?? {}), [projectHandle]: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle] ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle] ?? {}), components: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle]?.components ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components ?? {}), [componentHandle]: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle]?.components?.[componentHandle] ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components?.[componentHandle] ?? {}), commits: { - ...(get().state?.orgs?.[orgHandle]?.projects?.[projectHandle]?.components?.[componentHandle]?.commits ?? {}), + ...(get().state?.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components?.[componentHandle]?.commits ?? {}), [branch]: commits, }, }, @@ -149,10 +151,21 @@ export const dataCacheStore = createStore( set(({ state }) => ({ state: { ...state, orgs: updatedOrgs } })); }, getCommits: (orgHandle, projectHandle, componentHandle, branch) => { - const commitList = get().state.orgs?.[orgHandle]?.projects?.[projectHandle]?.components?.[componentHandle]?.commits?.[branch] ?? []; + const commitList = + get().state.orgs?.[getRootKey(orgHandle)]?.projects?.[projectHandle]?.components?.[componentHandle]?.commits?.[branch] ?? []; return commitList; }, }), - getGlobalStateStore("data-cache-zustand-storage-v1"), + getGlobalStateStore("data-cache-zustand-storage"), ), ); + +const getRootKey = (orgHandle: string) => { + const region = authStore.getState().state.region; + const env = ext.choreoEnv; + let orgRegionHandle = `${region}-${orgHandle}`; + if (env !== "prod") { + orgRegionHandle = `${env}-${orgRegionHandle}`; + } + return orgRegionHandle; +}; diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/location-store.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/location-store.ts index 831fa63881b..d68fc6776da 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/location-store.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/location-store.ts @@ -72,6 +72,6 @@ export const locationStore = createStore( .filter((item) => existsSync(item.fsPath)); }, }), - getGlobalStateStore("location-zustand-storage-v2"), + getGlobalStateStore("location-zustand-storage"), ), ); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/stores/store-utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/stores/store-utils.ts index 5c2da0bae69..8d3a7b97607 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/stores/store-utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/stores/store-utils.ts @@ -19,9 +19,11 @@ import { type PersistOptions, createJSONStorage } from "zustand/middleware"; import { ext } from "../extensionVariables"; +const version = "v4"; + export const getGlobalStateStore = (storeName: string): PersistOptions => { return { - name: storeName, + name: `${storeName}-${version}`, storage: createJSONStorage(() => ({ getItem: async (name) => { const value = await ext.context.globalState.get(name); @@ -36,7 +38,7 @@ export const getGlobalStateStore = (storeName: string): PersistOptions export const getWorkspaceStateStore = (storeName: string): PersistOptions => { return { - name: storeName, + name: `${storeName}-${version}`, storage: createJSONStorage(() => ({ getItem: async (name) => { const value = await ext.context.workspaceState.get(name); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts b/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts index 01b4c5564a0..b413bfe0b4c 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/uri-handlers.ts @@ -23,6 +23,7 @@ import { type ICloneProjectCmdParams, type Organization, type Project, + type UserInfo, getComponentKindRepoSource, type openClonedDirReq, parseGitURL, @@ -32,7 +33,6 @@ import { ResponseError } from "vscode-jsonrpc"; import { ErrorCode } from "./choreo-rpc/constants"; import { getUserInfoForCmd, isRpcActive } from "./cmds/cmd-utils"; import { updateContextFile } from "./cmds/create-directory-context-cmd"; -import { choreoEnvConfig } from "./config"; import { ext } from "./extensionVariables"; import { getGitRemotes, getGitRoot } from "./git/util"; import { getLogger } from "./logger/logger"; @@ -41,7 +41,7 @@ import { contextStore, getContextKey, waitForContextStoreToLoad } from "./stores import { dataCacheStore } from "./stores/data-cache-store"; import { locationStore } from "./stores/location-store"; import { webviewStateStore } from "./stores/webview-state-store"; -import { delay, isSamePath, openDirectory } from "./utils"; +import { isSamePath, openDirectory } from "./utils"; export function activateURIHandlers() { window.registerUriHandler({ @@ -68,9 +68,12 @@ export function activateURIHandlers() { async () => { try { const orgId = contextStore?.getState().state?.selected?.org?.id?.toString(); - const callbackUrl = extName === "Devant" ? `${choreoEnvConfig.getDevantUrl()}/vscode-auth` : undefined; - const clientId = extName === "Devant" ? choreoEnvConfig.getDevantAsgardeoClientId() : undefined; - const userInfo = await ext.clients.rpcClient.signInWithAuthCode(authCode, region, orgId, callbackUrl, clientId); + let userInfo: UserInfo | undefined; + if (extName === "Devant") { + userInfo = await ext.clients.rpcClient.signInDevantWithAuthCode(authCode, region, orgId); + } else { + userInfo = await ext.clients.rpcClient.signInWithAuthCode(authCode, region, orgId); + } if (userInfo) { if (contextStore?.getState().state?.selected) { const includesOrg = userInfo.organizations?.some((item) => item.handle === contextStore?.getState().state?.selected?.orgHandle); @@ -78,7 +81,8 @@ export function activateURIHandlers() { contextStore.getState().resetState(); } } - authStore.getState().loginSuccess(userInfo); + const region = await ext.clients.rpcClient.getCurrentRegion(); + authStore.getState().loginSuccess(userInfo, region); window.showInformationMessage(`Successfully signed into ${extName}`); } } catch (error: any) { @@ -100,7 +104,7 @@ export function activateURIHandlers() { } else if (uri.path === "/ghapp") { try { isRpcActive(ext); - getLogger().info("WSO2 Platform Githup auth Callback hit"); + getLogger().info("WSO2 Platform Github auth Callback hit"); const urlParams = new URLSearchParams(uri.query); const authCode = urlParams.get("code"); // const installationId = urlParams.get("installationId"); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts b/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts index 962d0988175..115f2467cd1 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/utils.ts @@ -196,8 +196,16 @@ export const saveFile = async ( }; export const isSamePath = (parent: string, sub: string): boolean => { - const normalizedParent = getNormalizedPath(parent).toLowerCase(); - const normalizedSub = getNormalizedPath(sub).toLowerCase(); + let normalizedParent = getNormalizedPath(parent).toLowerCase(); + if (normalizedParent.endsWith("/")) { + normalizedParent = normalizedParent.slice(0, -1); + } + + let normalizedSub = getNormalizedPath(sub).toLowerCase(); + if (normalizedSub.endsWith("/")) { + normalizedSub = normalizedSub.slice(0, -1); + } + if (normalizedParent === normalizedSub) { return true; } @@ -205,8 +213,16 @@ export const isSamePath = (parent: string, sub: string): boolean => { }; export const isSubpath = (parent: string, sub: string): boolean => { - const normalizedParent = getNormalizedPath(parent).toLowerCase(); - const normalizedSub = getNormalizedPath(sub).toLowerCase(); + let normalizedParent = getNormalizedPath(parent).toLowerCase(); + if (normalizedParent.endsWith("/")) { + normalizedParent = normalizedParent.slice(0, -1); + } + + let normalizedSub = getNormalizedPath(sub).toLowerCase(); + if (normalizedSub.endsWith("/")) { + normalizedSub = normalizedSub.slice(0, -1); + } + if (normalizedParent === normalizedSub) { return true; } @@ -413,3 +429,16 @@ export const getConfigFileDrifts = async ( return []; } }; + +export const parseJwt = (token: string): { iss: string } | null => { + try { + return JSON.parse(atob(token.split(".")[1])); + } catch (e) { + return null; + } +}; + +export const getExtVersion = (context: ExtensionContext): string => { + const packageJson = JSON.parse(readFileSync(path.join(context?.extensionPath, "package.json"), "utf8")); + return packageJson?.version; +}; \ No newline at end of file diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentDetailsView.ts b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentDetailsView.ts index 4fd5fd80195..d93bbe57507 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentDetailsView.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentDetailsView.ts @@ -32,10 +32,25 @@ class ComponentDetailsView { private _disposables: vscode.Disposable[] = []; private _rpcHandler: WebViewPanelRpc; - constructor(extensionUri: vscode.Uri, organization: Organization, project: Project, component: ComponentKind, directoryFsPath?: string) { + constructor( + extensionUri: vscode.Uri, + organization: Organization, + project: Project, + component: ComponentKind, + directoryFsPath?: string, + isNewComponent?: boolean, + ) { this._panel = ComponentDetailsView.createWebview(component); this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri, organization, project, component, directoryFsPath); + this._panel.webview.html = this._getWebviewContent( + this._panel.webview, + extensionUri, + organization, + project, + component, + directoryFsPath, + isNewComponent, + ); this._rpcHandler = new WebViewPanelRpc(this._panel); } @@ -67,6 +82,7 @@ class ComponentDetailsView { project: Project, component: ComponentKind, directoryFsPath?: string, + isNewComponent?: boolean, ) { // The JS file from the React build output const scriptUri = getUri(webview, extensionUri, ["resources", "jslibs", "main.js"]); @@ -99,6 +115,7 @@ class ComponentDetailsView { project, component, initialEnvs: dataCacheStore.getState().getEnvs(organization.handle, project.handler), + isNewComponent, } as WebviewProps)} ); } @@ -142,6 +159,7 @@ export const showComponentDetailsView = ( component: ComponentKind, directoryFsPath: string, viewColumn?: vscode.ViewColumn, + isNewComponent?: boolean, ) => { const webView = getComponentDetailsView(org.handle, project.handler, component.metadata.name); const componentKey = getComponentKey(org, project, component); @@ -150,7 +168,7 @@ export const showComponentDetailsView = ( webView?.reveal(viewColumn); } else { webviewStateStore.getState().onCloseComponentDrawer(getComponentKey(org, project, component)); - const componentDetailsView = new ComponentDetailsView(ext.context.extensionUri, org, project, component, directoryFsPath); + const componentDetailsView = new ComponentDetailsView(ext.context.extensionUri, org, project, component, directoryFsPath, isNewComponent); componentDetailsView.getWebview()?.reveal(viewColumn); componentViewMap.set(componentKey, componentDetailsView); diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentFormView.ts b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentFormView.ts index 080786b8e50..782ca77d959 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentFormView.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/ComponentFormView.ts @@ -43,7 +43,7 @@ export class ComponentFormView { const extName = webviewStateStore.getState().state?.extensionName; const panel = vscode.window.createWebviewPanel( "create-new-component", - extName === "Devant" ? "Create Integration" : "Create Component", + extName === "Devant" ? "Deploy Integration" : "Create Component", vscode.ViewColumn.One, { enableScripts: true, diff --git a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts index ef5b696cb5e..188185c3e14 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts +++ b/workspaces/wso2-platform/wso2-platform-extension/src/webviews/WebviewRPC.ts @@ -16,11 +16,29 @@ * under the License. */ -import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs"; +import { + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + rmdirSync, + statSync, + unlinkSync, + writeFileSync, +} from "fs"; +import * as fs from "fs"; +import * as os from "os"; import { join } from "path"; +import * as toml from "@iarna/toml"; import { AuthStoreChangedNotification, ClearWebviewCache, + CloneRepositoryIntoCompDir, + type CloneRepositoryIntoCompDirReq, CloseComponentViewDrawer, CloseWebViewNotification, type CommitHistory, @@ -57,6 +75,8 @@ import { OpenExternalChoreo, OpenSubDialogRequest, type ProxyConfig, + PushEverythingToRemoteRepo, + type PushEverythingToRemoteRepoReq, ReadFile, ReadLocalEndpointsConfig, ReadLocalProxyConfig, @@ -84,21 +104,22 @@ import { WebviewNotificationsMethodList, type WebviewQuickPickItem, WebviewStateChangedNotification, + buildGitURL, deepEqual, getShortenedHash, makeURLSafe, } from "@wso2/wso2-platform-core"; import * as yaml from "js-yaml"; -import { ProgressLocation, QuickPickItemKind, Uri, type WebviewPanel, type WebviewView, commands, env, window } from "vscode"; +import { ProgressLocation, QuickPickItemKind, Uri, type WebviewPanel, type WebviewView, commands, env, window, workspace } from "vscode"; import * as vscode from "vscode"; import { Messenger } from "vscode-messenger"; import { BROADCAST } from "vscode-messenger-common"; import { registerChoreoRpcResolver } from "../choreo-rpc"; -import { getChoreoEnv, getChoreoExecPath } from "../choreo-rpc/cli-install"; +import { getChoreoExecPath } from "../choreo-rpc/cli-install"; import { quickPickWithLoader } from "../cmds/cmd-utils"; import { submitCreateComponentHandler } from "../cmds/create-component-cmd"; -import { choreoEnvConfig } from "../config"; import { ext } from "../extensionVariables"; +import { initGit } from "../git/main"; import { getGitHead, getGitRemotes, getGitRoot, hasDirtyRepo, removeCredentialsFromGitURL } from "../git/util"; import { getLogger } from "../logger/logger"; import { authStore } from "../stores/auth-store"; @@ -168,11 +189,14 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W vscode.env.openExternal(vscode.Uri.parse(url)); }); messenger.onRequest(OpenExternalChoreo, (choreoPath: string) => { - if (webviewStateStore.getState().state.extensionName === "Devant") { - vscode.env.openExternal(vscode.Uri.joinPath(vscode.Uri.parse(choreoEnvConfig.getDevantUrl()), choreoPath)); - } else { - vscode.env.openExternal(vscode.Uri.joinPath(vscode.Uri.parse(choreoEnvConfig.getConsoleUrl()), choreoPath)); - } + vscode.env.openExternal( + vscode.Uri.joinPath( + vscode.Uri.parse( + (webviewStateStore.getState().state.extensionName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl) || "", + ), + choreoPath, + ), + ); }); messenger.onRequest(SetWebviewCache, async (params: { cacheKey: string; data: any }) => { await ext.context.workspaceState.update(params.cacheKey, params.data); @@ -250,7 +274,7 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W async (params: { orgName: string; projectName: string; componentName: string; deploymentTrackName: string; envName: string; type: string }) => { const { orgName, projectName, componentName, deploymentTrackName, envName, type } = params; // todo: export the env from here - if (getChoreoEnv() !== "prod") { + if (ext.choreoEnv !== "prod") { window.showErrorMessage( "Choreo extension currently displays runtime logs is only if 'WSO2.Platform.Advanced.ChoreoEnvironment' is set to 'prod'", ); @@ -271,16 +295,16 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W return Buffer.from(JSON.stringify(state), "binary").toString("base64"); }; messenger.onRequest(TriggerGithubAuthFlow, async (orgId: string) => { - const { authUrl, clientId, redirectUrl, devantRedirectUrl } = choreoEnvConfig.getGHAppConfig(); const extName = webviewStateStore.getState().state.extensionName; + const baseUrl = extName === "Devant" ? ext.config?.devantConsoleUrl : ext.config?.choreoConsoleUrl; + const redirectUrl = `${baseUrl}/ghapp`; const state = await _getGithubUrlState(orgId); - const ghURL = Uri.parse(`${authUrl}?redirect_uri=${extName === "Devant" ? devantRedirectUrl : redirectUrl}&client_id=${clientId}&state=${state}`); + const ghURL = Uri.parse(`${ext.config?.ghApp.authUrl}?redirect_uri=${redirectUrl}&client_id=${ext.config?.ghApp.clientId}&state=${state}`); await env.openExternal(ghURL); }); messenger.onRequest(TriggerGithubInstallFlow, async (orgId: string) => { - const { installUrl } = choreoEnvConfig.getGHAppConfig(); const state = await _getGithubUrlState(orgId); - const ghURL = Uri.parse(`${installUrl}?state=${state}`); + const ghURL = Uri.parse(`${ext.config?.ghApp.installUrl}?state=${state}`); await env.openExternal(ghURL); }); messenger.onRequest(SubmitComponentCreate, submitCreateComponentHandler); @@ -552,6 +576,116 @@ function registerWebviewRPCHandlers(messenger: Messenger, view: WebviewPanel | W messenger.onRequest(GetConfigFileDrifts, async (params: GetConfigFileDriftsReq) => { return getConfigFileDrifts(params.type, params.repoUrl, params.branch, params.repoDir, ext.context); }); + messenger.onRequest(CloneRepositoryIntoCompDir, async (params: CloneRepositoryIntoCompDirReq) => { + const extName = webviewStateStore.getState().state.extensionName; + const newGit = await initGit(ext.context); + if (!newGit) { + throw new Error("failed to retrieve Git details"); + } + const _repoUrl = buildGitURL(params.repo.orgHandler, params.repo.repo, params.repo.provider, true, params.repo.serverUrl); + if (!_repoUrl || !_repoUrl.startsWith("https://")) { + throw new Error("failed to parse git details"); + } + const urlObj = new URL(_repoUrl); + + if (process.env.CLOUD_STS_TOKEN) { + try { + const gitPat = await window.withProgress({ title: `Accessing the repository ${_repoUrl}...`, location: ProgressLocation.Notification }, () => + ext.clients.rpcClient.getGitTokenForRepository({ + orgId: params.org.id?.toString(), + gitOrg: params.repo.orgName, + gitRepo: params.repo.repo, + secretRef: params.repo.secretRef || "", + }), + ); + + urlObj.username = gitPat.username || "x-access-token"; + urlObj.password = gitPat.token; + } catch { + getLogger().debug(`Failed to get token for ${params}`); + } + } + + const repoUrl = urlObj.href; + + // if ballerina toml exists, need to update the org and name + const balTomlPath = join(params.cwd, "Ballerina.toml"); + if (existsSync(balTomlPath)) { + const fileContent = await fs.promises.readFile(balTomlPath, "utf-8"); + const parsedToml: any = toml.parse(fileContent); + if (parsedToml?.package) { + parsedToml.package.org = params.org.handle; + parsedToml.package.name = params.componentName?.replaceAll("-", "_"); + } + const updatedTomlContent = toml.stringify(parsedToml); + await fs.promises.writeFile(balTomlPath, updatedTomlContent, "utf-8"); + } + + if (params.repo?.isBareRepo && ["", "/", "."].includes(params.subpath)) { + // if component is to be created in the root of a bare repo, + // the we can initialize the current directory as the repo root + await window.withProgress({ title: `Initializing currently opened directory as repository ${_repoUrl}...`, location: ProgressLocation.Notification }, async () => { + await newGit.init(params.cwd); + const dotGit = await newGit?.getRepositoryDotGit(params.cwd); + const repo = newGit.open(params.cwd, dotGit); + await repo.addRemote("origin", repoUrl); + await repo.add(["."]); + await repo.commit(`Add source for new ${extName} ${extName === "Devant" ? "Integration" : "Component"} (${params.componentName})`); + await repo.push("origin", "main"); + await repo.fetch(); + }); + return params.cwd; + } + + const clonedPath = await window.withProgress( + { + title: `Cloning repository ${params.repo?.orgHandler}/${params.repo.repo}`, + location: ProgressLocation.Notification, + }, + async (progress, cancellationToken) => + newGit.clone( + repoUrl, + { + recursive: true, + ref: params.repo.branch, + parentPath: join(params.cwd, ".."), + progress: { + report: ({ increment, ...rest }: { increment: number }) => progress.report({ increment: increment, ...rest }), + }, + }, + cancellationToken, + ), + ); + + // Move everything into cloned dir + const cwdFiled = readdirSync(params.cwd); + const newPath = join(clonedPath, params.subpath); + fs.mkdirSync(newPath, { recursive: true }); + + for (const file of cwdFiled) { + const cwdFilePath = join(params.cwd, file); + const destFilePath = join(newPath, file); + fs.cpSync(cwdFilePath, destFilePath, { recursive: true }); + } + + return newPath; + }); + + messenger.onRequest(PushEverythingToRemoteRepo, async (params: PushEverythingToRemoteRepoReq) => { + const newGit = await initGit(ext.context); + if (!newGit) { + throw new Error("failed to initGit"); + } + const extName = webviewStateStore.getState().state.extensionName; + const repoRoot = await newGit?.getRepositoryRoot(params.dirPath); + const dotGit = await newGit?.getRepositoryDotGit(params.dirPath); + const repo = newGit.open(repoRoot, dotGit); + await window.withProgress({ title: "Pushing the changes to your remote repository...", location: ProgressLocation.Notification }, async () => { + await repo.add(["."]); + await repo.commit(`Add source for new ${extName} ${extName === "Devant" ? "Integration" : "Component"} (${params.componentName})`); + await repo.push(); + }); + }); // Register Choreo CLL RPC handler registerChoreoRpcResolver(messenger, ext.clients.rpcClient); diff --git a/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js b/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js index 0dbaf0a01bb..09d75062f98 100644 --- a/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js +++ b/workspaces/wso2-platform/wso2-platform-extension/webpack.config.js @@ -22,18 +22,19 @@ const CopyPlugin = require("copy-webpack-plugin"); const PermissionsOutputPlugin = require("webpack-permissions-plugin"); const webpack = require("webpack"); const dotenv = require("dotenv"); -const { createEnvDefinePlugin } = require('../../../common/scripts/env-webpack-helper'); +const { createEnvDefinePlugin } = require("../../../common/scripts/env-webpack-helper"); const envPath = path.resolve(__dirname, ".env"); const env = dotenv.config({ path: envPath }).parsed; + console.log("Fetching values for environment variables..."); const { envKeys, missingVars } = createEnvDefinePlugin(env); if (missingVars.length > 0) { - console.warn( - '\n⚠️ Environment Variable Configuration Warning:\n' + - `Missing required environment variables: ${missingVars.join(', ')}\n` + - `Please provide values in either .env file or runtime environment.\n` - ); + console.warn( + `\n⚠️ Environment Variable Configuration Warning:\n + Missing required environment variables: ${missingVars.join(", ")}\n + Please provide values in either .env file or runtime environment.\n`, + ); } //@ts-check diff --git a/workspaces/wso2-platform/wso2-platform-webviews/package.json b/workspaces/wso2-platform/wso2-platform-webviews/package.json index 5f74ed8270f..d82fb50ff8a 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/package.json +++ b/workspaces/wso2-platform/wso2-platform-webviews/package.json @@ -31,7 +31,7 @@ "clipboardy": "^4.0.0", "@formkit/auto-animate": "0.8.2", "timezone-support": "^3.1.0", - "swagger-ui-react": "^5.22.0", + "swagger-ui-react": "5.22.0", "@biomejs/biome": "^1.9.4", "@headlessui/react": "^2.1.2", "react-markdown": "^7.1.0", @@ -52,7 +52,6 @@ "@types/vscode-webview": "^1.57.5", "css-loader": "^7.1.2", "file-loader": "^6.2.0", - "node-sass": "^9.0.0", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", "ts-loader": "^9.5.2", diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/FormElements/Dropdown/Dropdown.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/FormElements/Dropdown/Dropdown.tsx index 0e4740fe05f..c94e58d6e20 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/FormElements/Dropdown/Dropdown.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/FormElements/Dropdown/Dropdown.tsx @@ -28,7 +28,7 @@ interface Props { required?: boolean; loading?: boolean; control?: Control; - items?: ({ value: string; label?: string } | string)[]; + items?: ({ value: string; label?: string; type?: "separator" } | string)[]; disabled?: boolean; wrapClassName?: HTMLProps["className"]; } @@ -53,10 +53,20 @@ export const Dropdown: FC = (props) => { disabled={disabled || loading || undefined} {...field} > - {items?.map((item) => ( - - {typeof item === "string" ? item : item?.label || item.value} - + {items?.map((item, index) => ( + <> + {typeof item !== "string" && item.type === "separator" ? ( + + ) : ( + + {typeof item === "string" ? item : item?.label || item.value} + + )} + ))} diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/SwaggerUI/SwaggerUI.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/SwaggerUI/SwaggerUI.tsx index f6e6fb1b45e..0aa9802ca2c 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/SwaggerUI/SwaggerUI.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/SwaggerUI/SwaggerUI.tsx @@ -18,7 +18,7 @@ import React, { type HTMLProps, type FC } from "react"; import SwaggerUIReact from "swagger-ui-react"; -import "@wso2/ui-toolkit/src/styles/swagger/main.scss"; +import "@wso2/ui-toolkit/src/styles/swagger/styles.css"; import classNames from "classnames"; import type SwaggerUIProps from "swagger-ui-react/swagger-ui-react"; import { Codicon } from "../Codicon/Codicon"; diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/components/VerticalStepper/VerticalStepper.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/components/VerticalStepper/VerticalStepper.tsx index 322b22d49c7..d04696240cc 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/components/VerticalStepper/VerticalStepper.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/components/VerticalStepper/VerticalStepper.tsx @@ -71,22 +71,27 @@ export const VerticalStepperItem: FC = ({ return (
-
-
- {index < currentStep ? : {index + 1}} + {totalSteps > 1 && ( +
+
+ {index < currentStep ? : {index + 1}} +
+
{item.label}
-
{item.label}
-
+ )} +
-
-
-
+ {totalSteps > 1 && ( +
+
+
+ )}
{index === currentStep && (
diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/hooks/use-queries.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/hooks/use-queries.tsx index fd5993d28a4..009d5b75aac 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/hooks/use-queries.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/hooks/use-queries.tsx @@ -31,6 +31,7 @@ import { type DeploymentLogsData, type DeploymentTrack, type Environment, + type GetAuthorizedGitOrgsResp, type GetAutoBuildStatusResp, type GetTestKeyResp, type Organization, @@ -46,29 +47,30 @@ export const queryKeys = { "has-config-drift", { directoryPath, component: component?.metadata?.id, branch }, ], - getProjectEnvs: (project: Project, org: Organization) => ["get-project-envs", { organization: org.handle, project: project.handler }], + getProjectEnvs: (project: Project, org: Organization) => ["get-project-envs", { organization: org.uuid, project: project.id }], getTestKey: (endpointApimId: string, env: Environment, org: Organization) => [ "get-test-key", - { endpoint: endpointApimId, env: env.id, org: org.handle }, + { endpoint: endpointApimId, env: env.id, org: org.uuid }, ], - getSwaggerSpec: (apiRevisionId: string, org: Organization) => ["get-swagger-spec", { selectedEndpoint: apiRevisionId, org: org.handle }], + getSwaggerSpec: (apiRevisionId: string, org: Organization) => ["get-swagger-spec", { selectedEndpoint: apiRevisionId, org: org.uuid }], getBuildPacks: (selectedType: string, org: Organization) => ["build-packs", { selectedType, orgId: org?.id }], + getAuthorizedGitOrgs: (orgId: string, provider: string, credRef = "") => ["get-authorized-github-orgs", { orgId, provider, credRef }], getGitBranches: (repoUrl: string, org: Organization, credRef: string, isAccessible: boolean) => [ "get-git-branches", { repo: repoUrl, orgId: org?.id, credRef, isAccessible }, ], getDeployedEndpoints: (deploymentTrack: DeploymentTrack, component: ComponentKind, org: Organization) => [ "get-deployed-endpoints", - { organization: org.handle, component: component.metadata.id, deploymentTrackId: deploymentTrack?.id }, + { organization: org.uuid, component: component.metadata.id, deploymentTrackId: deploymentTrack?.id }, ], getProxyDeploymentInfo: (component: ComponentKind, org: Organization, env: Environment, apiVersion: ApiVersion) => [ "get-proxy-deployment-info", - { org: org.handle, component: component.metadata.id, env: env?.id, apiVersion: apiVersion?.id }, + { org: org.uuid, component: component.metadata.id, env: env?.id, apiVersion: apiVersion?.id }, ], getDeploymentStatus: (deploymentTrack: DeploymentTrack, component: ComponentKind, org: Organization, env: Environment) => [ "get-deployment-status", { - organization: org.handle, + organization: org.uuid, component: component.metadata.id, deploymentTrackId: deploymentTrack?.id, envId: env.id, @@ -77,28 +79,34 @@ export const queryKeys = { getWorkflowStatus: (org: Organization, env: Environment, buildId: string) => [ "get-workflow-status", { - organization: org?.handle, + organization: org?.uuid, envId: env?.id, buildId, }, ], getBuilds: (deploymentTrack: DeploymentTrack, component: ComponentKind, project: Project, org: Organization) => [ "get-builds", - { component: component.metadata.id, organization: org.handle, project: project.handler, branch: deploymentTrack?.branch }, + { component: component.metadata.id, organization: org.uuid, project: project.id, branch: deploymentTrack?.branch }, ], - getBuildsLogs: (component: ComponentKind, project: Project, org: Organization, build: BuildKind) => [ + getBuildsLogs: (component: ComponentKind, deploymentTrack: DeploymentTrack, project: Project, org: Organization, build: BuildKind) => [ "get-build-logs", - { component: component.metadata.id, organization: org.handle, project: project.handler, build: build?.status?.runId }, + { + component: component.metadata.id, + deploymentTrack: deploymentTrack.id, + organization: org.uuid, + project: project.id, + build: build?.status?.runId, + }, ], getComponentConnections: (component: ComponentKind, project: Project, org: Organization) => [ "get-component-connections", - { component: component.metadata.id, organization: org.handle, project: project.handler }, + { component: component.metadata.id, organization: org.uuid, project: project.id }, ], - useComponentList: (project: Project, org: Organization) => ["get-components", { organization: org.handle, project: project.handler }], - getProjectConnections: (project: Project, org: Organization) => ["get-project-connections", { organization: org.handle, project: project.handler }], + useComponentList: (project: Project, org: Organization) => ["get-components", { organization: org.uuid, project: project.id }], + getProjectConnections: (project: Project, org: Organization) => ["get-project-connections", { organization: org.uuid, project: project.id }], getAutoBuildStatus: (component: ComponentKind, deploymentTrack: DeploymentTrack, org: Organization) => [ "get-auto-build-status", - { component: component.metadata.id, organization: org.handle, versionId: deploymentTrack?.id }, + { component: component.metadata.id, organization: org.uuid, versionId: deploymentTrack?.id }, ], }; @@ -151,6 +159,13 @@ export const useGetBuildPacks = (selectedType: string, org: Organization, option options, ); +export const useGetAuthorizedGitOrgs = (orgId: string, provider: string, credRef = "", options?: UseQueryOptions) => + useQuery( + queryKeys.getAuthorizedGitOrgs(orgId, provider, credRef), + () => ChoreoWebViewAPI.getInstance().getChoreoRpcClient().getAuthorizedGitOrgs({ orgId, credRef }), + options, + ); + export const useGetGitBranches = (repoUrl: string, org: Organization, credRef = "", isAccessible = false, options?: UseQueryOptions) => useQuery( queryKeys.getGitBranches(repoUrl, org, credRef, isAccessible), @@ -386,13 +401,14 @@ export const useGoToSource = () => { export const useGetBuildLogs = ( component: ComponentKind, + deploymentTrack: DeploymentTrack, org: Organization, project: Project, build: BuildKind, options?: UseQueryOptions, ) => useQuery( - queryKeys.getBuildsLogs(component, project, org, build), + queryKeys.getBuildsLogs(component, deploymentTrack, project, org, build), async () => { try { const buildLog = await ChoreoWebViewAPI.getInstance().getChoreoRpcClient().getBuildLogs({ @@ -400,8 +416,12 @@ export const useGetBuildLogs = ( displayType: component.spec.type, orgHandler: org.handle, orgId: org.id.toString(), + orgUuid: org.uuid, projectId: project.id, buildId: build.status?.runId, + buildRef: build.status?.buildRef, + clusterId: build.status?.clusterId, + deploymentTrackId: deploymentTrack.id, }); return buildLog ?? null; } catch { diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/providers/react-query-provider.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/providers/react-query-provider.tsx index a1d782893a2..192a9ec3cc3 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/providers/react-query-provider.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/providers/react-query-provider.tsx @@ -53,7 +53,7 @@ export const ChoreoWebviewQueryClientProvider = ({ type, children }: { type: str } persistOptions={{ persister: webviewStatePersister(`react-query-persister-${type}`), - buster: "choreo-webview-cache-v2", + buster: "choreo-webview-cache-v5", }} > {children} diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/utilities/vscode-webview-rpc.ts b/workspaces/wso2-platform/wso2-platform-webviews/src/utilities/vscode-webview-rpc.ts index 2b22d597887..f3239497e6b 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/utilities/vscode-webview-rpc.ts +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/utilities/vscode-webview-rpc.ts @@ -19,8 +19,11 @@ import { type AuthState, AuthStoreChangedNotification, + ChoreoRpcGetAuthorizedGitOrgsRequest, ChoreoRpcWebview, ClearWebviewCache, + CloneRepositoryIntoCompDir, + type CloneRepositoryIntoCompDirReq, CloseComponentViewDrawer, CloseWebViewNotification, type CommitHistory, @@ -39,6 +42,7 @@ import { ExecuteCommandRequest, FileExists, GetAuthState, + GetAuthorizedGitOrgsReq, GetConfigFileDrifts, type GetConfigFileDriftsReq, GetContextState, @@ -59,6 +63,8 @@ import { OpenExternalChoreo, OpenSubDialogRequest, type OpenTestViewReq, + PushEverythingToRemoteRepo, + type PushEverythingToRemoteRepoReq, ReadFile, ReadLocalEndpointsConfig, type ReadLocalEndpointsConfigResp, @@ -245,6 +251,14 @@ export class ChoreoWebViewAPI { return this._messenger.sendRequest(TriggerGithubAuthFlow, HOST_EXTENSION, orgId); } + public async cloneRepositoryIntoCompDir(params: CloneRepositoryIntoCompDirReq): Promise { + return this._messenger.sendRequest(CloneRepositoryIntoCompDir, HOST_EXTENSION, params); + } + + public async pushEverythingToRemoteRepo(params: PushEverythingToRemoteRepoReq): Promise { + return this._messenger.sendRequest(PushEverythingToRemoteRepo, HOST_EXTENSION, params); + } + public async triggerGithubInstallFlow(orgId: string): Promise { return this._messenger.sendRequest(TriggerGithubInstallFlow, HOST_EXTENSION, orgId); } diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/ComponentDetailsView.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/ComponentDetailsView.tsx index ab02ea7c246..d1b5edf5e26 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/ComponentDetailsView.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/ComponentDetailsView.tsx @@ -17,7 +17,7 @@ */ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { type BuildKind, ChoreoComponentType, @@ -51,9 +51,11 @@ export const ComponentDetailsView: FC = (props) = const deploymentTracks = component?.deploymentTracks ?? []; const [rightPanelRef] = useAutoAnimate(); const type = getTypeForDisplayType(props.component.spec?.type); + const queryClient = useQueryClient(); const [deploymentTrack, setDeploymentTrack] = useState(deploymentTracks?.find((item) => item.latest)); const [hasOngoingBuilds, setHasOngoingBuilds] = useState(false); + const [prevBuildList, setPrevBuildList] = useState([]); const [buildDetailsPanel, setBuildDetailsPanel] = useState<{ open: boolean; build?: BuildKind }>({ open: false, build: null }); useEffect(() => { @@ -125,12 +127,12 @@ export const ComponentDetailsView: FC = (props) = refetchOnWindowFocus: true, }); - const buildLogsQueryData = useGetBuildLogs(component, organization, project, buildDetailsPanel?.build, { + const buildLogsQueryData = useGetBuildLogs(component, deploymentTrack, organization, project, buildDetailsPanel?.build, { enabled: !!buildDetailsPanel?.build, }); const buildListQueryData = useGetBuildList(deploymentTrack, component, project, organization, { - onSuccess: (builds) => { + onSuccess: async (builds) => { setHasOngoingBuilds(builds.some((item) => item.status?.conclusion === "")); if (buildDetailsPanel?.open && buildDetailsPanel?.build) { const matchingItem = builds.find((item) => item.status?.runId === buildDetailsPanel?.build?.status?.runId); @@ -139,9 +141,27 @@ export const ComponentDetailsView: FC = (props) = } buildLogsQueryData.refetch(); } + const hasPrevSucceedBuilds = prevBuildList.filter((item) => item.status.conclusion === "success").length > 0; + if (!hasPrevSucceedBuilds && builds.length > 0 && builds[0].status?.conclusion === "success" && envs.length > 0) { + // have a new succeeded build, which should be auto deployed + await new Promise((resolve) => setTimeout(resolve, 10000)); + if (getTypeForDisplayType(component.spec?.type) === ChoreoComponentType.ApiProxy) { + queryClient.refetchQueries({ + queryKey: queryKeys.getProxyDeploymentInfo( + component, + organization, + envs[0], + component?.apiVersions?.find((item) => item.latest), + ), + }); + } else { + queryClient.refetchQueries({ queryKey: queryKeys.getDeploymentStatus(deploymentTrack, component, organization, envs[0]) }); + } + } + setPrevBuildList(builds); }, enabled: !!deploymentTrack, - refetchInterval: hasOngoingBuilds ? 5000 : false, + refetchInterval: hasOngoingBuilds || (props.isNewComponent && prevBuildList.length === 0) ? 5000 : false, }); const succeededBuilds = useMemo( diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/BuildConfigsSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/BuildConfigsSection.tsx index 8e8c5193b67..b36a3ca6c36 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/BuildConfigsSection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentDetailsView/sections/BuildConfigsSection.tsx @@ -30,6 +30,9 @@ import { type IRightPanelSectionItem, RightPanelSection, RightPanelSectionItem } export const BuildConfigsSection: FC<{ component: ComponentKind }> = ({ component }) => { const buildConfigList = getBuildConfigViewList(component); + if (buildConfigList.length === 0) { + return null; + } return ( diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/ComponentFormView.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/ComponentFormView.tsx index 268c7714aa4..762c816efe2 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/ComponentFormView.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/ComponentFormView.tsx @@ -18,7 +18,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ChoreoBuildPackNames, ChoreoComponentType, @@ -29,18 +29,20 @@ import { type NewComponentWebviewProps, type SubmitComponentCreateReq, WebAppSPATypes, + buildGitURL, getComponentTypeText, getIntegrationComponentTypeText, getRandomNumber, makeURLSafe, parseGitURL, } from "@wso2/wso2-platform-core"; -import React, { type FC, useState, useEffect } from "react"; +import React, { type FC, useState } from "react"; import { useForm } from "react-hook-form"; import type { z } from "zod"; import { HeaderSection } from "../../components/HeaderSection"; +import type { HeaderTag } from "../../components/HeaderSection/HeaderSection"; import { type StepItem, VerticalStepper } from "../../components/VerticalStepper"; -import { useComponentList } from "../../hooks/use-queries"; +import { queryKeys, useComponentList } from "../../hooks/use-queries"; import { useExtWebviewContext } from "../../providers/ext-vewview-ctx-provider"; import { ChoreoWebViewAPI } from "../../utilities/vscode-webview-rpc"; import { @@ -48,19 +50,23 @@ import { type componentEndpointsFormSchema, type componentGeneralDetailsSchema, type componentGitProxyFormSchema, + type componentRepoInitSchema, getComponentEndpointsFormSchema, getComponentFormSchemaBuildDetails, getComponentFormSchemaGenDetails, getComponentGitProxyFormSchema, + getRepoInitSchemaGenDetails, sampleEndpointItem, } from "./componentFormSchema"; import { ComponentFormBuildSection } from "./sections/ComponentFormBuildSection"; import { ComponentFormEndpointsSection } from "./sections/ComponentFormEndpointsSection"; import { ComponentFormGenDetailsSection } from "./sections/ComponentFormGenDetailsSection"; import { ComponentFormGitProxySection } from "./sections/ComponentFormGitProxySection"; +import { ComponentFormRepoInitSection } from "./sections/ComponentFormRepoInitSection"; import { ComponentFormSummarySection } from "./sections/ComponentFormSummarySection"; type ComponentFormGenDetailsType = z.infer; +type ComponentRepoInitType = z.infer; type ComponentFormBuildDetailsType = z.infer; type ComponentFormEndpointsType = z.infer; type ComponentFormGitProxyType = z.infer; @@ -76,6 +82,7 @@ export const ComponentFormView: FC = (props) => { existingComponents: existingComponentsCache, } = props; const type = initialValues?.type; + const queryClient = useQueryClient(); const [formSections] = useAutoAnimate(); const { extensionName } = useExtWebviewContext(); @@ -89,6 +96,20 @@ export const ComponentFormView: FC = (props) => { defaultValues: { name: initialValues?.name || "", subPath: "", gitRoot: "", repoUrl: "", branch: "", credential: "", gitProvider: "" }, }); + const repoInitForm = useForm({ + resolver: zodResolver(getRepoInitSchemaGenDetails(existingComponents), { async: true }, { mode: "async" }), + mode: "all", + defaultValues: { + org: "", + repo: "", + branch: "main", + subPath: "/", + name: initialValues?.name || "", + gitProvider: GitProvider.GITHUB, + serverUrl: "", + }, + }); + const name = genDetailsForm.watch("name"); const gitRoot = genDetailsForm.watch("gitRoot"); const subPath = genDetailsForm.watch("subPath"); @@ -156,16 +177,50 @@ export const ComponentFormView: FC = (props) => { }, }); - const { mutate: createComponent, isLoading: isCreatingComponent } = useMutation({ + const { mutateAsync: initializeRepoAsync, isLoading: initializingRepo } = useMutation({ mutationFn: async () => { + if (props.isNewCodeServerComp) { + const repoInitDetails = repoInitForm.getValues(); + const repoUrl = buildGitURL(repoInitDetails?.orgHandler, repoInitDetails.repo, repoInitDetails.gitProvider, false, repoInitDetails.serverUrl); + const branchesCache: string[] = queryClient.getQueryData(queryKeys.getGitBranches(repoUrl, organization, "", true)); + const newWorkspacePath = await ChoreoWebViewAPI.getInstance().cloneRepositoryIntoCompDir({ + cwd: props.directoryFsPath, + subpath: repoInitDetails.subPath, + org: props.organization, + componentName: makeURLSafe(repoInitDetails.name), + repo: { + orgHandler: repoInitDetails.orgHandler, + orgName: repoInitDetails.org, + branch: branchesCache?.length > 0 ? repoInitDetails.branch : undefined, + provider: repoInitDetails.gitProvider, + repo: repoInitDetails.repo, + serverUrl: repoInitDetails.serverUrl, + secretRef: repoInitDetails.credential || "", + isBareRepo: !(branchesCache?.length > 0), + }, + }); + + return newWorkspacePath; + } + }, + }); + + const { mutate: createComponent, isLoading: isCreatingComponent } = useMutation({ + mutationFn: async (newWorkspaceDir?: string) => { const genDetails = genDetailsForm.getValues(); + const repoInitDetails = repoInitForm.getValues(); const buildDetails = buildDetailsForm.getValues(); const gitProxyDetails = gitProxyForm.getValues(); - const componentName = makeURLSafe(genDetails.name); - + const name = props.isNewCodeServerComp ? repoInitDetails.name : genDetails.name; + const componentName = makeURLSafe(props.isNewCodeServerComp ? repoInitDetails.name : genDetails.name); + const branch = props.isNewCodeServerComp ? repoInitDetails.branch : genDetails.branch; const parsedRepo = parseGitURL(genDetails.repoUrl); - const provider = parsedRepo ? parsedRepo[2] : null; + const provider = props.isNewCodeServerComp ? repoInitDetails.gitProvider : parsedRepo[2]; + + const repoUrl = props.isNewCodeServerComp + ? buildGitURL(repoInitDetails.orgHandler, repoInitDetails.repo, repoInitDetails.gitProvider, false, repoInitDetails.serverUrl) + : genDetails.repoUrl; const createParams: Partial = { orgId: organization.id.toString(), @@ -173,14 +228,14 @@ export const ComponentFormView: FC = (props) => { projectId: project.id, projectHandle: project.handler, name: componentName, - displayName: genDetails.name, + displayName: name, type, componentSubType: initialValues?.subType || "", buildPackLang: buildDetails.buildPackLang, - componentDir: directoryFsPath, - repoUrl: genDetails.repoUrl, - gitProvider: genDetails.gitProvider, - branch: genDetails.branch, + componentDir: newWorkspaceDir || directoryFsPath, + repoUrl: repoUrl, + gitProvider: provider, + branch: branch, langVersion: buildDetails.langVersion, port: buildDetails.webAppPort, originCloud: extensionName === "Devant" ? "devant" : "choreo", @@ -248,8 +303,32 @@ export const ComponentFormView: FC = (props) => { onSuccess: () => setStepIndex(stepIndex + 1), }); - const steps: StepItem[] = [ - { + const steps: StepItem[] = []; + + if (props.isNewCodeServerComp) { + steps.push({ + label: "Repository Details", + content: ( + { + const newDirPath = await initializeRepoAsync(); + if (steps.length > 1) { + gitProxyForm.setValue("proxyContext", `/${makeURLSafe(genDetailsForm.getValues()?.name)}`); + setStepIndex(stepIndex + 1); + } else { + createComponent(newDirPath); + } + }} + /> + ), + }); + } else { + steps.push({ label: "General Details", content: ( = (props) => { form={genDetailsForm} componentType={type} onNextClick={() => { - gitProxyForm.setValue( - "proxyContext", - genDetailsForm.getValues()?.name ? `/${makeURLSafe(genDetailsForm.getValues()?.name)}` : `/path-${getRandomNumber()}`, - ); + gitProxyForm.setValue("proxyContext", `/${makeURLSafe(genDetailsForm.getValues()?.name)}`); setStepIndex(stepIndex + 1); }} /> ), - }, - ]; - - let showBuildDetails = false; - if (type !== ChoreoComponentType.ApiProxy) { - if (!initialValues?.buildPackLang) { - showBuildDetails = true; - } else { - if (initialValues?.buildPackLang === ChoreoBuildPackNames.Ballerina) { - showBuildDetails = type === ChoreoComponentType.Service; - } else if (initialValues?.buildPackLang === ChoreoBuildPackNames.MicroIntegrator) { - showBuildDetails = type === ChoreoComponentType.Service; - } else { + }); + + let showBuildDetails = false; + if (type !== ChoreoComponentType.ApiProxy) { + if (!initialValues?.buildPackLang) { + showBuildDetails = true; + } else if ( + ![ChoreoBuildPackNames.Ballerina, ChoreoBuildPackNames.MicroIntegrator].includes(initialValues?.buildPackLang as ChoreoBuildPackNames) + ) { showBuildDetails = true; } } - } - if (showBuildDetails) { - steps.push({ - label: "Build Details", - content: ( - setStepIndex(stepIndex + 1)} - onBackClick={() => setStepIndex(stepIndex - 1)} - form={buildDetailsForm} - selectedType={type} - subPath={subPath} - gitRoot={gitRoot} - baseUriPath={directoryUriPath} - /> - ), - }); - } + if (showBuildDetails) { + steps.push({ + label: "Build Details", + content: ( + setStepIndex(stepIndex + 1)} + onBackClick={() => setStepIndex(stepIndex - 1)} + form={buildDetailsForm} + selectedType={type} + subPath={subPath} + gitRoot={gitRoot} + baseUriPath={directoryUriPath} + /> + ), + }); + } - if (type === ChoreoComponentType.Service) { - if ( - ![ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildPackLang as ChoreoBuildPackNames) || - ([ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildPackLang as ChoreoBuildPackNames) && !useDefaultEndpoints) - ) { + if (type === ChoreoComponentType.Service && extensionName !== "Devant") { + if ( + ![ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildPackLang as ChoreoBuildPackNames) || + ([ChoreoBuildPackNames.MicroIntegrator, ChoreoBuildPackNames.Ballerina].includes(buildPackLang as ChoreoBuildPackNames) && + !useDefaultEndpoints) + ) { + steps.push({ + label: "Endpoint Details", + content: ( + submitEndpoints(data.endpoints as Endpoint[])} + onBackClick={() => setStepIndex(stepIndex - 1)} + isSaving={isSubmittingEndpoints} + form={endpointDetailsForm} + /> + ), + }); + } + } + if (type === ChoreoComponentType.ApiProxy) { steps.push({ - label: "Endpoint Details", + label: "Proxy Details", content: ( - submitEndpoints(data.endpoints as Endpoint[])} + key="git-proxy-step" + onNextClick={(data) => submitProxyConfig(data)} onBackClick={() => setStepIndex(stepIndex - 1)} - isSaving={isSubmittingEndpoints} - form={endpointDetailsForm} + isSaving={isSubmittingProxyConfig} + form={gitProxyForm} /> ), }); } - } - if (type === ChoreoComponentType.ApiProxy) { + steps.push({ - label: "Proxy Details", + label: "Summary", content: ( - submitProxyConfig(data)} + key="summary-step" + genDetailsForm={genDetailsForm} + buildDetailsForm={buildDetailsForm} + endpointDetailsForm={endpointDetailsForm} + gitProxyForm={gitProxyForm} + onNextClick={() => createComponent(undefined)} onBackClick={() => setStepIndex(stepIndex - 1)} - isSaving={isSubmittingProxyConfig} - form={gitProxyForm} + isCreating={isCreatingComponent} /> ), }); } - steps.push({ - label: "Summary", - content: ( - createComponent()} - onBackClick={() => setStepIndex(stepIndex - 1)} - isCreating={isCreatingComponent} - /> - ), - }); - const componentTypeText = extensionName === "Devant" ? getIntegrationComponentTypeText(type, initialValues?.subType) : getComponentTypeText(type); + const headerTags: HeaderTag[] = []; + + if (!props.isNewCodeServerComp) { + headerTags.push({ label: "Source Directory", value: subPath && subPath !== "." ? subPath : directoryName }); + } + headerTags.push({ label: "Project", value: project.name }, { label: "Organization", value: organization.name }); + return (
+
diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/componentFormSchema.ts b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/componentFormSchema.ts index c1a75b6048d..2ad8863b618 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/componentFormSchema.ts +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/componentFormSchema.ts @@ -26,6 +26,7 @@ import { GitProvider, GoogleProviderBuildPackNames, type OpenApiSpec, + Organization, WebAppSPATypes, capitalizeFirstLetter, makeURLSafe, @@ -35,14 +36,33 @@ import * as yaml from "js-yaml"; import { z } from "zod"; import { ChoreoWebViewAPI } from "../../utilities/vscode-webview-rpc"; +export const componentRepoInitSchema = z.object({ + org: z.string().min(1, "Required"), + orgHandler: z.string(), + repo: z.string().min(1, "Required"), + branch: z.string(), + subPath: z.string().regex(/^(\/)?([a-zA-Z0-9_-]+(\/)?)*$/, "Invalid path"), + name: z + .string() + .min(1, "Required") + .min(3, "Needs to be at least characters") + .max(60, "Max length exceeded") + .regex(/^[A-Za-z]/, "Needs to start with alphabetic letter") + .regex(/^[A-Za-z\s\d\-_]+$/, "Cannot have special characters"), + gitProvider: z.string().min(1, "Required"), + credential: z.string(), + serverUrl: z.string(), +}); + export const componentGeneralDetailsSchema = z.object({ name: z .string() .min(1, "Required") + .min(3, "Needs to be at least characters") .max(60, "Max length exceeded") .regex(/^[A-Za-z]/, "Needs to start with alphabetic letter") .regex(/^[A-Za-z\s\d\-_]+$/, "Cannot have special characters"), - subPath: z.string(), + subPath: z.string(), // todo: add regex gitRoot: z.string(), repoUrl: z.string().min(1, "Required"), gitProvider: z.string().min(1, "Required"), @@ -201,6 +221,16 @@ export const getComponentFormSchemaGenDetails = (existingComponents: ComponentKi } }); +export const getRepoInitSchemaGenDetails = (existingComponents: ComponentKind[]) => + componentRepoInitSchema.partial().superRefine(async (data, ctx) => { + if (existingComponents.some((item) => item.metadata.name === makeURLSafe(data.name))) { + ctx.addIssue({ path: ["name"], code: z.ZodIssueCode.custom, message: "Name already exists" }); + } + if (data.gitProvider !== GitProvider.GITHUB && !data.credential) { + ctx.addIssue({ path: ["credential"], code: z.ZodIssueCode.custom, message: "Required" }); + } + }); + export const getComponentFormSchemaBuildDetails = (type: string, directoryFsPath: string, gitRoot: string) => componentBuildDetailsSchema.partial().superRefine(async (data, ctx) => { if ( diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx index a8ccdb4829a..fb5a6b05aea 100644 --- a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormGenDetailsSection.tsx @@ -200,8 +200,7 @@ export const ComponentFormGenDetailsSection: FC = ({ onNextClick, organiz if (isRepoAuthorizedResp?.retrievedRepos) { invalidRepoMsg = ( - {extensionName} lacks access to the selected repository.{" "} - (Only public repos are allowed within the free tier.) + {extensionName} lacks access to the selected repository. ); invalidRepoAction = "Grant Access"; @@ -216,10 +215,7 @@ export const ComponentFormGenDetailsSection: FC = ({ onNextClick, organiz onInvalidRepoActionClick = () => ChoreoWebViewAPI.getInstance().openExternalChoreo(`organizations/${organization.handle}/settings/credentials`); if (isRepoAuthorizedResp?.retrievedRepos) { invalidRepoMsg = ( - - Selected Credential does not have sufficient permissions.{" "} - (Only public repos are allowed within the free tier.) - + Selected Credential does not have sufficient permissions to access the repository. ); invalidRepoAction = "Manage Credentials"; } else { diff --git a/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx new file mode 100644 index 00000000000..808fa12817c --- /dev/null +++ b/workspaces/wso2-platform/wso2-platform-webviews/src/views/ComponentFormView/sections/ComponentFormRepoInitSection.tsx @@ -0,0 +1,368 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { RequiredFormInput } from "@wso2/ui-toolkit"; +import { GitProvider, type NewComponentWebviewProps, buildGitURL } from "@wso2/wso2-platform-core"; +import debounce from "lodash.debounce"; +import React, { type FC, useCallback, useEffect, useState } from "react"; +import type { SubmitHandler, UseFormReturn } from "react-hook-form"; +import type { z } from "zod"; +import { Banner } from "../../../components/Banner"; +import { Button } from "../../../components/Button"; +import { Dropdown } from "../../../components/FormElements/Dropdown"; +import { TextField } from "../../../components/FormElements/TextField"; +import { useGetAuthorizedGitOrgs, useGetGitBranches } from "../../../hooks/use-queries"; +import { useExtWebviewContext } from "../../../providers/ext-vewview-ctx-provider"; +import { ChoreoWebViewAPI } from "../../../utilities/vscode-webview-rpc"; +import type { componentRepoInitSchema } from "../componentFormSchema"; + +type ComponentRepoInitSchemaType = z.infer; + +interface Props extends NewComponentWebviewProps { + onNextClick: () => void; + initializingRepo?: boolean; + initialFormValues?: ComponentRepoInitSchemaType; + form: UseFormReturn; + componentType: string; +} + +const connectMoreRepoText = "Connect More Repositories"; +const createNewRpoText = "Create New Repository"; +const createNewCredText = "Create New Credential"; + +export const ComponentFormRepoInitSection: FC = ({ onNextClick, organization, form, initializingRepo }) => { + const [compDetailsSections] = useAutoAnimate(); + const { extensionName } = useExtWebviewContext(); + const [creatingRepo, setCreatingRepo] = useState(false); + + const orgName = form.watch("org"); + const repo = form.watch("repo"); + const subPath = form.watch("subPath"); + const serverUrl = form.watch("serverUrl"); + const provider = form.watch("gitProvider"); + const credential = form.watch("credential"); + const repoError = form.formState?.errors?.repo; + const repoName = [connectMoreRepoText, createNewRpoText].includes(repo) ? "" : repo; + + const { + data: gitOrgs, + isLoading: loadingGitOrgs, + error: errorFetchingGitOrg, + } = useGetAuthorizedGitOrgs(organization.id?.toString(), provider, credential, { + refetchOnWindowFocus: true, + enabled: provider === GitProvider.GITHUB || !!credential, + }); + const matchingOrgItem = gitOrgs?.gitOrgs?.find((item) => item.orgName === orgName); + + const { data: gitCredentials = [], isLoading: isLoadingGitCred } = useQuery({ + queryKey: ["git-creds", { provider }], + queryFn: () => + ChoreoWebViewAPI.getInstance().getChoreoRpcClient().getCredentials({ orgId: organization?.id?.toString(), orgUuid: organization.uuid }), + select: (gitData) => gitData?.filter((item) => item.type === provider), + refetchOnWindowFocus: true, + enabled: provider !== GitProvider.GITHUB, + }); + + const { isLoading: isLoadingGitlabCreds } = useQuery({ + queryKey: ["gitlab-creds", { provider, credential }], + queryFn: () => + ChoreoWebViewAPI.getInstance() + .getChoreoRpcClient() + .getCredentialDetails({ orgId: organization?.id?.toString(), orgUuid: organization.uuid, credentialId: credential }), + enabled: provider === GitProvider.GITLAB_SERVER && !!credential, + onSuccess: (data) => form.setValue("serverUrl", data?.serverUrl), + }); + + useEffect(() => { + if (gitCredentials.length > 0 && (form.getValues("credential") || !gitCredentials.some((item) => item.id === form.getValues("credential")))) { + form.setValue("credential", gitCredentials[0]?.id); + } else if (gitCredentials.length === 0 && form.getValues("credential") !== "") { + form.setValue("credential", ""); + } + }, [gitCredentials]); + + const repoUrl = matchingOrgItem && repoName && buildGitURL(matchingOrgItem?.orgHandler, repoName, provider, false, serverUrl); + useEffect(() => { + if (gitOrgs?.gitOrgs.length > 0 && (form.getValues("org") === "" || !gitOrgs?.gitOrgs.some((item) => item.orgName === form.getValues("org")))) { + form.setValue("org", gitOrgs?.gitOrgs[0]?.orgName); + } else if (gitOrgs?.gitOrgs.length === 0 && form.getValues("org") !== "") { + form.setValue("org", ""); + } + }, [gitOrgs]); + + useEffect(() => { + if (matchingOrgItem?.repositories.length > 0 && !matchingOrgItem?.repositories?.some((item) => item.name === form.getValues("repo"))) { + setTimeout(() => form.setValue("repo", ""), 1000); + } + if (matchingOrgItem) { + form.setValue("orgHandler", matchingOrgItem.orgHandler); + } + }, [matchingOrgItem]); + + const { data: branches = [], isLoading: isLoadingBranches } = useGetGitBranches( + repoUrl, + organization, + provider === GitProvider.GITHUB ? "" : credential, + !errorFetchingGitOrg, + { + enabled: !!repoName && !!provider && !!repoUrl && (provider === GitProvider.GITHUB ? !errorFetchingGitOrg : !!credential), + refetchOnWindowFocus: true, + }, + ); + + useEffect(() => { + if (branches?.length > 0 && !branches.includes(form.getValues("branch"))) { + if (branches.includes("main")) { + form.setValue("branch", "main", { shouldValidate: true }); + } + if (branches.includes("master")) { + form.setValue("branch", "master", { shouldValidate: true }); + } else { + form.setValue("branch", branches[0], { shouldValidate: true }); + } + } + }, [branches]); + + useEffect(() => { + // TODO: avoid using useEffect and try to override the onChange handler + if (repo === createNewRpoText) { + setTimeout(() => form.setValue("repo", ""), 1000); + let newRepoLink = "https://github.com/new"; + if (provider === GitProvider.BITBUCKET) { + newRepoLink = `https://bitbucket.org/${orgName}/workspace/create/repository`; + } else if (provider === GitProvider.GITLAB_SERVER) { + newRepoLink = `${serverUrl}/projects/new`; + } + ChoreoWebViewAPI.getInstance().openExternal(newRepoLink); + setCreatingRepo(true); + } else if (repo === connectMoreRepoText) { + setTimeout(() => form.setValue("repo", ""), 1000); + ChoreoWebViewAPI.getInstance().triggerGithubInstallFlow(organization.id?.toString()); + } + }, [repo]); + + useEffect(() => { + // TODO: avoid using useEffect and try to override the onChange handler + if (credential === createNewCredText) { + setTimeout(() => form.setValue("credential", ""), 1000); + ChoreoWebViewAPI.getInstance().openExternalChoreo(`organizations/${organization.handle}/settings/credentials`); + } + }, [credential]); + + useEffect(() => { + setCreatingRepo(false); + }, [provider]); + + const debouncedUpdateName = useCallback( + debounce((subPath: string, repo: string) => { + if (subPath) { + const paths = subPath.split("/"); + const lastPath = paths.findLast((item) => !!item); + if (lastPath) { + form.setValue("name", lastPath); + return; + } + } + if (repo) { + form.setValue("name", repo); + return; + } + }, 1000), + [], + ); + + useEffect(() => { + debouncedUpdateName(subPath, repo); + }, [repo, subPath]); + + const { mutateAsync: getRepoMetadata, isLoading: isValidatingPath } = useMutation({ + mutationFn: (data: ComponentRepoInitSchemaType) => { + const subPath = data.subPath.startsWith("/") ? data.subPath.slice(1) : data.subPath; + return ChoreoWebViewAPI.getInstance() + .getChoreoRpcClient() + .getGitRepoMetadata({ + branch: data.branch, + gitOrgName: data.org, + gitRepoName: data.repo, + relativePath: subPath, + orgId: organization?.id?.toString(), + secretRef: data.credential || "", + }); + }, + }); + + const onSubmitForm: SubmitHandler = async (data) => { + try { + const resp = await getRepoMetadata(data); + if (resp?.metadata && !resp?.metadata?.isSubPathEmpty) { + form.setError("subPath", { message: "Path isn't empty in the remote repo" }); + } else { + onNextClick(); + } + } catch { + // the API will throw an error, if branch does not exist + onNextClick(); + } + }; + + const repoDropdownItems = [{ value: createNewRpoText }]; + if (provider === GitProvider.GITHUB) { + repoDropdownItems.push({ value: connectMoreRepoText }); + } + if (matchingOrgItem?.repositories?.length > 0) { + repoDropdownItems.push( + { type: "separator", value: "" } as { value: string }, + ...matchingOrgItem?.repositories?.map((item) => ({ value: item.name })), + ); + } + + const credentialDropdownItems = [{ value: createNewCredText }]; + if (gitCredentials?.length > 0) { + credentialDropdownItems.push( + { type: "separator", value: "" } as { value: string }, + ...gitCredentials?.map((item) => ({ value: item.id, label: item.name })), + ); + } + + return ( + <> +
+ + + {provider === GitProvider.GITHUB && errorFetchingGitOrg && ( + ChoreoWebViewAPI.getInstance().triggerGithubAuthFlow(organization.id?.toString()) }} + /> + )} + {provider !== GitProvider.GITHUB && ( + + )} + {(provider === GitProvider.GITHUB || credential) && ( + <> + ({ value: item.orgName }))} + loading={loadingGitOrgs || (provider === GitProvider.GITLAB_SERVER ? isLoadingGitlabCreds : false)} + /> + {creatingRepo ? ( +
+
+ + + + + {repoError?.message && ( + + )} +
+
+ + +
+
+ ) : ( + + )} + {repoName && branches?.length > 0 && ( + + )} + + )} + + + {repo && ( +
+ +
+ )} +
+ +
+ +
+ + ); +};