@devicechain/widgets

中文文档
1.4.0发布于 2026/11/12Apache-2.0TypeScriptES Module零依赖构建来源已验证近 30 天下载 4299

DeviceChain's built-in dashboard widgets across six channels — telemetry (time-series chart + gauge over Apache ECharts, latest card, table, and lazily loaded level, indicator and radial widgets: linear and level gauges, battery, status light, signal bars

原始使用文档
文档目录8 节
  1. Render a board
  2. The widgets
  3. Rendering a map: one required piece of host wiring
  4. Basemap tiles
  5. Theming
  6. Upgrading from 0.21
  7. Compatibility
  8. License

@devicechain/widgets

React dashboard widgets for DeviceChain, plus the renderer that lays a board out. Thirty-two widgets — charts, gauges, tables, a fleet table, alarm views, command controls, an entity selector and a map — all bound to live data through @devicechain/dashboards.

Themed entirely with CSS custom properties. No Tailwind, no global styles, no opinions about your app's shell — these are meant to be embedded.

npm install @devicechain/widgets @devicechain/dashboards @devicechain/client graphql react react-dom

Render a board

import { DashboardRenderer } from '@devicechain/widgets';
import { DashboardHub, parseDashboardDefinition } from '@devicechain/dashboards';

const hub = new DashboardHub({ resolver, authorities: user.scopes });
const definition = parseDashboardDefinition(json);

<DashboardRenderer definition={definition} hub={hub} actions={hub} />;

Omit actions for a strictly read-only mount and the acting widgets — alarm acknowledge, command send — render without their controls. That is the whole opt-in: a viewer that never passes actions cannot write, regardless of what the board contains.

A templated board takes its slot bindings on the renderer, not the hub: <DashboardRenderer bindings={bindings} … />. Keep one hub for the board (useDashboardHub(resolver, authorities) builds and disposes it); a new bindings map re-subscribes only the widgets whose slot moved.

Several built-ins load their component code on first mount, so a board without them never downloads it. Until that code arrives the widget's box is empty. To draw the whole board on its first frame, await preloadWidgets with the board's widget types before you render it:

import { createBuiltinWidgetRegistry, preloadWidgets } from '@devicechain/widgets';

const registry = createBuiltinWidgetRegistry();
await preloadWidgets(registry, definition.widgets.map((w) => w.type));

It never rejects: a chunk that fails to load still fails in the widget itself, where its error boundary shows it.

Or drive a single widget yourself with ConnectedWidget; it resolves the widget's component, channel and binding policy through a registry (the built-ins by default, or the one you pass as registry; see createBuiltinWidgetRegistry and builtinWidgetDefinitions, and @devicechain/widget-sdk to add your own). A widget bound to a slot needs its datasource resolved first (resolveWidgets or resolveWidgetDatasource).

The widgets

channelwidgets
measurementtimeseries-chart, latest-card, gauge, linear-gauge, level-gauge, battery, status-light, signal-bars, compass, progress-ring, table, bar-chart, pie-chart, fleet-table, fleet-count, device-status, online-doughnut, history-table, status-grid, state-timeline, scada-symbol
aggregatestat-card
none (static)label, image
alarmalarm-table, alarm-count, alarm-severity
controlcommand-button, command-slider, command-toggle
selectionentity-selector
locationmap

fleet-table, fleet-count, and bar-chart in its one-bar-per-device mode, read a device group member by member. The other measurement widgets, and stat-card, show one device and refuse a group. stat-card shows one figure (average, minimum, maximum, sum, count or latest) over the board's time range.

device-status and online-doughnut read the devices' connectivity (online or offline, and when each was last active) from device-state, not their readings. They ride the measurement channel's member feed (data.members[].presence), so the channel set is unchanged. device-status shows one device; online-doughnut takes a group.

history-table lists one device's stored history as rows, one per time bucket over the board's span, each cell the bucket's average (the table says "avg per 5m"). It reads the store's buckets (binding.history), not the live feed, and refuses a group.

A frame offers a data download from its title bar when its widget opts in: timeseries-chart, fleet-table and history-table hand it a table (CSV), and the charts drawn on a canvas (timeseries-chart, gauge, compass) a PNG. The file holds what the widget shows, for the span and bucket on screen, and its name says so. A widget of your own opts in with the optional exportRows prop of WidgetFrame (see the widget SDK).

The time-series chart, the dial gauge and the compass are drawn with Apache ECharts; the other widgets are plain DOM and SVG. The map is MapLibre GL, loaded lazily so a board with no map on it downloads none of it.

WIDGET_OPTIONS is the typed schema for every widget's options bag — validateWidgetOptions reports exactly where a stored bag disagrees with what the renderer expects, which is what lets an authoring UI show a problem instead of rendering something wrong.

Rendering a map: one required piece of host wiring

MapLibre parses tiles in a web worker, and it derives that worker's URL at runtime from its own module URL — a computed string no bundler can trace. So no bundler emits the file, the browser 404s it, new Worker() does not throw, and the map renders as an empty box with nothing in the console. It is a famously quiet failure, and it is not something this package can paper over: only your bundler can emit that file.

So the worker URL comes from you, through a provider. On Vite that is one import:

import { MapRuntimeProvider } from '@devicechain/widgets';
import { viteMapRuntime } from '@devicechain/widgets/vite';

<MapRuntimeProvider runtime={viteMapRuntime}>
  <DashboardRenderer definition={definition} hub={hub} />
</MapRuntimeProvider>;

On any other bundler you supply the URL yourself. There is one requirement, and it is the whole of the difficulty: MapLibre loads that URL as a module worker, so whatever you serve there must be a module with no unresolved imports left in it. maplibre-gl/dist/maplibre-gl-worker.mjs is not one on its own — its first line imports a sibling, maplibre-gl-shared.mjs.

So do not point at a lone copy of the worker file, and in particular do not reach for new URL('maplibre-gl/dist/maplibre-gl-worker.mjs', import.meta.url). It looks right and it is not: webpack copies the file as an asset, the sibling is never emitted, and the worker dies on its own first line — with a 200 on the worker URL, a canvas on screen, markers placed, the build exited 0 and nothing in the console. The map just has no map in it.

Two recipes that work. On webpack, give the worker its own entry, so webpack bundles its imports into it:

entry: {
  main: './src/index.tsx',
  'maplibre-worker': 'maplibre-gl/dist/maplibre-gl-worker.mjs',
},
output: {
  filename: (data) =>
    data.chunk.name === 'maplibre-worker' ? 'maplibre-worker.js' : '[name].[contenthash].js',
},

…and point at the filename you chose:

const runtime = { workerUrl: '/maplibre-worker.js' };

Or, on any bundler at all, copy the two files yourself:

// BOTH, under their own names, into one served directory:
//   maplibre-gl/dist/maplibre-gl-worker.mjs  ->  public/vendor/
//   maplibre-gl/dist/maplibre-gl-shared.mjs  ->  public/vendor/
const runtime = {
  workerUrl: '/vendor/maplibre-gl-worker.mjs',
  loadStyles: () => import('maplibre-gl/dist/maplibre-gl.css'),
};

loadStyles is optional — import MapLibre's stylesheet yourself if you would rather have it eagerly. Supplying it here keeps the 83 KB (10.7 KB gzipped) on the map's lazy chunk, so a viewer who opens no map downloads none of it.

A map widget with no provider above it renders a visible notice, not a blank canvas. That is deliberate: an unwired host should be able to see what is missing.

maplibre-gl is a peer dependency, so your app and this package share exactly one copy. A worker built from one version driving a main thread on another is the same blank map by a different route.

Basemap tiles

TenantBasemapProvider supplies the tile source. It is genuinely optional — with no provider the map falls back to a plain view — and the resolution cascade lives in @devicechain/client so every DeviceChain surface resolves an override over the tenant default identically.

Theming

Every colour, radius and font comes from a CSS custom property, so a host restyles widgets by setting variables on any ancestor. The DeviceChain values ship as @devicechain/brand; nothing here requires them.

Upgrading from 0.21

  • severityColor(severity) is now severityColor(colors, severity); the colours come from useSeverityColors() in @devicechain/widget-sdk.
  • The timeseries-chart option window (a sample count) is replaced by windowSeconds (a time span), and the chart's options version moves from 1 to 2. A stored board converts when it is parsed: the sample count is dropped and the chart follows the board's time range.
  • Thirteen widgets are new, so WidgetChannel and the widget id unions gained members; an exhaustive switch over them needs new cases.

See If you build against the npm packages in the release's upgrade guide.

Compatibility

React 19, ESM only, built for bundler resolution (Vite, webpack, Rollup, esbuild). The emitted declarations use extensionless specifiers, which resolve under TypeScript's bundler module resolution and not under node16/nodenext.

License

Apache-2.0. See LICENSE.

版本与发布记录

@devicechain/widgets 共发布 48 个版本,首次发布于 2026/09/02;过去 12 个月发布了 48 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.4.0latest2026/11/12今天
1.4.0-rc.2next2026/11/11今天
1.4.0-rc.12026/11/111 天前
1.3.02026/11/092 天前
1.3.0-rc.12026/11/092 天前
1.2.02026/11/083 天前

关于 @devicechain/widgets 的常见问题

@devicechain/widgets 是什么?

DeviceChain's built-in dashboard widgets across six channels — telemetry (time-series chart + gauge over Apache ECharts, latest card, table, and lazily loaded level, indicator and radial widgets: linear and level gauges, battery, status light, signal bars

如何安装 @devicechain/widgets?

在项目目录运行 npm install @devicechain/widgets,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19.3.0)、echarts(^6.0.0)、react-dom(^19.3.0)、maplibre-gl(^6.13.0)、@devicechain/client(1.4.0)、@devicechain/dashboards(1.4.0)、@devicechain/widget-sdk(1.4.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@devicechain/widgets 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@devicechain/widgets 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@devicechain/widgets 可以商用吗?

@devicechain/widgets 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@devicechain/widgets 的下载量有多少?

根据 npm 官方下载统计,@devicechain/widgets 在 2026/10/12 – 2026/11/10 这 30 天内被下载 4,299 次,日均约 143 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@devicechain/widgets 还在维护吗?

最新版本 1.4.0 发布于 2026/11/12(今天);至今共发布 48 个版本,其中过去 12 个月发布了 48 个;首次发布于 2026/09/02。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。