Skip to content

Commit

Permalink
Merge pull request #838 from FirebasePrivate/firemat.production-codel…
Browse files Browse the repository at this point in the history
…ense

Enable running codelenses in production
  • Loading branch information
hlshen committed Jan 31, 2024
2 parents 081591e + 5a15fda commit f96d588
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 56 deletions.
3 changes: 3 additions & 0 deletions firebase-vscode/common/messaging/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ export interface WebviewToExtensionParamsMap {
chooseQuickstartDir: {};

notifyAuthUserMockChange: UserMock;

/** Opens the "connect to instance" picker */
connectToInstance: void;
}

export interface FirematResults {
Expand Down
12 changes: 11 additions & 1 deletion firebase-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
"type": "boolean",
"default": true,
"description": "Enable web frameworks"
},
"firebase.firemat.alwaysAllowMutationsInProduction": {
"type": "boolean",
"default": false,
"description": "Always allow mutations in production. If false (default), trying to run a mutation in production will open a confirmation modal."
}
}
},
Expand Down Expand Up @@ -92,9 +97,14 @@
"id": "sidebar",
"name": "Firebase"
},
{
"type": "webview",
"id": "firemat",
"name": "Firebase Data Connect"
},
{
"id": "firebase.firemat.explorerView",
"name": "Firemat Explorer"
"name": "FDC Explorer"
}
],
"firebase-firemat-execution-view": [
Expand Down
106 changes: 63 additions & 43 deletions firebase-vscode/src/core/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function registerProject({
context: ExtensionContext;
broker: ExtensionBrokerImpl;
}): Disposable {
effect(async () => {
const effect1 = effect(async () => {
const user = currentUser.value;
if (user) {
pluginLogger.info("(Core:Project) New user detected, fetching projects");
Expand All @@ -60,68 +60,88 @@ export function registerProject({
}
});

effect(() => {
const effect2 = effect(() => {
broker.send("notifyProjectChanged", {
projectId: currentProject.value?.projectId ?? "",
});
});

// Update .firebaserc with defined project ID
effect(() => {
const effect3 = effect(() => {
const projectId = currentProjectId.value;
if (projectId) {
updateFirebaseRCProject(context, "default", currentProjectId.value);
}
});

broker.on("getInitialData", () => {
// Initialize currentProjectId to default project ID
const effect4 = effect(() => {
if (!currentProjectId.value) {
currentProjectId.value = firebaseRC.value?.projects.default;
}
});

const onGetInitialData = broker.on("getInitialData", () => {
broker.send("notifyProjectChanged", {
projectId: currentProject.value?.projectId ?? "",
});
});

broker.on("selectProject", async () => {
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.value.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,
const selectProjectCommand = vscode.commands.registerCommand(
"firebase.selectProject",
async () => {
if (process.env.MONOSPACE_ENV) {
pluginLogger.debug(
"selectProject: found MONOSPACE_ENV, " +
"prompting user using external flow",
);
} catch (e) {
vscode.window.showErrorMessage(e.message);
/**
* 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.value.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);
}
}
}
});
},
);

const onSelectProject = broker.on("selectProject", () =>
vscode.commands.executeCommand("firebase.selectProject"),
);

return {
dispose() {},
};
return vscode.Disposable.from(
selectProjectCommand,
{ dispose: onGetInitialData },
{ dispose: onSelectProject },
{ dispose: effect1 },
{ dispose: effect2 },
{ dispose: effect3 },
{ dispose: effect4 },
);
}

/** Get the user to select a project */
Expand Down
27 changes: 17 additions & 10 deletions firebase-vscode/src/firemat/code-lens-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { OperationLocation } from "./types";
import { Disposable } from "vscode";

import { isFirematEmulatorRunning } from "../core/emulators";
import { Signal, computed } from "@preact/signals-core";
import { Signal } from "@preact/signals-core";
import { firematConfig } from "../core/config";
import path from "path";
import { selectedInstance } from "./connect-instance";

abstract class ComputedCodeLensProvider implements vscode.CodeLensProvider {
private readonly _onChangeCodeLensesEmitter = new vscode.EventEmitter<void>();
Expand Down Expand Up @@ -86,25 +87,31 @@ export class OperationCodeLensProvider extends ComputedCodeLensProvider {
position: position,
};
const opKind = x.operation as string; // query or mutation
const schemaPath = configs.schema.main.source;

if (isPathInside(document.fileName, schemaPath)) {
const isInSchemaFolder = isPathInside(
document.fileName,
configs.schema.main.source,
);
const connectorPaths = Object.keys(configs.operationSet).map(
(key) => configs.operationSet[key]!.source,
);
const isInOperationFolder = connectorPaths.every(
(path) => !isPathInside(document.fileName, path),
);
const instance = this.watch(selectedInstance);

if (instance && (isInSchemaFolder || isInOperationFolder)) {
codeLenses.push(
new vscode.CodeLens(range, {
title: `$(play) Execute ${opKind}`,
title: `$(play) Run (${instance})`,
command: "firebase.firemat.executeOperation",
tooltip: "Execute the operation (⌘+enter or Ctrl+Enter)",
arguments: [x, operationLocation],
}),
);
}

const connectorPaths = Object.keys(configs.operationSet).map(
(key) => configs.operationSet[key]!.source,
);
if (
connectorPaths.every((path) => !isPathInside(document.fileName, path))
) {
if (isInOperationFolder) {
codeLenses.push(
new vscode.CodeLens(range, {
title: `$(plug) Move to connector`,
Expand Down
82 changes: 82 additions & 0 deletions firebase-vscode/src/firemat/connect-instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as vscode from "vscode";
import { registerWebview } from "../webview";
import { ExtensionBrokerImpl } from "../extension-broker";
import { isFirematEmulatorRunning } from "../core/emulators";
import { computed, effect, signal } from "@preact/signals-core";

export const selectedInstance = signal<string | undefined>(undefined);

export function registerFirebaseDataConnectView(
context: vscode.ExtensionContext,
broker: ExtensionBrokerImpl,
): vscode.Disposable {
const instanceOptions = computed(() => {
// Some fake options
const options = ["asia-east1", "europe-north1", "wonderland2"];

// We start with the emulator option
const emulator = "emulator";

// TODO refactor "start emulator" logic to enable the picker to start emulators
if (isFirematEmulatorRunning.value) {
options.splice(0, 0, emulator);
}

return options;
});

const selectedInstanceStatus = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
);
selectedInstanceStatus.tooltip = "Select a Firebase instance";
selectedInstanceStatus.command = "firebase.firemat.connectToInstance";

function syncStatusBarWithSelectedInstance() {
return effect(() => {
selectedInstanceStatus.text = selectedInstance.value ?? "<No instance>";
selectedInstanceStatus.show();
});
}

// Handle cases where the emulator is the currently selected instance,
// and the emulator is stopped.
// This also initializes the selectedInstance value to the first instance.
function initializeSelectedInstance() {
return effect(() => {
const isSelectedInstanceInOptions = instanceOptions.value?.includes(
selectedInstance.value,
);

if (!isSelectedInstanceInOptions) {
selectedInstance.value = instanceOptions.value?.[0];
}
});
}

return vscode.Disposable.from(
vscode.commands.registerCommand(
"firebase.firemat.connectToInstance",
async () => {
const selected = await vscode.window.showQuickPick(
instanceOptions.value,
);
if (!selected) {
return;
}

selectedInstance.value = selected;
},
),

selectedInstanceStatus,
{ dispose: syncStatusBarWithSelectedInstance() },
{ dispose: initializeSelectedInstance() },
{
dispose: broker.on("connectToInstance", async () => {
vscode.commands.executeCommand("firebase.firemat.connectToInstance");
}),
},

registerWebview({ name: "firemat", context, broker }),
);
}
37 changes: 35 additions & 2 deletions firebase-vscode/src/firemat/execution.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import vscode, { Disposable, ExtensionContext } from "vscode";
import vscode, {
ConfigurationTarget,
Disposable,
ExtensionContext,
} from "vscode";
import { ExtensionBrokerImpl } from "../extension-broker";
import { registerWebview } from "../webview";
import { ExecutionHistoryTreeDataProvider } from "./execution-history-provider";
Expand All @@ -13,10 +17,11 @@ import {
updateExecution,
} from "./execution-store";
import { batch, effect } from "@preact/signals-core";
import { OperationDefinitionNode, print } from "graphql";
import { OperationDefinitionNode, OperationTypeNode, print } from "graphql";
import { FirematService } from "./service";
import { FirematError, toSerializedError } from "../../common/error";
import { OperationLocation } from "./types";
import { selectedInstance } from "./connect-instance";

export function registerExecution(
context: ExtensionContext,
Expand Down Expand Up @@ -60,6 +65,34 @@ export function registerExecution(
ast: OperationDefinitionNode,
{ document, documentPath, position }: OperationLocation,
) {
const configs = vscode.workspace.getConfiguration("firebase.firemat");
const alwaysSettingsKey = "alwaysAllowMutationsInProduction";

// Warn against using mutations in production.
if (
selectedInstance.value !== "emulator" &&
!configs.get(alwaysSettingsKey) &&
ast.operation === OperationTypeNode.MUTATION
) {
const always = "Yes (always)";
const yes = "Yes";
const result = await vscode.window.showWarningMessage(
"You are about to perform a mutation in production environment. Are you sure?",
{ modal: true },
yes,
always,
);

if (result !== always && result !== yes) {
return;
}

// If the user selects "always", we update User settings.
if (result === always) {
configs.update(alwaysSettingsKey, true, ConfigurationTarget.Global);
}
}

const item = createExecution({
label: ast.name?.value ?? "anonymous",
timestamp: Date.now(),
Expand Down

0 comments on commit f96d588

Please sign in to comment.