Skip to content

[plugin] Split the dashboard into a list route and a run route #264

Description

@gabiudrescu

What the admin does today

One route answers two questions. /admin/durable/dashboard renders the run list and one run's history side by side, and ?run= picks which run fills the right column. When nobody picks, RunDashboard::pick() falls back to $runs[0].

Image

Two problems follow from the single route, and neither is a styling choice:

  1. The run has no address. A run page is a thing an operator sends to a colleague: "look at order 4711". Today the link is ?status=…&run=…&cursor=…, and it only resolves while that run is still on that page of that filter. Pass a cursor from yesterday and the link shows a different run.
  2. Both panels pay for each other. The list is squeezed into a third of the width, so it shows a workflow name, a run id and a date and nothing else. The history gets two thirds, which is not enough for a frieze plus its events. And every list load reads a history nobody asked for - build() always calls readHistory() for the fallback run.

What we built instead

Two routes in our own Sylius app, over the same RunDashboard facts:

  • /admin/durable/runs - the list, full width.
  • /admin/durable/runs/{runId} - one run, full width.
Image Image

What got better, concretely

  • A run id is a URL. /admin/durable/runs/order-000000017 resolves on its own, forever. No cursor, no filter, no page. It is the link you paste into a ticket.
  • The list became a real grid. Full width paid for two more columns (Ended, and an Actions column) and for the Sylius grid shell: table card-table table-vcenter, the same badge palette the Orders grid uses, the same btn btn-icon + tabler:eye action. A shop admin reads it without learning anything new.
  • Filters became worth having. With the width, we added Workflow and Run alongside Outcome, in the Sylius filter accordion. Finding a run stopped meaning "page until it appears".
  • The run page can be semantic. Freed of the list, it opens with four fact cards - outcome, started, ended, took - then the frieze, then "Step by step". The frieze labels stop being truncated at 12rem.
  • One wasted query per list load disappeared. No fallback run means no readHistory() nobody asked for.
  • 404 became possible. An unknown run id is an unknown run id. Under ?run=, an id that matches nothing silently shows the first run of the page instead.

What the port made hard

Both gaps are in WorkflowRunCatalogInterface, and both pushed us out of the library:

  1. No findRun(string $runId). readHistory() needs a WorkflowRunDescription, and only listRuns() produces one. Our first version of the show route paged through listRuns() until the id matched, capped at ten pages - so a run older than 200 runs was a 404 that should have been a page.
  2. listRuns() filters by outcome only. Filtering a cursor page in PHP shows an empty page while the matches wait on the next one, so a workflow or run-id filter cannot be built on the port at all.

We ended up querying durable_workflow_runs directly - same ordering, same cursor format, same UTC reading of started_at as DbalWorkflowRunCatalog, copied so the two pages agree. That is ~160 lines of application code duplicating bridge SQL, and it only works because we happen to run the DBAL backend. A Temporal shop could not copy it.

Proposal

  1. Split the plugin into gplanchat_durable_plugin_admin_run_index (/admin/durable/runs) and gplanchat_durable_plugin_admin_run_show (/admin/durable/runs/{runId}). Keep /admin/durable/dashboard as a redirect to the list so existing bookmarks survive.
  2. Add findRun(string $runId): ?WorkflowRunDescription to WorkflowRunCatalogInterface. One query on DBAL, one DescribeWorkflowExecution on Temporal.
  3. Widen listRuns(): ?string $workflowName and ?string $runIdFragment next to $status. Both are one WHERE clause on DBAL and both exist in Temporal's list filter.
  4. Split RunDashboard::build() in two - listing() without the selected run, run() for one - so a host that renders two pages does not read a history it discards. outcomeCounters() is already public and already reusable as is.

Point 1 alone is a plugin change. Points 2 to 4 are what make it buildable by anyone else without writing SQL against your tables.

Happy to send the split as a PR against the plugin if the shape above is the one you want.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions