Lattice Grid Buy a licence

demo D263

Support Desk Board: One Ticket Stream, Many Queues in React

One ticket stream, a live lane per queue and an open-tickets chart, with no backend

createDataRouter · MockWebSocket

This is a ready-made service desk: one ticket stream drives a live lane per queue and an open-tickets chart, with no backend. It shows a support view built from a single feed, ready to connect to a real ticketing source.

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 });

  // The vendored mock-socket module ships no ready-made desk feed, so this one
  // is written here, to the same contract MockWebSocket expects.
  function* deskFeed() {
    const queues = ['billing', 'technical', 'onboarding'];
    const agents = ['Ana', 'Bo', 'Cy', 'Dee'];
    let nextId = 1;
    const ticket = (queue) => ({
      id: 'K-' + String(1000 + nextId++), queue, subject: 'Ticket ' + nextId,
      priority: ['low', 'normal', 'high'][Math.floor(Math.random() * 3)],
      agent: agents[Math.floor(Math.random() * agents.length)],
      waitMins: Math.round(Math.random() * 90),
    });
    const rows = [];
    for (const q of queues) for (let i = 0; i < 6; i++) rows.push(ticket(q));
    yield { kind: 'snapshot', rows };
    while (true) {
      // A resolved ticket leaves its lane as a delete, a new one drops in as
      // an upsert, with no refresh.
      if (Math.random() < 0.3 && rows.length) {
        const gone = rows.splice(Math.floor(Math.random() * rows.length), 1)[0];
        yield { kind: 'delta', changes: [{ op: 'delete', row: { id: gone.id } }] };
      } else {
        const row = ticket(queues[Math.floor(Math.random() * queues.length)]);
        rows.push(row);
        yield { kind: 'delta', changes: [{ op: 'upsert', row }] };
      }
    }
  }

  const columns = [
    { field: 'id', title: 'Ticket', layout: { width: 92 } },
    { field: 'subject', title: 'Subject', layout: { flex: 1, min: 120 } },
    { field: 'priority', title: 'Priority', filter: { type: 'set' } },
    { field: 'agent', title: 'Agent', filter: { type: 'set' } },
    { field: 'waitMins', title: 'Wait', type: 'number', total: 'max' },
  ];

  function App() {
    // Partition on the queue: a resolved ticket leaves its lane as a delete, a
    // new one drops in as an upsert, with no refresh.
    const router = useLatticeRouter({ key: 'queue', 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: 'open', type: 'number' },
      ] });
      router.attach(metrics, 'metric', { rowKey: 't' });
      setMetricsGrid(metrics);

      const socket = new MockWebSocket({ feed: deskFeed(), rate: 1100, jitter: 300 });
      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="billing" rowKey="id" selection="single" highlightOnChange={{ duration: 400 }} columns={columns} rows={[]} style={{ height: '320px' }} />
          <LatticeGrid route="technical" rowKey="id" selection="single" highlightOnChange={{ duration: 400 }} columns={columns} rows={[]} style={{ height: '320px' }} />
          <LatticeGrid route="onboarding" rowKey="id" selection="single" highlightOnChange={{ duration: 400 }} columns={columns} rows={[]} style={{ height: '320px' }} />
        </div>
        {metricsGrid && <LatticeChart grid={metricsGrid} type="area" x="t" y="open" title="Open tickets across the desk" style={{ height: '200px', marginTop: '14px' }} />}
      </LatticeRouterProvider>
    );
  }

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