Saltar al contenido principal

Incrustar un panel

Incrustar es el punto en el que un panel deja de ser algo que tu equipo abre y se convierte en parte de un producto que usan tus clientes. Vibedasher tiene dos formas de hacerlo, y la diferencia no es cosmética — decide quién puede leer los datos.

Carril públicoCarril de token firmado
view_mode del panelpublicprotected
Quién puede verlocualquiera con la URLsolo un visor para el que tu backend generó un token
Filtrado de fila por visornosí, aplicado en el servidor
Qué incrustasuna URLuna URL más un token de corta duración

Un panel private no es incrustable por ninguno de los dos carriles. Ambos carriles requieren que el panel esté publicado primero — mira Publicar y compartir.

Incrustar es una de las dos respuestas

Incrustar pone nuestro panel renderizado dentro de un rectángulo de tu página. La otra respuesta es expulsar — llevarte el código React y SQL y renderizarlo tú mismo, de forma nativa, con nosotros fuera de la ruta de renderizado y solo el motor de datos por debajo. Cuál de los dos quieres lo deciden sobre todo dos preguntas: ¿tienen que ver filas distintas visores distintos? (incrustar es el único carril que hace eso), y ¿tiene el panel que verse y comportarse como el resto de tu aplicación? (solo expulsar te da eso). Las contrapartidas están puestas una al lado de la otra en Incrustar o expulsar.

El carril público​

Si el panel está publicado con view_mode: public, un iframe es toda la integración:

<iframe
src="https://cloud.vibedasher.com/v/<slug>"
width="100%"
height="800"
style="border: 0;"
title="Vibedasher dashboard"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allowfullscreen
loading="lazy"
></iframe>

allow-same-origin parece alarmante junto a allow-scripts y es seguro aquí específicamente porque el frame es de origen cruzado respecto a tu página: le concede al renderizador su propio origen, no el tuyo. allow-top-navigation está deliberadamente ausente, así que el frame no puede navegar tu página fuera de ella. allow-downloads es lo que hace funcionar la exportación de gráficos a PNG.

El fragmento funciona desde cualquier página que controles — no hay nada que configurar de nuestro lado primero. La audiencia bajo la que publicas es el control que importa aquí, y El límite del marco es donde se expone esa elección.

El diálogo Compartir en la aplicación te da este fragmento ya rellenado. Cógelo de ahí en lugar de ensamblar la URL a mano.

El carril de token firmado​

Este es el que vale la pena entender, porque es el único sitio donde Vibedasher hace seguridad a nivel de fila.

Tu backend genera el token — tú no firmas nada​

Vibedasher firma el token él mismo, ES256, con su propia clave rotatoria. Tu backend pide uno con su clave de API:

from vibedasher.models import EmbedTokenRequest, EmbedTokenRequestFilters

resp = client.embed_tokens.create_embed(
EmbedTokenRequest(
viz_id=1234,
filters=EmbedTokenRequestFilters.from_dict({"region": "EMEA"}),
ttl_seconds=600,
)
)
print(resp.token, resp.expires_in)
const resp = await client.embedTokens.createEmbed({
vizId: 1234,
filters: { region: 'EMEA' },
ttlSeconds: 600,
});
No existe ningún "secreto de incrustación"

No tienes ningún material de firma ni lo necesitas — genera los tokens como se muestra aquí.

O con HTTP plano, desde cualquier lenguaje:

curl -X POST https://api.vibedasher.com/api/v1/embed/token \
-H "X-Api-Key: $VIBEDASHER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"vizId": 1234, "ttlSeconds": 600, "filters": {"region": "EMEA"}}'
# → {"token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600}

El cuerpo de la petición acepta:

CampoObligatorioNotas
vizIdsíExactamente un panel. Un token no puede cubrir una carpeta o una cuenta.
filtersnoLos filtros de fila. Mira más abajo.
viewerIdnoUn id de asiento opaco. Solo auditoría y medición — no es una entrada de control de acceso. No confíes en él para acotar nada.
ttlSecondsnoPor defecto 600. Suelo 60, techo máximo 86 400 (24 h). Los valores fuera de rango se acotan al generar el token y se vuelven a comprobar al verificarlo. La verificación admite un pequeño margen de desfase de reloj por encima de exp, limitado a 120 s y a la mitad de la vida útil del propio token: así la vida útil efectiva nunca supera 1,5× expires_in y, a partir de un TTL de 240 s, el margen es el fijo de 120 s.
apiKeyIdnoSolo necesario al generarlo desde una sesión de navegador, para vincular el token a la lista de dominios permitidos y la revocación de una clave.

El account_id se estampa desde tu credencial y nunca se lee del cuerpo.

Generar el token falla con un mensaje que nombra la comprobación que falló — estás generando un token, no presentándolo, así que el error te dice qué cambiar en el panel o en la clave:

EstadoDetalleSolución
403la visualización pertenece a otra cuentagenera con una clave de API de la cuenta propietaria
403la visualización no se puede incrustar con un token: su visibilidad debe ser protectedcambia la visibilidad del panel a Protegido en el diálogo Compartir
404la visualización no está publicadapublícala primero
403no hay ninguna clave de API utilizable a la que vincular el tokenla clave falta, está inactiva, caducada o pertenece a otra cuenta
404Viz no encontradacomprueba vizId

Un panel public es rechazado por este carril — ya tiene el carril anónimo, y mezclar los dos sería una forma de creer que tienes filtros de fila cuando no los tienes.

Filtros de fila​

filters es un mapa de nombre de columna a un escalar o una lista. Un escalar se convierte en =, una lista en IN, y varias claves se combinan con Y. Las claves son el sqlName de la columna tal como lo informa GET /v1/datasets/{id} — el nombre que expone la vista SQL, no el nombre visible.

La plataforma no añade una cláusula WHERE a tu consulta; reescribe cada referencia a un conjunto de datos como una relación filtrada, conservando el alias original:

FROM orders
-> FROM (SELECT * FROM "orders" WHERE "tenant" = 'acme') AS orders

Eso es lo que hace que el alcance sea no ampliable, y no solo que esté presente. Lo que la página pida se aplica fuera de la relación filtrada, así que solo puede estrecharla más — WHERE tenant = 'globex' OR 1=1 sigue viendo un solo inquilino, una subconsulta envolvente envuelve la relación filtrada, ambas ramas de un UNION se reescriben, y una CTE que oculta el alias, una referencia calificada por esquema y cualquier función que lea archivos se rechazan de plano.

Tres propiedades lo hacen auditable:

  • Los filtros provienen únicamente del token verificado. Nunca se leen del cuerpo de la petición, así que un visor hurgando en la pestaña de red no puede ampliar su propio alcance.
  • Una clave de filtro que no nombra ninguna columna de los conjuntos de datos del panel se rechaza al emitir — 403, nombrando la clave y listando las columnas que sí existen. No se firma ningún token. Lo mismo ocurre con un valor que no puede convertirse al tipo de su columna. Un filtro que coincidiera silenciosamente con todo sería una fuga de datos disfrazada de control de seguridad, así que se rechaza antes de poder convertirse en un enlace que entregas a un cliente.
  • La emisión te dice qué permite el token, en el campo scope de su respuesta. Léelo; no lo supongas.

Lee scope.fullyScoped — no es «los filtros fueron aceptados»​

Un panel puede leer más de un conjunto de datos, y un filtro solo puede aplicarse a un conjunto que tenga esa columna — no se puede filtrar por una columna que una tabla no tiene. Así, un panel sobre orders y price_list, con alcance {"tenant": "acme"}, filtra orders y lee price_list entero. La página carga, cada mosaico se dibuja, cada número es plausible, y nada en la petición parece incorrecto.

Eso es correcto y deseado cuando el segundo conjunto es una tabla de consulta compartida — una lista de precios, un catálogo de planes, un conjunto de etiquetas. No es lo que quieres cuando el segundo conjunto contiene filas por cliente bajo una columna con otro nombre (tenant_id en vez de tenant, customer, org). La respuesta de emisión es donde descubres en cuál de los dos casos estás:

{
"token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600,
"scope": {
"vizId": 1234,
"filtered": [{"datasetId": 7, "datasetName": "orders",
"predicate": "\"tenant\" = 'acme'", "columns": ["tenant"]}],
"unfiltered": [{"datasetId": 8, "datasetName": "price_list",
"predicate": null, "columns": []}],
"unresolvedDatasetIds": [],
"fullyScoped": false,
"meaning": "PARTIALLY SCOPED — read this before handing the link out. ..."
}
}
CampoQué hacer con él
fullyScopedEl que hay que afirmar. true solo cuando cada conjunto de datos que el panel lee está acotado por estos filtros. Cualquier otra cosa es false.
filtered[].predicateEl SQL exacto aplicado a ese conjunto de datos. Es la frase que se pone delante de un revisor de seguridad.
unfiltered[]Conjuntos de datos que este token lee enteros. Decide, para cada uno, si es una tabla compartida o un hueco.
unresolvedDatasetIdsConjuntos cuyo esquema no pudo leerse. No es lo mismo que «sin filtrar»: «lee todo» y «no sabemos qué lee esto» son respuestas distintas, y ninguna se funde en la otra.

Una comprobación de producción razonable en tu ruta de emisión es negarte a devolver el token cuando scope.fullyScoped sea false y esperabas true — tú sabes cuáles de tus paneles deben ser por cliente, y la plataforma no.

Presenta el token​

El frame no puede fijar una cabecera, así que el token viaja en la URL:

<iframe
src="https://cloud.vibedasher.com/v/<slug>?embed_token=<token>"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allowfullscreen
></iframe>

Las respuestas de incrustación se envían con Referrer-Policy: no-referrer para que el token no se filtre en una cabecera Referer en subpeticiones. Sigue siendo una credencial portadora en una URL, que es la razón real por la que el TTL por defecto es de diez minutos: genera uno por carga de página, mantenlo corto, y renuévalo en lugar de subir el TTL.

Configura tu lista de dominios permitidos​

Una clave de API puede llevar una lista de dominios desde los que se aceptarán sus tokens de incrustación. Una lista vacía significa permitir todo. Es una decisión deliberada para que la funcionalidad funcione en el momento en que la pruebas, y significa que una clave sin configurar no protege nada a nivel de origen. Configura allowed_domains en la clave antes de pasar a producción.

El ciclo de renovación del token​

Un token de diez minutos en un panel que alguien deja abierto toda la tarde necesita renovarse, y el renderizador te avisa cuándo:

  1. El frame envía EMBED_TOKEN_EXPIRED (o EMBED_TOKEN_INVALID) a tu página.
  2. Tu página le pide a tu backend un token nuevo.
  3. Tu página envía UPDATE_TOKEN al frame: { type: 'UPDATE_TOKEN', token: '<new>' }.
window.addEventListener('message', async (e) => {
if (e.origin !== 'https://cloud.vibedasher.com') return;
if (e.data?.type === 'EMBED_TOKEN_EXPIRED' || e.data?.type === 'EMBED_TOKEN_INVALID') {
const token = await fetch('/api/vibedasher-embed-token').then((r) => r.text());
frame.contentWindow.postMessage({ type: 'UPDATE_TOKEN', token }, 'https://cloud.vibedasher.com');
}
});

Comprueba siempre e.origin. UPDATE_TOKEN es el único mensaje que un host de terceros puede enviar hacia dentro — todo lo demás se ignora.

Lo que tu página realmente recibe​

Los mensajes que recibe un host de terceros son una lista fija y cerrada de trece. Esta es la lista completa:

MensajeCuándo
READYEl renderizador arrancó.
LOADEDEl panel terminó de cargar.
RESIZELa altura del contenido cambió — úsalo para dimensionar el iframe.
BUNDLE_STARTED / BUNDLE_COMPLETED / BUNDLE_FAILED / BUNDLE_RETRYCiclo de vida del build del código del panel.
RUNTIME_ERROREl código del panel lanzó un error.
EMPTY_DATAUna consulta no devolvió filas.
RENDER_HEALTHSalud de renderizado por mosaico.
EMBED_TOKEN_EXPIRED / EMBED_TOKEN_INVALID / EMBED_UNAVAILABLECiclo de vida del token — mira arriba.

Cualquier otra cosa que puedas ver referenciada es interna. En particular, un conjunto de mensajes se declara explícitamente como nunca reenviado a un host de terceros — VIZ_DATA, FILES, UPDATE_TOKEN saliente, SQL_ERROR, CONSOLE_ERROR, REQUEST_NEW_AUTH_TOKEN, ELEMENT_SELECTED, FILTER_STATE_CHANGED — porque llevan el código fuente de tu panel o el estado bruto de sus consultas.

El límite del marco​

Dos propiedades deciden sobre qué puede construirse una integración, y merece la pena leer las dos antes de diseñar alrededor del marco en lugar de después.

El marco es dueño de su propio estado de filtrado. Los mensajes que recibe un anfitrión externo son los listados arriba; el estado de filtrado del panel no está entre ellos, en ninguna dirección. El marco es una vista autónoma: condúcelo por la URL y el token que emites, y déjale ser dueño de lo que ocurre dentro. Si tu interfaz necesita compartir una selección con el panel, pon esa selección en el predicado de filtrado del token, que la plataforma añade a cada consulta que autoriza — del lado del servidor, y con la misma respuesta para cada tarjeta.

Elige la audiencia pensando en el enmarcado. Un panel publicado como public es una URL pública, con todo lo que eso implica; el carril de token firmado es el que hay que usar cuando importa quién mira y desde dónde, y la lista de dominios permitidos de una clave de API es lo que ata sus respuestas a tu origen.

El carril de consulta con token firmado corre en eu-central-1. Ahí vive el motor de consulta de las incrustaciones protegidas. Si los datos de tu cuenta están en otra región, háblanos antes de construir sobre este carril.

Próximos pasos​