Contoh Admin Panel
Admin Panel yang disertakan di examples/, dipasang pada /admin dan hanya dapat diakses oleh user yang memiliki flag is_admin = true. Gunakan halaman ini ketika Anda sedang membangun Panel pertama dalam aplikasi dan membutuhkan contoh provider yang lengkap serta benar-benar bekerja untuk disalin, bukan sekadar daftar method yang harus dirangkai sendiri. Semua file yang disebutkan di sini tersedia di repository dan test suite framework dijalankan terhadap file-file tersebut.
Contoh minimal yang berfungsi
Generate Panel beserta directory-nya:
php artisan make:panel AdminDaftarkan provider — Panel didaftarkan secara eksplisit sehingga daftar panel application terlihat dari satu tempat:
// config/panda-panel.php
'panels' => [
App\Panels\Admin\AdminPanelProvider::class,
],2
3
4
5
Dengan itu Anda sudah memiliki Panel yang berfungsi di /admin, lengkap dengan dashboard, tiga halaman pengaturan account bawaan, dan belum memiliki fitur lain. Sign in lalu buka URL tersebut.
Provider contoh lengkap
examples/app/Panels/Admin/AdminPanelProvider.php:
<?php
declare(strict_types=1);
namespace App\Panels\Admin;
use App\Models\User;
use App\Panels\Admin\Pages\AccountsDashboard;
use Illuminate\Contracts\Auth\Authenticatable;
use PandaPanel\Actions\Action;
use PandaPanel\Actions\Enums\ActionVariant;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
use PandaPanel\Pages\Dashboard;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->name('Administrator')
->brandName((string) config('app.name'))
->icon('shield')
->sidebar(appearance: 'sidebar')
->auth()
->navigationGroups([
'User Management',
'System',
])
->dashboards([
Dashboard::class,
AccountsDashboard::class,
])
->discoverResources(app_path('Panels/Admin/Resources'))
->discoverPages(app_path('Panels/Admin/Pages'))
->discoverWidgets(app_path('Panels/Admin/Widgets'))
->configureActions(static function (Action $action): void {
if ($action->getVariant() === ActionVariant::Destructive) {
$action->requiresConfirmation();
}
})
->canAccess(static fn (?Authenticatable $user): bool => $user instanceof User && $user->is_admin);
}
}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
Bagian selanjutnya membedah provider tersebut satu pemanggilan method demi satu.
Identitas
ID diturunkan dari nama class — AdminPanelProvider menjadi admin — sedangkan nilai lainnya dideklarasikan secara eksplisit.
public function id(string $id): self
public function name(string $name): self
public function path(string $path): self
public function domain(?string $domain): self2
3
4
$panel
->path('admin') // the URL prefix: /admin
->name('Administrator') // what the panel calls itself in the shell
->domain('admin.example.com'); // optional; keeps the panel off other hosts2
3
4
Method pembaca menggunakan prefix get: getId(), getPath(), getName(), getDomain(). Nama route dibangun dari ID — misalnya panel.admin.dashboard dan panel.admin.resources.users.index — sehingga mengganti path tidak akan merusak pemanggilan Resource::url().
Branding dan shell
public function brandName(string $brandName): self
public function brandLogo(?string $brandLogo, ?string $darkBrandLogo = null): self
public function icon(?string $icon, ?string $darkIcon = null): self
public function favicon(?string $favicon, ?string $darkFavicon = null): self
public function darkMode(bool $darkMode = true): self
public function maxContentWidth(?string $maxContentWidth): self
public function sidebar(
bool $collapsible = true,
bool $defaultOpen = true,
string $variant = 'sidebar',
string $appearance = 'inset',
): self2
3
4
5
6
7
8
9
10
11
12
$panel
->brandName((string) config('app.name'))
->icon('shield') // an icon registry key, never a path
->sidebar(appearance: 'sidebar'); // 'inset' (default), 'floating', 'sidebar'2
3
4
variant: 'header' mengganti side rail menjadi top navigation. appearance mengatur tampilan rail dan diabaikan ketika shell menggunakan header. icon() menerima key dari resources/js/panel/icons/registry.ts — setelah menambahkan nama icon baru, jalankan php artisan panel:icons; jika tidak, icon tersebut tidak akan dirender.
Siapa yang boleh masuk
Ada dua pertanyaan, dan keduanya harus memberikan jawaban yang mengizinkan.
public function auth(bool $verified = true): self
public function canAccess(Closure $callback): self // Closure(?Authenticatable): bool
public function isAccessibleTo(?Authenticatable $user): bool2
3
$panel
->auth() // appends 'auth' and 'verified' to the panel's auth middleware
->canAccess(static fn (?Authenticatable $user): bool => $user instanceof User && $user->is_admin);2
3
auth() adalah middleware — guest akan diarahkan ke halaman login. canAccess() adalah predicate — authenticated user yang gagal memenuhi predicate mendapatkan 403, bukan redirect, karena user sudah berhasil login tetapi tetap tidak diizinkan masuk ke Panel tersebut.
Separuh aturan lainnya berada pada user model. App\Models\User pada example mengimplementasikan PandaPanel\Contracts\PanelUser:
public function canAccessPanel(Panel $panel): bool
{
return $this->hasVerifiedEmail();
}2
3
4
Aturan tentang Panel tertentu sebaiknya berada di canAccess(). Aturan tentang account user sebaiknya berada pada model, sehingga otomatis berlaku pada seluruh Panel dan tidak terlupa ketika Panel baru ditambahkan. Panel yang mengizinkan user tidak dapat membatalkan penolakan dari user model.
Navigation group
public function navigationGroups(array $groups): self$panel->navigationGroups([
'User Management',
'System',
'Access' => 'System', // nests Access under System
]);2
3
4
5
Group dirender sesuai urutan deklarasi. Group yang digunakan oleh sebuah class tetapi tidak pernah dideklarasikan Panel akan ditambahkan setelah group yang terdaftar dan diurutkan alfabetis. Pemanggilan method ini bersifat accumulating, bukan replace, sehingga plugin dapat menambahkan group tambahan.
Backed enum juga dapat digunakan sebagai pengganti string. Ini berguna ketika lebih dari satu class menggunakan nama group yang sama, karena typo pada string dapat secara diam-diam membuat group kedua yang terlihat hampir sama.
Dashboard
public function dashboard(string $page): self // class-string<Page>
public function dashboards(array $pages): self // list<class-string<Page>>
public function getExtraDashboards(): array2
3
$panel->dashboards([
Dashboard::class, // the panel root, /admin
AccountsDashboard::class, // its own route, navigation item, and filters
]);2
3
4
Entry pertama menjadi root Panel; sisanya diregistrasikan sebagai page biasa. PandaPanel\Pages\Dashboard menampilkan semua widget yang ditemukan Panel. AccountsDashboard hanya menggunakan tiga widget miliknya sendiri dan menambahkan page-wide filter:
// examples/app/Panels/Admin/Pages/AccountsDashboard.php
final class AccountsDashboard extends Dashboard
{
protected static ?string $title = 'Accounts';
protected static ?string $slug = 'accounts';
protected static ?string $navigationIcon = 'users';
protected static string|BackedEnum|null $navigationGroup = 'User Management';
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'),
]);
}
/** @return list<class-string<Widget>> */
public function widgets(): array
{
return [UserStats::class, UserGrowth::class, RecentUsers::class];
}
}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
Menggunakan dua dashboard lebih tepat daripada satu dashboard dengan dropdown ketika keduanya menjawab kebutuhan berbeda. Contohnya, "bagaimana perkembangan account" dan "apakah sistem sehat" biasanya dilihat oleh orang atau konteks yang berbeda.
Discovery
public function discoverResources(string ...$paths): self
public function discoverPages(string ...$paths): self
public function discoverWidgets(string ...$paths): self
public function resources(array $resources): self // class-string|ResourceConfiguration
public function pages(array $pages): self
public function widgets(array $widgets): self2
3
4
5
6
$panel
->discoverResources(app_path('Panels/Admin/Resources'))
->discoverPages(app_path('Panels/Admin/Pages'))
->discoverWidgets(app_path('Panels/Admin/Widgets'));2
3
4
Tidak ada class pada example Panel yang diregistrasikan satu-per-satu. Nama class ditentukan dari prefix PSR-4 Composer, bukan dengan parsing isi file. Hanya concrete class yang mengimplementasikan contract sesuai jenisnya yang dimasukkan, dan hasil discovery diurutkan agar dua mesin menghasilkan manifest yang sama.
Registrasi eksplisit tetap didukung dan digabungkan dengan hasil discovery. Pola ini digunakan ketika Panel perlu memakai class yang berada di luar directory tree miliknya.
Discovery path bersifat accumulating. Memanggil discoverWidgets() dua kali akan menambahkan dua directory.
Default Action untuk seluruh Panel
public function configureActions(Closure $callback): self // Closure(Action): void
public function actionConfigurator(): ?Closure2
$panel->configureActions(static function (Action $action): void {
if ($action->getVariant() === ActionVariant::Destructive) {
$action->requiresConfirmation();
}
});2
3
4
5
Ini menerapkan house style saat setiap Action dibangun. Schema tetap dapat menentukan konfigurasi Action miliknya sendiri setelah configurator berjalan. Konfigurasi ini bersifat request-scoped melalui current Panel, bukan configurator statis, sehingga dua Panel dapat memiliki aturan berbeda tanpa terjadi kebocoran antar-request.
Apa yang dihasilkan Panel
Dengan seluruh file example tersedia, /admin menyediakan route berikut:
| URL | Route name | Class |
|---|---|---|
/admin | panel.admin.dashboard | PandaPanel\Pages\Dashboard |
/admin/accounts | panel.admin.pages.accounts | AccountsDashboard |
/admin/settings | panel.admin.pages.settings | App\Panels\Admin\Pages\Settings |
/admin/users | panel.admin.resources.users.index | ListUsers |
/admin/users/create | .create, .store | CreateUser |
/admin/users/{record} | .view | ViewUser |
/admin/users/{record}/edit | .edit, .update | EditUser |
/admin/settings/profile | panel.admin.pages.settings-profile | bawaan |
/admin/settings/security | bawaan | berada di balik RequirePassword |
/admin/settings/appearance | bawaan | pengaturan tema client-side |
Selain itu, setiap Panel memiliki endpoint bersama seperti panel.admin.search, .options, .uploads, .form-state, .export-file, .import-file, .notifications.*, serta .actions.record / .bulk / .table / .infolist / .form / .submit / .cell / .reorder.
Class yang ditemukan dari example:
| File | Fungsinya |
|---|---|
Resources/Users/UserResource.php | resource user — lihat User Resource |
Pages/Settings.php | standalone page dengan Vue component sendiri |
Pages/AccountsDashboard.php | dashboard kedua |
Widgets/UserStats.php | StatsWidget, polling setiap 60 detik |
Widgets/UserGrowth.php | ChartWidget, lazy, dengan filter sendiri |
Widgets/RecentUsers.php | TableWidget |
Widgets/SystemInfo.php | CustomWidget dengan Vue component |
Perilaku yang layak dipertimbangkan untuk diaktifkan
Tidak satu pun konfigurasi berikut ada pada provider example, dan masing-masing sebaiknya dipilih berdasarkan kebutuhan, bukan disalin otomatis.
public function databaseTransactions(bool $databaseTransactions = true): self // on by default
public function strictAuthorization(bool $strictAuthorization = true): self // off by default
public function unsavedChangesAlerts(bool $unsavedChangesAlerts = true): self // on by default
public function bootUsing(Closure $callback): self // Closure(Panel): void
public function settings(bool $settings = true): self
public function notifications(bool $notifications = true): self
public function broadcasting(bool $broadcasting = true): self
public function globalSearch(bool $enabled = true, int $limit = 50, int $debounce = 300, array $keyBindings = ['mod+k']): self
public function assets(string ...$entrypoints): self
public function prefetch(bool|string $prefetch = 'hover'): self
public function subNavigationPosition(SubNavigationPosition $position): self2
3
4
5
6
7
8
9
10
11
$panel
->strictAuthorization() // a missing policy throws instead of denying
->globalSearch(limit: 30, keyBindings: ['mod+k', 'ctrl+k'])
->assets('resources/css/panels/admin.css') // must also be in vite.config.ts input
->bootUsing(static function (Panel $panel): void {
// Runs per request, after the access check passes.
});2
3
4
5
6
7
strictAuthorization() sangat layak diaktifkan sejak awal development. Fitur ini mengubah policy yang hilang, atau policy yang tidak mendefinisikan ability yang sedang diperiksa, menjadi PanelAuthorizationException alih-alih silent denial yang mudah disalahartikan sebagai rule yang memang sengaja menolak.
Memverifikasi konfigurasi
php artisan route:list --name=panel.admin
php artisan panel:cache # writes bootstrap/cache/panels.php
php artisan test --compact --filter=AdminPanelExample2
3
tests/Feature/Panel/AdminPanelExampleTest.php memastikan dashboard merender empat widget, navigation dibangun dari class hasil discovery mengikuti urutan group yang dideklarasikan, dan seluruh lifecycle user — list, create, view, edit, serta delete melalui action endpoint — bekerja untuk administrator.
Hal yang perlu diperhatikan
panel()berjalan saat provider boot. Jangan resolve service, membaca authenticated user, atau membuat URL di dalamnya karena request-scoped binding belum siap. Pekerjaan yang membutuhkan hal tersebut sebaiknya diletakkan dibootUsing().- Icon yang tidak ada di registry tidak dirender dan tidak melempar error.
php artisan panel:iconsmembangun ulang registry dari source dan gagal secara eksplisit ketika nama Lucide icon tidak tersedia. Gunakan--checkpada CI jika hanya ingin validasi tanpa menulis ulang file. ->assets()membutuhkan dua perubahan. Path asset juga harus ada padainputdivite.config.ts; jika tidak, page gagal dengan manifest error.canAccess()menghasilkan 403, bukan redirect. Authenticated user yang gagal predicate tidak dikirim kembali ke halaman login yang sudah mereka lewati.- Ketika manifest sudah di-cache, discovery tidak berjalan. Setelah menambahkan resource, page, atau widget di production, jalankan kembali
php artisan panel:cache.optimizedanoptimize:clearsudah menyertakan lifecycle cache Panel.
Lihat juga
- Contoh App Panel — Panel kedua dan mekanisme isolasi di antara keduanya
- User Resource — resource yang ditemukan Panel ini
- Mendefinisikan Panel
- Panel API Reference
- Dashboard
- Panel Access
- Discovery
- Caching
- make:panel