diff --git a/visual-embed/pre-rendering/src/App.css b/visual-embed/pre-rendering/src/App.css index 8e943d6..835d39b 100644 --- a/visual-embed/pre-rendering/src/App.css +++ b/visual-embed/pre-rendering/src/App.css @@ -1,44 +1,550 @@ #root { + margin: 0; + width: 100%; + height: 100%; + text-align: left; +} + +.app-layout { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* Top Nav */ +.top-nav { + display: flex; + align-items: center; + gap: 4px; + padding: 10px 16px; + background: #111118; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + overflow-x: auto; + flex-shrink: 0; +} + +.nav-home { + font-weight: 700; + font-size: 14px; + color: #fff !important; + margin-right: 8px; + white-space: nowrap; + text-decoration: none; +} + +.nav-link { + padding: 5px 10px; + border-radius: 6px; + font-size: 12px; + white-space: nowrap; + color: #aaa; + text-decoration: none; + transition: all 0.15s; +} + +.nav-link:hover { + background: color-mix(in srgb, var(--accent) 20%, transparent); + color: var(--accent); +} + +/* Home Page */ +.home { + padding: 48px 40px; + max-width: 960px; margin: 0 auto; - text-align: center; + width: 100%; +} + +.home h1 { + font-size: 2.4em; + font-weight: 700; + margin: 0 0 10px; + line-height: 1.2; +} + +.subtitle { + color: #888; + font-size: 1.05em; + margin: 0 0 40px; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; +.examples-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; } -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); + +.example-card { + display: block; + padding: 22px; + border-radius: 10px; + background: #1a1a26; + border: 1px solid rgba(255, 255, 255, 0.07); + border-left: 3px solid var(--accent); + transition: transform 0.15s, box-shadow 0.15s; + text-decoration: none; + color: inherit; + cursor: pointer; } -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); + +.example-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); } -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +.readme-details { + margin-top: 14px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding-top: 10px; } -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } +.readme-details summary { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--accent); + cursor: pointer; + user-select: none; + list-style: none; +} + +.readme-details summary::after { + content: " ↓"; +} + +.readme-details[open] summary::after { + content: " ↑"; +} + +.readme-content { + margin: 10px 0 0; + font-size: 11px; + line-height: 1.65; + white-space: pre-wrap; + word-break: break-word; + color: #888; + font-family: monospace; + max-height: 320px; + overflow-y: auto; } -.card { - padding: 2em; +.example-card h3 { + margin: 0 0 8px; + font-size: 0.95em; + color: var(--accent); } -.read-the-docs { +.example-card p { + margin: 0; + font-size: 0.82em; color: #888; + line-height: 1.55; +} + +/* Sub-nav for nested examples */ +.example-layout { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.sub-nav { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 16px; + background: color-mix(in srgb, var(--accent) 12%, #111118); + border-bottom: 1px solid color-mix(in srgb, var(--accent) 25%, transparent); + flex-shrink: 0; +} + +.back-link { + font-size: 12px; + color: #aaa !important; + text-decoration: none; +} + +.back-link:hover { + color: #fff !important; } -a { - margin: 0 10px; -} \ No newline at end of file +.divider { + width: 1px; + height: 14px; + background: rgba(255, 255, 255, 0.2); +} + +.example-title { + font-size: 13px; + font-weight: 600; + color: var(--accent); + flex: 1; +} + +.sub-link { + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + border: 1px solid color-mix(in srgb, var(--accent) 60%, transparent); + color: var(--accent); + text-decoration: none; + transition: all 0.15s; +} + +.sub-link:hover { + background: var(--accent); + color: #000; + border-color: var(--accent); +} + +/* Embed */ +.embed-div { + flex: 1; + width: 100%; + height: 100%; +} + +/* Pre-Render Home Page */ +.pre-render-home { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.pre-render-row { + display: flex; + flex-direction: row; + flex: 1; + min-height: 0; +} + +.pre-render-status { + display: flex; + flex: 1; + min-height: 0; + align-items: center; + justify-content: center; +} + +.status-badge { + display: flex; + align-items: center; + gap: 10px; + padding: 16px 28px; + border-radius: 12px; + font-size: 1em; + font-weight: 500; + transition: background 0.4s, border-color 0.4s, color 0.4s; + max-height: 50%; +} + +.status-badge.loading { + background: rgba(243, 156, 18, 0.12); + border: 1px solid rgba(243, 156, 18, 0.35); + color: #F39C12; +} + +.status-badge.loaded { + background: rgba(46, 204, 113, 0.12); + border: 1px solid rgba(46, 204, 113, 0.35); + color: #2ECC71; +} + +.status-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.status-badge.loading .status-dot { + animation: pulse 1.4s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.3; transform: scale(0.8); } +} + +.status-hint { + color: #666; + font-size: 0.82em; + margin: 0; + padding: 12px 24px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + flex-shrink: 0; +} + +/* Event Log */ +.event-log { + width: 300px; + display: flex; + flex-direction: column; + border-left: 1px solid rgba(255, 255, 255, 0.08); + overflow: hidden; + background: #0d0d14; + flex-shrink: 0; +} + +.event-log-title { + display: block; + padding: 8px 14px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #555; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.event-log-empty { + margin: 0; + padding: 12px 14px; + font-size: 12px; + color: #444; +} + +.event-list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + flex: 1; +} + +.event-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px 14px; + font-size: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); + animation: fadeIn 0.2s ease; +} + +.event-item:last-child { + border-bottom: none; +} + +.event-type { + font-family: monospace; + color: #c0c0c0; +} + +.event-ts { + color: #444; + font-size: 11px; + flex-shrink: 0; + margin-left: 12px; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} + +@media (prefers-color-scheme: light) { + .event-log { background: #f5f5f5; border-color: rgba(0,0,0,0.08); } + .event-log-title { color: #aaa; border-color: rgba(0,0,0,0.06); } + .event-log-empty { color: #bbb; } + .event-item { border-color: rgba(0,0,0,0.05); } + .event-type { color: #333; } + .event-ts { color: #bbb; } +} + +/* Render time badge in sub-nav */ +.render-time { + font-size: 12px; + color: var(--accent); + background: color-mix(in srgb, var(--accent) 15%, transparent); + border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); + padding: 3px 10px; + border-radius: 20px; + font-variant-numeric: tabular-nums; +} + +/* Nav toggle (checkbox in sub-nav) */ +.nav-toggle { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #aaa; + cursor: pointer; + user-select: none; + margin-left: auto; +} + +.nav-toggle input { + accent-color: var(--accent, #646cff); + cursor: pointer; +} + +/* Custom loader */ +.liveboard-wrapper { + position: relative; + flex: 1; + min-height: 0; +} + +.custom-loader { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + background: #111118; + z-index: 1; +} + +.custom-loader p { + margin: 0; + font-size: 14px; + color: #666; +} + +.loader-spinner { + width: 36px; + height: 36px; + border: 3px solid rgba(255, 255, 255, 0.08); + border-top-color: var(--accent, #646cff); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@media (prefers-color-scheme: light) { + .custom-loader { background: #fff; } + .loader-spinner { border-color: rgba(0,0,0,0.08); border-top-color: var(--accent, #646cff); } + .nav-toggle { color: #666; } +} + +/* Full Height Example */ +.full-height-example { + display: flex; + flex-direction: column; +} + +.full-height-banner { + padding: 24px 32px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.banner-title-row { + display: flex; + align-items: center; + gap: 12px; + margin: 0 0 6px; +} + +.banner-title-row h2 { + margin: 0; + font-size: 1.2em; + color: #fff; +} + +.full-height-banner p { + margin: 0; + font-size: 0.875em; + color: #aaa; +} + +.full-height-footer { + padding: 24px 32px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + font-size: 0.875em; + color: #aaa; +} + +.full-height-footer p { margin: 0; } + +.full-height-footer code { + background: rgba(255, 255, 255, 0.08); + padding: 1px 5px; + border-radius: 4px; + font-size: 0.9em; +} + +@media (prefers-color-scheme: light) { + .banner-title-row h2 { color: #111; } + .full-height-banner p, + .full-height-footer { color: #666; } + .full-height-banner { border-color: rgba(0, 0, 0, 0.08); } + .full-height-footer { border-color: rgba(0, 0, 0, 0.08); } + .full-height-footer code { background: rgba(0, 0, 0, 0.06); } +} + +/* Liveboard example wrapper with footer */ +.liveboard-example { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.liveboard-footer { + display: flex; + gap: 32px; + align-items: center; + padding: 10px 20px; + background: #0d0d14; + border-top: 1px solid rgba(255, 255, 255, 0.07); + font-size: 12px; + color: #888; + flex-shrink: 0; +} + +.liveboard-footer strong { + color: #ccc; + margin-right: 4px; +} + +.liveboard-footer code { + background: rgba(255, 255, 255, 0.08); + padding: 1px 5px; + border-radius: 4px; + font-size: 0.9em; + color: #aaa; +} + +@media (prefers-color-scheme: light) { + .liveboard-footer { background: #f5f5f5; border-color: rgba(0,0,0,0.07); } + .liveboard-footer strong { color: #333; } + .liveboard-footer code { background: rgba(0,0,0,0.06); color: #555; } +} + +/* Loading */ +.loading { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + font-size: 1em; + color: #555; + letter-spacing: 0.03em; +} + +@media (prefers-color-scheme: light) { + .app-layout { background: #fafafa; } + .top-nav { background: #fff; border-color: rgba(0, 0, 0, 0.08); } + .nav-home { color: #111 !important; } + .nav-link { color: #555; } + .nav-link:hover { color: var(--accent); } + .home h1 { color: #111; } + .subtitle { color: #666; } + .example-card { background: #fff; border-color: rgba(0, 0, 0, 0.07); box-shadow: 0 1px 4px rgba(0,0,0,0.06); } + .example-card p { color: #666; } + .readme-details { border-color: rgba(0,0,0,0.06); } + .readme-content { color: #888; } + .sub-nav { background: color-mix(in srgb, var(--accent) 8%, #fff); } + .loading { color: #aaa; } +} diff --git a/visual-embed/pre-rendering/src/App.tsx b/visual-embed/pre-rendering/src/App.tsx index 472caff..339aa5b 100644 --- a/visual-embed/pre-rendering/src/App.tsx +++ b/visual-embed/pre-rendering/src/App.tsx @@ -1,73 +1,125 @@ import { useEffect, useState } from "react"; import "./App.css"; -import { BrowserRouter, Link, Outlet, Route, Routes } from "react-router"; +import { BrowserRouter, Link, Navigate, Outlet, Route, Routes, useNavigate, useOutletContext } from "react-router"; +import normalReadme from "./examples/normal/README.md?raw"; +import normalLiveboardReadme from "./examples/normal-liveboard/README.md?raw"; +import preRenderReadme from "./examples/pre-render/README.md?raw"; +import preRenderOnDemandReadme from "./examples/pre-render-on-demand/README.md?raw"; +import preRenderWithIdReadme from "./examples/pre-render-with-liveboard-id/README.md?raw"; +import preRenderWithoutIdReadme from "./examples/pre-render-without-liveboard-id/README.md?raw"; +import preRenderFullHeightReadme from "./examples/pre-render-full-height/README.md?raw"; +import preRenderFullHeightNoIdReadme from "./examples/pre-render-full-height-no-id/README.md?raw"; +import PreRenderHome from "./PreRenderHome"; +import NormalEmbed from "./examples/normal"; +import PreRenderEmbed from "./examples/pre-render"; +import PreRenderEmbedOnDemand from "./examples/pre-render-on-demand"; +import PreRenderLiveboardWithLiveboardId from "./examples/pre-render-with-liveboard-id"; +import { Liveboard1 as PreRenderWithoutId1, Liveboard2 as PreRenderWithoutId2 } from "./examples/pre-render-without-liveboard-id"; +import NormalLiveboardEmbed from "./examples/normal-liveboard"; +import PreRenderWithFullHeight from "./examples/pre-render-full-height"; +import { Liveboard1 as FullHeightNoId1, Liveboard2 as FullHeightNoId2 } from "./examples/pre-render-full-height-no-id"; import { - NormalEmbed, - NormalLiveboardEmbed, - PreRenderEmbed, - PreRenderEmbedOnDemand, - PreRenderLiveboardWithLiveboardId, - PreRenderLiveboardWithoutLiveboardId_1, - PreRenderLiveboardWithoutLiveboardId_2, -} from "./embeds"; -import { - PreRenderedAppEmbed, AuthType, AuthStatus, useInit, - PreRenderedLiveboardEmbed, } from "@thoughtspot/visual-embed-sdk/react"; -const routesData = [ +type SubRoute = { path: string; title: string; element: React.ReactElement }; +type RouteData = { + path: string; + title: string; + description: string; + color: string; + readme: string; + element?: React.ReactElement; + children?: SubRoute[]; +}; + +const routesData: RouteData[] = [ { - path: "/normal", + path: "normal", title: "Normal Embed", - description: - "Normal Render is the default behavior of the ThoughtSpot SDK. Loads the ThoughtSpot app when the component is rendered.", + description: "Default SDK behavior — reloads ThoughtSpot on every visit.", + color: "#4A90E2", + readme: normalReadme, element: , }, { - path: "/pre-render", + path: "pre-render", title: "Pre-Render Embed", - description: - "Pre-Renders Embed when `` is rendered.", - element: , + description: "Starts loading the liveboard in the background. Navigate to the liveboard for an instant load.", + color: "#2ECC71", + readme: preRenderReadme, + children: [ + { path: "home", title: "Home", element: }, + { path: "liveboard", title: "View Liveboard", element: }, + ], }, { - path: "/pre-render-on-demand", - title: "Pre-Render On Demand Embed", - description: - "Pre-Renders Embed only when the Embed is rendered at least once.", - element: , + path: "pre-render-on-demand", + title: "Pre-Render On Demand", + description: "Starts loading on first visit; all subsequent visits use the cached instance.", + color: "#F39C12", + readme: preRenderOnDemandReadme, + children: [ + { path: "home", title: "Home", element: }, + { path: "liveboard", title: "View Liveboard", element: }, + ], }, { - path: "/pre-render-with-liveboard-id", - title: "Pre-Render Liveboard With Liveboard Id", - description: - "Pre-Renders a liveboard Embed when the is rendered.", - element: , + path: "pre-render-with-liveboard-id", + title: "Pre-Render Liveboard (With ID)", + description: "Pre-renders a specific liveboard by ID for instant load.", + color: "#9B59B6", + readme: preRenderWithIdReadme, + children: [ + { path: "home", title: "Home", element: }, + { path: "liveboard", title: "View Liveboard", element: }, + ], }, { - path: "/pre-render-without-liveboard-id-1", - title: "Pre-Render Liveboard Without Liveboard Id 1", - description: - "Pre-Renders a generic Embed when the is rendered. The liveboardId is passed when the Embed is rendered and we navigate to the liveboard. Since this is a generic pre-render we just load the basic assets needed for rendering the app, this might not be as fast as pre-rendering with liveboardId but it is faster than normal rendering.", - element: , + path: "pre-render-without-liveboard-id", + title: "Pre-Render Liveboard (Without ID)", + description: "Pre-renders a generic shell; reuse it across multiple liveboards.", + color: "#1ABC9C", + readme: preRenderWithoutIdReadme, + children: [ + { path: "home", title: "Home", element: }, + { path: "liveboard-1", title: "Liveboard 1", element: }, + { path: "liveboard-2", title: "Liveboard 2", element: }, + ], }, { - path: "/pre-render-without-liveboard-id-2", - title: "Pre-Render Liveboard Without Liveboard Id 2", - description: - "This is same as above but we can reuse the same pre-render shell here.", - element: , - }, - { - path: "/normal-liveboard", + path: "normal-liveboard", title: "Normal Liveboard", - description: - "Normal Render is the default behavior of the ThoughtSpot SDK. Loads the ThoughtSpot app when the component is rendered.", + description: "Default LiveboardEmbed behavior — reloads the liveboard on every visit.", + color: "#E74C3C", + readme: normalLiveboardReadme, element: , }, + { + path: "pre-render-full-height", + title: "Pre-Render + Full Height", + description: "Pre-renders the liveboard in the background with fullHeight enabled — expands to fit all content.", + color: "#E67E22", + readme: preRenderFullHeightReadme, + children: [ + { path: "home", title: "Home", element: }, + { path: "liveboard", title: "View Liveboard", element: }, + ], + }, + { + path: "pre-render-full-height-no-id", + title: "Pre-Render Full Height (No ID)", + description: "Pre-renders a generic shell with fullHeight — reuse across multiple liveboards without re-initialising.", + color: "#8E44AD", + readme: preRenderFullHeightNoIdReadme, + children: [ + { path: "home", title: "Home", element: }, + { path: "liveboard-1", title: "Liveboard 1", element: }, + { path: "liveboard-2", title: "Liveboard 2", element: }, + ], + }, ]; const EmbedInit = ({ children }: { children: React.ReactNode }) => { @@ -86,69 +138,114 @@ const EmbedInit = ({ children }: { children: React.ReactNode }) => { } }, []); - if (loading) { - return
Loading...
; - } + if (loading) return
Connecting to ThoughtSpot...
; - return ( -
- - - - {children} -
- ); + return <>{children}; }; const Home = () => { + const navigate = useNavigate(); return ( - <> +

ThoughtSpot Pre-Rendering

- {routesData.map(({ path, title, description }) => ( -
- {title} -

{description}

-
- ))} - +

Explore different embedding strategies and their performance trade-offs.

+
+ {routesData.map(({ path, title, description, color, readme }) => ( +
navigate(`/${path}`)} + > +

{title}

+

{description}

+
e.stopPropagation()}> + How it's implemented +
{readme}
+
+
+ ))} +
+
); }; -const Layout = () => { +const ExampleLayout = ({ route }: { route: RouteData }) => ( +
+
+ ← Home + + {route.title} + {route.children?.map((child) => ( + {child.title} + ))} +
+ +
+); + +export type LoaderContext = { showLoader: boolean }; + +const LoaderLayout = ({ route }: { route: RouteData }) => { + const [showLoader, setShowLoader] = useState(false); return ( -
- - - + +
+ ); }; -const App = () => { - return ( - - - - }> - } /> - {routesData.map(({ path, element }) => ( - - ))} - - - - - ); -}; +const Layout = () => ( +
+ + +
+); + +const App = () => ( + + + + }> + } /> + {routesData.map((route) => { + if (route.children) { + const loaderRoutes = new Set(["pre-render-full-height-no-id", "pre-render-without-liveboard-id"]); + const Layout = loaderRoutes.has(route.path) ? LoaderLayout : ExampleLayout; + return ( + }> + } /> + {route.children.map((child) => ( + + ))} + + ); + } + return ; + })} + + + + +); export default App; diff --git a/visual-embed/pre-rendering/src/PreRenderHome.tsx b/visual-embed/pre-rendering/src/PreRenderHome.tsx new file mode 100644 index 0000000..85a4b99 --- /dev/null +++ b/visual-embed/pre-rendering/src/PreRenderHome.tsx @@ -0,0 +1,59 @@ +import { useState, useEffect, useRef } from "react"; +import { PreRenderedLiveboardEmbed, EmbedEvent } from "@thoughtspot/visual-embed-sdk/react"; + +type EmbedEventEntry = { type: string; ts: string }; + +const PreRenderHome = ({ preRenderId, liveboardId }: { preRenderId: string; liveboardId?: string }) => { + const [rendered, setRendered] = useState(false); + const [events, setEvents] = useState([]); + const embedRef = useRef(null); + + useEffect(() => { + embedRef.current?.on(EmbedEvent.ALL, (payload: any) => { + const type = payload?.type; + if (!type) return; + setEvents((prev) => [{ type, ts: new Date().toLocaleTimeString() }, ...prev].slice(0, 10)); + }); + }, []); + + return ( +
+ setRendered(true)} + /> +
+
+
+ + {rendered ? "Liveboard loaded in the background" : "Pre-rendering liveboard in the background..."} +
+
+
+ Embed Events + {events.length === 0 ? ( +

Waiting for events...

+ ) : ( +
    + {events.map((e, i) => ( +
  • + {e.type} + {e.ts} +
  • + ))} +
+ )} +
+
+

+ {rendered + ? "Navigate to View Liveboard for an instant load experience." + : "The liveboard will open instantly once pre-rendering is done."} +

+
+ ); +}; + +export default PreRenderHome; diff --git a/visual-embed/pre-rendering/src/embeds.tsx b/visual-embed/pre-rendering/src/embeds.tsx deleted file mode 100644 index 83002b5..0000000 --- a/visual-embed/pre-rendering/src/embeds.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { AppEmbed, LiveboardEmbed } from "@thoughtspot/visual-embed-sdk/react"; - -/** - * This will re-render the thoughtspot instance every time user visits this. - */ -const NormalEmbed = () => { - return ; -}; - -/** - * This will start loading the thoughtspot embed as soon as - * is rendered - * - * The actual embed will only show up when is rendered - * - * The will ensure when user views this its already loaded, and all the subsequent visit to this - * will show the already loaded instance. - * - */ -const PreRenderEmbed = () => { - return ; -}; - -/** - * This will start loading the thoughtspot embed as soon as - * this components is rendered and users may see a loader - * But all the subsequent visit to this will show the already loaded instance. - */ -const PreRenderEmbedOnDemand = () => { - return ( - - ); -}; - - -const PreRenderLiveboardWithLiveboardId = () => { - return ( - - ); -}; - -/** - * Pre-Renders a generic Embed when the - * - * is rendered. - * The liveboardId is passed when the Embed is rendered and we navigate to the liveboard. - * Since this is a generic pre-render we just load the basic assets needed for rendering the app, - * this might not be as fast as pre-rendering with liveboardId but it is faster than normal rendering. - */ -const PreRenderLiveboardWithoutLiveboardId_1 = () => { - return ( - - ); -}; - -/** - * This is same as above but we can reuse the same pre-render shell here. - */ -const PreRenderLiveboardWithoutLiveboardId_2 = () => { - return ( - - ); -}; - - -/** - * Normal Render is the default behavior of the ThoughtSpot SDK. - * Loads the ThoughtSpot app when the component is rendered. - */ -const NormalLiveboardEmbed = () => { - return ( - - ); -}; - -export { - NormalEmbed, - PreRenderEmbed, - PreRenderEmbedOnDemand, - PreRenderLiveboardWithLiveboardId, - PreRenderLiveboardWithoutLiveboardId_1, - PreRenderLiveboardWithoutLiveboardId_2, - NormalLiveboardEmbed -}; diff --git a/visual-embed/pre-rendering/src/examples/normal-liveboard/README.md b/visual-embed/pre-rendering/src/examples/normal-liveboard/README.md new file mode 100644 index 0000000..b9803cf --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/normal-liveboard/README.md @@ -0,0 +1,28 @@ +# Normal Liveboard + +**Route:** `/normal-liveboard` + +Identical behaviour to the Normal Embed example but embeds a different liveboard. Useful as a second cold-load baseline when comparing against the pre-render examples that also target two boards. + +## How it works + +```tsx + setRenderTime(Date.now() - mountTime.current)} +/> +``` + +- Same render-time measurement pattern as the Normal Embed: `mountTime` ref reset in `useEffect`, `onLiveboardRendered` records elapsed time. +- No `preRenderId` — every visit is a cold load. + +## Key prop + +| Prop | Value | +|---|---| +| `liveboardId` | Different board ID from Normal Embed | + +## What to observe + +Compare the load time here against Pre-Render Without Liveboard ID (Liveboard 2), which uses the same board but pre-renders the shell in advance. diff --git a/visual-embed/pre-rendering/src/examples/normal-liveboard/index.tsx b/visual-embed/pre-rendering/src/examples/normal-liveboard/index.tsx new file mode 100644 index 0000000..587b668 --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/normal-liveboard/index.tsx @@ -0,0 +1,33 @@ +import { useState, useEffect, useRef } from "react"; +import { LiveboardEmbed } from "@thoughtspot/visual-embed-sdk/react"; + +const NormalLiveboardEmbed = () => { + const [renderTime, setRenderTime] = useState(null); + const mountTime = useRef(Date.now()); + + useEffect(() => { + mountTime.current = Date.now(); + }, []); + + return ( +
+
+
+

Normal Liveboard

+ {renderTime !== null && Loaded in {(renderTime / 1000).toFixed(2)}s} +
+

No pre-rendering — ThoughtSpot loads fresh on every visit.

+
+ setRenderTime(Date.now() - mountTime.current)} + /> +
+ Navigate away and back to see the full reload cost each time. +
+
+ ); +}; + +export default NormalLiveboardEmbed; diff --git a/visual-embed/pre-rendering/src/examples/normal/README.md b/visual-embed/pre-rendering/src/examples/normal/README.md new file mode 100644 index 0000000..c62cd3e --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/normal/README.md @@ -0,0 +1,29 @@ +# Normal Embed + +**Route:** `/normal` + +A plain `LiveboardEmbed` with no pre-rendering. ThoughtSpot initialises a fresh iframe on every visit — there is no cached state between navigations. + +## How it works + +```tsx + setRenderTime(Date.now() - mountTime.current)} +/> +``` + +- `mountTime` is captured in a `useRef` inside `useEffect` so it resets on every mount. +- `onLiveboardRendered` fires once the SDK signals the liveboard is visible. The difference between those two timestamps is the cold-load time shown in the header badge. +- No `preRenderId` — the SDK creates and destroys the iframe on each mount/unmount. + +## Key prop + +| Prop | Value | +|---|---| +| `liveboardId` | Fixed board ID | + +## What to observe + +Navigate away and back. The render time resets and climbs again each time, showing the full cost of a cold load. Compare this against any pre-render example to see the difference. diff --git a/visual-embed/pre-rendering/src/examples/normal/index.tsx b/visual-embed/pre-rendering/src/examples/normal/index.tsx new file mode 100644 index 0000000..fe643e2 --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/normal/index.tsx @@ -0,0 +1,36 @@ +import { useState, useEffect, useRef, useCallback } from "react"; +import { LiveboardEmbed } from "@thoughtspot/visual-embed-sdk/react"; + +const NormalEmbed = () => { + const [renderTime, setRenderTime] = useState(null); + const mountTime = useRef(Date.now()); + + useEffect(() => { + mountTime.current = Date.now(); + }, []); + const updateTime = useCallback(() => { + setRenderTime(Date.now() - mountTime.current) + }, []); + + return ( +
+
+
+

Normal Embed

+ {renderTime !== null && Loaded in {(renderTime / 1000).toFixed(2)}s} +
+

No pre-rendering — ThoughtSpot loads fresh on every visit.

+
+ +
+ Navigate away and back to see the full reload cost each time. +
+
+ ); +}; + +export default NormalEmbed; diff --git a/visual-embed/pre-rendering/src/examples/pre-render-full-height-no-id/README.md b/visual-embed/pre-rendering/src/examples/pre-render-full-height-no-id/README.md new file mode 100644 index 0000000..ba0f10f --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-full-height-no-id/README.md @@ -0,0 +1,51 @@ +# Pre-Render Full Height (No ID) + +**Routes:** `/pre-render-full-height-no-id/home` · `/pre-render-full-height-no-id/liveboard-1` · `/pre-render-full-height-no-id/liveboard-2` + +Combines the "no ID" shell pattern with `fullHeight`. One generic pre-rendered shell is reused across two liveboards, both rendered at full content height. + +## How it works + +### Home tab — generic shell (`PreRenderHome`) + +```tsx + +``` + +A shell iframe is created in the background without loading any specific board. + +### Liveboard tabs — consumers with `fullHeight` + +```tsx + +``` + +Each tab claims the same shell and supplies its own `liveboardId`. `fullHeight` makes the iframe grow to fit that board's content. + +## Custom loader & render time + +State is shared from the `LoaderLayout` (sub-nav) down to each component via React Router's outlet context (`LoaderContext`): + +```tsx +// In LoaderLayout (App.tsx) + + +// In each liveboard component +const { showLoader } = useOutletContext(); +``` + +- **Custom loader** — toggled by a checkbox in the sub-nav. When enabled, an absolutely positioned overlay (`position: absolute; inset: 0`) covers the `liveboard-wrapper` until `onLiveboardRendered` fires. The checkbox state persists across tab switches because it lives in the parent layout, not the child component. +- **Render time** — each component records `Date.now()` on mount (via `useRef` in `useEffect`) and computes elapsed time when `onLiveboardRendered` fires. Displayed as a badge inline with the `

` in the banner. + +## What to observe + +1. Enable the custom loader checkbox, then switch tabs — the overlay appears on each navigation until that board finishes rendering. +2. The render time resets on every tab switch, reflecting how fast the shell can swap boards. +3. Compare the time here against the Normal Liveboard example to see the shell-reuse benefit. diff --git a/visual-embed/pre-rendering/src/examples/pre-render-full-height-no-id/index.tsx b/visual-embed/pre-rendering/src/examples/pre-render-full-height-no-id/index.tsx new file mode 100644 index 0000000..02b4c82 --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-full-height-no-id/index.tsx @@ -0,0 +1,94 @@ +import { useState, useEffect, useRef } from "react"; +import { LiveboardEmbed } from "@thoughtspot/visual-embed-sdk/react"; +import { useOutletContext } from "react-router"; +import type { LoaderContext } from "../../App"; + +export const Liveboard1 = () => { + const { showLoader } = useOutletContext(); + const [loaded, setLoaded] = useState(false); + const [renderTime, setRenderTime] = useState(null); + const mountTime = useRef(Date.now()); + + useEffect(() => { + mountTime.current = Date.now(); + setLoaded(false); + setRenderTime(null); + }, []); + + return ( +
+
+
+

Liveboard 1 — Full Height

+ {renderTime !== null && Loaded in {(renderTime / 1000).toFixed(2)}s} +
+

Pre-rendered without a specific ID. The shell is reused across liveboards.

+
+
+ {showLoader && !loaded && ( +
+
+

Loading liveboard...

+
+ )} + { + setLoaded(true); + setRenderTime(Date.now() - mountTime.current); + }} + /> +
+
+

With fullHeight the iframe expands to fit every tile.

+
+
+ ); +}; + +export const Liveboard2 = () => { + const { showLoader } = useOutletContext(); + const [loaded, setLoaded] = useState(false); + const [renderTime, setRenderTime] = useState(null); + const mountTime = useRef(Date.now()); + + useEffect(() => { + mountTime.current = Date.now(); + setLoaded(false); + setRenderTime(null); + }, []); + + return ( +
+
+
+

Liveboard 2 — Full Height

+ {renderTime !== null && Loaded in {(renderTime / 1000).toFixed(2)}s} +
+

Same pre-render shell reused for a different liveboard.

+
+
+ {showLoader && !loaded && ( +
+
+

Loading liveboard...

+
+ )} + { + setLoaded(true); + setRenderTime(Date.now() - mountTime.current); + }} + /> +
+
+

Both liveboards share one pre-render shell — one background iframe, two liveboards.

+
+
+ ); +}; diff --git a/visual-embed/pre-rendering/src/examples/pre-render-full-height/README.md b/visual-embed/pre-rendering/src/examples/pre-render-full-height/README.md new file mode 100644 index 0000000..7d31a6c --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-full-height/README.md @@ -0,0 +1,45 @@ +# Pre-Render + Full Height + +**Routes:** `/pre-render-full-height/home` · `/pre-render-full-height/liveboard` + +Combines pre-rendering with `fullHeight` mode. The iframe expands to match the total height of the liveboard content, making the page scrollable rather than clipping the board to a fixed viewport. + +## How it works + +### Home tab — shell with a fixed board (`PreRenderHome`) + +```tsx + +``` + +Standard pre-rendering — the board loads silently in the background. + +### Liveboard tab — full height consumer + +```tsx + +``` + +`fullHeight` instructs the SDK to observe the iframe's content height and resize the iframe element to match. Combined with pre-rendering, the board appears at full size instantly with no layout jump. + +## Layout + +The component uses `.full-height-example` (a flex column) with a banner at the top and a footer below. Because `fullHeight` grows the iframe to fit content, the page becomes scrollable — the footer is reachable by scrolling past the liveboard. + +## Key prop + +| Prop | Effect | +|---|---| +| `fullHeight` | Iframe resizes to match liveboard content height | +| `preRenderId` | Links to the hidden shell started on the Home tab | + +## What to observe + +Scroll past the iframe — the footer is always reachable. Compare the load time against the Normal Embed to see the pre-render benefit. diff --git a/visual-embed/pre-rendering/src/examples/pre-render-full-height/index.tsx b/visual-embed/pre-rendering/src/examples/pre-render-full-height/index.tsx new file mode 100644 index 0000000..84f57db --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-full-height/index.tsx @@ -0,0 +1,20 @@ +import { LiveboardEmbed } from "@thoughtspot/visual-embed-sdk/react"; + +const PreRenderWithFullHeight = () => ( +
+
+

Pre-Render + Full Height

+

The embed expands to match the full height of the liveboard content.

+
+ +
+

This content sits below the liveboard. With fullHeight the embed grows to fit all tiles.

+
+
+); + +export default PreRenderWithFullHeight; diff --git a/visual-embed/pre-rendering/src/examples/pre-render-on-demand/README.md b/visual-embed/pre-rendering/src/examples/pre-render-on-demand/README.md new file mode 100644 index 0000000..f1864dd --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-on-demand/README.md @@ -0,0 +1,41 @@ +# Pre-Render On Demand + +**Routes:** `/pre-render-on-demand/home` · `/pre-render-on-demand/liveboard` + +Same as Pre-Render Embed, but pre-rendering only begins when the user first visits the Home tab — not at app start. All subsequent visits to the Liveboard tab reuse the cached shell. + +## How it works + +### Home tab — starts pre-rendering on first visit (`PreRenderHome`) + +```tsx + +``` + +The `PreRenderedLiveboardEmbed` mounts when the `/home` route renders for the first time. The SDK creates the hidden iframe at that point. Because React Router keeps the parent layout mounted, the shell persists as you navigate to `/liveboard` and back. + +### Liveboard tab — connects to the shell + +```tsx + +``` + +Identical to the Pre-Render Embed consumer — `preRenderId` is the link. + +## Difference from Pre-Render Embed + +| | Pre-Render Embed | Pre-Render On Demand | +|---|---|---| +| When does loading start? | At app load (global init) | On first visit to `/home` | +| Useful when | You always need this board | You pre-render only if the user reaches this section | + +## What to observe + +Navigate directly to `/liveboard` without visiting `/home` first — it cold-loads. Then go to `/home` and wait for the green badge, then switch to `/liveboard` — instant. diff --git a/visual-embed/pre-rendering/src/examples/pre-render-on-demand/index.tsx b/visual-embed/pre-rendering/src/examples/pre-render-on-demand/index.tsx new file mode 100644 index 0000000..2bd4cd2 --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-on-demand/index.tsx @@ -0,0 +1,11 @@ +import { LiveboardEmbed } from "@thoughtspot/visual-embed-sdk/react"; + +const PreRenderEmbedOnDemand = () => ( + +); + +export default PreRenderEmbedOnDemand; diff --git a/visual-embed/pre-rendering/src/examples/pre-render-with-liveboard-id/README.md b/visual-embed/pre-rendering/src/examples/pre-render-with-liveboard-id/README.md new file mode 100644 index 0000000..9dd824a --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-with-liveboard-id/README.md @@ -0,0 +1,42 @@ +# Pre-Render Liveboard (With ID) + +**Routes:** `/pre-render-with-liveboard-id/home` · `/pre-render-with-liveboard-id/liveboard` + +Pre-renders a specific liveboard by ID. Both the hidden shell and the visible consumer name the same `liveboardId`, so the SDK can begin fetching data for that exact board during pre-rendering. + +## How it works + +### Home tab — shell with a fixed board (`PreRenderHome`) + +```tsx + +``` + +Passing `liveboardId` to `PreRenderedLiveboardEmbed` tells the SDK which board to load inside the hidden iframe. Data fetching starts immediately. + +### Liveboard tab — consumer with the same ID + +```tsx + +``` + +Because the `liveboardId` matches, the SDK promotes the hidden iframe to visible without re-fetching. + +## Difference from Pre-Render Without Liveboard ID + +| | With ID | Without ID | +|---|---|---| +| Shell specificity | One board pre-loaded | Generic shell, board set at show time | +| Can serve multiple boards? | No — shell is tied to one `liveboardId` | Yes — any `liveboardId` can reuse the shell | +| Data pre-fetched? | Yes | No — data loads when `liveboardId` is set on the consumer | + +## What to observe + +The Liveboard tab appears near-instantly after the Home tab signals loaded. The data was already fetched during pre-rendering. diff --git a/visual-embed/pre-rendering/src/examples/pre-render-with-liveboard-id/index.tsx b/visual-embed/pre-rendering/src/examples/pre-render-with-liveboard-id/index.tsx new file mode 100644 index 0000000..9800583 --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-with-liveboard-id/index.tsx @@ -0,0 +1,11 @@ +import { LiveboardEmbed } from "@thoughtspot/visual-embed-sdk/react"; + +const PreRenderLiveboardWithLiveboardId = () => ( + +); + +export default PreRenderLiveboardWithLiveboardId; diff --git a/visual-embed/pre-rendering/src/examples/pre-render-without-liveboard-id/README.md b/visual-embed/pre-rendering/src/examples/pre-render-without-liveboard-id/README.md new file mode 100644 index 0000000..1c6f0dc --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-without-liveboard-id/README.md @@ -0,0 +1,50 @@ +# Pre-Render Liveboard (Without ID) + +**Routes:** `/pre-render-without-liveboard-id/home` · `/pre-render-without-liveboard-id/liveboard-1` · `/pre-render-without-liveboard-id/liveboard-2` + +One pre-rendered shell serves two different liveboards. Because no `liveboardId` is given to the shell, any consumer can claim it and supply its own board ID, actions, and styles. + +## How it works + +### Home tab — generic shell (`PreRenderHome`) + +```tsx + +``` + +The SDK creates a hidden iframe but does not load any board yet. The shell is ready and waiting. + +### Liveboard tabs — consumers supply the board + +```tsx + +``` + +When a consumer mounts, it claims the shell and provides the `liveboardId`. The SDK loads that board into the existing iframe. Each tab also applies its own `hiddenActions` and CSS variable overrides. + +## Per-tab configuration + +| | Liveboard 1 | Liveboard 2 | +|---|---|---| +| `hiddenActions` | Share, Present | Edit, Download as CSV | +| Accent color | `#1976D2` (blue) | `#7B1FA2` (purple) | +| Background | `#0a1929` | `#1a0a2e` | + +## Custom loader & render time + +This example uses `LoaderContext` (via React Router's outlet context) to share state from the sub-nav layout down to each liveboard component: + +- **Custom loader** — a checkbox in the sub-nav toggles an overlay spinner that covers the embed until `onLiveboardRendered` fires. +- **Render time** — recorded from component mount to `onLiveboardRendered`, displayed in the footer. + +## What to observe + +Switch between Liveboard 1 and Liveboard 2. The shell iframe is reused — only the board content and styles change. One background connection, two different views. diff --git a/visual-embed/pre-rendering/src/examples/pre-render-without-liveboard-id/index.tsx b/visual-embed/pre-rendering/src/examples/pre-render-without-liveboard-id/index.tsx new file mode 100644 index 0000000..3267f9e --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render-without-liveboard-id/index.tsx @@ -0,0 +1,110 @@ +import { useState, useEffect, useRef } from "react"; +import { LiveboardEmbed, Action } from "@thoughtspot/visual-embed-sdk/react"; +import { useOutletContext } from "react-router"; +import type { LoaderContext } from "../../App"; + +export const Liveboard1 = () => { + const { showLoader } = useOutletContext(); + const [loaded, setLoaded] = useState(false); + const [renderTime, setRenderTime] = useState(null); + const mountTime = useRef(Date.now()); + + useEffect(() => { + mountTime.current = Date.now(); + setLoaded(false); + setRenderTime(null); + }, []); + + return ( +
+
+ {showLoader && !loaded && ( +
+
+

Loading liveboard...

+
+ )} + { + setLoaded(true); + setRenderTime(Date.now() - mountTime.current); + }} + /> +
+
+ {renderTime !== null && Loaded in {(renderTime / 1000).toFixed(2)}s} + hiddenActions: Share, Present + customizations: Blue accent — --ts-var-root-color: #1976D2 · background --ts-var-root-background: #0a1929 +
+
+ ); +}; + +export const Liveboard2 = () => { + const { showLoader } = useOutletContext(); + const [loaded, setLoaded] = useState(false); + const [renderTime, setRenderTime] = useState(null); + const mountTime = useRef(Date.now()); + + useEffect(() => { + mountTime.current = Date.now(); + setLoaded(false); + setRenderTime(null); + }, []); + + return ( +
+
+ {showLoader && !loaded && ( +
+
+

Loading liveboard...

+
+ )} + { + setLoaded(true); + setRenderTime(Date.now() - mountTime.current); + }} + /> +
+
+ {renderTime !== null && Loaded in {(renderTime / 1000).toFixed(2)}s} + hiddenActions: Edit, Download as CSV + customizations: Purple accent — --ts-var-root-color: #7B1FA2 · background --ts-var-root-background: #1a0a2e +
+
+ ); +}; diff --git a/visual-embed/pre-rendering/src/examples/pre-render/README.md b/visual-embed/pre-rendering/src/examples/pre-render/README.md new file mode 100644 index 0000000..f17b382 --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render/README.md @@ -0,0 +1,43 @@ +# Pre-Render Embed + +**Routes:** `/pre-render/home` · `/pre-render/liveboard` + +Demonstrates the core pre-rendering pattern. The liveboard starts loading in the background as soon as you land on the Home tab. Switching to the Liveboard tab connects to the already-running iframe — no restart. + +## How it works + +### Home tab — starts pre-rendering (`PreRenderHome`) + +```tsx + +``` + +`PreRenderedLiveboardEmbed` tells the SDK to create a hidden iframe in `` and begin loading the liveboard immediately. The iframe is identified by `preRenderId`. + +### Liveboard tab — connects to the shell + +```tsx + +``` + +When `preRenderId` is present and matches an active shell, the SDK moves the existing iframe into this component's DOM position instead of creating a new one. The board appears instantly. + +## Key props + +| Prop | Where | Purpose | +|---|---|---| +| `preRenderId` | Both | Links the shell to its consumer — must match exactly | +| `liveboardId` | Both | The board to load; set on both ends | + +## What to observe + +1. Open Home — watch the event log and status badge while the board loads silently. +2. Once the badge turns green, switch to Liveboard. It appears immediately. +3. Switch back to Home and then to Liveboard again — still instant, the shell is still alive. diff --git a/visual-embed/pre-rendering/src/examples/pre-render/index.tsx b/visual-embed/pre-rendering/src/examples/pre-render/index.tsx new file mode 100644 index 0000000..da5df72 --- /dev/null +++ b/visual-embed/pre-rendering/src/examples/pre-render/index.tsx @@ -0,0 +1,11 @@ +import { LiveboardEmbed } from "@thoughtspot/visual-embed-sdk/react"; + +const PreRenderEmbed = () => ( + +); + +export default PreRenderEmbed; diff --git a/visual-embed/pre-rendering/src/index.css b/visual-embed/pre-rendering/src/index.css index bfc7c6d..3c802f8 100644 --- a/visual-embed/pre-rendering/src/index.css +++ b/visual-embed/pre-rendering/src/index.css @@ -28,8 +28,6 @@ a:hover { body { margin: 0; - display: flex; - place-items: center; min-width: 320px; height: 100vh; } @@ -71,15 +69,7 @@ button:focus-visible { } } -.embed-init, .embed-div, #root { +#root { width: 100%; height: 100%; -} -nav { - display: flex; -} -nav a { - width: 100%; - height: 100%; - text-align: center; } \ No newline at end of file