In ogni applicazione gestionale arriva il momento della tabella numero venti: un'altra interfaccia TypeScript, un altro DTO, un altro componente con paginazione, ordinamento, filtri ed export. È codice utile, ma tutto uguale, e cresce a ogni richiesta. In questo articolo mostro come, per i dati di consultazione e analisi, Angular con Looker Embedded permetta di eliminare quasi del tutto quel boilerplate.
Il problema del boilerplate tradizionale
L'approccio classico per mostrare un dataset in Angular richiede sempre gli stessi pezzi:
- Interfacce e DTO che duplicano sul frontend la forma dei dati del backend, da aggiornare a ogni modifica.
- Un componente tabella dedicato, con Angular Material o PrimeNG, colonne definite a mano e template per ogni campo.
- Paginazione, ordinamento e filtri da collegare e testare, spesso anche lato server.
- Formattazione di valute, date e stati, ripetuta in ogni tabella.
- Export in CSV o Excel, scritto a mano o affidato a un'altra libreria.
Ecco come appare, in forma ridotta, una sola tabella ordini:
// 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
E questo è solo il frontend: per ogni nuova colonna serve cambiare il DTO, il mapping, il template, i test, e rilasciare backend e frontend insieme.
La svolta: Angular con Looker Embedded
Con Looker il contratto dati e la presentazione vivono nel modello LookML: dimensioni, misure, formati, etichette e relazioni sono definiti una volta sola, lato analytics. Il frontend non ha più bisogno di conoscere la forma dei dati: deve solo mostrare un contenuto già pronto, che Looker incorpora in un iframe sicuro.
Si inizializza l'SDK una volta per l'applicazione, indicando l'endpoint del nostro backend che firma gli URL di 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')),
],
};
Poi basta un componente generico, <app-looker-table>, che riceve solo l'ID del contenuto e non sa nulla di colonne o tipi:
@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
Notate cosa manca: nessuna interface, nessun DTO, nessuna definizione di colonna, nessuna funzione di export. Lo stesso componente mostra gli ordini, le fatture o qualunque altro dataset.
I vantaggi per il team
Colonne nuove senza deploy del frontend
Aggiungere una colonna, cambiare un formato o un'etichetta è una modifica al modello LookML o al Look: appena salvata, è visibile nell'applicazione. Il frontend non va né ricompilato né rilasciato.
Export, drill-down e paginazione inclusi
Download in CSV, Excel e PDF, ordinamento, paginazione e drill-down sui valori sono funzioni di Looker, già testate e coerenti in ogni tabella, e si abilitano con i permessi dell'utente.
Sicurezza a livello di riga
Con il signed embed, il nostro backend genera l'URL firmato con gli attributi dell'utente loggato. Nel modello LookML un access_filter usa quegli attributi per filtrare ogni query: un utente non può vedere le righe di un'altra regione, nemmeno manipolando l'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
}
}
Il confronto
| Aspetto | Tabella tradizionale in Angular | Angular con Looker Embedded |
|---|---|---|
| Codice frontend per un nuovo dataset | Decine o centinaia di righe: service, mapping, componente, template | Una riga di template, con un componente condiviso |
| Interfacce TypeScript per dataset | Almeno due: DTO e modello di vista | Nessuna |
| Aggiungere una colonna | Sviluppo, review e deploy di backend e frontend | Modifica al modello LookML, senza deploy frontend |
| Paginazione, ordinamento, filtri, export | Da implementare e testare | Inclusi |
| Sicurezza sui dati per utente | Logica dedicata nel backend | Attributi utente e access filter in LookML |
| Manutenzione | Cresce con il numero di tabelle | Concentrata nel modello dati |
Quando non usarlo
- Tabelle modificabili: inserimento, modifica in linea e azioni sulle righe restano compito dell'applicazione.
- Interfacce molto personalizzate: il contenuto vive in un iframe, quindi lo stile si adatta con temi Looker, non con il CSS dell'app.
- Costo e infrastruttura: Looker è una piattaforma con licenza e un modello dati da mantenere; ha senso quando i dataset sono molti e cambiano spesso.
- La logica applicativa ha ancora bisogno dei suoi tipi: le interfacce spariscono per le tabelle di consultazione, non per form e regole di business.
Conclusioni
- Time-to-market: un nuovo report diventa una riga di template, non uno sprint.
- Manutenibilità: la codebase Angular smette di crescere a ogni dataset; la complessità resta nel modello dati, dove deve stare.
- Sicurezza: le regole di visibilità dei dati sono centralizzate e applicate a ogni query.
- Divisione dei ruoli: gli sviluppatori frontend si concentrano sull'esperienza dell'applicazione, chi lavora sui dati sul modello.
Per i dati di consultazione e analisi, Angular con Looker Embedded sposta il lavoro dal codice ripetitivo al modello dati. Se volete approfondire come organizzare il resto dell'applicazione, ho scritto di architettura Angular e di design system con componenti riutilizzabili; se state valutando un'integrazione simile, scrivetemi.