Skip to content

Commit

Permalink
feat(sdk): embedding cli opens the metabase store to get trial token …
Browse files Browse the repository at this point in the history
…and applies the license (metabase#46810)

* open metabase store to get trial token

* remove license env from instance setup as we provide the license key later

* activate metabase license

* fix password field missing

* fix formatting for generated component files message

* activate license key

* add missing auth options for postgres

* add function to print with padding

* prevent infinite loop and update section representation

* show the activate license error in red

* rename variable
  • Loading branch information
heypoom authored Aug 21, 2024
1 parent 1b9a267 commit 4453abb
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export const CLI_SHOWN_DB_FIELDS = [
"password",
"ssl",

// PostgreSQL fields - support authentication providers
"use-auth-provider",
"auth-provider",
"azure-managed-identity-client-id",
"oauth-token-url",
"oauth-token-headers",

// Snowflake fields
"use-hostname",
"account",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export const EMBEDDING_FAILED_MESSAGE = `
export const PREMIUM_TOKEN_REQUIRED_MESSAGE =
" Don't forget to add your premium token to your Metabase instance in the admin settings! The embedding demo will not work without a license.";

export const getGeneratedComponentFilesMessage = (path: string) => `
Generated example React components files in "${path}".
You can import the <AnalyticsPage /> component in your React app.
`;

export const getMetabaseInstanceSetupCompleteMessage = (instanceUrl: string) =>
// eslint-disable-next-line no-unconditional-metabase-links-render -- link for the CLI message
`
Expand Down
4 changes: 2 additions & 2 deletions enterprise/frontend/src/embedding-sdk/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
} from "./constants/messages";
import {
addDatabaseConnectionStep,
addEmbeddingToken,
checkIfReactProject,
checkIsDockerRunning,
checkSdkAvailable,
Expand All @@ -16,6 +15,7 @@ import {
generateReactComponentFiles,
pickDatabaseTables,
pollMetabaseInstance,
setupLicense,
setupMetabaseInstance,
showMetabaseCliTitle,
startLocalMetabaseContainer,
Expand All @@ -27,7 +27,6 @@ export const CLI_STEPS = [
{ id: "showMetabaseCliTitle", executeStep: showMetabaseCliTitle },
{ id: "checkIfReactProject", executeStep: checkIfReactProject },
{ id: "checkSdkAvailable", executeStep: checkSdkAvailable },
{ id: "addEmbeddingToken", executeStep: addEmbeddingToken },
{ id: "checkIsDockerRunning", executeStep: checkIsDockerRunning },
{ id: "generateCredentials", executeStep: generateCredentials },
{
Expand All @@ -44,6 +43,7 @@ export const CLI_STEPS = [
id: "generateReactComponentFiles",
executeStep: generateReactComponentFiles,
},
{ id: "setupLicense", executeStep: setupLicense },
] as const;

export async function runCli() {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from "fs/promises";

import { input } from "@inquirer/prompts";

import { getGeneratedComponentFilesMessage } from "../constants/messages";
import { ANALYTICS_CSS_SNIPPET } from "../snippets/analytics-css-snippet";
import type { CliStepMethod } from "../types/cli";
import { getComponentSnippets } from "../utils/get-component-snippets";
Expand Down Expand Up @@ -59,9 +60,7 @@ export const generateReactComponentFiles: CliStepMethod = async state => {

await fs.writeFile(`${path}/index.js`, exportIndexContent);

printSuccess(
`Generated example React components files in "${path}". You can import the <AnalyticsPage /> component in your React app from this path.`,
);
printSuccess(getGeneratedComponentFilesMessage(path));

return [{ type: "done" }, state];
};
2 changes: 1 addition & 1 deletion enterprise/frontend/src/embedding-sdk/cli/steps/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./add-embedding-token";
export * from "./check-docker-running";
export * from "./create-api-key";
export * from "./generate-credentials";
Expand All @@ -12,3 +11,4 @@ export * from "./add-database-connection";
export * from "./pick-database-tables";
export * from "./create-models-and-xrays";
export * from "./generate-component-files";
export * from "./setup-license";
91 changes: 91 additions & 0 deletions enterprise/frontend/src/embedding-sdk/cli/steps/setup-license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { input, select } from "@inquirer/prompts";
import chalk from "chalk";
import toggle from "inquirer-toggle";
import open from "open";

import type { CliStepMethod } from "embedding-sdk/cli/types/cli";

import { printEmptyLines, printInfo, printWithPadding } from "../utils/print";
import { propagateErrorResponse } from "../utils/propagate-error-response";

const trialUrl = `https://store.metabase.com/checkout?plan=pro&deployment=self-hosted`;
const trialUrlWithUtm = `${trialUrl}&utm_source=product&utm_medium=checkout&utm_campaign=embedding-sdk&utm_content=embedding-sdk-cli`;

export const setupLicense: CliStepMethod = async state => {
const hasLicenseKey = await toggle({
message: "Do you already have a Metabase Pro license key?",
default: false,
});

if (!hasLicenseKey) {
printEmptyLines(1);
printWithPadding(
`Please sign up for a free trial of Metabase Pro self-hosted or purchase a license.`,
);

const shouldOpenInBrowser = await toggle({
message: `Open the store to get a license key?`,
default: true,
});

if (shouldOpenInBrowser) {
try {
await open(trialUrlWithUtm);
printWithPadding(`Opened ${chalk.blue(trialUrl)} in your browser.`);
} catch (error) {
printInfo(`Please visit ${chalk.blue(trialUrl)} to get a license key.`);
}
}
}

// Activate the license
// eslint-disable-next-line no-constant-condition -- ask until user provides a valid license key
while (true) {
try {
const token = await input({
message: "Enter your Metabase Pro license key:",
required: true,
validate: value => {
if (value.length !== 64 || !/^[0-9A-Fa-f]+$/.test(value)) {
return "License key must be a 64-character hexadecimal string.";
}

return true;
},
});

const endpoint = `${state.instanceUrl}/api/setting/premium-embedding-token`;

const res = await fetch(endpoint, {
method: "PUT",
body: JSON.stringify({ value: token.trim() }),
headers: {
"content-type": "application/json",
cookie: state.cookie ?? "",
},
});

await propagateErrorResponse(res);

return [{ type: "success" }, { ...state, token }];
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);

printWithPadding(
chalk.red(`Failed to activate license. Reason: ${reason}`),
);

const skipLicenseSetup = await select({
message: `Do you want to try another license key?`,
choices: [
{ name: "Try another license key.", value: false },
{ name: "I'll activate the license later.", value: true },
],
});

if (skipLicenseSetup) {
return [{ type: "success" }, state];
}
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@ import toggle from "inquirer-toggle";
import ora from "ora";
import { promisify } from "util";

import type { CliOutput, CliStepMethod } from "embedding-sdk/cli/types/cli";
import {
OUTPUT_STYLES,
printEmptyLines,
printInfo,
} from "embedding-sdk/cli/utils/print";

import { CONTAINER_NAME, SITE_NAME } from "../constants/config";
import { EMBEDDING_DEMO_SETUP_TOKEN } from "../constants/env";
import {
EMBEDDING_FAILED_MESSAGE,
INSTANCE_CONFIGURED_MESSAGE,
} from "../constants/messages";
import type { CliOutput, CliStepMethod } from "../types/cli";
import { OUTPUT_STYLES, printEmptyLines } from "../utils/print";
import { retry } from "../utils/retry";

const exec = promisify(execCallback);
Expand All @@ -38,10 +33,9 @@ export const setupMetabaseInstance: CliStepMethod = async state => {
// If the instance we are configuring is not clean,
// therefore we cannot ensure the setup steps are performed.
const onInstanceConfigured = async (): Promise<CliOutput> => {
spinner.fail();
printEmptyLines();
printInfo(
"The instance is already configured. Do you want to delete the container and start over?",
console.log(
" The instance is already configured. Delete the container and start over?",
);
const shouldRestartSetup = await toggle({
message: `${OUTPUT_STYLES.error("WARNING: This will delete all data.")}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,6 @@ export const startLocalMetabaseContainer: CliStepMethod = async state => {
...METABASE_INSTANCE_DEFAULT_ENVS,
};

if (state.token) {
envVars.MB_PREMIUM_EMBEDDING_TOKEN = state.token;
}

// Pass default configuration as environment variables
const envFlags = Object.entries(envVars)
.map(([key, value]) => `-e ${key}='${value}'`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type { Engine, EngineField } from "metabase-types/api";

import { CLI_SHOWN_DB_FIELDS } from "../constants/database";

import { printWithPadding } from "./print";

interface Options {
engine: Engine;
engineKey: string;
Expand All @@ -33,13 +35,26 @@ export async function askForDatabaseConnectionInfo(options: Options) {
continue;
}

const visibleIf = field["visible-if"];

const shouldShowField =
!visibleIf ||
Object.entries(visibleIf).every(
([key, expected]) => connection[key] === expected,
);

// Skip fields that should be hidden
if (!shouldShowField) {
continue;
}

const name = field["display-name"];
const helperText = field["helper-text"];

const message = `${name}:`;

if (helperText) {
console.log(` ${chalk.gray(helperText)}`);
printWithPadding(`${chalk.gray(helperText)}`);
}

const value = await askForConnectionValue(field, message, engineKey);
Expand Down Expand Up @@ -72,6 +87,13 @@ const askForConnectionValue = (

return fs.readFile(path, "utf-8");
})
.with("select", () => {
return select({
message,
choices: field.options ?? [],
default: field.default,
});
})
.with("section", () => askSectionChoice(field))
.otherwise(() =>
input({
Expand All @@ -94,29 +116,42 @@ const getIntegerFieldDefault = (field: EngineField, engine: string) => {
};

const askSectionChoice = async (field: EngineField) => {
// Snowflake allows to connect with either hostname or account name.
if (field.name === "use-hostname") {
// Postgres allows connecting with either password or an authentication provider.
if (field.name === "use-auth-provider") {
const choice = await select({
message: "Do you want to connect with hostname or account name?",
message:
"Do you want to connect with password or an authentication provider?",
choices: [
{ name: "Hostname", value: "hostname" },
{ name: "Account name", value: "account" },
{ name: "Password", value: "password" },
{ name: "Auth Provider", value: "auth-provider" },
],
default: "password",
});

return choice === "hostname";
return choice === "auth-provider";
}

// MongoDB allows to connect with either hostname or connection string.
// Snowflake allows connecting with either hostname or account name.
if (field.name === "use-hostname") {
return select({
message: "Do you want to connect with hostname or account name?",
choices: [
{ name: "Hostname", value: true },
{ name: "Account name", value: false },
],
default: field.default,
});
}

// MongoDB allows connecting with either hostname or connection string.
if (field.name === "use-conn-uri") {
const choice = await select({
return select({
message: "Do you want to connect with hostname or connection string?",
choices: [
{ name: "Hostname", value: "hostname" },
{ name: "Connection String", value: "conn-uri" },
{ name: "Hostname", value: false },
{ name: "Connection String", value: true },
],
default: field.default,
});

return choice === "conn-uri";
}
};
4 changes: 4 additions & 0 deletions enterprise/frontend/src/embedding-sdk/cli/utils/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ export const printSuccess = (message: string) =>

export const printInfo = (message: string) =>
_print(OUTPUT_STYLES.info, message);

/** Aligns the message with the rest of the input prompt. */
export const printWithPadding = (message: string) =>
console.log(` ` + message);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@
"mysql2": "^3.9.8",
"node-polyfill-webpack-plugin": "2.0.1",
"null-loader": "^4.0.1",
"open": "^10.1.0",
"open-cli": "^8.0.0",
"patch-package": "^8.0.0",
"pg": "^8.8.0",
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17812,7 +17812,7 @@ open-cli@^8.0.0:
open "^10.0.0"
tempy "^3.1.0"

open@^10.0.0:
open@^10.0.0, open@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/open/-/open-10.1.0.tgz#a7795e6e5d519abe4286d9937bb24b51122598e1"
integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==
Expand Down

0 comments on commit 4453abb

Please sign in to comment.