anbaric-plugins 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/package.json +24 -0
- package/src/modules.d.ts +5 -0
- package/src/state-machines/StateMachinesWidget.tsx +230 -0
- package/src/state-machines/index.tsx +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# anbaric-plugins
|
|
2
|
+
|
|
3
|
+
Open-source plugins for the Anbaric platform dashboard. A plugin registers
|
|
4
|
+
pages and widgets on the platform's root app: pages appear in the side nav,
|
|
5
|
+
widgets are React components rendered into a page's `<main>`.
|
|
6
|
+
|
|
7
|
+
This package ships the plugins the platform loads by default:
|
|
8
|
+
|
|
9
|
+
| Plugin module | What it registers |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `anbaric-plugins/state-machines` | The `/` Dashboard page with the state machines and jobs overview |
|
|
12
|
+
|
|
13
|
+
## How plugins work
|
|
14
|
+
|
|
15
|
+
A plugin is a TypeScript module that exports a `plugin` object. The platform
|
|
16
|
+
server (`anbaric-cloud-hosting`) loads the modules named in the
|
|
17
|
+
`ANBARIC_PLUGINS` environment variable (comma-separated) at boot, compiles
|
|
18
|
+
each one into a browser bundle, and serves it to the dashboard, which renders
|
|
19
|
+
the components. The dashboard's own build is plugin-agnostic — installing a
|
|
20
|
+
plugin is configuration only:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
npm install some-plugin-package
|
|
24
|
+
ANBARIC_PLUGINS=some-plugin-package,anbaric-plugins/state-machines
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If `ANBARIC_PLUGINS` is unset the platform loads
|
|
28
|
+
`anbaric-plugins/state-machines`, so the Dashboard is there by default.
|
|
29
|
+
Setting the variable replaces the default list, so include it explicitly if
|
|
30
|
+
you still want the Dashboard.
|
|
31
|
+
|
|
32
|
+
## Writing a plugin
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import type { Plugin } from 'anbaric-cloud-hosting'
|
|
36
|
+
import { Card } from '@anbaric/design-system/components/Card'
|
|
37
|
+
|
|
38
|
+
function GreetingWidget({ fetchData }: { fetchData: (parameters?: Record<string, string>) => Promise<any> }) {
|
|
39
|
+
return <Card>Hello from a plugin.</Card>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const plugin: Plugin = {
|
|
43
|
+
name: 'greeting',
|
|
44
|
+
pages: [{ path: '/greeting', title: 'Greeting', icon: 'waving_hand', navOrder: 50 }],
|
|
45
|
+
widgets: [
|
|
46
|
+
{
|
|
47
|
+
page: '/greeting',
|
|
48
|
+
id: 'greeting',
|
|
49
|
+
component: GreetingWidget,
|
|
50
|
+
data: async (parameters) => ({ greeted: parameters.name ?? 'world' }),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { plugin }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **Pages** are registered by path; `/` is the homepage. Plugin page paths
|
|
59
|
+
take precedence over deployed apps with the same name; `icon` is a
|
|
60
|
+
Material Symbols name; lower `navOrder` sorts higher in the nav.
|
|
61
|
+
- **Widgets** attach to any page path — including pages registered by other
|
|
62
|
+
plugins. `position` orders widgets on a page; a `title` renders a heading
|
|
63
|
+
above the widget.
|
|
64
|
+
- **`data`** is an optional server-side function. The widget's component
|
|
65
|
+
receives a `fetchData(parameters?)` prop that invokes it on the platform
|
|
66
|
+
(`GET /plugins/data?plugin=<name>&widget=<id>&…`) and resolves with its
|
|
67
|
+
JSON-serialisable return value. Because the function's code is also
|
|
68
|
+
carried (unexecuted) in the browser bundle, do not import server-only
|
|
69
|
+
modules at the top level of files that components also live in.
|
|
70
|
+
- Components render with the **platform's own React and design system**:
|
|
71
|
+
imports of `react`, `react-dom` and `@anbaric/design-system/components/*`
|
|
72
|
+
are provided by the dashboard at runtime, not bundled. Style with design
|
|
73
|
+
system components and tokens; a plugin's own `.css` imports are ignored.
|
|
74
|
+
The chart components (`Graph`, `ConcentrationCurve`, `ExposureBars`) are
|
|
75
|
+
not available to plugins.
|
|
76
|
+
|
|
77
|
+
A package can ship several plugins by exposing one module per plugin via
|
|
78
|
+
`exports` subpaths, as this package does.
|
|
79
|
+
|
|
80
|
+
## Layout
|
|
81
|
+
|
|
82
|
+
One folder per plugin under `src/`, exported as a subpath:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
src/state-machines/index.tsx the plugin object
|
|
86
|
+
src/state-machines/StateMachinesWidget.tsx
|
|
87
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "anbaric-plugins",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "Open-source Anbaric platform plugins: pages and widgets loaded by the hosting server via ANBARIC_PLUGINS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "chris@anbaric.ai",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./state-machines": "./src/state-machines/index.tsx"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"react": ">=19"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@anbaric/design-system": "file:../anbaric-design-system",
|
|
16
|
+
"@types/react": "^19.2.14",
|
|
17
|
+
"anbaric-cloud-hosting": "^1.7.0",
|
|
18
|
+
"react": "^19.2.6",
|
|
19
|
+
"typescript": "^7.0.2"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/modules.d.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useEffect, useState, type CSSProperties } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Alert } from '@anbaric/design-system/components/Alert'
|
|
4
|
+
import { Badge } from '@anbaric/design-system/components/Badge'
|
|
5
|
+
import { Card } from '@anbaric/design-system/components/Card'
|
|
6
|
+
|
|
7
|
+
interface StateMachine {
|
|
8
|
+
workflowId: string
|
|
9
|
+
url: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Job {
|
|
13
|
+
id: string
|
|
14
|
+
state: string
|
|
15
|
+
workflowId?: string
|
|
16
|
+
properties: Record<string, unknown>
|
|
17
|
+
startedAt?: string
|
|
18
|
+
startedBy?: string
|
|
19
|
+
lastUpdated?: string
|
|
20
|
+
transitions?: Array<{ from: string; to: string; actor: string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const formatDate = (iso?: string) =>
|
|
24
|
+
iso
|
|
25
|
+
? new Date(iso).toLocaleString(undefined, {
|
|
26
|
+
day: 'numeric',
|
|
27
|
+
month: 'short',
|
|
28
|
+
year: 'numeric',
|
|
29
|
+
hour: 'numeric',
|
|
30
|
+
minute: '2-digit',
|
|
31
|
+
})
|
|
32
|
+
: '—'
|
|
33
|
+
|
|
34
|
+
const machineHeading: CSSProperties = {
|
|
35
|
+
margin: 0,
|
|
36
|
+
fontFamily: 'var(--font-title)',
|
|
37
|
+
fontSize: '1.15rem',
|
|
38
|
+
textTransform: 'var(--title-transform)' as CSSProperties['textTransform'],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cell: CSSProperties = {
|
|
42
|
+
textAlign: 'left',
|
|
43
|
+
padding: 'calc(var(--space-sm) / 2) var(--space-sm)',
|
|
44
|
+
borderBottom: '1px solid color-mix(in srgb, var(--color-grey) 12%, transparent)',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const headerCell: CSSProperties = {
|
|
48
|
+
...cell,
|
|
49
|
+
fontSize: '0.72rem',
|
|
50
|
+
textTransform: 'uppercase',
|
|
51
|
+
letterSpacing: '0.06em',
|
|
52
|
+
color: 'var(--color-foreground-tint-2)',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mono: CSSProperties = {
|
|
56
|
+
fontFamily: 'var(--font-mono)',
|
|
57
|
+
fontSize: '0.78rem',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const muted: CSSProperties = {
|
|
61
|
+
color: 'var(--color-foreground-tint-2)',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function PropertyList({ job }: { job: Job }) {
|
|
65
|
+
const entries: Array<[string, unknown]> = [['id', job.id], ...Object.entries(job.properties)]
|
|
66
|
+
return (
|
|
67
|
+
<dl style={{ margin: 0, display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
|
68
|
+
{entries.map(([name, value]) => (
|
|
69
|
+
<div key={name} style={{ display: 'flex', gap: 'var(--space-sm)', alignItems: 'baseline' }}>
|
|
70
|
+
<dt style={{ ...mono, ...muted, flex: 'none' }}>{name}</dt>
|
|
71
|
+
<dd style={{ margin: 0, overflowWrap: 'anywhere' }}>{String(value)}</dd>
|
|
72
|
+
</div>
|
|
73
|
+
))}
|
|
74
|
+
</dl>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function TransitionHistory({ transitions }: { transitions: Job['transitions'] }) {
|
|
79
|
+
if (!transitions || transitions.length === 0) {
|
|
80
|
+
return <span style={muted}>—</span>
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
|
84
|
+
{transitions.map((transition, index) => (
|
|
85
|
+
<div key={index}>
|
|
86
|
+
{transition.from} → {transition.to}{' '}
|
|
87
|
+
<span style={{ ...muted, fontSize: '0.78rem' }}>by {transition.actor}</span>
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function JobsTable({ jobs }: { jobs: Job[] }) {
|
|
95
|
+
if (jobs.length === 0) {
|
|
96
|
+
return <p style={{ margin: 0, color: 'var(--color-foreground-tint-2)' }}>No jobs yet.</p>
|
|
97
|
+
}
|
|
98
|
+
return (
|
|
99
|
+
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
100
|
+
<thead>
|
|
101
|
+
<tr>
|
|
102
|
+
<th style={headerCell}>Properties</th>
|
|
103
|
+
<th style={headerCell}>State</th>
|
|
104
|
+
<th style={headerCell}>Started</th>
|
|
105
|
+
<th style={headerCell}>Updated</th>
|
|
106
|
+
<th style={headerCell}>History</th>
|
|
107
|
+
</tr>
|
|
108
|
+
</thead>
|
|
109
|
+
<tbody>
|
|
110
|
+
{jobs.map((job) => (
|
|
111
|
+
<tr key={job.id}>
|
|
112
|
+
<td style={cell}>
|
|
113
|
+
<PropertyList job={job} />
|
|
114
|
+
</td>
|
|
115
|
+
<td style={cell}>
|
|
116
|
+
<Badge tone="primary" dot>
|
|
117
|
+
{job.state}
|
|
118
|
+
</Badge>
|
|
119
|
+
</td>
|
|
120
|
+
<td style={cell}>
|
|
121
|
+
{formatDate(job.startedAt)}
|
|
122
|
+
<div style={{ ...muted, fontSize: '0.78rem' }}>by {job.startedBy ?? 'system'}</div>
|
|
123
|
+
</td>
|
|
124
|
+
<td style={cell}>{formatDate(job.lastUpdated)}</td>
|
|
125
|
+
<td style={cell}>
|
|
126
|
+
<TransitionHistory transitions={job.transitions} />
|
|
127
|
+
</td>
|
|
128
|
+
</tr>
|
|
129
|
+
))}
|
|
130
|
+
</tbody>
|
|
131
|
+
</table>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function StateMachinesWidget() {
|
|
136
|
+
const [machines, setMachines] = useState<StateMachine[] | undefined>(undefined)
|
|
137
|
+
const [jobs, setJobs] = useState<Job[] | undefined>(undefined)
|
|
138
|
+
const [failed, setFailed] = useState(false)
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
void (async () => {
|
|
142
|
+
try {
|
|
143
|
+
const [machinesResponse, jobsResponse] = await Promise.all([
|
|
144
|
+
fetch('/state-machines'),
|
|
145
|
+
fetch('/jobs'),
|
|
146
|
+
])
|
|
147
|
+
if (!machinesResponse.ok || !jobsResponse.ok) {
|
|
148
|
+
setFailed(true)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
setMachines(await machinesResponse.json())
|
|
152
|
+
setJobs(await jobsResponse.json())
|
|
153
|
+
} catch {
|
|
154
|
+
setFailed(true)
|
|
155
|
+
}
|
|
156
|
+
})()
|
|
157
|
+
}, [])
|
|
158
|
+
|
|
159
|
+
const registered = new Set((machines ?? []).map((machine) => machine.workflowId))
|
|
160
|
+
const workflowIds = [
|
|
161
|
+
...new Set([
|
|
162
|
+
...registered,
|
|
163
|
+
...(jobs ?? [])
|
|
164
|
+
.map((job) => job.workflowId)
|
|
165
|
+
.filter((workflowId): workflowId is string => Boolean(workflowId)),
|
|
166
|
+
]),
|
|
167
|
+
].sort()
|
|
168
|
+
const unassigned = (jobs ?? []).filter((job) => !job.workflowId)
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-lg)' }}>
|
|
172
|
+
{failed ? (
|
|
173
|
+
<Alert variant="danger" title="Could not load the platform state">
|
|
174
|
+
The platform rejected the request — try reloading the page.
|
|
175
|
+
</Alert>
|
|
176
|
+
) : null}
|
|
177
|
+
{machines && workflowIds.length === 0 ? (
|
|
178
|
+
<Card>
|
|
179
|
+
<p style={{ margin: 0 }}>
|
|
180
|
+
No state machines yet. Deploy an app with <code>anbaric deploy</code> and its
|
|
181
|
+
machines will appear here.
|
|
182
|
+
</p>
|
|
183
|
+
</Card>
|
|
184
|
+
) : null}
|
|
185
|
+
{workflowIds.map((workflowId) => {
|
|
186
|
+
const machineJobs = (jobs ?? []).filter((job) => job.workflowId === workflowId)
|
|
187
|
+
return (
|
|
188
|
+
<Card key={workflowId}>
|
|
189
|
+
<div
|
|
190
|
+
style={{
|
|
191
|
+
display: 'flex',
|
|
192
|
+
alignItems: 'center',
|
|
193
|
+
justifyContent: 'space-between',
|
|
194
|
+
gap: 'var(--space-md)',
|
|
195
|
+
marginBottom: 'var(--space-md)',
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<h2 style={machineHeading}>{workflowId}</h2>
|
|
199
|
+
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}>
|
|
200
|
+
<Badge tone="neutral">
|
|
201
|
+
{machineJobs.length} {machineJobs.length === 1 ? 'job' : 'jobs'}
|
|
202
|
+
</Badge>
|
|
203
|
+
{registered.has(workflowId) ? (
|
|
204
|
+
<Badge tone="success" dot>
|
|
205
|
+
consumer connected
|
|
206
|
+
</Badge>
|
|
207
|
+
) : (
|
|
208
|
+
<Badge tone="warning" dot>
|
|
209
|
+
no consumer
|
|
210
|
+
</Badge>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<JobsTable jobs={machineJobs} />
|
|
215
|
+
</Card>
|
|
216
|
+
)
|
|
217
|
+
})}
|
|
218
|
+
{unassigned.length > 0 ? (
|
|
219
|
+
<Card>
|
|
220
|
+
<h2 style={{ ...machineHeading, marginBottom: 'var(--space-md)' }}>
|
|
221
|
+
Unassigned jobs
|
|
222
|
+
</h2>
|
|
223
|
+
<JobsTable jobs={unassigned} />
|
|
224
|
+
</Card>
|
|
225
|
+
) : null}
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export { StateMachinesWidget }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from 'anbaric-cloud-hosting'
|
|
2
|
+
|
|
3
|
+
import { StateMachinesWidget } from './StateMachinesWidget'
|
|
4
|
+
|
|
5
|
+
const plugin: Plugin = {
|
|
6
|
+
name: 'state-machines',
|
|
7
|
+
pages: [{ path: '/', title: 'Dashboard', icon: 'dashboard', navOrder: 0 }],
|
|
8
|
+
widgets: [{ page: '/', id: 'state-machines', component: StateMachinesWidget }],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { plugin }
|