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 Table Card Laravel Package

whitespacecode/nova-table-card

Nova Table Card adds a flexible table card to Laravel Nova dashboards, letting you display custom rows and columns from any data source. Configure headers, values, and formatting to build compact, readable summaries without leaving Nova.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the precondition first. This package is useless outside Laravel Nova — it is a Nova card, not a generic table widget. Check composer.json in the installed package for the exact nova, laravel/framework, and php constraints before anything else. Release 1.1.0 (2024-12-16) added Nova 5 support; whether Nova 4 remained supported after that minor bump is an inference from the changelog, not a documented support matrix. Read the constraint floor rather than assuming it.

composer require whitespacecode/nova-table-card

No migrations, no queues, no services. There is typically nothing to publish for a baseline card — if the package ships a config or views, php artisan vendor:publish --provider="WhitespaceCode\NovaTableCard\..." will list them; check vendor/whitespacecode/nova-table-card/ for a config/ or resources/ directory before assuming there isn't one.

Where to look first. Read the package's own card class (usually src/TableCard.php or similar) — that is the real API documentation for a 10-star package. You are looking for three things: the method that accepts the data source (query/builder/collection/array), the method that defines columns, and any limit/sort methods. Confirm whether limit and sort are applied server-side in SQL or client-side in the Vue component; that single detail determines whether the card is safe on a large table.

Your first card, minimal shape. Create a class in app/Nova/Cards/ that extends the package card. The method names below follow the package's documented capabilities (columns with labels and callbacks, query or array input, optional limit and sorting) — verify the exact signatures against the source before committing.

namespace App\Nova\Cards;

use WhitespaceCode\NovaTableCard\TableCard; // verify namespace

class RecentOrders extends TableCard
{
    public function __construct()
    {
        parent::__construct();

        $this->width('1/2');
    }

    public function source()
    {
        return \App\Models\Order::query()
            ->with('customer')
            ->latest()
            ->limit(10);
    }

    public function columns(): array
    {
        return [
            ['label' => 'Order',   'value' => fn ($o) => $o->reference],
            ['label' => 'Customer','value' => fn ($o) => $o->customer?->name ?? '—'],
            ['label' => 'Total',   'value' => fn ($o) => number_format($o->total / 100, 2)],
            ['label' => 'Placed',  'value' => fn ($o) => $o->created_at->diffForHumans()],
        ];
    }
}

Register it. For a dashboard card, add it to NovaServiceProvider::cards() or to a cards() array inside a dashboard class. For a resource-detail card, register it in the resource's cards(Request $request) method instead — same class, different mount point.

// app/Providers/NovaServiceProvider.php
public function cards()
{
    return [
        new \App\Nova\Cards\RecentOrders,
    ];
}

First use case to ship: a read-only "last N records" table on the main dashboard. It is the lowest-risk probe of whether the card earns its place, and it is under a day of work.

Implementation Patterns

Wrap the package in a base class before you write card two. Because you cannot control the maintainer's release cadence, isolate the coupling in one place:

namespace App\Nova\Cards;

abstract class BaseTableCard extends \WhitespaceCode\NovaTableCard\TableCard
{
    protected function scopedQuery(string $model)
    {
        return $model::query()
            ->when(auth()->user()->tenant_id, fn ($q, $id) => $q->where('tenant_id', $id));
    }
}

Every concrete card extends BaseTableCard. If the package is abandoned, or a Nova major breaks it, you replace one class instead of auditing every dashboard.

Push the query into a dedicated class, not the card. The card is a view concern; the data concern belongs in an invokable query object or a model scope so it is testable without booting Nova's HTTP stack.

class RecentOrdersQuery
{
    public function forUser(User $user)
    {
        return Order::query()
            ->where('tenant_id', $user->tenant_id)
            ->with('customer:id,name')
            ->latest()
            ->limit(10);
    }
}

The card's data method becomes a one-liner delegating to that class. You can now unit-test authorisation scoping with actingAs($restrictedUser) and asserting on the SQL result, independent of the card's rendering.

Column callbacks: return scalars, always. Cards serialise their payload to JSON for the Vue component. Returning an Eloquent model, a raw Carbon instance you have not formatted, or a collection will at best render [object Object] and at worst bloat the payload. Format dates and money into strings inside the callback. Truncate long text there too (Str::limit($post->title, 40)) rather than fighting column width in CSS.

Use the array/collection input for data that is not a single Eloquent query. Aggregate rows (daily counts across a union), statically defined status tables, or data from an external API call all work — but compute them in a service class and pass the finished array. Do not run multi-query reporting logic inside the card class; it becomes untestable and re-runs on every dashboard refresh.

Detail-screen cards should be pre-filtered by the resource. In a resource's cards() method you get the request; extract the resource ID from the route and scope the card's query to the parent record. Do not rely on a generic "latest N" query on a detail screen — users will read it as belonging to the record and draw the wrong conclusion.

Always apply limit server-side and always eager-load. A card without a limit on a table with 200k rows will eventually be the slowest thing on the dashboard, and the failure is silent until someone complains the admin is sluggish. Pair ->with() against the columns you actually display; an N+1 inside a card renders fine and is invisible in normal monitoring.

Test the negative case explicitly. Write a feature or unit test that logs in a user who can see the dashboard but lacks viewAny on the underlying resource, hits the dashboard, and asserts the restricted rows are absent. There is no natural alarm for this failure — the card renders correctly and the query succeeds — so the only detection mechanism is a test you wrote on purpose.

Respect the scope boundary in code review. The moment a card grows filters, search, pagination, CSV export, or a date-range picker, you have started building a reporting surface on top of a presentation widget. Flag that in review and escalate; those requirements belong in a real reporting tool.

Gotchas and Tips

Authorization is the sharpest edge. Dashboard cards execute a query outside Nova's normal index request lifecycle. If the card does not honour authorizedToViewAny and your index-query scoping, a user who can see the dashboard but not the resource can see rows they should not. Do not assume the package handles it — verify with a restricted user and real data, and treat a failure as a launch blocker rather than a follow-up ticket. The safest pattern is a mandatory tenant/team scope applied inside your BaseTableCard, which holds regardless of what the package does with Nova's authorisation hooks.

Pin the version and sequence your Nova upgrades. Never bump Nova and this card in the same deploy — if the dashboard breaks you will not know which side caused it. Upgrade Nova first, let it stabilise, then bump the card. "whitespacecode/nova-table-card": "1.1.0" in composer.json, not a caret range you forget about.

Blank or unstyled card after a Nova point release is the signature failure. Nova cards ship compiled front-end assets; a mismatch between the card's compiled JS and Nova's runtime presents as an empty card or a console error, not a clean PHP exception. Debugging path: hard-refresh the browser, check composer show whitespacecode/nova-table-card against your Nova version, then open the browser console — the PHP side will look perfectly healthy while the Vue side has thrown. Budget for this being opaque; the package gives you no server-side trace of it.

Re-check whether "limit" is real. If the package's limit is applied in the Vue component rather than in SQL, the card is fetching your entire table on every load. Test by logging the executed query (Laravel Debugbar or DB::listen) with a limit set. If the limit is client-side, add ->limit() to the query yourself and never rely on the card's.

Every dashboard load is a query cost. If the card supports polling, that cost multiplies by the interval and by the number of users with the dashboard open. For expensive queries, wrap the result in Cache::remember() with a short TTL (30–60s) inside your query class — a dashboard card does not need second-level freshness, and a cached card that is a minute stale is strictly better than a dashboard that times out at 9am.

Column callbacks and escaping. If callbacks can return markup, the XSS surface is the data, not the column definition. Prefer plain-text formatting. If you need links, check whether the package exposes a link/URL column option before reaching for raw HTML, and never pass user-supplied content through unescaped.

Extension point to prefer over forking. Subclass the card class and override the column/source/limit methods in your BaseTableCard. Forking the vendor package means you own the Vue asset build pipeline — the exact two-week cost you adopted this package to avoid. If you must fork, keep it to composer.json path-repo on a repo that changes only the PHP card class, and never touch the compiled JS.

Rollback is genuine. composer remove whitespacecode/nova-table-card, delete your card classes, drop the registration lines. There is no datastore, migration, or background process to clean up. This reversibility is the main argument for trying it — which is also the reason to keep the integration shallow enough that reversal stays trivial.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core