khalin/nova4-indicator-field
A Laravel Nova 4 field for displaying compact status indicators in resource tables and detail views. Render colored dots, labels, and conditional states with a simple API—handy for showing boolean flags, health checks, or workflow status at a glance.
Verify it resolves before writing any resource code. The package name (khalin/nova4-indicator-field) and the repository owner (oleghalin) do not match, and two prior assessments disagree on whether a service provider exists. Two minutes of checking saves an afternoon:
composer show khalin/nova4-indicator-field --all
If Packagist returns nothing, stop and add a VCS repository entry or vendor the source directly — either is a signal worth heeding before compiled JavaScript runs inside an authenticated admin panel.
composer require khalin/nova4-indicator-field
Read vendor/ before trusting the README. The documented API is thin. Confirm three things in the source: (1) whether a service provider is auto-discovered, (2) whether the tagged release ships a dist/ folder with compiled assets, and (3) the exact method used to set the color map. The examples below use options(), but verify — a wrong method name fails silently as an unstyled field.
use Khalin\Nova4IndicatorField\Indicator;
public function fields(NovaRequest $request)
{
return [
Indicator::make('Status')
->options([
'active' => 'green',
'pending' => 'amber',
'failed' => 'red',
'cancelled' => 'gray',
])
->exceptOnForms(),
];
}
First use case: swap a status column on the index view. Pick a resource where operators scan a list — orders, jobs, subscriptions, tickets — and replace Text::make('Status') with the indicator. That is the entire value proposition: a colored dot lets someone triage a hundred rows without reading every cell. Get it working end to end, including a real deploy (not just artisan serve), before adding labels, tooltips, or detail-view variants.
Centralize the color map once. The map is effectively the whole configuration surface. If you let each resource define its own, you will end up with three subtly different definitions of "amber." A small helper class pays for itself on the first lens:
final class StatusIndicator
{
public const COLORS = [
'active' => 'green',
'pending' => 'amber',
'failed' => 'red',
'cancelled' => 'gray',
];
public static function make(string $name = 'Status'): Indicator
{
return Indicator::make($name)->options(self::COLORS);
}
}
Normalize values with resolveUsing. Enums, integers, and nullable booleans all need to become the string key the indicator understands. resolveUsing is a standard Nova field method, so it composes normally:
StatusIndicator::make()
->resolveUsing(fn ($value) => $value?->value ?? $value ?? 'unknown')
->options(StatusIndicator::COLORS + ['unknown' => 'gray']);
Keep normalization in resolveUsing, not in an Eloquent accessor — sorting and filtering still need to hit the real database column, and an accessor that rewrites the value will confuse both.
Always define a fallback color. Unmapped values are the most common production surprise: a status added by a migration, a legacy row, a null. Decide deliberately whether an unknown value renders blank or a neutral gray, and add the unknown key up front.
Keep indicators off forms. They are read-only presentation. ->exceptOnForms() (or the equivalent in the version you install) keeps them off create/update screens where they would render as dead decoration next to the real status input.
Use labels and tooltips on the detail view only. On an index row a dot is a scan aid. On a detail page the same field should carry a label and secondary context. Verbose tooltips in index tables add hover noise to a view people scroll past quickly.
Budget the query cost before it reaches the index. Every row on the index evaluates the field. If the value comes from a relation or an aggregate, you have an N+1 hidden behind a colored dot. Eager load or aggregate in indexQuery:
public static function indexQuery(NovaRequest $request, $query)
{
return $query->withCount('openTickets');
}
If the underlying number is genuinely expensive, compute it in a Nova metric with caching and present it elsewhere — do not let a display field drive query cost.
Gate visibility with canSee where the status is sensitive. Indicator color can leak information (e.g., a "flagged" state) to users who should not see it. Wrap the field in ->canSee(...) rather than relying on hiding the resource.
Reuse it across lenses and detail views. A lens that returns the same resources gets the same field definition, so extract the factory method and reference it from both fields() and any shared field sets. This is the payoff of centralizing the map.
Add a smoke test. With zero visible CI upstream, nobody else has exercised this code path. One feature test asserting the field renders on a resource's index and detail pages is the minimum bar:
public function test_status_indicator_renders_on_index()
{
$this->actingAs(User::factory()->create())
->get(Nova::url('/resources/orders'))
->assertOk()
->assertSee('Status');
}
Pair it with a visual check in your release checklist for any page using the indicator.
Vendor it if the spike survives. Given the maintenance posture — single author, last release April 2022, zero dependents — the sustainable pattern is to copy the field class and Vue component into app/Nova/Fields/ under your own namespace, preserving MIT attribution, and register the asset yourself. The surface is small enough to be hours of work and removes the upgrade ceiling.
Pin Nova itself and re-validate on every minor bump. Nothing indicates validation past Nova 4.x. Field packages couple tightly to Nova's internal front-end contracts — Vue component registration, the Nova.mix() asset pipeline, the laravel-nova dev dependency. A Nova patch release is the most likely breaker, and the failure mode is a broken admin page, not graceful degradation. Add a line to your Nova upgrade runbook.
dist/ may be missing from the tag. Many Nova field repos are source-only and require an npm build against laravel-nova. Check the tagged archive before assuming composer require is enough — 404'd compiled assets after a deploy is the classic failure mode here.
Confirm Vue 3 cleanliness. Nova 4 is Vue 3. A 2022-era field may carry Vue 2 residue (new Vue(), filters, $listeners). Read resources/js/. If it is Vue 2 code, treat the repo as a reference implementation rather than a dependency.
Grep for v-html on interpolated values. The component renders inside an authenticated admin panel with privileged reach. If it uses v-html on model data instead of text interpolation, any user-influenced value in that column is an XSS vector against your admins.
Color strings must match the component's own vocabulary. green / amber / red are the component's set, not arbitrary Tailwind classes. Passing emerald-500 or a hex value renders wrong or unstyled, silently. Establish the accepted set from the source before documenting it for your team.
The service-provider question decides asset loading. If auto-discovery handles it, you are done. If not, you must register Nova::script() / Nova::style() manually — especially after vendoring. Either way, make the registration visible in your own code so the behavior is obvious to the next developer.
Clear caches when a field renders empty or unstyled. php artisan config:clear && php artisan nova:publish — a stale asset manifest looks exactly like a broken package.
Sorting and filtering hit the column, not the dot. The indicator is presentation. If operators need to sort by status or filter by it, keep the underlying column intact and confirm Nova's sortable/filterable wiring still targets it. Do not expect the field to add filter UI of its own.
Decide the Nova 5 posture now. If Nova 5 is anywhere on your roadmap, do not port this — build the equivalent against the current Nova field API. Making that call after shipping it across a dozen resources is the expensive version of the same decision.
How can I help you explore Laravel packages today?