En toda aplicación de gestión llega el momento de la tabla número veinte: otra interfaz TypeScript, otro DTO, otro componente con paginación, ordenación, filtros y exportación. Es código útil, pero siempre igual, y crece con cada petición. En este artículo muestro cómo, para datos de consulta y análisis, Angular con Looker Embedded permite eliminar casi todo ese boilerplate.
El problema del boilerplate tradicional
El enfoque clásico para mostrar un dataset en Angular necesita siempre las mismas piezas:
- Interfaces y DTOs que duplican en el frontend la forma de los datos del backend, y que hay que actualizar con cada cambio.
- Un componente de tabla dedicado, con Angular Material o PrimeNG, columnas definidas a mano y plantillas para cada campo.
- Paginación, ordenación y filtros que conectar y probar, a menudo también en el servidor.
- Formato de monedas, fechas y estados, repetido en cada tabla.
- Exportación a CSV o Excel, escrita a mano o delegada a otra librería más.
Así queda, en versión reducida, una sola tabla de pedidos:
// 1. The API contract, duplicated on the frontend
export interface OrderDto {
id: number; customer_name: string; region: string;
total_cents: number; status: 'OPEN' | 'PAID' | 'CANCELLED'; created_at: string;
}
// 2. The view model, because the API shape is not the table shape
export interface OrderRow {
id: number; customer: string; region: string;
total: number; status: string; createdAt: Date;
}
@Component({ /* ... MatTableModule, MatPaginatorModule, MatSortModule ... */ })
export class OrdersTableComponent implements AfterViewInit {
readonly columns = ['id', 'customer', 'region', 'total', 'status', 'createdAt'];
readonly dataSource = new MatTableDataSource<OrderRow>([]);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(private api: OrdersApi) {
this.api.list().subscribe(dtos => (this.dataSource.data = dtos.map(toRow)));
}
ngAfterViewInit(): void {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
applyFilter(value: string): void { this.dataSource.filter = value.trim().toLowerCase(); }
exportCsv(): void { /* build the CSV by hand, escape quotes, trigger the download... */ }
}
// 3. Mapping and formatting, also by hand
const toRow = (d: OrderDto): OrderRow => ({
id: d.id, customer: d.customer_name, region: d.region,
total: d.total_cents / 100, status: d.status, createdAt: new Date(d.created_at),
});
// ...plus the HTML template with one <ng-container matColumnDef> per column
Y esto es solo el frontend: cada nueva columna obliga a cambiar el DTO, el mapeo, la plantilla y los tests, y a desplegar backend y frontend a la vez.
El cambio: Angular con Looker Embedded
Con Looker, el contrato de datos y la presentación viven en el modelo LookML: dimensiones, medidas, formatos, etiquetas y relaciones se definen una sola vez, en la capa de analítica. El frontend ya no necesita conocer la forma de los datos: solo tiene que mostrar un contenido ya preparado, que Looker incrusta en un iframe seguro.
El SDK se inicializa una vez para toda la aplicación, indicando el endpoint de nuestro backend que firma las URLs de embed:
// app.config.ts — once for the whole application
import { LookerEmbedSDK } from '@looker/embed-sdk';
export const appConfig: ApplicationConfig = {
providers: [
// The SDK asks our backend for a signed embed URL for each content item
provideAppInitializer(() => LookerEmbedSDK.init('analytics.example.com', '/api/looker/embed-auth')),
],
};
Después basta con un componente genérico, <app-looker-table>, que solo recibe el ID del contenido y no sabe nada de columnas ni de tipos:
@Component({
selector: 'app-looker-table',
template: `<div #host class="looker-host"></div>`,
styles: `.looker-host, .looker-host iframe { width: 100%; height: 100%; border: 0; }`,
})
export class LookerTableComponent {
/** Look ID defined in Looker: the component knows nothing about the data. */
readonly lookId = input.required<string>();
private readonly host = viewChild.required<ElementRef<HTMLDivElement>>('host');
constructor() {
afterNextRender(() => {
LookerEmbedSDK.createLookWithId(this.lookId())
.appendTo(this.host().nativeElement)
.build()
.connect()
.catch(err => console.error('Looker embed failed', err));
});
}
}
// Usage: the same component for any dataset
// <app-looker-table lookId="42" /> orders
// <app-looker-table lookId="57" /> invoices
Fíjate en lo que falta: ninguna interface, ningún DTO, ninguna definición de columna, ninguna función de exportación. El mismo componente muestra pedidos, facturas o cualquier otro dataset.
Las ventajas para el equipo
Columnas nuevas sin desplegar el frontend
Añadir una columna o cambiar un formato o una etiqueta es un cambio en el modelo LookML o en el Look: en cuanto se guarda, es visible en la aplicación. El frontend no hay que recompilarlo ni desplegarlo.
Exportación, drill-down y paginación incluidos
Las descargas en CSV, Excel y PDF, la ordenación, la paginación y el drill-down sobre los valores son funciones de Looker, ya probadas y coherentes en todas las tablas, y se activan con los permisos del usuario.
Seguridad a nivel de fila
Con el signed embed, nuestro backend genera la URL firmada con los atributos del usuario autenticado. En el modelo LookML, un access_filter usa esos atributos para filtrar todas las consultas: un usuario no puede ver las filas de otra región, ni siquiera manipulando la URL.
// NestJS: signs the embed URL with the logged-in user's attributes
@Get('looker/embed-auth')
async embedAuth(@Query('src') src: string, @CurrentUser() user: User) {
const embed = await this.looker.ok(this.looker.create_sso_embed_url({
target_url: `https://analytics.example.com${src}`,
session_length: 3600,
external_user_id: String(user.id),
first_name: user.firstName,
last_name: user.lastName,
permissions: ['access_data', 'see_looks', 'download_without_limit'],
models: ['sales'],
user_attributes: { region: user.region }, // drives row-level security
}));
return { url: embed.url };
}
# LookML: every query on this explore is filtered by the user's region
explore: orders {
access_filter: {
field: customers.region
user_attribute: region
}
}
La comparación
| Aspecto | Tabla tradicional en Angular | Angular con Looker Embedded |
|---|---|---|
| Código frontend para un nuevo dataset | Decenas o cientos de líneas: service, mapeo, componente, plantilla | Una línea de plantilla, con un componente compartido |
| Interfaces TypeScript por dataset | Al menos dos: DTO y modelo de vista | Ninguna |
| Añadir una columna | Desarrollo, revisión y despliegue de backend y frontend | Un cambio en el modelo LookML, sin despliegue de frontend |
| Paginación, ordenación, filtros, exportación | A implementar y probar | Incluidos |
| Seguridad de los datos por usuario | Lógica específica en el backend | Atributos de usuario y access filters en LookML |
| Mantenimiento | Crece con el número de tablas | Concentrado en el modelo de datos |
Cuándo no usarlo
- Tablas editables: inserción, edición en línea y acciones sobre las filas siguen siendo tarea de la aplicación.
- Interfaces muy personalizadas: el contenido vive en un iframe, así que el estilo se adapta con los temas de Looker, no con el CSS de la aplicación.
- Coste e infraestructura: Looker es una plataforma con licencia y un modelo de datos que mantener; compensa cuando los datasets son muchos y cambian a menudo.
- La lógica de la aplicación sigue necesitando sus tipos: las interfaces desaparecen en las tablas de consulta, no en los formularios ni en las reglas de negocio.
Conclusiones
- Time-to-market: un nuevo informe pasa a ser una línea de plantilla, no un sprint.
- Mantenibilidad: la codebase de Angular deja de crecer con cada dataset; la complejidad se queda en el modelo de datos, donde debe estar.
- Seguridad: las reglas de visibilidad de los datos están centralizadas y se aplican a cada consulta.
- Reparto de roles: los desarrolladores frontend se centran en la experiencia de la aplicación, y quienes trabajan con datos, en el modelo.
Para datos de consulta y análisis, Angular con Looker Embedded traslada el trabajo del código repetitivo al modelo de datos. Si quieres profundizar en cómo organizar el resto de la aplicación, he escrito sobre arquitectura Angular y sobre design systems con componentes reutilizables; si estás valorando una integración parecida, escríbeme.