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:
- Extract the tenant ID from your auth system
- Add your Unblind API key
- 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
- Next.js Integration - Use with Next.js for simpler setup
- API Reference - Explore available endpoints
- Metrics Guide - Learn about metric types and operators