Lattice Grid Buy a licence

demo D313

Reading Splunk live: JavaScript Data Grid Demo

A grid over Splunk’s own search REST API, filter and sort turned into SPL; run here against a mock of that API, since there is no public instance or credential this site can publish

createPushdownSource · splunkAdapter · lastPlan()

This grid reads Splunk's own search REST API, turning a filter or a sort into SPL and paging the job's results. There is no public Splunk instance and no credential this site could publish safely, so it runs against a mock of that API in the same shape a real instance answers with, and shows the split between what reached the search and what stayed with the grid. In production the token never reaches the browser; see Where the credential lives in the adapter guide.

Building…
Loading a live grid…

The configuration

<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.104.0/lattice-grid.min.css">
<script src="https://cdn.latticegrid.dev/lattice-grid@1.104.0/lattice-grid.min.js"></script>

<div id="grid" style="height:520px"></div>

<script>
  // There is no public Splunk instance to run this sample against and no
  // credential this page could publish, so `fetch` here answers the search
  // REST API's own dispatch/poll/results/cancel sequence from a small mock
  // instead of a real instance. Point `fetch` at your own wrapper (or drop
  // it and pass a token in `headers`) to run this against a real one.
  const mockEvents = Array.from({ length: 40 }, (_, i) => ({
    event_id: 'evt-' + (i + 1),
    _time: new Date(Date.UTC(2026, 8, 27, 0, i * 30)).toISOString(),
    src_ip: '10.20.' + (i % 255) + '.' + ((i * 3) % 255),
    action: i % 3 === 0 ? 'allowed' : 'blocked',
  }));
  // Read the filter and `| sort 0 …` out of the dispatched SPL and answer
  // with the events that actually match it, not the whole set.
  const runSearch = (search) => {
    let body = search.replace(/^search\s+/, '');
    const sortMatch = / \| sort 0 (.+)$/.exec(body);
    if (sortMatch) body = body.slice(0, sortMatch.index);
    const expr = body.replace(/^index=security sourcetype=firewall\s*/, '').trim();
    const test = expr
      ? new Function('e', 'return ' + expr
          .replace(/([A-Za-z_]\w*)=("(?:[^"\\\\]|\\\\.)*")/g, (_, f, v) => {
            const inner = v.slice(1, -1).replace(/\\\\(.)/g, '$1');
            return inner.includes('*')
              ? `(/^${inner.replace(/[.+?^${}()|[\]\\\\]/g, '\\\\$&').replace(/\*/g, '.*')}$/i.test(String(e.${f})))`
              : `(String(e.${f}) === ${JSON.stringify(inner)})`;
          })
          .replace(/\bAND\b/g, '&&').replace(/\bOR\b/g, '||').replace(/\bNOT\b/g, '!'))
      : () => true;
    const matched = mockEvents.filter(test);
    if (sortMatch) {
      const entries = sortMatch[1].split(',').map((s) => s.trim());
      matched.sort((a, b) => {
        for (const entry of entries) {
          const dir = entry[0] === '-' ? -1 : 1;
          const field = entry.replace(/^[+-]/, '');
          if (a[field] < b[field]) return -1 * dir;
          if (a[field] > b[field]) return 1 * dir;
        }
        return 0;
      });
    }
    return matched;
  };
  const jobs = new Map();
  const mockFetch = async (input, init = {}) => {
    const url = new URL(String(input));
    const method = String(init.method || 'GET').toUpperCase();
    if (method === 'POST' && url.pathname.endsWith('/services/search/v2/jobs')) {
      const sid = 'mock-' + (jobs.size + 1);
      const params = new URLSearchParams(String(init.body || ''));
      jobs.set(sid, runSearch(params.get('search') || ''));
      return new Response(JSON.stringify({ sid }));
    }
    if (method === 'GET' && url.pathname.endsWith('/results')) {
      const sid = url.pathname.match(/\/jobs\/([^/]+)\/results$/)[1];
      const rows = jobs.get(sid) || [];
      const offset = Number(url.searchParams.get('offset') || 0);
      const count = Number(url.searchParams.get('count') || 0) || rows.length;
      return new Response(JSON.stringify({ results: rows.slice(offset, offset + count) }));
    }
    if (method === 'GET') {
      const sid = url.pathname.match(/\/jobs\/([^/]+)$/)[1];
      const rows = jobs.get(sid) || [];
      return new Response(JSON.stringify({ isDone: true, dispatchState: 'DONE', resultCount: rows.length }));
    }
    return new Response(null, { status: 200 }); // DELETE, cancelling the job
  };

  // Against a real instance: drop `fetch` and put a bearer token in `headers`,
  // or supply your own `fetch` wrapper for one that expires and needs refreshing.
  // In production the token never reaches the browser at all; see "Where the
  // credential lives" at /docs/sources/splunk/#credential for the setup.
  const adapter = LatticeGrid.splunkAdapter({
    url: 'https://splunk.example.com:8089',
    search: 'index=security sourcetype=firewall',
    earliest: '-24h',
    fetch: mockFetch,
  });
  const source = LatticeGrid.createPushdownSource({
    adapter,
    compute: LatticeGrid,
    pageSize: 25,
  });

  const grid = LatticeGrid.createGrid(document.getElementById('grid'), {
    rowKey: 'event_id',
    toolPanel: { side: 'left', panels: ['filters', 'columns'] },
    columns: [
      { field: '_time', title: 'Time', type: 'dateString' },
      { field: 'src_ip', title: 'Source IP' },
      { field: 'action', title: 'Action', filter: { type: 'set' } },
    ],
    source,
  });

  // After each query, lastPlan() reports what the search ran and what stayed here.
  grid.on('rows:changed', () => {
    const plan = source.lastPlan();
    if (plan) console.log(plan.unpushed.length ? 'pushed part' : 'pushed all', plan);
  });
</script>