Skip to main content

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.React from the dashboard shell, don't bundle React
  • CSS variable theming โ€” automatic light/dark mode support
  • ZIP packaging โ€” simple manifest.json + bundle.js structure

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โ€‹

FieldTypeRequiredDescription
idstringYESUnique identifier. Lowercase, hyphens only. Cannot match built-in widget IDs
nameobject/stringYESDisplay name. Supports i18n: {"en": "Name", "zh": "ๅ็งฐ"}
descriptionobject/stringYESWidget description. Supports i18n
iconstringNOLucide icon name (default: "Box")
categorystringYESOne of: indicators, charts, controls, display, spatial, business, custom
global_namestringYESJS global variable name. Convention: NeoMind{PascalCaseId}
export_namestringNOExport method (default: "default")
versionstringNOSemantic version (default: "1.0.0")
authorstringNOAuthor name
size_constraintsobjectYESGrid size limits
has_data_sourcebooleanYESWhether widget accepts data source bindings
max_data_sourcesnumberNOMaximum data sources (0 = none, omit = unlimited)
has_display_configbooleanNOWhether widget has display configuration
has_actionsbooleanNOWhether widget sends commands (e.g., toggle)
config_schemaobjectNOJSON Schema for display and config fields
default_configobjectNODefault 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โ€‹

  1. IIFE only โ€” no import, require, or ES modules
  2. React.createElement only โ€” JSX is not available
  3. Use global.React โ€” React is provided by the dashboard shell
  4. Root element fills container โ€” width: '100%', height: '100%'
  5. CSS variables for colors โ€” use var(--color-*) tokens
  6. Match global_name โ€” the global assignment must match manifest
  7. 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:

VariableUsage
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} or extension:{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โ€‹

  1. 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
  2. console.log debugging: add console.log(props) in the component body to see the actual data structure received
  3. Check global assignment: verify window.NeoMindMyWidget is a function after the component loads
  4. ZIP structure: manifest.json and bundle.js must be at the ZIP root level, not nested in a subfolder

Troubleshootingโ€‹

ProblemCauseSolution
Widget not in libraryIIFE didn't assign to globalVerify global['{global_name}'] = Component matches manifest
Renders blankRoot not filling containerAdd width: '100%', height: '100%' to outer div
"Reserved ID" errorID matches built-inCheck neomind widget list, choose different ID
Data not showingWrong data source fieldVerify with neomind device get <ID>
Colors wrongHardcoded CSSUse var(--color-*) variables
Install failsInvalid ZIP structureZIP must have manifest.json + bundle.js at root

Next Stepsโ€‹


Last updated: 2026-06-15