Filter Widget
Filter adalah form kecil yang mengubah apa yang dilaporkan oleh sebuah widget atau seluruh dashboard: rentang tanggal, region, status, dan sebagainya. Gunakan filter ketika widget yang sama dapat menjawab pertanyaan yang memiliki lebih dari satu jawaban benar — misalnya "sign-up dalam enam bulan terakhir" dan "dalam dua tahun terakhir" tetap merupakan widget yang sama dengan parameter berbeda, bukan dua widget terpisah.
Terdapat dua level filter dan keduanya dapat dikombinasikan. Dashboard dapat memfilter semua widget di dalamnya sekaligus; sebuah widget juga dapat memiliki filter sendiri yang hanya relevan untuk widget tersebut. Value disimpan pada query string, sehingga dashboard yang sudah difilter tetap dapat dibagikan sebagai link.
Contoh minimal yang dapat langsung digunakan
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use App\Models\User;
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Widgets\StatsWidget;
use PandaPanel\Widgets\Support\Stat;
final class SignUps extends StatsWidget
{
protected static ?string $heading = 'Sign-ups';
public function filterSchema(): FormSchema
{
return FormSchema::make()->schema([
Select::make('days')
->label('Window')
->options(['7' => 'Last 7 days', '30' => 'Last 30 days'])
->default('30'),
]);
}
/**
* @return list<Stat>
*/
public function stats(): array
{
$days = (int) $this->filter('days', 30);
return [
Stat::make('New accounts', User::query()
->where('created_at', '>=', now()->subDays($days))
->count()),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Sebuah select muncul di samping heading widget. Memilih "Last 7 days" akan membuka ?widgets[sign-ups][days]=7 dan nilai yang ditampilkan berubah.
Mendeklarasikan filter pada widget
filterSchema()
public function filterSchema(): ?FormSchemaMethod ini didefinisikan pada PandaPanel\Widgets\Widget dan secara default mengembalikan null. Itu adalah default yang tepat untuk kebanyakan widget karena widget umumnya cukup menggunakan filter dari page. Kembalikan PandaPanel\Forms\FormSchema jika widget membutuhkan control miliknya sendiri.
Semua form field dapat digunakan. Sebaiknya pilih control yang dapat di-resolve tanpa round trip tambahan:
use PandaPanel\Forms\Components\DatePicker;
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\Components\Toggle;
use PandaPanel\Forms\FormSchema;
public function filterSchema(): FormSchema
{
return FormSchema::make()->schema([
Select::make('region')->options(['eu' => 'Europe', 'us' => 'US'])->default('eu'),
DatePicker::make('since'),
Toggle::make('verified_only'),
]);
}2
3
4
5
6
7
8
9
10
11
12
13
Schema berfungsi sebagai whitelist. Key yang tidak pernah dideklarasikan oleh schema akan dibuang, sama seperti field yang tidak dikenal pada sebuah form — query string adalah request, bukan source of truth.
filtersInModal()
public static function filtersInModal(): boolDefault-nya false: control ditampilkan inline di atas widget. Kembalikan true jika Anda ingin widget menampilkan tombol Filters yang membuka dialog.
public static function filtersInModal(): bool
{
return true;
}2
3
4
Modal lebih cocok ketika filter memiliki lebih dari satu atau dua control dan form inline akan menjadi lebih besar daripada widget yang difilternya. Judul dialog menggunakan $heading widget, atau kata "Filters" jika widget tidak memiliki heading.
Membaca value filter
filter()
protected function filter(string $name, mixed $default = null): mixedMengembalikan satu value yang sudah dibatasi oleh schema yang mendeklarasikannya. null dan '' akan menggunakan $default; false dan 0 tetap dianggap sebagai value yang valid.
$days = (int) $this->filter('days', 30);
$region = (string) $this->filter('region', 'eu');
$verifiedOnly = (bool) $this->filter('verified_only', false);2
3
Selalu berikan default. Nilai tersebut mendefinisikan perilaku widget ketika filter belum ditentukan dan melindungi widget dari edge case yang dijelaskan di bagian berikutnya.
Value berasal dari query string sebagai string. Lakukan cast dan batasi setiap value yang akan memengaruhi query:
$months = max(1, min(24, (int) $this->filter('months', 6)));filters()
protected function filters(): arrayMengembalikan semua filter yang diterima widget, termasuk filter dari page, dalam bentuk name => value.
withFilters()
public function withFilters(array $filters): staticDipanggil oleh page, dan juga dapat Anda gunakan langsung pada test:
$widget = (new UserGrowth)->withFilters(['months' => '12']);
expect($widget->labels())->toHaveCount(12);2
3
Filter dashboard
PandaPanel\Pages\Page — dan berarti juga setiap PandaPanel\Pages\Dashboard — dapat mendeklarasikan satu form filter yang dibaca oleh semua widget di dalam page tersebut.
public function filterSchema(): ?FormSchema<?php
declare(strict_types=1);
namespace App\Panels\Admin\Pages;
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Pages\Dashboard;
final class AccountsDashboard extends Dashboard
{
protected static ?string $title = 'Accounts';
protected static ?string $slug = 'accounts';
public function filterSchema(): FormSchema
{
return FormSchema::make()->schema([
Select::make('period')
->label('Period')
->options([
'month' => 'This month',
'quarter' => 'This quarter',
'year' => 'This year',
])
->default('month'),
]);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Control dirender di atas widget grid. Semua widget pada page dapat membaca period melalui $this->filter('period', 'month') tanpa harus mendeklarasikan field tersebut sendiri.
Pertanyaan seperti "this quarter" adalah pertanyaan untuk seluruh page, bukan hanya satu angka tertentu. Gunakan filter milik widget untuk parameter yang memang hanya relevan bagi widget tersebut.
Cara kedua level filter digabungkan
Widget menerima filter dari page terlebih dahulu, kemudian filter miliknya sendiri ditaruh di atasnya:
[...$dashboardFilters, ...$widgetFilters]Dengan demikian, jika sebuah widget mendeklarasikan months, maka value months milik widget tersebut yang digunakan. Widget yang tidak mendeklarasikannya tetap melihat months milik page. Perilaku ini diterapkan oleh PandaPanel\Widgets\Support\WidgetFilters::for().
use PandaPanel\Widgets\Support\WidgetFilters;
$filters = WidgetFilters::fromRequest($request, $pageSchema, ['user-growth' => $widgetSchema]);
$filters->dashboard(); // ['months' => '6']
$filters->for('user-growth'); // ['months' => '24'] — its own wins
$filters->for('recent-users'); // ['months' => '6'] — the page's2
3
4
5
6
7
Query string
| Level | Bentuk parameter | Contoh |
|---|---|---|
| Page / dashboard | filters[{field}] | ?filters[period]=quarter |
| Widget | widgets[{widgetId}][{field}] | ?widgets[user-growth][months]=24 |
Widget id adalah basename class dalam kebab-case — UserGrowth menjadi user-growth.
Control langsung diterapkan ketika value berubah, bukan melalui tombol Apply. Navigasi menggunakan preserveScroll dan preserveState, sehingga page tidak meloncat dan state lain yang sedang diketik tidak hilang. Value null, '', atau false akan menghapus parameter daripada menuliskan parameter kosong. Parameter page juga selalu dibuang setiap kali filter berubah, karena menampilkan page keempat dari hasil sebelum difilter biasanya menghasilkan halaman kosong.
Default, clear, dan session
WidgetFilters me-resolve value setiap schema menggunakan tiga aturan berikut. Memahami aturan ini penting agar filter yang sudah di-clear tidak tiba-tiba kembali ke value sebelumnya.
| Kondisi request | Value yang diterima widget |
|---|---|
| group parameter sama sekali tidak ada | value yang tersimpan di session jika tersedia, jika tidak maka default() masing-masing field |
| group tersedia dan key ada di dalamnya | value yang dikirim |
| group tersedia tetapi key tidak ada | null — field telah di-clear |
Hanya kondisi absent yang melakukan fallback. Mengosongkan filter adalah keputusan pengguna; mengembalikan value session setelah pengguna menghapusnya justru akan mengabaikan tindakan terbaru pengguna.
State disimpan per page, bukan hanya per panel:
protected function filterSessionKey(): string
{
return 'panel.'.$this->panel()->getId().'.page.'.static::slug();
}2
3
4
Filter page disimpan di {key}.filters, sedangkan filter widget disimpan di {key}.widgets.{widgetId}. Dua dashboard yang memiliki filter berbeda merepresentasikan dua pertanyaan berbeda; menggunakan state salah satunya untuk yang lain akan menghasilkan jawaban yang tidak sesuai. Override filterSessionKey() pada page jika Anda membutuhkan key berbeda.
Persistence dilewati sepenuhnya jika request tidak memiliki session.
Mekanisme internal
Anda jarang perlu memanggil API berikut secara langsung, tetapi inilah yang dilakukan page di balik layar.
use PandaPanel\Pages\WidgetCollection;
use PandaPanel\Widgets\Support\WidgetFilters;
/** @return array<string, FormSchema> keyed by widget id */
WidgetCollection::filterSchemas([UserStats::class, UserGrowth::class]);
WidgetFilters::none(); // no filters at all
WidgetFilters::fromRequest(
Request $request,
?FormSchema $dashboardSchema = null,
array $widgetSchemas = [], // keyed by widget id
?string $sessionKey = null,
);2
3
4
5
6
7
8
9
10
11
12
13
14
filterSchemas() melewati widget yang canView()-nya mengembalikan false, sehingga schema milik widget yang tidak berizin tidak pernah menjadi bagian dari whitelist.
Data yang diterima frontend
Widget yang memiliki filter membawa struktur berikut pada definition-nya:
'filters' => [
'inModal' => false,
'form' => [/* the serialized FormSchema, holding the values the server resolved */],
],2
3
4
Untuk widget tanpa filter nilainya null, sehingga renderer tidak menggambar bar kosong. Filter milik page dikirim sebagai prop filters terpisah dengan key form yang sama.
Value pada form tersebut adalah value yang benar-benar diterapkan oleh server, bukan sekadar value yang diminta client. Karena itu key yang ditolak atau dibuang tidak akan muncul di UI seolah-olah sudah diterapkan.
Hal yang perlu diperhatikan
- Filter pada resource page hanya berada di level widget.
ResourcePage::widgetProps()tidak memiliki form filter page-wide sepertiDashboarddanPage, tetapi tetap me-resolvefilterSchema()milik setiap widget. State diingat berdasarkan panel, resource, page, dan record key. - Table widget menggunakan group yang sama. State search, sort, dan page milik
TableWidgetjuga berada di bawahwidgets[{id}]. Ketika pagination mengirim group tersebut, group menjadi present, sehingga filter widget yang tidak ikut dikirim dapat di-resolve menjadinullberdasarkan aturan di atas. Membaca value dengan default eksplisit —$this->filter('months', 6)— membuat kondisi ini aman; itulah sebabnya semua contoh menggunakan default. - Field
live()tidak memiliki state endpoint di dalam widget filter, sehingga berperilaku seperti field biasa: tidak ada request tambahan, tidak ada rebuild schema, dan tidak adaafterStateUpdated(). Lihat Live fields. Toggleyang dimatikan tidak menulis parameter, sehingga toggle "off" dan toggle yang belum pernah disentuh terlihat sama di URL. Gunakandefault(false)pada field dan baca dengan$this->filter('flag', false).- Value filter adalah string.
'0'bernilai truthy di PHP; lakukan cast sebelum melakukan pemeriksaan boolean atau numerik. filterSchema()dapat dipanggil lebih dari sekali dalam satu request — sekali untuk membangun whitelist dan sekali untuk men-serialize definition. Hindari query di dalamnya atau lakukan memoization terhadap option yang mahal.- Validation rule pada field filter tidak dijalankan. Schema membatasi key yang boleh diterima, tetapi tidak memvalidasi value. Tetap batasi value di dalam widget sebelum digunakan pada query.