@t4h.framework/vite-panel-plugin 0.0.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 +193 -0
- package/dist/client/assets/index-C8OZoaNO.js +44 -0
- package/dist/client/index.html +51 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +30 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +76 -0
- package/dist/node.js.map +1 -0
- package/dist/panel-api.d.ts +134 -0
- package/dist/panel-api.d.ts.map +1 -0
- package/dist/panel-api.js +2 -0
- package/dist/panel-api.js.map +1 -0
- package/dist/panel-config.d.ts +71 -0
- package/dist/panel-config.d.ts.map +1 -0
- package/dist/panel-config.js +60 -0
- package/dist/panel-config.js.map +1 -0
- package/dist/vite-panel-plugin.d.ts +7 -0
- package/dist/vite-panel-plugin.d.ts.map +1 -0
- package/dist/vite-panel-plugin.js +12 -0
- package/dist/vite-panel-plugin.js.map +1 -0
- package/package.json +98 -0
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @t4h.framework/vite-panel-plugin
|
|
2
|
+
|
|
3
|
+
The panel of the framework: the workflows of an app with a form to start runs
|
|
4
|
+
from their input schema, the runs with filters and a live execution trace
|
|
5
|
+
(activities as a waterfall, child runs nested under the activity that started
|
|
6
|
+
them, input, output, duration, logs, the activity that failed), and an inbox of
|
|
7
|
+
activities waiting for an external advance.
|
|
8
|
+
|
|
9
|
+
It ships with `@t4h.framework/dev`, which plugs it into the Vite instance it
|
|
10
|
+
runs in middleware mode, so there is nothing to install or configure: run
|
|
11
|
+
`framework dev` and open the printed base URL. API paths (`/projects/*`,
|
|
12
|
+
`/_dev/*`) keep going to the dev server; everything else is the panel.
|
|
13
|
+
|
|
14
|
+
## Hosting the panel elsewhere
|
|
15
|
+
|
|
16
|
+
The package is a Vite plugin (`apply: 'serve'`) that serves the pre-built SPA
|
|
17
|
+
from `dist/client` and injects the host configuration in the page:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import panel from '@t4h.framework/vite-panel-plugin'
|
|
21
|
+
|
|
22
|
+
panel({
|
|
23
|
+
config: {
|
|
24
|
+
basePath: '/panel',
|
|
25
|
+
apiUrl: '/tenants/acme/projects/billing/versions/v3',
|
|
26
|
+
api: { manifest: '/manifest.json' },
|
|
27
|
+
project: 'billing',
|
|
28
|
+
metadata: { tenant: 'acme', version: 'v3' },
|
|
29
|
+
fields: {
|
|
30
|
+
run: 'input,output,rejectedReason,activities(pendingStatus,childrenCount)',
|
|
31
|
+
runActivity: 'pendingStatus,childrenCount,input,output,pendings,children',
|
|
32
|
+
},
|
|
33
|
+
capabilities: {
|
|
34
|
+
trigger: false,
|
|
35
|
+
advance: false,
|
|
36
|
+
events: false,
|
|
37
|
+
rebuild: false,
|
|
38
|
+
},
|
|
39
|
+
routes: {
|
|
40
|
+
runs: {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
path: '/tenants/:tenant/projects/:project/versions/:version/workflows/:workflow/runs',
|
|
43
|
+
},
|
|
44
|
+
advance: {
|
|
45
|
+
method: 'PATCH',
|
|
46
|
+
path: '/tenants/:tenant/projects/:project/versions/:version/workflows/:workflow/runs/:run/activities/:activity',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Option | Default (local dev server) | Meaning |
|
|
54
|
+
| ---------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `title` | `Framework Panel` | Name shown in the sidebar and the browser tab |
|
|
56
|
+
| `basePath` | `''` (root) | Path the panel is mounted at; the plugin only answers under it and injects `<base href>` so the relative asset URLs resolve |
|
|
57
|
+
| `apiUrl` | `/_dev` | Where the panel API described below lives, exactly as the panel should call it: a same-origin path or an absolute URL, with the route paths appended |
|
|
58
|
+
| `project` | the manifest id | Project (app) id, the one id the core defines; shown in the top bar and filled into `:project` of the mirror routes |
|
|
59
|
+
| `metadata` | `{}` | `{ [key]: string }` labels of the host about this panel (the runtime sends `tenant` and `version`); each one is a box in the top bar and fills the `:key` of the same name in the routes |
|
|
60
|
+
| `fields` | `{}` | The `q` the panel sends on `api.runs` (`runs`), `api.run` (`run`) and `api.runActivity` (`runActivity`), in the host's own field-selection grammar; omitted, no `q` is sent |
|
|
61
|
+
| `api` | the paths of the local dev server (`/manifest`, `/runs/:run`, `/runs/:run/activities/:activity`, …) | Where each read and write of the panel API lives under `apiUrl`, as path templates with `:workflow`, `:run` and `:activity`; the panel knows what it asks, not where a host mounts it |
|
|
62
|
+
| `capabilities.trigger` | `true` | The host exposes `POST {apiUrl}/workflows/:id/runs` without authorization; otherwise the Run button becomes "Copy as curl" of the mirror route |
|
|
63
|
+
| `capabilities.advance` | `true` | Same for `POST {apiUrl}/activities/:id/advance` and the inbox |
|
|
64
|
+
| `capabilities.events` | `true` | The host streams `GET {apiUrl}/events`; otherwise the panel never polls and the top bar gets a Refresh button |
|
|
65
|
+
| `capabilities.rebuild` | `true` | The host rebuilds the app and starts over, like the dev server: the top bar shows the build state and empty lists mention the last build; off for a host that keeps its runs |
|
|
66
|
+
| `routes` | `POST /projects/:project/workflows/:workflow/runs` and `PATCH .../runs/:run/activities/:activity` | `{ method, path, headers? }` of the authorized routes a client must call, shown as `curl` examples with those headers; `path` is expanded with `project` and `metadata` |
|
|
67
|
+
|
|
68
|
+
The configuration is read from `window.__T4H_PANEL__`, written by the plugin in
|
|
69
|
+
place of the `<!--panel-config-->` marker of `index.html`; `<!--panel-base-->`
|
|
70
|
+
receives the `<base href>`. The host forwards `basePath` requests to the Vite
|
|
71
|
+
middlewares; everything else stays with the host.
|
|
72
|
+
|
|
73
|
+
## Hosting without Vite
|
|
74
|
+
|
|
75
|
+
A host that has no Vite (the production runtime) takes the same pieces from
|
|
76
|
+
`@t4h.framework/vite-panel-plugin/node`, which imports nothing from `vite`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import {
|
|
80
|
+
createPanelAssetsHandler,
|
|
81
|
+
createPanelHandler,
|
|
82
|
+
render,
|
|
83
|
+
resolvePanelConfig,
|
|
84
|
+
} from '@t4h.framework/vite-panel-plugin/node'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Export | Use |
|
|
88
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
89
|
+
| `createPanelHandler` | The whole panel as one `node:http` middleware — the page for a navigation under `basePath`, the assets for everything else. What the plugin uses |
|
|
90
|
+
| `createPanelAssetsHandler` | The hashed assets, cached forever, for a host that mounts them at a shared public path; `index: false` so a miss falls through |
|
|
91
|
+
| `render` | `index.html` with the config and the `<base href>` in place; `{ baseHref }` overrides the default `` `${basePath}/` `` |
|
|
92
|
+
| `resolvePanelConfig` | Fills the local defaults into a partial config, what `render` expects; `createPanelHandler` does it for you |
|
|
93
|
+
| `PANEL_CLIENT_DIST` | `dist/client`, the built SPA |
|
|
94
|
+
| `PANEL_CLIENT_ASSETS_DIR` | `dist/client/assets`, safe to serve publicly and cache forever — the names are hashed and `index.html` is not in there |
|
|
95
|
+
| `assertPanelBuilt` | Throws when the package was not built |
|
|
96
|
+
|
|
97
|
+
The runtime serves one panel per project version, so `basePath` carries the
|
|
98
|
+
tenant and the version while the assets live at a single shared path. Splitting
|
|
99
|
+
the two is what `baseHref` is for:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
app.use('/panel/assets', createPanelAssetsHandler())
|
|
103
|
+
|
|
104
|
+
// inside the authenticated controller of `.../versions/:version/panel`
|
|
105
|
+
return render(
|
|
106
|
+
resolvePanelConfig({
|
|
107
|
+
basePath: `/tenants/${tenant}/projects/${project}/versions/${version}/panel`,
|
|
108
|
+
apiUrl: `/tenants/${tenant}/projects/${project}/versions/${version}/panel/api`,
|
|
109
|
+
project,
|
|
110
|
+
metadata: { tenant, version },
|
|
111
|
+
capabilities: { trigger: false, advance: false, events: false },
|
|
112
|
+
}),
|
|
113
|
+
{ baseHref: '/panel/' },
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Every link the SPA renders is absolute (`basePath` is the router `basename`)
|
|
118
|
+
and `fetch` goes to `apiUrl` as given, so a `<base href>` that does not match
|
|
119
|
+
`basePath` only affects where the assets are looked up.
|
|
120
|
+
|
|
121
|
+
## Panel API
|
|
122
|
+
|
|
123
|
+
A host implements these routes under `apiUrl`, at the paths below or wherever
|
|
124
|
+
`api` relocates them. Dates are ISO strings;
|
|
125
|
+
`input`, `output`, `result`, `payload` and `args` are framework tagged JSON.
|
|
126
|
+
Every listing is a page: the panel asks for `limit` items from `offset` (25 at
|
|
127
|
+
a time, never more than 50) and the host answers the `count` of everything that
|
|
128
|
+
matched alongside, so the panel never holds more than one page. Child runs are
|
|
129
|
+
the exception: "show more" under an activity fetches the next page of 15 and
|
|
130
|
+
keeps the ones already on screen.
|
|
131
|
+
|
|
132
|
+
| Method | Route | Answer |
|
|
133
|
+
| ------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| `GET` | `/manifest` | `{ id, description, workflows: [{ id, description, schema, authorization? }], authorization? }`; a host may leave `authorization` out |
|
|
135
|
+
| `GET` | `/state` | `{ state: 'idle' \| 'starting' \| 'ready' \| 'exited' }`; only called with `capabilities.rebuild` |
|
|
136
|
+
| `GET` | `/runs?status&workflow&id&parentRunId&parentActivityId&rootOnly&createdAfter&createdBefore&limit&offset` | `{ count, runs: Run[] }` newest first; `id` is a prefix, `createdAfter`/`createdBefore` are epoch milliseconds |
|
|
137
|
+
| `GET` | `/runs/:id?q` | `Run & { input, output, rejectedReason }`; `q` is `fields.run` |
|
|
138
|
+
| `GET` | `/runs/:id/activities?limit&offset&q` | `{ count, activities: Activity[] }` newest first; `q` is `fields.runActivities` |
|
|
139
|
+
| `GET` | `/runs/:id/activities/:activityId?q` | `Activity & { input, output }`; `q` is `fields.runActivity` |
|
|
140
|
+
| `GET` | `/runs/:id/activities/:activityId/pendings?limit&offset&q` | `{ count, pendings: Pending[] }` newest first; `q` is `fields.runActivityPendings` |
|
|
141
|
+
| `GET` | `/runs/:id/logs?limit&offset` | `{ count, logs: [{ level, args, at }] }` newest first |
|
|
142
|
+
| `GET` | `/activities/waiting?limit&offset` | `{ count, activities: [{ id, pendingId, runId, workflowId, type, schema, timeoutAt }] }` newest first |
|
|
143
|
+
| `GET` | `/events` | SSE: `manifest`, `build:status`, `build:fail`, `app:exit`, `run:created`, `run:updated` (both with a `Run`), `ping`; only opened with `capabilities.events` |
|
|
144
|
+
| `POST` | `/workflows/:workflowId/runs` | Only with `capabilities.trigger`; body is the input, answers `201 { id }` |
|
|
145
|
+
| `POST` | `/activities/:activityId/advance` | Only with `capabilities.advance`; body `{ payload }`, answers `{ id, type, status }` |
|
|
146
|
+
|
|
147
|
+
`Run` is `{ id, workflow, status, rejectedActivityId, parentWorkflowRunId,
|
|
148
|
+
parentWorkflowRunActivityId, createdAt, updatedAt }` with `status` in
|
|
149
|
+
`running | fulfilled | rejected`. `Activity` is `{ id, type, kind, createdAt }`
|
|
150
|
+
plus, on request, `hasChildren`, `input` and `output`; an async one (`kind:
|
|
151
|
+
'async'`) also carries `completedAt` and, on request, `pendingStatus` (status
|
|
152
|
+
of its last pending). `Pending` is `{ id, previousId, status, schema, timeoutAt,
|
|
153
|
+
result, payload, createdAt, updatedAt }`.
|
|
154
|
+
|
|
155
|
+
No answer embeds a list: a run does not carry its activities, an activity does
|
|
156
|
+
not carry its pendings or the runs it started. Each is its own paged route,
|
|
157
|
+
newest first, and the trace reads one page at a time when the reader gets
|
|
158
|
+
there: the activities of the run, the pendings of an open async activity, and
|
|
159
|
+
the runs an activity started as `GET /runs?parentActivityId=…`, 15 at a time
|
|
160
|
+
behind "show more", since a `startWorkflowMany` can fan out to hundreds of
|
|
161
|
+
thousands. A sync activity records no completion: the trace
|
|
162
|
+
takes the next activity starting, or the run ending, as its end. Every listing
|
|
163
|
+
is newest first, logs included.
|
|
164
|
+
|
|
165
|
+
`q` is opaque to the panel: it sends `fields.run` and `fields.runActivity`
|
|
166
|
+
from its configuration, verbatim, and a host that answers the full shape
|
|
167
|
+
without one (the local dev server) leaves them out. The panel reads a run's
|
|
168
|
+
activities as summaries and asks for one activity's input, output, pendings
|
|
169
|
+
and children only when the reader opens it, so a trace of hundreds of
|
|
170
|
+
activities costs one request until then.
|
|
171
|
+
|
|
172
|
+
## Developing the panel
|
|
173
|
+
|
|
174
|
+
Inside the SPA every read of the API is a TanStack Query (`src/queries.ts`,
|
|
175
|
+
which also holds the mutations and the cache writes each server event turns
|
|
176
|
+
into) and the single `EventSource` lives in `src/events.tsx`: nothing
|
|
177
|
+
subscribes to events on its own — a component reads a query and the stream
|
|
178
|
+
keeps it current; nothing polls, and a host with `capabilities.events` off
|
|
179
|
+
gets a Refresh button in the top bar that invalidates every query. The filters of the runs page are
|
|
180
|
+
typed URL state through nuqs (`src/filters.ts`), which is where the query
|
|
181
|
+
string of a shared link is defined.
|
|
182
|
+
|
|
183
|
+
Every instant on screen is measured by the server; what is still going counts
|
|
184
|
+
against the browser clock through `useTimer` (`src/hooks/useTimer.ts`), one
|
|
185
|
+
timer per rate shared by every counter, read in the leaf that shows the number
|
|
186
|
+
so a tick never re-renders the page around it. `yarn test` covers the three.
|
|
187
|
+
|
|
188
|
+
`yarn dev` starts Vite on the panel sources with `/_dev` proxied to a dev
|
|
189
|
+
server on port 3000, so run `framework dev` on any project first. `yarn build`
|
|
190
|
+
produces `dist/client` (the SPA, validated by the Vite build) and the two entry
|
|
191
|
+
points `dist/index.js` (the Vite plugin) and `dist/node.js` (the Vite-free
|
|
192
|
+
handler), type-checked by `check-types`; the published package ships all of
|
|
193
|
+
them.
|