TypeScript / React
Dashboard SDK
Build signed browser-module dashboards that ServiceRadar imports, verifies, and renders with SRQL data, settings, navigation, theme, and map libraries supplied by the host.
@carverauto/serviceradar-dashboard-sdk is the supported surface for
customer-owned dashboards. A dashboard package is built outside ServiceRadar,
published as a signed artifact, imported by an administrator, and rendered
inside the ServiceRadar web host.
Most teams should start with the React helpers. They hide the host lifecycle, keep SRQL updates debounced, decode data frames, and manage Mapbox / deck.gl integration without taking ownership of the ServiceRadar shell.
Build Path
Use this path for a browser-module dashboard:
- Create a package in your dashboard repository.
-
Build a
renderer.jsbrowser module plus a manifest. - Include a SHA256 digest and any operator-required signing metadata.
- Import the manifest into ServiceRadar as a dashboard source.
- Enable a dashboard instance and choose where it appears in the UI.
- Let ServiceRadar load the verified artifact and provide host services.
The runtime contract is intentionally small: you ship the dashboard renderer; ServiceRadar supplies SRQL execution, data frames, settings, theme, navigation, Mapbox, deck.gl, and lifecycle cleanup.
Quick Start
For a new dashboard, scaffold from a template and use the generated npm scripts:
npm create @carverauto/create-dashboard@latest my-dashboard -- --template react-map
cd my-dashboard
npm run dev
npm run validate
npm run buildFor an existing dashboard repository, install from the committed lockfile:
npm ci
npm run dev
Project scripts resolve serviceradar-cli from ./node_modules/.bin, so a
global install is not required. For ad-hoc commands, run npx serviceradar-cli ... from the dashboard repository.
Every React dashboard exports one mountDashboard entrypoint:
import {mountReactDashboard, useFrameRows} from "@carverauto/serviceradar-dashboard-sdk/react"
function SitesDashboard() {
const sites = useFrameRows("sites")
return <div>{sites.length} sites</div>
}
export const mountDashboard = mountReactDashboard(SitesDashboard)The manifest points ServiceRadar at that exported entrypoint:
{
"kind": "browser_module",
"interface_version": "dashboard-browser-module-v1",
"artifact": "renderer.js",
"sha256": "...",
"trust": "trusted",
"entrypoint": "mountDashboard"
}
Use waitForReady when the dashboard has async setup that should complete
before the host marks the renderer as mounted:
export const mountDashboard = mountReactDashboard(SitesDashboard, {waitForReady: true})
The local dev harness prints a URL such as http://127.0.0.1:4177/. Open it
in a browser to exercise the renderer with hot module replacement, sample data
frames, theme switching, Mapbox settings, and the host API activity footer.
CLI Scaffolding
The dashboard CLI is the recommended way to start a new dashboard package. It creates the project shape, wires npm scripts to the SDK harness, and includes sample frames and settings so the dashboard can be exercised before it is imported into ServiceRadar.
npm create @carverauto/create-dashboard@latest my-dashboard -- --template react-map
cd my-dashboard
npm run dev
npm run validate
npm run build
npm create @carverauto/create-dashboard forwards to the canonical CLI command:
npx serviceradar-cli dashboard init my-dashboard --template react-mapChoose the template that matches the first screen you need to build:
| Template | Starting point |
|---|---|
react-map |
Mapbox dashboard using useMapboxMap, with deck.gl helpers available for high-volume layers |
react-table |
Frame-driven inventory table with search, status filtering, and pagination |
react-blank |
Minimum React dashboard that renders rows from one primary data frame |
Generated projects include these scripts:
| Script | What it does |
|---|---|
npm run dev |
Starts the ServiceRadar dashboard harness with Vite HMR and remounts the renderer as source files change |
npm run validate |
Checks dashboard.config.mjs, manifest fields, sample frames, and sample settings without building or calling the network |
npm run build |
Runs validation, bundles dist/renderer.js, writes dist/manifest.json, stamps the renderer SHA256, and copies samples |
The CLI also exposes direct subcommands for CI and release automation:
npx serviceradar-cli doctor
npx serviceradar-cli dashboard manifest
npx serviceradar-cli dashboard publish --instance https://serviceradar.example.com --route my-dashboard
doctor reports Node, npm, CLI, SDK, Vite, config, renderer entry, and auth
state. dashboard publish verifies that the manifest digest still matches the
renderer artifact before uploading.
Installing Published Packages
Dashboard projects should depend on published npm packages:
npm install @carverauto/serviceradar-dashboard-sdk react react-dom
npm install -D @vitejs/plugin-react vite @playwright/test
@carverauto/serviceradar-dashboard-sdk depends on
@carverauto/serviceradar-cli, so serviceradar-cli is available to npm
scripts through ./node_modules/.bin. If you want to pin the CLI explicitly,
add it as a direct dependency:
npm install @carverauto/serviceradar-cli
A typical customer dashboard keeps these scripts in package.json:
{
"scripts": {
"dev": "serviceradar-cli dashboard dev",
"build": "serviceradar-cli dashboard build",
"validate": "serviceradar-cli dashboard validate",
"test:unit": "node --test tests/*.test.mjs"
}
}
Use npm ci for repeatable installs in a checked-out customer repo. If this
command reports invalid versions, remove node_modules and rerun npm ci so
the installed packages match package-lock.json:
npm ls @carverauto/serviceradar-cli @carverauto/serviceradar-dashboard-sdkLocal Harness
npm run dev starts the ServiceRadar dashboard harness with Vite HMR. The
harness loads dashboard.config.mjs, mounts the renderer entry directly, and
supplies the same host API shape that web-ng supplies at runtime.
Common local commands:
npm run dev
npm run dev -- --open
npm run dev -- --port 4191
npm run dev -- --mapbox-token pk...
The right-side harness panel controls host theme, Mapbox token, fixture
selection, and renderer reload. The footer activity log shows host calls such
as mounted com.example.network-map and srql.update in:sites limit:500.
When a dashboard interaction changes the query state, the footer should show
the SRQL update that the host would receive.
Sample frames and settings come from dashboard.config.mjs:
export default defineDashboardConfig({
samples: {
frames: "fixtures/sample-frames.json",
settings: "fixtures/sample-settings.json",
},
})Commit sample fixtures that are required for another developer to clone the repository and reproduce the dashboard locally.
Updating Package Versions
Update the SDK and CLI when ServiceRadar publishes fixes needed by the local harness or renderer:
npm outdated @carverauto/serviceradar-dashboard-sdk @carverauto/serviceradar-cli
npm update @carverauto/serviceradar-dashboard-sdk @carverauto/serviceradar-cliTo move to a specific published version:
npm install @carverauto/serviceradar-dashboard-sdk@0.1.4 @carverauto/serviceradar-cli@0.1.4
Commit package-lock.json whenever resolved package versions change, and
commit package.json when dependency ranges change. After any package update,
run:
npm run validate
npm run test:unit
npm run buildCustomer repositories should validate against published npm packages. If a needed SDK or CLI fix exists in ServiceRadar source but has not been published, publish the upstream package first, then update the customer dashboard repo.
Publishing
npx serviceradar-cli dashboard publish --instance <url> [--route <slug>] [--enable] [--yes] posts the built dist/manifest.json and renderer
artifact to /api/v1/dashboard-packages as a multipart upload. The
ServiceRadar instance must implement the publish API.
Endpoint contract
POST /api/v1/dashboard-packages — upload a manifest + renderer.
The request is multipart/form-data with three parts:
| Part | Required | Type | Cap |
|---|---|---|---|
manifest |
yes |
application/json |
256 KB |
renderer |
yes |
application/javascript, text/javascript, or application/wasm |
Storage.max_upload_bytes (50 MB) |
route |
no |
text slug matching ^[a-z0-9][a-z0-9-]{1,62}$ |
n/a |
The bearer JWT must carry the dashboard.publish scope (minted by
auth login); the user must hold the cli.dashboard.publish RBAC
permission.
Successful response (200 OK):
{
"id": "package-uuid",
"dashboard_id": "com.example.foo",
"version": "0.1.0",
"route_slug": "example-foo",
"status": "staged",
"content_hash": "sha256-of-renderer-bytes",
"result": "written"
}
result is "idempotent_noop" when the same id@version already exists
with a matching content_hash — re-publishes are safe to retry without
churning the persisted bytes.
POST /api/v1/dashboard-packages/:id/enable — flip a package live and
optionally bind/rebind a route slug. Body (JSON, optional):
{"route": "<slug>"}. Requires cli.dashboard.enable.
POST /api/v1/dashboard-packages/:id/disable — take a package out of
service without deleting it. The renderer asset endpoint stops serving
once the package is disabled. Requires cli.dashboard.disable.
Error envelopes
| HTTP |
error |
When |
|---|---|---|
| 400 |
missing_part |
Missing manifest or renderer part. |
| 400 |
invalid_route |
route slug fails the regex. |
| 400 |
invalid_manifest |
Manifest fails server-side schema validation. |
| 401 |
unauthorized |
No bearer / revoked / unknown JWT (handled by ApiAuth). |
| 403 |
insufficient_scope |
Bearer JWT lacks the dashboard.publish scope claim. |
| 403 |
forbidden |
User does not hold the matching RBAC permission. |
| 409 |
slug_in_use |
route already enabled-bound to a different dashboard_id. |
| 409 |
version_already_published |
Same id@version exists with different bytes against an enabled package. |
| 413 |
payload_too_large |
A part exceeded its size cap. part field names which one. |
| 415 |
unsupported_media_type |
A part declared a content type outside the allow-list. |
| 422 |
unprocessable_renderer |
manifest.renderer.sha256 does not match the uploaded bytes. |
| 429 |
rate_limited |
11+ publishes in 60 s on the same JWT. Retry-After header is included. |
The CLI surfaces each envelope with an actionable hint; the raw HTTP
status and error code are also included so they can be parsed by
automation.
Invariants
-
Slug ownership. A
route_slugrow indashboard_instancesbelongs to exactly onedashboard_idwhileenabled = true. Concurrent publishes to a fresh slug serialize through the unique index; the loser receives 409slug_in_use. -
Version overwrite. Same
id@versionre-push with the same bytes is an idempotent no-op. Sameid@versionwith different bytes against an enabled or verified package is rejected. Sameid@versionwith different bytes against a:disabledpackage overwrites and resetsverification_status: "pending". -
Audit logging. Every publish/enable/disable hop emits one
dashboard_packageaudit row with{actor_user_id, jti, action, dashboard_id, version, route_slug, content_hash, result}via the existing audit sink. Audit failures never fail the request.
RBAC permissions
| Permission | Default roles | Surface |
|---|---|---|
cli.dashboard.publish |
admins |
Upload via POST /api/v1/dashboard-packages. |
cli.dashboard.enable |
admins |
Flip a package live + bind a route via POST /api/v1/dashboard-packages/:id/enable. |
cli.dashboard.disable |
admins |
Take a package out of service via POST /api/v1/dashboard-packages/:id/disable. |
Package Shape
A typical repository keeps dashboard code, build output, and test fixtures separate:
my-dashboard/
src/
dashboard.tsx
data.ts
map.ts
public/
sample-frames.json
sample-settings.json
dist/
manifest.json
renderer.js
The SDK is published as @carverauto/serviceradar-dashboard-sdk with these
subpath exports:
| Export | Use it for |
|---|---|
/react |
React mount lifecycle, host hooks, frame hooks, filter hooks |
/map |
Mapbox setup, optional deck.gl setup, and layer factories |
/popup |
React content rendered into Mapbox popups |
/query-state |
Framework-agnostic SRQL query state |
/filtering |
Framework-agnostic indexed local filtering |
/frames, /arrow |
Raw frame and Arrow IPC helpers |
/srql |
SRQL client and query builder helpers |
/config |
dashboard.config.mjs helpers used by the CLI |
Host Contract
ServiceRadar calls your exported mount function with the root element, host record, and bounded API. React dashboards normally consume these through hooks, but the underlying shape is:
export async function mountDashboard(root, host, api) {
const settings = api.settings()
const frame = api.frame("sites")
return {
destroy() {
// Release timers, event listeners, maps, overlays, and React roots.
},
}
}Use the raw API when you need direct control. For ordinary React dashboards, prefer the hooks below because they handle digest caching, cleanup, and stable references for you.
Data Frames
Dashboards receive named frames from ServiceRadar. useFrameRows is the usual
entry point: it decodes JSON or Arrow IPC frames, caches by frame digest, and
optionally projects each row into a stable shape.
import {useFrameRows} from "@carverauto/serviceradar-dashboard-sdk/react"
const SITE_SHAPE = Object.freeze({
id: "site_id",
code: (row) => String(row.site_code || "").toUpperCase(),
region: "region",
latitude: (row) => Number(row.latitude ?? row.lat),
longitude: (row) => Number(row.longitude ?? row.lon),
})
function SiteCount() {
const sites = useFrameRows("sites", {decode: "auto", shape: SITE_SHAPE})
return <span>{sites.length} active sites</span>
}Keep shape objects at module scope. Their identity is part of the projection cache key, so recreating them on every render defeats the cache.
Filtering And Queries
Use local filtering for instant UI response, and SRQL query state for server roundtrips.
useFilterState owns chip groups, search inputs, and debounced fields:
import {useFilterState, useIndexedRows} from "@carverauto/serviceradar-dashboard-sdk/react"
const INDEX_BY = {region: "region", vendor: "vendor"}
function FilteredSites({sites}) {
const filters = useFilterState({
initialState: {regions: [], vendors: [], search: ""},
debounceMs: 300,
debounceFields: ["search"],
})
const indexed = useIndexedRows(sites, {
indexBy: INDEX_BY,
searchText: ["code", "name"],
})
const visible = indexed.applyFilters({
region: filters.state.regions,
vendor: filters.state.vendors,
search: filters.debouncedState.search,
})
return <SiteList sites={visible} />
}
useDashboardQueryState turns dashboard state into SRQL and only calls
api.srql.update when the query fingerprint changes:
import {useDashboardQueryState} from "@carverauto/serviceradar-dashboard-sdk/react"
const queryState = useDashboardQueryState({
initialState: {regions: []},
debounceMs: 300,
buildQuery: (state) =>
state.regions.length
? `in:sites region:(${state.regions.join(",")}) limit:500`
: "in:sites limit:500",
})
queryState.apply({regions: ["AMER"]})Use both together when the page needs immediate local interaction plus a debounced server refresh.
Map Dashboards
ServiceRadar injects Mapbox GL JS, MapboxOverlay, and deck.gl constructors
through api.libraries. Use useMapboxMap when the dashboard only needs the
map lifecycle, DOM markers, or Mapbox sources/layers:
import {useMapboxMap} from "@carverauto/serviceradar-dashboard-sdk/map"
function SitesMap() {
const handle = useMapboxMap({
initialViewState: {center: [-98.5, 39.8], zoom: 3.7},
viewportThrottleMs: 120,
})
return <div ref={handle.containerRef} className="absolute inset-0" />
}
Mapbox GL JS itself is WebGL-backed. This path removes deck.gl/luma from the
dashboard renderer, but it still uses the browser GPU through Mapbox. Use
deck.gl only when you need GPU-backed data layers. useDeckMap composes
useMapboxMap; useDeckLayers owns layer reconciliation:
import {scatter, useDeckLayers, useDeckMap} from "@carverauto/serviceradar-dashboard-sdk/map"
function SitesMap({sites}) {
const handle = useDeckMap({
initialViewState: {center: [-98.5, 39.8], zoom: 3.7},
viewportThrottleMs: 120,
})
const accessors = useMemo(() => ({
getPosition: (site) => [site.longitude, site.latitude],
getRadius: 8,
}), [])
const visualProps = useMemo(() => ({
pickable: true,
radiusUnits: "pixels",
getFillColor: [3, 105, 161, 230],
}), [])
useDeckLayers(handle, {
sites: scatter("sites", {data: sites, accessors, visualProps}),
})
return <div ref={handle.containerRef} className="absolute inset-0" />
}
The performance rule is simple: keep data, accessors, and visualProps
references stable. Inline accessors allocate new functions each render and can
force deck.gl to rebuild GPU buffers.
Available layer helpers: scatter, text, icon, and line. They are thin
wrappers around deck.gl layer specs; use raw layer constructors only when you
need an option the SDK does not wrap.
Popups And Navigation
Use useMapPopup when Mapbox popups need React content. It creates the popup
on first open, re-renders the React subtree on updates, and unmounts the root
when the popup closes.
import {useMapPopup} from "@carverauto/serviceradar-dashboard-sdk/popup"
function SitePopup({handle, focusedSite, onClose}) {
const popup = useMapPopup(handle.map, {closeOnClick: false, offset: 18, onClose})
useEffect(() => {
if (!focusedSite) {
popup.close()
return
}
popup.open({
coordinates: [focusedSite.longitude, focusedSite.latitude],
content: <strong>{focusedSite.code}</strong>,
})
}, [focusedSite, popup])
return null
}For ServiceRadar panels and route changes, use the host navigation hooks:
-
useDashboardNavigation()for dashboard and device navigation. -
useDashboardDetails()for ServiceRadar detail panels. -
useDashboardPopup()for host-managed in-page popups.
Settings And Capabilities
Admins can provide instance settings and capability grants when enabling a dashboard. Read them through hooks instead of hardcoding deployment-specific values:
import {
useDashboardCapability,
useDashboardMapbox,
useDashboardSettings,
useDashboardTheme,
} from "@carverauto/serviceradar-dashboard-sdk/react"
const settings = useDashboardSettings()
const mapbox = useDashboardMapbox()
const theme = useDashboardTheme()
const canReadBasemap = useDashboardCapability("map.basemap.read")Common React hooks:
| Hook | Purpose |
|---|---|
useDashboardHost() / useDashboardApi() |
Raw host record and bounded API |
useDashboardSrql() |
SRQL client with query, update, build, escapeValue, list |
useDashboardSettings() |
Operator-supplied instance settings |
useDashboardTheme() |
"dark" or "light" with host updates |
useDashboardMapbox() |
Mapbox token, styles, and map configuration |
useDashboardLibraries() |
Host-injected map and deck.gl libraries |
useDashboardPreferences() |
User preference read/write helpers |
useDashboardSavedQueries() |
Saved query list and apply helpers |
Declare required capabilities in dashboard.config.mjs so ServiceRadar can
review the package before it is enabled. The CLI preserves these fields when it
writes the published manifest:
import {defineDashboardConfig} from "@carverauto/serviceradar-dashboard-sdk/config"
export default defineDashboardConfig({
manifest: {
id: "com.example.network-map",
name: "Network Map",
version: "0.1.0",
capabilities: ["srql.execute", "map.basemap.read"],
data_frames: [
{
id: "sites",
query: "in:sites limit:500",
encoding: "json_rows",
required: true,
},
],
settings_schema: {
required: ["mapbox"],
},
},
renderer: {entry: "src/main.jsx"},
samples: {
frames: "fixtures/sample-frames.json",
settings: "fixtures/sample-settings.json",
},
})
Use srql.execute for dashboards that ask ServiceRadar to run SRQL. Add
map.basemap.read only when the dashboard needs host-provided Mapbox settings.
Keep capabilities narrow; they are part of the import and enablement review.
Lower-Level APIs
Non-React dashboards can use the framework-agnostic exports directly.
For SRQL:
import {buildSrqlQuery, createSrqlClient} from "@carverauto/serviceradar-dashboard-sdk/srql"
const srql = createSrqlClient(api)
const query = buildSrqlQuery({
entity: "sites",
search: "ORD",
searchField: "site_code",
where: ["down_count:>0"],
limit: 500,
})
srql.update(query)For raw frame helpers:
import {isArrowFrame, requireArrowFrameBytes} from "@carverauto/serviceradar-dashboard-sdk/frames"
const frame = api.frame("sites")
if (isArrowFrame(frame)) {
const bytes = requireArrowFrameBytes(frame)
// Hand bytes to an Arrow decoder or table pipeline.
}WebAssembly Render Models
Some constrained render-model engines ship WebAssembly artifacts instead of
browser modules. The Go helpers at
github.com/carverauto/serviceradar-sdk-dashboard/srdashboard cover the host
ABI:
-
srdashboard.DataFrameEncoding(index)returns the frame encoding. -
srdashboard.DataFrameBytes(index)returns the raw frame payload. -
srdashboard.BuildSRQL(SRQLQuery{...})builds deterministic SRQL. -
srdashboard.EmitRenderModelJSON(model)emits a host render model.
//export sr_dashboard_frames_updated
func framesUpdated() {
if srdashboard.DataFrameEncoding(0) == srdashboard.FrameEncodingArrowIPC {
payload := srdashboard.DataFrameBytes(0)
// Decode payload and emit a render model.
}
}In this model ServiceRadar owns deck.gl, Mapbox, popup behavior, and event wiring. The WebAssembly module emits constrained render models.
Implementation Checklist
-
Export exactly one
mountDashboardentrypoint. - Keep row shapes, filter indexes, deck accessors, and visual props stable.
- Debounce SRQL updates that follow text input.
- Read settings and capabilities from the host.
- Validate against the published SDK/CLI packages that customers will install.
- Release maps, overlays, timers, listeners, and React roots in cleanup paths.
- Test with sample frames before publishing the signed artifact.
See Also
-
Dashboard SDK package:
@carverauto/serviceradar-dashboard-sdk -
Dashboard CLI package:
@carverauto/serviceradar-cli -
Dashboard host interface:
dashboard-browser-module-v1 -
Local harness command:
npm run dev