Building Monitoring Dashboards with React

The @unblind/react package provides React hooks, components, and providers for building monitoring dashboards in any React application. Use this package if you're not using Next.js or need more control over the API layer.


Installation

Install the React package along with its peer dependencies:

npm install @unblind/react

Setup

1. Configure Your API Proxy

Since the React package doesn’t include a server component like Next.js does, you’ll need to set up your own API proxy to securely communicate with Unblind when not using Next.js. The proxy should:

  1. Extract the tenant ID from your auth system
  2. Add your Unblind API key
  3. Forward requests to the Unblind API

Follow our Express example for a concrete implementation.

2. Add Unblind css

Add Unblind styles to your global tailwind file.

globals.css

@import "@unblind/react/styles.css";
@import "tailwindcss";
/* ... */

3. Wrap Your App with UnblindProvider

The UnblindProvider sets up React Query and provides configuration to all Unblind components and hooks.

App.tsx

import { UnblindProvider } from '@unblind/react'

export default function App() {
  return (
    <UnblindProvider>
      <Dashboard />
    </UnblindProvider>
  )
}

Building Your Dashboard

Basic Dashboard

Use the Timeseries component to display metrics:

Dashboard.tsx

import { Timeseries } from '@unblind/react'

export function Dashboard() {
  return (
    <div className="grid grid-cols-2 gap-4">
      <div>
        <h3>CPU Usage</h3>
        <Timeseries
          metrics={['system.cpu.utilization']}
        />
      </div>

      <div>
        <h3>Memory Usage</h3>
        <Timeseries
          metrics={['system.memory.usage']}
        />
      </div>

      <div>
        <h3>Request Duration (p95)</h3>
        <Timeseries
          metrics={['http.server.duration']}
          groupBy={['http.route']}
          operator="p95"
          type="bar"
        />
      </div>

      <div>
        <h3>Error Count</h3>
        <Timeseries
          metrics={['http.server.request.count']}
          attributes={{
            'http.status_code': ['500', '502', '503']
          }}
        />
      </div>
    </div>
  )
}

With Dynamic Time Ranges

Allow users to change the time range:

import { useState } from 'react'
import { UnblindScope, Timeseries, TimeRange } from '@unblind/react'

export function Dashboard() {
  const [timeRange, setTimeRange] = useState<TimeRange>('6h')

  return (
    <div>
      <div className="header">
        <h1>System Metrics</h1>
        <select
          value={timeRange}
          onChange={(e) => setTimeRange(e.target.value as TimeRange)}
        >
          <option value="1h">Last Hour</option>
          <option value="6h">Last 6 Hours</option>
          <option value="24h">Last 24 Hours</option>
          <option value="7d">Last 7 Days</option>
        </select>
      </div>

      <UnblindScope timeRange={timeRange}>
        <div className="grid">
          <Timeseries metrics={['system.cpu.utilization']} />
          <Timeseries metrics={['system.memory.usage']} />
          <Timeseries metrics={['http.server.duration']} />
        </div>
      </UnblindScope>
    </div>
  )
}

Available Hooks

useMetrics

Fetches the list of available metrics with their metadata:

import { useMetrics } from '@unblind/react'

export function MetricSelector() {
  const { list, isLoading, hasError } = useMetrics()

  if (isLoading) return <div>Loading metrics...</div>
  if (hasError) return <div>Error loading metrics</div>

  return (
    <select>
      {list?.map((metric) => (
        <option key={metric.name} value={metric.name}>
          {metric.name}
        </option>
      ))}
    </select>
  )
}

useTimeseries

Fetches timeseries data for one or more metrics:

import { useTimeseries } from '@unblind/react'

export function CustomChart() {
  const { data, isLoading, hasError, refetch } = useTimeseries({
    queries: [
      {
        metrics: ['system.cpu.utilization'],
        operator: 'avg',
        groupBy: ['host.name'],
      },
      {
        metrics: ['system.memory.usage'],
        operator: 'max',
        groupBy: ['host.name'],
      }
    ],
    timeRange: '1h',
    interval: 60000, // 1 minute
  })

  if (isLoading) return <div>Loading...</div>
  if (hasError) return <div>Error loading data</div>

  const { series, times, metadata } = data

  return (
    <div>
      <button onClick={refetch}>Refresh</button>
      {series.map((serie) => (
        <div key={serie.metric}>
          <h4>{metadata[serie.metric]?.name}</h4>
          <p>Values: {serie.values.length}</p>
          <p>Latest: {serie.values[serie.values.length - 1]}</p>
        </div>
      ))}
    </div>
  )
}

useLogs

Fetches logs with infinite scroll pagination:

import { useLogs } from '@unblind/react'

export function LogViewer() {
  const {
    logs,
    isLoading,
    hasError,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage
  } = useLogs({
    timeRange: '1h',
    filters: [
      { name: 'severity', value: 'ERROR' },
      { name: 'service.name', value: 'api' }
    ]
  })

  if (isLoading) return <div>Loading logs...</div>
  if (hasError) return <div>Error loading logs</div>

  return (
    <div>
      <div className="logs">
        {logs.map((log, i) => (
          <div key={i} className="log-entry">
            <span className="timestamp">
              {new Date(log.timestamp).toISOString()}
            </span>
            <span className={`severity ${log.severity_text}`}>
              {log.severity_text}
            </span>
            <span className="message">{log.body}</span>
            {log.service_name && (
              <span className="service">{log.service_name}</span>
            )}
          </div>
        ))}
      </div>

      {hasNextPage && (
        <button
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  )
}

useUsage

Fetches usage analytics:

import { useUsage } from '@unblind/react'

export function UsageChart() {
  const { usage, isLoading, hasError } = useUsage({
    timeRange: '30d'
  })

  if (isLoading) return <div>Loading...</div>
  if (hasError) return <div>Error</div>

  return (
    <table>
      <thead>
        <tr>
          <th>Date</th>
          <th>Metric Units</th>
          <th>Log Units</th>
          <th>Log Bytes</th>
        </tr>
      </thead>
      <tbody>
        {usage.map((day) => (
          <tr key={day.date}>
            <td>{day.date}</td>
            <td>{day.metrics.units.toLocaleString()}</td>
            <td>{day.logs.units.toLocaleString()}</td>
            <td>{(day.logs.bytes / 1024 / 1024).toFixed(2)} MB</td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}

useRefresh

Manually refresh all queries:

import { useRefresh } from '@unblind/react'

export function RefreshButton() {
  const refresh = useRefresh()

  return (
    <button onClick={refresh}>
      🔄 Refresh Dashboard
    </button>
  )
}

useScope

Access the current scope configuration:

import { useScope } from '@unblind/react'

export function ScopeInfo() {
  const scope = useScope()

  return (
    <div>
      <p>Time Range: {scope.timeRange}</p>
      <p>Operator: {scope.operator}</p>
      <p>Group By: {scope.groupBy?.join(', ')}</p>
    </div>
  )
}

Advanced Patterns

Multiple Scopes

Use multiple UnblindScope components for different sections with different configurations:

import { UnblindScope, Timeseries } from '@unblind/react'

export function Dashboard() {
  return (
    <div>
      {/* System metrics with 1 hour view */}
      <UnblindScope timeRange="1h" operator="avg">
        <h2>System Metrics (Last Hour)</h2>
        <Timeseries metrics={['system.cpu.utilization']} />
        <Timeseries metrics={['system.memory.usage']} />
      </UnblindScope>

      {/* Application metrics with 24 hour view */}
      <UnblindScope timeRange="24h" operator="p95">
        <h2>Application Metrics (Last 24 Hours)</h2>
        <Timeseries metrics={['http.server.duration']} />
        <Timeseries metrics={['http.server.request.count']} />
      </UnblindScope>
    </div>
  )
}

Custom Chart with Low-Level API

Build completely custom visualizations using the raw data:

import { useTimeseries } from '@unblind/react'
import { Line } from 'react-chartjs-2'

export function CustomLineChart() {
  const { data, isLoading } = useTimeseries({
    queries: [{
      metrics: ['system.cpu.utilization'],
      operator: 'avg',
    }],
    timeRange: '1h',
  })

  if (isLoading) return <div>Loading...</div>

  const { series, times } = data

  // Transform data for your charting library
  const chartData = {
    labels: times.map(t => new Date(t).toLocaleTimeString()),
    datasets: series.map(serie => ({
      label: serie.metric,
      data: serie.values,
    }))
  }

  return <Line data={chartData} />
}

Filtering by Attributes

Filter metrics by specific attribute values:

<Timeseries
  metrics={['http.server.duration']}
  attributes={{
    'http.method': ['GET', 'POST'],
    'http.route': ['/api/users'],
    'service.name': ['api']
  }}
  groupBy={['http.method']}
/>

Custom Appearance

Customize loading, error, and empty states:

import { UnblindProvider } from '@unblind/react'

function CustomLoading() {
  return <div className="spinner">Loading...</div>
}

function CustomError() {
  return <div className="error">Failed to load data</div>
}

function CustomEmpty() {
  return <div className="empty">No data available</div>
}

export function App() {
  return (
    <UnblindProvider
      appearance={{
        components: {
          Loading: CustomLoading,
          Error: CustomError,
          Empty: CustomEmpty,
        }
      }}
    >
      <Dashboard />
    </UnblindProvider>
  )
}

Custom Colors

Provide custom colors for chart series:

// Array of colors
<Timeseries
  metrics={['cpu', 'memory', 'disk']}
  appearance={{
    colors: ['#3b82f6', '#ef4444', '#10b981']
  }}
/>

// Function for dynamic colors
<Timeseries
  metrics={['http.server.duration']}
  groupBy={['http.route']}
  appearance={{
    colors: (serie, index) => {
        const colors = ['#0ea5e9', '#8b5cf6', '#ec4899', '#f59e0b']
        return colors[index % colors.length]
    }
  }}
/>

Next Steps

Was this page helpful?