Notification Center
Notification Center adalah bell di header Panel beserta tiga endpoint di belakangnya. Fitur ini menampilkan stored notification milik user yang sedang login, menandainya sebagai read, dan membersihkannya. Fitur tersedia secara default pada setiap Panel; halaman ini membahas cara menonaktifkannya, memanggil endpoint secara langsung, dan alasan endpoint tersebut tidak membutuhkan policy.
Contoh minimal yang berfungsi
Kirim persistent notification, lalu buka Panel:
<?php
use PandaPanel\Notifications\Notification;
Notification::make('export-ready')
->title('Your export is ready')
->body('1,204 records')
->success()
->persistent()
->send($user);2
3
4
5
6
7
8
9
10
Pada request Panel berikutnya badge bell menampilkan 1. Membuka bell akan mengambil daftar notifikasi.
Endpoint
Endpoint didaftarkan di dalam route group setiap Panel oleh PanelRouteRegistrar, menggunakan prefix notifications, dan dilayani oleh PandaPanel\Http\Controllers\PanelNotificationController:
| Route name | Method | Path | Controller method |
|---|---|---|---|
panel.{id}.notifications.index | GET | /{panel path}/notifications | index |
panel.{id}.notifications.read | POST | /{panel path}/notifications/read | read |
panel.{id}.notifications.clear | POST | /{panel path}/notifications/clear | clear |
Semuanya berada di dalam middleware Panel, sehingga Panel yang menggunakan ->auth() juga melindungi endpoint ini dengan auth.
route('panel.admin.notifications.index'); // http://localhost/admin/notificationsResponse berbentuk JSON, bukan Inertia. Bell terbuka di atas page yang sedang tampil; full-page response justru akan menggantikan page tersebut.
index
public function index(Request $request): JsonResponseMengembalikan 30 notifikasi terbaru serta unread count:
{
"notifications": [
{
"id": "9b5f…",
"title": "Your export is ready",
"body": "1,204 records",
"color": "success",
"icon": "download",
"actions": [
{ "name": "download", "label": "Download", "url": "/admin/exports/users.csv",
"variant": "outline", "markAsRead": true, "newTab": false }
],
"read": false,
"createdAt": "2 minutes ago"
}
],
"unread": 1
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PanelNotificationController::LIMIT bernilai 30 dan merupakan private constant, bukan configuration. Notification center bukan archive: setelah lebih dari satu layar hampir tidak ada pengguna yang akan terus scroll, sedangkan query tanpa batas pada table yang terus tumbuh akan membuat halaman makin lambat. Row yang lebih lama tetap ada dan dapat diakses dengan Eloquent.
createdAt menggunakan diffForHumans() yang dihitung di server sehingga frontend tidak perlu membawa date library.
read
public function read(Request $request): JsonResponse| Body | Efek |
|---|---|
{} | seluruh unread notification user ini mendapat read_at = now() |
{"id": "9b5f…"} | hanya notifikasi tersebut, jika memang milik user ini |
{"id": 42} — bukan string | 422 Invalid notification. |
curl -X POST https://example.test/admin/notifications/read \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"id":"9b5f7c2e-1a4d-4c1a-9c33-0f5b7c9d2e11"}'2
3
4
{ "unread": 0 }ID milik user lain tidak menghasilkan match, bukan 403. Hasil akhirnya tetap tidak dapat diakses, sekaligus tidak membocorkan apakah ID tersebut ada.
clear
public function clear(Request $request): JsonResponse| Body | Efek |
|---|---|
{} atau {"all": false} | menghapus notifikasi read milik user ini |
{"all": true} | menghapus semua notifikasi milik user ini |
curl -X POST https://example.test/admin/notifications/clear \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"all":true}'2
3
4
{ "unread": 0 }Digunakan $request->boolean('all'), sehingga "true", 1, dan on dianggap true; nilai lain false dan hanya row yang sudah read yang dihapus.
Mengapa tidak ada policy
Semua query dimulai dari $request->user():
$user->notifications()->latest()->limit(self::LIMIT)->get();
$user->notifications()->whereKey($id)->update(['read_at' => now()]);2
Tidak ada ID dari request yang dapat menjangkau row milik user lain. Scope tersebut adalah authorization, sehingga endpoint tidak memiliki authorization check tambahan yang berpotensi salah. Negative test package memverifikasi hal ini secara langsung: ID milik user pertama yang dikirim oleh user kedua tidak menandai apa pun.
Satu-satunya guard berada di pintu masuk:
abort_unless(
$user instanceof PanelNotifiable || (is_object($user) && method_exists($user, 'unreadNotifications')),
403,
);2
3
4
User model yang bukan Notifiable tidak dapat menjawab query ini; 403 menjelaskannya daripada membiarkan fatal error muncul beberapa baris kemudian.
Stored row adalah data tidak tepercaya
serialize() memperlakukan kolom data seperti request body karena isinya adalah JSON yang mungkin ditulis seminggu lalu:
| Stored value | Yang disajikan |
|---|---|
color di luar empat case | info, beserta icon info |
title bukan string | "" |
body bukan string | null |
| action gagal diparse | dibuang dari list dan tidak dirender |
$data['color'] = 'chartreuse'; // serves color: 'info', icon: 'info'
$data['actions'] = ['nonsense']; // serves actions: []2
Tidak ada exception, dan tidak ada value yang dapat mencapai class name yang tidak tersedia di bundle.
Shared prop
SharePanelData menempatkan endpoint serta unread count pada setiap request Panel:
'notifications' => [
'enabled' => true,
'indexUrl' => '/admin/notifications',
'readUrl' => '/admin/notifications/read',
'clearUrl' => '/admin/notifications/clear',
'unread' => 1,
],2
3
4
5
6
7
| Field | Type | Nilai saat disabled |
|---|---|---|
enabled | bool | false |
indexUrl, readUrl, clearUrl | string|null | null |
unread | int | 0 |
Fitur disabled ketika tidak ada current Panel, Panel memanggil ->notifications(false), atau tidak ada user yang login.
Count dibaca pada setiap request Panel daripada melalui polling, sehingga badge langsung benar setelah navigasi tanpa round trip tambahan. Ini hanya satu indexed count yang dibatasi pada satu user. Jika table notifications belum ada, QueryException ditangkap dan hasilnya 0.
Di Vue:
import { usePanel } from '@/panel/composables/usePanel';
const { notifications } = usePanel();
notifications.value.unread; // number
notifications.value.indexUrl; // string | null2
3
4
5
6
Mematikan bell
use PandaPanel\Core\Panel;
$panel->notifications(false);2
3
| Method | Signature | Default |
|---|---|---|
notifications | notifications(bool $notifications = true): self | true |
hasNotifications | hasNotifications(): bool | true |
Endpoint tetap terdaftar — job masih dapat menulis notifikasi yang nantinya dibaca dari Panel lain — tetapi shared prop mengirim enabled: false dan PanelNotifications.vue tidak merender control apa pun.
Perilaku component
resources/js/panel/components/PanelNotifications.vue, dipasang oleh PanelHeader.vue:
- Badge menggunakan shared count sampai local action mengubahnya; setelah itu nilai lokal dipakai sampai navigasi berikutnya.
- List hanya di-fetch ketika sheet dibuka. Notifikasi yang tidak pernah dilihat tidak menambah request.
- Event
windowpanel:notification, yang dipicuusePanelBroadcastingketika persistent notification masuk, melakukan refetch jika list sedang terbuka dan menambah badge jika tertutup. Payload broadcast tidak langsung dimasukkan ke list karena tidak membawa row id; list yang berisi entry yang tidak dapat ditandai read lebih buruk daripada satu request refetch. - "Mark all read" melakukan POST
{}kereadUrl; "Clear read" melakukan POST{ all: false }keclearUrl. - Menekan action terlebih dahulu menandai notifikasi sebagai read (kecuali action meminta sebaliknya), lalu membuka URL-nya —
window.open(url, '_blank', 'noopener')untuknewTab, ataurouter.visit(url)untuk tab yang sama. Unsafe scheme diabaikan sebelum kedua operasi. - Response yang gagal diparse membiarkan list lama tetap tampil dan menampilkan "Notifications could not be loaded."
Setiap request membawa Accept: application/json dan credentials: 'same-origin'; dua POST menggunakan postJson() dari resources/js/panel/forms/http.ts, yang menambahkan X-Requested-With dan CSRF token.
Catatan
- Bell bersifat per Panel, tetapi row notifikasi tidak. Semua Panel membaca table
notificationsyang sama untuk user yang sama, sehingga notifikasi dari job pada requestadminjuga muncul di bell Panelapp. unreadpada response bersifat authoritative. Ketiga endpoint mengembalikan count yang dihitung ulang; component mempercayai nilai ini setelah mutation.readtanpa id hanya meng-update unread rows, sehingga "mark all read" berulang tidak melakukan full-table update.- Delete bersifat permanen.
clearmenjalankandelete(), bukan soft delete. Tidak ada undo, dan frontend tidak menambahkan confirmation lain di luar tombol itu sendiri.
Lihat juga
- Database notifications — row yang berada di belakang bell
- Notification actions — tombol pada setiap row
- Toast notifications — sisi transient
- Broadcasting — sumber event
panel:notification - Metadata server ke Vue — shared props lengkap
- Panel API reference
- Testing notifications