Dashboard Component Development
This page covers how to build a NeoMind dashboard custom component from scratch โ from ZIP package structure to IIFE bundle.js, through installation and debugging. By the end you can write your own visualization components.
Pure frontend โ no Rust backend code needed. Components use the IIFE JavaScript format, executed directly in the browser.
Architecture Overviewโ
Your Component (ZIP)
โโโ manifest.json โ Metadata + config schema
โโโ bundle.js โ IIFE React component
Installation Flow:
ZIP โ API upload โ data/frontend-components/{id}/
โ manifest.json + bundle.js on disk
Rendering Flow:
Dashboard โ ComponentRegistry โ loads bundle.js via <script>
โ IIFE assigns to window[global_name]
โ ComponentRenderer calls the function with props
Key characteristics:
- IIFE format โ no build tools required, runs directly in the browser
- React runtime provided โ uses
window.Reactfrom the dashboard shell, don't bundle React - CSS variable theming โ automatic light/dark mode support
- ZIP packaging โ simple
manifest.json+bundle.jsstructure
Quick Startโ
1. Scaffoldโ
neomind widget create "Temperature Gauge" --widget-type gauge
This creates a temperature-gauge/ directory with template files.
2. Edit manifest.jsonโ
{
"id": "temperature-gauge",
"name": { "en": "Temperature Gauge", "zh": "ๆธฉๅบฆ่กจ" },
"description": { "en": "Displays temperature with min/max range" },
"icon": "thermometer",
"category": "indicators",
"global_name": "NeoMindTemperatureGauge",
"export_name": "default",
"version": "1.0.0",
"size_constraints": {
"min_w": 2, "min_h": 2,
"default_w": 3, "default_h": 3,
"max_w": 6, "max_h": 6
},
"has_data_source": true,
"max_data_sources": 1,
"has_display_config": true,
"config_schema": {
"display": {
"type": "object",
"properties": {
"unit": { "type": "string", "description": "Temperature unit (ยฐC, ยฐF)" },
"minValue": { "type": "number", "description": "Minimum value on gauge" },
"maxValue": { "type": "number", "description": "Maximum value on gauge" }
}
},
"config": { "type": "object", "properties": {} }
},
"default_config": {
"display": { "unit": "ยฐC", "minValue": -20, "maxValue": 50 }
}
}
3. Edit bundle.jsโ
(function(global) {
'use strict';
var React = global.React;
function TemperatureGauge(props) {
var value = props.dataSource && props.dataSource[0]
? props.dataSource[0].value : null;
var display = props.display || {};
var unit = display.unit || 'ยฐC';
var min = display.minValue !== undefined ? display.minValue : -20;
var max = display.maxValue !== undefined ? display.maxValue : 50;
var pct = value !== null
? Math.max(0, Math.min(100, (value - min) / (max - min) * 100))
: 0;
return React.createElement('div', {
style: { width: '100%', height: '100%', display: 'flex',
flexDirection: 'column', alignItems: 'center',
justifyContent: 'center', gap: '0.5rem' }
},
React.createElement('div', {
style: { fontSize: '2.5rem', fontWeight: 'bold',
color: 'var(--color-text-primary)' }
}, value !== null ? value.toFixed(1) + unit : '--'),
React.createElement('div', {
style: { width: '80%', height: '6px', borderRadius: '3px',
background: 'var(--color-border)' }
},
React.createElement('div', {
style: { width: pct + '%', height: '100%', borderRadius: '3px',
background: 'var(--color-success)',
transition: 'width 0.3s ease' }
})
)
);
}
global['NeoMindTemperatureGauge'] = TemperatureGauge;
})(window);
4. Package and Installโ
cd temperature-gauge
zip -r ../temperature-gauge.zip manifest.json bundle.js
neomind widget install ../temperature-gauge.zip
5. Verifyโ
neomind widget list # Should show temperature-gauge
neomind widget get temperature-gauge # Check full manifest
manifest.json Complete Referenceโ
| Field | Type | Required | Description |
|---|---|---|---|
id | string | YES | Unique identifier. Lowercase, hyphens only. Cannot match built-in widget IDs |
name | object/string | YES | Display name. Supports i18n: {"en": "Name", "zh": "ๅ็งฐ"} |
description | object/string | YES | Widget description. Supports i18n |
icon | string | NO | Lucide icon name (default: "Box") |
category | string | YES | One of: indicators, charts, controls, display, spatial, business, custom |
global_name | string | YES | JS global variable name. Convention: NeoMind{PascalCaseId} |
export_name | string | NO | Export method (default: "default") |
version | string | NO | Semantic version (default: "1.0.0") |
author | string | NO | Author name |
size_constraints | object | YES | Grid size limits |
has_data_source | boolean | YES | Whether widget accepts data source bindings |
max_data_sources | number | NO | Maximum data sources (0 = none, omit = unlimited) |
has_display_config | boolean | NO | Whether widget has display configuration |
has_actions | boolean | NO | Whether widget sends commands (e.g., toggle) |
config_schema | object | NO | JSON Schema for display and config fields |
default_config | object | NO | Default configuration values |
Built-in Widget IDs (reserved, cannot be used)โ
value-card, led-indicator, sparkline, progress-bar, line-chart, area-chart, bar-chart, pie-chart, radar-chart, toggle-switch, markdown-display, image-display, image-history, web-display, map-display, video-display, custom-layer, agent-monitor-widget, ai-analyst
size_constraintsโ
The dashboard uses a 12-column grid. Specify min/default/max width and height in grid units:
{
"min_w": 2, "min_h": 2,
"default_w": 4, "default_h": 3,
"max_w": 12, "max_h": 8
}
config_schemaโ
Describes the fields your widget accepts:
displayโ visual configuration set by users in the dashboard editor (unit, color, etc.)configโ internal configuration (content for markdown, URL for web display, etc.)
bundle.js IIFE Formatโ
Rulesโ
- IIFE only โ no
import,require, or ES modules React.createElementonly โ JSX is not available- Use
global.Reactโ React is provided by the dashboard shell - Root element fills container โ
width: '100%', height: '100%' - CSS variables for colors โ use
var(--color-*)tokens - Match
global_nameโ the global assignment must match manifest - Keep small โ target under 50KB
Skeleton Templateโ
(function(global) {
'use strict';
var React = global.React;
function MyWidget(props) {
// Your component implementation
return React.createElement('div', {
style: { width: '100%', height: '100%' }
}, 'Hello');
}
// MUST match global_name in manifest.json
global['NeoMindMyWidget'] = MyWidget;
})(window);
Component Props APIโ
interface WidgetProps {
config: Record<string, any>; // Internal config from manifest config_schema
display: Record<string, any>; // Display config from manifest config_schema
dataSource: Array<{ // Data source values
value: number | string; // Current value
timestamp: number; // Unix timestamp (ms)
label?: string; // Data source label
values?: Array<{ // Time-series (for charts)
value: number;
timestamp: number;
}>;
}>;
id: string; // Component instance ID
title: string; // Widget title
type: string; // Widget type
actions?: { // Command actions (if has_actions: true)
sendCommand: (cmd: string, payload?: any) => void;
};
}
CSS Variable Themingโ
Never hardcode colors. Use these design tokens:
| Variable | Usage |
|---|---|
var(--color-text-primary) | Primary text |
var(--color-text-secondary) | Secondary text |
var(--color-text-muted) | Muted/hint text |
var(--color-bg-primary) | Main background |
var(--color-bg-secondary) | Card background |
var(--color-border) | Borders |
var(--color-success) | Positive/success |
var(--color-error) | Error/danger |
var(--color-warning) | Warning |
var(--color-info) | Information |
var(--color-accent) | Accent/highlight |
Data Source Bindingโ
When has_data_source: true, users bind metrics to your widget.
Single Value (Indicators)โ
var currentTemp = props.dataSource[0].value;
Time-Series (Charts)โ
var history = props.dataSource[0].values || [];
history.forEach(function(point) {
// point.value, point.timestamp
});
Multi-source (Charts)โ
When max_data_sources > 1, dataSource is an array where each element is a separate series:
props.dataSource.forEach(function(ds, i) {
var label = ds.label || 'Series ' + (i + 1);
var points = ds.values || [];
// render each series...
});
DataSourceId format:
device:{device_id}:{metric_name}orextension:{ext_id}:{metric_name}. The dashboard editor's data source picker auto-lists all available metrics.
Installation Methodsโ
Method 1: Local ZIPโ
cd my-widget && zip -r ../my-widget.zip manifest.json bundle.js
neomind widget install ../my-widget.zip
Method 2: Web UIโ
In NeoMind's Extensions page, click Install Widget and upload the ZIP file.
Method 3: Uninstallโ
neomind widget uninstall my-widget
Using Components in Dashboardsโ
# Check the component's config_schema first
neomind widget get my-widget
# Add to a dashboard
neomind dashboard update <DASHBOARD_ID> --components '[{
"id": "c1",
"type": "my-widget",
"title": "My Widget",
"position": {"x": 0, "y": 0, "w": 4, "h": 3},
"data_source": {
"type": "device",
"sourceId": "sensor-01",
"property": "temperature"
},
"display": {"unit": "ยฐC"},
"config": {}
}]'
Complete Example: Line Chart Componentโ
Here is a component that draws a simple SVG line chart from time-series data:
(function(global) {
'use strict';
var React = global.React;
function SimpleLineChart(props) {
var ds = props.dataSource && props.dataSource[0];
var points = (ds && ds.values) || [];
var display = props.display || {};
var strokeColor = display.color || 'var(--color-accent)';
if (points.length < 2) {
return React.createElement('div', {
style: { width: '100%', height: '100%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--color-text-muted)' }
}, 'Waiting for data...');
}
var w = 300, h = 100, pad = 10;
var values = points.map(function(p) { return p.value; });
var minV = Math.min.apply(null, values);
var maxV = Math.max.apply(null, values);
var range = maxV - minV || 1;
var stepX = (w - pad * 2) / (points.length - 1);
var pathData = points.map(function(p, i) {
var x = pad + i * stepX;
var y = h - pad - ((p.value - minV) / range) * (h - pad * 2);
return (i === 0 ? 'M' : 'L') + x + ',' + y;
}).join(' ');
return React.createElement('svg', {
width: '100%', height: '100%', viewBox: '0 0 ' + w + ' ' + h,
preserveAspectRatio: 'none'
},
React.createElement('path', {
d: pathData, fill: 'none',
stroke: strokeColor, strokeWidth: 2
})
);
}
global['NeoMindSimpleLineChart'] = SimpleLineChart;
})(window);
Corresponding manifest.json:
{
"id": "simple-line-chart",
"name": { "en": "Simple Line Chart", "zh": "็ฎๅๆ็บฟๅพ" },
"description": { "en": "A minimal SVG line chart" },
"category": "charts",
"global_name": "NeoMindSimpleLineChart",
"size_constraints": {
"min_w": 3, "min_h": 2,
"default_w": 6, "default_h": 4
},
"has_data_source": true,
"max_data_sources": 1,
"has_display_config": true,
"config_schema": {
"display": {
"type": "object",
"properties": {
"color": { "type": "string", "description": "Line color (CSS variable or hex)" }
}
}
}
}
Debugging Tipsโ
- Test in the browser first: open the browser Console, directly define
window.NeoMindMyWidget = function(props) { ... }, then add the component type in the dashboard to test - console.log debugging: add
console.log(props)in the component body to see the actual data structure received - Check global assignment: verify
window.NeoMindMyWidgetis a function after the component loads - ZIP structure:
manifest.jsonandbundle.jsmust be at the ZIP root level, not nested in a subfolder
Troubleshootingโ
| Problem | Cause | Solution |
|---|---|---|
| Widget not in library | IIFE didn't assign to global | Verify global['{global_name}'] = Component matches manifest |
| Renders blank | Root not filling container | Add width: '100%', height: '100%' to outer div |
| "Reserved ID" error | ID matches built-in | Check neomind widget list, choose different ID |
| Data not showing | Wrong data source field | Verify with neomind device get <ID> |
| Colors wrong | Hardcoded CSS | Use var(--color-*) variables |
| Install fails | Invalid ZIP structure | ZIP must have manifest.json + bundle.js at root |
Next Stepsโ
- Component data sources from devices โ Device Type Development
- Component data sources from extensions โ Extension Development
- Dashboard API โ REST API โ Dashboards
Last updated: 2026-06-15