In jeder Verwaltungsanwendung kommt der Moment von Tabelle Nummer zwanzig: noch ein TypeScript-Interface, noch ein DTO, noch eine Komponente mit Paginierung, Sortierung, Filtern und Export. Nützlicher Code, aber immer derselbe, und er wächst mit jeder Anforderung. In diesem Artikel zeige ich, wie Angular mit Looker Embedded diesen Boilerplate für Auswertungs- und Analysedaten fast vollständig überflüssig macht.
Das Problem mit klassischem Boilerplate
Der klassische Ansatz, ein Dataset in Angular anzuzeigen, braucht immer dieselben Bausteine:
- Interfaces und DTOs, die die Datenstruktur des Backends im Frontend duplizieren und bei jeder Änderung angepasst werden müssen.
- Eine eigene Tabellenkomponente mit Angular Material oder PrimeNG, von Hand definierten Spalten und Templates für jedes Feld.
- Paginierung, Sortierung und Filter, die verdrahtet und getestet werden müssen, oft auch serverseitig.
- Formatierung von Beträgen, Datumsangaben und Status, in jeder Tabelle aufs Neue.
- Export nach CSV oder Excel, von Hand geschrieben oder an eine weitere Bibliothek delegiert.
So sieht – stark gekürzt – eine einzige Auftragstabelle aus:
// 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
Und das ist nur das Frontend: Jede neue Spalte bedeutet Änderungen an DTO, Mapping, Template und Tests sowie ein gemeinsames Release von Backend und Frontend.
Die Wende: Angular mit Looker Embedded
Mit Looker leben Datenvertrag und Darstellung im LookML-Modell: Dimensionen, Kennzahlen, Formate, Beschriftungen und Beziehungen werden einmal definiert, auf der Analytics-Seite. Das Frontend muss die Struktur der Daten nicht mehr kennen: Es zeigt nur fertige Inhalte an, die Looker in einem sicheren iframe einbettet.
Das SDK wird einmal für die gesamte Anwendung initialisiert, mit dem Endpoint unseres Backends, der die Embed-URLs signiert:
// 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')),
],
};
Danach genügt eine generische Komponente, <app-looker-table>, die nur die ID des Inhalts erhält und nichts über Spalten oder Typen weiß:
@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
Beachte, was fehlt: kein interface, kein DTO, keine Spaltendefinition, keine Exportfunktion. Dieselbe Komponente zeigt Aufträge, Rechnungen oder jedes andere Dataset.
Die Vorteile für das Team
Neue Spalten ohne Frontend-Deployment
Eine Spalte hinzufügen oder ein Format bzw. eine Beschriftung ändern ist eine Änderung am LookML-Modell oder am Look: Sobald sie gespeichert ist, ist sie in der Anwendung sichtbar. Das Frontend muss weder neu gebaut noch ausgeliefert werden.
Export, Drill-down und Paginierung inklusive
Downloads als CSV, Excel und PDF, Sortierung, Paginierung und Drill-down auf Werte sind Looker-Funktionen – bereits getestet, in jeder Tabelle einheitlich und über die Berechtigungen des Nutzers steuerbar.
Sicherheit auf Zeilenebene
Beim Signed Embedding erzeugt unser Backend die signierte URL mit den Attributen des angemeldeten Nutzers. Im LookML-Modell nutzt ein access_filter diese Attribute, um jede Abfrage zu filtern: Ein Nutzer kann die Zeilen einer anderen Region nicht sehen, auch nicht durch Manipulation der 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
}
}
Der Vergleich
| Aspekt | Klassische Tabelle in Angular | Angular mit Looker Embedded |
|---|---|---|
| Frontend-Code für ein neues Dataset | Dutzende bis Hunderte Zeilen: Service, Mapping, Komponente, Template | Eine Template-Zeile mit einer gemeinsamen Komponente |
| TypeScript-Interfaces pro Dataset | Mindestens zwei: DTO und View-Model | Keine |
| Eine Spalte hinzufügen | Entwicklung, Review und Deployment von Backend und Frontend | Änderung am LookML-Modell, kein Frontend-Deployment |
| Paginierung, Sortierung, Filter, Export | Selbst implementieren und testen | Enthalten |
| Datensicherheit pro Nutzer | Eigene Logik im Backend | Nutzerattribute und Access Filter in LookML |
| Wartung | Wächst mit der Zahl der Tabellen | Im Datenmodell gebündelt |
Wann man es nicht einsetzen sollte
- Bearbeitbare Tabellen: Erfassen, Inline-Bearbeitung und Zeilenaktionen bleiben Aufgabe der Anwendung.
- Stark individualisierte Oberflächen: Der Inhalt lebt in einem iframe, das Styling wird also über Looker-Themes angepasst, nicht über das CSS der App.
- Kosten und Infrastruktur: Looker ist eine lizenzpflichtige Plattform mit einem zu pflegenden Datenmodell; es lohnt sich bei vielen, häufig wechselnden Datasets.
- Anwendungslogik braucht weiterhin ihre Typen: Interfaces entfallen für Auswertungstabellen, nicht für Formulare und Geschäftsregeln.
Fazit
- Time-to-Market: Ein neuer Report wird zu einer Template-Zeile, nicht zu einem Sprint.
- Wartbarkeit: Die Angular-Codebasis wächst nicht mehr mit jedem Dataset; die Komplexität bleibt im Datenmodell, wo sie hingehört.
- Sicherheit: Regeln zur Datensichtbarkeit sind zentralisiert und gelten für jede Abfrage.
- Klare Rollen: Frontend-Entwickler konzentrieren sich auf die Anwendung, Datenleute auf das Modell.
Für Auswertungs- und Analysedaten verlagert Angular mit Looker Embedded die Arbeit vom repetitiven Code ins Datenmodell. Wenn du tiefer einsteigen willst, wie man den Rest der Anwendung organisiert, habe ich über Angular-Architektur und über Design Systems mit wiederverwendbaren Komponenten geschrieben; wenn du eine ähnliche Integration planst, schreib mir.