Error Notifications
Ketika request dari dalam Panel gagal, Panel menampilkan toast sebagai pengganti error overlay milik Inertia. Copy pesan dimiliki oleh Panel dan dipetakan berdasarkan HTTP status, sehingga 403 dapat dibaca sebagai kalimat yang membantu user mengambil tindakan, bukan modal yang menampilkan exception page.
Enam status memiliki default. Sebuah Panel dapat mengganti copy salah satunya, menambahkan status yang tidak memiliki default framework, atau menonaktifkan pesan untuk status tertentu sepenuhnya.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Panels\Admin;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->auth()
->errorNotification(403, 'Not your area', 'Ask an administrator for access.');
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Action yang ditolak di Admin Panel sekarang menampilkan toast Not your area beserta description tersebut dan tidak menampilkan overlay. Status lain tetap menggunakan default.
Default
private const DEFAULT_ERROR_NOTIFICATIONS = [
403 => ['title' => 'Not allowed', 'body' => 'You do not have permission to do that.'],
404 => ['title' => 'Not found', 'body' => 'That record no longer exists.'],
419 => ['title' => 'Session expired', 'body' => 'Refresh the page and try again.'],
429 => ['title' => 'Too many requests', 'body' => 'Wait a moment and try again.'],
500 => ['title' => 'Something went wrong', 'body' => 'The request could not be completed.'],
503 => ['title' => 'Temporarily unavailable', 'body' => 'The application is down for maintenance.'],
];2
3
4
5
6
7
8
| Status | Title | Body |
|---|---|---|
| 403 | Not allowed | You do not have permission to do that. |
| 404 | Not found | That record no longer exists. |
| 419 | Session expired | Refresh the page and try again. |
| 429 | Too many requests | Wait a moment and try again. |
| 500 | Something went wrong | The request could not be completed. |
| 503 | Temporarily unavailable | The application is down for maintenance. |
$notifications = panel('admin')->getErrorNotifications();
array_keys($notifications); // [403, 404, 419, 429, 500, 503]
$notifications[403]['title']; // 'Not allowed'2
3
4
Method
public function errorNotification(int $status, string $title, ?string $body = null): self;
public function hideErrorNotification(int $status): self;
/** @return array<int, array{title: string, body: string|null}|null> */
public function getErrorNotifications(): array;2
3
4
5
errorNotification()
Mengganti — atau menambahkan — notifikasi untuk satu status. getErrorNotifications() menggabungkan entry milik Panel di atas default framework menggunakan array_replace(), sehingga satu status dapat dikustomisasi tanpa perlu menuliskan ulang semua default lain.
use PandaPanel\Core\Panel;
$panel = Panel::make('custom')
->errorNotification(403, 'Nope', 'Ask an administrator.');
$panel->getErrorNotifications()[403]; // ['title' => 'Nope', 'body' => 'Ask an administrator.']
$panel->getErrorNotifications()[404]['title']; // 'Not found' — untouched2
3
4
5
6
7
$body bersifat opsional:
$panel->errorNotification(422, 'Check the form');
// ['title' => 'Check the form', 'body' => null]2
Status yang tidak memiliki default juga dapat ditambahkan dengan cara yang sama — 402, 423, 451, atau status lain yang digunakan aplikasi:
$panel->errorNotification(402, 'Payment required', 'Your plan does not include this.');hideErrorNotification()
Menyimpan status sebagai null. Ini merupakan outcome ketiga dan berbeda dari status yang sama sekali tidak ada: tidak ada toast dan tidak ada overlay. Gunakan untuk status yang memang ditangani sendiri oleh aplikasi.
$panel = Panel::make('quiet')->hideErrorNotification(404);
$panel->getErrorNotifications()[404]; // null
$panel->toSharedArray()['errorNotifications'][404]; // null2
3
4
Tiga outcome
Seluruh mekanisme pada dasarnya adalah lookup dengan tiga jawaban:
| Entry untuk status | Toast | Overlay Inertia |
|---|---|---|
| Sebuah array | ditampilkan | disuppress |
null — ditetapkan dengan hideErrorNotification() | tidak ada | disuppress |
| Tidak ada — tidak pernah dikonfigurasi | tidak ada | dibiarkan |
Baris ketiga paling penting. Status yang tidak diketahui Panel lebih baik ditampilkan secara raw daripada ditelan. Contohnya, 502 yang tidak terduga saat development seharusnya menampilkan Inertia overlay, bukan diam tanpa informasi.
Data yang dikirim ke frontend
Map menjadi bagian dari shared props Panel dan menggunakan status sebagai key:
'errorNotifications' => [
403 => ['title' => 'Not allowed', 'body' => 'You do not have permission to do that.'],
404 => null,
// …
],2
3
4
5
export interface PanelErrorNotification {
title: string;
body: string | null;
}
export interface PanelDefinition {
// …
errorNotifications: Record<string, PanelErrorNotification | null>;
}2
3
4
5
6
7
8
9
Integer key PHP menjadi string key di JSON. Karena itu composable melakukan lookup dengan String(status). Tidak ada executable value di dalam map — test framework sendiri menelusuri serialized map dan memastikan tidak ada value berupa Closure.
Frontend
resources/js/panel/composables/useErrorNotifications.ts mendaftarkan satu Inertia listener:
stop = router.on('httpException', (event) => {
const status = event.detail.response.status;
const notification = notificationFor(status);
if (notification === undefined) {
return; // no entry: leave Inertia alone
}
if (notification !== null) {
toast.error(notification.title, {
description: notification.body ?? undefined,
});
}
return false; // cancels Inertia's own handling
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Mengembalikan false dari handler membatalkan overlay. Listener dilepas saat unmount.
Listener dipanggil satu kali di PanelLayout.vue:
// Registered on the shell rather than per page, so one listener covers every
// panel route and is torn down when the panel is left.
useErrorNotifications();2
3
Karena itu semua Page di dalam Panel tercakup, sedangkan Page di luar Panel tidak. Custom Page yang memakai layout sendiri dan tidak menggunakan PanelLayout juga tidak mendapatkan interception ini.
Toast dirender menggunakan vue-sonner melalui <Toaster /> pada Panel shell — channel yang sama dengan flash message. Lihat Toast notifications.
Menentukan pesan yang tepat
Copy bersifat user-facing dan setiap default ditulis sesuai failure yang sebenarnya:
- 403 adalah failure paling umum di Panel karena setiap action, page, dan widget melakukan authorization sendiri. Beri tahu user siapa yang perlu dihubungi, bukan detail internal apa yang ditolak.
- 419 berarti session atau CSRF expired; reload biasanya menyelesaikannya. Sampaikan hal itu.
- 429 berasal dari throttling middleware — misalnya login throttling Panel atau rate limit aplikasi pada action.
- 500 dan 503 adalah dua error yang tidak bisa diperbaiki user; jaga pesan tetap singkat.
Panel dengan vocabulary sendiri sebaiknya meng-override copy daripada menerima pesan generic:
$panel
->errorNotification(403, 'Restricted', 'This record belongs to another branch.')
->errorNotification(404, 'Gone', 'Somebody deleted it while you were looking.')
->hideErrorNotification(419); // the application redirects to login itself2
3
4
Gotchas
- Mekanisme ini menangani failed request, bukan validation. 422 dari form ditangani form dengan menampilkan field error. Menambahkan entry 422 akan menghasilkan toast pada setiap validation failure.
- Hanya status yang ada di map yang di-intercept. Status lain tetap menggunakan default behavior Inertia, yang di production berarti apa pun yang dirender error page aplikasi.
hideErrorNotification()menyembunyikan lebih dari toast. Method ini juga men-suppress overlay sehingga user tidak melihat apa pun. Gunakan hanya ketika aplikasi memang sudah menangani kasus tersebut.- Listener hidup pada Panel layout. Custom Page yang tidak memakai
PanelLayoutjuga keluar dari mekanisme ini. - Key berupa integer di PHP dan string di JSON. Gunakan
String(status)ketika membaca map dari code frontend Anda sendiri. - Scoped per Panel, bukan global. Dua Panel dapat menggunakan copy berbeda untuk status yang sama. Tidak ada value yang bocor antar-Panel karena map ikut shared props milik Panel tersebut.