Skip to content

Commit

Permalink
Add Sdk generation flow UI (#7539)
Browse files Browse the repository at this point in the history
* Add Sdk generation flow UI

* revert emulator change

* addres comments
  • Loading branch information
hlshen authored Aug 12, 2024
1 parent 9c88d7a commit 83154d9
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 5 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 @@ -96,6 +96,9 @@ export interface WebviewToExtensionParamsMap {
/** Deploy all connectors/services to production */
"fdc.deploy-all": void;

/** Configures generated SDK */
"fdc.configure-sdk": void;

// Initialize "result" tab.
getDataConnectResults: void;

Expand Down
5 changes: 5 additions & 0 deletions firebase-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
"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."
},
"firebase.dataConnect.skipToAppFolderSelect": {
"type": "boolean",
"default": false,
"description": "Skip app folder selection prompt. If false (default), trying to generate SDK config will open a confirmation modal."
}
}
},
Expand Down
3 changes: 3 additions & 0 deletions firebase-vscode/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export enum DATA_CONNECT_EVENT_NAME {
MOVE_TO_CONNECTOR = "move_to_connector",
START_EMULATOR_FROM_EXECUTION = "start_emulator_from_execution",
REFUSE_START_EMULATOR_FROM_EXECUTION = "refuse_start_emulator_from_execution",
INIT_SDK = "init_sdk",
INIT_SDK_CLI = "init_sdk_cli",
INIT_SDK_CODELENSE = "init_sdk_codelense",
}

export class AnalyticsLogger {
Expand Down
37 changes: 37 additions & 0 deletions firebase-vscode/src/data-connect/code-lens-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,40 @@ export class SchemaCodeLensProvider extends ComputedCodeLensProvider {
return codeLenses;
}
}
/**
* CodeLensProvider for Configure SDK in Connector.yaml
*/
export class ConfigureSdkCodeLensProvider extends ComputedCodeLensProvider {
provideCodeLenses(
document: vscode.TextDocument,
token: vscode.CancellationToken,
): vscode.CodeLens[] {
// Wait for configs to be loaded
const fdcConfigs = this.watch(dataConnectConfigs)?.tryReadValue;
const rc = this.watch(firebaseRC)?.tryReadValue;
if (!fdcConfigs || !rc) {
return [];
}

const codeLenses: vscode.CodeLens[] = [];
const range = new vscode.Range(0, 0, 0, 0);
const serviceConfig = fdcConfigs.findEnclosingServiceForPath(
document.fileName,
);
const connectorConfig = serviceConfig.findEnclosingConnectorForPath(
document.fileName,
);
if (serviceConfig) {
codeLenses.push(
new vscode.CodeLens(range, {
title: `$(tools) Configure Generated SDK`,
command: "fdc.connector.configure-sdk",
tooltip: "Configure a generated SDK for this connector",
arguments: [connectorConfig.tryReadValue],
}),
);
}

return codeLenses;
}
}
6 changes: 6 additions & 0 deletions firebase-vscode/src/data-connect/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ export class ResolvedDataConnectConfig {
return this.connectorDirs.map((connectorDir) => connectorDir.replace(".", this.relativePath));
}

findConnectorById(connectorId: string): ResolvedConnectorYaml {
return this.resolvedConnectors.find(
(connector) => connector.tryReadValue.value.connectorId === connectorId,
).tryReadValue;
}

containsPath(path: string) {
return isPathInside(path, this.path);
}
Expand Down
5 changes: 1 addition & 4 deletions firebase-vscode/src/data-connect/emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,12 @@ export class DataConnectEmulatorController implements vscode.Disposable {
// on schema reload, restart language server and run introspection again
private async schemaReload() {
vscode.commands.executeCommand("fdc-graphql.restart");
vscode.commands.executeCommand(
"firebase.dataConnect.executeIntrospection",
);
vscode.commands.executeCommand("firebase.dataConnect.executeIntrospection");
}

// Commands to run after the emulator is started successfully
private async connectToEmulator() {
const configs = dataConnectConfigs.value?.tryReadValue;

runEmulatorIssuesStream(
configs,
this.emulatorsController.getLocalEndpoint().value,
Expand Down
10 changes: 10 additions & 0 deletions firebase-vscode/src/data-connect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { registerExplorer } from "./explorer";
import { registerAdHoc } from "./ad-hoc-mutations";
import { DataConnectService as FdcService } from "./service";
import {
ConfigureSdkCodeLensProvider,
OperationCodeLensProvider,
SchemaCodeLensProvider,
} from "./code-lens-provider";
Expand All @@ -29,6 +30,7 @@ import { registerTerminalTasks } from "./terminal";
import { registerWebview } from "../webview";

import { DataConnectEmulatorController } from "./emulator";
import { registerFdcSdkGeneration } from "./sdk-generation";

class CodeActionsProvider implements vscode.CodeActionProvider {
constructor(
Expand Down Expand Up @@ -159,6 +161,7 @@ export function registerFdc(
fdcEmulatorsController,
);
const schemaCodeLensProvider = new SchemaCodeLensProvider(emulatorController);
const configureSdkCodeLensProvider = new ConfigureSdkCodeLensProvider();

// activate language client/serer
let client: LanguageClient;
Expand Down Expand Up @@ -222,6 +225,7 @@ export function registerFdc(
registerAdHoc(fdcService, telemetryLogger),
registerConnectors(context, broker, fdcService, telemetryLogger),
registerFdcDeploy(broker, telemetryLogger),
registerFdcSdkGeneration(broker, telemetryLogger),
registerTerminalTasks(broker, telemetryLogger),
operationCodeLensProvider,
vscode.languages.registerCodeLensProvider(
Expand All @@ -245,6 +249,12 @@ export function registerFdc(
],
schemaCodeLensProvider,
),
vscode.languages.registerCodeLensProvider(
[
{ scheme: "file", language: "yaml", pattern: "**/connector.yaml" },
],
configureSdkCodeLensProvider,
),
{
dispose: () => {
client.stop();
Expand Down
205 changes: 205 additions & 0 deletions firebase-vscode/src/data-connect/sdk-generation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import * as vscode from "vscode";
import { Uri } from "vscode";
import { firstWhere, firstWhereDefined } from "../utils/signal";
import { currentOptions } from "../options";
import { dataConnectConfigs, ResolvedConnectorYaml } from "./config";
import { runCommand } from "./terminal";
import { ExtensionBrokerImpl } from "../extension-broker";
import { DATA_CONNECT_EVENT_NAME } from "../analytics";
import {
getPlatformFromFolder,
generateSdkYaml,
} from "../../../src/dataconnect/fileUtils";
import { ConnectorYaml, Platform } from "../../../src/dataconnect/types";
import * as yaml from "yaml";
import * as fs from "fs-extra";
import path from "path";
export function registerFdcSdkGeneration(
broker: ExtensionBrokerImpl,
telemetryLogger: vscode.TelemetryLogger,
): vscode.Disposable {
const initSdkCmd = vscode.commands.registerCommand("fdc.init-sdk", () => {
telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.INIT_SDK_CLI);
runCommand("firebase init dataconnect:sdk");
});

// codelense from inside connector.yaml file
const configureSDKCodelense = vscode.commands.registerCommand(
"fdc.connector.configure-sdk",
async (connectorConfig) => {
telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.INIT_SDK_CODELENSE);
const configs = await firstWhereDefined(dataConnectConfigs).then(
(c) => c.requireValue,
);
await openAndWriteYaml(connectorConfig);
},
);

// Sidebar "configure generated sdk" button
const configureSDK = vscode.commands.registerCommand(
"fdc.configure-sdk",
async () => {
telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.INIT_SDK);
const configs = await firstWhereDefined(dataConnectConfigs).then(
(c) => c.requireValue,
);

// pick service, auto pick if only one
const pickedService =
configs.serviceIds.length === 1
? configs.serviceIds[0]
: await pickService(configs.serviceIds);
const serviceConfig = configs.findById(pickedService);
const connectorIds = serviceConfig?.connectorIds;

// pick connector for service, auto pick if only one
const pickedConnectorId =
connectorIds.length === 1
? connectorIds[0]
: await pickConnector(connectorIds);
const connectorConfig =
serviceConfig.findConnectorById(pickedConnectorId);

await openAndWriteYaml(connectorConfig);
},
);

async function openAndWriteYaml(connectorConfig: ResolvedConnectorYaml) {
const connectorYamlPath = Uri.joinPath(
Uri.file(connectorConfig.path),
"connector.yaml",
);
const connectorYaml = connectorConfig.value;

// open connector.yaml file
await vscode.window.showTextDocument(connectorYamlPath);

const appFolder = await selectAppFolder();
if (appFolder) {
await writeYaml(
appFolder,
connectorYamlPath,
connectorConfig.path, // needed for relative path comparison
connectorYaml,
);
}
}

async function selectAppFolder() {
// confirmation prompt for selecting app folder; skip if configured to skip
const configs = vscode.workspace.getConfiguration("firebase.dataConnect");
const skipToAppFolderSelect = "skipToAppFolderSelect";
if (!configs.get(skipToAppFolderSelect)) {
const result = await vscode.window.showInformationMessage(
"Please select your app folder to generate an SDK for.",
{ modal: true },
"Yes",
"Don't show again",
);
if (result !== "Yes" && result !== "Don't show again") {
return;
}
if (result === "Don't show again") {
configs.update(
skipToAppFolderSelect,
true,
vscode.ConfigurationTarget.Global,
);
}
}

// open app folder selector
const folderUris: Uri[] = await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
title: "Select your app folder to link Data Connect to:",
openLabel: "Select app folder",
});

return folderUris[0].fsPath; // can only pick one folder, but return type is an array
}

async function writeYaml(
appFolder: string,
connectorYamlPath: Uri,
connectorYamlFolderPath: string,
connectorYaml: ConnectorYaml,
) {
const platform = await getPlatformFromFolder(appFolder);
// if app platform undetermined, run init command
if (platform === Platform.UNDETERMINED) {
vscode.window.showErrorMessage(
"Could not determine platform for specified app folder. Configuring from command line.",
);
vscode.commands.executeCommand("fdc.init-sdk");
} else {
// generate yaml
const newConnectorYaml = generateSdkYaml(
platform,
connectorYaml,
connectorYamlFolderPath,
appFolder,
);
const connectorYamlContents = yaml.stringify(newConnectorYaml);
fs.writeFileSync(connectorYamlPath.fsPath, connectorYamlContents, "utf8");
}
}

const configureSDKSub = broker.on("fdc.configure-sdk", async () =>
vscode.commands.executeCommand("fdc.configure-sdk"),
);
return vscode.Disposable.from(
initSdkCmd,
configureSDK,
configureSDKCodelense,
{
dispose: configureSDKSub,
},
);
}

async function pickService(serviceIds: string[]): Promise<string | undefined> {
const options = firstWhere(
currentOptions,
(options) => options.project?.length !== 0,
).then((options) => {
return serviceIds.map((serviceId) => {
return {
label: serviceId,
options,
picked: true,
};
});
});

const picked = await vscode.window.showQuickPick(options, {
title: "Select service",
canPickMany: false,
});

return picked.label;
}

async function pickConnector(
connectorIds: string[] | undefined,
): Promise<string | undefined> {
const options = firstWhere(
currentOptions,
(options) => options.project?.length !== 0,
).then((options) => {
return connectorIds?.map((connectorId) => {
return {
label: connectorId,
options,
picked: true,
};
});
});

const picked = await vscode.window.showQuickPick(options, {
title: `Select connector to generate SDK for.`,
canPickMany: false,
});

return picked.label;
}
16 changes: 15 additions & 1 deletion firebase-vscode/webviews/data-connect/data-connect.entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ function DataConnect() {
<PanelSection title="Local Development">
{!isConnectedToPostgres && (
<p>
Connect to Local PostgreSQL. See also:{" "}
Connect to Local PostgreSQL.
<br></br>
See also:{" "}
<a href="https://firebase.google.com/docs/data-connect/quickstart#optional_install_postgresql_locally">
Working with PostgreSQL
</a>
Expand All @@ -49,6 +51,18 @@ function DataConnect() {
Connect to Local PostgreSQL
</VSCodeButton>
)}
<Spacer size="xlarge" />
<p>
Configure a generated SDK.
<br></br>
See also:{" "}
<a href="https://firebase.google.com/docs/data-connect/gp/web-sdk">
Working with generated SDKs
</a>
</p>
<VSCodeButton onClick={() => broker.send("fdc.configure-sdk")}>
Configure Generated SDK
</VSCodeButton>
</PanelSection>
<PanelSection title="Production" isLast={true}>
<p>
Expand Down

0 comments on commit 83154d9

Please sign in to comment.