demo D295
A network operations centre in Angular
2,000 devices and 10,000 alarms on one feed: two grids, two charts and a KPI strip, with a live incident to watch the whole screen react
createDataRouter · router.link · router.attach(rollup)
One mixed feed of devices and alarms, routed to two grids, two charts and a KPI strip at once. Click a device and the alarm grid narrows to it; run the incident and watch the whole screen react to the same feed.
This is the Angular version. A standalone component takes the whole configuration through one config input, surfaces grid events as outputs, and exposes the live grid on a getter for anything the inputs do not cover.
The configuration
import { Component, signal, inject, type Signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid, createHeadlessGrid, type Grid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
import { LatticeGridComponent, LatticeChartComponent, LatticeRouter, provideLattice, provideLatticeRouter } from '@toclocoinc/lattice-grid/angular';
@Component({
selector: 'app-root',
standalone: true,
imports: [LatticeGridComponent, LatticeChartComponent],
// One feed, partitioned by kind: a device roster, its alarm stream and a
// severity rollup, all off createDataRouter.
providers: [provideLatticeRouter({ key: 'kind', rowKey: 'id' })],
template:
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">' +
' <lattice-grid route="device" [config]="deviceConfig" style="display:block;height:320px"></lattice-grid>' +
' <lattice-grid route="alarm" [config]="alarmConfig" style="display:block;height:320px"></lattice-grid>' +
'</div>' +
'@if (severityGrid(); as g) {' +
' <lattice-chart [grid]="g" [config]="chartConfig" style="display:block;height:220px;margin-top:14px"></lattice-chart>' +
'}',
})
export class AppComponent {
private routerService = inject(LatticeRouter);
severityGrid = signal<Grid | null>(null);
chartConfig = { type: 'bar', x: 'level', y: 'count', title: 'Open alarms by severity' };
deviceConfig = {
rowKey: 'id', columns: [
{ field: 'id', title: 'Device', layout: { width: 110, pin: 'start' } },
{ field: 'site', title: 'Site', filter: { type: 'set' } },
{ field: 'status', title: 'Status', filter: { type: 'set' } },
],
};
alarmConfig = {
rowKey: 'id', highlightOnChange: { duration: 500 }, columns: [
{ field: 'id', title: 'Alarm', layout: { width: 96, pin: 'start' } },
{ field: 'device', title: 'Device' },
{ field: 'severity', title: 'Severity', filter: { type: 'set' } },
],
};
constructor() {
const severity = createHeadlessGrid({ rowKey: 'level', columns: [
{ field: 'level', title: 'Severity' }, { field: 'count', title: 'Count', type: 'number' },
] });
this.routerService.attach(severity, 'rollup');
this.severityGrid.set(severity);
}
}
bootstrapApplication(AppComponent, {
providers: [provideLattice({ createGrid, createChart, createDataRouter })],
});
A whole operations screen from one feed
A network operations centre is the shape a live desk usually takes to its most demanding extreme: thousands of devices, a stream of alarms, a handful of headline figures and a couple of trend lines, all supposed to agree with each other from one moment to the next. Here two thousand devices and ten thousand alarm records are one feed into one Data Router, partitioned by what each record is. Device records land in the Devices grid; alarm records land in the Active Alarms grid, held to the ones still open. A KPI strip and two charts read the very same partitioned stream, so a number on a tile and a bar on a chart are never a stale read of a screen that has already moved on.
The devices and alarms are not just two grids sharing a page - they are related. Every alarm carries the deviceId of the device it belongs to, and the router’s link ties the two grids on that field. Click a device and the alarms grid narrows to its own alarms on the spot; clear the selection and it opens back up. That is the whole trick behind drilling from a summary into the record behind it, and here it costs one line of configuration rather than a query written by hand.
Watching a real incident, live
The SIMULATE NETWORK INCIDENT button degrades twenty devices at one site over a few seconds: their latency and packet loss climb, and a shadow column - Trend, tracking each device’s health against where it stood when the page opened - falls sharply. New alarms open against those devices as they degrade, the KPI strip’s alarm and SLA figures move, both charts move, and because the Devices grid is sorted on that same Trend column, the affected devices climb straight to the top on their own. Nothing here is a separate demo mode: the incident pushes rows through the identical feed the ambient traffic already uses, the same way a real network’s fault stream is just more of the same feed, not a different one.
How do I split one feed into a grid, a linked grid, a chart and a KPI strip?
Create one router with createDataRouter({ key: 'type', rowKey: 'id', overlap: true }) and call router.attach(view, value, opts) once per view: a grid for 'device', a grid for 'alarm' (with a filter to keep only the open ones), a headless grid feeding a chart, and a KPI panel. overlap: true matters as soon as more than one view reads the same value - the KPI strip and the severity chart both read 'device'/'alarm' alongside the grids that already claim them, and without it only the first-attached view on a value would ever see it. router.link(devices, alarms, { from: 'deviceId', to: 'deviceId' }) makes a pick in the devices grid narrow the alarms grid to the same device. For the alarms-by-severity chart, attach a headless grid with rollup: { groupBy: 'severity', aggregate: { count: { op: 'count' } } } and the router keeps one summary row per severity, updated by keyed diff as alarms open and close.