Gambaran Umum Pencarian Global
Pencarian global adalah palet perintah yang tersedia di atas setiap halaman panel: satu kotak pencarian yang dapat menjangkau berbagai resource, bukan satu kotak pencarian untuk setiap tabel. Gunakan fitur ini ketika pengguna mengetahui apa yang sedang dicari, tetapi tidak mengetahui di mana data tersebut berada. Kotak pencarian pada tabel menjawab "baris mana di dalam daftar ini", sedangkan palet pencarian global menjawab "record mana di dalam panel ini".
Tidak ada resource yang dapat dicari sampai resource tersebut secara eksplisit mengaktifkannya. Mekanisme opt-in ini memang disengaja: menambahkan resource ke sebuah panel tidak boleh secara diam-diam memperluas data yang dapat dijangkau oleh pencarian.
Contoh minimal yang dapat digunakan
Deklarasikan atribut yang boleh dicocokkan pada sebuah resource:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users;
use App\Models\User;
use PandaPanel\Resources\Resource;
final class UserResource extends Resource
{
protected static string $model = User::class;
protected static ?string $navigationIcon = 'users';
/** @var list<string> */
protected static array $globalSearchAttributes = ['name', 'email'];
// ... table(), form(), pages()
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Buka panel lalu tekan mod+k (⌘K di macOS, Ctrl+K di platform lain), atau klik ikon kaca pembesar pada header. Ketik minimal dua karakter dan hasil pencarian akan muncul, dikelompokkan di bawah Users, dengan setiap hasil mengarah ke halaman view pengguna tersebut.
Tidak diperlukan konfigurasi lain. Palet pencarian pada panel aktif secara default; sebelumnya palet hanya tidak memiliki resource yang dapat dicari sampai properti di atas dideklarasikan.
Apa yang terjadi ketika pengguna mengetik
- Palet menunggu selama
debouncemilidetik setelah tombol terakhir ditekan (default 300), lalu mengirimGETke{panel path}/search?q={term}dengan headerAccept: application/json. - Request melewati middleware milik panel —
web, kemudian middleware yang ditambahkan olehauth(), laluResolvePanel, yang mengembalikan 403 jika pengguna tidak diizinkan mengakses panel. PandaPanel\Http\Controllers\PanelSearchControllermemvalidasiq, me-resolve panel aktif, lalu menyerahkan keduanya kePandaPanel\Search\GlobalSearch.GlobalSearchmenelusuri resource yang terdaftar pada panel: resource yang tidak mengaktifkan pencarian dilewati, resource dengancanViewAny()bernilai false juga dilewati, sedangkan resource lainnya di-query melaluiglobalSearchQuery(), yang dimulai dariResource::query().- Setiap record diubah menjadi
PandaPanel\Search\GlobalSearchResult— berisi judul, URL, dan map string detail. Model maupun query tidak ikut melewati tahap ini. - JSON dikembalikan dalam bentuk beberapa grup, satu grup per resource, dengan urutan yang deterministik.
- Palet menampilkan hasil tersebut. Tombol panah bergerak di seluruh daftar hasil yang telah diratakan, sedangkan Enter membuka hasil yang sedang dipilih melalui Inertia.
Sisi resource
Berikut seluruh konfigurasi yang dapat dideklarasikan atau di-override oleh sebuah resource:
| Member | Signature | Default |
|---|---|---|
$globalSearchAttributes | protected static array (list<string>) | [] — tidak dapat dicari |
$globalSearchLimit | protected static int | 5 |
$globalSearchSort | protected static int | 0 |
globalSearchAttributes() | public static function (): array | mengembalikan properti |
isGloballySearchable() | public static function (): bool | globalSearchAttributes() !== [] |
globalSearchLimit() | public static function (): int | mengembalikan properti |
globalSearchSort() | public static function (): int | mengembalikan properti |
globalSearchQuery() | public static function (): Builder | static::query() |
globalSearchResultTitle() | public static function (Model $record): string | static::recordTitle($record) |
globalSearchResultDetails() | public static function (Model $record): array | [] |
globalSearchResultUrl() | public static function (Model $record): string | view → edit → index |
Contoh lengkap yang menggunakan seluruh konfigurasi tersebut:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Posts;
use App\Models\Post;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Resources\Resource;
final class PostResource extends Resource
{
protected static string $model = Post::class;
protected static ?string $recordTitleAttribute = 'title';
/** @var list<string> */
protected static array $globalSearchAttributes = ['title', 'slug', 'author.name'];
protected static int $globalSearchLimit = 3;
protected static int $globalSearchSort = 10;
/** @var list<string> */
protected static array $with = ['author'];
public static function globalSearchQuery(): Builder
{
return static::query()->whereNotNull('published_at');
}
public static function globalSearchResultTitle(Model $record): string
{
return (string) $record->getAttribute('title');
}
/**
* @return array<string, string>
*/
public static function globalSearchResultDetails(Model $record): array
{
$author = $record->getAttribute('author');
return [
'Author' => $author instanceof Model ? (string) $author->getAttribute('name') : 'Unknown',
'Slug' => (string) $record->getAttribute('slug'),
];
}
public static function globalSearchResultUrl(Model $record): string
{
return static::url('edit', $record);
}
// ... table(), form(), pages()
}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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Setiap bagian memiliki halaman dokumentasi tersendiri: Resource yang dapat dicari, Atribut pencarian, Pencarian melalui relasi, Detail hasil pencarian, dan URL hasil pencarian.
Sisi panel
use PandaPanel\Core\Panel;
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->auth()
->globalSearch(
enabled: true,
limit: 50,
debounce: 300,
keyBindings: ['mod+k'],
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
| Argument | Tipe | Default | Keterangan |
|---|---|---|---|
$enabled | bool | true | menentukan apakah panel memiliki palet pencarian |
$limit | int | 50 | jumlah hasil untuk keseluruhan pencarian, bukan per resource |
$debounce | int | 300 | jeda dalam milidetik sebelum input menghasilkan request |
$keyBindings | list<string> | ['mod+k'] | mod adalah tombol command utama sesuai platform |
Method pembaca yang tersedia: hasGlobalSearch(): bool, getGlobalSearchLimit(): int, getGlobalSearchDebounce(): int, getGlobalSearchKeyBindings(): array. Lihat Konfigurasi pencarian panel.
Endpoint
Satu route didaftarkan untuk setiap panel di dalam route group panel oleh PandaPanel\Routing\PanelRouteRegistrar:
| Method dan path | GET {panel path}/search |
| Nama route | panel.{panelId}.search |
| Controller | PandaPanel\Http\Controllers\PanelSearchController |
| Query parameter | q — nullable, string, max:255 |
| Response | application/json |
curl --cookie jar.txt -H 'Accept: application/json' 'https://example.test/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
Response menggunakan JSON, bukan halaman Inertia, karena palet melakukan request saat pengguna sedang mengetik. Melakukan render ulang halaman yang sedang dibuka hanya untuk merespons satu input keyboard jelas tidak diperlukan.
Hasil yang sama juga dapat diperoleh langsung dari PHP — pola ini digunakan dalam pengujian:
use PandaPanel\Search\GlobalSearch;
$groups = app(GlobalSearch::class)->for(panel('admin'), 'Lovelace');2
3
GlobalSearch::for(Panel $panel, string $term): array adalah seluruh permukaan API service ini. Method tersebut mengembalikan list<array{resource: string, label: string, icon: string|null, results: list<array<string, mixed>>}>.
Data yang dikirim ke Vue
Shell perlu mengetahui apakah palet harus ditampilkan dan endpoint mana yang harus dipanggil. PandaPanel\Http\Middleware\SharePanelData menyediakan informasi tersebut melalui shared props pada key search:
export interface PanelSearchSettings {
enabled: boolean;
/** Null ketika pencarian dinonaktifkan sehingga tidak ada endpoint yang perlu dipanggil. */
url: string | null;
debounce: number;
keyBindings: string[];
}
export interface PanelSearchResult {
title: string;
url: string;
details: Record<string, string>;
}
export interface PanelSearchGroup {
resource: string;
label: string;
icon: string | null;
results: PanelSearchResult[];
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Akses konfigurasi tersebut dari komponen panel mana pun:
import { usePanel } from '@/panel/composables/usePanel';
const { search } = usePanel();
search.value.enabled; // boolean
search.value.url; // '/admin/search' atau null
search.value.debounce; // 300
search.value.keyBindings; // ['mod+k']2
3
4
5
6
7
8
enabled bernilai false ketika panel menonaktifkan pencarian atau ketika tidak ada resource di dalam panel yang mengaktifkan pencarian. url selalu bernilai null ketika enabled bernilai false. Palet yang hanya dapat menghasilkan daftar kosong lebih buruk daripada tidak menampilkan palet sama sekali, sehingga resources/js/panel/components/PanelSearch.vue tidak me-render apa pun pada kondisi tersebut — termasuk tombol pada header.
Palet pencarian
PanelSearch.vue dipasang oleh PanelHeader.vue. Perilakunya adalah sebagai berikut:
- Shortcut dari
search.keyBindingsmembuka palet; tombol pada header melakukan hal yang sama. - Input menggunakan
search.debounce. Kurang dari dua karakter non-spasi akan menghapus hasil tanpa mengirim request. - Hanya satu request yang aktif pada satu waktu. Input baru membatalkan fetch sebelumnya, sehingga response lama yang lambat tidak dapat menimpa response terbaru yang lebih cepat.
ArrowDowndanArrowUpbergerak pada hasil yang telah diratakan sesuai urutan tampil, dan akan kembali ke ujung lain ketika mencapai batas.Entermembuka hasil yang dipilih menggunakanrouter.visit().- Setiap hasil adalah
<Link>milik Inertia, sehingga klik bekerja seperti navigasi SPA biasa. - Navigasi Inertia apa pun akan menutup palet — pengguna sudah menemukan tujuan yang dicari.
- Response non-2xx atau body yang tidak valid akan diperlakukan sebagai tidak ada hasil, bukan melempar error di dalam dialog.
Batas hasil
Dua batas digunakan secara bersamaan. Batas panel merupakan anggaran untuk seluruh pencarian, sedangkan batas resource menentukan kontribusi maksimum resource tersebut:
$panel->globalSearch(limit: 50); // seluruh pencarian
protected static int $globalSearchLimit = 5; // resource ini2
Setiap resource diminta mengembalikan maksimal min($resource::globalSearchLimit(), $remaining) baris, lalu $remaining dikurangi sebanyak hasil yang benar-benar dikembalikan. Ketika anggaran mencapai nol, proses berhenti. Artinya, resource yang berada setelah resource lain yang telah menghabiskan anggaran tidak akan menyumbangkan hasil apa pun.
Urutan
Resource diurutkan berdasarkan globalSearchSort(), kemudian slug(). Jika nilai sort sama, slug digunakan sebagai penentu, bukan urutan registrasi atau urutan file. Dengan demikian, urutan grup tetap konsisten pada setiap request dan menambahkan resource baru tidak mengacak resource yang sudah berada di atasnya.
Di dalam satu grup, record mengikuti urutan yang dikembalikan database; tidak ada pemeringkatan berdasarkan relevansi. Tambahkan orderBy pada globalSearchQuery() jika urutan tertentu diperlukan.
Batasan pencarian global
- Bukan sebuah index pencarian. Pencocokan menggunakan
LIKE %escaped-term%terhadap kolom yang Anda deklarasikan. Tidak ada full-text index, ranking, fuzzy matching, ataupun highlighting. - Bukan Laravel Scout. Tidak ada integrasi dengan Laravel Scout.
globalSearchQuery()adalah titik yang dapat digunakan jika Anda ingin memperkenalkan engine lain, tetapi term pencarian tetap diterapkan olehGlobalSearchsetelahnya dan tidak diteruskan sebagai parameter ke query Anda. - Bukan pemeriksaan permission per record.
canViewAny()membatasi resource; baris individual dibatasi melaluiquery(), bukancanView(). Lihat Keamanan pencarian. - Tidak menggunakan pagination. Limit benar-benar merupakan batas jumlah hasil; tidak ada fitur "tampilkan lebih banyak".
Hal yang perlu diperhatikan
- Minimal dua karakter setelah
trim.mb_strlen(trim($term)) < 2mengembalikan[], dan palet bahkan tidak mengirim request. Nama keluarga satu karakter tidak dapat ditemukan melalui pencarian ini. qyang lebih panjang dari 255 karakter menghasilkan 422, bukan hasil kosong. Palet tidak akan mengirim input sepanjang itu, tetapi request manual bisa melakukannya.- Resource tanpa hasil tidak disertakan di dalam
groups, bukan dikirim dengan arrayresultskosong. - Ikon grup menggunakan
$navigationIconmilik class resource itu sendiri, bukan ikon yang dikonfigurasi panel melaluiResourceConfiguration::navigationIcon(). Label dan slug tetap mengikuti konfigurasi per panel. - Request yang gagal terlihat seperti "Nothing found." Palet menelan response non-2xx, sehingga error 419 setelah sesi kedaluwarsa tampak seperti pencarian tanpa hasil. Periksa tab Network sebelum menyimpulkan bahwa pencarian bermasalah.
- Framework tidak menambahkan rate limiting pada endpoint. Endpoint hanya mewarisi middleware milik panel; tambahkan
throttleke middleware stack panel jika diperlukan.