@jitto/analyze-embed 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +72 -0
- package/README.md +324 -2
- package/dist/core/embed-target.d.ts +13 -0
- package/dist/core/embed-target.js +35 -0
- package/dist/core/frame.d.ts +28 -0
- package/dist/core/frame.js +93 -0
- package/dist/core/mount.d.ts +10 -0
- package/dist/core/mount.js +226 -0
- package/dist/core/timer.d.ts +9 -0
- package/dist/core/timer.js +13 -0
- package/dist/core/token.d.ts +39 -0
- package/dist/core/token.js +142 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/react.d.ts +8 -0
- package/dist/react.js +69 -0
- package/dist/types.d.ts +324 -0
- package/dist/types.js +61 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
JamLabs Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JamLabs. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software, including its source code, compiled code, documentation, API
|
|
6
|
+
definitions and examples (the "Software"), is the proprietary property of
|
|
7
|
+
JamLabs. It is not open source.
|
|
8
|
+
|
|
9
|
+
"Redistributable Code" means only the runtime code of the @jitto/analyze-embed package
|
|
10
|
+
as JamLabs publishes it on npm, including copies of it that you have bundled,
|
|
11
|
+
minified, transpiled or tree-shaken while building your application, and the
|
|
12
|
+
changes Section 1 allows. It does not include this repository's other files,
|
|
13
|
+
such as its specifications, API definitions, documentation, examples and
|
|
14
|
+
checks.
|
|
15
|
+
|
|
16
|
+
1. Permitted use. You may use the Software only if you, or the organisation you
|
|
17
|
+
work for, have a current written agreement with JamLabs for Jitto (the
|
|
18
|
+
"Agreement"), and only to embed Jitto in your own applications for the
|
|
19
|
+
purposes and term that the Agreement allows. You may copy and modify the
|
|
20
|
+
Software only as far as needed for that integration, and you may build the
|
|
21
|
+
Redistributable Code into your application with your usual build tools.
|
|
22
|
+
|
|
23
|
+
2. Restrictions. Unless the Agreement or JamLabs in writing expressly allows it,
|
|
24
|
+
you may not:
|
|
25
|
+
a. distribute, publish, sublicense, sell, rent or lend the Software, or make
|
|
26
|
+
it available to any third party, except that you may deliver the
|
|
27
|
+
Redistributable Code to your own end users as part of your own
|
|
28
|
+
application (for example in the JavaScript a browser loads), but not on
|
|
29
|
+
its own, and not as a library or kit for others to build with;
|
|
30
|
+
b. use the Software to embed or access any service other than Jitto;
|
|
31
|
+
c. reverse engineer, decompile or disassemble the Software or the Jitto
|
|
32
|
+
service, except to the extent applicable law expressly permits it despite
|
|
33
|
+
this limitation;
|
|
34
|
+
d. use the Software to build, train or benchmark a product or service that
|
|
35
|
+
competes with Jitto;
|
|
36
|
+
e. remove or alter any copyright, licence or proprietary notice.
|
|
37
|
+
|
|
38
|
+
3. Ownership. JamLabs and its licensors keep all rights, title and interest in
|
|
39
|
+
the Software. No rights are granted except those stated here.
|
|
40
|
+
|
|
41
|
+
4. Confidentiality. Parts of the Software that JamLabs has not made public are
|
|
42
|
+
JamLabs confidential information, and the confidentiality terms of the
|
|
43
|
+
Agreement apply to them. Parts that JamLabs has made public (the package as
|
|
44
|
+
published on npm and documentation JamLabs publishes) are not confidential,
|
|
45
|
+
but this licence still governs their use. Code you deliver to end users
|
|
46
|
+
under Section 2a is not a breach of confidentiality.
|
|
47
|
+
|
|
48
|
+
4a. Third-party components. Any third-party component included in the Software,
|
|
49
|
+
if any, is licensed under its own terms, which are shipped with that
|
|
50
|
+
component. This licence does not limit the rights those terms grant you in
|
|
51
|
+
that component.
|
|
52
|
+
|
|
53
|
+
5. Termination. Your rights under this licence end automatically when the
|
|
54
|
+
Agreement ends or if you breach this licence. You must then stop using the
|
|
55
|
+
Software and delete all copies, except copies you must keep by law.
|
|
56
|
+
|
|
57
|
+
6. No warranty. Except as the Agreement states otherwise, the Software is
|
|
58
|
+
provided "as is", without warranty of any kind, express or implied, including
|
|
59
|
+
merchantability, fitness for a particular purpose and non-infringement.
|
|
60
|
+
|
|
61
|
+
7. Limitation of liability. Except as the Agreement states otherwise, JamLabs is
|
|
62
|
+
not liable for any indirect, incidental, special or consequential damages, or
|
|
63
|
+
for loss of profits, revenue or data, arising from the Software. JamLabs'
|
|
64
|
+
total liability arising from the Software is limited as the Agreement states;
|
|
65
|
+
if the Agreement sets no limit, it will not exceed the fees you paid JamLabs
|
|
66
|
+
under the Agreement in the 12 months before the event that gave rise to the
|
|
67
|
+
claim.
|
|
68
|
+
|
|
69
|
+
8. Precedence. If this licence and the Agreement conflict, the Agreement
|
|
70
|
+
prevails.
|
|
71
|
+
|
|
72
|
+
For licensing questions, contact your JamLabs account contact.
|
package/README.md
CHANGED
|
@@ -1,3 +1,325 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @jitto/analyze-embed
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Show a Jitto dashboard inside your own web app, for your own signed-in users, in React or plain JavaScript.
|
|
4
|
+
Each user sees only the rows Jitto's row rules allow them; your page never holds a Jitto password or secret.
|
|
5
|
+
|
|
6
|
+
> **Status: 0.2, first public release.** Expect small changes before 1.0.
|
|
7
|
+
>
|
|
8
|
+
> **Not available yet:** `filters`, `setFilters` and the `filterChanged` and `filterRejected` events are specified but not built in
|
|
9
|
+
> this version. The `filters` option is ignored with a console warning, and `setFilters` rejects. Everything else in this
|
|
10
|
+
> README works as described.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @jitto/analyze-embed
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
One package, two entry points: `@jitto/analyze-embed` (plain JavaScript) and `@jitto/analyze-embed/react` (React).
|
|
19
|
+
Both run on the same core, so the same options give the same events. TypeScript declarations are included.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
1. **Get three things from your Jitto tenant administrator:** your tenant's Jitto host (for example
|
|
24
|
+
`https://example-tenant.jittocloud.com`; `-uat` and `-dev` hosts for the other environments), the id of the dashboard, and confirmation that your website's origin (for example
|
|
25
|
+
`https://portal.client.example`) is on the tenant's allowed origins. Origins are exact: `https://`, host, optional
|
|
26
|
+
port.
|
|
27
|
+
2. **Install:** `npm install @jitto/analyze-embed`.
|
|
28
|
+
3. **Write `fetchToken`** (see [Tokens](#tokens)): it asks Jitto for a dashboard token with the signed-in user's own
|
|
29
|
+
access token from your sign-in library.
|
|
30
|
+
4. **Mount the dashboard:**
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { JittoDashboard } from '@jitto/analyze-embed/react';
|
|
34
|
+
|
|
35
|
+
export function DeskOverview() {
|
|
36
|
+
return (
|
|
37
|
+
<JittoDashboard
|
|
38
|
+
dashboardId="3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0"
|
|
39
|
+
embedOrigin="https://example-tenant.jittocloud.com"
|
|
40
|
+
fetchToken={fetchToken} // see Tokens: it sends the user's own access token
|
|
41
|
+
style={{ height: 600 }}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Open the page as a test user. You see that user's rows.
|
|
48
|
+
|
|
49
|
+
## React: `<JittoDashboard>`
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { useRef } from 'react';
|
|
53
|
+
import { JittoDashboard } from '@jitto/analyze-embed/react';
|
|
54
|
+
import type { JittoDashboardHandle } from '@jitto/analyze-embed';
|
|
55
|
+
|
|
56
|
+
export function Report() {
|
|
57
|
+
const dashboard = useRef<JittoDashboardHandle>(null);
|
|
58
|
+
return (
|
|
59
|
+
<JittoDashboard
|
|
60
|
+
ref={dashboard}
|
|
61
|
+
dashboardId="3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0"
|
|
62
|
+
embedOrigin="https://example-tenant.jittocloud.com"
|
|
63
|
+
fetchToken={fetchToken}
|
|
64
|
+
theme={{ mode: 'light', colorPrimary: '#0b7a75' }}
|
|
65
|
+
onLoaded={({ durationMs }) => console.log(`loaded in ${durationMs} ms`)}
|
|
66
|
+
onFailed={({ reason, message }) => console.warn(reason, message)}
|
|
67
|
+
onSizeChanged={({ height }) => console.log('content height', height)}
|
|
68
|
+
className="report"
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Required | What it does |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| `dashboardId` | `string` | yes | The Jitto dashboard id your tenant administrator gave you. |
|
|
77
|
+
| `embedOrigin` | `string` | yes | Your tenant's Jitto host, as an origin (`https://example-tenant.jittocloud.com`). The SDK frames only this origin followed by `/embed` (the API's `embed_url`) and talks only to this origin. It derives no host from anything else. |
|
|
78
|
+
| `fetchToken` | `(dashboardId, signal) => Promise<EmbedTokenResponse>` | yes | Gets a dashboard token for the signed-in user with their own access token. See [Tokens](#tokens). |
|
|
79
|
+
| `filters` | `Filters` | no | Filters applied before the first render, by filter name. See [Filters](#filters). **Not available yet in this version.** |
|
|
80
|
+
| `theme` | `ThemeTokens` | no | Colours, font, light or dark. See [Theme](#theme). |
|
|
81
|
+
| `tokenDeadlineMs` | `number` | no | Deadline for one token request. Default `20000`. |
|
|
82
|
+
| `loadDeadlineMs` | `number` | no | How long the frame may stay silent before `failed` fires with reason `network`. Default `30000`. |
|
|
83
|
+
| `retryDelaysMs` | `readonly number[]` | no | Waits before each retry of a retryable token failure (429, 500, 502, deadline, network). Default `[2000, 5000, 15000]`. |
|
|
84
|
+
| `onLoaded` | `(e) => void` | no | The dashboard rendered. See [Events](#events). |
|
|
85
|
+
| `onFailed` | `(e) => void` | no | The dashboard could not load, or stopped working. |
|
|
86
|
+
| `onFilterChanged` | `(e) => void` | no | The filters in effect changed. **Not available yet in this version.** |
|
|
87
|
+
| `onFilterRejected` | `(e) => void` | no | The dashboard refused filters from `setFilters`. Nothing was applied. **Not available yet in this version.** |
|
|
88
|
+
| `onSizeChanged` | `(e) => void` | no | The dashboard's content size changed. |
|
|
89
|
+
| `className` | `string` | no | Class of the container element. |
|
|
90
|
+
| `style` | `Record<string, string \| number>` | no | Style of the container element. Give it a height. |
|
|
91
|
+
| `ref` | `Ref<JittoDashboardHandle>` | no | The same handle `mountJittoDashboard` returns. |
|
|
92
|
+
|
|
93
|
+
How prop changes apply:
|
|
94
|
+
|
|
95
|
+
- A change to `dashboardId` or `embedOrigin` remounts the dashboard.
|
|
96
|
+
- `theme` is compared by value. An equal value does nothing, so an inline object is safe. A different value is
|
|
97
|
+
applied in place, as `setTheme` would. `filters` is not available yet in this version: it is ignored, also when it
|
|
98
|
+
changes after the mount.
|
|
99
|
+
- Callbacks, the token function and the deadlines are read when used. A new function takes effect from its next use,
|
|
100
|
+
with no remount.
|
|
101
|
+
|
|
102
|
+
The component mounts in the browser only. In a server-rendered app, render it on the client.
|
|
103
|
+
|
|
104
|
+
## Plain JavaScript: `mountJittoDashboard`
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import { mountJittoDashboard } from '@jitto/analyze-embed';
|
|
108
|
+
|
|
109
|
+
const element = document.getElementById('dashboard');
|
|
110
|
+
if (!element) throw new Error('no #dashboard element on the page');
|
|
111
|
+
|
|
112
|
+
const dashboard = mountJittoDashboard(element, {
|
|
113
|
+
dashboardId: '3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0',
|
|
114
|
+
embedOrigin: 'https://example-tenant.jittocloud.com',
|
|
115
|
+
fetchToken,
|
|
116
|
+
onFailed: ({ reason, message }) => console.warn(reason, message),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const stop = dashboard.on('loaded', ({ durationMs }) => console.log(`loaded in ${durationMs} ms`));
|
|
120
|
+
|
|
121
|
+
// Later, when your app's own controls change:
|
|
122
|
+
export function useDarkTheme() {
|
|
123
|
+
dashboard.setTheme({ mode: 'dark' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// When your page or view goes away:
|
|
127
|
+
export function teardown() {
|
|
128
|
+
stop();
|
|
129
|
+
dashboard.unmount();
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`mountJittoDashboard(element, options)` takes the same options and callbacks as the React props (except `className`,
|
|
134
|
+
`style` and `ref`) and returns a handle:
|
|
135
|
+
|
|
136
|
+
| Member | What it does |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `dashboardId` | The dashboard id this mount shows. |
|
|
139
|
+
| `setFilters(filters)` | Replaces the filters your app set. Returns a promise that resolves once the dashboard applied them; then `filterChanged` fires with `source: 'host'`. See [Filters](#filters). **Not available yet in this version.** |
|
|
140
|
+
| `setTheme(theme)` | Replaces the theme tokens in place. The frame does not reload. |
|
|
141
|
+
| `on(event, handler)` | Subscribes to an event. Returns a function that unsubscribes. |
|
|
142
|
+
| `unmount()` | Removes the dashboard and cancels pending token requests and timers. Safe to call twice. |
|
|
143
|
+
|
|
144
|
+
> Not available yet: in this version `setFilters` always rejects with a plain `Error`, so the outcomes and `FilterError`
|
|
145
|
+
> described below do not occur yet.
|
|
146
|
+
|
|
147
|
+
The `setFilters` promise always settles. It rejects with a `SetFiltersError` whose `outcome` is:
|
|
148
|
+
|
|
149
|
+
- `deadline`: the filters were not applied within `loadDeadlineMs`. `failed` fires with reason `network` and the
|
|
150
|
+
mount ends.
|
|
151
|
+
- `ended`: the mount ended first, or had already ended.
|
|
152
|
+
- `replaced`: a later `setFilters` call came before this one was applied.
|
|
153
|
+
|
|
154
|
+
If the dashboard refuses a name or a value, `setFilters` rejects with a `FilterError` instead and applies nothing.
|
|
155
|
+
Its `code` is `unknown_filter` or `invalid_filter_value`, `filterNames` lists the refused names and
|
|
156
|
+
`knownFilterNames` lists every filter the dashboard has. `filterRejected` fires with the same error.
|
|
157
|
+
|
|
158
|
+
## Tokens
|
|
159
|
+
|
|
160
|
+
Your page asks Jitto for a short-lived dashboard token with the signed-in user's own access token, the one your
|
|
161
|
+
sign-in library already holds. That token is the user's identity: there is no token endpoint on your backend, no
|
|
162
|
+
shared account and no secret, and your code never states who the user is or what they may see.
|
|
163
|
+
|
|
164
|
+
1. The SDK calls your `fetchToken(dashboardId, signal)` on mount and again at 80 % of the token's lifetime (at most
|
|
165
|
+
about 24.8 days later, the longest delay a browser timer holds).
|
|
166
|
+
2. Your `fetchToken` sends the user's own access token to Jitto, from the browser:
|
|
167
|
+
|
|
168
|
+
```http
|
|
169
|
+
POST /api/v1/embed-tokens HTTP/1.1
|
|
170
|
+
Host: example-tenant.jittocloud.com
|
|
171
|
+
Origin: https://portal.client.example
|
|
172
|
+
Authorization: Bearer <the user's own access token>
|
|
173
|
+
Content-Type: application/json
|
|
174
|
+
|
|
175
|
+
{"dashboard_id": "3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0"}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
3. Jitto checks the token against your tenant's identity provider (issuer, signature, audience, expiry), reads the
|
|
179
|
+
user's groups from it, and applies the row rules your tenant administrator stored in Jitto. It checks the `Origin`
|
|
180
|
+
against your tenant's allowed origins, and answers the browser's CORS preflight only for those origins.
|
|
181
|
+
4. Jitto answers with a dashboard token that lives 5 minutes. The answer has `Cache-Control: no-store`:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"token": "<opaque>",
|
|
186
|
+
"dashboard_id": "3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0",
|
|
187
|
+
"embed_url": "https://example-tenant.jittocloud.com/embed",
|
|
188
|
+
"expires_in": 300
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
5. Your `fetchToken` hands it to the SDK with the SDK's field names (`dashboardId`, `embedUrl`, `expiresIn`), values
|
|
193
|
+
unchanged. The SDK checks the answer itself and refuses a wrong one with reason `token`.
|
|
194
|
+
|
|
195
|
+
On an error Jitto answers with an HTTP status and a problem body (`code`, `correlation_id`), and with `Retry-After`
|
|
196
|
+
on a 429. A token that is expired, for another audience or from another issuer is refused with 401
|
|
197
|
+
`unauthenticated` (reason `token`); a user in no group the row rules read gets 403 `no_group` (reason `permission`).
|
|
198
|
+
Your tenant administrator registers your identity provider with Jitto once.
|
|
199
|
+
|
|
200
|
+
Your `fetchToken` rejects on an HTTP error with the status and the API's `code`. This exact code is tested against
|
|
201
|
+
401, 403, 422, 429, 502 and malformed answers. `getAccessToken()` is your sign-in library's:
|
|
202
|
+
|
|
203
|
+
<!-- example: fetchToken -->
|
|
204
|
+
```js
|
|
205
|
+
async function fetchToken(dashboardId, signal) {
|
|
206
|
+
const r = await fetch('https://example-tenant.jittocloud.com/api/v1/embed-tokens', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
signal,
|
|
209
|
+
cache: 'no-store',
|
|
210
|
+
headers: {
|
|
211
|
+
Authorization: `Bearer ${await getAccessToken()}`, // the signed-in user's own token, from your sign-in library
|
|
212
|
+
'Content-Type': 'application/json',
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify({ dashboard_id: dashboardId }), // nothing else: Jitto reads who the user is from the token
|
|
215
|
+
});
|
|
216
|
+
const body = await r.json().catch(() => null);
|
|
217
|
+
if (!r.ok) {
|
|
218
|
+
const retryAfter = Number(r.headers.get('Retry-After'));
|
|
219
|
+
throw Object.assign(new Error(`token request failed with HTTP ${r.status}`), {
|
|
220
|
+
status: r.status,
|
|
221
|
+
code: body?.code,
|
|
222
|
+
correlationId: body?.correlation_id,
|
|
223
|
+
retryAfterSeconds: retryAfter > 0 ? retryAfter : undefined,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (!body || typeof body !== 'object') return body; // the SDK refuses it with reason `token`
|
|
227
|
+
const answer = { token: body.token, dashboardId: body.dashboard_id, embedUrl: body.embed_url };
|
|
228
|
+
if ('expires_in' in body) answer.expiresIn = body.expires_in;
|
|
229
|
+
return answer; // the SDK checks it and refuses a wrong answer with reason `token`
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Events
|
|
234
|
+
|
|
235
|
+
Subscribe with the `on…` props or callbacks, or with `handle.on(name, handler)`.
|
|
236
|
+
|
|
237
|
+
| Event | Callback | Payload | When |
|
|
238
|
+
|---|---|---|---|
|
|
239
|
+
| `loaded` | `onLoaded` | `{ dashboardId, durationMs }` | The dashboard rendered for the first time after a mount. |
|
|
240
|
+
| `failed` | `onFailed` | `{ dashboardId, reason, message, status?, code?, correlationId? }` | The dashboard could not load, a token refresh failed for good, or a `setFilters` reload did not render in time. Fires at most once per mount; the mount is then finished and the SDK shows a short Jitto message in its place. Mount again to retry. |
|
|
241
|
+
| `filterChanged` | `onFilterChanged` | `{ dashboardId, filters, source }` | The filters in effect changed. `source` is `'user'` (the filter bar) or `'host'` (your `setFilters` or `filters`). **Not available yet in this version.** |
|
|
242
|
+
| `filterRejected` | `onFilterRejected` | `{ dashboardId, error }` | The dashboard refused filters from `setFilters`. `error` is the `FilterError` the call rejects with. Nothing was applied and the dashboard was not reloaded. **Not available yet in this version.** |
|
|
243
|
+
| `sizeChanged` | `onSizeChanged` | `{ dashboardId, width, height }` | The dashboard's content size changed. Resize your container to fit if you want no inner scroll bar. |
|
|
244
|
+
|
|
245
|
+
`reason` is one of `token`, `permission`, `network` and `not_found`.
|
|
246
|
+
|
|
247
|
+
## Filters
|
|
248
|
+
|
|
249
|
+
> Not available yet: this section describes the specified behaviour. See the note at the top of this README.
|
|
250
|
+
|
|
251
|
+
Filters set what the dashboard shows first, by the names its filter bar uses (for example `Currency`, `Report date`).
|
|
252
|
+
|
|
253
|
+
- Set them on mount with `filters`, and later with `setFilters` (or a new `filters` prop).
|
|
254
|
+
- A value is a string, number or boolean, a list (several values), a date range `{ from, to }` (ISO dates such as
|
|
255
|
+
`2026-06-30`, or relative forms such as `Last week`), or `null` to clear the filter.
|
|
256
|
+
- **Filters narrow the view. They are not security.** A user can change them in the filter bar. What a user may see
|
|
257
|
+
is decided only by the row rules in Jitto.
|
|
258
|
+
- Today `setFilters` reloads the dashboard inside the frame. Your page stays mounted and no new token is requested,
|
|
259
|
+
but the user's own filter-bar changes and scroll position are reset. Set filters on mount when you can.
|
|
260
|
+
|
|
261
|
+
## Theme
|
|
262
|
+
|
|
263
|
+
Theme tokens change colours, font and light or dark. Jitto keeps the layout; there is no CSS hook.
|
|
264
|
+
|
|
265
|
+
| Token | What it changes |
|
|
266
|
+
|---|---|
|
|
267
|
+
| `mode` | `'light'` or `'dark'`. |
|
|
268
|
+
| `colorPrimary` | Buttons, selected states, links. |
|
|
269
|
+
| `colorBackground` | The dashboard's page background. |
|
|
270
|
+
| `colorSurface` | Chart and filter card background. |
|
|
271
|
+
| `colorText` | Body text. |
|
|
272
|
+
| `colorBorder` | Borders and dividers. |
|
|
273
|
+
| `fontFamily` | A CSS `font-family` list. The font must already be loadable by the dashboard host. |
|
|
274
|
+
|
|
275
|
+
Colours are CSS colour strings (`#1a73e8`, `rgb(…)`). Anything else is ignored with a console warning. `setTheme`
|
|
276
|
+
never reloads the frame.
|
|
277
|
+
|
|
278
|
+
## Errors and troubleshooting
|
|
279
|
+
|
|
280
|
+
Every failure arrives as one `failed` event with one of four reasons. `status`, `code` and `correlationId` come from
|
|
281
|
+
the API when it answered; quote the `correlationId` when you ask Jitto for help.
|
|
282
|
+
|
|
283
|
+
| Reason | Common causes | What to do |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `token` | The user's token was refused (401 `unauthenticated`: expired, wrong audience, wrong issuer); too many token requests (429 `rate_limited`, retried first); no token within `tokenDeadlineMs`; an answer for another dashboard or with an `embedUrl` that is not your `embedOrigin` followed by `/embed`. | Check the user is signed in and your sign-in library returns a current token. Check `embedOrigin`. Request a token once per page view and let the SDK refresh. |
|
|
286
|
+
| `permission` | The dashboard is not on your tenant's embeddable list (403 `not_embeddable`); your origin is not allowed (403 `origin_not_allowed`); the user is in no group the row rules read (403 `no_group`); a dataset of the dashboard has no row rule (422 `row_rules_incomplete`); embedding is not switched on (503 `embedding_not_configured`). | Ask your tenant administrator to fix the setting the `code` names. |
|
|
287
|
+
| `not_found` | No dashboard with this id (404 `unknown_dashboard`). | Check `dashboardId`. |
|
|
288
|
+
| `network` | No answer, a server error (500, 502, retried first), a refused CORS preflight, or the frame stayed silent for `loadDeadlineMs`. A CORS error on the token request, or a `frame-ancestors` error, in the browser console means the page's origin is not allowed. | Check the network. For a CORS or `frame-ancestors` error, ask for your origin to be added exactly as the browser shows it (`localhost` and `127.0.0.1` differ, and so do ports). |
|
|
289
|
+
|
|
290
|
+
Other symptoms:
|
|
291
|
+
|
|
292
|
+
| You see | Cause | Fix |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| The dashboard loads with no rows | The user's groups match no row rule | Ask your tenant administrator which groups the row rules read. |
|
|
295
|
+
| A blank area with no error | The container has no height | Give the container (`style` or `className`) a height. |
|
|
296
|
+
| Your own CSP blocks the frame or the token request | Your `frame-src` or `connect-src` does not allow your tenant's Jitto host | Add your `embedOrigin` (for example `https://example-tenant.jittocloud.com`) to both `frame-src` and `connect-src`. |
|
|
297
|
+
|
|
298
|
+
## Supported browsers and frameworks
|
|
299
|
+
|
|
300
|
+
Proposed for 1.0.0; the end-to-end tests confirm the list before release.
|
|
301
|
+
|
|
302
|
+
- **Browsers:** the current and previous major versions of Chrome, Edge, Firefox and Safari.
|
|
303
|
+
Pages must be served over `https://`, including local development (for example `https://localhost:5173`).
|
|
304
|
+
- **React:** 18 and 19, through `@jitto/analyze-embed/react`.
|
|
305
|
+
- **Everything else** (Angular, Vue, server-rendered pages, no framework): use `mountJittoDashboard` from
|
|
306
|
+
`@jitto/analyze-embed`. Dedicated wrappers may follow on the same core.
|
|
307
|
+
- **TypeScript:** declarations are included. Every option, event and payload is typed.
|
|
308
|
+
|
|
309
|
+
## Versioning
|
|
310
|
+
|
|
311
|
+
- The package follows [semantic versioning](https://semver.org). A breaking change to an option, event, payload or
|
|
312
|
+
failure reason is a new major version.
|
|
313
|
+
- The package's major version matches the embed API's path version: `@jitto/analyze-embed` 1.x talks to
|
|
314
|
+
`/api/v1/` only. Additive API changes stay in v1.
|
|
315
|
+
- A deprecated API version answers with `Deprecation`, `Sunset` and `Link` headers for at least 90 days before it is
|
|
316
|
+
removed.
|
|
317
|
+
|
|
318
|
+
## Full guide and API reference
|
|
319
|
+
|
|
320
|
+
- [Embedding guide](../docs/embedding-guide.md): the whole flow, the tenant administrator's settings, and a security
|
|
321
|
+
checklist.
|
|
322
|
+
- [API reference](../docs/api/index.html), generated from the OpenAPI 3.1 contract
|
|
323
|
+
([`openapi/embed.yaml`](../openapi/embed.yaml)).
|
|
324
|
+
|
|
325
|
+
Both move to the Jitto docs site before 1.0.0, and these links then point there.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `embedUrl` the SDK may frame and send a token to: only the tenant's embed host. The answer comes from
|
|
3
|
+
* Jitto (`embed_url`); the only thing the SDK compares it with is the host app's own `embedOrigin`. No host pattern
|
|
4
|
+
* lives here: no tenant name is turned into a host, and no Jitto domain is named.
|
|
5
|
+
*/
|
|
6
|
+
/** True when `value` is a bare `https` origin in canonical form: `https://host[:port]`, nothing else. */
|
|
7
|
+
export declare function isCanonicalHttpsOrigin(value: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* True only when `embedUrl` is exactly `embedOrigin` followed by `/embed`: same scheme, host and port, no user or
|
|
10
|
+
* password, the path exactly `/embed`, no query, no fragment. False for every other value, and for an `embedOrigin`
|
|
11
|
+
* that is not a canonical `https` origin.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isExpectedEmbedTarget(embedUrl: string, embedOrigin: string): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `embedUrl` the SDK may frame and send a token to: only the tenant's embed host. The answer comes from
|
|
3
|
+
* Jitto (`embed_url`); the only thing the SDK compares it with is the host app's own `embedOrigin`. No host pattern
|
|
4
|
+
* lives here: no tenant name is turned into a host, and no Jitto domain is named.
|
|
5
|
+
*/
|
|
6
|
+
/** True when `value` is a bare `https` origin in canonical form: `https://host[:port]`, nothing else. */
|
|
7
|
+
export function isCanonicalHttpsOrigin(value) {
|
|
8
|
+
let u;
|
|
9
|
+
try {
|
|
10
|
+
u = new URL(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return u.protocol === 'https:' && u.origin === value && u.username === '' && u.password === '';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True only when `embedUrl` is exactly `embedOrigin` followed by `/embed`: same scheme, host and port, no user or
|
|
19
|
+
* password, the path exactly `/embed`, no query, no fragment. False for every other value, and for an `embedOrigin`
|
|
20
|
+
* that is not a canonical `https` origin.
|
|
21
|
+
*/
|
|
22
|
+
export function isExpectedEmbedTarget(embedUrl, embedOrigin) {
|
|
23
|
+
if (!isCanonicalHttpsOrigin(embedOrigin))
|
|
24
|
+
return false;
|
|
25
|
+
if (embedUrl !== `${embedOrigin}/embed`)
|
|
26
|
+
return false;
|
|
27
|
+
let u;
|
|
28
|
+
try {
|
|
29
|
+
u = new URL(embedUrl);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return u.origin === embedOrigin && u.pathname === '/embed' && u.search === '' && u.hash === '' && u.username === '' && u.password === '';
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard frame and its message channel. The core speaks the frame's protocol with Jitto's own code; no
|
|
3
|
+
* third-party embedding library is bundled. The frame is `embedUrl` + `/dashboard/<id>` on the tenant's
|
|
4
|
+
* own host. The SDK opens one `MessageChannel` to it, posted only to `embedOrigin`, and from then on talks over that
|
|
5
|
+
* private port: it hands the frame each token, and asks it for its size. Nothing else on the page can read or
|
|
6
|
+
* write that channel.
|
|
7
|
+
*/
|
|
8
|
+
export interface FrameSize {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class DashboardFrame {
|
|
13
|
+
readonly iframe: HTMLIFrameElement;
|
|
14
|
+
private port;
|
|
15
|
+
private pending;
|
|
16
|
+
private nextId;
|
|
17
|
+
private connected;
|
|
18
|
+
constructor(container: HTMLElement, embedUrl: string, dashboardId: string, embedOrigin: string);
|
|
19
|
+
private onMessage;
|
|
20
|
+
private send;
|
|
21
|
+
/** Hand the frame a token: the first, or a refreshed one. */
|
|
22
|
+
giveToken(token: string): Promise<void>;
|
|
23
|
+
/** The frame's content size, or `undefined` when it does not answer within `timeoutMs`. */
|
|
24
|
+
size(timeoutMs: number): Promise<FrameSize | undefined>;
|
|
25
|
+
/** Apply theme tokens in place. Best effort: a frame that cannot apply them keeps its look. */
|
|
26
|
+
applyTheme(theme: Record<string, string>, mode: 'light' | 'dark' | undefined): Promise<void>;
|
|
27
|
+
remove(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard frame and its message channel. The core speaks the frame's protocol with Jitto's own code; no
|
|
3
|
+
* third-party embedding library is bundled. The frame is `embedUrl` + `/dashboard/<id>` on the tenant's
|
|
4
|
+
* own host. The SDK opens one `MessageChannel` to it, posted only to `embedOrigin`, and from then on talks over that
|
|
5
|
+
* private port: it hands the frame each token, and asks it for its size. Nothing else on the page can read or
|
|
6
|
+
* write that channel.
|
|
7
|
+
*/
|
|
8
|
+
const HANDSHAKE = { type: '__embedded_comms__', handshake: 'port transfer' };
|
|
9
|
+
/** The frame's display options: Jitto's choice, not the host's (the host keeps its own title). */
|
|
10
|
+
const FRAME_QUERY = 'uiConfig=1&show_filters=1&expand_filters=0';
|
|
11
|
+
export class DashboardFrame {
|
|
12
|
+
iframe;
|
|
13
|
+
port = null;
|
|
14
|
+
pending = new Map();
|
|
15
|
+
nextId = 0;
|
|
16
|
+
connected;
|
|
17
|
+
constructor(container, embedUrl, dashboardId, embedOrigin) {
|
|
18
|
+
const iframe = document.createElement('iframe');
|
|
19
|
+
iframe.src = `${embedUrl}/dashboard/${encodeURIComponent(dashboardId)}?${FRAME_QUERY}`;
|
|
20
|
+
iframe.title = 'Dashboard';
|
|
21
|
+
iframe.style.width = '100%';
|
|
22
|
+
iframe.style.height = '100%';
|
|
23
|
+
iframe.style.border = '0';
|
|
24
|
+
iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
|
|
25
|
+
this.iframe = iframe;
|
|
26
|
+
this.connected = new Promise((resolve) => {
|
|
27
|
+
iframe.addEventListener('load', () => {
|
|
28
|
+
const channel = new MessageChannel();
|
|
29
|
+
this.port = channel.port1;
|
|
30
|
+
this.port.addEventListener('message', (event) => this.onMessage(event.data));
|
|
31
|
+
this.port.start();
|
|
32
|
+
// Posted to the tenant's origin only: a frame that ended up anywhere else never gets the port.
|
|
33
|
+
iframe.contentWindow?.postMessage(HANDSHAKE, embedOrigin, [channel.port2]);
|
|
34
|
+
resolve();
|
|
35
|
+
}, { once: true });
|
|
36
|
+
});
|
|
37
|
+
container.appendChild(iframe);
|
|
38
|
+
}
|
|
39
|
+
onMessage(data) {
|
|
40
|
+
if (!data || typeof data !== 'object' || typeof data.messageId !== 'string')
|
|
41
|
+
return;
|
|
42
|
+
const settle = this.pending.get(data.messageId);
|
|
43
|
+
if (!settle)
|
|
44
|
+
return;
|
|
45
|
+
this.pending.delete(data.messageId);
|
|
46
|
+
settle(data.switchboardAction === 'reply' ? data.result : undefined);
|
|
47
|
+
}
|
|
48
|
+
send(message) {
|
|
49
|
+
this.port?.postMessage(message);
|
|
50
|
+
}
|
|
51
|
+
/** Hand the frame a token: the first, or a refreshed one. */
|
|
52
|
+
async giveToken(token) {
|
|
53
|
+
await this.connected;
|
|
54
|
+
this.send({ switchboardAction: 'emit', method: 'guestToken', args: { guestToken: token } });
|
|
55
|
+
}
|
|
56
|
+
/** The frame's content size, or `undefined` when it does not answer within `timeoutMs`. */
|
|
57
|
+
async size(timeoutMs) {
|
|
58
|
+
await this.connected;
|
|
59
|
+
const messageId = `jitto-${(this.nextId += 1)}`;
|
|
60
|
+
const answer = new Promise((resolve) => {
|
|
61
|
+
this.pending.set(messageId, resolve);
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
if (this.pending.delete(messageId))
|
|
64
|
+
resolve(undefined);
|
|
65
|
+
}, timeoutMs);
|
|
66
|
+
});
|
|
67
|
+
this.send({ switchboardAction: 'get', method: 'getScrollSize', messageId, args: {} });
|
|
68
|
+
const result = (await answer);
|
|
69
|
+
const { width, height } = result ?? {};
|
|
70
|
+
const valid = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
71
|
+
if (!valid(width) || !valid(height))
|
|
72
|
+
return undefined;
|
|
73
|
+
return { width, height };
|
|
74
|
+
}
|
|
75
|
+
/** Apply theme tokens in place. Best effort: a frame that cannot apply them keeps its look. */
|
|
76
|
+
async applyTheme(theme, mode) {
|
|
77
|
+
await this.connected;
|
|
78
|
+
if (Object.keys(theme).length > 0) {
|
|
79
|
+
this.send({ switchboardAction: 'emit', method: 'setThemeConfig', args: { themeConfig: { token: theme } } });
|
|
80
|
+
}
|
|
81
|
+
if (mode) {
|
|
82
|
+
this.send({ switchboardAction: 'emit', method: 'setThemeMode', args: { mode: mode === 'dark' ? 'dark' : 'default' } });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
remove() {
|
|
86
|
+
for (const settle of this.pending.values())
|
|
87
|
+
settle(undefined);
|
|
88
|
+
this.pending.clear();
|
|
89
|
+
this.port?.close();
|
|
90
|
+
this.port = null;
|
|
91
|
+
this.iframe.remove();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mountJittoDashboard`: the plain JavaScript embed, on which the React wrapper sits. One package gives a React and a
|
|
3
|
+
* plain-JavaScript embed on one core.
|
|
4
|
+
*
|
|
5
|
+
* Not yet in this version: `setFilters` and the `filters` option. The dashboard's filters cannot yet be set by name
|
|
6
|
+
* from outside its frame; `setFilters` rejects and `filters` is ignored with a console warning. `setTheme` is
|
|
7
|
+
* applied in place, best effort.
|
|
8
|
+
*/
|
|
9
|
+
import type { MountJittoDashboard } from '../types.ts';
|
|
10
|
+
export declare const mountJittoDashboard: MountJittoDashboard;
|