diff --git a/resources/views/livewire/summary/index.blade.php b/resources/views/livewire/summary/index.blade.php index 6e72333..77844e8 100644 --- a/resources/views/livewire/summary/index.blade.php +++ b/resources/views/livewire/summary/index.blade.php @@ -11,17 +11,52 @@ use WithPagination; public array $filters = [ - 'search' => null, - 'model' => null, - 'suite' => null, + 'gpu' => [], + 'model' => [], + 'quant' => [], + 'engine' => [], + 'suite' => [], ]; + public function updatedFilters(): void + { + $this->resetPage(); + } + public function resetFilters(): void { - $this->filters = ['search' => null, 'model' => null, 'suite' => null]; + $this->filters = [ + 'gpu' => [], + 'model' => [], + 'quant' => [], + 'engine' => [], + 'suite' => [], + ]; $this->resetPage(); } + public function toggleFilter(string $field, string $value): void + { + if (! array_key_exists($field, $this->filters)) { + return; + } + + $this->filters[$field] = array_values(array_unique( + in_array($value, $this->filters[$field], true) + ? array_diff($this->filters[$field], [$value]) + : array_merge($this->filters[$field], [$value]) + )); + + $this->resetPage(); + } + + public function filterLabel(string $field): string + { + $count = count($this->filters[$field] ?? []); + + return $count === 0 ? '' : ' ('.$count.')'; + } + #[Computed] public function sessions() { @@ -63,34 +98,64 @@ private function bandQuery($user) }); } }) - ->when($this->filters['search'], fn ($q, $search) => $q->where(function ($q) use ($search) { - $q->where('label', 'like', "%{$search}%") - ->orWhere('model_id', 'like', "%{$search}%") - ->orWhere('gpu', 'like', "%{$search}%") - ->orWhere('runner_name', 'like', "%{$search}%"); - })) - ->when($this->filters['model'], fn ($q, $model) => $q->where('model_id', $model)) - // Suite filter: keep bands containing the chosen suite - ->when($this->filters['suite'], fn ($q, $suite) => $q->whereExists(function ($sub) use ($suite) { + ->when($this->filters['gpu'], fn ($q, array $gpus) => $q->whereIn('gpu', $gpus)) + ->when($this->filters['model'], fn ($q, array $models) => $q->whereIn('model_id', $models)) + ->when($this->filters['quant'], fn ($q, array $quants) => $q->whereIn('model_quant', $quants)) + ->when($this->filters['engine'], fn ($q, array $engines) => $q->whereIn('engine', $engines)) + ->when($this->filters['suite'], fn ($q, array $suites) => $q->whereExists(function ($sub) use ($suites) { $sub->select(\Illuminate\Support\Facades\DB::raw(1)) ->from('session_suite_rows') ->whereColumn('session_suite_rows.session_summary_id', 'session_summaries.id') - ->where('session_suite_rows.suite', $suite); + ->whereIn('session_suite_rows.suite', $suites); })) ->orderByDesc('ran_at') ->orderBy('sequence'); } + #[Computed] + public function gpus(): array + { + return Cache::remember('summary:gpus', 300, function () { + return \Illuminate\Support\Facades\DB::table('session_summaries') + ->where('visibility', 'public') + ->whereNotNull('gpu') + ->distinct()->orderBy('gpu')->pluck('gpu')->all(); + }); + } + #[Computed] public function models(): array { return Cache::remember('summary:models', 300, function () { return \Illuminate\Support\Facades\DB::table('session_summaries') ->where('visibility', 'public') + ->whereNotNull('model_id') ->distinct()->orderBy('model_id')->pluck('model_id')->all(); }); } + #[Computed] + public function quants(): array + { + return Cache::remember('summary:quants', 300, function () { + return \Illuminate\Support\Facades\DB::table('session_summaries') + ->where('visibility', 'public') + ->whereNotNull('model_quant') + ->distinct()->orderBy('model_quant')->pluck('model_quant')->all(); + }); + } + + #[Computed] + public function engines(): array + { + return Cache::remember('summary:engines', 300, function () { + return \Illuminate\Support\Facades\DB::table('session_summaries') + ->where('visibility', 'public') + ->whereNotNull('engine') + ->distinct()->orderBy('engine')->pluck('engine')->all(); + }); + } + #[Computed] public function suites(): array { @@ -114,28 +179,43 @@ public function suites(): array
- + @php + $filterGroups = [ + ['field' => 'gpu', 'label' => __('GPU'), 'options' => $this->gpus], + ['field' => 'model', 'label' => __('Model'), 'options' => $this->models], + ['field' => 'quant', 'label' => __('Quant'), 'options' => $this->quants], + ['field' => 'engine', 'label' => __('Inference engine'), 'options' => $this->engines], + ['field' => 'suite', 'label' => __('Suite'), 'options' => $this->suites], + ]; + @endphp - - {{ __('All models') }} - @foreach ($this->models as $model) - {{ $model }} - @endforeach - + @foreach ($filterGroups as $group) + + + {{ $group['label'] }}{{ $this->filterLabel($group['field']) }} + + - - {{ __('All suites') }} - @foreach ($this->suites as $suite) - {{ $suite }} - @endforeach - + + @foreach ($group['options'] as $option) + @php $checked = in_array($option, $this->filters[$group['field']], true); @endphp + + + @if ($checked) + + @else + + @endif + {{ $option }} + + + @endforeach + + + @endforeach {{ __('Clear') }}
diff --git a/tests/Feature/SessionSummaryTest.php b/tests/Feature/SessionSummaryTest.php index dd2dd6b..ddd242f 100644 --- a/tests/Feature/SessionSummaryTest.php +++ b/tests/Feature/SessionSummaryTest.php @@ -6,6 +6,7 @@ use App\Models\SoftbenchResult; use App\Models\User; use Illuminate\Support\Facades\Artisan; +use Livewire\Volt\Volt; test('festbench:summary splits runs by label into separate bands', function () { $me = User::factory()->create(['name' => 'peterZ']); @@ -158,3 +159,122 @@ ->assertSee('All Task Rows') ->assertSee('flat-task'); }); + +test('multi-filter: suite filter returns bands containing any selected suite', function () { + $me = User::factory()->create(); + + $soft = BenchmarkRun::factory()->create([ + 'suite' => 'softbench', 'label' => 'soft-only', 'model_id' => 'm', 'model_quant' => 'Q5_K_M', + 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public', + 'ran_at' => '2026-09-03 12:00:00', + ]); + SoftbenchResult::factory()->create(['benchmark_run_id' => $soft->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']); + + $infra = BenchmarkRun::factory()->create([ + 'suite' => 'infrabench', 'label' => 'infra-only', 'model_id' => 'm', 'model_quant' => 'Q5_K_M', + 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public', + 'ran_at' => '2026-09-03 12:05:00', + ]); + InfrabenchResult::factory()->create(['benchmark_run_id' => $infra->id, 'ok' => true, 'user_tps' => 30.0, 'total' => 100, 'level' => 'low']); + + Artisan::call('festbench:summary'); + + // No filter: both bands visible. + Volt::test('summary.index')->assertSee('soft-only')->assertSee('infra-only'); + + // Filter to infrabench: only the infra band should match. + Volt::test('summary.index', ['filters' => [ + 'gpu' => [], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => ['infrabench'], + ]]) + ->assertDontSee('soft-only') + ->assertSee('infra-only'); +}); + +test('multi-filter: AND across categories, OR within a category', function () { + $me = User::factory()->create(); + + // Band A: GPU1 + model-A + Q5_K_M + llama.cpp + $a = BenchmarkRun::factory()->create([ + 'suite' => 'softbench', 'label' => 'band-a', 'model_id' => 'model-A', 'model_quant' => 'Q5_K_M', + 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public', + 'ran_at' => '2026-09-03 12:00:00', + ]); + SoftbenchResult::factory()->create(['benchmark_run_id' => $a->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']); + + // Band B: GPU2 + model-B + UD-Q4_K_M + ollama-cloud + $b = BenchmarkRun::factory()->create([ + 'suite' => 'softbench', 'label' => 'band-b', 'model_id' => 'model-B', 'model_quant' => 'UD-Q4_K_M', + 'gpu' => 'GPU2', 'gpu_count' => 1, 'engine' => 'ollama-cloud', 'user_id' => $me->id, 'visibility' => 'public', + 'ran_at' => '2026-09-04 12:00:00', + ]); + SoftbenchResult::factory()->create(['benchmark_run_id' => $b->id, 'ok' => true, 'user_tps' => 40.0, 'total' => 100, 'level' => 'off']); + + Artisan::call('festbench:summary'); + + // GPU filter alone: OR within GPU → both bands. + Volt::test('summary.index', ['filters' => [ + 'gpu' => ['GPU1', 'GPU2'], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => [], + ]]) + ->assertSee('band-a') + ->assertSee('band-b'); + + // GPU + model: AND across → only matching band. + Volt::test('summary.index', ['filters' => [ + 'gpu' => ['GPU1', 'GPU2'], 'model' => ['model-A'], 'quant' => [], 'engine' => [], 'suite' => [], + ]]) + ->assertSee('band-a') + ->assertDontSee('band-b'); + + // Engine filter: only llama.cpp band. + Volt::test('summary.index', ['filters' => [ + 'gpu' => [], 'model' => [], 'quant' => [], 'engine' => ['llama.cpp'], 'suite' => [], + ]]) + ->assertSee('band-a') + ->assertDontSee('band-b'); + + // Quant filter: only UD-Q4_K_M band. + Volt::test('summary.index', ['filters' => [ + 'gpu' => [], 'model' => [], 'quant' => ['UD-Q4_K_M'], 'engine' => [], 'suite' => [], + ]]) + ->assertDontSee('band-a') + ->assertSee('band-b'); +}); + +test('multi-filter: multiple suites use OR for band inclusion', function () { + $me = User::factory()->create(); + + // Band with only softbench. + $soft = BenchmarkRun::factory()->create([ + 'suite' => 'softbench', 'label' => 'has-soft', 'model_id' => 'm', 'model_quant' => 'Q5_K_M', + 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public', + 'ran_at' => '2026-09-03 12:00:00', + ]); + SoftbenchResult::factory()->create(['benchmark_run_id' => $soft->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']); + + // Band with only infrabench. + $infra = BenchmarkRun::factory()->create([ + 'suite' => 'infrabench', 'label' => 'has-infra', 'model_id' => 'm', 'model_quant' => 'Q5_K_M', + 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public', + 'ran_at' => '2026-09-03 12:05:00', + ]); + InfrabenchResult::factory()->create(['benchmark_run_id' => $infra->id, 'ok' => true, 'user_tps' => 30.0, 'total' => 100, 'level' => 'low']); + + Artisan::call('festbench:summary'); + + // Both suites selected → both bands visible (OR within suite). + Volt::test('summary.index', ['filters' => [ + 'gpu' => [], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => ['softbench', 'infrabench'], + ]]) + ->assertSee('has-soft') + ->assertSee('has-infra'); +}); + +test('toggleFilter adds and removes a value from a filter array', function () { + $component = Volt::test('summary.index'); + + $component->call('toggleFilter', 'suite', 'infrabench') + ->assertSet('filters.suite', ['infrabench']); + + $component->call('toggleFilter', 'suite', 'infrabench') + ->assertSet('filters.suite', []); +});