diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md index 2844d0693d9..e0cbeedd9c4 100644 --- a/firebase-vscode/CHANGELOG.md +++ b/firebase-vscode/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT -- [Feature] Added `debug` setting to run commands with `--debug` +- [Added] Added support for emulator import/export. +- [Added] Added `debug` setting to run commands with `--debug` ## 0.12.0 diff --git a/firebase-vscode/common/messaging/protocol.ts b/firebase-vscode/common/messaging/protocol.ts index 68b5edbff54..a3dfa3bf98c 100644 --- a/firebase-vscode/common/messaging/protocol.ts +++ b/firebase-vscode/common/messaging/protocol.ts @@ -55,9 +55,12 @@ export interface WebviewToExtensionParamsMap { /** Calls the `firebase init` CLI */ runFirebaseInit: void; - /** Calls the `firebase init` CLI */ + /** Calls the `firebase emulators:start` CLI */ runStartEmulators: void; + /** Calls the `firebase emulators:export` CLI */ + runEmulatorsExport: void; + /** * Show a UI message using the vscode interface */ @@ -100,6 +103,12 @@ export interface WebviewToExtensionParamsMap { /** Opens generated docs */ "fdc.open-docs": void; + /** Opens settings page searching for Data Connect emualtor settings */ + "fdc.open-emulator-settings": void; + + /** Clears data from a running data connect emulator */ + "fdc.clear-emulator-data": void; + // Initialize "result" tab. getDataConnectResults: void; diff --git a/firebase-vscode/package.json b/firebase-vscode/package.json index 8321c6862d4..1ae236d40b4 100644 --- a/firebase-vscode/package.json +++ b/firebase-vscode/package.json @@ -78,6 +78,20 @@ "markdownDescription": "%ext.config.idx.viewMetricNotice%", "scope": "application" }, + "firebase.emulators.importPath": { + "type": "string", + "markdownDescription": "%ext.config.emulators.importPath%" + }, + "firebase.emulators.exportPath": { + "type": "string", + "default": "./exportedData", + "markdownDescription": "%ext.config.emulators.exportPath%" + }, + "firebase.emulators.exportOnExit":{ + "type": "boolean", + "default": false, + "markdownDescription": "%ext.config.emulators.exportOnExit%" + }, "firebase.debug": { "type": "boolean", "default": false, diff --git a/firebase-vscode/package.nls.json b/firebase-vscode/package.nls.json index 6064872232b..b61e3840ba3 100644 --- a/firebase-vscode/package.nls.json +++ b/firebase-vscode/package.nls.json @@ -4,7 +4,10 @@ "ext.config.firebasePath": "Path to the `Firebase` module, e.g. `./node_modules/firebase`", "ext.config.hosting.useFrameworks": "Enable web frameworks", "ext.config.npmPath": "Path to NPM executable in local environment", + "ext.config.title": "Firebase Data Connect", + "ext.config.idx.viewMetricNotice": "Show data collection notice on next startup (IDX Only)", + "ext.config.emulators.importPath": "Path to import emulator data from", + "ext.config.emulators.exportPath": "Path to export emulator data to", + "ext.config.emulators.exportOnExit": "If true, data will be exported to exportPath when the emulator shuts down" "ext.config.debug": "When true, add the --debug flag to any commands run by the extension", - "ext.config.title": "Prettier", - "ext.config.idx.viewMetricNotice": "Show data collection notice on next startup (IDX Only)" } diff --git a/firebase-vscode/src/analytics.ts b/firebase-vscode/src/analytics.ts index 58516fd17eb..298d62b3c6a 100644 --- a/firebase-vscode/src/analytics.ts +++ b/firebase-vscode/src/analytics.ts @@ -32,6 +32,7 @@ export enum DATA_CONNECT_EVENT_NAME { START_EMULATORS = "start_emulators", AUTO_COMPLETE = "auto_complete", SESSION_CHAR_COUNT = "session_char_count", + EMULATOR_EXPORT ="emulator_export", SETUP_FIREBASE_BINARY = "setup_firebase_binary", } diff --git a/firebase-vscode/src/core/emulators.ts b/firebase-vscode/src/core/emulators.ts index 410f057924f..ead6149ed7a 100644 --- a/firebase-vscode/src/core/emulators.ts +++ b/firebase-vscode/src/core/emulators.ts @@ -6,6 +6,7 @@ import { EmulatorsStatus, RunningEmulatorInfo } from "../messaging/types"; import { EmulatorHubClient } from "../../../src/emulator/hubClient"; import { GetEmulatorsResponse } from "../../../src/emulator/hub"; import { EmulatorInfo } from "../emulator/types"; +import { getSettings } from "../utils/settings"; export class EmulatorsController implements Disposable { constructor(private broker: ExtensionBrokerImpl) { this.emulatorStatusItem.command = "firebase.openFirebaseRc"; @@ -21,6 +22,25 @@ export class EmulatorsController implements Disposable { this.setEmulatorsStarting(); }), ); + + // Subscription to open up settings window + this.subscriptions.push( + broker.on("fdc.open-emulator-settings", () => { + vscode.commands.executeCommand( 'workbench.action.openSettings', 'firebase.emulators' ); + }) + ); + + // Subscription to trigger clear emulator data when button is clicked. + this.subscriptions.push( + broker.on("fdc.clear-emulator-data", () => { + vscode.commands.executeCommand("firebase.emulators.clearData"); + }), + ); + + // Subscription to trigger emulator exports when button is clicked. + this.subscriptions.push(broker.on("runEmulatorsExport", () => { + vscode.commands.executeCommand("firebase.emulators.exportData") + })); } readonly emulatorStatusItem = vscode.window.createStatusBarItem("emulators"); @@ -39,6 +59,17 @@ export class EmulatorsController implements Disposable { this.setEmulatorsStopped.bind(this), ); + private readonly clearEmulatorDataCommand = vscode.commands.registerCommand( + "firebase.emulators.clearData", + this.clearDataConnectData.bind(this), + ); + + + private readonly exportEmulatorDataCommand = vscode.commands.registerCommand( + "firebase.emulators.exportData", + this.exportEmulatorData.bind(this), + ); + readonly emulators: { status: EmulatorsStatus; infos?: RunningEmulatorInfo } = { status: "stopped", @@ -107,11 +138,8 @@ export class EmulatorsController implements Disposable { async findRunningCliEmulators(): Promise< { status: EmulatorsStatus; infos?: RunningEmulatorInfo } | undefined > { - const projectId = firebaseRC.value?.tryReadValue?.projects?.default; - // TODO: think about what to without projectID, in potentially a logged out mode - const hubClient = new EmulatorHubClient(projectId!); - - if (hubClient.foundHub()) { + const hubClient = this.getHubClient(); + if (hubClient) { const response: GetEmulatorsResponse = await hubClient.getEmulators(); if (Object.values(response)) { @@ -119,11 +147,38 @@ export class EmulatorsController implements Disposable { } else { this.setEmulatorsStopped(); } + } + return this.emulators; + } + + async clearDataConnectData(): Promise { + const hubClient = this.getHubClient(); + if (hubClient) { + await hubClient.clearDataConnectData(); + vscode.window.showInformationMessage(`Data Connect emulator data has been cleared.`); + } + } + + async exportEmulatorData(): Promise { + const settings = getSettings(); + const exportDir = settings.exportPath; + const hubClient = this.getHubClient(); + if (hubClient) { + // TODO: Make exportDir configurable + await hubClient.postExport({path: exportDir, initiatedBy: "Data Connect VSCode extension"}); + vscode.window.showInformationMessage(`Emulator Data exported to ${exportDir}`); + } + } + + private getHubClient(): EmulatorHubClient | undefined { + const projectId = firebaseRC.value?.tryReadValue?.projects?.default; + // TODO: think about what to without projectID, in potentially a logged out mode + const hubClient = new EmulatorHubClient(projectId!); + if (hubClient.foundHub()) { + return hubClient; } else { this.setEmulatorsStopped(); } - - return this.emulators; } public areEmulatorsRunning() { @@ -135,5 +190,7 @@ export class EmulatorsController implements Disposable { this.findRunningEmulatorsCommand.dispose(); this.emulatorStatusItem.dispose(); this.emulatorsStoppped.dispose(); + this.clearEmulatorDataCommand.dispose(); + this.exportEmulatorDataCommand.dispose(); } } diff --git a/firebase-vscode/src/data-connect/terminal.ts b/firebase-vscode/src/data-connect/terminal.ts index f61cb56aac7..e48147adb97 100644 --- a/firebase-vscode/src/data-connect/terminal.ts +++ b/firebase-vscode/src/data-connect/terminal.ts @@ -90,12 +90,19 @@ export function registerTerminalTasks( const startEmulatorsTaskBroker = broker.on("runStartEmulators", () => { analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.START_EMULATORS); - // TODO: optional debug mode + + let cmd = `${settings.firebasePath} emulators:start --project ${currentProjectId.value}`; + + if (settings.importPath) { + cmd += ` --import ${settings.importPath}`; + } + if (settings.exportOnExit) { + cmd += ` --export-on-exit ${settings.exportPath}`; + } runTerminalTask( "firebase emulators", - `${settings.firebasePath} emulators:start --project ${currentProjectId.value}`, - // emulators:start almost never ask interactive questions. - { focus: false }, + cmd, + { focus: true }, ); }); diff --git a/firebase-vscode/src/test/integration/fishfood/emulator.ts b/firebase-vscode/src/test/integration/fishfood/emulator.ts index 7dba9c16c73..76ea1577ca2 100644 --- a/firebase-vscode/src/test/integration/fishfood/emulator.ts +++ b/firebase-vscode/src/test/integration/fishfood/emulator.ts @@ -1,6 +1,9 @@ import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; import { FirebaseCommands } from "../../utils/page_objects/commands"; import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; + +import { TerminalView } from "../../utils/page_objects/terminal"; +import { Notifications } from "../../utils/page_objects/notifications"; import { mockUser } from "../../utils/user"; import { mockProject } from "../../utils/projects"; @@ -12,6 +15,8 @@ firebaseSuite("Emulators", async function () { const sidebar = new FirebaseSidebar(workbench); const commands = new FirebaseCommands(); + const terminal = new TerminalView(workbench); + const notifications = new Notifications(workbench); await sidebar.openExtensionSidebar(); await commands.waitForUser(); @@ -26,6 +31,15 @@ firebaseSuite("Emulators", async function () { const current = await sidebar.currentEmulators(); expect(current).toContain("dataconnect :9399"); + + await sidebar.clearEmulatorData(); + const text = await terminal.getTerminalText(); + expect(text.includes("Clearing data from Data Connect data sources")).toBeTruthy(); + + await sidebar.exportEmulatorData(); + const exportNotification = await notifications.getExportNotification(); + expect(exportNotification).toExist(); + }, ); }); diff --git a/firebase-vscode/src/test/test_projects/fishfood/.firebaserc b/firebase-vscode/src/test/test_projects/fishfood/.firebaserc index 433140fb0b2..36fd8c67a70 100644 --- a/firebase-vscode/src/test/test_projects/fishfood/.firebaserc +++ b/firebase-vscode/src/test/test_projects/fishfood/.firebaserc @@ -1,6 +1,6 @@ { "projects": { - "default": "dart-firebase-admin" + "default": "test-project" }, "targets": {}, "etags": {}, diff --git a/firebase-vscode/src/test/utils/page_objects/notifications.ts b/firebase-vscode/src/test/utils/page_objects/notifications.ts new file mode 100644 index 00000000000..f92e3baa1b8 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/notifications.ts @@ -0,0 +1,13 @@ +import { Workbench } from "wdio-vscode-service"; + +export class Notifications { + constructor(readonly workbench: Workbench) {} + + async getExportNotification() { + const notifications = await this.workbench.getNotifications(); + return notifications.find(async n => { + const message = await n.getMessage(); + return message.includes("Emulator Data exported to"); + }); + } +} \ No newline at end of file diff --git a/firebase-vscode/src/test/utils/page_objects/sidebar.ts b/firebase-vscode/src/test/utils/page_objects/sidebar.ts index efec7e458ac..d06a7dc82f6 100644 --- a/firebase-vscode/src/test/utils/page_objects/sidebar.ts +++ b/firebase-vscode/src/test/utils/page_objects/sidebar.ts @@ -49,6 +49,20 @@ export class FirebaseSidebar { }); } + async clearEmulatorData() { + return this.runInStudioContext(async (studio) => { + const btn = await studio.clearEmulatorDataBtn; + return btn.click(); + }); + } + + async exportEmulatorData() { + return this.runInStudioContext(async (studio) => { + const btn = await studio.exportEmulatorDataBtn; + return btn.click(); + }); + } + async startDeploy() { return this.runInStudioContext(async (studio) => { await studio.fdcDeployElement.waitForDisplayed(); @@ -86,6 +100,14 @@ export class StudioView { return $("vscode-button=Start emulators"); } + get clearEmulatorDataBtn() { + return $("vscode-button=Clear Data Connect data"); + } + + get exportEmulatorDataBtn() { + return $("vscode-button=Export emulator data"); + } + get addSdkToAppBtn() { return $("vscode-button=Add SDK to app"); } diff --git a/firebase-vscode/src/test/utils/page_objects/terminal.ts b/firebase-vscode/src/test/utils/page_objects/terminal.ts new file mode 100644 index 00000000000..2ee4050a33d --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/terminal.ts @@ -0,0 +1,11 @@ +import { Workbench } from "wdio-vscode-service"; + +export class TerminalView { + constructor(readonly workbench: Workbench) {} + + private readonly bottomBar = this.workbench.getBottomBar(); + async getTerminalText() { + const tv = await this.bottomBar.openTerminalView(); + return tv.getText(); + } +} diff --git a/firebase-vscode/src/utils/settings.ts b/firebase-vscode/src/utils/settings.ts index 680fbdfe6b4..a61499725a1 100644 --- a/firebase-vscode/src/utils/settings.ts +++ b/firebase-vscode/src/utils/settings.ts @@ -7,6 +7,9 @@ export interface Settings { readonly npmPath: string; readonly useFrameworks: boolean; readonly shouldShowIdxMetricNotice: boolean; + readonly importPath?: string; + readonly exportPath: string; + readonly exportOnExit: boolean; readonly debug: boolean; } @@ -38,6 +41,9 @@ export function getSettings(): Settings { "idx.viewMetricNotice", true, ), + importPath: config.get("emulators.importPath"), + exportPath: config.get("emulators.exportPath", "./exportedData"), + exportOnExit: config.get("emulators.exportOnExit", false), debug: config.get("debug", false), }; } diff --git a/firebase-vscode/webviews/SidebarApp.tsx b/firebase-vscode/webviews/SidebarApp.tsx index c04f5150e59..0abd8c02f95 100644 --- a/firebase-vscode/webviews/SidebarApp.tsx +++ b/firebase-vscode/webviews/SidebarApp.tsx @@ -105,6 +105,13 @@ function EmulatorsPanel() { Start emulators +