Aplikasi Multi-Panel
Satu aplikasi dapat memiliki lebih dari satu panel: /admin untuk staff dan /app untuk customer, reporting panel pada subdomain sendiri, atau back office yang memakai model yang sama dengan sisi customer tetapi hanya menampilkan subset data tertentu. Panel terisolasi secara struktural — masing-masing memiliki registry, route, dan access rule sendiri — sehingga tantangan utamanya adalah menentukan apa yang ingin dibagikan, bukan bagaimana memisahkannya.
Dua panel
// config/panda-panel.php
'panels' => [
App\Panels\Admin\AdminPanelProvider::class,
App\Panels\App\AppPanelProvider::class,
],2
3
4
5
6
<?php
declare(strict_types=1);
namespace App\Panels\App;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AppPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('app')
->name('Application')
->icon('layout-grid')
->auth()
->discoverResources(app_path('Panels/App/Resources'))
->discoverPages(app_path('Panels/App/Pages'))
->discoverWidgets(app_path('Panels/App/Widgets'));
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Setiap panel melakukan discovery dari directory tree miliknya sendiri. Itulah mekanisme isolasinya: app/Panels/Admin/Resources adalah milik Admin dan tidak otomatis menjadi bagian panel lain.
Urutan registrasi menentukan panel default
Urutan di config/panda-panel.php adalah urutan yang digunakan PanelManager::firstAccessibleTo():
use PandaPanel\Core\PanelManager;
app(PanelManager::class)->firstAccessibleTo(request()->user()); // ?Panel2
3
Dua fitur menggunakan hasilnya dan keduanya memengaruhi screen pertama setelah login:
RedirectPanelHomemengirim signed-in user yang membuka/dashboardke panel pertama yang dapat mereka masuki — lihat Home Redirect.- Alamat
/settings/*milik starter kit melakukan redirect ke panel yang sama.
Dengan id admin dan app, urutan id akan memeriksa Admin sebelum App: administrator masuk ke /admin, sedangkan user biasa yang ditolak Admin masuk ke /app. Mengubah id panel dapat mengubah hasil tersebut, sehingga id adalah bagian dari policy routing.
PanelManager::all() diurutkan berdasarkan id, bukan urutan config, dan urutan inilah yang digunakan oleh route registration maupun firstAccessibleTo() agar hasilnya stabil antar-machine.
Hal yang terisolasi
Semua yang didaftarkan panel bersifat scoped ke panel tersebut.
$manager = app(PanelManager::class);
$manager->resources('admin')->all(); // list<class-string>
$manager->pages('app')->all();
$manager->widgets('admin')->all();
$manager->navigation('admin'); // NavigationRegistry2
3
4
5
6
- Resource yang didaftarkan pada Admin tidak memiliki route di App.
/app/usersmenghasilkan 404, bukan 403. - Page slug yang terdaftar di satu panel tidak terdaftar pada panel lain.
- Action endpoint tersedia pada setiap panel, tetapi resource name pada payload di-resolve terhadap registry panel tersebut. Mengalamatkan Admin resource melalui
/app/actions/recordmenghasilkan 404 — resource tersebut tidak ada di panel App, apa pun session user-nya. - Widget bersifat per-panel, sehingga dashboard hanya pernah menampilkan widget milik panel tersebut.
Resource::url(panel: 'app')melemparPanelRegistrationExceptionjika resource tidak terdaftar di panel tujuan daripada menghasilkan URL ke route yang tidak ada.
use App\Panels\Admin\Resources\Users\UserResource;
UserResource::url(panel: 'admin'); // '/admin/users'
UserResource::url(panel: 'app'); // throws: not registered in the panel [app]2
3
4
Membagikan satu resource ke beberapa panel
Class resource yang sama boleh didaftarkan pada dua panel. PandaPanel\Resources\ResourceConfiguration digunakan untuk menjelaskan arti resource tersebut pada masing-masing panel: slug berbeda, posisi navigation berbeda, atau query yang lebih sempit.
use App\Panels\Admin\Resources\Users\UserResource;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Resources\ResourceConfiguration;
$panel->resources([
ResourceConfiguration::for(UserResource::class)
->slug('people')
->pluralLabel('People')
->navigationLabel('Directory')
->navigationGroup('Company')
->navigationIcon('building-2')
->navigationSort(99)
->modifyQueryUsing(
static fn (Builder $query): Builder => $query->where('is_admin', false),
),
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class yang sama sekarang dapat menjadi /admin/users pada satu panel dan /directory/people pada panel lain. Panel kedua juga tidak dapat membaca row administrator sama sekali — modifyQueryUsing() mempersempit Resource::query(), dan seluruh read melewati query tersebut. Record di luar scope menghasilkan 404, bukan sekadar row yang disembunyikan dari table.
Aturan yang perlu diketahui:
- Configuration didaftarkan sebelum bare class, sehingga class yang memiliki per-panel configuration tidak didaftarkan lagi menggunakan default slug.
- Satu class tidak boleh didaftarkan dua kali pada satu panel. Resource registry dikey berdasarkan slug, sedangkan
Resource::url()tidak memiliki cara untuk menentukan registration mana yang dimaksud jika class yang sama memiliki dua entry. - Route name tetap menggunakan slug resource pada panel tersebut:
panel.directory.resources.people.index.
Lihat Per-Panel Configuration untuk daftar option lengkap.
Panel switcher
Setiap panel page mengirim daftar panel yang boleh dimasuki current user melalui shared prop panels, yang dibangun oleh SharePanelData:
[
'id' => 'admin',
'name' => 'Administrator',
'brandName' => 'Acme',
'path' => '/admin',
'icon' => 'shield',
'url' => '/admin',
'current' => true,
]2
3
4
5
6
7
8
9
Daftar difilter menggunakan Panel::isAccessibleTo() — predicate yang sama dengan route — sehingga panel yang akan menolak user tidak pernah ditawarkan sebagai destination. Di luar panel, list kosong. Header control menyembunyikan dirinya ketika user hanya dapat mengakses satu panel karena menawarkan perpindahan ke panel yang sedang dibuka bukanlah kontrol yang berguna.
import { usePanel } from '@/panel/composables/usePanel';
const { panels, canSwitchPanels } = usePanel(); // canSwitchPanels is panels.length > 12
3
Lihat Panel Switcher.
Membagi panel berdasarkan domain
Panel boleh memiliki path yang sama jika domain-nya berbeda. Ini memungkinkan central panel dan per-tenant panel hidup berdampingan:
$admin->domain('admin.example.com')->path('/');
$app->domain('{team}.example.com')->path('/');2
PanelManager::resolveFromRequest() melewati panel yang domain-nya tidak cocok dengan request host, sehingga kedua panel tidak saling berebut route. Dua panel dengan path dan domain yang sama melempar exception saat boot.
Dua panel, satu user model
Panel access diperiksa melalui dua rule dan keduanya harus setuju: predicate canAccess() pada panel dan PanelUser::canAccessPanel() pada user model. Dalam aplikasi multi-panel, rule kedua biasanya menjadi tempat account-level constraint — account suspended harus suspended di seluruh panel. Menempatkan rule pada model membuat constraint tersebut tidak terlupakan ketika panel baru ditambahkan.
use PandaPanel\Contracts\PanelUser;
use PandaPanel\Core\Panel;
final class User extends Authenticatable implements PanelUser
{
public function canAccessPanel(Panel $panel): bool
{
return ! $this->suspended;
}
}2
3
4
5
6
7
8
9
10
Lihat Panel Access Rules.
Catatan
- Dua panel yang memakai satu build berbagi frontend bundle yang sama. Styling per-panel dilakukan dengan
colors(),cssHooks(), danassets(), bukan build kedua — lihat Branding dan Panel Assets. - Panel manifest memiliki satu section per panel, sehingga
php artisan panel:cachemeng-cache semuanya dalam satu file. Menambah panel berarti cache perlu dibangun ulang. ResetPanelContextmembersihkan current panel pada awal setiap web request sehingga state tidak bocor antara dua panel dalam satu Octane worker atau satu test process.- Settings page tersedia per-panel secara default. Karena itu
/admin/settings/profiledan/app/settings/profilekeduanya ada dan dirender menggunakan shell masing-masing. Gunakansettings(false)untuk panel yang memang tidak membutuhkan account settings.