{"_id":"@concepttocloud/saiku-embed","_rev":"4-a77d78b0f596b8d43ddee4ece13c41e1","name":"@concepttocloud/saiku-embed","dist-tags":{"latest":"3.20.0"},"versions":{"3.18.2":{"name":"@concepttocloud/saiku-embed","version":"3.18.2","keywords":["saiku","olap","embed","web-component","custom-element","dashboard","chart"],"license":"Apache-2.0","_id":"@concepttocloud/saiku-embed@3.18.2","maintainers":[{"name":"magicaltroutconcepttocloud","email":"tom@concepttocloud.com"}],"homepage":"https://github.com/spiculedata/saiku","bugs":{"url":"https://github.com/spiculedata/saiku/issues"},"dist":{"shasum":"dece7e537f82b54796fce5c3bc583cd8ce09be9a","tarball":"https://registry.npmjs.org/@concepttocloud/saiku-embed/-/saiku-embed-3.18.2.tgz","fileCount":4,"integrity":"sha512-WXNIF2eFtGWZ5bqkwN4M+TxJ2Gf+VlRoNWMWSDG0g/DPVnXSVwVkeIWLcekqEpxoHaBjXx3nnGRKFOlNY57vDA==","signatures":[{"sig":"MEUCIQCxxLHgwHS3oxqWpMEtawOs/FJHKA7TBQlydLH/6KmLRgIgJfO8sQoNMcyIqhG5MNruU7TgWzGxrWy0M2xB1wEumBQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4278596},"main":"./saiku-embed.js","type":"module","gitHead":"ce9f39a5beebba0d2e14dd3dd15dd14fe66973a3","_npmUser":{"name":"magicaltroutconcepttocloud","email":"tom@concepttocloud.com"},"repository":{"url":"git+https://github.com/spiculedata/saiku.git","type":"git","directory":"saiku-ui/src/embed"},"_npmVersion":"10.9.4","description":"<saiku-embed> Web Component — drop a saved Saiku query or dashboard into any page.","directories":{},"_nodeVersion":"22.21.1","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/saiku-embed_3.18.2_1781120890311_0.5595425245728984","host":"s3://npm-registry-packages-npm-production"}},"3.19.0":{"name":"@concepttocloud/saiku-embed","version":"3.19.0","keywords":["saiku","olap","embed","web-component","custom-element","dashboard","chart"],"license":"Apache-2.0","_id":"@concepttocloud/saiku-embed@3.19.0","maintainers":[{"name":"ameliaux","email":"amelia@concepttocloud.com"},{"name":"magicaltroutconcepttocloud","email":"tom@concepttocloud.com"}],"homepage":"https://github.com/spiculedata/saiku","bugs":{"url":"https://github.com/spiculedata/saiku/issues"},"dist":{"shasum":"85e31d922ba773a70a22c921535b3d3ef948ee13","tarball":"https://registry.npmjs.org/@concepttocloud/saiku-embed/-/saiku-embed-3.19.0.tgz","fileCount":4,"integrity":"sha512-nD1S911NS5cxnn/ARV+MAKVe9n1Zah5yRexq4gJfc49kDWMJV0jlE8KKPWpSrIPHOOp8cZi5UFA5X107yPKsQw==","signatures":[{"sig":"MEUCIQD/1eFc2YLcgSLD/EelaIzPv6i4gnE+xVKyZZcunZwFYgIgK31GUUwwcOcZ43dQST0BAdypXRuoUiZ1r6eVJQ/LRXE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4334962},"main":"./saiku-embed.js","type":"module","gitHead":"1b0d4786a6a2034ec8e422adf87ddfec30df7740","_npmUser":{"name":"magicaltroutconcepttocloud","email":"tom@concepttocloud.com"},"repository":{"url":"git+https://github.com/spiculedata/saiku.git","type":"git","directory":"saiku-ui/src/embed"},"_npmVersion":"10.9.4","description":"<saiku-embed> Web Component — drop a saved Saiku query or dashboard into any page.","directories":{},"_nodeVersion":"22.21.1","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/saiku-embed_3.19.0_1783747184078_0.5885681471696746","host":"s3://npm-registry-packages-npm-production"}},"3.20.0":{"name":"@concepttocloud/saiku-embed","version":"3.20.0","description":"<saiku-embed> Web Component — drop a saved Saiku query or dashboard into any page.","license":"Apache-2.0","type":"module","main":"./saiku-embed.js","keywords":["saiku","olap","embed","web-component","custom-element","dashboard","chart"],"homepage":"https://github.com/spiculedata/saiku","repository":{"type":"git","url":"git+https://github.com/spiculedata/saiku.git","directory":"saiku-ui/src/embed"},"bugs":{"url":"https://github.com/spiculedata/saiku/issues"},"_id":"@concepttocloud/saiku-embed@3.20.0","gitHead":"e3b8b36ba3f2c3604d31d9c88f6ea9b301f179fd","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-8HaltlmYT8uwkH6bQJakHetuLGt7xtqM8tJ1n+quX/yEcNQeWs+D+gl17hy1re0sCIM/bwAkEsOZbrmLcqmDRA==","shasum":"416ea29031edcaecfb5f2df6d119617105a2cc3e","tarball":"https://registry.npmjs.org/@concepttocloud/saiku-embed/-/saiku-embed-3.20.0.tgz","fileCount":4,"unpackedSize":4358596,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICvaFXQxM+Jf6kfGk9hpjyAAaRSgAwUYrE/CLfjnVtPxAiAG2J+x6WImhYv0VMbaJOhyWRcXR7mMzoG+1iTInO18JA=="}]},"_npmUser":{"name":"magicaltroutconcepttocloud","email":"tom@concepttocloud.com"},"directories":{},"maintainers":[{"name":"ameliaux","email":"amelia@concepttocloud.com"},{"name":"magicaltroutconcepttocloud","email":"tom@concepttocloud.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/saiku-embed_3.20.0_1783910793034_0.8814976385633988"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-10T19:48:10.121Z","modified":"2026-07-13T02:46:33.316Z","3.18.2":"2026-06-10T19:48:10.570Z","3.19.0":"2026-07-11T05:19:44.253Z","3.20.0":"2026-07-13T02:46:33.201Z"},"bugs":{"url":"https://github.com/spiculedata/saiku/issues"},"license":"Apache-2.0","homepage":"https://github.com/spiculedata/saiku","keywords":["saiku","olap","embed","web-component","custom-element","dashboard","chart"],"repository":{"type":"git","url":"git+https://github.com/spiculedata/saiku.git","directory":"saiku-ui/src/embed"},"description":"<saiku-embed> Web Component — drop a saved Saiku query or dashboard into any page.","maintainers":[{"name":"ameliaux","email":"amelia@concepttocloud.com"},{"name":"magicaltroutconcepttocloud","email":"tom@concepttocloud.com"}],"readme":"# `<saiku-embed>` — Saiku Web Component\n\nDrop a saved Saiku query or dashboard into any page on the open web. Same\ntag works in React, Vue, Svelte, and vanilla HTML — it's a real\n[Custom Element](https://developer.mozilla.org/docs/Web/API/Web_components),\nso the host page doesn't have to know anything about Saiku internals.\n\n## Install\n\n```html\n<script src=\"https://YOUR-SAIKU.example.com/ui/saiku-embed.js\"></script>\n```\n\nOr via npm (for React / Vue / SPA projects that bundle their own JS):\n\n```bash\nnpm install @concepttocloud/saiku-embed\n```\n\n```ts\nimport \"@concepttocloud/saiku-embed\";\n```\n\nThe import has the side effect of registering the `saiku-embed` tag\nglobally — no further setup.\n\n## Use\n\n### A saved query, rendered as a table\n\n```html\n<saiku-embed\n  server=\"https://YOUR-SAIKU.example.com\"\n  token=\"...\"\n  path=\"homes/admin/Examples/Trend.saiku\"\n  height=\"400px\"\n></saiku-embed>\n```\n\n### A saved query, rendered as a bar / line / pie chart\n\n```html\n<saiku-embed\n  server=\"...\"\n  token=\"...\"\n  path=\"homes/admin/Examples/Sales.saiku\"\n  render=\"chart\"\n  mode=\"bar\"\n  height=\"500px\"\n></saiku-embed>\n```\n\n### A saved query, rendered as a hierarchical matrix (v3.19)\n\nMatrix mode preserves the row / column axis structure — measures on\ncolumns, dimension members on rows — instead of flattening to a single\nrow-key map like `render=\"table\"` does. Useful for pivot-style reports.\n\n```html\n<saiku-embed\n  server=\"...\"\n  token=\"...\"\n  path=\"homes/admin/Examples/Sales.saiku\"\n  render=\"matrix\"\n  height=\"500px\"\n></saiku-embed>\n```\n\n### An AI ask widget over a cube (v3.19, `kind=\"ai\"`)\n\nPoint the token at a cube (rather than a saved query) and drop in a\nplain-English ask box. Behind the scenes it POSTs to\n`/rest/saiku/api/embed/ai/{cubeId}/ask`, which runs the question through\nthe server's configured LLM provider under the pinned owner's data\nscope. Requires an AI-kind token (see \"Minting an AI token\" below).\n\n```html\n<saiku-embed\n  server=\"...\"\n  token=\"...\"\n  kind=\"ai\"\n  path=\"foodmart/FoodMart/FoodMart/Sales\"\n  height=\"200px\"\n></saiku-embed>\n```\n\n### A single KPI tile (v3.20, `render=\"kpi\"`)\n\nThe most common embed shape — one governed figure rendered large. Derives its\nvalue from the same records response as `render=\"table\"`, so it needs no server\nchange. Point it at a saved query whose last measure is the headline number;\nwhen the query carries a prior measure column, a delta chip is shown.\n\n```html\n<saiku-embed\n  server=\"...\"\n  token=\"...\"\n  path=\"homes/admin/Examples/NetRevenue.saiku\"\n  render=\"kpi\"\n  height=\"160px\"\n></saiku-embed>\n```\n\n### A saved query, sliced at embed time (v3.20, `filter`)\n\nPass slicer overrides as a JSON array. They ride the same validated slicer path\nthe dashboard filter tiles use — the saved query's cube binding and axes are\nuntouched, so a host can parameterise an embed without re-authoring the query.\n\n```html\n<saiku-embed\n  server=\"...\"\n  token=\"...\"\n  path=\"homes/admin/Examples/Sales.saiku\"\n  filter='[{\"dimension\":\"Time\",\"level\":\"Year\",\"members\":[\"[Time].[2024]\"]}]'\n></saiku-embed>\n```\n\n### A persona-scoped AI ask (v3.20, `space`)\n\nAdd a `space` to a `kind=\"ai\"` embed to scope the assistant to an admin-authored\n[Agent Space](../../../docs/AGENT-SPACES-SPEC.md) persona. The persona's system\nprompt, skill filter, and cube allowlist apply server-side. The cube stays\npinned by the token, so a space can only **narrow** what the guest reaches — if\nthe space's allowlist excludes the pinned cube, the ask fails closed.\n\n```html\n<saiku-embed\n  server=\"...\"\n  token=\"...\"\n  kind=\"ai\"\n  path=\"foodmart/FoodMart/FoodMart/Sales\"\n  space=\"foodmart-sales-analyst\"\n  height=\"240px\"\n></saiku-embed>\n```\n\n### A saved dashboard\n\n```html\n<saiku-embed\n  server=\"...\"\n  token=\"...\"\n  kind=\"dashboard\"\n  path=\"homes/admin/exec.saikudash\"\n  height=\"700px\"\n></saiku-embed>\n```\n\n### Anonymous public embed\n\nIf the resource is marked publicly embeddable on the server\n(see \"Public grants\" below), omit the token entirely:\n\n```html\n<saiku-embed\n  server=\"...\"\n  path=\"shared/public-chart.saiku\"\n  render=\"chart\"\n></saiku-embed>\n```\n\n## Attributes\n\n| Attribute | Default     | Notes                                                                  |\n|-----------|-------------|------------------------------------------------------------------------|\n| `server`  | _(optional)_| Origin of the Saiku launcher, e.g. `https://demo.saiku.bi`. Leave empty for same-origin (v3.19+) |\n| `path`    | _(required)_| `kind=query`: saved query path (`.saiku`) — `kind=dashboard`: dashboard path (`.saikudash`) — `kind=ai`: cube ref `connection/catalog/schema/cubeName` |\n| `kind`    | `query`     | `query`, `dashboard`, or `ai`                                          |\n| `token`   | _(none)_    | Embed token from `POST /saiku/api/embed/tokens`. Omit for public reads |\n| `render`  | `table`     | For `kind=query`: `table`, `matrix`, `chart`, or `kpi` (v3.20)         |\n| `mode`    | `bar`       | For `render=chart`: `bar`, `line`, or `pie`                            |\n| `height`  | `400px`     | CSS height of the rendered surface                                     |\n| `space`   | _(none)_    | For `kind=ai`: Agent Space persona id — scopes the ask server-side (v3.20) |\n| `filter`  | _(none)_    | For `kind=query`: JSON array of slicer overrides applied at embed time (v3.20) |\n| `theme`   | _(light)_   | `light`, `dark`, or `auto` (follow `prefers-color-scheme`) (v3.20)     |\n\nThe component re-renders whenever an attribute changes, so frameworks\nbinding state to attrs (React's JSX, Vue's `:server=\"...\"`, etc.) just\nwork.\n\n## Events (v3.20)\n\nThe element emits namespaced `CustomEvent`s so the host page can react to what\nhappens inside the embed. All bubble and are `composed`, so a listener on the\n`<saiku-embed>` element receives them:\n\n| Event             | `detail`                          | Fires when                              |\n|-------------------|-----------------------------------|-----------------------------------------|\n| `saiku:load`      | `{ kind, rows }`                  | a query / matrix / kpi surface loads    |\n| `saiku:error`     | `{ message }`                     | a query load fails (friendly message)   |\n| `saiku:select`    | `{ row }`                         | a table row is clicked (`render=table`) |\n| `saiku:ai-query`  | `{ question, degraded }`          | an AI ask resolves (`kind=ai`)          |\n\n```js\nconst el = document.querySelector(\"saiku-embed\");\nel.addEventListener(\"saiku:load\", (e) => console.log(\"loaded\", e.detail.rows, \"rows\"));\nel.addEventListener(\"saiku:select\", (e) => showDetail(e.detail.row));\n```\n\nIn React (via `@concepttocloud/saiku-embed-react`) the same events are exposed\nas `onLoad` / `onError` / `onSelect` / `onAiQuery` callback props.\n\n## Server-side: minting a token\n\nAuthenticated as a user who has GRANT on the saved query / dashboard:\n\n```bash\ncurl -X POST 'https://YOUR-SAIKU/rest/saiku/api/embed/tokens' \\\n  -u admin:admin \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"resourceKind\": \"query\",\n    \"resourcePath\": \"homes/admin/Examples/Trend.saiku\",\n    \"ttlHours\": 72,\n    \"label\": \"Marketing landing page\"\n  }'\n# → { \"status\": \"OK\", \"token\": \"tx-...\", \"expiresAt\": ... }\n```\n\n### Minting an AI token (v3.19)\n\nFor `kind=\"ai\"` embeds. `resourcePath` is a cube ref rather than a\nfile path. Mint is admin-only for v1 (cube-level ACLs are a follow-up).\n\n```bash\ncurl -X POST 'https://YOUR-SAIKU/rest/saiku/api/embed/tokens' \\\n  -u admin:admin \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"resourceKind\": \"ai\",\n    \"resourcePath\": \"foodmart/FoodMart/FoodMart/Sales\",\n    \"ttlHours\": 72,\n    \"label\": \"DimSum widget on marketing site\"\n  }'\n```\n\nServer-side, an AI ask requires the launcher to have an LLM provider\nconfigured (`saiku.ai.ask.provider = anthropic | openai` plus the\nmatching API key). Without it, the widget renders a degraded message\nrather than an error.\n\nPaste the `token` into the host page's `<saiku-embed token=\"...\">`.\nTokens are server-authoritative — revoke any time via:\n\n```bash\ncurl -X DELETE 'https://YOUR-SAIKU/rest/saiku/api/embed/tokens/<token>' \\\n  -u admin:admin\n```\n\n## Server-side: public grants\n\nTo make a resource readable WITHOUT a token (e.g. for a public blog post):\n\n```bash\ncurl -X POST 'https://YOUR-SAIKU/rest/saiku/api/embed/public' \\\n  -u admin:admin \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"resourceKind\": \"query\",\n    \"resourcePath\": \"shared/public-chart.saiku\",\n    \"label\": \"Homepage chart\"\n  }'\n```\n\nPublic reads still run under the grantor's data scope, so any\nsession-injected filters render from the grantor's perspective.\nRevoke:\n\n```bash\ncurl -X DELETE 'https://YOUR-SAIKU/rest/saiku/api/embed/public?kind=query&path=shared/public-chart.saiku' \\\n  -u admin:admin\n```\n\n## Styling\n\nThe embed lives inside an\n[open shadow root](https://developer.mozilla.org/docs/Web/API/ShadowRoot),\nso host page CSS can't leak in and vice versa.\n\nFor a quick dark surface, set `theme=\"dark\"` (or `theme=\"auto\"` to follow the\nviewer's `prefers-color-scheme`) — it swaps the whole palette without the host\nhaving to set each variable (v3.20). Leaving `theme` unset keeps the original\nlight palette, so existing embeds are unchanged.\n\nTo fine-tune individual colours, set CSS variables on the host page:\n\n```css\nsaiku-embed {\n  --saiku-embed-fg: #0f172a;\n  --saiku-embed-bg: transparent;\n  --saiku-embed-border: #cbd5e1;\n  --saiku-embed-header-bg: #f1f5f9;\n  --saiku-embed-tile-bg: #ffffff;\n  --saiku-embed-row-hover: #e2e8f0;\n  --saiku-embed-negative: #b91c1c;\n  --saiku-embed-error: #b91c1c;\n  --saiku-embed-muted: #64748b;\n}\n```\n\n### Theming the chart itself\n\nThe variables above style the embed **chrome** (frame, header, table). To brand\nthe **chart series + axes**, set these (custom properties inherit through the\nshadow boundary, so the canvas chart picks them up):\n\n```css\nsaiku-embed {\n  /* Series colour cycle — set as many as you need, 1..8, contiguously.\n     Any unset → the chart falls back to the built-in palette. */\n  --saiku-embed-chart-1: #2563eb;\n  --saiku-embed-chart-2: #16a34a;\n  --saiku-embed-chart-3: #dc2626;\n  /* …up to --saiku-embed-chart-8 */\n\n  /* Axis labels / legend / titles use --saiku-embed-fg;\n     axis + split lines use --saiku-embed-muted (both shared with the chrome). */\n}\n```\n\nAn embed with none of these set renders exactly as before (ECharts defaults).\n\n## Security model\n\n- **Header-only token transport.** The token travels as\n  `X-Saiku-Embed-Token`. Never a `?token=` query parameter — those leak\n  into access logs, proxy logs, browser history, and outbound\n  `Referer`.\n- **Server-side authoritative.** Tokens are opaque random 256-bit ids\n  with no embedded claims; the server looks them up on every request.\n  Revocation takes effect on the very next request.\n- **Per-resource scope.** A token pins exactly one query or dashboard.\n  Replaying it against any other resource (or any other endpoint)\n  returns the same opaque `EMBED_INVALID` 401, regardless of whether\n  the request used the wrong kind, the wrong path, an expired token,\n  or a revoked one. Probes can't enumerate.\n- **Cross-origin cookie isolation.** The embed sends\n  `credentials: \"omit\"`, so the host page's Saiku session cookie (if\n  the user happens to be logged in) doesn't flow with embed reads.\n  The token IS the only auth carrier on this surface.\n\n## Bundle size\n\nAround **213 KB gzipped** at the time of writing — Svelte 5 custom\nelement runtime + ECharts (core + bar / line / pie + four common\ncomponents, modular tree-shaken) + the embed renderers.\n\n## Limitations\n\n- Records-format only. The matrix format isn't rendered in v1.\n- Dashboard `filter` tiles are skipped — the embed renders the\n  authored data as-is without an interactive filter bar.\n- Markdown in `text` tiles renders as plain text (no `marked`\n  dependency to keep the bundle tight).\n- AI Query results (`/ai/query`) aren't wired as an `<saiku-embed>`\n  source yet — coming in a follow-up.\n","readmeFilename":"README.md"}