@xeplr/ui-actions
v1.1.0
Published
React components for @xeplr/actions — ActionExplorer + ActionRunner (builds on @xeplr/ui-schema-handler)
Maintainers
Readme
@xeplr/ui-actions
React components for an @xeplr/actions registry. <ActionExplorer> lists registered actions and expands each one into tables of its input and output schema. <ActionRunner> is a "try this action" panel: pick an action, fill its inputs, press Run, and see the result, whether it succeeded or failed. The input form is DynamicForm from @xeplr/ui-schema-handler. Neither component imports @xeplr/actions. They take the plain shape its list() returns, and the runner calls whatever onRun you give it.
(The package name on npm is @xeplr/ui-actions. The GitHub repo and folder are named xeplr-ui-actions.)
Install
npm i @xeplr/ui-actions @xeplr/ui-schema-handler @xeplr/schema-handler @xeplr/actions react-router-domPeer dependencies: react ^18 || ^19, @xeplr/actions ^1, @xeplr/ui-schema-handler ^1 (which needs @xeplr/schema-handler ^1), and react-router-dom ^6 || ^7 — imported by @xeplr/ui-utils' useUrlState (a dependency, ^1.1.2), which keeps the explorer's open cards in the URL. With no router above, they stay in memory.
The package ships its source (main: src/index.js, JSX included), so your bundler must compile JSX from node_modules. Vite does this. The demo also lists the CommonJS @xeplr/* packages in Vite's optimizeDeps.include and resolve.alias, so their nested requires resolve.
Both stylesheets are opt-in. The runner's form uses the schema-handler classes, so import both for the default look:
import '@xeplr/ui-schema-handler/src/styles.css'
import '@xeplr/ui-actions/src/styles.css'Quick start
From demo/src/App.jsx:
import * as actions from '@xeplr/actions'
import { ActionExplorer, ActionRunner } from '@xeplr/ui-actions'
import '@xeplr/ui-schema-handler/src/styles.css'
import '@xeplr/ui-actions/src/styles.css'
actions.register('add', {
description: 'Add two numbers.',
inputSchema: [
{ name: 'a', type: 'number', required: true, order: 1 },
{ name: 'b', type: 'number', required: true, order: 2 }
],
execute: async ({ input }) => ({ sum: input.a + input.b })
})
const REGISTERED = actions.list()
export default function App() {
return (
<>
<ActionExplorer actions={REGISTERED} />
<ActionRunner
actions={REGISTERED}
onRun={(name, input) => actions.runAction({ name, input, system: { source: 'demo' }, timeoutMs: 5000 })}
/>
</>
)
}runAction never throws. It resolves to { status: 'success' | 'failed', output, error, durationMs }, which is the shape ActionRunner displays. If the actions run on a server, onRun can make a network call instead, as long as it resolves to the same shape.
Exports
| export | kind | props | what it does |
|---|---|---|---|
| ActionExplorer | component | { actions, onSelect, selected, urlKey } | One card per action. Clicking a card opens its Inputs/Outputs schema tables. |
| ActionRunner | component | { actions, onRun, initialAction } | Action picker, an input form (DynamicForm), Run / Reset buttons, and a result panel |
| useActionExplorer | hook | { urlKey } | Which cards are open: { open, isOpen(name), toggle(name) } — for an explorer of your own design |
<ActionExplorer>
| prop | type | default | meaning |
|---|---|---|---|
| actions | Array<{ name, description?, inputSchema?, outputSchema? }> | [] | For example actions.list(). Cards are keyed by name. |
| onSelect | (name) => void | — | Called when a card header is clicked |
| selected | string | — | Name of the card to highlight (xeplr-ax-card--selected) |
| urlKey | string | — | Keep the open cards in the URL, under <urlKey>Open — see below. Omit it and they stay in memory |
- A card always shows its name and
description. Clicking the header toggles it open and callsonSelectin the same click. - An open card shows an Inputs table and, only if the action has an
outputSchema, an Outputs table. The columns are Name, Type (defaultstring), Req (●), Default (JSON) and Description, sorted byorder. An empty or missing input schema reads "Inputs: none". - With no actions it shows "No actions registered."
Open cards live in the URL: urlKey
An open card is a place — the schema someone was reading — so with urlKey the names of the open cards are written to the URL's query, and a refresh or a shared link opens them again:
<ActionExplorer actions={REGISTERED} urlKey="actions" /> // ?actionsOpen=add,send.email- The key is the host's, never a default. A page may hold two explorers and the host app has keys of its own, so nothing is written without a
urlKey— give each explorer its own. Without one, the open cards stay in memory, as before. - Several cards can be open: the value is their names, comma-separated, each URI-encoded. A name that is no longer registered simply opens nothing.
- Opening or closing a card adds a history entry, so Back closes it again.
- Needs a router above the explorer (
react-router-dom). The value format issrc/explorerUrlState.js(explorerUrlKey,encodeOpen,decodeOpen,toggleOpen); the hook issrc/useActionExplorer.js.
<ActionRunner>
| prop | type | default | meaning |
|---|---|---|---|
| actions | Array<{ name, description?, inputSchema? }> | [] | Actions in the picker |
| onRun | (name, input) => Promise<{ status, output?, error?, durationMs? }> | — | Runs the action. Without it, Run does nothing. |
| initialAction | string | first action | The action selected when the component mounts |
initialActionis read once. Changing it later does not change the picker. To make the runner follow a selection elsewhere, such asActionExplorer'sselected, remount it withkey={selected}.- Switching actions clears the input and the last result. Values typed for one schema are never sent to another.
- The inputs come from
DynamicFormwith the action'sinputSchema. It is disabled while a run is in progress. The component does no validation before callingonRun.runActiondoes that, and a validation failure comes back as afailedresult. - Run shows "Running…" and is disabled until
onRunsettles. Reset clears the input and result. - If
onRunthrows, the result becomes{ status: 'failed', error: { name, message }, durationMs: 0 }.
The result panel:
| status | shows |
|---|---|
| success | "✓ success", durationMs if it is a number, and output as pretty JSON |
| anything else (failed, skipped, …) | "✗ status", then error.name: error.message, and error.details as a bulleted list |
error.details is rendered directly as list items, so each entry must be a string. A ValidationError from the current @xeplr/schema-handler has details as { field, message } objects, and runAction passes them through. React cannot render those objects. Until the runner handles them, flatten them in onRun:
<ActionRunner
actions={REGISTERED}
onRun={async (name, input) => {
const r = await actions.runAction({ name, input, timeoutMs: 5000 })
if (r.error && Array.isArray(r.error.details)) {
r.error.details = r.error.details.map((d) => (typeof d === 'string' ? d : d.message))
}
return r
}}
/>Theming
The stylesheet has no CSS variables and does not read the --xeplr-* tokens from @xeplr/ui-account's ThemeProvider. Its colours are fixed and light. Every rule uses the xeplr-ax-* prefix, so you can override it easily or leave it out.
| component | classes |
|---|---|
| ActionExplorer | xeplr-ax-explorer, xeplr-ax-empty, xeplr-ax-card, xeplr-ax-card--selected, xeplr-ax-card-hd, xeplr-ax-name, xeplr-ax-hint, xeplr-ax-desc, xeplr-ax-body, xeplr-ax-schema-title, xeplr-ax-schema-empty, xeplr-ax-schema-tbl |
| ActionRunner | xeplr-ax-runner, xeplr-ax-runner-row, xeplr-ax-runner-lbl, xeplr-ax-runner-select, xeplr-ax-runner-desc, xeplr-ax-runner-form, xeplr-ax-runner-actions, xeplr-ax-primary |
| result panel | xeplr-ax-result, xeplr-ax-result--success / --failed / --skipped, xeplr-ax-result-hd, xeplr-ax-result-dur, xeplr-ax-result-body, xeplr-ax-result-err-title, xeplr-ax-result-err-details |
The form inside the runner uses xeplr-shf-* classes from @xeplr/ui-schema-handler.
Try it
The demo links sibling checkouts (../../xeplr-actions, ../../xeplr-schema-handler, ../../xeplr-ui-schema-handler), so run it from inside the xeplr-os workspace:
cd demo
npm install
npm run dev # http://localhost:5274 — four sample actions: greet, add, slow-echo, boom (throws)Files
src/
index.js ─ exports ActionExplorer, ActionRunner, useActionExplorer
ActionExplorer.jsx ─ cards + input/output schema tables
useActionExplorer.js ─ which cards are open: in the URL under urlKey, or in memory
explorerUrlState.js ─ the open cards' URL value (React-free)
ActionRunner.jsx ─ picker, DynamicForm inputs, run, result panel
styles.css ─ optional default styles, xeplr-ax-*
demo/ ─ Vite app (not published)Tests
npm test # node --test test/*.test.jstest/explorerUrlState.test.js covers the open cards' URL value, and useActionExplorer reading it back from a real MemoryRouter and being toggled in memory, rendered with react-dom/server. The components themselves have no tests.
License
MIT
