Saltar al contenido principal

Escribir un widget personalizado

Un widget personalizado es un componente de React más un pequeño fragmento de datos simples que lo describe. Escribes ambos con @devicechain/widget-sdk, los distribuyes en un paquete de npm y registras el paquete en la aplicación que muestra los paneles. Los widgets integrados se declaran con exactamente la misma interfaz, así que un widget personalizado obtiene el mismo enlace de datos, la misma validación de opciones y el mismo manejo de errores.

Tu widget recibe datos ya resueltos. No autentica, no resuelve fuentes de datos, no hace sondeos ni abre una conexión de telemetría. El anfitrión hace todo eso y entrega a tu componente una instantánea.

Un paquete completo que puedes copiar está en el repositorio, en examples/widget-tank-level. Se compila fuera del monorepo contra tarballs empaquetados del SDK.

Instala el SDK​

npm install @devicechain/widget-sdk @devicechain/dashboards react

En tu paquete de widgets, declara @devicechain/widget-sdk, @devicechain/dashboards y react como dependencias peer, nunca como dependencias. El SDK define los contextos de React que lee un widget (selección, zona horaria, raíz del tema), y un contexto se identifica por el objeto que lo creó. Por eso el anfitrión y cada paquete de widgets deben resolver una sola copia del SDK.

La entrada raíz del SDK no contiene código de gráficos ni de mapas. Un widget que solo dibuja DOM importa desde la raíz y no añade al empaquetado del anfitrión ni Apache ECharts, ni MapLibre, ni los widgets integrados. Las utilidades de gráficos (EChart, buildLineOption, buildGaugeOption, useChartTheme) están en @devicechain/widget-sdk/echarts, y echarts es una dependencia peer opcional que solo necesita un paquete que importe esa entrada.

Declara un widget​

import { WidgetFrame, defineWidget, formatValue, pickSample } from '@devicechain/widget-sdk';

export const tankLevelWidget = defineWidget({
descriptor: {
id: 'acme/tank-level',
version: '1.0.0',
sdkApiVersion: 1,
optionsVersion: 1,
display: { title: 'Tank level', category: 'Telemetry' },
channel: 'measurement',
datasource: { mode: 'required', selectors: ['device', 'slot'] },
binding: { window: { option: 'window', min: 1, default: 1 } },
options: {
unit: { kind: 'string', doc: 'Unit shown beside the level.', label: 'Unit' },
window: { kind: 'number', integer: true, min: 1, doc: 'Samples kept.', label: 'Samples kept' },
},
defaults: { unit: '%', window: 1 },
interactions: {},
},
implementation: {
kind: 'trusted-react',
component: ({ data }) => (
<WidgetFrame title="Tank level">{formatValue(pickSample(data.latest, undefined)?.value, 1)}</WidgetFrame>
),
},
});

defineWidget infiere el canal y el esquema de opciones a partir del literal. Un componente cuyos datos no coinciden con el canal, valores por defecto que no coinciden con el esquema, un enlace que nombra una opción inexistente o una interacción que el canal no puede transportar son errores de compilación. El registro vuelve a comprobar las mismas reglas cuando el anfitrión lo construye, para quienes eluden el compilador.

El descriptor​

El descriptor son datos simples serializables como JSON. No contiene componentes, callbacks, URL de importación ni tokens, así que el analizador de definiciones y los validadores pueden leerlo sin cargar ni confiar en tu código.

CampoSignificado
idUn id con espacio de nombres, scope/name, como acme/tank-level, en letras minúsculas, dígitos y guiones. Los ids sin barra están reservados para los widgets integrados. En esta versión la propiedad del espacio de nombres es una convención entre operadores; no demuestra quién publicó un paquete.
versionLa versión de este widget, como MAJOR.MINOR.PATCH. Es procedencia. Consulta versiones y compatibilidad de widgets.
sdkApiVersionLa generación del contrato del SDK para la que se escribió el widget. Actualmente 1.
optionsVersionLa forma de las opciones almacenadas. Increméntala solo cuando las opciones almacenadas dejen de significar lo mismo.
displaytitle y, opcionalmente, description y category, para el menú Añadir de un anfitrión. titleId y categoryId son ids de mensaje opcionales, descritos en etiquetas y textos de ayuda.
channelQué datos consume el widget. Ver abajo.
datasource{ mode: 'none' }, o mode: 'optional' o 'required' con los selectors que un autor puede elegir: device, anchor o slot. Un widget del canal control apunta a un único dispositivo, así que solo ofrece device y slot.
bindingCómo se suscribe el anfitrión, escrito como constantes y referencias a tus propias claves de opción.
optionsEl esquema de opciones.
constraintsReglas opcionales que abarcan más de una opción, como greater-than.
defaultsLas opciones iniciales de un widget recién añadido.
interactionsLos callbacks del anfitrión que el widget solicita.

Canales y política de enlace​

Un widget declara un canal:

CanalEl componente recibePolítica de enlace
measurementlatest por nombre de medición, una ventana móvil samples y un errorwindow: cuántas muestras conservar
alarmLas filas de alarma actuales, un recuento total e indicadores de carga y errorpageSize y, opcionalmente, los filtros state, severity y acknowledged
controlEl historial de comandos de un dispositivo destinopageSize
locationMuestras de ubicación de la entidad enlazadaninguna
selectionSin datos. El widget gobierna un slot a partir de la búsqueda de candidatos del anfitrión.ninguna
noneSin datos. Un widget estático.ninguna

Un valor de enlace es una constante, { const: 25 }, o una referencia a una clave de opción, { option: 'window', min: 1, default: 1 }. La referencia se comprueba contra el tipo de la opción al compilar. No puedes aportar una cadena GraphQL ni una función de suscripción: el transporte es del anfitrión. Los canales selection y none no abren ningún canal de datos y deben declarar datasource: { mode: 'none' }.

Cuando un slot no tiene dispositivo, o un dispositivo enlazado se elimina, el anfitrión muestra su estado estándar de sin enlazar o no disponible y no entrega a tu componente los datos de la entidad anterior.

Opciones​

Una opción tiene un kind (string, number, boolean o enum) y una frase doc. Un número puede declarar min, max e integer. Un enum declara sus values. Una cadena puede declarar format: 'json', que exige que el valor se pueda analizar como JSON. required marca una opción cuya ausencia deja al widget sin nada que mostrar; una cadena vacía cuenta como ausente. El anfitrión valida las opciones almacenadas contra el esquema, y la consola genera su formulario de ajustes a partir de él.

Lee las opciones de forma defensiva con los ayudantes del SDK optString, optNumber y optBoolean. Devuelven undefined ante un valor editado a mano del tipo equivocado en lugar de lanzar una excepción.

Etiquetas y textos de ayuda​

Da a cada opción un label portable y, cuando una frase ayude, un help. Un anfitrión que nunca ha oído hablar de tu widget muestra ese texto tal cual, de modo que tus opciones se entienden sin ninguna traducción en el anfitrión. Cada texto también puede llevar un id de mensaje que el anfitrión busca primero en sus propias traducciones:

TextoTexto portableId de mensaje opcional
Etiqueta de la opciónlabellabelId
Ayuda de la opciónhelphelpId
Opciones de un enumvalueLabels, por valorvalueLabelIds, por valor
La entrada sin valor de un enumplaceholderplaceholderId
Título y categoría del widgetdisplay.title, display.categorydisplay.titleId, display.categoryId

La consola usa el id de mensaje cuando sus propias traducciones lo definen y, si no, tu texto portable. Una etiqueta sin ninguno de los dos recurre a la clave de la opción hecha legible (maxRows se muestra como «Max rows»), y una opción de enum sin ninguno muestra su valor tal cual. Un id de mensaje que el anfitrión no define no es un error, así que tu paquete funciona en un anfitrión que nunca lo ha visto.

options: {
show: {
kind: 'enum',
values: ['bar', 'number'],
doc: 'Whether the card draws the fill bar under the number.',
label: 'Show',
valueLabels: { bar: 'Number and fill bar', number: 'Number only' },
placeholder: 'Number and fill bar',
},
},

Controles de edición especializados​

Una opción puede nombrar un flujo de edición especializado en su campo control. No tiene relación con el canal de datos control. Sin él, la opción recibe el campo genérico de su tipo: un cuadro de texto, un cuadro numérico, una casilla o una lista desplegable.

El vocabulario es abierto. Cualquier cadena no vacía se registra, y un anfitrión que no implementa el control nombrado muestra en su lugar el campo genérico de esa opción. Así, un paquete sigue siendo instalable en un anfitrión más antiguo que los controles que nombra. Nombra tus propios controles en tu espacio de nombres, como acme/capacity-presets. Los nombres sin prefijo pertenecen al anfitrión.

La consola implementa los controles enumerados en HOST_OPTION_CONTROLS, exportado por @devicechain/dashboards:

ControlFlujo
command-vocabularyElige uno de los comandos del dispositivo enlazado y escribe juntos su clave, su etiqueta y sus parámetros.
slot-targetElige uno de los slots del panel.
basemapMarca la fuente de teselas ráster del mapa y su atribución.

Un control que la consola no implementa, incluido cualquier control con espacio de nombres, recibe el campo genérico. Para dar a tus opciones una experiencia de edición propia, distribuye un editor.

Un editor personalizado​

implementation.editor es un componente de React opcional. Cuando un widget tiene uno, la consola lo muestra en lugar de los campos de opción generados. Los campos de la fuente de datos y los avisos de la consola siguen siendo de la consola. El editor recibe WidgetEditorProps, exportado por @devicechain/widget-sdk:

PropSignificado
optionsLas opciones almacenadas del widget, de solo lectura.
setOptions(patch)La única forma de cambiarlas. Varias claves en una llamada se aplican juntas.
servicesServicios del anfitrión, pasados de forma explícita. Ver abajo.
descriptorEl descriptor de tu registro, para las etiquetas, los límites y los valores de enum de sus opciones.

setOptions se comprueba contra tu esquema de opciones antes de aplicarse. Una clave que el esquema no declara, o un valor del tipo equivocado, lanza una excepción en el momento en que el editor comete el error, en lugar de almacenarse sin que nada la lea. undefined y una cadena vacía eliminan una clave, igual que false en un booleano. Un número fuera de los límites declarados se ajusta a ellos.

services (WidgetEditorServices) contiene:

ServicioSignificado
slotsLos slots del panel, por nombre. Vacío en un panel sin slots.
deviceTokenEl token del dispositivo al que el widget está enlazado directamente, cuando lo está.
commandChoices(deviceToken)Los comandos que acepta un dispositivo. Opcional: un anfitrión sin vocabulario de comandos lo omite.

Un editor no recibe hub, ni cliente, ni token de acceso, y no debe importar nada de la aplicación anfitriona. Un servicio que el anfitrión no puede proporcionar está ausente, y tu editor debe tolerarlo. Si el editor lanza una excepción al mostrarse, la consola lo sustituye por un mensaje de error y un botón para reintentar, y se conservan los demás cambios del autor en el panel.

implementation: {
kind: 'trusted-react',
component: TankLevelCard,
editor: function TankLevelCardEditor({ options, setOptions, descriptor }) {
return (
<label>
{descriptor.options.unit?.label ?? 'unit'}
<input value={options.unit ?? ''} onChange={(e) => setOptions({ unit: e.target.value })} />
</label>
);
},
},

Dentro de defineWidget, options y setOptions se tipan a partir de tu esquema de opciones, así que una clave mal escrita también es un error de compilación. El widget de tarjeta del paquete de ejemplo tiene un editor completo.

Interacciones y modelo de confianza​

interactions enumera los callbacks del anfitrión que usa el widget: alarmActions (reconocer y cerrar; solo canal de alarmas), commandSend (solo canal de control), selection y candidates. El anfitrión proporciona únicamente los callbacks que declaraste, limitados además a lo que el anfitrión admite y a lo que el espectador tiene permitido. Un widget sin callback se comporta como de solo lectura. El servidor sigue siendo la autoridad: una acción que el espectador no puede realizar es rechazada por el servidor, y el rechazo llega a tu componente.

Un paquete de widgets es código de confianza. Se empaqueta en la aplicación anfitriona en tiempo de compilación y se ejecuta en la página del anfitrión con los privilegios del anfitrión. Las declaraciones de un descriptor dan forma a la API; no son una frontera de seguridad y no detienen código malicioso en un paquete. El SDK no pasa a tu componente el token de acceso, el cliente ni el hub sin procesar, pero nada impide que código de la misma página los alcance. Instala solo paquetes que hayas revisado, de editores en los que confíes. El anfitrión envuelve cada widget en un límite de error, de modo que un widget que lanza una excepción muestra un error en su propio marco y sus vecinos siguen mostrándose. Eso no contiene un bucle infinito ni código hostil.

A continuación​