Skip to content

Build a custom Stackyard widget

A widget is one folder in ui/widgets/. This page builds one from the template in the repository.

Small
Medium
The finished widget, running with sample data.

A widget has three files. widget.json defines its settings. data.js runs on the server and calls your service. index.html runs on the dashboard and draws the result.

cp -r docs/widget-template ui/widgets/mywidget
{
"name": "mywidget",
"label": "My Widget",
"sizes": ["small", "medium"],
"fields": [
{ "key": "url", "type": "text", "label": "Service URL", "placeholder": "http://host:port" },
{ "key": "apiKey", "type": "secret", "label": "API key", "optional": true },
{ "key": "showTotal", "type": "toggle", "label": "Show total", "default": true, "cosmetic": true }
]
}

name must match the folder. Each field becomes a row in the widget’s settings. A secret never returns to the browser. cosmetic marks a field that can change without asking for the secret again. A toggle that was never saved is absent from ctx.config, so test for false. See Manifest.

module.exports = async function (ctx) {
const { url, apiKey, showTotal } = ctx.config;
if (!url) ctx.fail('Enter the service URL.', { kind: ctx.KIND.INVALID });
const r = await ctx.fetchJSON(`${ctx.normalizeBase(url)}/api/items`, {
headers: apiKey ? { 'X-Api-Key': apiKey } : {},
timeout: 8000,
});
if (r.status === 401 || r.status === 403) ctx.fail('Auth failed (check API key)', { kind: ctx.KIND.AUTH });
if (r.status >= 400) ctx.fail('HTTP ' + r.status);
if (!r.data || !Array.isArray(r.data.items)) ctx.fail('Reply has no items');
return {
items: r.data.items.slice(0, 10).map(i => ({ name: i.name })),
total: showTotal === false ? null : (r.data.total ?? r.data.items.length),
};
};

The return value is served at /api/widget-data/<id>. Report every failure through ctx.fail, as the template does, so the widget shows its failure state. See Data.

<script type="module">
import { formatNumber, poll } from '/js/widget-toolbox.js?v=1';
const root = document.getElementById('root');
poll({
render: data => {
const names = data.items.slice(0, 3).map(i => i.name).join(', ');
root.textContent = data.total == null ? names : `${formatNumber(data.total)} items: ${names}`;
},
isEmpty: data => !data.items?.length,
interval: 30000,
});
</script>

poll() fetches the data and handles loading, empty, stale and failed states. The template adds the markup and styles. See Widget page.

Start the dev instance if it is not running. A new folder in ui/widgets/ restarts it. Then in Settings, Dashboard, press Add and pick My Widget. If it is missing, the container log says why.

Before a pull request, go through the checklist.