83 - Isolate dashboard panels with Livewire islands

This commit is contained in:
myrmidex 2026-08-12 00:20:46 +02:00
parent a6eff35586
commit 8a98990aea
3 changed files with 103 additions and 6 deletions

View file

@ -21,6 +21,13 @@ public function mount(): void
$this->applyPreset('today');
}
/**
* Islands whose content depends on the global range; skipped islands never re-render on their own.
*
* @var array<int, string>
*/
private const RANGE_DEPENDENT_ISLANDS = ['article-statistics'];
public function applyPreset(string $preset): void
{
try {
@ -29,13 +36,29 @@ public function applyPreset(string $preset): void
return;
}
$this->from = $range->from->toDateString();
$this->to = $range->to->toDateString();
$this->applyRange($range->from->toDateString(), $range->to->toDateString());
}
public function applyRange(string $from, string $to): void
{
$this->from = $from;
$this->to = $to;
$this->validateRange();
$this->refreshRangeDependentIslands();
}
private function refreshRangeDependentIslands(): void
{
foreach (self::RANGE_DEPENDENT_ISLANDS as $island) {
$this->renderIsland(name: $island);
}
}
/**
* @return array<string, string>
*/
#[Computed]
public function presets(): array
{
return DateRange::presets();
@ -100,6 +123,7 @@ public function updated(string $property): void
{
if (in_array($property, ['from', 'to'], true)) {
$this->validateRange();
$this->refreshRangeDependentIslands();
}
}
@ -127,8 +151,6 @@ private function validateRange(): void
public function render(): View
{
return view('livewire.dashboard', [
'presets' => $this->presets(),
])->layout('layouts.app');
return view('livewire.dashboard')->layout('layouts.app');
}
}

View file

@ -2,6 +2,7 @@
<x-page-header title="Dashboard" subtitle="Overview of your feed management system" />
<!-- System Statistics -->
@island('system-overview')
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4 dark:text-gray-100">System Overview</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
@ -78,6 +79,7 @@
</div>
</div>
</div>
@endisland
<!-- Article Statistics -->
<div>
@ -86,7 +88,7 @@
<div class="flex flex-col gap-2 sm:flex-row sm:items-end">
<div class="flex flex-wrap gap-1">
@foreach ($presets as $value => $label)
@foreach ($this->presets as $value => $label)
<button
type="button"
wire:click="applyPreset('{{ $value }}')"
@ -125,6 +127,7 @@ class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:rin
<p class="mb-4 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
@island('article-statistics')
<div @class(['grid grid-cols-1 md:grid-cols-3 gap-6', 'opacity-40' => ! $rangeIsValid])>
<!-- Articles Fetched -->
<div class="bg-white p-6 rounded-lg shadow-sm dark:bg-gray-800">
@ -177,5 +180,6 @@ class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:rin
</div>
</div>
</div>
@endisland
</div>
</div>

View file

@ -6,6 +6,7 @@
use App\Models\Article;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Tests\TestCase;
@ -119,4 +120,74 @@ public function test_it_renders_the_preset_buttons(): void
->assertSee('This Month')
->assertSee('All Time');
}
public function test_it_renders_each_panel_in_its_own_island(): void
{
$html = Livewire::test(Dashboard::class)->html();
foreach (['system-overview', 'article-statistics'] as $island) {
$this->assertSame(
1,
substr_count($html, "[if FRAGMENT:type=island|name={$island}|"),
"Expected exactly one [{$island}] island.",
);
}
}
/**
* @param Testable<Dashboard> $component
*/
private function islandFragments(Testable $component): string
{
/** @var array<int, string> $fragments */
$fragments = $component->effects['islandFragments'] ?? [];
return implode("\n", $fragments);
}
public function test_it_re_renders_the_article_statistics_island_when_the_range_changes(): void
{
Article::factory()->count(3)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
$fragments = $this->islandFragments(
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
);
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*3\s*</s', $fragments);
}
public function test_it_reflects_a_narrowed_range_in_the_re_rendered_island(): void
{
Article::factory()->count(3)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
$fragments = $this->islandFragments(
Livewire::test(Dashboard::class)->call('applyRange', '2026-08-01', '2026-08-31')
);
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*0\s*</s', $fragments);
}
public function test_it_refreshes_only_the_range_dependent_island(): void
{
$fragments = $this->islandFragments(
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
);
$this->assertStringContainsString('name=article-statistics|', $fragments);
$this->assertStringNotContainsString('name=system-overview|', $fragments);
$this->assertStringNotContainsString('Active Feeds', $fragments);
}
public function test_it_re_renders_the_article_statistics_island_when_a_preset_is_applied(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Article::factory()->count(2)->create(['created_at' => Carbon::parse('2026-07-15 09:00:00')]);
$fragments = $this->islandFragments(
Livewire::test(Dashboard::class)->call('applyPreset', 'month')
);
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*2\s*</s', $fragments);
}
}