diff --git a/firebase-vscode/common/messaging/protocol.ts b/firebase-vscode/common/messaging/protocol.ts index 076a68f46ee..41948441d5f 100644 --- a/firebase-vscode/common/messaging/protocol.ts +++ b/firebase-vscode/common/messaging/protocol.ts @@ -141,6 +141,8 @@ export interface ExtensionToWebviewParamsMap { notifyIsConnectedToPostgres: boolean; + notifyPostgresStringChanged: string; + /** Triggered when new environment variables values are found. */ notifyEnv: { env: { isMonospace: boolean } }; diff --git a/firebase-vscode/src/core/emulators.ts b/firebase-vscode/src/core/emulators.ts index c969e3afbfd..2d61620f6f3 100644 --- a/firebase-vscode/src/core/emulators.ts +++ b/firebase-vscode/src/core/emulators.ts @@ -178,7 +178,7 @@ export class EmulatorsController implements Disposable { displayInfo: listRunningEmulators(), }, }; - this.emulatorStatusItem.text = "$(data-connect) Emulators: running"; + this.emulatorStatusItem.text = "$(data-connect) Connected to local Postgres"; // data connect specifics; including temp logging implementation if ( @@ -207,10 +207,10 @@ export class EmulatorsController implements Disposable { // Updating the status bar label as "running", but don't "show" it. // We only show the status bar item when explicitly by interacting with the sidebar. - this.emulatorStatusItem.text = "$(data-connect) Emulators: running"; + this.emulatorStatusItem.text = "$(data-connect) Connected to local Postgres"; this.emulatorStatusItem.backgroundColor = undefined; } catch (e) { - this.emulatorStatusItem.text = "Emulators: errored"; + this.emulatorStatusItem.text = "$(data-connect) Emulators: errored"; this.emulatorStatusItem.backgroundColor = new ThemeColor( "statusBarItem.errorBackground", ); @@ -241,6 +241,24 @@ export class EmulatorsController implements Disposable { }; } + // TODO: Move all api calls to CLI DataConnectEmulatorClient + public getLocalEndpoint = () => computed(() => { + const emulatorInfos = this.emulators.value.infos?.displayInfo; + const dataConnectEmulator = emulatorInfos?.find( + (emulatorInfo) => emulatorInfo.name === Emulators.DATACONNECT, + ); + + if (!dataConnectEmulator) { + return undefined; + } + + // handle ipv6 + if (dataConnectEmulator.host.includes(":")) { + return `http://[${dataConnectEmulator.host}]:${dataConnectEmulator.port}`; + } + return `http://${dataConnectEmulator.host}:${dataConnectEmulator.port}`; + }); + dispose(): void { this.stopEmulators(); this.subscriptions.forEach((subscription) => subscription()); diff --git a/firebase-vscode/src/core/index.ts b/firebase-vscode/src/core/index.ts index b78b68ac181..ddec9c62886 100644 --- a/firebase-vscode/src/core/index.ts +++ b/firebase-vscode/src/core/index.ts @@ -59,7 +59,7 @@ export async function registerCore( "firebase.openFirebaseRc", () => { for (const root of getRootFolders()) { - upsertFile(vscode.Uri.file(".firebaserc"), () => ""); + upsertFile(vscode.Uri.file(`${root}/.firebaserc`), () => ""); } }, ); diff --git a/firebase-vscode/src/data-connect/emulator.ts b/firebase-vscode/src/data-connect/emulator.ts index 23427562bd3..96afb1adf1b 100644 --- a/firebase-vscode/src/data-connect/emulator.ts +++ b/firebase-vscode/src/data-connect/emulator.ts @@ -1,15 +1,15 @@ import { EmulatorsController } from "../core/emulators"; import * as vscode from "vscode"; import { ExtensionBrokerImpl } from "../extension-broker"; -import { Signal, effect, signal } from "@preact/signals-core"; -import { RC } from "../rc"; +import { effect, signal } from "@preact/signals-core"; import { firebaseRC, updateFirebaseRCProject } from "../core/config"; +import { DataConnectEmulatorClient } from "../../../src/emulator/dataconnectEmulator"; /** FDC-specific emulator logic */ export class DataConnectEmulatorController implements vscode.Disposable { constructor( readonly emulatorsController: EmulatorsController, - broker: ExtensionBrokerImpl, + readonly broker: ExtensionBrokerImpl, ) { function notifyIsConnectedToPostgres(isConnected: boolean) { broker.send("notifyIsConnectedToPostgres", isConnected); @@ -17,7 +17,6 @@ export class DataConnectEmulatorController implements vscode.Disposable { this.subs.push( broker.on("connectToPostgres", () => this.connectToPostgres()), - broker.on("disconnectPostgres", () => this.disconnectPostgres()), // Notify webviews when the emulator status changes effect(() => { @@ -34,12 +33,14 @@ export class DataConnectEmulatorController implements vscode.Disposable { readonly isPostgresEnabled = signal(false); private readonly subs: Array<() => void> = []; - private async promptConnectionString(): Promise { + private async promptConnectionString( + defaultConnectionString: string, + ): Promise { const connectionString = await vscode.window.showInputBox({ title: "Enter a Postgres connection string", prompt: - "A Postgres database must be configured to use the emulator locally. ", - placeHolder: "postgres://user:password@localhost:5432/dbname", + "A Postgres database must be configured to use the emulator locally.", + value: defaultConnectionString, }); return connectionString; @@ -47,27 +48,29 @@ export class DataConnectEmulatorController implements vscode.Disposable { private async connectToPostgres() { const rc = firebaseRC.value?.tryReadValue; + const newConnectionString = await this.promptConnectionString( + rc?.getDataconnect()?.postgres.localConnectionString || + "postgres://user:password@localhost:5432/dbname", + ); + if (!newConnectionString) { + return; + } - if (!rc?.getDataconnect()?.postgres) { - const newConnectionString = await this.promptConnectionString(); - if (!newConnectionString) { - return; - } + // notify sidebar webview of connection string + this.broker.send("notifyPostgresStringChanged", newConnectionString); - updateFirebaseRCProject({ - fdcPostgresConnectionString: newConnectionString, - }); - } + updateFirebaseRCProject({ + fdcPostgresConnectionString: newConnectionString, + }); + + // configure the emulator to use the local psql string + const emulatorClient = new DataConnectEmulatorClient(); + emulatorClient.configureEmulator({ connectionString: newConnectionString }); this.isPostgresEnabled.value = true; this.emulatorsController.emulatorStatusItem.show(); } - private disconnectPostgres() { - this.isPostgresEnabled.value = false; - this.emulatorsController.emulatorStatusItem.hide(); - } - dispose() { this.subs.forEach((sub) => sub()); } diff --git a/firebase-vscode/src/data-connect/index.ts b/firebase-vscode/src/data-connect/index.ts index 106b915e323..24b037301e7 100644 --- a/firebase-vscode/src/data-connect/index.ts +++ b/firebase-vscode/src/data-connect/index.ts @@ -185,14 +185,14 @@ export function registerFdc( context.subscriptions.push({ dispose: effect(() => { const configs = dataConnectConfigs.value?.tryReadValue; - if (configs && fdcService.localEndpoint.value) { + if (configs && emulatorController.getLocalEndpoint().value) { // TODO move to client.start or setupLanguageClient vscode.commands.executeCommand("fdc-graphql.restart"); vscode.commands.executeCommand( "firebase.dataConnect.executeIntrospection", ); - runEmulatorIssuesStream(configs, fdcService.localEndpoint.value); - runDataConnectCompiler(configs, fdcService.localEndpoint.value); + runEmulatorIssuesStream(configs, emulatorController.getLocalEndpoint().value); + runDataConnectCompiler(configs, emulatorController.getLocalEndpoint().value); } }), }); diff --git a/firebase-vscode/src/data-connect/service.ts b/firebase-vscode/src/data-connect/service.ts index ab4a152b16b..9da5c9f65d0 100644 --- a/firebase-vscode/src/data-connect/service.ts +++ b/firebase-vscode/src/data-connect/service.ts @@ -24,6 +24,7 @@ import { } from "../dataconnect/types"; import { ClientResponse } from "../apiv2"; import { InstanceType } from "./code-lens-provider"; +import { pluginLogger } from "../logger-wrapper"; /** * DataConnect Emulator service @@ -34,22 +35,6 @@ export class DataConnectService { private emulatorsController: EmulatorsController, ) {} - readonly localEndpoint = computed(() => { - const emulatorInfos = - this.emulatorsController.emulators.value.infos?.displayInfo; - const dataConnectEmulator = emulatorInfos?.find( - (emulatorInfo) => emulatorInfo.name === Emulators.DATACONNECT, - ); - - if (!dataConnectEmulator) { - return undefined; - } - - return ( - "http://" + dataConnectEmulator.host + ":" + dataConnectEmulator.port - ); - }); - async servicePath( path: string, instance: InstanceType, @@ -192,7 +177,7 @@ export class DataConnectService { return { data: (introspectionResults as any).data }; } catch (e) { // TODO: surface error that emulator is not connected - console.error("error: ", e); + pluginLogger.error("error: ", e); return { data: undefined }; } } @@ -214,7 +199,7 @@ export class DataConnectService { extensions: {}, // Introspection is the only caller of executeGraphqlRead }); const resp = await fetch( - (await firstWhereDefined(this.localEndpoint)) + + (await firstWhereDefined(this.emulatorsController.getLocalEndpoint())) + `/v1alpha/projects/p/locations/l/services/${serviceId}:executeGraphqlRead`, { method: "POST", @@ -230,7 +215,7 @@ export class DataConnectService { return result; } catch (e) { // TODO: actual error handling - console.log(e); + pluginLogger.error(e); return null; } } @@ -265,7 +250,7 @@ export class DataConnectService { return this.handleProdResponse(resp); } else { const resp = await fetch( - (await firstWhereDefined(this.localEndpoint)) + + (await firstWhereDefined(this.emulatorsController.getLocalEndpoint())) + `/v1alpha/${servicePath}:executeGraphql`, { method: "POST", @@ -280,4 +265,24 @@ export class DataConnectService { return this.handleResponse(resp); } } + + async connectToPostgres(connectionString: string): Promise { + try { + await fetch( + firstWhereDefined(this.emulatorsController.getLocalEndpoint()) + + `/emulator/configure?connectionString=${connectionString}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "x-mantle-admin": "all", + }, + }, + ); + return true; + } catch (e: any) { + pluginLogger.error(e); + return false; + } + } } diff --git a/firebase-vscode/src/test/integration/fishfood/emulator_status.ts b/firebase-vscode/src/test/integration/fishfood/emulator_status.ts index c586881f011..0a0d91d9aae 100644 --- a/firebase-vscode/src/test/integration/fishfood/emulator_status.ts +++ b/firebase-vscode/src/test/integration/fishfood/emulator_status.ts @@ -23,6 +23,6 @@ firebaseTest("When emulators are running, lists them", async function () { await commands.waitEmulators(); expect(await statusBar.emulatorsStatus.getText()).toContain( - "Emulators: running" + "Connected to local Postgres" ); }); diff --git a/firebase-vscode/webviews/data-connect.entry.tsx b/firebase-vscode/webviews/data-connect.entry.tsx index 56fb17569e5..551167be06c 100644 --- a/firebase-vscode/webviews/data-connect.entry.tsx +++ b/firebase-vscode/webviews/data-connect.entry.tsx @@ -1,9 +1,9 @@ import React from "react"; import { createRoot } from "react-dom/client"; -import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"; import { Spacer } from "./components/ui/Spacer"; import styles from "./globals/index.scss"; -import { broker, useBroker } from "./globals/html-broker"; +import { broker, useBroker, useBrokerListener } from "./globals/html-broker"; import { PanelSection } from "./components/ui/PanelSection"; // Prevent webpack from removing the `style` import above @@ -18,23 +18,27 @@ function DataConnect() { initialRequest: "getInitialIsConnectedToPostgres", }) ?? false; + const psqlString = useBroker("notifyPostgresStringChanged"); + return ( <> - -

- Start the FDC emulator. See also:{" "} - - Working with the emulator - -

+ + {!isConnectedToPostgres && (

+ Connect to Local PostgreSQL. See also:{" "} + + Working with the emulator + +

) + } {isConnectedToPostgres ? ( - broker.send("disconnectPostgres")}> - Stop emulator - + <> + + + ) : ( broker.send("connectToPostgres")}> - Start emulator + Connect to Local PostgreSQL )}
diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index dd43d24077b..9b0dfb238c8 100755 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -843,6 +843,9 @@ export async function startAll( rc: options.rc, }); await startEmulator(dataConnectEmulator); + if (!utils.isVSCodeExtension()) { + dataConnectEmulator.connectToPostgres(); + } } if (listenForEmulator.storage) { diff --git a/src/emulator/dataconnectEmulator.ts b/src/emulator/dataconnectEmulator.ts index 33f45a5c1f7..25305d0c6a5 100644 --- a/src/emulator/dataconnectEmulator.ts +++ b/src/emulator/dataconnectEmulator.ts @@ -9,6 +9,8 @@ import { EmulatorLogger } from "./emulatorLogger"; import { RC } from "../rc"; import { BuildResult, requiresVector } from "../dataconnect/types"; import { listenSpecsToString } from "./portUtils"; +import { Client, ClientResponse } from "../apiv2"; +import { EmulatorRegistry } from "./registry"; export interface DataConnectEmulatorArgs { projectId?: string; @@ -30,11 +32,14 @@ export interface DataConnectBuildArgs { } export class DataConnectEmulator implements EmulatorInstance { - constructor(private args: DataConnectEmulatorArgs) {} + private emulatorClient: DataConnectEmulatorClient; + + constructor(private args: DataConnectEmulatorArgs) { + this.emulatorClient = new DataConnectEmulatorClient(); + } private logger = EmulatorLogger.forEmulator(Emulators.DATACONNECT); async start(): Promise { - this.logger.log("DEBUG", `Using Postgres connection string: ${this.getLocalConectionString()}`); try { const info = await DataConnectEmulator.build({ configDir: this.args.configDir }); if (requiresVector(info.metadata)) { @@ -83,6 +88,7 @@ export class DataConnectEmulator implements EmulatorInstance { timeout: 10_000, }; } + getName(): Emulators { return Emulators.DATACONNECT; } @@ -138,4 +144,42 @@ export class DataConnectEmulator implements EmulatorInstance { } return this.args.rc.getDataconnect()?.postgres?.localConnectionString; } + + public async connectToPostgres( + localConnectionString?: string, + database?: string, + serviceId?: string, + ): Promise { + const connectionString = localConnectionString ?? this.getLocalConectionString(); + if (!connectionString) { + this.logger.log("DEBUG", "No Postgres connection string found, not connecting to Postgres"); + return false; + } + await this.emulatorClient.configureEmulator({ connectionString, database, serviceId }); + return true; + } +} + +type ConfigureEmulatorRequest = { + // Defaults to the local service in dataconnect.yaml if not provided + serviceId?: string; + // The Postgres connection string to connect the new service to. This is + // required in order to configure the emulator service. + connectionString: string; + // The Postgres database to connect the new service to. If this field is + // populated, then any database specified in the connection_string will be + // overwritten. + database?: string; +}; + +export class DataConnectEmulatorClient { + private readonly client: Client; + constructor() { + this.client = EmulatorRegistry.client(Emulators.DATACONNECT); + } + + public async configureEmulator(body: ConfigureEmulatorRequest): Promise> { + const res = await this.client.post("emulator/configure", body); + return res; + } }