Skip to content

Custom panels ​

Panels let an app add its own tabs without any UI code. You describe a panel as a list of blocks; the UI draws it as a tab next to the built-in ones, and button clicks and form submissions run handlers in your app.

ts
import {panelsModule} from '@himmetkaan/remote-devtools';

const panels = panelsModule();
RemoteDevTools.init({/* … */ modules: [panels]});

const queue = panels.define({
    id: 'jobs',
    title: 'Job queue',
    icon: '📬',
    blocks: [
        {id: 'note', type: 'text', tone: 'info', text: 'Failed jobs are retried three times.'},
        {id: 'stats', type: 'stats', items: [{label: 'Pending', value: 0}, {label: 'Failed', value: 0, tone: 'error'}]},
        {
            id: 'list',
            type: 'table',
            title: 'Jobs',
            columns: [{key: 'id', title: 'ID'}, {key: 'name', title: 'Name'}, {key: 'state', title: 'State'}],
            rows: [],
            rowActions: [
                {id: 'retry', label: 'Retry', handler: (row) => jobs.retry(row.id)},
                {id: 'drop', label: 'Drop', danger: true, confirm: 'Drop this job?', handler: (row) => jobs.drop(row.id)},
            ],
        },
        {
            id: 'enqueue',
            type: 'form',
            title: 'Enqueue a job',
            fields: {name: {type: 'text', required: true}, priority: {type: 'number', min: 0, max: 9, default: 5}},
            submit: {label: 'Enqueue', handler: ({name, priority}) => jobs.add(name, priority)},
        },
        {id: 'tools', type: 'actions', actions: [{id: 'flush', label: 'Flush all', danger: true, handler: () => jobs.flush()}]},
        {id: 'config', type: 'keyValue', title: 'Config', data: {concurrency: 4, backoff: 'exponential'}},
        {id: 'raw', type: 'json', title: 'Raw state', data: jobs.snapshot()},
    ],
});

Blocks ​

TypeFieldsShows
texttext, tone?A note; tone is info, success, warn or error
statsitems: {label, value, hint?, tone?}[]Big number cards
keyValuedata: Record<string, string | number | boolean | null>A two-column table
tablecolumns: {key, title}[], rows, rowActions?A table; a filter appears from 8 rows
jsondataA collapsible JSON tree
actionsactions: {id, label, confirm?, danger?, handler}[]Buttons
formfields, submit: {label, danger?, handler}A form built from the field DSL

Every block has an id (unique in the panel) and an optional title.

Handlers run on the device. Their return value appears under the block and in Logs; a thrown error is reported as a failure. Row actions receive the row as the UI shows it. Form values are validated against fields on the device before submit.handler runs.

Updating a panel ​

Push changes whenever your data changes. Only the fields you pass are sent:

ts
jobs.on('change', () => {
    queue.update('stats', {items: [{label: 'Pending', value: jobs.pending}, {label: 'Failed', value: jobs.failed, tone: 'error'}]});
    queue.update('list', {rows: jobs.list().map(({id, name, state}) => ({id, name, state}))});
});
  • queue.setBlocks(blocks) replaces all blocks (to add or remove blocks).
  • queue.remove() or panels.remove('jobs') removes the tab.
  • Panels can be defined before the SDK connects; the current content is sent when it does.

Released under the MIT License.