diff --git a/enterprise/frontend/src/embedding-sdk/README.md b/enterprise/frontend/src/embedding-sdk/README.md index f64f9aab89415cb9f319368cd464fc632b304d8a..cd9205fc42942e34b4bb8743ca20df92e213298f 100644 --- a/enterprise/frontend/src/embedding-sdk/README.md +++ b/enterprise/frontend/src/embedding-sdk/README.md @@ -46,6 +46,17 @@ Features not yet supported: # Getting started +## Quickstart + +Run `npx @metabase/embedding-sdk-react start` in your React project to get started. + +This will start a local Metabase instance with Docker and generate sample dashboard components from your data. First run of this command will take 1 - 2 minutes to install the dependencies. + +Prerequisites: + +- [Node.js 18.x LTS](https://nodejs.org/en) or higher +- [Docker](https://docker.com) + ## Start Metabase Currently, the SDK only works with Metabase version 50. diff --git a/enterprise/frontend/src/embedding-sdk/cli/actions/start.ts b/enterprise/frontend/src/embedding-sdk/cli/actions/start.ts index 79d7e84c57a9fc8b04685f2205d904c76e23bc9a..bce8ffbf5ebe11504a13f90024255fb4e9ee50ca 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/actions/start.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/actions/start.ts @@ -2,6 +2,9 @@ import { runCli } from "../run"; import { printError } from "../utils/print"; export async function start() { + // When the user runs the CLI with npx, there will be some deprecation warnings that we should clear. + console.clear(); + try { await runCli(); } catch (error) { diff --git a/enterprise/frontend/src/embedding-sdk/cli/constants/code-sample.ts b/enterprise/frontend/src/embedding-sdk/cli/constants/code-sample.ts deleted file mode 100644 index f1e9710f7c46ffca7e87e3bb40e8c027d24f4431..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/embedding-sdk/cli/constants/code-sample.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SDK_PACKAGE_NAME } from "embedding-sdk/cli/constants/config"; - -export const getCodeSample = (url: string, apiKey: string) => ` -import { - MetabaseProvider, - InteractiveDashboard, -} from '${SDK_PACKAGE_NAME}' - -/** @type {import('${SDK_PACKAGE_NAME}').SDKConfig} */ -const config = { - metabaseInstanceUrl: \`${url}\`, - apiKey: '${apiKey}' -} - -export const Analytics = () => ( - <MetabaseProvider config={config}> - <InteractiveDashboard dashboardId={1} /> - </MetabaseProvider> -) -`; diff --git a/enterprise/frontend/src/embedding-sdk/cli/constants/messages.ts b/enterprise/frontend/src/embedding-sdk/cli/constants/messages.ts index 6e8dcb4a5fb68dfcf9788a8ef245389f0993c3b5..8e17b384fa7243d6e6f257eb4222cd7400380021 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/constants/messages.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/constants/messages.ts @@ -27,13 +27,19 @@ export const EMBEDDING_FAILED_MESSAGE = ` ${DELETE_CONTAINER_MESSAGE} `; -// eslint-disable-next-line no-unconditional-metabase-links-render -- link for the CLI message -export const METABASE_INSTANCE_SETUP_COMPLETE_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 getMetabaseInstanceSetupCompleteMessage = (instanceUrl: string) => + // eslint-disable-next-line no-unconditional-metabase-links-render -- link for the CLI message + ` Metabase instance is ready for embedding. + Go to ${instanceUrl} to start using Metabase. + You can find your login credentials at METABASE_LOGIN.json Don't forget to put this file in your .gitignore. - Metabase will phone home some data collected via Google Analytics and Snowplow. + Metabase will phone home some data collected via Snowplow. We don’t collect any usernames, emails, server IPs, database details of any kind, or any personally identifiable information (PII). diff --git a/enterprise/frontend/src/embedding-sdk/cli/run.ts b/enterprise/frontend/src/embedding-sdk/cli/run.ts index 49c40cf86ffb67f00c02b5dca634c7a712cf9eed..dfc7cbf4557420c9b85958a057011a865cf234f1 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/run.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/run.ts @@ -1,10 +1,14 @@ -import { METABASE_INSTANCE_SETUP_COMPLETE_MESSAGE } from "./constants/messages"; +import chalk from "chalk"; + +import { + PREMIUM_TOKEN_REQUIRED_MESSAGE, + getMetabaseInstanceSetupCompleteMessage, +} from "./constants/messages"; import { addEmbeddingToken, checkIsDockerRunning, createApiKey, generateCredentials, - generateCodeSample, pollMetabaseInstance, setupMetabaseInstance, showMetabaseCliTitle, @@ -14,6 +18,7 @@ import { addDatabaseConnectionStep, pickDatabaseTables, createModelsAndXrays, + generateReactComponentFiles, } from "./steps"; import type { CliState } from "./types/cli"; import { printEmptyLines, printInfo } from "./utils/print"; @@ -35,7 +40,10 @@ export const CLI_STEPS = [ { id: "addDatabaseConnection", executeStep: addDatabaseConnectionStep }, { id: "pickDatabaseTables", executeStep: pickDatabaseTables }, { id: "createModelsAndXrays", executeStep: createModelsAndXrays }, - { id: "generateCodeSample", executeStep: generateCodeSample }, + { + id: "generateReactComponentFiles", + executeStep: generateReactComponentFiles, + }, ] as const; export async function runCli() { @@ -57,12 +65,10 @@ export async function runCli() { state = nextState; } - console.log(METABASE_INSTANCE_SETUP_COMPLETE_MESSAGE); + console.log(getMetabaseInstanceSetupCompleteMessage(state.instanceUrl ?? "")); if (!state.token) { - console.log( - " Don't forget to add your premium token to your Metabase instance in the admin settings!", - ); + console.log(chalk.bold(PREMIUM_TOKEN_REQUIRED_MESSAGE)); } printEmptyLines(1); diff --git a/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-css-snippet.ts b/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-css-snippet.ts new file mode 100644 index 0000000000000000000000000000000000000000..d984f90438318a194a0b21952b04248cf2aa0c97 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-css-snippet.ts @@ -0,0 +1,49 @@ +export const ANALYTICS_CSS_SNIPPET = ` +.theme-switcher { + width: 28px; + height: 28px; + cursor: pointer; +} + +.analytics-container { + width: 100%; + max-width: 1000px; + margin: 0 auto; + min-height: 100vh; + padding: 30px 0; +} + +.analytics-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 30px 0; + column-gap: 15px; +} + +.analytics-header-right { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 30px 0; + column-gap: 15px; +} + +.analytics-header-right > a { + color: #509EE3; +} + +.dashboard-select { + background: transparent; + color: #509EE3; + border: none; + font-family: inherit; + font-size: 14px; + cursor: pointer; +} + +.dashboard-select:focus { + outline: 1px solid #509EE3; + border-radius: 2px; +} +`.trim(); diff --git a/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-dashboard-snippet.ts b/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-dashboard-snippet.ts new file mode 100644 index 0000000000000000000000000000000000000000..083d7550021384574467e422fc9eae87df663052 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-dashboard-snippet.ts @@ -0,0 +1,50 @@ +import { SDK_PACKAGE_NAME } from "../constants/config"; +import type { DashboardInfo } from "../types/dashboard"; + +export const getAnalyticsDashboardSnippet = ( + instanceUrl: string, + dashboards: DashboardInfo[], +) => ` +import { useState } from 'react' +import { InteractiveDashboard } from '${SDK_PACKAGE_NAME}' + +import { ThemeSwitcher } from './theme-switcher' + +export const AnalyticsDashboard = () => { + const [dashboardId, setDashboardId] = useState(DASHBOARDS[0].id) + + const editLink = \`${instanceUrl}/dashboard/\${dashboardId}\` + + return ( + <div className="analytics-container"> + <div className="analytics-header"> + <div> + <select + className="dashboard-select" + onChange={(e) => setDashboardId(e.target.value)} + > + {DASHBOARDS.map((dashboard) => ( + <option key={dashboard.id} value={dashboard.id}> + {dashboard.name} + </option> + ))} + </select> + </div> + + <div className="analytics-header-right"> + {/** TODO: Remove. This is just a link to edit the dashboard in Metabase for your convenience. */} + <a href={editLink} target="_blank"> + Edit this dashboard + </a> + + <ThemeSwitcher /> + </div> + </div> + + <InteractiveDashboard dashboardId={dashboardId} withTitle withDownloads /> + </div> + ) +} + +const DASHBOARDS = ${JSON.stringify(dashboards, null, 2)} +`; diff --git a/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-page-snippet.ts b/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-page-snippet.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4753a32cf59586fbd3d3841f8bbf60e862645f1 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-page-snippet.ts @@ -0,0 +1,18 @@ +/** + * A minimal setup that brings together SDK provider, + * theme switcher, and a sample dashboard. + */ +export const ANALYTICS_PAGE_SNIPPET = ` +import { AnalyticsDashboard } from './analytics-dashboard' +import { MetabaseEmbedProvider, SampleThemeProvider } from './metabase-provider' + +import './analytics.css' + +export const AnalyticsPage = () => ( + <SampleThemeProvider> + <MetabaseEmbedProvider> + <AnalyticsDashboard /> + </MetabaseEmbedProvider> + </SampleThemeProvider> +) +`; diff --git a/enterprise/frontend/src/embedding-sdk/cli/snippets/index.ts b/enterprise/frontend/src/embedding-sdk/cli/snippets/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..65b4b0e141c9a268308e663ce2b00bc9fbc6b28b --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/snippets/index.ts @@ -0,0 +1,4 @@ +export * from "./analytics-dashboard-snippet"; +export * from "./metabase-provider-snippet"; +export * from "./analytics-page-snippet"; +export * from "./theme-switcher-snippet"; diff --git a/enterprise/frontend/src/embedding-sdk/cli/snippets/metabase-provider-snippet.ts b/enterprise/frontend/src/embedding-sdk/cli/snippets/metabase-provider-snippet.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f587e181cd48afd585666faac5f7e6f53bff854 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/snippets/metabase-provider-snippet.ts @@ -0,0 +1,75 @@ +import { SDK_PACKAGE_NAME } from "embedding-sdk/cli/constants/config"; + +export const getMetabaseProviderSnippet = (url: string, apiKey: string) => ` +import { createContext, useContext, useMemo, useState } from 'react' +import { MetabaseProvider } from '${SDK_PACKAGE_NAME}' + +/** @type {import('${SDK_PACKAGE_NAME}').SDKConfig} */ +const config = { + metabaseInstanceUrl: \`${url}\`, + apiKey: '${apiKey}' +} + +// Used for the example theme switcher component +export const SampleThemeContext = createContext({ themeKey: 'light' }) + +export const MetabaseEmbedProvider = ({ children }) => { + const { themeKey } = useContext(SampleThemeContext) + const theme = useMemo(() => THEMES[themeKey], [themeKey]) + + return ( + <MetabaseProvider config={config} theme={theme}> + {children} + </MetabaseProvider> + ) +} + +export const SampleThemeProvider = ({ children }) => { + const [themeKey, setThemeKey] = useState('light') + + return ( + <SampleThemeContext.Provider value={{themeKey, setThemeKey}}> + {children} + </SampleThemeContext.Provider> + ) +} + +/** + * Sample themes for Metabase components. + * + * @type {Record<string, import('${SDK_PACKAGE_NAME}').MetabaseTheme>} + */ +const THEMES = { + // Light theme + "light": { + colors: { + brand: "#509EE3", + filter: "#7172AD", + "text-primary": "#4C5773", + "text-secondary": "#696E7B", + "text-tertiary": "#949AAB", + border: "#EEECEC", + background: "#F9FBFC", + "background-hover": "#F9FBFC", + positive: "#84BB4C", + negative: "#ED6E6E" + } + }, + + // Dark theme + "dark": { + colors: { + brand: "#509EE3", + filter: "#7172AD", + "text-primary": "#FFFFFF", + "text-secondary": "#FFFFFF", + "text-tertiary": "#FFFFFF", + border: "#5A5F6B", + background: "#2D353A", + "background-hover": "#2D353A", + positive: "#84BB4C", + negative: "#ED6E6E" + } + } +} +`; diff --git a/enterprise/frontend/src/embedding-sdk/cli/snippets/theme-switcher-snippet.ts b/enterprise/frontend/src/embedding-sdk/cli/snippets/theme-switcher-snippet.ts new file mode 100644 index 0000000000000000000000000000000000000000..56678a358f553faf9e8ed7540b1ae66c28390fdf --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/snippets/theme-switcher-snippet.ts @@ -0,0 +1,47 @@ +export const THEME_SWITCHER_SNIPPET = ` +import { useContext } from 'react' + +import { SampleThemeContext } from './metabase-provider' + +export const ThemeSwitcher = () => { + const { themeKey, setThemeKey } = useContext(SampleThemeContext) + + const ThemeIcon = ICONS[themeKey] + + return ( + <div + className="theme-switcher" + onClick={() => setThemeKey(themeKey === 'light' ? 'dark' : 'light')} + > + <ThemeIcon /> + </div> + ) +} + +const ICONS = { + light: () => ( + <svg viewBox="0 0 24 24"> + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + d="M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0m-5 0h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7l-.7.7m0 11.4l.7.7m-12.1-.7l-.7.7" + /> + </svg> + ), + dark: () => ( + <svg viewBox="0 0 24 24"> + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z" + /> + </svg> + ) +} +`; diff --git a/enterprise/frontend/src/embedding-sdk/cli/steps/create-models-and-xrays.ts b/enterprise/frontend/src/embedding-sdk/cli/steps/create-models-and-xrays.ts index 50e12164a88f6a6f1a31f7be0f6a847968a84fbf..731ebf9b0f356dc940a23e48af6b4e8aa5306337 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/steps/create-models-and-xrays.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/steps/create-models-and-xrays.ts @@ -1,10 +1,9 @@ import ora from "ora"; -import { createXrayDashboardFromModel } from "embedding-sdk/cli/utils/xray-models"; -import type { DashboardId } from "metabase-types/api"; - import type { CliStepMethod } from "../types/cli"; +import type { DashboardInfo } from "../types/dashboard"; import { createModelFromTable } from "../utils/create-model-from-table"; +import { createXrayDashboardFromModel } from "../utils/xray-models"; export const createModelsAndXrays: CliStepMethod = async state => { const { instanceUrl = "", databaseId, cookie = "", tables = [] } = state; @@ -17,7 +16,7 @@ export const createModelsAndXrays: CliStepMethod = async state => { try { // Create a model for each table - const modelIds = await Promise.all( + const models = await Promise.all( tables.map(table => createModelFromTable({ table, @@ -30,23 +29,23 @@ export const createModelsAndXrays: CliStepMethod = async state => { spinner.start("X-raying your data to create dashboards..."); - const dashboardIds: DashboardId[] = []; + const dashboards: DashboardInfo[] = []; // We create dashboard sequentially to prevent multiple // "Automatically Generated Dashboards" collection from being created. - for (const modelId of modelIds) { + for (const model of models) { const dashboardId = await createXrayDashboardFromModel({ - modelId, + modelId: model.modelId, instanceUrl, cookie, }); - dashboardIds.push(dashboardId); + dashboards.push({ id: dashboardId, name: model.modelName }); } spinner.succeed(); - return [{ type: "done" }, { ...state, dashboardIds }]; + return [{ type: "done" }, { ...state, dashboards }]; } catch (error) { spinner.fail(); diff --git a/enterprise/frontend/src/embedding-sdk/cli/steps/generate-component-files.ts b/enterprise/frontend/src/embedding-sdk/cli/steps/generate-component-files.ts new file mode 100644 index 0000000000000000000000000000000000000000..475edf60f8ae073da316e5bca8d93b01e454403f --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/steps/generate-component-files.ts @@ -0,0 +1,67 @@ +import fs from "fs/promises"; + +import { input } from "@inquirer/prompts"; + +import { ANALYTICS_CSS_SNIPPET } from "../snippets/analytics-css-snippet"; +import type { CliStepMethod } from "../types/cli"; +import { getComponentSnippets } from "../utils/get-component-snippets"; +import { printError, printSuccess } from "../utils/print"; + +export const generateReactComponentFiles: CliStepMethod = async state => { + const { instanceUrl, apiKey, dashboards = [] } = state; + + if (!instanceUrl || !apiKey) { + return [ + { type: "error", message: "Missing instance URL or API key." }, + state, + ]; + } + + let path: string; + + // eslint-disable-next-line no-constant-condition -- ask until user provides a valid path + while (true) { + path = await input({ + message: "Where do you want to save the example React components?", + default: "./components/metabase", + }); + + // Create a directory if it doesn't already exist. + try { + await fs.mkdir(path, { recursive: true }); + break; + } catch (error) { + printError( + `The current path is not writeable. Please pick a different path.`, + ); + } + } + + const sampleComponents = getComponentSnippets({ + instanceUrl, + apiKey, + dashboards, + }); + + // Generate sample components files in the specified directory. + for (const { name, content } of sampleComponents) { + await fs.writeFile(`${path}/${name}.jsx`, content); + } + + // Generate analytics.css sample styles. + await fs.writeFile(`${path}/analytics.css`, ANALYTICS_CSS_SNIPPET); + + // Generate index.js file with all the component exports. + const exportIndexContent = sampleComponents + .map(file => `export * from "./${file.name}";`) + .join("\n") + .trim(); + + 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.`, + ); + + return [{ type: "done" }, state]; +}; diff --git a/enterprise/frontend/src/embedding-sdk/cli/steps/generate-credentials.ts b/enterprise/frontend/src/embedding-sdk/cli/steps/generate-credentials.ts index 59cd4d9e00a100ad9da1753ad2fef4edd6539648..73b5bb044f8f7e851037f931abd2202fff96cc95 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/steps/generate-credentials.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/steps/generate-credentials.ts @@ -2,11 +2,13 @@ import fs from "fs/promises"; import { input } from "@inquirer/prompts"; -import type { CliStepMethod } from "embedding-sdk/cli/types/cli"; -import { generateRandomDemoPassword } from "embedding-sdk/cli/utils/generate-password"; -import { OUTPUT_STYLES, printEmptyLines } from "embedding-sdk/cli/utils/print"; import { isEmail } from "metabase/lib/email"; +import type { CliStepMethod } from "../types/cli"; +import { addFileToGitIgnore } from "../utils/add-file-to-git-ignore"; +import { generateRandomDemoPassword } from "../utils/generate-password"; +import { OUTPUT_STYLES, printEmptyLines } from "../utils/print"; + export const generateCredentials: CliStepMethod = async state => { printEmptyLines(); const email = await input({ @@ -18,9 +20,13 @@ export const generateCredentials: CliStepMethod = async state => { const password = generateRandomDemoPassword(); + const credentialFile = "METABASE_LOGIN.json"; + + await addFileToGitIgnore(credentialFile); + // Store the login credentials to a file. await fs.writeFile( - "./METABASE_LOGIN.json", + `./${credentialFile}`, JSON.stringify({ email, password }, null, 2), ); diff --git a/enterprise/frontend/src/embedding-sdk/cli/steps/get-code-sample.ts b/enterprise/frontend/src/embedding-sdk/cli/steps/get-code-sample.ts deleted file mode 100644 index be3fb11329f89a7c85a50980545d8f1d6031d224..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/embedding-sdk/cli/steps/get-code-sample.ts +++ /dev/null @@ -1,57 +0,0 @@ -import clipboard from "clipboardy"; -import toggle from "inquirer-toggle"; - -import { getCodeSample } from "embedding-sdk/cli/constants/code-sample"; -import type { CliStepMethod } from "embedding-sdk/cli/types/cli"; -import { - printEmptyLines, - printInfo, - printSuccess, -} from "embedding-sdk/cli/utils/print"; - -export const generateCodeSample: CliStepMethod = async state => { - if (!state.instanceUrl || !state.apiKey) { - return [ - { - type: "error", - message: "Missing instance URL or API key", - }, - state, - ]; - } - - printEmptyLines(1); - printSuccess( - "API key generated successfully. Here's a code sample to embed the Metabase SDK in your React application:", - ); - - const codeSample = getCodeSample(state.instanceUrl, state.apiKey); - printEmptyLines(); - printInfo(codeSample.trim()); - printEmptyLines(); - - const shouldCopyToClipboard = await toggle({ - message: "Would you like to copy the code to your clipboard?", - default: true, - }); - - if (shouldCopyToClipboard) { - await clipboard.write(codeSample); - printSuccess( - "Code copied to clipboard. Paste it into your React application.", - ); - } else { - printSuccess( - "Paste the code above into your React application to embed Metabase.", - ); - printInfo( - "Then, put <Analytics /> in your component to view the dashboard!", - ); - } - return [ - { - type: "done", - }, - state, - ]; -}; diff --git a/enterprise/frontend/src/embedding-sdk/cli/steps/index.ts b/enterprise/frontend/src/embedding-sdk/cli/steps/index.ts index 33aca04ce5720bda3db03c5056f347e5a686fe2c..0d6dd3021fc3bc14cbbaa6c1174605c45257aa11 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/steps/index.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/steps/index.ts @@ -2,7 +2,6 @@ export * from "./add-embedding-token"; export * from "./check-docker-running"; export * from "./create-api-key"; export * from "./generate-credentials"; -export * from "./get-code-sample"; export * from "./poll-metabase-instance"; export * from "./setup-metabase-instance"; export * from "./show-metabase-cli-title"; @@ -12,3 +11,4 @@ export * from "./check-sdk-available"; export * from "./add-database-connection"; export * from "./pick-database-tables"; export * from "./create-models-and-xrays"; +export * from "./generate-component-files"; diff --git a/enterprise/frontend/src/embedding-sdk/cli/steps/setup-metabase-instance.ts b/enterprise/frontend/src/embedding-sdk/cli/steps/setup-metabase-instance.ts index 5d3931c810b31beaa780dc117afa04ee030e0a13..4558406e3a764b95db2f70ad4555c3fbc1b1bcfb 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/steps/setup-metabase-instance.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/steps/setup-metabase-instance.ts @@ -94,6 +94,7 @@ export const setupMetabaseInstance: CliStepMethod = async state => { if (!res.ok) { const errorMessage = await res.text(); + spinner.fail(); // Error message: The /api/setup route can only be used to create the first user, however a user currently exists. if (errorMessage.includes("a user currently exists")) { @@ -162,6 +163,7 @@ export const setupMetabaseInstance: CliStepMethod = async state => { if (!res.ok) { const errorMessage = await res.text(); + spinner.fail(); if (errorMessage.includes("Unauthenticated")) { return onInstanceConfigured(); diff --git a/enterprise/frontend/src/embedding-sdk/cli/types/cli.ts b/enterprise/frontend/src/embedding-sdk/cli/types/cli.ts index cf9b36a8271b27295da68daf294e4fb25e7a07f0..bee2148e92d4472487d840792d072ac416721b61 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/types/cli.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/types/cli.ts @@ -1,5 +1,7 @@ import type { CLI_STEPS } from "embedding-sdk/cli/run"; -import type { DashboardId, Settings, Table } from "metabase-types/api"; +import type { Settings, Table } from "metabase-types/api"; + +import type { DashboardInfo } from "../types/dashboard"; export type CliState = Partial<{ port: number; @@ -17,8 +19,8 @@ export type CliState = Partial<{ /** Database tables selected by the user */ tables: Table[]; - /** IDs of auto-generated dashboards */ - dashboardIds: DashboardId[]; + /** IDs and names of auto-generated dashboards */ + dashboards: DashboardInfo[]; }>; export type CliError = { diff --git a/enterprise/frontend/src/embedding-sdk/cli/types/dashboard.ts b/enterprise/frontend/src/embedding-sdk/cli/types/dashboard.ts new file mode 100644 index 0000000000000000000000000000000000000000..27e710a8f124874ea7148242b3ec619cab533652 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/types/dashboard.ts @@ -0,0 +1,3 @@ +import type { DashboardId } from "metabase-types/api"; + +export type DashboardInfo = { id: DashboardId; name: string }; diff --git a/enterprise/frontend/src/embedding-sdk/cli/utils/add-file-to-git-ignore.ts b/enterprise/frontend/src/embedding-sdk/cli/utils/add-file-to-git-ignore.ts new file mode 100644 index 0000000000000000000000000000000000000000..22245a05e50dafc3fa803d4a69a638f162af122c --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/utils/add-file-to-git-ignore.ts @@ -0,0 +1,23 @@ +import fs from "fs/promises"; + +const GITIGNORE_PATH = ".gitignore"; + +/** + * Adds the credential file to .gitignore if exists. + */ +export async function addFileToGitIgnore(fileName: string) { + try { + // Check if .gitignore exists + await fs.access(GITIGNORE_PATH); + + let gitignoreContent = await fs.readFile(GITIGNORE_PATH, "utf-8"); + + // If the credential file is not in .gitignore, add it. + if (!gitignoreContent.includes(fileName)) { + gitignoreContent += `\n${fileName}`; + await fs.writeFile(GITIGNORE_PATH, gitignoreContent); + } + } catch (error) { + // skip if .gitignore does not exist + } +} diff --git a/enterprise/frontend/src/embedding-sdk/cli/utils/create-model-from-table.ts b/enterprise/frontend/src/embedding-sdk/cli/utils/create-model-from-table.ts index fe95dc5eb9af814261ca227206f4500b64d65c48..19c4eb1c934b1ce28d37180b6ffcf199d090533c 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/utils/create-model-from-table.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/utils/create-model-from-table.ts @@ -55,5 +55,5 @@ export async function createModelFromTable(options: Options) { const { id: modelId } = (await res.json()) as { id: number }; - return modelId; + return { modelId, modelName: table.display_name }; } diff --git a/enterprise/frontend/src/embedding-sdk/cli/utils/get-component-snippets.ts b/enterprise/frontend/src/embedding-sdk/cli/utils/get-component-snippets.ts new file mode 100644 index 0000000000000000000000000000000000000000..38e009295981e534f22ebbbec9a0f0c17112b509 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/utils/get-component-snippets.ts @@ -0,0 +1,46 @@ +import { + getAnalyticsDashboardSnippet, + getMetabaseProviderSnippet, + THEME_SWITCHER_SNIPPET, + ANALYTICS_PAGE_SNIPPET, +} from "../snippets"; +import type { DashboardInfo } from "../types/dashboard"; + +interface Options { + instanceUrl: string; + apiKey: string; + dashboards: DashboardInfo[]; +} + +export function getComponentSnippets(options: Options) { + const { instanceUrl, apiKey, dashboards } = options; + + const analyticsDashboardSnippet = getAnalyticsDashboardSnippet( + instanceUrl, + dashboards, + ).trim(); + + const metabaseProviderSnippet = getMetabaseProviderSnippet( + instanceUrl, + apiKey, + ).trim(); + + return [ + { + name: "metabase-provider", + content: metabaseProviderSnippet, + }, + { + name: "analytics-dashboard", + content: analyticsDashboardSnippet, + }, + { + name: "theme-switcher", + content: THEME_SWITCHER_SNIPPET.trim(), + }, + { + name: "analytics-page", + content: ANALYTICS_PAGE_SNIPPET.trim(), + }, + ]; +} diff --git a/enterprise/frontend/src/embedding-sdk/cli/utils/xray-models.ts b/enterprise/frontend/src/embedding-sdk/cli/utils/xray-models.ts index 33294fff445cb74be242af6ca8d21285ebaa415e..e60f00ed92c159be85489c9aff286d6a9adcc6f5 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/utils/xray-models.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/utils/xray-models.ts @@ -1,5 +1,5 @@ import { uuid } from "metabase/lib/uuid"; -import type { Dashboard } from "metabase-types/api"; +import type { Dashboard, DashboardId } from "metabase-types/api"; import { propagateErrorResponse } from "./propagate-error-response"; @@ -10,7 +10,9 @@ interface Options { instanceUrl: string; } -export async function createXrayDashboardFromModel(options: Options) { +export async function createXrayDashboardFromModel( + options: Options, +): Promise<DashboardId> { const { modelId, instanceUrl, cookie = "" } = options; // Queries an auto-generated dashboard layout for the model