Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Filterable Cards Laravel Package

beyondcode/nova-filterable-cards

Nova Filterable Cards lets you add interactive, filterable cards to Laravel Nova dashboards. Define metrics and filters so users can quickly narrow results by date range or custom criteria, making dashboard insights more flexible. Simple to configure and ideal for dashboards that need more than stat...

View on GitHub
Deep Wiki
Context7

Getting Started

Run the compatibility check before anything else. The last release was August 2018 and the repository is archived. Its composer.json pins php ^7.1, laravel/framework 5.6|5.7, and laravel/nova ^1.0. Do this first:

composer require beyondcode/nova-filterable-cards --dry-run

If resolution fails on PHP 8 / Laravel 10+ / Nova 3+, stop. --ignore-platform-reqs only moves the failure from Composer to a blank card in production. In that case your day-to-day use of this package is as MIT-licensed source material: it is small and conventional, and the whole mechanism is readable in an afternoon.

Where to look first, in order:

  1. src/ — the PHP base card class. This is the payload: it reads Nova's filters parameter, decodes it, and replays each filter's apply() against the card's query.
  2. resources/js/ — the Vue 2 component injected into the card header. Skip this if you are on Nova 4+ (Vue 3); rewrite the control in Card.vue instead.
  3. composer.json — confirm the version constraints so you know exactly which Nova major the PHP side was written against.
  4. NovaServiceProvider in your app, not the package — there is no service provider asset registration here. You register cards the way you always do.

First use case — one metric, one date range. Register a normal Nova filter and attach it to a card, so the dashboard-filter and card-filter vocabulary are the same class:

// app/Nova/NovaServiceProvider.php
public function filters()
{
    return [new \App\Nova\Filters\OrderDateRange];
}

public function cards()
{
    return [
        (new \App\Nova\Metrics\TotalRevenue)->width('1/3'),
    ];
}
// app/Nova/Filters/OrderDateRange.php
class OrderDateRange extends Filter
{
    public function apply(Request $request, $query, $value)
    {
        [$from, $to] = array_pad(explode(' - ', (string) $value), 2, null);

        return $query->whereBetween('created_at', [
            Carbon::parse($from)->startOfDay(),
            Carbon::parse($to)->endOfDay(),
        ]);
    }

    public function options(Request $request)
    {
        return [
            'Last 7 days'  => now()->subDays(7)->toDateString().' - '.now()->toDateString(),
            'Last 30 days' => now()->subDays(30)->toDateString().' - '.now()->toDateString(),
        ];
    }
}

The card re-runs its query, the filter's apply() scopes it, and the header control reflects the active value. That single "Monthly Revenue, scoped to a date range" card is the reason to read this repository at all.

Implementation Patterns

The core mechanism, in about ten lines. Nova sends the dashboard's active filters to card and metric endpoints as a base64-encoded JSON array of {class, value} pairs. The package's whole trick is decoding that and replaying it. If you reimplement, this is the shape:

protected function applyFilters(NovaRequest $request, $query)
{
    $filters = json_decode(base64_decode($request->input('filters', '')), true) ?: [];

    foreach ($filters as $filter) {
        if (! is_subclass_of($filter['class'] ?? '', Filter::class)) {
            continue; // never instantiate arbitrary classes
        }

        (new $filter['class'])->apply($request, $query, $filter['value']);
    }

    return $query;
}

On Nova 4+ prefer the framework's Laravel\Nova\FilterDecoder rather than hand-rolling the base64 step.

Apply filters where the query is built, not where it is counted. Nova's Value/Trend/Partition metrics build their query in calculate(). The durable pattern is a small subclass that scopes the query in one place:

abstract class FilterableValue extends Value
{
    public function calculate(NovaRequest $request)
    {
        $query = $this->applyFilters($request, User::query());

        return $this->count($request, $query);
    }
}

Every metric then extends FilterableValue and inherits filter support with no per-metric code. This is the pattern to copy even if you never install the package.

Reuse the same filter classes in filters() and on cards. The dashboard filter bar and individual cards then share one source of truth for "what is a valid date range," which is the cheapest way to stop metrics disagreeing with resource lists.

Compose with native Nova rather than replacing it. On Nova 3+, refreshWhenFiltersChange() on the card already re-fetches when a global dashboard filter changes. Use that for the coarse "everyone on this dashboard" scope, and reserve per-card filters for the one or two cards that need an independent slice. If native refresh already does the job, the card-level control is decoration.

Scope by policy inside the filter, not the card. Tenant and permission scoping belongs in apply():

$query->where('tenant_id', $request->user()->tenant_id);

A card is a query builder with an HTTP endpoint behind it; the filter is the last place you get to constrain it before data reaches the browser.

Cache the expensive ones. Each card is its own aggregate query and re-runs on every filter change. Key on the filter payload plus user:

$key = 'card:revenue:'.$request->user()->tenant_id.':'.md5($request->input('filters', ''));

return Cache::remember($key, now()->addMinutes(5), fn () => $this->sum($request, $query, 'amount'));

Pass filter options from PHP to the Vue control via withMeta() rather than hard-coding choices in JavaScript — it keeps selectable ranges and plan lists server-driven and testable.

Gotchas and Tips

The apply() signature is the thing that breaks on upgrade. Nova's Filter contract has changed across majors (value argument, request type). Check the actual signature in your installed Nova before lifting the package's filter classes wholesale; a mismatch is a silent no-op or a fatal error, depending on PHP version.

Filter values arrive as strings. Everything the front end sends is a string, including date ranges that arrive as "2024-01-01 - 2024-01-31". Parse with array_pad(explode(' - ', $value), 2, null) and cast explicitly; a null end date silently turning into whereBetween('created_at', ['2024-01-01', null]) returns zero rows and reads as "no revenue."

Date boundaries and timezones. endOfDay() on a UTC-stored created_at compared against a user's local date is off by hours, and whereBetween is inclusive on both ends — a record at 23:59:59.999 may land in two adjacent buckets. Decide inclusive/exclusive up front and write one helper used by every filter.

Class names are persisted in the URL. The filters payload carries the filter's FQCN. Rename or move App\Nova\Filters\OrderDateRange and every saved dashboard link or bookmarked query state breaks. Treat filter class names as API surface.

Decode defensively. Unknown or uninstantiable classes must be skipped with a log line, not new $class'd blind. Nova's filter payload reaches the server from the client; treat it as untrusted input.

Do not reference the GitHub URL as a vcs Composer repository. The repository is archived. If it disappears, every build in the estate breaks. Vendor the code into your own VCS with the MIT header intact and rewrite the constraints to your owned PHP/Laravel/Nova ranges, or reimplement.

Blank cards are the characteristic failure. A Nova upgrade that leaves a Vue 2 component running under Vue 3 renders nothing — no error, no console noise, just an empty rectangle that passes code review. Add a smoke assertion in staging that each dashboard card returns a non-null payload after upgrades.

Expect the latency to show up on the dashboard, not in your APM. With six cards and a filter bar, one click is six aggregate queries over potentially large tables. Index the filtered timestamp columns (created_at, paid_at) and cap or debounce refresh before rolling this across a finance or ops dashboard.

Extension points worth knowing: the base card's query hook (where filters are applied), withMeta() (server-side options for the Vue control), and canSee() (per-card visibility by role — the cleanest way to keep a sensitive revenue card off the CS dashboard). Everything else is boilerplate you can delete.

Ramp-up reality: a mid-level Laravel developer reads this codebase in under a day. That is also the argument against depending on it — the effort to understand it is most of the effort to replace it, and replacing it removes the archived-upstream risk entirely.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource