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 index 7f11c88811041f02f584875ab015ad25cc7f6784..87b4ef0463d55b247a4744f8461f859b25b4153e 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-dashboard-snippet.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/snippets/analytics-dashboard-snippet.ts @@ -2,13 +2,12 @@ import { SDK_PACKAGE_NAME } from "../constants/config"; import type { DashboardInfo } from "../types/dashboard"; interface Options { - instanceUrl: string; dashboards: DashboardInfo[]; userSwitcherEnabled: boolean; } export const getAnalyticsDashboardSnippet = (options: Options) => { - const { instanceUrl, dashboards, userSwitcherEnabled } = options; + const { dashboards, userSwitcherEnabled } = options; let imports = `import { ThemeSwitcher } from './theme-switcher'`; @@ -17,8 +16,8 @@ export const getAnalyticsDashboardSnippet = (options: Options) => { } return ` -import { useState, useContext } from 'react' -import { InteractiveDashboard } from '${SDK_PACKAGE_NAME}' +import { useState, useContext, useReducer } from 'react' +import { InteractiveDashboard, CreateQuestion } from '${SDK_PACKAGE_NAME}' import { AnalyticsContext } from "./analytics-provider" ${imports} @@ -27,30 +26,33 @@ export const AnalyticsDashboard = () => { const {email} = useContext(AnalyticsContext) const [dashboardId, setDashboardId] = useState(DASHBOARDS[0].id) - const editLink = \`${instanceUrl}/dashboard/\${dashboardId}\` + const [isCreateQuestion, toggleCreateQuestion] = useReducer((s) => !s, false) + + const isDashboard = !isCreateQuestion 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> - ${userSwitcherEnabled ? "<UserSwitcher />" : ""} </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 + {isDashboard && ( + <select + className="dashboard-select" + onChange={(e) => setDashboardId(e.target.value)} + > + {DASHBOARDS.map((dashboard) => ( + <option key={dashboard.id} value={dashboard.id}> + {dashboard.name} + </option> + ))} + </select> + )} + + <a href="#!" onClick={toggleCreateQuestion}> + {isCreateQuestion ? 'Back to dashboard' : 'Create Question'} </a> <ThemeSwitcher /> @@ -58,7 +60,16 @@ export const AnalyticsDashboard = () => { </div> {/** Reload the dashboard when user changes with the key prop */} - <InteractiveDashboard dashboardId={dashboardId} withTitle withDownloads key={email} /> + {isDashboard && ( + <InteractiveDashboard + dashboardId={dashboardId} + withTitle + withDownloads + key={email} + /> + )} + + {isCreateQuestion && <CreateQuestion />} </div> ) } 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 81d6f175bceb763c81e0a4d3e2b5450af8dc2f2d..b6940036e68d368b9b0591bab17a96b89b5abe98 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,5 +1,7 @@ import ora from "ora"; +import { createCollection } from "embedding-sdk/cli/utils/create-collection"; + import type { CliStepMethod } from "../types/cli"; import type { DashboardInfo } from "../types/dashboard"; import { createModelFromTable } from "../utils/create-model-from-table"; @@ -22,11 +24,20 @@ export const createModelsAndXrays: CliStepMethod = async state => { try { const models = []; + // Create the "Our models" collection to store the models. + // This helps us to allow access to models when sandboxing. + const modelCollectionId = await createCollection({ + name: "Our models", + instanceUrl, + cookie, + }); + // Create a model for each table for (const table of chosenTables) { const model = await createModelFromTable({ table, databaseId, + collectionId: modelCollectionId, cookie, instanceUrl, }); @@ -52,7 +63,7 @@ export const createModelsAndXrays: CliStepMethod = async state => { spinner.succeed(); - return [{ type: "done" }, { ...state, dashboards }]; + return [{ type: "done" }, { ...state, dashboards, modelCollectionId }]; } catch (error) { spinner.fail(); diff --git a/enterprise/frontend/src/embedding-sdk/cli/steps/setup-permission.ts b/enterprise/frontend/src/embedding-sdk/cli/steps/setup-permission.ts index 06516e1aefb736a09a0180a7c1418906eced62c2..5692d774f3f3507a9f538b5bd277e65be9621950 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/steps/setup-permission.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/steps/setup-permission.ts @@ -1,8 +1,9 @@ import { SANDBOXED_GROUP_NAMES } from "../constants/config"; import { getNoTenantMessage } from "../constants/messages"; import type { CliStepMethod } from "../types/cli"; -import { getCollectionPermissions } from "../utils/get-collection-permissions"; +import { createCollection } from "../utils/create-collection"; import { getPermissionsForGroups } from "../utils/get-permission-groups"; +import { getSandboxedCollectionPermissions } from "../utils/get-sandboxed-collection-permissions"; import { getTenancyIsolationSandboxes } from "../utils/get-tenancy-isolation-sandboxes"; import { cliError, @@ -11,7 +12,12 @@ import { import { sampleTenantIdsFromTables } from "../utils/sample-tenancy-column-values"; export const setupPermissions: CliStepMethod = async state => { - const { cookie = "", instanceUrl = "", tenancyColumnNames = {} } = state; + const { + cookie = "", + instanceUrl = "", + tenancyColumnNames = {}, + modelCollectionId = 0, + } = state; let res; const collectionIds: number[] = []; @@ -19,21 +25,12 @@ export const setupPermissions: CliStepMethod = async state => { // Create new customer collections sequentially try { for (const groupName of SANDBOXED_GROUP_NAMES) { - res = await fetch(`${instanceUrl}/api/collection`, { - method: "POST", - headers: { "content-type": "application/json", cookie }, - body: JSON.stringify({ - parent_id: null, - authority_level: null, - color: "#509EE3", - description: null, - name: groupName, - }), + const collectionId = await createCollection({ + name: groupName, + instanceUrl, + cookie, }); - await propagateErrorResponse(res); - - const { id: collectionId } = (await res.json()) as { id: number }; collectionIds.push(collectionId); } } catch (error) { @@ -107,7 +104,16 @@ export const setupPermissions: CliStepMethod = async state => { } try { - const groups = getCollectionPermissions({ groupIds, collectionIds }); + const groups = getSandboxedCollectionPermissions({ + groupIds, + collectionIds, + }); + + // Grant access to the "Our models" collection for all customer groups. + // This is so they can search and select their models in the entity picker. + for (const groupId of groupIds) { + groups[groupId][modelCollectionId] = "write"; + } // Update the permissions for sandboxed collections res = await fetch(`${instanceUrl}/api/collection/graph`, { diff --git a/enterprise/frontend/src/embedding-sdk/cli/types/cli.ts b/enterprise/frontend/src/embedding-sdk/cli/types/cli.ts index 8843c8aa2ff47c130fa4ca6a0b4967188ee98e1b..d2ef24ddd9ea7c174b8982e4f778761374cc95d4 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/types/cli.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/types/cli.ts @@ -31,6 +31,9 @@ export type CliState = Partial<{ /** Sampled values of the tenancy columns from the selected tables (e.g. tenancy_id -> [1, 2, 3]) */ tenantIdsMap: Record<string, (string | number)[]>; + /** ID of the "Our models" collection */ + modelCollectionId: number; + /** Directory where the Express.js mock server is saved to */ mockServerDir: string; diff --git a/enterprise/frontend/src/embedding-sdk/cli/utils/create-collection.ts b/enterprise/frontend/src/embedding-sdk/cli/utils/create-collection.ts new file mode 100644 index 0000000000000000000000000000000000000000..33ad45a5eb117269d59e925e2ecf73719a91a4f1 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/cli/utils/create-collection.ts @@ -0,0 +1,30 @@ +import { propagateErrorResponse } from "embedding-sdk/cli/utils/propagate-error-response"; + +interface Options { + name: string; + + instanceUrl: string; + cookie: string; +} + +export async function createCollection(options: Options) { + const { name, instanceUrl, cookie } = options; + + const res = await fetch(`${instanceUrl}/api/collection`, { + method: "POST", + headers: { "content-type": "application/json", cookie }, + body: JSON.stringify({ + parent_id: null, + authority_level: null, + color: "#509EE3", + description: null, + name, + }), + }); + + await propagateErrorResponse(res); + + const { id: collectionId } = (await res.json()) as { id: number }; + + return collectionId; +} 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 e5174e0fb91f24110d3197f5cf90a8578b8abfbe..96dba536872719c638b6ddaf77942285d7900a60 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 @@ -5,13 +5,14 @@ import { propagateErrorResponse } from "./propagate-error-response"; interface Options { table: Table; databaseId: number; + collectionId: number | null; cookie: string; instanceUrl: string; } export async function createModelFromTable(options: Options) { - const { databaseId, table, instanceUrl, cookie = "" } = options; + const { databaseId, collectionId, table, instanceUrl, cookie = "" } = options; const datasetQuery = { type: "query", @@ -28,7 +29,7 @@ export async function createModelFromTable(options: Options) { type: "model", display: "table", result_metadata: null, - collection_id: null, + collection_id: collectionId, collection_position: 1, visualization_settings: {}, dataset_query: datasetQuery, diff --git a/enterprise/frontend/src/embedding-sdk/cli/utils/get-collection-permissions.ts b/enterprise/frontend/src/embedding-sdk/cli/utils/get-sandboxed-collection-permissions.ts similarity index 92% rename from enterprise/frontend/src/embedding-sdk/cli/utils/get-collection-permissions.ts rename to enterprise/frontend/src/embedding-sdk/cli/utils/get-sandboxed-collection-permissions.ts index 618eb8b6e0ea5b5433f25f4eb486aa4e637eb2cf..2fae08d7faca537088b5e93c7a8116e29a2e6c1b 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/utils/get-collection-permissions.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/utils/get-sandboxed-collection-permissions.ts @@ -5,7 +5,7 @@ interface Options { const ALL_USERS_GROUP_ID = 1; -export function getCollectionPermissions(options: Options) { +export function getSandboxedCollectionPermissions(options: Options) { const { groupIds, collectionIds } = options; const groups: Record<string, Record<string, string>> = {}; diff --git a/enterprise/frontend/src/embedding-sdk/cli/utils/permissions-graph.unit.spec.ts b/enterprise/frontend/src/embedding-sdk/cli/utils/permissions-graph.unit.spec.ts index 09ff49d66ee1c81494617d5ee7d95e03695e270b..221f9a89535e88c7e9a1ef833b381295c4da00c5 100644 --- a/enterprise/frontend/src/embedding-sdk/cli/utils/permissions-graph.unit.spec.ts +++ b/enterprise/frontend/src/embedding-sdk/cli/utils/permissions-graph.unit.spec.ts @@ -1,7 +1,7 @@ import { createMockField, createMockTable } from "metabase-types/api/mocks"; -import { getCollectionPermissions } from "./get-collection-permissions"; import { getPermissionsForGroups } from "./get-permission-groups"; +import { getSandboxedCollectionPermissions } from "./get-sandboxed-collection-permissions"; import { getTenancyIsolationSandboxes } from "./get-tenancy-isolation-sandboxes"; const getMockTable = (id: number) => @@ -36,7 +36,7 @@ describe("permission graph generation for embedding cli", () => { }); it("should generate valid permissions for collections", () => { - const groups = getCollectionPermissions({ + const groups = getSandboxedCollectionPermissions({ groupIds: [3, 4, 5], collectionIds: [9, 10, 11], });