diff --git a/package-lock.json b/package-lock.json index bb73bac3..943bce85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-couchbase", - "version": "1.2.2", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-couchbase", - "version": "1.2.2", + "version": "1.2.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@chatscope/chat-ui-kit-styles": "^1.4.0", diff --git a/package.json b/package.json index 4d2ab58f..ff931584 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-couchbase", "displayName": "Couchbase", "description": "", - "version": "1.2.2", + "version": "1.2.3", "engines": { "vscode": "^1.63.1" }, diff --git a/src/commands/iq/couchbaseIqWebviewProvider.ts b/src/commands/iq/couchbaseIqWebviewProvider.ts index e1997bf0..b0eb6e84 100644 --- a/src/commands/iq/couchbaseIqWebviewProvider.ts +++ b/src/commands/iq/couchbaseIqWebviewProvider.ts @@ -14,20 +14,24 @@ * limitations under the License. */ -import * as vscode from 'vscode'; -import * as path from 'path'; -import { getIQWebviewContent } from '../../webViews/iq/couchbaseIq.webview'; -import { iqChatHandler } from './chat/iqChatHandler'; -import { iqLoginHandler, iqSavedLoginDataGetter, iqSavedLoginHandler, verifyOrganization } from './iqLoginhandler'; -import { Memory, getUUID } from '../../util/util'; -import { Constants } from '../../util/constants'; -import { CacheService } from '../../util/cacheService/cacheService'; -import { iqFeedbackHandler } from './iqFeedbackHandler'; -import { IIqStoredMessages } from './chat/types'; -import { removeJWT } from './iqLogoutHandler'; -import { applyQuery } from '../queryHistory/applyQuery'; - - +import * as vscode from "vscode"; +import * as path from "path"; +import { getIQWebviewContent } from "../../webViews/iq/couchbaseIq.webview"; +import { iqChatHandler } from "./chat/iqChatHandler"; +import { + iqLoginHandler, + iqSavedLoginDataGetter, + iqSavedLoginHandler, + verifyOrganization, + handleIqSupplementalTerms, +} from "./iqLoginhandler"; +import { Memory, getUUID } from "../../util/util"; +import { Constants } from "../../util/constants"; +import { CacheService } from "../../util/cacheService/cacheService"; +import { iqFeedbackHandler } from "./iqFeedbackHandler"; +import { IIqStoredMessages } from "./chat/types"; +import { removeJWT } from "./iqLogoutHandler"; +import { applyQuery } from "../queryHistory/applyQuery"; export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { public _view?: vscode.WebviewView; @@ -46,16 +50,24 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { this._view.webview.options = { enableScripts: true, localResourceRoots: [ - vscode.Uri.file(path.join(this._context.extensionPath, "dist")) - ] + vscode.Uri.file(path.join(this._context.extensionPath, "dist")), + ], }; const reactAppPathOnDisk = vscode.Uri.file( - path.join(this._context.extensionPath, "dist", "iq", "reactBuild.js") + path.join( + this._context.extensionPath, + "dist", + "iq", + "reactBuild.js" + ) ); const reactAppUri = this._view.webview.asWebviewUri(reactAppPathOnDisk); - this._view.webview.html = getIQWebviewContent(reactAppUri, this._context); + this._view.webview.html = getIQWebviewContent( + reactAppUri, + this._context + ); // Save view id to memory so it can be accessed from outside Memory.state.update(Constants.IQ_WEBVIEW, this._view); @@ -64,12 +76,12 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { if (newTheme.kind === vscode.ColorThemeKind.Dark) { this._view?.webview.postMessage({ command: "vscode-couchbase.iq.changeColorTheme", - theme: "Dark" + theme: "Dark", }); } else { this._view?.webview.postMessage({ command: "vscode-couchbase.iq.changeColorTheme", - theme: "Light" + theme: "Light", }); } }); @@ -79,25 +91,27 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { case "vscode-couchbase.iq.login": { let organizations; try { - organizations = await iqLoginHandler(message.value); + organizations = await iqLoginHandler(message.value); if (!organizations || organizations.length === 0) { // TODO: send login error and do break this._view?.webview.postMessage({ command: "vscode-couchbase.iq.forcedLogout", - error: "There are no organizations available for the account" + error: "There are no organizations available for the account", }); break; } - } catch(error:any) { + } catch (error: any) { this._view?.webview.postMessage({ command: "vscode-couchbase.iq.forcedLogout", - error: error.message.toString() + error: error.message.toString(), }); break; } - - const config = vscode.workspace.getConfiguration('couchbase'); - const savedOrganization = config.get('iQ.savedOrganization'); // Get saved organization from vscode couchbase settings + const config = + vscode.workspace.getConfiguration("couchbase"); + const savedOrganization = config.get( + "iQ.savedOrganization" + ); // Get saved organization from vscode couchbase settings if (savedOrganization !== "") { let savedOrganizationDetail = undefined; for (let org of organizations) { @@ -111,26 +125,44 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { // Remove organization from saved settings. config.update("iQ.savedOrganization", ""); this._view?.webview.postMessage({ - command: "vscode-couchbase.iq.organizationDetails", + command: + "vscode-couchbase.iq.organizationDetails", organizations: organizations, isSavedOrganization: false, - savedOrganization: undefined + savedOrganization: undefined, }); } else { - const {isOrgVerified, errorMessage} = await verifyOrganization(savedOrganizationDetail.data.id); - if(isOrgVerified){ + const { + shouldAcceptIqTerms, + isOrgVerified, + errorMessage, + } = await verifyOrganization( + savedOrganizationDetail.data.id + ); + if (isOrgVerified) { this._view?.webview.postMessage({ - command: "vscode-couchbase.iq.organizationDetails", + command: + "vscode-couchbase.iq.organizationDetails", organizations: organizations, isSavedOrganization: true, - savedOrganization: savedOrganizationDetail + savedOrganization: savedOrganizationDetail, }); } else { - config.update("iQ.savedOrganization", ""); - this._view?.webview.postMessage({ - command: "vscode-couchbase.iq.forcedLogout", - error: errorMessage - }); + if (shouldAcceptIqTerms) { + this._view?.webview.postMessage({ + command: + "vscode-couchbase.iq.acceptSupplementalTerms", + error: errorMessage, + organization: savedOrganizationDetail, + }); + } else { + config.update("iQ.savedOrganization", ""); + this._view?.webview.postMessage({ + command: + "vscode-couchbase.iq.forcedLogout", + error: errorMessage, + }); + } } } } else { @@ -138,51 +170,60 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { command: "vscode-couchbase.iq.organizationDetails", organizations: organizations, isSavedOrganization: false, - savedOrganization: undefined + savedOrganization: undefined, }); } break; } case "vscode-couchbase.iq.sendMessageToIQ": { - const result = await iqChatHandler(this._context, message.value, this.cacheService, this.allMessages, webviewView); + const result = await iqChatHandler( + this._context, + message.value, + this.cacheService, + this.allMessages, + webviewView + ); if (result.error !== undefined) { let errorMsg = ""; try { - if(typeof(result.error) !== "string"){ + if (typeof result.error !== "string") { errorMsg = JSON.stringify(result.error); } else { errorMsg = result.error; } - } catch { - errorMsg = "Internal Error: Please try again later or check settings on couchbase cloud"; + } catch { + errorMsg = + "Internal Error: Please try again later or check settings on couchbase cloud"; } - if(result.status.length > 3){ // No 4xx or 5xx error + if (result.status.length > 3) { + // No 4xx or 5xx error console.log("chat completed"); this._view?.webview.postMessage({ command: "vscode-couchbase.iq.chatCompleted", - error: errorMsg + error: errorMsg, }); - } - else if (result.status === "401") { + } else if (result.status === "401") { console.log("Got forced logout"); - + this._view?.webview.postMessage({ command: "vscode-couchbase.iq.forcedLogout", - error: errorMsg + error: errorMsg, }); } else { // TODO: Handle If some other error is received this._view?.webview.postMessage({ command: "vscode-couchbase.iq.forcedLogout", - error: errorMsg + error: errorMsg, }); } } else { this._view?.webview.postMessage({ command: "vscode-couchbase.iq.getMessageFromIQ", content: result.content, - isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark + isDarkTheme: + vscode.window.activeColorTheme.kind === + vscode.ColorThemeKind.Dark, }); } break; @@ -191,32 +232,54 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { const savedLoginDetails = await iqSavedLoginDataGetter(); this._view?.webview.postMessage({ command: "vscode-couchbase.iq.savedLoginDetails", - savedLoginDetails: savedLoginDetails + savedLoginDetails: savedLoginDetails, + }); + break; + } + case "vscode-couchbase.iq.acceptIqSupplementalTerms": { + await handleIqSupplementalTerms(message.value.data); + // Bypass straight to IQ Chat with above org details + this._view?.webview.postMessage({ + command: "vscode-couchbase.iq.organizationDetails", + isSavedOrganization: true, + savedOrganization: message.value, + }); + break; + } + case "vscode-couchbase.iq.cancelIqSupplementalTerms": { + this._view?.webview.postMessage({ + command: "vscode-couchbase.iq.forcedLogout", + error: "You have not accepted the Capella iQ Supplemental terms, Please login again and accept terms to continue", }); break; } case "vscode-couchbase.iq.singleClickSignIn": { let organizations; try { - organizations = await iqSavedLoginHandler(message.value.username); + organizations = await iqSavedLoginHandler( + message.value.username + ); if (!organizations || organizations.length === 0) { // TODO: send login error and do break this._view?.webview.postMessage({ command: "vscode-couchbase.iq.forcedLogout", - error: "There are no organizations available for the account" + error: "There are no organizations available for the account", }); break; } - } catch (error:any){ + } catch (error: any) { this._view?.webview.postMessage({ command: "vscode-couchbase.iq.forcedLogout", - error: error.message.toString() + error: error.message.toString(), }); break; } - const config = vscode.workspace.getConfiguration('couchbase'); - const savedOrganization = config.get('iQ.savedOrganization'); // Get saved organization from vscode couchbase settings + const config = + vscode.workspace.getConfiguration("couchbase"); + const savedOrganization = config.get( + "iQ.savedOrganization" + ); // Get saved organization from vscode couchbase settings if (savedOrganization !== "") { let savedOrganizationDetail = undefined; for (let org of organizations) { @@ -230,26 +293,44 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { // Remove organization from saved settings. config.update("iQ.savedOrganization", ""); this._view?.webview.postMessage({ - command: "vscode-couchbase.iq.organizationDetails", + command: + "vscode-couchbase.iq.organizationDetails", organizations: organizations, isSavedOrganization: false, - savedOrganization: undefined + savedOrganization: undefined, }); } else { - const {isOrgVerified, errorMessage} = await verifyOrganization(savedOrganizationDetail.data.id); - if(isOrgVerified) { + const { + shouldAcceptIqTerms, + isOrgVerified, + errorMessage, + } = await verifyOrganization( + savedOrganizationDetail.data.id + ); + if (isOrgVerified) { this._view?.webview.postMessage({ - command: "vscode-couchbase.iq.organizationDetails", + command: + "vscode-couchbase.iq.organizationDetails", organizations: organizations, isSavedOrganization: true, - savedOrganization: savedOrganizationDetail + savedOrganization: savedOrganizationDetail, }); } else { - config.update("iQ.savedOrganization", ""); - this._view?.webview.postMessage({ - command: "vscode-couchbase.iq.forcedLogout", - error: errorMessage - }); + if (shouldAcceptIqTerms) { + this._view?.webview.postMessage({ + command: + "vscode-couchbase.iq.acceptSupplementalTerms", + error: errorMessage, + organization: savedOrganizationDetail, + }); + } else { + config.update("iQ.savedOrganization", ""); + this._view?.webview.postMessage({ + command: + "vscode-couchbase.iq.forcedLogout", + error: errorMessage, + }); + } } } } else { @@ -257,26 +338,48 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { command: "vscode-couchbase.iq.organizationDetails", organizations: organizations, isSavedOrganization: false, - savedOrganization: undefined + savedOrganization: undefined, }); } break; } case "vscode-couchbase.iq.verifyOrganizationAndSave": { - const {isOrgVerified, errorMessage} = await verifyOrganization(message.value.organizationDetails.data.id); - if(isOrgVerified && message.value.rememberOrgChecked) { - const config = vscode.workspace.getConfiguration('couchbase'); - config.update('iQ.savedOrganization', message.value.organizationDetails.data.id, vscode.ConfigurationTarget.Global); - } else if(!isOrgVerified) { - this._view?.webview.postMessage({ - command: "vscode-couchbase.iq.forcedLogout", - error: errorMessage - }); + const { shouldAcceptIqTerms, isOrgVerified, errorMessage } = + await verifyOrganization( + message.value.organizationDetails.data.id + ); + if (isOrgVerified && message.value.rememberOrgChecked) { + const config = + vscode.workspace.getConfiguration("couchbase"); + config.update( + "iQ.savedOrganization", + message.value.organizationDetails.data.id, + vscode.ConfigurationTarget.Global + ); + } else if (!isOrgVerified) { + if (shouldAcceptIqTerms) { + this._view?.webview.postMessage({ + command: + "vscode-couchbase.iq.acceptSupplementalTerms", + error: errorMessage, + organization: + message.value.organizationDetails, + }); + } else { + this._view?.webview.postMessage({ + command: "vscode-couchbase.iq.forcedLogout", + error: errorMessage, + }); + } } break; } case "vscode-couchbase.iq.sendFeedbackPerMessageEmote": { - await iqFeedbackHandler(this._context, message.value, this.allMessages); + await iqFeedbackHandler( + this._context, + message.value, + this.allMessages + ); break; } case "vscode-couchbase.iq.executeActionCommand": { @@ -311,10 +414,10 @@ export class CouchbaseIqWebviewProvider implements vscode.WebviewViewProvider { } case "vscode-couchbase.iq.applyQuery": { const query = message.value; - applyQuery({query: query, id: getUUID()}); + applyQuery({ query: query, id: getUUID() }); break; } } }); } -} \ No newline at end of file +} diff --git a/src/commands/iq/iqLoginhandler.ts b/src/commands/iq/iqLoginhandler.ts index a0284903..f46f46ef 100644 --- a/src/commands/iq/iqLoginhandler.ts +++ b/src/commands/iq/iqLoginhandler.ts @@ -1,7 +1,7 @@ import { Constants } from "../../util/constants"; import { Global, Memory } from "../../util/util"; import { iqRestApiService } from "./iqRestApiService"; -import * as keytar from 'keytar'; +import * as keytar from "keytar"; interface IFormData { username: string; @@ -9,98 +9,181 @@ interface IFormData { rememberMe: boolean; } +type User = { + id: string; + roles: string[]; +}; + export interface ISavedLoginDataGetter { doesLoginDetailsExists: boolean; - username: string + username: string; } const getSessionsJwt = async (formData: IFormData): Promise => { try { - return await iqRestApiService.capellaLogin(formData.username, formData.password); + return await iqRestApiService.capellaLogin( + formData.username, + formData.password + ); } catch (error: any) { throw new Error(error.message.toString()); } }; - export const iqLoginHandler = async (formData: IFormData) => { try { - // Check for remember me + // Check for remember me if (formData.rememberMe === true) { Global.state.update(Constants.IQ_USER_ID, formData.username); - keytar.setPassword(Constants.IQ_PASSWORD, formData.username, formData.password); + keytar.setPassword( + Constants.IQ_PASSWORD, + formData.username, + formData.password + ); } // Return organization select page data const jwtToken = await getSessionsJwt(formData); Memory.state.update("vscode-couchbase.iq.jwtToken", jwtToken); - const organizations = await iqRestApiService.loadOrganizations(jwtToken); + const organizations = await iqRestApiService.loadOrganizations( + jwtToken + ); return organizations; - } catch(error: any){ + } catch (error: any) { throw new Error(error.message.toString()); } }; -export const iqSavedLoginDataGetter = async (): Promise => { - const username = Global.state.get(Constants.IQ_USER_ID); - if (username && username !== "") { - // Username exists, sending it back to webview - return { - doesLoginDetailsExists: true, - username: username - }; - } else { - return { - doesLoginDetailsExists: false, - username: "" - }; - } -}; +export const iqSavedLoginDataGetter = + async (): Promise => { + const username = Global.state.get(Constants.IQ_USER_ID); + if (username && username !== "") { + // Username exists, sending it back to webview + return { + doesLoginDetailsExists: true, + username: username, + }; + } else { + return { + doesLoginDetailsExists: false, + username: "", + }; + } + }; export const iqSavedLoginHandler = async (username: string) => { try { - const password = await keytar.getPassword(Constants.IQ_PASSWORD, username); + const password = await keytar.getPassword( + Constants.IQ_PASSWORD, + username + ); if (password) { // Return organization select page data const jwtToken = await getSessionsJwt({ username: username, password: password, - rememberMe: true + rememberMe: true, }); Memory.state.update("vscode-couchbase.iq.jwtToken", jwtToken); - const organizations = await iqRestApiService.loadOrganizations(jwtToken); + const organizations = await iqRestApiService.loadOrganizations( + jwtToken + ); return organizations; } else { return undefined; } - } catch(error: any) { + } catch (error: any) { throw new Error(error.message.toString()); } }; +export const handleIqSupplementalTerms = async ( + orgDetails: any +): Promise => { + const jwtToken = Memory.state.get("vscode-couchbase.iq.jwtToken"); + if (jwtToken === undefined) { + return { + isOrgVerified: false, + errorMessage: "", + }; + } + if (!orgDetails.iq.other) { + orgDetails.iq.other = {}; + } + // Set the isTermsAcceptedForOrg to True + orgDetails.iq.other.isTermsAcceptedForOrg = true; + const response = await iqRestApiService.acceptIqSupplementalTerms( + jwtToken, + orgDetails.id, + orgDetails + ); +}; + +export const checkIfUserIsOrgOwner = async ( + userId: string, + user: User +): Promise => { + if (user.id === userId) { + return user.roles.includes("organizationOwner"); + } + return false; +}; + export const verifyOrganization = async (orgId: string): Promise => { const jwtToken = Memory.state.get("vscode-couchbase.iq.jwtToken"); if (jwtToken === undefined) { return { + shouldAcceptIqTerms: false, isOrgVerified: false, - errorMessage: "" + errorMessage: "", }; } - const orgDetails = await iqRestApiService.getOrganizationDetails(jwtToken, orgId); - if (!orgDetails.iq || orgDetails.iq.enabled === false) { + const orgDetails = await iqRestApiService.getOrganizationDetails( + jwtToken, + orgId + ); + const userId = Memory.state.get("vscode-couchbase.iq.userId"); + if (userId === undefined) { return { + shouldAcceptIqTerms: false, isOrgVerified: false, - errorMessage: `Capella iQ is not enabled for this organization, Please enable it on cloud.couchbase.com` + errorMessage: "", }; } - if (orgDetails.iq.other.isTermsAcceptedForOrg === false) { + if (!orgDetails.iq || orgDetails.iq.enabled === false) { return { + shouldAcceptIqTerms: false, isOrgVerified: false, - errorMessage: `Terms and conditions to use Capella iQ are not accepted for this organization, Please accept it on cloud.couchbase.com` + errorMessage: `Capella iQ is not enabled for this organization, Please enable it on cloud.couchbase.com`, }; } + if ( + !orgDetails.iq.other || + orgDetails.iq.other.isTermsAcceptedForOrg === false + ) { + const userList = await iqRestApiService.fetchUserData( + jwtToken, + orgId, + userId + ); + // Allow to Accept Terms ONLY if user is org owner + if (await checkIfUserIsOrgOwner(userId, userList)) { + return { + shouldAcceptIqTerms: true, + isOrgVerified: false, + errorMessage: `Capella iQ uses a third-party large language model (LLM). Please do not enter sensitive data into iQ and review its output before using. `, + }; + } else { + return { + shouldAcceptIqTerms: false, + isOrgVerified: false, + errorMessage: `Capella iQ Supplemental Terms have not yet been accepted. To continue, kindly request the organization owner to review and agree to the terms and conditions. `, + }; + } + } return { + shouldAcceptIqTerms: false, isOrgVerified: true, - errorMessage: "" + errorMessage: "", }; -}; \ No newline at end of file +}; diff --git a/src/commands/iq/iqRestApiService.ts b/src/commands/iq/iqRestApiService.ts index e49ae445..873daee8 100644 --- a/src/commands/iq/iqRestApiService.ts +++ b/src/commands/iq/iqRestApiService.ts @@ -1,107 +1,178 @@ import axios from "axios"; -import * as vscode from 'vscode'; +import * as vscode from "vscode"; import { logger } from "../../logger/logger"; import { feedbackLambdaMessageType, iqChatResult } from "./chat/types"; - +import { Global, Memory } from "../../util/util"; export class iqRestApiService { // Capella Prod domain - private static readonly CAPELLA_URL_DOMAIN = "https://api.cloud.couchbase.com"; + private static readonly CAPELLA_URL_DOMAIN = + "https://api.cloud.couchbase.com"; private static readonly SESSIONS_API_URL = `${this.CAPELLA_URL_DOMAIN}/sessions`; private static readonly FETCH_ORGANIZATIONS_URL = `${this.CAPELLA_URL_DOMAIN}/v2/organizations`; public static capellaLogin = async (username: string, password: string) => { try { - const content = await axios.post(this.SESSIONS_API_URL, {}, { - auth: { - username: username, - password: password + const content = await axios.post( + this.SESSIONS_API_URL, + {}, + { + auth: { + username: username, + password: password, + }, } - }); + ); + Memory.state.update( + "vscode-couchbase.iq.userId", + content.data.user.id + ); return content.data.jwt; } catch (error) { logger.error("failed to login with capella creds: " + error); - throw new Error("Invalid Credentials: please retry with correct capella credentials "); + throw new Error( + "Invalid Credentials: please retry with correct capella credentials " + ); } }; public static capellaLogout = async (jwt: string) => { await axios.delete(this.SESSIONS_API_URL, { headers: { - Authorization: `Bearer ${jwt}` - } + Authorization: `Bearer ${jwt}`, + }, }); }; public static loadOrganizations = async (jwt: string) => { const content = await axios.get(this.FETCH_ORGANIZATIONS_URL, { headers: { - Authorization: `Bearer ${jwt}` - } + Authorization: `Bearer ${jwt}`, + }, }); return content.data.data; }; - public static getOrganizationDetails = async (jwt: string, orgId: string) => { - const content = await axios.get(this.FETCH_ORGANIZATIONS_URL + "/" + orgId, { - headers: { - Authorization: `Bearer ${jwt}` + public static getOrganizationDetails = async ( + jwt: string, + orgId: string + ) => { + const content = await axios.get( + this.FETCH_ORGANIZATIONS_URL + "/" + orgId, + { + headers: { + Authorization: `Bearer ${jwt}`, + }, } - }); + ); return content.data.data[0]; }; - public static sendIqMessage = async (jwt: string, orgId: string, messageBody: any): Promise => { + public static acceptIqSupplementalTerms = async ( + jwt: string, + orgId: string, + messageBody: any + ) => { + try { + const content = await axios.put( + `${this.CAPELLA_URL_DOMAIN}/v2/organizations/` + orgId, + messageBody, + { + headers: { + Authorization: `Bearer ${jwt}`, + }, + } + ); + return content.data; + } catch (error) { + logger.error( + "failed to Accept Capella iQ Terms, Please retry or try from Capella UI " + + error + ); + throw new Error( + "Failed to Accept Capella iQ Terms, Retry or try again from Capella UI " + ); + } + }; + + public static fetchUserData = async (jwt: string, orgId: string, userId:string) => { + try { + const content = await axios.get( + this.FETCH_ORGANIZATIONS_URL + "/" + orgId + "/users" + "/" + userId, + { + headers: { + Authorization: `Bearer ${jwt}`, + }, + } + ); + return content.data.data; + } catch (error) { + logger.error("failed to Fetch Users Data in the organization " + error); + throw new Error("Failed to fetch users data in the organization "); + } + }; + + public static sendIqMessage = async ( + jwt: string, + orgId: string, + messageBody: any + ): Promise => { let result: iqChatResult = { content: "", error: undefined, - status: "" + status: "", }; try { - const content = await axios.post(`${this.CAPELLA_URL_DOMAIN}/v2/organizations/` + orgId + "/integrations/iq/openai/chat/completions", + const content = await axios.post( + `${this.CAPELLA_URL_DOMAIN}/v2/organizations/` + + orgId + + "/integrations/iq/openai/chat/completions", messageBody, { headers: { Authorization: "Bearer " + jwt, "Content-Type": "application/json", - Connection: "keep-alive" + Connection: "keep-alive", }, - }, + } ); - if (content.data.choices === undefined || content.data.choices.length === 0) { + if ( + content.data.choices === undefined || + content.data.choices.length === 0 + ) { result.status = "NoLogout"; result.error = content.data.error; return result; } result.content = content.data.choices[0].message.content; result.status = content.status.toString(); - } - catch (error: any) { + } catch (error: any) { try { if (error.response && error.response.status === 401) { - result.error = "The current session has expired. Please login again."; + result.error = + "The current session has expired. Please login again."; result.status = "401"; - } - else if (error.response.data !== undefined && error.response.data.errorType !== undefined) { + } else if ( + error.response.data !== undefined && + error.response.data.errorType !== undefined + ) { result.error = error.response.data.message; result.status = error.response.data.errorType; - } - else if (error.status) { + } else if (error.status) { result.error = error.statusText; result.status = error.status; - } - else if (error.response) { + } else if (error.response) { result.error = error.response; result.status = error.response.status.toString(); - } - else { - - logger.error("Error while receiving message from iQ: " + error); + } else { + logger.error( + "Error while receiving message from iQ: " + error + ); result.status = "400"; - result.error = "Error while receiving message from iQ: " + error; - + result.error = + "Error while receiving message from iQ: " + error; } } catch (e) { result.status = "400"; @@ -111,14 +182,17 @@ export class iqRestApiService { return result; }; - public static sendMessageToLambda = async (context: vscode.ExtensionContext, message: feedbackLambdaMessageType) => { - const URL = context.globalState.get('feedbackLambdaUrl'); - await axios.post(URL || "", message, - { - headers: { - "X-Secret": context.globalState.get('feedbackLambdaSecret') - } - } - ); + public static sendMessageToLambda = async ( + context: vscode.ExtensionContext, + message: feedbackLambdaMessageType + ) => { + const URL = context.globalState.get("feedbackLambdaUrl"); + await axios.post(URL || "", message, { + headers: { + "X-Secret": context.globalState.get( + "feedbackLambdaSecret" + ), + }, + }); }; -} \ No newline at end of file +} diff --git a/src/reactViews/iq/bootstrap.tsx b/src/reactViews/iq/bootstrap.tsx index a7385a40..406d5992 100644 --- a/src/reactViews/iq/bootstrap.tsx +++ b/src/reactViews/iq/bootstrap.tsx @@ -7,6 +7,7 @@ import SelectOrganizationPage from "pages/organizationSelect/SelectOrganization" import LoginSingleClick from "pages/login/LoginSingleClick"; import IqChat from "pages/chatscreen/IqChat"; import { Modal } from "components/modals/Modal"; +import { ModaliQTerm } from "components/modals/ModaliQTerms"; const container: HTMLElement = document.getElementById("vscodeRootIQ"); const newRoot = createRoot(container); @@ -14,7 +15,9 @@ const newRoot = createRoot(container); export const App: React.FC = () => { const [isLoading, setIsLoading] = React.useState(true); const [showPage, setShowPage] = React.useState(<>); - const [showErrorModal, setShowErrorModal] = React.useState(false); + const [showTermsModal, setShowTermsModal] = React.useState(false); + const [showModal, setShowModal] = React.useState(false); + const [organizationDetails, setOrganizationDetails] = React.useState(null); const [errorMessage, setErrorMessage] = React.useState(<>); const messageHandler = (event) => { @@ -42,7 +45,14 @@ export const App: React.FC = () => { value: "", }); setIsLoading(false); - setShowErrorModal(true); + setShowModal(true); + setErrorMessage(message.error || ""); + break; + } + case "vscode-couchbase.iq.acceptSupplementalTerms": { + setIsLoading(false); + setShowTermsModal(true); + setOrganizationDetails(message.organization); setErrorMessage(message.error || ""); break; } @@ -76,6 +86,22 @@ export const App: React.FC = () => { } }; + const handleModalClose = () => { + setShowTermsModal(false); + tsvscode.postMessage({ + command: "vscode-couchbase.iq.cancelIqSupplementalTerms", + value: "", + }); + }; + + const handleAcceptTerms = () => { + setShowTermsModal(false); + tsvscode.postMessage({ + command: "vscode-couchbase.iq.acceptIqSupplementalTerms", + value: organizationDetails, + }); + }; + React.useEffect(() => { window.addEventListener("message", messageHandler); @@ -94,19 +120,29 @@ export const App: React.FC = () => { return (
{isLoading && } - } - onClose={() => { - setShowErrorModal(false); - setShowPage(); - setIsLoading(false); - tsvscode.postMessage({ - command: "vscode-couchbase.iq.getSavedLogin", - value: "", - }); - }} - /> + {showTermsModal && ( + } + onClose={handleModalClose} + onAccept={handleAcceptTerms} + /> + )} + {showModal && ( + } + onClose={() => { + setShowModal(false); + setShowPage(); + setIsLoading(false); + tsvscode.postMessage({ + command: "vscode-couchbase.iq.getSavedLogin", + value: "", + }); + }} + /> + )} {showPage}
); diff --git a/src/reactViews/iq/components/modals/Modal.scss b/src/reactViews/iq/components/modals/Modal.scss index dabf790c..8f218877 100644 --- a/src/reactViews/iq/components/modals/Modal.scss +++ b/src/reactViews/iq/components/modals/Modal.scss @@ -21,11 +21,31 @@ border-radius: 5px; } + .modal-header { + font-size: 20px; + font-weight: bold; + text-align: center; + margin-bottom: 15px; + } + .button-container { display: flex; justify-content: flex-end; + margin-top: 16px; gap: 10px; } + + .modal-close-btn { + position: absolute; + top: 0; + right: 0; + } + + .terms-checkbox { + input[type="checkbox"] { + margin-left: 0px; + } + } button { padding: 5px 10px; diff --git a/src/reactViews/iq/components/modals/ModaliQTerms.tsx b/src/reactViews/iq/components/modals/ModaliQTerms.tsx new file mode 100644 index 00000000..f3f3fbc0 --- /dev/null +++ b/src/reactViews/iq/components/modals/ModaliQTerms.tsx @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import "./Modal.scss"; + +export function ModaliQTerm({ isOpen, onClose, content, onAccept }) { + const [isChecked, setIsChecked] = useState(false); + + const handleCheckboxChange = (e) => { + setIsChecked(e.target.checked); + }; + + const handleClose = () => { + if (isChecked) { + onAccept(); + } else { + alert("You must accept the terms and conditions to proceed."); + } + }; + + return ( +
+
+ +

Accept Capella iQ Supplemental Terms

+

{content}

+ {/* Checkbox for accepting Capella iQ Terms */} +
+ +
+
+ +
+
+
+ ); +}