Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
560 changes: 533 additions & 27 deletions visual-embed/pre-rendering/src/App.css

Large diffs are not rendered by default.

285 changes: 191 additions & 94 deletions visual-embed/pre-rendering/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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: <NormalEmbed />,
},
{
path: "/pre-render",
path: "pre-render",
title: "Pre-Render Embed",
description:
"Pre-Renders Embed when `<PreRenderedAppEmbed preRenderId='pre-render' />` is rendered.",
element: <PreRenderEmbed />,
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: <PreRenderHome preRenderId="pre-render" liveboardId="e40c0727-01e6-49db-bb2f-5aa19661477b" /> },
{ path: "liveboard", title: "View Liveboard", element: <PreRenderEmbed /> },
],
},
{
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: <PreRenderEmbedOnDemand />,
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: <PreRenderHome preRenderId="pre-render-on-demand" liveboardId="e40c0727-01e6-49db-bb2f-5aa19661477b" /> },
{ path: "liveboard", title: "View Liveboard", element: <PreRenderEmbedOnDemand /> },
],
},
{
path: "/pre-render-with-liveboard-id",
title: "Pre-Render Liveboard With Liveboard Id",
description:
"Pre-Renders a liveboard Embed when the <PreRenderedLiveboardEmbed liveboardId='<id>' preRenderId='pre-render-with-liveboard-id' /> is rendered.",
element: <PreRenderLiveboardWithLiveboardId />,
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: <PreRenderHome preRenderId="pre-render-with-liveboard-id" liveboardId="e40c0727-01e6-49db-bb2f-5aa19661477b" /> },
{ path: "liveboard", title: "View Liveboard", element: <PreRenderLiveboardWithLiveboardId /> },
],
},
{
path: "/pre-render-without-liveboard-id-1",
title: "Pre-Render Liveboard Without Liveboard Id 1",
description:
"Pre-Renders a generic Embed when the <PreRenderedLiveboardEmbed preRenderId='pre-render-without-liveboard-id' /> 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: <PreRenderLiveboardWithoutLiveboardId_1 />,
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: <PreRenderHome preRenderId="pre-render-without-liveboard-id" /> },
{ path: "liveboard-1", title: "Liveboard 1", element: <PreRenderWithoutId1 /> },
{ path: "liveboard-2", title: "Liveboard 2", element: <PreRenderWithoutId2 /> },
],
},
{
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: <PreRenderLiveboardWithoutLiveboardId_2 />,
},
{
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: <NormalLiveboardEmbed />,
},
{
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: <PreRenderHome preRenderId="pre-render-full-height" liveboardId="e40c0727-01e6-49db-bb2f-5aa19661477b" /> },
{ path: "liveboard", title: "View Liveboard", element: <PreRenderWithFullHeight /> },
],
},
{
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: <PreRenderHome preRenderId="pre-render-full-height-no-id" /> },
{ path: "liveboard-1", title: "Liveboard 1", element: <FullHeightNoId1 /> },
{ path: "liveboard-2", title: "Liveboard 2", element: <FullHeightNoId2 /> },
],
},
];

const EmbedInit = ({ children }: { children: React.ReactNode }) => {
Expand All @@ -86,69 +138,114 @@ const EmbedInit = ({ children }: { children: React.ReactNode }) => {
}
}, []);

if (loading) {
return <div>Loading...</div>;
}
if (loading) return <div className="loading">Connecting to ThoughtSpot...</div>;

return (
<div className="embed-init">
<PreRenderedAppEmbed preRenderId="pre-render" />
<PreRenderedLiveboardEmbed preRenderId="pre-render-without-liveboard-id" />
<PreRenderedLiveboardEmbed
liveboardId="e40c0727-01e6-49db-bb2f-5aa19661477b"
preRenderId="pre-render-with-liveboard-id"
/>
{children}
</div>
);
return <>{children}</>;
};

const Home = () => {
const navigate = useNavigate();
return (
<>
<div className="home">
<h1>ThoughtSpot Pre-Rendering</h1>
{routesData.map(({ path, title, description }) => (
<div className="card" key={path}>
<Link to={path}>{title}</Link>
<p className="read-the-docs">{description}</p>
</div>
))}
</>
<p className="subtitle">Explore different embedding strategies and their performance trade-offs.</p>
<div className="examples-grid">
{routesData.map(({ path, title, description, color, readme }) => (
<div
key={path}
className="example-card"
style={{ "--accent": color } as React.CSSProperties}
onClick={() => navigate(`/${path}`)}
>
<h3>{title}</h3>
<p>{description}</p>
<details className="readme-details" onClick={e => e.stopPropagation()}>
<summary>How it's implemented</summary>
<pre className="readme-content">{readme}</pre>
</details>
</div>
))}
</div>
</div>
);
};

const Layout = () => {
const ExampleLayout = ({ route }: { route: RouteData }) => (
<div className="example-layout" style={{ "--accent": route.color } as React.CSSProperties}>
<div className="sub-nav">
<Link to="/" className="back-link">← Home</Link>
<span className="divider" />
<span className="example-title">{route.title}</span>
{route.children?.map((child) => (
<Link key={child.path} to={child.path} className="sub-link">{child.title}</Link>
))}
</div>
<Outlet />
</div>
);

export type LoaderContext = { showLoader: boolean };

const LoaderLayout = ({ route }: { route: RouteData }) => {
const [showLoader, setShowLoader] = useState(false);
return (
<div className="embed-init">
<nav>
<Link to="/">Home</Link>
{routesData.map(({ path, title }) => (
<Link key={path} to={path}>
{title}
</Link>
<div className="example-layout" style={{ "--accent": route.color } as React.CSSProperties}>
<div className="sub-nav">
<Link to="/" className="back-link">← Home</Link>
<span className="divider" />
<span className="example-title">{route.title}</span>
{route.children?.map((child) => (
<Link key={child.path} to={child.path} className="sub-link">{child.title}</Link>
))}
</nav>

<Outlet />
<label className="nav-toggle">
<input type="checkbox" checked={showLoader} onChange={e => setShowLoader(e.target.checked)} />
Custom loader
</label>
</div>
<Outlet context={{ showLoader } satisfies LoaderContext} />
</div>
);
};

const App = () => {
return (
<BrowserRouter>
<EmbedInit>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
{routesData.map(({ path, element }) => (
<Route key={path} path={path} element={element} />
))}
</Route>
</Routes>
</EmbedInit>
</BrowserRouter>
);
};
const Layout = () => (
<div className="app-layout">
<nav className="top-nav">
<Link to="/" className="nav-home">ThoughtSpot Pre-Rendering</Link>
{routesData.map(({ path, title, color }) => (
<Link key={path} to={`/${path}`} className="nav-link" style={{ "--accent": color } as React.CSSProperties}>
{title}
</Link>
))}
</nav>
<Outlet />
</div>
);

const App = () => (
<BrowserRouter>
<EmbedInit>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
{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 key={route.path} path={route.path} element={<Layout route={route} />}>
<Route index element={<Navigate to="home" replace />} />
{route.children.map((child) => (
<Route key={child.path} path={child.path} element={child.element} />
))}
</Route>
);
}
return <Route key={route.path} path={route.path} element={route.element} />;
})}
</Route>
</Routes>
</EmbedInit>
</BrowserRouter>
);

export default App;
59 changes: 59 additions & 0 deletions visual-embed/pre-rendering/src/PreRenderHome.tsx
Original file line number Diff line number Diff line change
@@ -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<EmbedEventEntry[]>([]);
const embedRef = useRef<any>(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 (
<div className="pre-render-home">
<PreRenderedLiveboardEmbed
ref={embedRef}
liveboardId={liveboardId}
preRenderId={preRenderId}
onLiveboardRendered={() => setRendered(true)}
/>
<div className="pre-render-row">
<div className="pre-render-status">
<div className={`status-badge ${rendered ? "loaded" : "loading"}`}>
<span className="status-dot" />
{rendered ? "Liveboard loaded in the background" : "Pre-rendering liveboard in the background..."}
</div>
</div>
<div className="event-log">
<span className="event-log-title">Embed Events</span>
{events.length === 0 ? (
<p className="event-log-empty">Waiting for events...</p>
) : (
<ul className="event-list">
{events.map((e, i) => (
<li key={i} className="event-item">
<span className="event-type">{e.type}</span>
<span className="event-ts">{e.ts}</span>
</li>
))}
</ul>
)}
</div>
</div>
<p className="status-hint">
{rendered
? "Navigate to View Liveboard for an instant load experience."
: "The liveboard will open instantly once pre-rendering is done."}
</p>
</div>
);
};

export default PreRenderHome;
Loading
Loading