Lattice Grid Buy a licence

demo D262

IoT Fleet Telemetry Dashboard: A Grid Per Device in Angular

One telemetry feed, a live grid per device class and a units-online chart, with no backend

createDataRouter · MockWebSocket · rng

This is a ready-made fleet telemetry screen: one telemetry feed drives a live grid per device class and a units-online chart, with no backend. It is a working layout for monitoring many devices from one stream, ready to point at real hardware.

Building…
Loading a live grid…

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, OnDestroy, 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 { MockWebSocket } from '@toclocoinc/lattice-grid/modules/mock-socket';
import { LatticeGridComponent, LatticeChartComponent, LatticeRouter, provideLattice, provideLatticeRouter } from '@toclocoinc/lattice-grid/angular';

// The vendored mock-socket module ships no ready-made fleet feed, so this one
// is written here, to the same contract MockWebSocket expects (a snapshot,
// then deltas forever).
function* fleetFeed(): Generator<any> {
  const kinds = [
    { kind: 'truck', n: 4, extra: () => ({ speed: 40 + Math.round(Math.random() * 40), fuel: 30 + Math.round(Math.random() * 60) }) },
    { kind: 'drone', n: 3, extra: () => ({ altitude: 80 + Math.round(Math.random() * 300), battery: 20 + Math.round(Math.random() * 70) }) },
    { kind: 'sensor', n: 3, extra: () => ({ tempC: 10 + Math.round(Math.random() * 25), battery: 20 + Math.round(Math.random() * 70) }) },
  ];
  const regions = ['EMEA', 'AMER', 'APAC'];
  const rows: any[] = [];
  for (const k of kinds) for (let i = 0; i < k.n; i++) {
    rows.push({ id: k.kind + '-' + i, kind: k.kind, region: regions[i % regions.length], status: 'ok', ...k.extra() });
  }
  yield { kind: 'snapshot', rows };
  while (true) {
    const r = rows[Math.floor(Math.random() * rows.length)];
    const k = kinds.find((x) => x.kind === r.kind)!;
    yield { kind: 'delta', changes: [{ op: 'upsert', row: { ...r, ...k.extra() } }] };
  }
}

const UNIT = { field: 'id', title: 'Unit', layout: { width: 92 } };
const REGION = { field: 'region', title: 'Region', filter: { type: 'set' } };
const STATUS = { field: 'status', title: 'Status' };

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent, LatticeChartComponent],
  providers: [provideLatticeRouter({ key: 'kind', rowKey: 'id' })],
  template:
    '<div style="display:grid;grid-template-columns:repeat(3, 1fr);gap:14px">' +
    '  <lattice-grid route="truck" [config]="truckConfig" style="display:block;height:300px"></lattice-grid>' +
    '  <lattice-grid route="drone" [config]="droneConfig" style="display:block;height:300px"></lattice-grid>' +
    '  <lattice-grid route="sensor" [config]="sensorConfig" style="display:block;height:300px"></lattice-grid>' +
    '</div>' +
    '@if (metricsGrid(); as m) {' +
    '  <lattice-chart [grid]="m" [config]="chartConfig" style="display:block;height:200px;margin-top:14px"></lattice-chart>' +
    '}',
})
export class AppComponent implements OnDestroy {
  private routerService = inject(LatticeRouter);
  private socket: MockWebSocket;

  metricsGrid = signal<Grid | null>(null);
  chartConfig = { type: 'line', x: 't', y: 'online', title: 'Units online per tick' };
  truckConfig = { rowKey: 'id', highlightOnChange: { duration: 400 }, columns: [UNIT, REGION, { field: 'speed', title: 'Speed', type: 'number' }, { field: 'fuel', title: 'Fuel %', type: 'number' }, STATUS] };
  droneConfig = { rowKey: 'id', highlightOnChange: { duration: 400 }, columns: [UNIT, REGION, { field: 'altitude', title: 'Alt (m)', type: 'number' }, { field: 'battery', title: 'Battery %', type: 'number' }, STATUS] };
  sensorConfig = { rowKey: 'id', highlightOnChange: { duration: 400 }, columns: [UNIT, REGION, { field: 'tempC', title: 'Temp C', type: 'number' }, { field: 'battery', title: 'Battery %', type: 'number' }, STATUS] };

  constructor() {
    const metrics = createHeadlessGrid({ rowKey: 't', columns: [
      { field: 't', type: 'number' }, { field: 'online', type: 'number' },
    ] });
    this.routerService.attach(metrics, 'metric', { rowKey: 't' });
    this.metricsGrid.set(metrics);

    this.socket = new MockWebSocket({ feed: fleetFeed(), rate: 800, jitter: 200 });
    this.socket.onmessage = (event: any) => {
      const message = JSON.parse(event.data);
      if (message.kind === 'snapshot') this.routerService.router.load(message.rows);
      else this.routerService.router.apply(message.changes);
    };
  }

  ngOnDestroy() { this.socket.close(); }
}

bootstrapApplication(AppComponent, {
  providers: [provideLattice({ createGrid, createChart, createDataRouter })],
});