UNPKG

embed-dossier-mstr-react

Version:

A production-ready React library for embedding MicroStrategy Dossiers, Reports, and Bot Consumption pages with full TypeScript support.

919 lines (903 loc) 26.6 kB
// src/constants/eventType.ts var EVENT_TYPE = { ON_COMPONENT_SELECTION_CHANGED: "onComponentSelectionChanged", ON_DOSSIER_AUTHORING_CLOSED: "onDossierAuthoringClosed", ON_DOSSIER_AUTHORING_SAVED: "onDossierAuthoringSaved", ON_DOSSIER_INSTANCE_CHANGED: "onDossierInstanceChanged", ON_DOSSIER_INSTANCE_ID_CHANGE: "onDossierInstanceIDChange", ON_ERROR: "onError", ON_FILTER_UPDATED: "onFilterUpdated", ON_GRAPHICS_SELECTED: "onGraphicsSelected", ON_LAYOUT_CHANGED: "onLayoutChanged", ON_LIBRARY_ITEM_SELECTED: "onLibraryItemSelected", ON_LIBRARY_ITEM_SELECTION_CLEARED: "onLibraryItemSelectionCleared", ON_LIBRARY_MENU_SELECTED: "onLibraryMenuSelected", ON_PAGE_LOADED: "onPageLoaded", ON_PAGE_RENDER_FINISHED: "onPageRenderFinished", ON_PAGE_SWITCHED: "onPageSwitched", ON_PANEL_SWITCHED: "onPanelSwitched", ON_PROMPT_ANSWERED: "onPromptAnswered", ON_PROMPT_LOADED: "onPromptLoaded", ON_SESSION_ERROR: "onSessionError", ON_VISUALIZATION_RESIZED: "onVisualizationResized", ON_VIZ_ELEMENT_CHANGED: "onVisualizationElementsChanged", ON_VIZ_SELECTION_CHANGED: "onVizSelectionChanged" }; // src/hooks/useCreateDashboard.ts import { useEffect as useEffect2, useRef, useState as useState2 } from "react"; // src/hooks/useLoadMstrSDK.ts import { useEffect, useState } from "react"; var useLoadMstrSDK = ({ serverUrlLibrary }) => { const [isSdkLoaded, setIsSdkLoaded] = useState(false); const [isSdkError, setIsSdkError] = useState({ isError: false, message: "" }); useEffect(() => { if (window.microstrategy) { setIsSdkLoaded(true); return; } if (!serverUrlLibrary) { setIsSdkError({ isError: true, message: "serverUrlLibrary is required to load the MicroStrategy SDK" }); return; } const script = document.createElement("script"); script.type = "text/javascript"; script.src = `${serverUrlLibrary}/javascript/embeddinglib.js`; script.onload = () => { setIsSdkLoaded(true); }; script.onerror = () => { setIsSdkError({ isError: true, message: `Failed to load MicroStrategy SDK from ${serverUrlLibrary}` }); }; document.head.appendChild(script); return () => { script.remove(); }; }, [serverUrlLibrary]); return { isSdkLoaded, isSdkError }; }; // src/hooks/useCreateDashboard.ts var useCreateDashboard = ({ serverUrlLibrary, config }) => { const containerRef = useRef(null); const [dashboard, setDashboard] = useState2(null); const dashboardRef = useRef(null); const [isDashboardError, setIsDashboardError] = useState2(false); const { isSdkLoaded } = useLoadMstrSDK({ serverUrlLibrary }); useEffect2(() => { if (!isSdkLoaded) return; let mounted = true; async function createDashboard() { try { const dossier = await window.microstrategy.dossier.create({ ...config, placeholder: containerRef.current }); if (mounted) { dashboardRef.current = dossier; setDashboard(dossier); } } catch (error) { if (mounted) { console.error("Failed to create dashboard:", error); setIsDashboardError(true); } } } createDashboard(); return () => { mounted = false; if (dashboardRef.current) { try { dashboardRef.current.close(); } catch { } dashboardRef.current = null; } }; }, [isSdkLoaded]); return { dashboard, containerRef, isSdkLoaded, isDashboardError }; }; // src/utils.ts var getInfoFromUrl = (url) => { if (!url || typeof url !== "string") { throw new Error("A valid MicroStrategy dossier URL is required"); } let parsed; try { parsed = new URL(url); } catch { throw new Error( `Invalid URL format: "${url}". Expected format: https://{host}/{libraryName}/app/{projectId}/{dossierId}` ); } const pathSegments = parsed.pathname.split("/").filter(Boolean); if (pathSegments.length < 4 || pathSegments[1] !== "app") { throw new Error( `Invalid MicroStrategy URL path: "${parsed.pathname}". Expected format: /{libraryName}/app/{projectId}/{dossierId}` ); } const serverUrl = parsed.origin; const libraryName = pathSegments[0]; const serverUrlLibrary = `${serverUrl}/${libraryName}`; const projectId = pathSegments[2]; const dossierId = pathSegments[3]; return { serverUrl, serverUrlLibrary, libraryName, projectId, dossierId }; }; var getServerUrl = (url) => { const appIndex = url.indexOf("/app/"); if (appIndex === -1) { throw new Error( `URL does not contain "/app/" segment: "${url}". Expected format: https://{host}/{libraryName}/app/{projectId}/{dossierId}` ); } return url.substring(0, appIndex); }; var getAuthToken = async ({ serverUrlLibrary }) => { const options = { method: "GET", credentials: "include", mode: "cors", headers: { "content-type": "application/json" } }; try { const response = await fetch(`${serverUrlLibrary}/api/auth/token`, options); if (response.ok) { const data = await response.json(); return data.token; } return void 0; } catch (error) { console.error("Failed to get auth token:", error); return void 0; } }; // src/components/DashboardEmbed.tsx import { clsx } from "clsx"; import { jsx } from "react/jsx-runtime"; var DashboardEmbed = ({ dossierUrl, className, style, config }) => { let serverUrlLibrary = ""; try { const urlInfo = getInfoFromUrl(dossierUrl); serverUrlLibrary = urlInfo.serverUrlLibrary; } catch (error) { console.error("Failed to parse dossier URL:", error); } const { containerRef } = useCreateDashboard({ serverUrlLibrary, config: { url: dossierUrl, ...config } }); return /* @__PURE__ */ jsx("div", { ref: containerRef, className: clsx(className), style }); }; // src/hooks/useCreateDashboardWithAuth.ts import { useEffect as useEffect3, useState as useState3 } from "react"; var useCreateDashboardWithAuth = ({ serverUrlLibrary, placeholder, config, loginMode, username, password }) => { const [dashboard, setDashboard] = useState3(null); const [authState, setAuthState] = useState3({ isAuthenticated: false, isAuthenticating: false, error: null }); const { isSdkLoaded } = useLoadMstrSDK({ serverUrlLibrary }); useEffect3(() => { if (!isSdkLoaded || !placeholder) return; let mounted = true; const initializeDashboard = async () => { if (!mounted) return; setAuthState((prev) => ({ ...prev, isAuthenticating: true, error: null })); try { switch (loginMode) { case "standard": case "ldap": { if (!username || !password) { throw new Error( "Username and password are required for standard/LDAP authentication" ); } const standardOptions = { method: "POST", credentials: "include", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify({ loginMode: 1, username, password }) }; await fetch(`${serverUrlLibrary}/api/auth/login`, standardOptions); break; } case "guest": { const guestOptions = { method: "POST", credentials: "include", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify({ loginMode: 8 }) }; await fetch(`${serverUrlLibrary}/api/auth/login`, guestOptions); break; } case "saml": try { await window.microstrategy.auth.samlLogin(serverUrlLibrary); } catch (e) { if (e instanceof Error && e.message.includes("Failed to open a new tab")) { throw new Error( "Please enable popups to be directed to the SAML login site" ); } throw e; } break; case "oidc": try { await window.microstrategy.auth.oidcLogin(serverUrlLibrary); } catch (e) { if (e instanceof Error && e.message.includes("Failed to open a new tab")) { throw new Error( "Please enable popups to be directed to the OIDC login site" ); } throw e; } break; } const dossier = await window.microstrategy.dossier.create({ ...config, placeholder, enableCustomAuthentication: true, customAuthenticationType: window.microstrategy.dossier.CustomAuthenticationType.AUTH_TOKEN, getLoginToken: async () => { const options = { method: "GET", credentials: "include", mode: "cors", headers: { "content-type": "application/json" } }; const response = await fetch( `${serverUrlLibrary}/api/auth/token`, options ); return response.ok ? response.headers.get("x-mstr-authtoken") : null; } }); if (mounted) { setDashboard(dossier); setAuthState((prev) => ({ ...prev, isAuthenticated: true })); } } catch (error) { if (mounted) { console.error("Dashboard auth error:", error); setAuthState((prev) => ({ ...prev, error: error instanceof Error ? error.message : "Failed to initialize dashboard", isAuthenticated: false })); } } finally { if (mounted) { setAuthState((prev) => ({ ...prev, isAuthenticating: false })); } } }; initializeDashboard(); return () => { mounted = false; }; }, [ isSdkLoaded, placeholder, loginMode, serverUrlLibrary, config, username, password ]); return { dashboard, ...authState }; }; // src/components/DashboardEmbedWithAuth.tsx import { clsx as clsx2 } from "clsx"; import { useRef as useRef2 } from "react"; import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime"; var DashboardEmbedWithAuth = ({ dossierUrl, className, style, config, loginMode, username, password, loadingComponent = /* @__PURE__ */ jsx2("div", { children: "Loading..." }), errorComponent = (error) => /* @__PURE__ */ jsxs( "div", { style: { textAlign: "center", color: "#ef4444", padding: "16px", maxWidth: "448px" }, children: [ /* @__PURE__ */ jsx2("h3", { style: { fontSize: "18px", fontWeight: 600, marginBottom: "8px" }, children: "Authentication Error" }), /* @__PURE__ */ jsx2("p", { children: error }) ] } ) }) => { const { serverUrlLibrary } = getInfoFromUrl(dossierUrl); const divRef = useRef2(null); const { isAuthenticating, error } = useCreateDashboardWithAuth({ serverUrlLibrary, placeholder: divRef.current, config: { url: dossierUrl, enableResponsive: true, enableCustomAuthentication: true, containerHeight: "600px", containerWidth: "100%", ...config }, loginMode, username, password }); if (isAuthenticating) { return /* @__PURE__ */ jsx2(Fragment, { children: loadingComponent }); } if (error) { return /* @__PURE__ */ jsx2(Fragment, { children: typeof errorComponent === "function" ? errorComponent(error) : errorComponent }); } return /* @__PURE__ */ jsx2( "div", { ref: divRef, className: clsx2(className), style: { width: "100%", height: "600px", ...style } } ); }; // src/hooks/useCreateLibraryPage.ts import { useEffect as useEffect4, useState as useState4, useRef as useRef3 } from "react"; var useCreateLibraryPage = ({ serverUrlLibrary, config }) => { const libraryPageRef = useRef3(null); const [libraryPage, setLibraryPage] = useState4(null); const [libraryPageState, setLibraryPageState] = useState4({ isLoading: false, error: null }); const { isSdkLoaded } = useLoadMstrSDK({ serverUrlLibrary }); const prevConfigRef = useRef3(config); useEffect4(() => { if (!isSdkLoaded) return; if (!libraryPage && !libraryPageState.isLoading) { createLibraryPage(); } }, [isSdkLoaded]); useEffect4(() => { const configChanged = JSON.stringify(config) !== JSON.stringify(prevConfigRef.current); if (libraryPage && configChanged) { createLibraryPage(); } prevConfigRef.current = config; }, [config]); const createLibraryPage = async () => { setLibraryPageState((prev) => ({ ...prev, isLoading: true, error: null })); try { const embeddingContext = await window.microstrategy.embeddingContexts.embedLibraryPage({ placeholder: libraryPageRef.current, serverUrl: serverUrlLibrary, errorHandler: (error) => { console.error("Library page error:", error); setLibraryPageState((prev) => ({ ...prev, error: error.message || "Failed to load library page" })); }, sessionErrorHandler: (error) => { console.error("Session error:", error); setLibraryPageState((prev) => ({ ...prev, error: "Session expired. Please refresh the page." })); }, ...config }); setLibraryPage(embeddingContext); } catch (error) { setLibraryPageState((prev) => ({ ...prev, error: error instanceof Error ? error.message : "Failed to create library page" })); } finally { setLibraryPageState((prev) => ({ ...prev, isLoading: false })); } }; return { libraryPage, libraryPageRef, isSdkLoaded, isLoading: libraryPageState.isLoading, error: libraryPageState.error }; }; // src/components/LibraryPageEmbed.tsx import { clsx as clsx3 } from "clsx"; import { jsx as jsx3 } from "react/jsx-runtime"; var LibraryPageEmbed = ({ serverUrlLibrary, config, className, style }) => { const { libraryPageRef } = useCreateLibraryPage({ serverUrlLibrary, config }); return /* @__PURE__ */ jsx3("div", { ref: libraryPageRef, className: clsx3(className), style }); }; // src/hooks/useCreateLibraryPageWithAuth.ts import { useEffect as useEffect5, useState as useState5 } from "react"; var useCreateLibraryPageWithAuth = ({ serverUrlLibrary, placeholder, config, loginMode, username, password }) => { const [libraryPage, setLibraryPage] = useState5(null); const [authState, setAuthState] = useState5({ isAuthenticated: false, error: null }); const { isSdkLoaded } = useLoadMstrSDK({ serverUrlLibrary }); useEffect5(() => { if (!isSdkLoaded || !placeholder) return; let mounted = true; const initializeLibraryPage = async () => { if (!mounted) return; setAuthState((prev) => ({ ...prev, error: null })); try { switch (loginMode) { case "standard": case "ldap": { if (!username || !password) { throw new Error( "Username and password are required for standard/LDAP authentication" ); } await fetch(`${serverUrlLibrary}/api/auth/login`, { method: "POST", credentials: "include", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify({ loginMode: 1, username, password }) }); break; } case "guest": { await fetch(`${serverUrlLibrary}/api/auth/login`, { method: "POST", credentials: "include", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify({ loginMode: 8 }) }); break; } case "saml": await window.microstrategy.auth.samlLogin(serverUrlLibrary); break; case "oidc": await window.microstrategy.auth.oidcLogin(serverUrlLibrary); break; } const page = await window.microstrategy.embeddingContexts.embedLibraryPage({ ...config, placeholder, serverUrl: serverUrlLibrary, enableCustomAuthentication: true, customAuthenticationType: window.microstrategy.dossier.CustomAuthenticationType.AUTH_TOKEN, getLoginToken: async () => { const response = await fetch( `${serverUrlLibrary}/api/auth/token`, { method: "GET", credentials: "include", mode: "cors", headers: { "content-type": "application/json" } } ); return response.ok ? response.headers.get("x-mstr-authtoken") : null; } }); if (mounted) { setLibraryPage(page); setAuthState((prev) => ({ ...prev, isAuthenticated: true })); } } catch (error) { if (mounted) { console.error("Library page auth error:", error); setAuthState((prev) => ({ ...prev, error: error instanceof Error ? error.message : "Failed to initialize library page", isAuthenticated: false })); } } finally { if (mounted) { setAuthState((prev) => ({ ...prev })); } } }; initializeLibraryPage(); return () => { mounted = false; }; }, [ isSdkLoaded, placeholder, loginMode, serverUrlLibrary, config, username, password ]); return { libraryPage, ...authState }; }; // src/components/LibraryPageEmbedWithAuth.tsx import { clsx as clsx4 } from "clsx"; import { useRef as useRef4 } from "react"; import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime"; var LibraryPageEmbedWithAuth = ({ serverUrlLibrary, config, loginMode, username, password, className, style, loadingComponent = /* @__PURE__ */ jsx4("div", { children: "Loading..." }), errorComponent = (error) => /* @__PURE__ */ jsx4("div", { children: error }) }) => { const containerRef = useRef4(null); const { libraryPage, isAuthenticated, error } = useCreateLibraryPageWithAuth({ serverUrlLibrary, placeholder: containerRef.current, config, loginMode, username, password }); if (error) { return /* @__PURE__ */ jsx4(Fragment2, { children: errorComponent(error) }); } if (!isAuthenticated || !libraryPage) { return /* @__PURE__ */ jsx4(Fragment2, { children: loadingComponent }); } return /* @__PURE__ */ jsx4( "div", { ref: containerRef, className: clsx4(className), style: { width: "100%", height: "600px", ...style } } ); }; // src/hooks/useCreateBotConsumptionPage.ts import { useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "react"; var useCreateBotConsumptionPage = ({ serverUrlLibrary, projectId, objectId, config }) => { const botConsumptionPageRef = useRef5(null); const [botConsumptionPage, setBotConsumptionPage] = useState6(null); const [isBotConsumptionPageError, setIsBotConsumptionPageError] = useState6(false); const { isSdkLoaded, isSdkError } = useLoadMstrSDK({ serverUrlLibrary }); useEffect6(() => { if (!isSdkLoaded) return; async function createBotConsumptionPage() { try { const botConsumptionPage2 = await window.microstrategy.embeddingContexts.embedBotConsumptionPage({ ...config, placeholder: botConsumptionPageRef.current, serverUrl: serverUrlLibrary, projectId, objectId }); setBotConsumptionPage(botConsumptionPage2); } catch (error) { console.error(error); setIsBotConsumptionPageError(true); } } createBotConsumptionPage(); }, [isSdkLoaded]); return { botConsumptionPage, botConsumptionPageRef, isBotConsumptionPageError, isSdkError, isSdkLoaded }; }; // src/components/BotConsumptionPage.tsx import { clsx as clsx5 } from "clsx"; import { jsx as jsx5 } from "react/jsx-runtime"; var BotConsumptionPage = ({ serverUrlLibrary, projectId, objectId, config, className, style }) => { const { botConsumptionPageRef } = useCreateBotConsumptionPage({ serverUrlLibrary, projectId, objectId, config }); return /* @__PURE__ */ jsx5( "div", { ref: botConsumptionPageRef, className: clsx5(className), style } ); }; // src/hooks/useCreateBotConsumptionPageWithAuth.ts import { useEffect as useEffect7, useState as useState7 } from "react"; var useCreateBotConsumptionPageWithAuth = ({ serverUrlLibrary, placeholder, projectId, objectId, config, loginMode, username, password }) => { const [botConsumptionPage, setBotConsumptionPage] = useState7(null); const [authState, setAuthState] = useState7({ isAuthenticated: false, isAuthenticating: false, error: null }); const { isSdkLoaded } = useLoadMstrSDK({ serverUrlLibrary }); useEffect7(() => { if (!isSdkLoaded || !placeholder) return; let mounted = true; const initializeBotConsumptionPage = async () => { if (!mounted) return; setAuthState((prev) => ({ ...prev, isAuthenticating: true, error: null })); try { switch (loginMode) { case "standard": case "ldap": { if (!username || !password) { throw new Error( "Username and password are required for standard/LDAP authentication" ); } await fetch(`${serverUrlLibrary}/api/auth/login`, { method: "POST", credentials: "include", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify({ loginMode: 1, username, password }) }); break; } case "guest": { await fetch(`${serverUrlLibrary}/api/auth/login`, { method: "POST", credentials: "include", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify({ loginMode: 8 }) }); break; } case "saml": await window.microstrategy.auth.samlLogin(serverUrlLibrary); break; case "oidc": await window.microstrategy.auth.oidcLogin(serverUrlLibrary); break; } const botPage = await window.microstrategy.embeddingContexts.embedBotConsumptionPage({ ...config, placeholder, serverUrl: serverUrlLibrary, projectId, objectId, enableCustomAuthentication: true, customAuthenticationType: window.microstrategy.dossier.CustomAuthenticationType.AUTH_TOKEN, getLoginToken: async () => { const response = await fetch( `${serverUrlLibrary}/api/auth/token`, { method: "GET", credentials: "include", mode: "cors", headers: { "content-type": "application/json" } } ); return response.ok ? response.headers.get("x-mstr-authtoken") : null; } }); if (mounted) { setBotConsumptionPage(botPage); setAuthState((prev) => ({ ...prev, isAuthenticated: true })); } } catch (error) { if (mounted) { console.error("Bot consumption page auth error:", error); setAuthState((prev) => ({ ...prev, error: error instanceof Error ? error.message : "Failed to initialize bot consumption page", isAuthenticated: false })); } } finally { if (mounted) { setAuthState((prev) => ({ ...prev, isAuthenticating: false })); } } }; initializeBotConsumptionPage(); return () => { mounted = false; }; }, [ isSdkLoaded, placeholder, loginMode, serverUrlLibrary, projectId, objectId, config, username, password ]); return { botConsumptionPage, ...authState }; }; // src/components/BotConsumptionPageWithAuth.tsx import { clsx as clsx6 } from "clsx"; import { useRef as useRef6 } from "react"; import { Fragment as Fragment3, jsx as jsx6 } from "react/jsx-runtime"; var BotConsumptionPageWithAuth = ({ serverUrlLibrary, projectId, objectId, config, loginMode, username, password, className, style, loadingComponent = /* @__PURE__ */ jsx6("div", { children: "Loading..." }), errorComponent = (error) => /* @__PURE__ */ jsx6("div", { children: error }) }) => { const containerRef = useRef6(null); const { isAuthenticated, isAuthenticating, error } = useCreateBotConsumptionPageWithAuth({ serverUrlLibrary, placeholder: containerRef.current, projectId, objectId, config, loginMode, username, password }); if (isAuthenticating) { return /* @__PURE__ */ jsx6(Fragment3, { children: loadingComponent }); } if (error) { return /* @__PURE__ */ jsx6(Fragment3, { children: errorComponent(error) }); } if (!isAuthenticated) { return null; } return /* @__PURE__ */ jsx6( "div", { ref: containerRef, className: clsx6(className), style: { width: "100%", height: "600px", ...style } } ); }; export { BotConsumptionPage, BotConsumptionPageWithAuth, DashboardEmbed, DashboardEmbedWithAuth, EVENT_TYPE, LibraryPageEmbed, LibraryPageEmbedWithAuth, getAuthToken, getInfoFromUrl, getServerUrl, useCreateBotConsumptionPage, useCreateBotConsumptionPageWithAuth, useCreateDashboard, useCreateDashboardWithAuth, useCreateLibraryPage, useCreateLibraryPageWithAuth, useLoadMstrSDK };