Lattice Grid Buy a licence

demo D262

IoT Fleet Telemetry Dashboard: A Grid Per Device in React

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 React version. The grid mounts through the createLatticeGrid adapter, which takes its configuration as ordinary props and hands back the live grid through a ref. The grid below is the same one every other tab runs.

The configuration

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/lattice-grid.min.css">

<div id="app"></div>

<script type="module">
  import React from 'https://esm.sh/react@18';
  import { createRoot } from 'https://esm.sh/react-dom@18/client';
  import { createGrid, createHeadlessGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/lattice-grid.esm.min.js';
  import { createLatticeReact } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/modules/react.esm.min.js';
  import { createChart } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/modules/charts.esm.min.js';
  import { createDataRouter } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/modules/data-router.esm.min.js';
  import { MockWebSocket } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/modules/mock-socket.esm.min.js';

  const { LatticeGrid, LatticeChart, useLatticeRouter, LatticeRouterProvider } =
    createLatticeReact({ React, createGrid, createChart, createDataRouter });

  // A feed is any generator that yields { kind: 'snapshot', rows } first, then
  // { kind: 'delta', changes } forever; each record carries a kind and an id.
  // The vendored mock-socket module ships no ready-made fleet feed, so this
  // one is written here, to the same contract.
  function* fleetFeed() {
    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 = [];
    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 unitColumn = { field: 'id', title: 'Unit', layout: { width: 92 } };
  const regionColumn = { field: 'region', title: 'Region', filter: { type: 'set' } };
  const statusColumn = { field: 'status', title: 'Status' };
  const truckColumns = [unitColumn, regionColumn, { field: 'speed', title: 'Speed', type: 'number' }, { field: 'fuel', title: 'Fuel %', type: 'number' }, statusColumn];
  const droneColumns = [unitColumn, regionColumn, { field: 'altitude', title: 'Alt (m)', type: 'number' }, { field: 'battery', title: 'Battery %', type: 'number' }, statusColumn];
  const sensorColumns = [unitColumn, regionColumn, { field: 'tempC', title: 'Temp C', type: 'number' }, { field: 'battery', title: 'Battery %', type: 'number' }, statusColumn];

  function App() {
    const router = useLatticeRouter({ key: 'kind', rowKey: 'id' });
    const [metricsGrid, setMetricsGrid] = React.useState(null);

    React.useEffect(() => {
      if (!router) return undefined;
      const metrics = createHeadlessGrid({ rowKey: 't', columns: [
        { field: 't', type: 'number' }, { field: 'online', type: 'number' },
      ] });
      router.attach(metrics, 'metric', { rowKey: 't' });
      setMetricsGrid(metrics);

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

    return (
      <LatticeRouterProvider router={router}>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '14px' }}>
          <LatticeGrid route="truck" rowKey="id" highlightOnChange={{ duration: 400 }} columns={truckColumns} rows={[]} style={{ height: '300px' }} />
          <LatticeGrid route="drone" rowKey="id" highlightOnChange={{ duration: 400 }} columns={droneColumns} rows={[]} style={{ height: '300px' }} />
          <LatticeGrid route="sensor" rowKey="id" highlightOnChange={{ duration: 400 }} columns={sensorColumns} rows={[]} style={{ height: '300px' }} />
        </div>
        {metricsGrid && <LatticeChart grid={metricsGrid} type="line" x="t" y="online" title="Units online per tick" style={{ height: '200px', marginTop: '14px' }} />}
      </LatticeRouterProvider>
    );
  }

  createRoot(document.getElementById('app')).render(<App />);
</script>