Integrasi Global Search
Command palette yang tersedia di atas setiap page Panel mencari Resource yang melakukan opt-in. Opt-in hanya membutuhkan satu property; bagian lain seperti title result, detail tambahan, link, dan limit sudah memiliki default yang dapat di-override. Halaman ini membahas seluruh konfigurasi tersebut.
Mengaktifkan Global Search pada Resource
use PandaPanel\Resources\Resource;
final class UserResource extends Resource
{
protected static string $model = User::class;
/** @var list<string> */
protected static array $globalSearchAttributes = ['name', 'email'];
// ...
}2
3
4
5
6
7
8
9
10
11
Itulah seluruh proses opt-in. Setelah itu, mengetik dua karakter atau lebih pada palette dapat menemukan user berdasarkan name atau email, mengelompokkan hasil di bawah Users, lalu mengarahkan setiap result ke view page.
Resource yang tidak mendeklarasikan attribute sama sekali tidak ikut dicari. Menambahkan Resource baru ke Panel tidak boleh secara diam-diam memperluas data yang dapat dicari, sehingga default-nya [] dan command palette sama sekali tidak mengetahui Resource tersebut.
Deklarasi
| Property | Type | Default | Efek |
|---|---|---|---|
$globalSearchAttributes | list<string> | [] | Column yang boleh dicari. Array kosong berarti Resource tidak searchable |
$globalSearchLimit | int | 5 | Jumlah maksimal result yang dapat disumbangkan Resource |
$globalSearchSort | int | 0 | Urutan presentasi antar-Resource; nilai sama diurutkan berdasarkan slug |
/** @var list<string> */
protected static array $globalSearchAttributes = ['title', 'author.name'];
protected static int $globalSearchLimit = 3;
protected static int $globalSearchSort = 10;2
3
4
5
6
Accessor bersifat public karena search service yang menanyakannya:
public static function globalSearchAttributes(): array; // list<string>
public static function isGloballySearchable(): bool; // globalSearchAttributes() !== []
public static function globalSearchLimit(): int;
public static function globalSearchSort(): int;2
3
4
Mencari melalui relation
Attribute yang mengandung titik akan melakukan search melalui relation yang disebutnya:
/** @var list<string> */
protected static array $globalSearchAttributes = ['title', 'author.name', 'author.email'];2
title diterjemahkan menjadi where('title', 'like', "%term%"), sedangkan author.name menjadi whereHas('author', fn ($q) => $q->where('name', 'like', "%term%")). Seluruh attribute digabungkan menggunakan OR di dalam satu grouped where, sehingga search tidak pernah memperlebar scope Resource itu sendiri.
Daftar attribute adalah whitelist. Tidak ada value dari request yang pernah digunakan sebagai nama column; search term hanya digunakan sebagai bound value.
Query
use Illuminate\Database\Eloquent\Builder;
public static function globalSearchQuery(): Builder
{
return static::query();
}2
3
4
5
6
Global search dimulai dari Resource::query() seperti lookup lain. Karena itu tenant scope, team scope, atau per-Panel narrowing mempersempit command palette persis seperti mempersempit list. Override method jika search perlu menjangkau subset yang lebih sempit daripada Resource page:
use Illuminate\Database\Eloquent\Builder;
public static function globalSearchQuery(): Builder
{
return static::query()->whereNotNull('published_at');
}2
3
4
5
6
Bentuk sebuah search result
use Illuminate\Database\Eloquent\Model;
public static function globalSearchResultTitle(Model $record): string;
/**
* @return array<string, string>
*/
public static function globalSearchResultDetails(Model $record): array;
public static function globalSearchResultUrl(Model $record): string;2
3
4
5
6
7
8
9
10
Title secara default menggunakan recordTitle(). Resource yang sudah mendeklarasikan $recordTitleAttribute biasanya tidak perlu melakukan konfigurasi tambahan.
Details adalah baris tambahan di bawah title dan default-nya kosong. Gunakan scalar/string saja karena data ini merupakan presentation yang akan diserialisasi ke Vue.
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
/**
* @return array<string, string>
*/
public static function globalSearchResultDetails(Model $record): array
{
return [
'Email' => (string) $record->getAttribute('email'),
'Role' => $record instanceof User && $record->is_admin ? 'Administrator' : 'Member',
];
}2
3
4
5
6
7
8
9
10
11
12
13
URL default-nya menuju view page jika Resource mendeklarasikannya, kemudian edit page bila view page tidak ada, dan index sebagai fallback terakhir. Setiap destination tetap melakukan authorization sendiri ketika dibuka.
use Illuminate\Database\Eloquent\Model;
public static function globalSearchResultUrl(Model $record): string
{
return static::url('edit', $record);
}2
3
4
5
6
Setting pada level Panel
$panel->globalSearch(
enabled: true,
limit: 50,
debounce: 300,
keyBindings: ['mod+k'],
);2
3
4
5
6
| Argument | Default | Arti |
|---|---|---|
$enabled | true | Apakah command palette tersedia pada Panel |
$limit | 50 | Total result untuk seluruh search, bukan per Resource |
$debounce | 300 | Millisecond sebelum ketikan menjadi request |
$keyBindings | ['mod+k'] | mod berarti command key sesuai platform |
Kedua limit digabungkan. Setiap Resource diminta maksimal min($resource::globalSearchLimit(), $remaining), lalu proses berhenti setelah budget Panel habis. Karena itu Panel dengan limit 2 tetap membatasi Resource yang sebenarnya memiliki limit 5.
Command palette aktif secara default, tetapi tidak mencari apa pun sampai setidaknya satu Resource mendeklarasikan attribute. Panel tanpa Resource searchable tidak menampilkan palette sama sekali; frontend menerima search.enabled: false dan tidak diberi search URL.
$panel->globalSearch(false); // off entirelyEndpoint
Setiap Panel memiliki satu route: GET {panel path}/search, dengan route name panel.{panelId}.search, dan route tersebut berada di balik middleware Panel yang sama dengan page lain.
curl '/admin/search?q=Lovelace'{
"groups": [
{
"resource": "users",
"label": "Users",
"icon": "users",
"results": [
{
"title": "Ada Lovelace",
"url": "/admin/users/1",
"details": { "Email": "ada@example.com", "Role": "Administrator" }
}
]
}
]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Endpoint mengembalikan JSON, bukan Inertia page. Palette mengirim request ketika user sedang mengetik; merender ulang page yang sedang dibuka hanya untuk menjawab search jelas tidak masuk akal. Setiap group membawa slug Resource, plural label, dan navigation icon yang semuanya sudah di-resolve server, sehingga frontend tidak perlu mengambil keputusan tambahan.
Rule yang selalu berlaku
- Search term kurang dari dua karakter menghasilkan
[]. Blank term juga sama. Parameterqdivalidasi sebagai nullable string maksimal 255 karakter. canViewAny()diperiksa sebelum Resource di-query, sehingga Resource yang ditolak tidak menjalankan query dan tidak membocorkan informasi.- Setiap result URL mengarah ke page yang tetap memiliki policy check sendiri, dan page melakukan authorization lagi ketika dibuka.
- Urutan selalu deterministic: berdasarkan
globalSearchSort(), lalu slug. - Resource tanpa result tidak dimasukkan ke response daripada dikirim sebagai empty group.
- Model, Closure, maupun query tidak pernah masuk payload. Pada saat
GlobalSearchResultdibentuk, record sudah di-authorize dan dipersempit menjadi title, URL, dan map string.
Catatan penting
- Matching menggunakan
LIKE %term%. Tidak ada full-text index, ranking, maupun fuzzy matching. Table sangat besar sebaiknya menggunakan search engine khusus, danglobalSearchQuery()adalah extension point untuk mengarahkannya. - Command palette bukan cara melewati Resource scope. Search membaca melalui
query(), sehingga record yang tidak dapat dibuka Panel juga tidak dapat ditemukan palette. - Details harus berupa string.
Carbonobject atau model di dalam array detail adalah masalah serialization; format datanya di server. $globalSearchLimitadalah limit per Resource, sedangkanPanel::globalSearch(limit:)adalah limit per search. Jika sebuah Resource berhenti di bawah limit-nya sendiri, biasanya budget Panel sudah dihabiskan Resource yang diproses lebih awal.- Ketika sort value sama, tie-breaker menggunakan slug, bukan registration order. Menambahkan Resource baru tidak membuat group lama tiba-tiba berpindah urutan secara tidak terduga.