Building a Dashboard Data Provider
Step-by-step guide for creating a plugin that provides data to the Network Overview dashboard
Overview#
The Network Overview dashboard (/dashboard) gets its data from a provider plugin via the event bus. Any plugin can become a dashboard data provider by implementing resolver functions against the shared GraphQL schema.
This guide walks you through creating a provider from scratch, or starting from the reference mock plugin.
Quick Start (5 minutes)#
The fastest way to get started is to clone the mock provider plugin:
Prerequisites#
Your plugin needs the graphql package (used internally by createDashboardProvider):
Step 1: Implement Resolvers#
Each resolver corresponds to a dashboard widget. Implement only the ones your plugin provides:
| 1 | // frontend/src/provider.ts |
| 2 | import { |
| 3 | createDashboardProvider, |
| 4 | type IEventBus, |
| 5 | type DashboardKPI, |
| 6 | type DashboardProtocol, |
| 7 | } from '@naap/plugin-sdk'; |
| 8 | |
| 9 | export function registerProvider(eventBus: IEventBus, api: IApiClient) { |
| 10 | return createDashboardProvider(eventBus, { |
| 11 | // Each resolver returns typed data matching the GraphQL schema |
| 12 | kpi: async (args) => { |
| 13 | const stats = await api.get('/api/v1/my-service/stats'); |
| 14 | return { |
| 15 | successRate: { value: stats.successRate, delta: stats.successRateDelta }, |
| 16 | orchestratorsOnline: { value: stats.orchCount, delta: stats.orchDelta }, |
| 17 | dailyUsageMins: { value: stats.usageMins, delta: stats.usageDelta }, |
| 18 | dailyStreamCount: { value: stats.streams, delta: stats.streamsDelta }, |
| 19 | }; |
| 20 | }, |
| 21 | |
| 22 | protocol: async () => { |
| 23 | const proto = await api.get('/api/v1/my-service/protocol'); |
| 24 | return { |
| 25 | currentRound: proto.round, |
| 26 | blockProgress: proto.blockProgress, |
| 27 | totalBlocks: proto.totalBlocks, |
| 28 | totalStakedLPT: proto.totalStaked, |
| 29 | }; |
| 30 | }, |
| 31 | |
| 32 | // Omit resolvers you don't support — they return null |
| 33 | }); |
| 34 | } |
Step 2: Register on Mount#
In your plugin's App component, register the provider in a useEffect:
| 1 | // frontend/src/App.tsx |
| 2 | import React, { useEffect, useRef } from 'react'; |
| 3 | import { createPlugin, useShell } from '@naap/plugin-sdk'; |
| 4 | import { registerProvider } from './provider'; |
| 5 | |
| 6 | const ProviderApp: React.FC = () => { |
| 7 | const shell = useShell(); |
| 8 | const cleanupRef = useRef<(() => void) | null>(null); |
| 9 | |
| 10 | useEffect(() => { |
| 11 | const cleanup = registerProvider(shell.eventBus, shell.api!); |
| 12 | cleanupRef.current = cleanup; |
| 13 | return () => { |
| 14 | cleanup(); |
| 15 | cleanupRef.current = null; |
| 16 | }; |
| 17 | }, [shell.eventBus, shell.api]); |
| 18 | |
| 19 | return null; // Headless — no UI |
| 20 | }; |
| 21 | |
| 22 | const plugin = createPlugin({ |
| 23 | name: 'my-network-provider', |
| 24 | version: '1.0.0', |
| 25 | routes: [], |
| 26 | App: ProviderApp, |
| 27 | }); |
| 28 | |
| 29 | export default plugin; |
Step 3: Implement the Job Feed (Optional)#
If your plugin provides live job data, register a job feed handler:
| 1 | import { |
| 2 | DASHBOARD_JOB_FEED_EVENT, |
| 3 | DASHBOARD_JOB_FEED_EMIT_EVENT, |
| 4 | type IEventBus, |
| 5 | type JobFeedSubscribeResponse, |
| 6 | type JobFeedEntry, |
| 7 | } from '@naap/plugin-sdk'; |
| 8 | |
| 9 | export function registerJobFeed(eventBus: IEventBus) { |
| 10 | // Option A: Event bus fallback (local dev) |
| 11 | const unsubscribe = eventBus.handleRequest( |
| 12 | DASHBOARD_JOB_FEED_EVENT, |
| 13 | async (): Promise<JobFeedSubscribeResponse> => ({ |
| 14 | channelName: null, |
| 15 | eventName: 'job', |
| 16 | useEventBusFallback: true, |
| 17 | }) |
| 18 | ); |
| 19 | |
| 20 | // Emit job events (from your backend or simulated) |
| 21 | // eventBus.emit(DASHBOARD_JOB_FEED_EMIT_EVENT, jobEntry); |
| 22 | |
| 23 | return unsubscribe; |
| 24 | |
| 25 | // Option B: Ably channel (production) |
| 26 | // Return { channelName: 'my-jobs-channel', eventName: 'job', useEventBusFallback: false } |
| 27 | // Your backend publishes to the Ably channel |
| 28 | } |
GraphQL Schema Reference#
The full schema is exported as DASHBOARD_SCHEMA from @naap/plugin-sdk:
| 1 | type Query { |
| 2 | kpi(window: String): KPI |
| 3 | protocol: Protocol |
| 4 | fees(days: Int): FeesInfo |
| 5 | pipelines(limit: Int): [PipelineUsage!] |
| 6 | gpuCapacity: GPUCapacity |
| 7 | pricing: [PipelinePricing!] |
| 8 | } |
Types#
| Type | Fields |
|---|---|
KPI | successRate, orchestratorsOnline, dailyUsageMins, dailyStreamCount (all MetricDelta!) |
MetricDelta | value: Float!, delta: Float! |
Protocol | currentRound: Int!, blockProgress: Int!, totalBlocks: Int!, totalStakedLPT: Float! |
FeesInfo | totalEth: Float!, entries: [FeeEntry!]! |
FeeEntry | day: String!, eth: Float! |
PipelineUsage | name: String!, mins: Int!, color: String |
GPUCapacity | totalGPUs: Int!, availableCapacity: Float! |
PipelinePricing | pipeline: String!, unit: String!, price: Float!, outputPerDollar: String! |
Testing Your Provider#
Write tests using a mock event bus:
| 1 | import { describe, it, expect, vi } from 'vitest'; |
| 2 | import { DASHBOARD_QUERY_EVENT } from '@naap/plugin-sdk'; |
| 3 | import { registerProvider } from './provider'; |
| 4 | |
| 5 | function createMockEventBus() { |
| 6 | const handlers = new Map(); |
| 7 | return { |
| 8 | // ...same pattern as SDK contract tests |
| 9 | handleRequest: vi.fn((event, handler) => { |
| 10 | handlers.set(event, handler); |
| 11 | return () => handlers.delete(event); |
| 12 | }), |
| 13 | _invoke: async (event, data) => handlers.get(event)?.(data), |
| 14 | }; |
| 15 | } |
| 16 | |
| 17 | describe('My provider', () => { |
| 18 | it('responds with KPI data', async () => { |
| 19 | const bus = createMockEventBus(); |
| 20 | registerProvider(bus as any, mockApi); |
| 21 | |
| 22 | const response = await bus._invoke(DASHBOARD_QUERY_EVENT, { |
| 23 | query: '{ kpi { successRate { value } } }', |
| 24 | }); |
| 25 | |
| 26 | expect(response.data.kpi.successRate.value).toBeGreaterThan(0); |
| 27 | }); |
| 28 | }); |
Adding a New Widget#
To add a custom widget to the dashboard:
- Schema — Add a new root field and type to
DASHBOARD_SCHEMAinpackages/plugin-sdk/src/contracts/dashboard.ts - TypeScript — Add the corresponding TypeScript interface
- Resolver — Implement the resolver in your provider plugin
- Query — Add the field to the
NETWORK_OVERVIEW_QUERYin the dashboard page - Render — Add the widget component to the dashboard page
No changes needed to hooks, event bus, or other existing widgets.
Troubleshooting#
"No dashboard data provider installed"#
The dashboard shows this when no plugin has registered a handler for dashboard:query. Ensure your provider plugin is installed and mounts successfully.
Timeout errors#
The dashboard waits 8 seconds for a response. If your API calls take longer, consider:
- Caching data in the plugin
- Returning partial results (implement fast resolvers first)
- Increasing the timeout in
useDashboardQueryoptions
Schema mismatch#
If your resolver returns data that doesn't match the schema types, GraphQL will return null for those fields with an error message. Check the browser console for partial error warnings.