Skip to content

Repository files navigation

Schema-Driven Admin Console

A React + TypeScript admin console that discovers backend services entirely from a metadata document (schema.json) each service exposes. The frontend holds no knowledge of any specific service, resource, or field — install a service, and its resources, table columns, actions, and realtime updates all come from what it declares.

CRUD data-fetching, pagination, and per-record/collection actions are now implemented on top of @ferrumec/dashboard (in packages/dashboard) rather than hand-rolled react-query hooks and a manually-built @tanstack/react-table instance. See Architecture below for what that changed.

Setup

npm install   # resolves packages/dashboard via the npm workspace
npm run dev

Requires Node 18+. This was written without network access to run npm install in the sandbox it was built in, so if a dependency version resolves to something incompatible, loosen the pin in package.json and reinstall.

Try it with the mock service

A tiny mock backend implementing examples/example-schema.json is included so you can see the console work end-to-end without wiring up a real service first.

npm i -D ws
node examples/mock-server.mjs

Then, in the console, click Install service and enter:

http://localhost:4001/schema.json

It emits a random product price update over WebSocket every few seconds — watch the row update live with no refresh.

Wiring up a real service

Any backend can plug in by exposing:

  1. A schema.json (or any path — you supply the URL) matching the contract in src/types/metadata.ts. This wire format is unchanged — it's the console's own service-discovery schema, separate from @ferrumec/dashboard's ResourceSchema/ItemSchema. src/lib/resourceAdapter.ts is what translates one into the other.
  2. The REST endpoints it declares under endpoints (list is required for a resource to render; create/update/delete/subscribe are all optional).
  3. Optionally, a WebSocket endpoint that broadcasts { topic, operation, key, data } messages for realtime resources.

For a Rust/Actix backend, the schema.json can be served as a static file or generated from route metadata; the endpoints it points to are just your normal REST handlers — nothing console-specific is required on the backend beyond returning { data, total } (or a plain array) from list endpoints.

Architecture

  • src/types/metadata.ts — the metadata contract fetched from each installed service. Unchanged from before — every other module is typed against this, never against a concrete resource shape.
  • src/lib/metadataValidation.ts — validates and rejects malformed or version-mismatched schema.json documents before they reach the UI. Unchanged.
  • src/lib/api.ts — now just fetchServiceMetadata/ApiError. The old fetchResourceList/executeAction/deleteRow are gone; that logic now lives inside the generated ResourceApi/ItemApi/actions built by resourceAdapter.ts.
  • src/lib/resourceAdapter.ts (new) — the bridge. Turns one declared resource (fields, endpoints, actions, capabilities) into a real @ferrumec/dashboard Resource: field metadata (with format reusing the existing cellRenderers.tsx so currency/status/image/etc. render exactly as before), a ResourceApi wired to the declared REST endpoints with framework-native pagination, and an ActionMap per declared ResourceAction (row-scoped actions get a per-record map via itemActions, global ones via actions). This is the only place in the app that calls fetch for CRUD.
  • src/lib/resourceRegistry.ts (new) — caches one ResourceController per (service, resource), rebuilt if the declared schema changes (e.g. via MetadataLoader's background refresh).
  • src/store/useAdminStore.ts — simplified. Per-resource table UI state (page/pageSize/search/sort) is gone entirely — each ResourceController now owns that itself. The store is back to just installed services and the current selection.
  • src/components/table/ResourceListLayout.tsx (new) — a ListLayoutComponent for @ferrumec/dashboard, replacing GenericTable.tsx + columns.tsx + TableToolbar.tsx. Reuses the same cellRenderers.tsx, SearchBar.tsx, Pagination.tsx, and ActionButton UI as before; sortable column headers now drive the framework's actions.setQuery.
  • src/components/table/cellRenderers.tsx — unchanged; still renders a cell from nothing but a field's declared type.
  • src/components/actions/actionExecutor.ts / ActionButton.tsx — decoupled from service/endpoint construction. They only handle confirm/pending/error UX now; run is whatever the framework generated (actions.runAction / actions.runItemAction).
  • src/pages/ResourcePage.tsx — now just looks up a cached ResourceController and renders controller.ListView({ layout: ResourceListLayout }). Realtime events debounce into a controller.refresh() rather than patching a react-query cache directly — see the comment in that file for why.
  • src/hooks/useUrlSync.ts — unchanged; keeps /services/:serviceId/:resourceName in the address bar in sync with the store.

Known trade-offs from the rewrite

  • Realtime is coarser. The old applyRealtimeEvent patched the react-query cache in place per event. @ferrumec/dashboard's Resource store isn't mutable from outside a Resource — only refresh() is public — so a realtime event now triggers a debounced full refetch of the current page instead. Fine for the mock server's occasional price update; worth revisiting if a service pushes high-frequency events.
  • The resource-controller cache doesn't evict. resourceRegistry.ts keys on a schema fingerprint so a background metadata refresh gets a fresh controller, but old entries stay in the Map. Not a problem for a console with a handful of installed services in one session.
  • No detail/update views yet. The console never had them (table + actions only), so the rewrite doesn't add them — but resource.item(id) now exists on every ResourceController, so wiring up DetailView/ UpdateView for a "row → detail drawer" feature is a much smaller change than it would have been before.

Extending

  • Forms / detail drawersresource.item(id).DetailView() / .UpdateView() are already available from @ferrumec/dashboard; write a layout component (mirroring ResourceListLayout.tsx) and wire it to a row click in ResourceListLayout's navigation.viewItem.
  • Bulk actions / CSV exportresource.capabilities.export is already read by ResourceListLayout; wire the button to a CSV serializer over the current page, or declare it as a global ResourceAction and it'll show up automatically via actions.runAction.
  • Auth — a fetch wrapper in resourceAdapter.ts is the single choke point for adding auth headers/token refresh across every service.
  • Multiple realtime transportsResourceRealtime.transport is already a discriminant; add an SSE implementation alongside websocketManager.ts and branch on it in useRealtimeResource.ts.

About

schema driven admin dashboard

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages