Referensi Pages
PandaPanel\Pages\Page digunakan untuk membuat screen di dalam Panel yang bukan Resource.
Sebuah Page:
- tidak memiliki model;
- tidak memiliki table;
- tidak memiliki record;
- tetapi tetap mendapatkan shell Panel;
- navigation;
- breadcrumbs;
- authorization;
- widgets;
- filters;
- render hooks;
- dan routing.
Gunakan Page untuk screen seperti:
- system status;
- reports;
- settings;
- analytics;
- dashboards tambahan;
- monitoring;
- application-wide configuration;
- atau screen lain yang tidak secara langsung merepresentasikan satu Resource.
Page Paling Sederhana
<?php
namespace App\Panels\Admin\Pages;
use PandaPanel\Pages\Page;
final class SystemStatus extends Page
{
protected static ?string $title =
'System status';
public function props(): array
{
return [
'queue' =>
config('queue.default'),
'env' =>
app()->environment(),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Jika class tersebut berada di discovery path Panel, PandaBear otomatis mendaftarkannya.
Misalnya Panel memiliki path:
/adminmaka Page tersebut tersedia di:
/admin/system-statusdengan route name:
panel.admin.pages.system-statusdan secara default juga muncul di sidebar.
Page Tidak Selalu Membutuhkan File Vue
Page sederhana tidak membutuhkan custom Vue component.
Property:
$componentsecara default menggunakan:
panel/PageGeneric renderer tersebut dapat menampilkan:
- heading;
- breadcrumbs;
- header actions;
- widgets;
- filters;
- serta data dari
props().
Jadi untuk Page yang hanya membutuhkan data server sederhana:
public function props(): arraysudah cukup.
Declarations
Property utama pada Page:
| Property | Type | Default |
|---|---|---|
$title | ?string | headline dari basename class |
$heading | ?string | null, mengikuti title |
$subheading | ?string | null |
$slug | ?string | kebab-case basename class |
$component | string | 'panel/Page' |
$navigationLabel | ?string | mengikuti title |
$navigationIcon | ?string | null |
$activeNavigationIcon | ?string | mengikuti navigation icon |
$navigationGroup | string|BackedEnum|null | null |
$navigationSort | int | 0 |
$shouldRegisterNavigation | bool | true |
$cluster | ?class-string<Cluster> | null |
$middleware | list<string> | [] |
Contoh konfigurasi:
use BackedEnum;
use PandaPanel\Pages\Page;
final class Settings extends Page
{
protected static ?string $title =
'Settings';
protected static ?string $subheading =
'Application-wide configuration.';
protected static ?string $slug =
'settings';
protected static string $component =
'Panels/Admin/Pages/Settings';
protected static ?string
$navigationIcon =
'settings';
protected static string|BackedEnum|null
$navigationGroup =
'System';
protected static int
$navigationSort =
100;
}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
$component
$component adalah nama Inertia Page.
Component di-resolve dari:
resources/js/pages/**Contoh:
protected static string $component =
'Panels/Admin/Pages/Settings';2
mengarah pada application-owned component seperti:
resources/js/pages/
└── Panels/
└── Admin/
└── Pages/
└── Settings.vue2
3
4
5
Sedangkan nama:
panel/Page
panel/Dashboard
panel/settings/Profile2
3
merupakan component yang dipublish oleh PandaBear.
$component merupakan build-time name, bukan arbitrary runtime path.
Static API
public static function slug(): string;
public static function routePath(): string;
// slug atau '{cluster}/{slug}'
public static function title(): string;
public static function heading(): string;
// mengikuti title()
public static function cluster(): ?string;
public static function activeNavigationIcon(): ?string;
public static function middleware(): array;
public static function canAccess(): bool;
// default true
public static function navigationItem(
PanelContract $panel
): ?NavigationItem;
public static function routeName(
Panel|string|null $panel = null
): string;
public static function url(
Panel|string|null $panel = null
): string;
public static function renderHookScope(): string;
// 'page:{slug}'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
url() dan routeName()
Jika Panel tidak diberikan:
Settings::url();
Settings::routeName();2
3
PandaBear mencoba menggunakan current Panel.
Contoh:
Settings::url();
// '/admin/settings'
Settings::url('app');
// Page yang sama pada Panel 'app'
Settings::routeName();
// 'panel.admin.pages.settings'2
3
4
5
6
7
8
Jika dipanggil di luar Panel request dan tidak ada explicit Panel:
PanelRegistrationException::noCurrentPanel()akan dilempar.
routePath()
Secara default routePath() mengikuti slug.
Namun Anda dapat mengoverride path tanpa mengubah slug.
Contoh:
public static function routePath(): string
{
return 'settings/profile';
}2
3
4
Dengan begitu:
slug
→ profile
route name
→ menggunakan slug
URL path
→ settings/profile2
3
4
5
6
7
8
Ini penting karena slug memiliki dua fungsi:
- registry key;
- route name segment.
Sedangkan route path menentukan apa yang terlihat di address bar.
Instance API
public function props(): array;
// []
public function widgets(): array;
// []
// list<class-string<Widget>>
public function breadcrumbs(): array;
// list<Breadcrumb>
public function headerActions(): array;
// []
public function filterSchema():
?FormSchema;
// null
public function render(): Response;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
render()
render() memulai dengan authorization:
abort_unless(
static::canAccess(),
403
);2
3
4
Setelah itu Page mengirim lima kelompok props utama:
page
widgets
widgetData
filters
+
props() milik Page2
3
4
5
6
Secara konsep:
Page::render()
↓
canAccess()
↓
metadata()
↓
resolve widgets
↓
resolve filters
↓
props()
↓
Inertia2
3
4
5
6
7
8
9
10
11
12
13
props()
props() merupakan payload khusus Page.
Return value harus serializable.
Contoh:
public function props(): array
{
return [
'settings' => [
[
'label' =>
'Application name',
'value' =>
(string)
config('app.name'),
],
[
'label' =>
'Environment',
'value' =>
app()->environment(),
],
],
];
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Gunakan props() untuk data yang memang hanya dibutuhkan Page tersebut.
breadcrumbs()
Default breadcrumbs:
Dashboard
↓
Navigation Group jika ada
↓
Current Page2
3
4
5
Contoh custom:
use PandaPanel\Support\Breadcrumb;
public function breadcrumbs(): array
{
return [
Breadcrumb::make(
'Dashboard'
)
->url(
$this->dashboardUrl()
),
Breadcrumb::make(
'Reports'
)
->url(
ReportsIndex::url()
),
Breadcrumb::make(
static::title()
)
->current(),
];
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
headerActions()
Standalone Page tidak menggunakan Action endpoint seperti Resource Action secara otomatis.
headerActions() mengembalikan serialized Action array, bukan object Action.
Contoh:
use PandaPanel\Actions\Enums\ActionVariant;
use PandaPanel\Pages\Settings\ProfileSettings;
public function headerActions(): array
{
return [[
'name' =>
'edit-profile',
'label' =>
'Edit profile',
'icon' =>
'settings',
'variant' =>
ActionVariant::Default->value,
'type' =>
'link',
'url' =>
ProfileSettings::url(
$this->panel()
),
'confirmation' =>
null,
]];
}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
Shape tersebut mengikuti output yang biasa dihasilkan:
Action::toArray()Untuk standalone Page, header Action terutama digunakan sebagai link.
canAccess()
Authorization Page bukan hanya mekanisme untuk menyembunyikan navigation.
Route sendiri melakukan enforcement.
Contoh:
public static function canAccess(): bool
{
return auth()
->user()
?->can(
'viewReports'
) === true;
}2
3
4
5
6
7
8
Jika user tidak diizinkan:
403dikembalikan bahkan jika user mencoba membuka URL Page secara manual.
Prinsipnya:
hidden navigation
≠
authorization2
3
Gunakan Middleware jika Harus Redirect
canAccess() hanya dapat menjawab:
yes
atau
no2
3
Jika flow membutuhkan redirect daripada denial, gunakan middleware.
Contohnya password confirmation:
use Illuminate\Auth\Middleware\RequirePassword;
protected static array $middleware = [
RequirePassword::class,
];2
3
4
5
Middleware tersebut ditambahkan di atas middleware stack milik Panel.
Ini lebih tepat untuk flow seperti:
- password reconfirmation;
- signed URL validation;
- custom challenge.
Protected Helpers
protected function metadata(): array;
protected function panel(): Panel;
protected function dashboardUrl(): string;
protected function resolveWidgets(
?WidgetFilters $filters = null
): WidgetCollection;
protected function resolveFilters():
WidgetFilters;
protected function filterSessionKey(): string;
// 'panel.{id}.page.{slug}'
protected static function resolvePanel(
Panel|string|null $panel
): Panel;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
metadata()
metadata() membangun prop:
pageyang berisi:
title
heading
subheading
breadcrumbs
headerActions
scope
cluster2
3
4
5
6
7
Widgets pada Page
Page dapat mendeklarasikan Widget:
use PandaPanel\Widgets\Widget;
/**
* @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
WidgetCollection
Widget resolution dilakukan melalui:
PandaPanel\Pages\WidgetCollectionAPI:
public static function for(
array $classes,
?PageContext $context = null,
?WidgetFilters $filters = null
): self;
public static function filterSchemas(
array $classes
): array;
// array<string, FormSchema>
// keyed by widget id
public function merge(
self $other
): self;
public function definitions(): array;
// list<array>
public function deferred(): mixed;
// Inertia::defer(...) atau null2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Widget Authorization
Sebelum data Widget diproses:
Widget::canView()diperiksa terlebih dahulu.
Jika false:
canView()
↓
false
↓
Widget tidak dimasukkan
↓
data() tidak dipanggil
↓
query tidak berjalan2
3
4
5
6
7
8
9
Jadi Widget yang tidak boleh dilihat user tidak hanya disembunyikan di frontend.
Backend juga tidak menjalankan data query-nya.
Urutan Widget
Widgets diurutkan berdasarkan:
[sort(), id()]Dengan demikian order konsisten pada setiap request.
Eager dan Lazy Widgets
Eager Widget membawa data langsung bersama Page response.
Secara konsep:
Page request
↓
Widget data dihitung
↓
Inertia response membawa data2
3
4
5
Lazy Widget berbeda.
Definition dikirim lebih dulu dengan:
data = nullKemudian seluruh lazy widget data dikumpulkan ke satu deferred prop.
Secara konsep:
Page Request
├── Page
├── Eager Widgets
└── Lazy Widget Definitions
↓
Deferred Request
└── Lazy Widget Data2
3
4
5
6
7
8
9
Dengan demikian aggregate lambat hanya menunda dirinya sendiri.
Jika Tidak Ada Lazy Widget
Jika tidak ada Widget lazy:
deferred()mengembalikan:
nullPage tidak mengiklankan deferred request yang sebenarnya tidak diperlukan.
Page-Level Filters
Sebuah Page dapat memiliki satu Form Schema yang menjadi filter untuk seluruh Widget di Page tersebut.
Contoh:
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
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
Default:
nullJika filterSchema() mengembalikan null:
filters = nulldan frontend tidak merender empty filter bar.
Filter State Berada di Query String
Sama seperti Table State, Page Filter disimpan di query string.
Contoh:
/admin/reports?period=monthKeuntungannya:
filtered dashboard
→ shareable URL2
User dapat mengirim URL yang merepresentasikan view yang sama ke user lain.
Filter Diingat per Page
Filter state tidak disimpan global per Panel.
State bersifat per Page.
Contohnya:
Dashboard A:
period = month
Dashboard B:
period = year2
3
4
5
Keduanya dianggap pertanyaan yang berbeda dan tidak saling menimpa.
Widget Dapat Memiliki Filter Sendiri
Widget tetap dapat memiliki filter tambahan sendiri.
Ketika Page filter dan Widget filter digabungkan:
Page filters
+
Widget filters2
3
Widget-level filter memiliki prioritas jika key sama.
Secara konsep:
merged = [
...pageFilters,
...widgetFilters,
]2
3
4
Dashboard
Built-in Dashboard:
namespace PandaPanel\Pages;
class Dashboard extends Page
{
protected static ?string $title =
'Dashboard';
protected static ?string $slug =
'dashboard';
protected static string $component =
'panel/Dashboard';
protected static ?string
$navigationIcon =
'layout-grid';
public function widgets(): array;
public function breadcrumbs(): array;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Dashboard merupakan Page khusus yang Widget-nya berasal dari Panel Widget Registry.
Bukan dari daftar Widget milik Page itu sendiri.
Dashboard Berada di Root Panel
Dashboard utama dirender di:
/adminbukan:
/admin/dashboardController:
PanelDashboardControllerme-resolve:
Panel::getDashboard()kemudian memanggil:
render()Dashboard Tambahan
Anda dapat membuat dashboard kedua.
Contoh:
use PandaPanel\Pages\Dashboard;
final class AccountsDashboard
extends Dashboard
{
protected static ?string $title =
'Accounts';
protected static ?string $slug =
'accounts';
protected static ?string
$navigationIcon =
'users';
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
Kemudian:
$panel->dashboards([
Dashboard::class,
AccountsDashboard::class,
]);2
3
4
Dashboard Pertama adalah Root
Pada:
$panel->dashboards([
Dashboard::class,
AccountsDashboard::class,
]);2
3
4
Dashboard pertama:
Dashboard::classmenjadi root Panel.
Dashboard sisanya didaftarkan sebagai Page biasa.
Artinya dashboard tambahan:
- memiliki route sendiri;
- memiliki authorization sendiri;
- dapat muncul di navigation;
- memiliki filter state sendiri.
Method:
Panel::getExtraDashboards()mengembalikan dashboard selain yang pertama.
Built-In Settings Pages
Setiap Panel mendapatkan tiga Settings Page secara default kecuali:
Panel::settings(false)digunakan.
Ketiganya adalah subclass Page biasa.
Artinya:
- routing;
- authorization;
- navigation;
- discovery behavior;
tetap mengikuti mekanisme Page biasa.
Daftar Settings Page
| Class | Slug | Path | Component | Group |
|---|---|---|---|---|
PandaPanel\Pages\Settings\ProfileSettings | settings-profile | settings/profile | panel/settings/Profile | Account, sort 10 |
PandaPanel\Pages\Settings\SecuritySettings | settings-security | settings/security | panel/settings/Security | Account, sort 20 |
PandaPanel\Pages\Settings\AppearanceSettings | settings-appearance | settings/appearance | panel/settings/Appearance | Account, sort 30 |
ProfileSettings
ProfileSettings hanya menangani rendering.
Save profile tetap diarahkan ke profile controller milik application.
Tujuannya:
Tetap hanya ada satu tempat yang bertanggung jawab menulis profile.
PandaBear tidak membuat duplicate profile write flow.
SecuritySettings
Page ini menggunakan:
RequirePasswordpada:
$middlewarebukan pada:
canAccess()Alasannya stale session perlu diarahkan ke password confirmation.
Jika menggunakan:
canAccess()hasilnya hanya:
403padahal yang dibutuhkan adalah:
redirect ke password confirmation
↓
user confirm
↓
kembali ke security settings2
3
4
5
AppearanceSettings
Page ini tidak mengirim props tambahan.
Theme preference disimpan frontend menggunakan:
- local storage;
- cookie.
Jadi tidak diperlukan Page-specific backend state.
Settings Page Masuk Melalui getPages()
Built-in settings pages digabungkan melalui:
Panel::getPages()Dengan demikian:
- discovery;
- caching;
- route registration;
memperlakukan built-in settings sama seperti custom Page.
Clusters
Cluster merupakan kombinasi:
URL prefix
+
shared sub-navigation2
3
Membership dideklarasikan oleh Page atau Resource yang menjadi member.
Contoh Cluster
use PandaPanel\Clusters\Cluster;
use PandaPanel\Enums\ClusterPosition;
final class SettingsCluster
extends Cluster
{
protected static ?string $title =
'Settings';
protected static ?string
$navigationIcon =
'settings';
protected static ClusterPosition
$position =
ClusterPosition::Header;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Kemudian Page:
final class RolesPage extends Page
{
protected static ?string $cluster =
SettingsCluster::class;
}2
3
4
5
Cluster Mengubah Route Path
Jika:
SettingsCluster
→ slug settings
RolesPage
→ slug roles2
3
4
5
maka:
RolesPage::routePath()menjadi:
settings/rolesNamun route name tetap:
panel.{id}.pages.rolesJadi Cluster hanya mengubah:
URL pathbukan:
Page registry key
route name2
Karena itu:
RolesPage::url()tetap bekerja tanpa perlu mengetahui implementasi prefix Cluster.
Cluster Navigation
Sub-navigation Cluster dibangun melalui:
PandaPanel\Support\ClusterNavigationMember yang tidak diizinkan melalui authorization tidak dimasukkan.
Jika seluruh member Cluster ditolak:
Cluster navigation tidak direnderdaripada menampilkan empty bar.
ClusterPosition
Pilihan:
ClusterPosition::Header
ClusterPosition::RightBar
ClusterPosition::Sidebar2
3
4
5
Route Registration
Secara konsep route Page didaftarkan:
$this->router
->get(
$page::routePath(),
PanelPageController::class
)
->defaults(
'page',
$page
)
->name(
'pages.'
. $page::slug()
);2
3
4
5
6
7
8
9
10
11
12
13
Page Class Tidak Berasal dari Request
Class Page disimpan sebagai route default:
defaults(
'page',
$page
)2
3
4
Controller tidak melakukan:
$request->pageClassatau me-resolve arbitrary class name dari URL/request.
Request hanya mengakses route yang sudah didaftarkan.
Collision antara Page dan Resource
Page slug diperiksa terhadap Resource slug saat registration.
Jika Page dan Resource menggunakan slug yang sama:
PageRegistrymelempar:
slugCollidesWithResource()Hal ini mencegah Page dan Resource saling menimpa route secara diam-diam.
Page Middleware
Jika:
Page::middleware()mengembalikan array non-empty, middleware tersebut diterapkan pada route Page.
Jika kosong:
[]tidak ada additional middleware.
Render Hooks
Metadata Page memiliki scope:
page:{slug}Contoh:
page:settingsRender Hook dapat dibatasi hanya untuk Page tertentu.
Contoh:
use PandaPanel\Enums\RenderHook;
$panel->renderHook(
RenderHook::PageStart,
'AnnouncementBanner',
[
'message' =>
'Maintenance at 22:00',
],
[
Settings::class,
],
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class:
Settings::classdireduksi menjadi scope:
page:settingssebelum dikirim ke frontend.
Class Name Tidak Dikirim ke Vue
Frontend tidak menerima PHP class name seperti:
App\Panels\Admin\Pages\SettingsSebaliknya server mengubahnya menjadi identifier:
page:settingsDengan demikian internal PHP namespace tidak menjadi frontend API.
Hal yang Perlu Diperhatikan
Page Generik Tidak Membutuhkan Vue File
Jika Page tidak memiliki custom UI:
panel/Pagesudah dapat merender:
- heading;
- subheading;
- breadcrumbs;
- header actions;
- widgets;
- filters;
- generic body dari
props().
$component adalah Build-Time Name
$component bukan filesystem path arbitrary.
Inertia harus sudah mengetahui component tersebut melalui:
resources/js/pages/**Component name yang tidak pernah ditemukan build tidak akan dapat di-resolve.
canAccess() Bersifat Static
Method:
public static function canAccess(): booldijalankan sebelum Page instance dibuat.
Karena itu canAccess() tidak dapat bergantung pada data yang baru dihitung instance.
Jangan mengandalkan:
$this->somethingdi dalam static authorization.
Dashboard Memiliki Slug tetapi Tidak Menggunakan Page Registry
Built-in:
PandaPanel\Pages\Dashboardmemiliki slug:
dashboardtetapi Dashboard utama tidak didaftarkan sebagai ordinary Page.
Dashboard utama ditangani oleh:
PanelDashboardControllerpada root:
/adminController me-resolve:
Panel::getDashboard()kemudian memanggil:
render()Hanya dashboard tambahan dari:
dashboards()yang mendapatkan ordinary Page route.
title() dan heading() Sengaja Dipisahkan
Secara default:
heading follows titleTetapi keduanya dapat dipisahkan.
Misalnya:
protected static ?string $title =
'System Health | Admin';
protected static ?string $heading =
'System Health';2
3
4
5
Title dapat digunakan untuk browser tab.
Heading digunakan sebagai heading visual Page.
Page Hanya Didiscover jika Mengimplementasikan PageContract
Discovery hanya mendaftarkan class yang:
- concrete;
- mengimplementasikan
PageContract.
Abstract base Page di directory yang sama tidak akan diregistrasikan.
Ringkasan Page Lifecycle
Discovery
↓
PageContract?
↓
register Page
↓
PageRegistry
↓
Route registration
↓
HTTP request
↓
Panel middleware
↓
Page middleware
↓
PanelPageController
↓
Page::render()
↓
canAccess()
↓
metadata()
↓
filters
↓
widgets
↓
props()
↓
Inertia response2
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
Ringkasan Page Metadata
Page
├── title
├── heading
├── subheading
├── breadcrumbs
├── headerActions
├── scope
└── cluster2
3
4
5
6
7
8
Metadata tersebut menjadi:
pageprop pada frontend.
Ringkasan Navigation
Page
↓
shouldRegisterNavigation?
↓
canAccess?
↓
navigationLabel
navigationIcon
navigationGroup
navigationSort
↓
NavigationItem
↓
Sidebar2
3
4
5
6
7
8
9
10
11
12
13
14
Tetapi navigation visibility bukan authorization.
Route tetap melakukan:
canAccess()secara terpisah.
Ringkasan Widget Resolution
Page::widgets()
↓
WidgetCollection::for()
↓
Widget::canView()
↓
allowed?
┌────┴────┐
│ │
no yes
│ ↓
skip eager/lazy?
↓
definitions
↓
widget data2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Widget yang ditolak tidak menjalankan query.
Ringkasan Lazy Widgets
Initial Page Request
↓
Widget Definitions
├── eager → data included
└── lazy → data null
↓
Inertia render
↓
Deferred request
↓
Lazy widget payload2
3
4
5
6
7
8
9
10
11
Ringkasan Page Filters
URL Query String
↓
Page Filter Schema
↓
WidgetFilters
↓
Widget
↓
Widget-specific filters
↓
Merge
↓
Widget filter wins on collision2
3
4
5
6
7
8
9
10
11
12
13
Filter Page berada di query string sehingga dapat dibagikan melalui URL.
Ringkasan Dashboard
Panel
├── Dashboard utama
│ └── Panel root
│
└── Extra dashboards
├── ordinary Page
├── own route
├── own authorization
├── own navigation
└── own filters2
3
4
5
6
7
8
9
10
Ringkasan Cluster
SettingsCluster
├── prefix: settings
├── shared navigation
│
├── RolesPage
│ └── /settings/roles
│
└── PermissionsPage
└── /settings/permissions2
3
4
5
6
7
8
9
Route name masing-masing Page tetap berdasarkan slug Page.
Perbedaan Page dan Resource
| Aspek | Page | Resource |
|---|---|---|
| Model | tidak | ya |
| Record | tidak | ya |
| Table | optional melalui custom UI/widget | built-in |
| Forms | optional | built-in Resource Forms |
| CRUD | tidak otomatis | ya |
| Navigation | ya | ya |
| Authorization | canAccess() | Resource abilities/policies |
| Widgets | ya | dapat melalui dashboard/page |
| Filters | Page-level Widget filters | Table filters |
| URL | Page::routePath() | Resource Pages |
| Cluster | ya | ya |
Prinsip Utama Pages
PandaBear menggunakan Page untuk screen yang membutuhkan seluruh infrastructure Panel tetapi tidak merepresentasikan satu model CRUD.
Secara konsep:
Resource menjawab "bagaimana mengelola record dari sebuah model?", sedangkan Page menjawab "bagaimana menampilkan sebuah screen di dalam Panel?"
Page tetap mendapatkan:
Panel Context
Navigation
Authorization
Breadcrumbs
Widgets
Filters
Clusters
Render Hooks
Inertia Shell2
3
4
5
6
7
8
9
tanpa harus memiliki:
Model
Record
Table
CRUD2
3
4
Dengan pemisahan ini, screen seperti analytics, reports, settings, monitoring, dan custom workflows tidak perlu dipaksa menjadi Resource palsu hanya agar mendapatkan shell dan navigation PandaBear.