React SDK
@concepttocloud/saiku-embed-react is a typed React wrapper around
the existing <saiku-embed> custom element. The element
already works in React — React 18+ forwards unknown attributes straight
through to the DOM — but React teams evaluate against npm install +
typed imports, not <script> tags. This SDK closes that gap without
changing the underlying runtime.
Ships in saiku v4.7 as saiku#1432.
Install
npm install @concepttocloud/saiku-embed-react reactThe package peer-depends on React 18+. The base @concepttocloud/saiku-embed is a runtime dependency and gets pulled in automatically.
Use
import { SaikuEmbed } from "@concepttocloud/saiku-embed-react";
function Dashboard({ token }: { token: string }) { return ( <SaikuEmbed server="https://YOUR-WORKSPACE.saiku.bi" token={token} path="homes/admin/Sales.saiku" render="chart" mode="bar" height="480px" /> );}Importing the package has the side effect of registering the
underlying custom element — you don’t need a separate
import "@concepttocloud/saiku-embed" unless you also want the tag
available outside of the React tree.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
server | string | optional | Origin of the Saiku launcher. Omit for same-origin embeds. |
path | string | required | Query path / dashboard path / cube ref depending on kind. |
kind | "query" | "dashboard" | "ai" | "query" | Selects the embed flavour. |
token | string | optional | Embed token minted server-side. Omit for public grants. |
render | "table" | "matrix" | "chart" | "table" | Only meaningful for kind="query". |
mode | "bar" | "line" | "pie" | "bar" | Only meaningful for render="chart". |
height | string | "400px" | CSS height of the rendered surface. |
style | React.CSSProperties | optional | Standard React style prop. |
className | string | optional | Standard React className prop. |
id | string | optional | Passed through for e2e selectors. |
data-testid | string | optional | Passed through to the DOM node. |
Full type declarations ship in the package’s index.d.ts.
Kinds
Saved query as a table
<SaikuEmbed server="https://saiku.example.com" token={token} path="homes/admin/Sales.saiku" height="400px"/>Chart
<SaikuEmbed server="https://saiku.example.com" token={token} path="homes/admin/Sales.saiku" render="chart" mode="bar" height="500px"/>Matrix
Matrix mode preserves the row / column axis structure — measures on
columns, dimension members on rows — instead of flattening to a single
row-key map like render="table" does. Useful for pivot-style reports.
<SaikuEmbed server="https://saiku.example.com" token={token} path="homes/admin/Sales.saiku" render="matrix" height="500px"/>AI ask widget
Point the token at a cube (rather than a saved query) and drop in a plain-English ask box. Requires an AI-kind embed token and a launcher with an LLM provider configured.
<SaikuEmbed server="https://saiku.example.com" token={aiToken} kind="ai" path="foodmart/FoodMart/FoodMart/Sales" height="240px"/>Saved dashboard
<SaikuEmbed server="https://saiku.example.com" token={token} kind="dashboard" path="homes/admin/exec.saikudash" height="700px"/>Anonymous public embed
If the resource is marked publicly embeddable on the server (see Public embeds), omit the token entirely:
<SaikuEmbed server="https://saiku.example.com" path="shared/public-chart.saiku" render="chart"/>Minting a token from your server
mintEmbedToken() is a Node / edge-function helper for the very
common case of minting an embed token on behalf of an end user before
rendering <SaikuEmbed>.
import { mintEmbedToken } from "@concepttocloud/saiku-embed-react";
export async function POST(request: Request) { const auth = "Basic " + Buffer.from( `${process.env.SAIKU_USER}:${process.env.SAIKU_PASS}`, ).toString("base64");
const { token, expiresAt } = await mintEmbedToken({ server: process.env.SAIKU_URL!, authorization: auth, resourceKind: "query", resourcePath: "homes/admin/Sales.saiku", ttlHours: 24, label: "Public marketing page", });
return Response.json({ token, expiresAt });}import { mintEmbedToken } from "@concepttocloud/saiku-embed-react";
export async function loader() { const auth = "Basic " + Buffer.from( `${process.env.SAIKU_USER}:${process.env.SAIKU_PASS}`, ).toString("base64");
const { token } = await mintEmbedToken({ server: process.env.SAIKU_URL!, authorization: auth, resourceKind: "query", resourcePath: "homes/admin/Sales.saiku", });
return { token };}import { mintEmbedToken } from "@concepttocloud/saiku-embed-react";
export default { async fetch(request: Request, env: Env) { const auth = "Basic " + btoa(`${env.SAIKU_USER}:${env.SAIKU_PASS}`); const { token } = await mintEmbedToken({ server: env.SAIKU_URL, authorization: auth, resourceKind: "query", resourcePath: "homes/admin/Sales.saiku", fetch: fetch.bind(globalThis), }); return Response.json({ token }); },};Client-side the token lands in the token prop:
"use client";import useSWR from "swr";import { SaikuEmbed } from "@concepttocloud/saiku-embed-react";
export default function EmbedTile() { const { data } = useSWR("/api/embed-token", (u) => fetch(u, { method: "POST" }).then((r) => r.json())); if (!data?.token) return <div>Loading…</div>; return <SaikuEmbed server="…" token={data.token} path="homes/admin/Sales.saiku" />;}mintEmbedToken options
| Option | Type | Notes |
|---|---|---|
server | string | Base URL of the launcher. |
authorization | string | Value for the Authorization header (Basic … or Bearer …). |
resourceKind | "query" | "dashboard" | "ai" | Kind of resource the token pins. |
resourcePath | string | Path (for query/dashboard) or cube ref (for ai). |
ttlHours | number (optional) | Token lifetime; server default is 72h. |
label | string (optional) | Human label the admin UI shows next to the token. |
fetch | typeof fetch (optional) | Overridable for tests + non-browser runtimes (e.g. Cloudflare Workers). |
Returns { token, expiresAt }. Throws if the server returns non-2xx
or the response body isn’t a token envelope.
Autocomplete on the raw tag
The package augments both the global JSX.IntrinsicElements
(React 17/18) and React.JSX.IntrinsicElements (React 19+) so the raw
custom element gets the same typed prop set as <SaikuEmbed>. Use
whichever you prefer:
// Typed React component<SaikuEmbed server="…" token={token} path="…" render="chart" />
// Or the raw custom element (also typed)<saiku-embed server="…" token={token} path="…" render="chart" />Theming
The embed lives inside a shadow root, so host page CSS can’t leak in. Recolour via CSS custom properties on the wrapper:
<SaikuEmbed server="…" token={token} path="…" style={{ "--saiku-embed-fg": "#0f172a", "--saiku-embed-bg": "transparent", "--saiku-embed-border": "#cbd5e1", "--saiku-embed-header-bg": "#f1f5f9", "--saiku-embed-tile-bg": "#ffffff", "--saiku-embed-row-hover": "#e2e8f0", "--saiku-embed-negative": "#b91c1c", } as React.CSSProperties}/>Full list of themable variables is on the base embed page.
Version pinning
The React SDK’s version tracks the base @concepttocloud/saiku-embed
release one-to-one. 3.19.0 of the SDK uses 3.19.0 of the base
runtime — the runtime dep is pinned at release time so a consumer
can never accidentally mix majors across the two.
Bundle size
- Wrapper: ~1 KB gzipped (the whole runtime is a single
React.createElementcall). - Base custom element: ~213 KB gzipped (Svelte 5 CE runtime + ECharts + the embed renderers).
- React: peer dep — not counted against either package.
Non-goals for v1
- React hooks for query results (
useSaikuQuery) — the custom element handles its own state; a typed data hook duplicates the AI Query API surface. Follow-up if someone asks. - Server-component variant —
<SaikuEmbed>is browser-only (it renders a shadow root). Server-rendered initial data for hydration is a follow-up if the pattern proves painful. - Per-kind bundle splitting — the current wrapper always pulls the full base bundle. Tree-shaking by kind is a base-package change, not a wrapper change.