Component Registries
Metadata Panel mereferensikan Vue component dan icon menggunakan nama. Nama tersebut hanya dapat di-resolve melalui registry build-time yang disediakan framework. Jika sebuah nama tidak ikut dikompilasi ke dalam bundle, frontend akan merender fallback — framework tidak melakukan fetch component secara dinamis.
Gunakan dokumentasi ini ketika Custom Column, Field, Widget, Hook, Shell component, atau icon tidak menampilkan apa pun meskipun metadata PHP sudah terlihat benar.
Bentuk Aturannya
Sebuah class PHP mendeklarasikan nama component:
use PandaPanel\Widgets\CustomWidget;
final class SystemInfo extends CustomWidget
{
protected static string $component = 'Panels/Admin/Widgets/SystemInfo';
}2
3
4
5
6
Kemudian harus ada file Vue pada path yang tepat di bawah resources/js/pages/:
resources/js/pages/Panels/Admin/Widgets/SystemInfo.vueRegistry menghubungkan nama tersebut dengan file:
import { resolveWidgetComponent } from '@/panel/widgets/registry';
const loader = resolveWidgetComponent('Panels/Admin/Widgets/SystemInfo');
// () => Promise<{ default: Component }> — or null2
3
4
Nama component adalah path relatif di bawah pages/, tanpa extension .vue.
Itulah seluruh naming convention-nya, dan rule yang sama berlaku untuk seluruh component registry.
Mengapa Menggunakan Registry, Bukan Dynamic Import
Ada dua alasan.
Pertama, me-resolve arbitrary component name yang dikirim server langsung ke bundle akan membuat metadata backend memiliki jalur terlalu bebas menuju executable frontend code. Nama memang berasal dari PHP class yang sudah diregistrasikan, bukan request input, sehingga registry bukan security boundary pertama. Namun registry tetap menjadi lapisan allowlist kedua antara data dan code execution.
Alasan kedua lebih mendasar: bundler harus mengetahui file apa saja yang perlu dimasukkan ke bundle.
Dynamic import berdasarkan arbitrary runtime string tidak dapat dianalisis secara static, sehingga bundler tidak mengetahui file mana yang perlu dihasilkan.
import.meta.glob dapat dianalisis saat build-time. Semua file yang cocok dengan glob akan diketahui dan dimasukkan ke bundle.
Lima Component Registry
| Registry | Glob | Resolver |
|---|---|---|
@/panel/tables/registry | pages/Panels/**/Columns/*.vue | resolveColumnComponent |
@/panel/forms/registry | pages/Panels/**/{Fields,Schemas,Entries,Modals}/*.vue | resolveFormComponent |
@/panel/widgets/registry | pages/Panels/**/Widgets/*.vue | resolveWidgetComponent |
@/panel/hooks/registry | pages/Panels/**/Hooks/*.vue | resolveHookComponent |
@/panel/shell/registry | pages/Panels/**/Shell/*.vue | resolveShellComponent |
Semua resolver menggunakan contract yang sama: nama yang dikenal menghasilkan loader, sedangkan nama yang tidak dikenal menghasilkan null.
export function resolveColumnComponent(name: string): ColumnLoader | null;
export function resolveFormComponent(name: string): ComponentLoader | null;
export function resolveWidgetComponent(name: string): WidgetLoader | null;
export function resolveHookComponent(name: string): HookLoader | null;
export function resolveShellComponent(name: string): ComponentLoader | null;2
3
4
5
Empat registry juga menyediakan membership test:
export function hasColumnComponent(name: string): boolean;
export function hasFormComponent(name: string): boolean;
export function hasWidgetComponent(name: string): boolean;
export function hasHookComponent(name: string): boolean;2
3
4
resolveShellComponent tidak memiliki pasangan has*. Layout cukup memanggil resolver; jika hasilnya null, layout mempertahankan built-in sidebar atau topbar.
Fungsi Setiap Registry dan Sumber Nama PHP-nya
| Registry | Directory | Sisi PHP |
|---|---|---|
| Columns | Panels/{Panel}/Columns/ | CustomColumn::component(string $component) |
| Fields | Panels/{Panel}/Fields/ | CustomField::component(string $component) |
| Layouts | Panels/{Panel}/Schemas/ | CustomComponent::make(string $component) |
| Entries | Panels/{Panel}/Entries/ | CustomEntry::component(string $component) |
| Modals | Panels/{Panel}/Modals/ | Modal::content(string $component, array $config = []) |
| Widgets | Panels/{Panel}/Widgets/ | CustomWidget::$component |
| Hooks | Panels/{Panel}/Hooks/ | Panel::renderHook(RenderHook, string, …) |
| Shell | Panels/{Panel}/Shell/ | Panel::sidebarComponent(), Panel::topbarComponent() |
Fields, Layouts, Entries, dan Modals berbagi satu form registry. Yang penting bukan lokasi deklarasi PHP-nya, tetapi bahwa sebuah nama hanya dapat di-resolve ke component yang sudah diketahui saat build.
Contoh Custom Column dari Backend sampai Vue
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Tables\Columns\CustomColumn;
CustomColumn::make('accountAge')
->label('Account age')
->component('Panels/Admin/Columns/AccountAge')
->state(static fn (Model $record): array => [
'days' => (int) $record->created_at->diffInDays(now()),
]);2
3
4
5
6
7
8
9
<!-- resources/js/pages/Panels/Admin/Columns/AccountAge.vue -->
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{ state: unknown }>();
const days = computed(() => {
const value = props.state;
if (typeof value !== 'object' || value === null) {
return null;
}
const { days } = value as { days?: unknown };
return typeof days === 'number' ? days : null;
});
</script>
<template>
<span v-if="days !== null">{{ days }} days</span>
<span v-else class="text-muted-foreground">—</span>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Apa pun yang dikembalikan state() menjadi prop yang diterima component. Nilai tersebut harus dapat diserialisasi sebagai scalar atau array, sama seperti metadata cell lainnya.
Di browser nilainya tiba sebagai JSON tanpa static type guarantee. Karena itu component melakukan type narrowing daripada langsung melakukan assertion. Payload dengan bentuk tidak sesuai sebaiknya menghasilkan empty cell, bukan exception yang merusak seluruh Table.
Kegagalan Menghasilkan Fallback, Bukan Exception
Semua resolver mengembalikan null, bukan melempar exception. Caller kemudian merender fallback netral:
| Registry | Behavior saat nama tidak dikenal |
|---|---|
| Columns | Cell menampilkan placeholder; satu typo tidak boleh merusak seluruh Table |
| Forms | Renderer menampilkan placeholder; Field di sekitarnya tetap dapat diedit |
| Widgets | WidgetFallback; satu typo tidak boleh merusak Dashboard |
| Hooks | Tidak merender apa pun; decorative injection tidak boleh merusak Page |
| Shell | Menggunakan built-in sidebar/topbar; typo tidak boleh membuat user terjebak tanpa navigasi |
Form dan Widget registry memberikan warning saat development:
[panel] The widget component [Panels/Admin/Widgets/SystemInfo] is not in the
build-time registry, so a fallback is drawn instead. It has to live under
resources/js/pages/Panels/{Panel}/Widgets/ — check the path and the spelling,
then rebuild.2
3
4
Warning hanya diberikan satu kali per nama ketika import.meta.env.DEV aktif.
Pada production, fallback adalah satu-satunya behavior. Ini adalah build problem; menulis console warning di live Panel tidak memberikan solusi bagi end user.
Icon Registry
Icon menggunakan prinsip yang sama, tetapi hasil resolusi adalah Vue component langsung, bukan async loader. Allowlist icon juga digenerate, bukan menggunakan glob.
import { resolveIcon, isPanelIconName } from '@/panel/icons/registry';
import type { PanelIconName } from '@/panel/icons/registry';
resolveIcon('shield'); // Component
resolveIcon('not-an-icon'); // null
resolveIcon(null); // null
isPanelIconName('shield'); // true2
3
4
5
6
7
export type PanelIconName = keyof typeof ICONS;
export function isPanelIconName(name: string): name is PanelIconName;
export function resolveIcon(name: string | null | undefined): Component | null;2
3
File resources/js/panel/icons/registry.ts dibuat ulang melalui:
php artisan panel:icons # rewrite the registry from the source
php artisan panel:icons --check # fail if it is out of date, for CI2
Command tersebut memindai dua tree:
app/milik application;- source package PandaBear sendiri.
Scanner mencari seluruh bentuk deklarasi icon berikut:
->icon('x')
$navigationIcon = 'x'
icon: 'x'
'icon' => 'x'
Icon::make('x')2
3
4
5
Selain itu scanner membaca setiap string literal di dalam method bernama icon(), karena enum biasanya menyimpan icon per case melalui method tersebut.
Setiap nama kemudian diverifikasi terhadap icon yang benar-benar tersedia pada Lucide, lalu registry ditulis ulang.
Lucide menyediakan lebih dari seribu icon, tetapi sebuah Panel biasanya hanya membutuhkan beberapa puluh. Hanya icon yang benar-benar digunakan yang perlu masuk bundle.
Source package harus ikut dipindai karena banyak icon berasal dari built-in Action milik framework — misalnya delete, edit, dan export. Jika scanner hanya membaca app/, registry dapat kehilangan icon built-in dan Action framework akan tampil tanpa icon.
Nama yang tidak tersedia di Lucide menyebabkan command gagal dengan nama icon yang salah. Ini penting karena icon yang tidak terdaftar pada runtime hanya menghasilkan null.
Unknown icon juga menggunakan development-only warning seperti component registry.
Menambahkan Custom Component
- Buat file pada:
resources/js/pages/Panels/{Panel}/{Kind}/{Name}.vue. Nama directory harus sesuai glob; directory lain tidak ikut dipindai. - Deklarasikan nama di PHP sebagai path relatif di bawah
pages/, tanpa.vue. - Rebuild frontend:
npm run devuntuk development;npm run builduntuk production.
Jika root frontend Panel tidak menggunakan resources/js/pages/Panels, ubah panda-panel.frontend.pages_path dan glob registry.
Glob registry ditulis sebagai literal relative path dalam file registry masing-masing. File tersebut dipublish ke application, sehingga memang boleh dan diharapkan untuk diedit jika struktur frontend application berbeda.
Hal yang Perlu Diperhatikan
- Glob menggunakan relative path, bukan alias
@. Vite dev server dapat me-resolve aliased glob menjadi object kosong sementara production build berhasil. Akibatnya semua custom component fallback di development tetapi bekerja setelah build — failure mode yang sangat membingungkan. - Key registry berasal dari actual path, bukan direkonstruksi dari nama. Format key Vite mengikuti pattern glob dan dapat berbeda antara dev server dan production build. Registry mencari segment
/pages/pada emitted key lalu mengambil bagian setelahnya. - Hanya direct child dari directory jenis component yang cocok. Glob berakhir pada
*.vue, bukan**/*.vue. JadiWidgets/Charts/Revenue.vuetidak diregistrasikan. - File baru membutuhkan rebuild.
import.meta.globdievaluasi saat build-time. Dev server dapat melihat file baru, tetapi production bundle yang dibuat sebelum file tersebut ada tidak memilikinya. - Nama adalah registry key, bukan filesystem path atau PHP class.
./Widgets/X.vue,@/pages/Panels/…, maupunApp\Panels\Admin\Widgets\SystemInfosemuanya tidak akan ter-resolve. - Case-sensitive.
Panels/Admin/Widgets/systemInfodanPanels/Admin/Widgets/SystemInfomerupakan dua key berbeda. Pada filesystem yang case-insensitive, kesalahan ini sering baru terlihat di CI/Linux. panel:iconsadalah build-time allowlist, bukan runtime lookup. Menambahkan nama icon di PHP tanpa menjalankan command berarti icon tersebut tidak ada di registry. Gunakan--checkdi CI agar kondisi ini menggagalkan build.