From 12fa01c86af4d64a046c2479a00fd31f7af280d7 Mon Sep 17 00:00:00 2001 From: TJ Lavelle Date: Mon, 11 Sep 2023 13:46:40 -0400 Subject: [PATCH] Turndown workflow file (#6348) Updated project-picking at the same time (could only have one picker at a time) --- firebase-vscode/src/cli.ts | 128 +++++++++++------ firebase-vscode/src/core/index.ts | 12 +- firebase-vscode/src/core/project.ts | 79 +++++++++-- firebase-vscode/src/core/user.ts | 4 + firebase-vscode/src/extension.ts | 5 +- firebase-vscode/src/workflow.ts | 208 ---------------------------- 6 files changed, 166 insertions(+), 270 deletions(-) delete mode 100644 firebase-vscode/src/workflow.ts diff --git a/firebase-vscode/src/cli.ts b/firebase-vscode/src/cli.ts index 8b99e300753..8ffc471d315 100644 --- a/firebase-vscode/src/cli.ts +++ b/firebase-vscode/src/cli.ts @@ -23,12 +23,15 @@ import { listChannels } from "../../src/hosting/api"; import { EmulatorUiSelections, ChannelWithId } from "../common/messaging/types"; import { pluginLogger } from "./logger-wrapper"; import { Config } from "../../src/config"; -import { currentUser } from "./workflow"; import { setAccessToken } from "../../src/apiv2"; -import { startAll as startAllEmulators, cleanShutdown as stopAllEmulators } from "../../src/emulator/controller"; +import { + startAll as startAllEmulators, + cleanShutdown as stopAllEmulators, +} from "../../src/emulator/controller"; import { EmulatorRegistry } from "../../src/emulator/registry"; import { EmulatorInfo, Emulators } from "../../src/emulator/types"; import * as commandUtils from "../../src/emulator/commandUtils"; +import { currentUser } from "./core/user"; /** * Try to get a service account by calling requireAuth() without @@ -41,7 +44,7 @@ async function getServiceAccount() { // to requireAuth which would prevent autoAuth() from being reached. // We do need to send isVSCE to prevent project selection popup const optionsMinusUser = await getCommandOptions(undefined, { - ...currentOptions + ...currentOptions, }); delete optionsMinusUser.user; delete optionsMinusUser.tokens; @@ -55,12 +58,15 @@ async function getServiceAccount() { if (process.env.MONOSPACE_ENV) { // If it can't find a service account in Monospace, that's a blocking // error and we should throw. - throw new Error(`Unable to find service account. ` - +`requireAuthError: ${errorMessage}`); + throw new Error( + `Unable to find service account. ` + `requireAuthError: ${errorMessage}` + ); } else { // In other environments, it is common to not find a service account. - pluginLogger.debug(`No service account found (this may be normal), ` - +`requireAuth error output: ${errorMessage}`); + pluginLogger.debug( + `No service account found (this may be normal), ` + + `requireAuth error output: ${errorMessage}` + ); } return null; } @@ -69,13 +75,16 @@ async function getServiceAccount() { // the metadata server doesn't currently return the credentials // for the workspace service account. Remove when Monospace is // updated to return credentials through the metadata server. - pluginLogger.debug(`Using WORKSPACE_SERVICE_ACCOUNT_EMAIL env ` - + `variable to get service account email: ` - + `${process.env.WORKSPACE_SERVICE_ACCOUNT_EMAIL}`); + pluginLogger.debug( + `Using WORKSPACE_SERVICE_ACCOUNT_EMAIL env ` + + `variable to get service account email: ` + + `${process.env.WORKSPACE_SERVICE_ACCOUNT_EMAIL}` + ); return process.env.WORKSPACE_SERVICE_ACCOUNT_EMAIL; } - pluginLogger.debug(`Got service account email through credentials:` - + ` ${email}`); + pluginLogger.debug( + `Got service account email through credentials:` + ` ${email}` + ); return email; } @@ -87,20 +96,20 @@ async function getServiceAccount() { async function requireAuthWrapper(showError: boolean = true): Promise { // Try to get global default from configstore. For some reason this is // often overwritten when restarting the extension. - pluginLogger.debug('requireAuthWrapper'); + pluginLogger.debug("requireAuthWrapper"); let account = getGlobalDefaultAccount(); if (!account) { // If nothing in configstore top level, grab the first "additionalAccount" const accounts = getAllAccounts(); for (const additionalAccount of accounts) { - if (additionalAccount.user.email === currentUser.email) { + if (additionalAccount.user.email === currentUser.value.email) { account = additionalAccount; setGlobalDefaultAccount(account); } } } const commandOptions = await getCommandOptions(undefined, { - ...currentOptions + ...currentOptions, }); // `requireAuth()` is not just a check, but will also register SERVICE // ACCOUNT tokens in memory as a variable in apiv2.ts, which is needed @@ -110,7 +119,10 @@ async function requireAuthWrapper(showError: boolean = true): Promise { try { const serviceAccountEmail = await getServiceAccount(); // Priority 1: Service account exists and is the current selected user - if (serviceAccountEmail && currentUser.email === serviceAccountEmail) { + if ( + serviceAccountEmail && + currentUser.value.email === serviceAccountEmail + ) { // requireAuth should have been run and apiv2 token should be stored // already due to getServiceAccount() call above. return true; @@ -128,14 +140,16 @@ async function requireAuthWrapper(showError: boolean = true): Promise { // requireAuth was already run as part of getServiceAccount() above return true; } - pluginLogger.debug('No user found (this may be normal)'); + pluginLogger.debug("No user found (this may be normal)"); return false; } catch (e) { if (showError) { // Show error to user - show a popup and log it with log level // "error". Usually set on user-triggered actions such as // init hosting and deploy. - pluginLogger.error(`requireAuth error: ${e.original?.message || e.message}`); + pluginLogger.error( + `requireAuth error: ${e.original?.message || e.message}` + ); vscode.window.showErrorMessage("Not logged in", { modal: true, detail: `Log in by clicking "Sign in with Google" in the sidebar.`, @@ -143,8 +157,10 @@ async function requireAuthWrapper(showError: boolean = true): Promise { } else { // User shouldn't need to see this error - not actionable, // but we should log it for debugging purposes. - pluginLogger.debug('requireAuth error output: ', - e.original?.message || e.message); + pluginLogger.debug( + "requireAuth error output: ", + e.original?.message || e.message + ); } return false; } @@ -165,7 +181,9 @@ export async function getAccounts(): Promise> { return accounts; } -export async function getChannels(firebaseJSON: Config): Promise { +export async function getChannels( + firebaseJSON: Config +): Promise { if (!firebaseJSON) { return []; } @@ -180,16 +198,17 @@ export async function getChannels(firebaseJSON: Config): Promise ({ - ...channel, id: channel.name.split("/").pop() + return channels.map((channel) => ({ + ...channel, + id: channel.name.split("/").pop(), })); } catch (e) { - pluginLogger.error('Error in getChannels()', e); + pluginLogger.error("Error in getChannels()", e); vscode.window.showErrorMessage("Error finding hosting channels", { modal: true, detail: `Error finding hosting channels: ${e}`, @@ -219,22 +238,24 @@ export async function listProjects() { return listFirebaseProjects(); } -export async function initHosting( - options: { spa: boolean; public?: string, useFrameworks: boolean } -): Promise { +export async function initHosting(options: { + spa: boolean; + public?: string; + useFrameworks: boolean; +}): Promise { const loggedIn = await requireAuthWrapper(true); if (!loggedIn) { - pluginLogger.error('No user found, canceling hosting init'); + pluginLogger.error("No user found, canceling hosting init"); return false; } let webFrameworksOptions = {}; if (options.useFrameworks) { - pluginLogger.debug('Setting web frameworks options'); + pluginLogger.debug("Setting web frameworks options"); webFrameworksOptions = { // Should use auto-discovered framework useDiscoveredFramework: true, // Should set up a new framework - do not do this on Monospace - useWebFrameworks: false + useWebFrameworks: false, }; } const commandOptions = await getCommandOptions(undefined, currentOptions); @@ -243,13 +264,16 @@ export async function initHosting( ...options, ...webFrameworksOptions, // False for now, we can let the user decide if needed - github: false + github: false, }; - pluginLogger.debug('Calling hosting init with inquirer options', inspect(inquirerOptions)); + pluginLogger.debug( + "Calling hosting init with inquirer options", + inspect(inquirerOptions) + ); setInquirerOptions(inquirerOptions); try { await initAction("hosting", commandOptions); - } catch(e) { + } catch (e) { pluginLogger.error(e.message); return false; } @@ -261,7 +285,7 @@ export async function deployToHosting( deployTarget: string ) { if (!(await requireAuthWrapper(true))) { - pluginLogger.error('No user found, canceling deployment'); + pluginLogger.error("No user found, canceling deployment"); return { success: false, hostingUrl: "", consoleUrl: "" }; } @@ -269,17 +293,29 @@ export async function deployToHosting( try { const options = { ...currentOptions }; // TODO(hsubox76): handle multiple hosting configs - pluginLogger.debug('Calling getDefaultHostingSite() with options', inspect(options)); - firebaseJSON.set('hosting', { ...firebaseJSON.get('hosting'), site: await getDefaultHostingSite(options) }); - pluginLogger.debug('Calling getCommandOptions() with options', inspect(options)); + pluginLogger.debug( + "Calling getDefaultHostingSite() with options", + inspect(options) + ); + firebaseJSON.set("hosting", { + ...firebaseJSON.get("hosting"), + site: await getDefaultHostingSite(options), + }); + pluginLogger.debug( + "Calling getCommandOptions() with options", + inspect(options) + ); const commandOptions = await getCommandOptions(firebaseJSON, options); - pluginLogger.debug('Calling hosting deploy with command options', inspect(commandOptions)); - if (deployTarget === 'live') { + pluginLogger.debug( + "Calling hosting deploy with command options", + inspect(commandOptions) + ); + if (deployTarget === "live") { await deploy(["hosting"], commandOptions); } else { await hostingChannelDeployAction(deployTarget, commandOptions); } - pluginLogger.debug('Hosting deploy complete'); + pluginLogger.debug("Hosting deploy complete"); } catch (e) { let message = `Error deploying to hosting`; if (e.message) { @@ -294,16 +330,20 @@ export async function deployToHosting( return { success: true, hostingUrl: "", consoleUrl: "" }; } -export async function emulatorsStart(emulatorUiSelections: EmulatorUiSelections) { +export async function emulatorsStart( + emulatorUiSelections: EmulatorUiSelections +) { const commandOptions = await getCommandOptions(undefined, { ...currentOptions, project: emulatorUiSelections.projectId, exportOnExit: emulatorUiSelections.exportStateOnExit, import: emulatorUiSelections.importStateFolderPath, - only: emulatorUiSelections.mode === "hosting" ? "hosting" : "" + only: emulatorUiSelections.mode === "hosting" ? "hosting" : "", }); // Adjusts some options, export on exit can be a boolean or a path. - commandUtils.setExportOnExitOptions(commandOptions as commandUtils.ExportOnExitOptions); + commandUtils.setExportOnExitOptions( + commandOptions as commandUtils.ExportOnExitOptions + ); return startAllEmulators(commandOptions, /*showUi=*/ true); } diff --git a/firebase-vscode/src/core/index.ts b/firebase-vscode/src/core/index.ts index 6d3d2ae7fa5..d7ce0fa4251 100644 --- a/firebase-vscode/src/core/index.ts +++ b/firebase-vscode/src/core/index.ts @@ -1,4 +1,4 @@ -import vscode, { Disposable } from "vscode"; +import vscode, { Disposable, ExtensionContext } from "vscode"; import { ExtensionBrokerImpl } from "../extension-broker"; import { registerConfig } from "./config"; import { registerEmulators } from "./emulators"; @@ -10,7 +10,13 @@ import { registerUser } from "./user"; import { registerProject } from "./project"; import { registerQuickstart } from "./quickstart"; -export function registerCore(broker: ExtensionBrokerImpl): Disposable { +export function registerCore({ + broker, + context, +}: { + broker: ExtensionBrokerImpl; + context: ExtensionContext; +}): Disposable { const settings = getSettings(); if (settings.npmPath) { @@ -38,7 +44,7 @@ export function registerCore(broker: ExtensionBrokerImpl): Disposable { registerEmulators(broker), registerEnv(broker), registerUser(broker), - registerProject(broker), + registerProject({ context, broker }), registerQuickstart(broker) ); } diff --git a/firebase-vscode/src/core/project.ts b/firebase-vscode/src/core/project.ts index e285175a460..39d2533ebc6 100644 --- a/firebase-vscode/src/core/project.ts +++ b/firebase-vscode/src/core/project.ts @@ -1,11 +1,14 @@ -import vscode, { Disposable, QuickPickItem } from "vscode"; +import vscode, { Disposable, ExtensionContext, QuickPickItem } from "vscode"; import { ExtensionBrokerImpl } from "../extension-broker"; import { computed, effect, signal } from "@preact/signals-react"; import { firebaseRC } from "./config"; import { FirebaseProjectMetadata } from "../types/project"; -import { currentUser } from "./user"; +import { currentUser, isServiceAccount } from "./user"; import { listProjects } from "../cli"; import { pluginLogger } from "../logger-wrapper"; +import { selectProjectInMonospace } from "../../../src/monospace"; +import { currentOptions } from "../options"; +import { updateFirebaseRCProject } from "../config-files"; /** Available projects */ export const projects = signal>({}); @@ -13,17 +16,31 @@ export const projects = signal>({}); /** Currently selected project ID */ export const currentProjectId = signal(""); +const userScopedProjects = computed(() => { + return projects.value[currentUser.value?.email ?? ""] ?? []; +}); + /** Gets the currently selected project, fallback to first default project in RC file */ export const currentProject = computed( () => { - const userProjects = projects.value[currentUser.value?.email ?? ""] ?? []; + // Service accounts should only have one project + if (isServiceAccount.value) { + return userScopedProjects.value[0]; + } + const wantProjectId = currentProjectId.value || firebaseRC.value.projects["default"]; - return userProjects.find((p) => p.projectId === wantProjectId); + return userScopedProjects.value.find((p) => p.projectId === wantProjectId); } ); -export function registerProject(broker: ExtensionBrokerImpl): Disposable { +export function registerProject({ + context, + broker, +}: { + context: ExtensionContext; + broker: ExtensionBrokerImpl; +}): Disposable { effect(async () => { const user = currentUser.value; if (user) { @@ -42,6 +59,14 @@ export function registerProject(broker: ExtensionBrokerImpl): Disposable { }); }); + // Update .firebaserc with defined project ID + effect(() => { + const projectId = currentProjectId.value; + if (projectId) { + updateFirebaseRCProject(context, "default", currentProjectId.value); + } + }); + broker.on("getInitialData", () => { broker.send("notifyProjectChanged", { projectId: currentProject.value?.projectId ?? "", @@ -49,10 +74,42 @@ export function registerProject(broker: ExtensionBrokerImpl): Disposable { }); broker.on("selectProject", async () => { - // TODO: implement at the same time we teardown the old picker - // const projects = await listProjects(); - // const id = await promptUserForProject(projects); - // pluginLogger.info("foo:", { id }); + if (process.env.MONOSPACE_ENV) { + pluginLogger.debug( + "selectProject: found MONOSPACE_ENV, " + + "prompting user using external flow" + ); + /** + * Monospace case: use Monospace flow + */ + const monospaceExtension = + vscode.extensions.getExtension("google.monospace"); + process.env.MONOSPACE_DAEMON_PORT = + monospaceExtension.exports.getMonospaceDaemonPort(); + try { + const projectId = await selectProjectInMonospace({ + projectRoot: currentOptions.cwd, + project: undefined, + isVSCE: true, + }); + + if (projectId) { + currentProjectId.value = projectId; + } + } catch (e) { + pluginLogger.error(e); + } + } else if (isServiceAccount.value) { + return; + } else { + try { + currentProjectId.value = await promptUserForProject( + userScopedProjects.value + ); + } catch (e) { + vscode.window.showErrorMessage(e.message); + } + } }); return { @@ -67,6 +124,6 @@ async function promptUserForProject(projects: FirebaseProjectMetadata[]) { description: p.displayName, })); - const projectId = await vscode.window.showQuickPick(items); - return projectId; + const item = await vscode.window.showQuickPick(items); + return item.label; } diff --git a/firebase-vscode/src/core/user.ts b/firebase-vscode/src/core/user.ts index f2df89623b6..42176e37073 100644 --- a/firebase-vscode/src/core/user.ts +++ b/firebase-vscode/src/core/user.ts @@ -18,6 +18,10 @@ export const currentUser = computed(() => { return users.value[currentUserId.value] ?? Object.values(users.value)[0]; }); +export const isServiceAccount = computed(() => { + return (currentUser.value as ServiceAccountUser)?.type === "service_account"; +}); + export function registerUser(broker: ExtensionBrokerImpl): Disposable { effect(() => { broker.send("notifyUsers", { users: Object.values(users.value) }); diff --git a/firebase-vscode/src/extension.ts b/firebase-vscode/src/extension.ts index bdb924829f2..be0300ef974 100644 --- a/firebase-vscode/src/extension.ts +++ b/firebase-vscode/src/extension.ts @@ -8,7 +8,6 @@ import { ExtensionToWebviewParamsMap, WebviewToExtensionParamsMap, } from "../common/messaging/protocol"; -import { setupWorkflow } from "./workflow"; import { logSetup, pluginLogger } from "./logger-wrapper"; import { registerWebview } from "./webview"; import { registerCore } from "./core"; @@ -27,10 +26,8 @@ export function activate(context: vscode.ExtensionContext) { vscode.Webview >(new ExtensionBroker()); - setupWorkflow(context, broker); - context.subscriptions.push( - registerCore(broker), + registerCore({ broker, context }), registerWebview({ name: "sidebar", broker, diff --git a/firebase-vscode/src/workflow.ts b/firebase-vscode/src/workflow.ts deleted file mode 100644 index 581c0f130a0..00000000000 --- a/firebase-vscode/src/workflow.ts +++ /dev/null @@ -1,208 +0,0 @@ -import * as vscode from "vscode"; -import { ExtensionContext } from "vscode"; - -import { FirebaseProjectMetadata } from "../../src/types/project"; -import { ExtensionBrokerImpl } from "./extension-broker"; -import { - deployToHosting, - getAccounts, - getChannels, - initHosting, - listProjects, - login, - logoutUser, -} from "./cli"; -import { User } from "../../src/types/auth"; -import { currentOptions } from "./options"; -import { selectProjectInMonospace } from "../../src/monospace"; -import { pluginLogger } from "./logger-wrapper"; -import { - readAndSendFirebaseConfigs, - setupFirebaseJsonAndRcFileSystemWatcher, - updateFirebaseRCProject, -} from "./config-files"; -import { ServiceAccountUser } from "../common/types"; - -let users: Array = []; -export let currentUser: User | ServiceAccountUser; -// Stores a mapping from user email to list of projects for that user -let projectsUserMapping = new Map(); - -async function fetchUsers() { - const accounts = await getAccounts(); - users = accounts.map((account) => account.user); -} - -/** - * Get the user to select a project. - */ -async function promptUserForProject(projects: FirebaseProjectMetadata[]) { - const items = projects.map(({ projectId }) => projectId); - - return new Promise((resolve, reject) => { - vscode.window.showQuickPick(items).then(async (projectId) => { - const project = projects.find((p) => p.projectId === projectId); - if (!project) { - if (currentOptions.rc?.projects?.default) { - // Don't show an error message if a project was previously selected, - // just do nothing. - resolve(null); - } - reject("Invalid project selected. Please select a project to proceed"); - } else { - resolve(project.projectId); - } - }); - }); -} - -function updateCurrentUser( - users: User[], - broker: ExtensionBrokerImpl, - newUser?: User | ServiceAccountUser -) { - const previousCurrentUser = currentUser; - if (newUser) { - if (newUser.email !== currentUser?.email) { - currentUser = newUser; - } - } - if (!newUser) { - if (users.length > 0) { - currentUser = users[0]; - } else { - currentUser = null; - } - } - broker.send("notifyUserChanged", { user: currentUser }); - return currentUser; -} - -export async function setupWorkflow( - context: ExtensionContext, - broker: ExtensionBrokerImpl -) { - broker.on("getInitialData", async () => { - // Firebase JSON and RC - readAndSendFirebaseConfigs(broker, context); - - // User login state - await fetchUsers(); - broker.send("notifyUsers", { users }); - currentUser = updateCurrentUser(users, broker, currentUser); - - // Project - if (currentOptions.rc?.projects?.default) { - broker.send("notifyProjectChanged", { - projectId: currentOptions.rc.projects.default, - }); - } - }); - - broker.on("logout", async ({ email }: { email: string }) => { - try { - await logoutUser(email); - const accounts = await getAccounts(); - users = accounts.map((account) => account.user); - broker.send("notifyUsers", { users }); - currentUser = updateCurrentUser(users, broker); - } catch (e) { - // ignored - } - }); - - broker.on("addUser", async () => { - const { user } = await login(); - users.push(user); - if (users) { - broker.send("notifyUsers", { users }); - currentUser = updateCurrentUser(users, broker, user); - } - }); - - broker.on( - "requestChangeUser", - ({ user: requestedUser }: { user: User | ServiceAccountUser }) => { - if (users.some((user) => user.email === requestedUser.email)) { - currentUser = requestedUser; - broker.send("notifyUserChanged", { user: currentUser }); - } - } - ); - - broker.on("selectProject", selectProject); - - context.subscriptions.push( - setupFirebaseJsonAndRcFileSystemWatcher(broker, context) - ); - - async function selectProject() { - let projectId; - const isServiceAccount = - (currentUser as ServiceAccountUser).type === "service_account"; - const email = currentUser.email; - if (process.env.MONOSPACE_ENV) { - pluginLogger.debug( - "selectProject: found MONOSPACE_ENV, " + - "prompting user using external flow" - ); - /** - * Monospace case: use Monospace flow - */ - const monospaceExtension = - vscode.extensions.getExtension("google.monospace"); - process.env.MONOSPACE_DAEMON_PORT = - monospaceExtension.exports.getMonospaceDaemonPort(); - try { - projectId = await selectProjectInMonospace({ - projectRoot: currentOptions.cwd, - project: undefined, - isVSCE: true, - }); - } catch (e) { - pluginLogger.error(e); - } - } else if (isServiceAccount) { - /** - * Non-Monospace service account case: get the service account's only - * linked project. - */ - pluginLogger.debug( - "selectProject: MONOSPACE_ENV not found, " + - " but service account found" - ); - const projects = (await listProjects()) as FirebaseProjectMetadata[]; - projectsUserMapping.set(email, projects); - // Service accounts should only have one project. - projectId = projects[0].projectId; - } else { - /** - * Default Firebase login case, let user choose from projects that - * Firebase login has access to. - */ - pluginLogger.debug( - "selectProject: no service account or MONOSPACE_ENV " + - "found, using firebase account to list projects" - ); - let projects = []; - if (projectsUserMapping.has(email)) { - pluginLogger.info(`using cached projects list for ${email}`); - projects = projectsUserMapping.get(email)!; - } else { - pluginLogger.info(`fetching projects list for ${email}`); - vscode.window.showQuickPick(["Loading...."]); - projects = (await listProjects()) as FirebaseProjectMetadata[]; - projectsUserMapping.set(email, projects); - } - try { - projectId = await promptUserForProject(projects); - } catch (e) { - vscode.window.showErrorMessage(e.message); - } - } - if (projectId) { - await updateFirebaseRCProject(context, "default", projectId); - broker.send("notifyProjectChanged", { projectId }); - } - } -}