Skip to content

Custom cards

Custom cards

Any package (or the app itself) can contribute pages and cards. A card is a Livewire component: PHP + Blade, no JavaScript build, using whatever PromQL/TraceQL/LogQL fits — including metrics and spans the core UI knows nothing about.

// e.g. in cboxdk/queue-autoscale's service provider
use Cbox\TelemetryUi\Facades\TelemetryUi;

public function boot(): void
{
    if (class_exists(TelemetryUi::class)) {
        TelemetryUi::page('autoscale', 'Autoscale', group: 'Activity');
        TelemetryUi::card(\Cbox\QueueAutoscale\Ui\ScalingDecisions::class, page: 'autoscale');
        TelemetryUi::card(\Cbox\QueueAutoscale\Ui\WorkerFleet::class, page: 'autoscale');
    }
}

That's all the wiring you need — no routes to register. The dashboard's catch-all route resolves any registered page slug to a generic page view that renders the page's cards, so /telemetry-ui/autoscale works the moment you page('autoscale', …). Register the cards on that slug and they appear.

page() also takes an optional detectMetric: name pattern: the page only shows when a matching metric exists in the backend — the same autodetection the built-in Statamic group uses. group: places it under a sidebar heading, icon: sets its glyph.

use Cbox\TelemetryUi\Cards\Card;

final class ScalingDecisions extends Card
{
    public function render(): View
    {
        [$start, $end] = $this->range();

        return view('queue-autoscale::cards.scaling-decisions', [
            'series' => $this->toChartSeries($this->metrics()->queryRange(
                'sum by (queue) (rate(autoscale_scaling_events_total[5m])) * 60',
                $start, $end,
            ), label: 'queue'),
        ]);
    }
}
<x-telemetry-ui::card title="Scaling decisions" span="2">
    <x-telemetry-ui::chart :series="$series" type="bar" unit="events/min" />
</x-telemetry-ui::card>

Interactive cards

Cards are ordinary Livewire components, so actions and forms just work:

final class ExceptionActions extends Card
{
    public function createTicket(string $exceptionClass): void
    {
        // call Linear/GitHub, dispatch a job, open a modal…
    }
}

Long-running/AI output can stream with wire:stream. This is the intended home for ticket-creation and AI-resolve flows on the roadmap.

Conventions

  • Query through $this->metrics() / $this->traces() / $this->logs() so named connections and tenancy keep working.
  • Catch SourceException and render an inline error state; a broken backend must not take the page down.
  • Respect $this->range(); don't hardcode time windows.

Add, replace, remove

The registry supports more than appending — useful for white-labelling or embedding, where you want to reshape the built-in dashboard:

use Cbox\TelemetryUi\Facades\TelemetryUi;

// Add — append a card to any page (default: the dashboard).
TelemetryUi::card(MyCard::class, page: 'requests');

// Replace — swap a page's whole card list for your own (a branded dashboard).
TelemetryUi::setCards('dashboard', [MyHeadline::class, MyChart::class]);

// Remove — drop a single built-in card…
TelemetryUi::removeCard(\Cbox\TelemetryUi\Cards\Builtin\JobsOverview::class, 'dashboard');

// …or a whole page/section from the sidebar + routing.
TelemetryUi::removePage('users');

Re-registering a page slug with TelemetryUi::page(...) overwrites it, so you can relabel or regroup a built-in page too. To extend a built-in card instead of replacing it, subclass it (the built-in overview cards are not final — the detail pages do exactly this) and register your subclass.