Panel configuration
A Panel is Arrel's embedding unit: it groups the Resources, pages and widgets you want to expose under a URL, with its own branding. An application can register more than one (for example, an admin panel and a branded-demo panel with different colors for a specific customer).
Registering a Panel
It is registered against the PanelRegistry singleton that Arrel's ServiceProvider already binds, normally from the boot() of a ServiceProvider of your own application:
use Arrel\Panels\Panel;
use Arrel\Panels\PanelRegistry;
public function boot(PanelRegistry $panels): void
{
$panels->register(
Panel::make('admin')
->resources([
PostResource::class,
])
->pages([
SiteSettingsPage::class,
])
->customPages([
CustomPage::make('about', 'About'),
])
->widgets([
Widget::make('total_posts')
->label('Total Posts')
->value(fn (): int => Post::count()),
])
);
}Panel::make($id) takes the identifier that will appear in the URL. The resources(), pages(), widgets() and customPages() methods are all optional and can be freely combined. Requesting a panel that is not registered answers 404.
Prefix and panels
By default, the admin panel lives at /arrel/admin. You can change it in config/arrel.php:
return [
'prefix' => 'arrel',
'panels' => [],
'middleware' => [],
'authenticated_middleware' => [],
'extra_scripts' => [],
];| Key | Description |
|---|---|
prefix | First segment of the SPA URL: /{prefix}/{panel}. It can be any string, or empty. |
panels | List of panel identifiers. Only needed when prefix is empty. |
middleware | Middleware that runs before authentication. See Middleware. |
authenticated_middleware | Middleware that runs after authentication. |
extra_scripts | URLs of scripts loaded in all panels. See Scripts and stylesheets. |
A panel at the root of the domain needs an empty prefix and the list of its identifiers, so that Arrel only answers to those and does not swallow the rest of your application's routes:
'prefix' => '',
'panels' => ['admin'],With that, the admin panel lives at /admin. The API stays under /arrel/api/{panel}/..., whatever the prefix.
To build the URL of a panel page from your own code, use Arrel\Support\PanelUrl:
use Arrel\Support\PanelUrl;
PanelUrl::to('admin', 'posts'); // "/arrel/admin/posts"Your own branding
Panel::make('branded-demo')
->brandName('Branded Demo')
->logo('/images/branded-demo-logo.svg')
->colors(['brand' => '#2E7D32', 'brand-strong' => '#1B5E20'])
->resources([PostResource::class]);brandName(), logo() and colors() are exposed to the frontend through window.Arrel (brand name in the <title> and in the interface, logo, and the color tokens that override the default palette). Without brandName(), the panel is called "Arrel".
For your own typography, fonts() and stylesheet() work like colors() but for font families:
Panel::make('branded-demo')
->fonts(['sans' => '"Poppins", sans-serif'])
->stylesheet('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap');fonts() overrides the font family tokens (sans, display...) just as colors() does with the color ones. stylesheet() loads an external stylesheet (typically a Google Fonts link) in the <head>, before Arrel's own CSS, so that the @font-face is already registered when needed.
Theme
Panel::make('admin')->theme('light');theme() accepts auto (the default, follows the system), light or dark. Any other value throws an InvalidArgumentException.
Footer
Panel::make('admin')->footer(fn (): array => [
'text' => '© '.now()->year.' Acme',
'links' => [
['label' => 'Cookies', 'url' => '/cookies'],
],
]);footer() takes an array, or a closure that returns one, with text and a list of links. It is shown below every page of the panel.
Interface texts
The interface texts (buttons, states, security screens...) are in English. They can be translated with translations(), which takes an array of key => text or a closure that returns one. The closure is evaluated on every request, so it can depend on the active language:
Panel::make('admin')->translations(fn (): array => (array) __('arrel'));The keys Arrel recognizes are:
save, cancel, remove, saved, loading, search_placeholder, searching, no_results, clear_filters, select_all, clear_selection, sign_in, sign_out, dashboard, email, password, invalid_credentials, something_went_wrong, any, new_prefix, edit_prefix, main_navigation, active, all_with_trashed, trashed_only, selected_suffix, columns, select_row_prefix, home, date_placeholder, today, clear, previous_month, next_month, two_factor_code, two_factor_code_hint, verify, use_another_account, security, two_factor_title, two_factor_on, two_factor_off, two_factor_required, two_factor_enable, two_factor_disable, two_factor_scan, two_factor_secret, two_factor_confirm, recovery_codes_title, recovery_codes_hint, continue, two_factor_disable_hint, verify_email_title, verify_email_hint, verify_email_send, verify_email_resend, verify_email_sent, verify_email_continue, verify_email_pending, notifications, mark_all_as_read and no_notifications.
A key you do not declare keeps its default text.
The error messages generated by the server (sign-in, second factor, email verification) and the names of the built-in actions are Laravel translations under the arrel:: namespace, and can be overridden with files in lang/vendor/arrel.
Profile page and home page
Panel::make('admin')
->profile(AccountPage::class)
->home(fn (): ?string => auth()->user()?->is_teacher ? PanelUrl::to('admin', 'custom/pass-list') : null);profile()takes aPageand turns it into the user's profile link in the sidebar. It is only shown if the page'scanView()is true.home()takes a closure that returns the route where the user should land when opening the panel (on loading it, or coming from sign-in or the security page). Withnull, or an empty value, the user lands on the dashboard. The closure is evaluated per user, so everyone can have their own home page.
Scripts and stylesheets
To load the JavaScript of your custom pages there are two ways:
Panel::make('admin')
->scripts(fn (): array => [Vite::asset('resources/js/arrel/pages.ts')])
->stylesheets(fn (): array => [Vite::asset('resources/css/arrel.css')]);scripts() adds <script type="module"> after arrel.js, and stylesheets() adds <link rel="stylesheet"> after Arrel's CSS. Both take an array of URLs or a closure that returns one, and are per panel. config('arrel.extra_scripts') does the same for all panels.
Authentication and security
Everything about Sanctum, email verification, second factor and notifications is in Authentication. Authorization and attempt limits are in Security and authorization.
Middleware
config('arrel.middleware') is added, in order, before Arrel's own middleware on all web and API routes. It is the entry point for anything your application needs to resolve before Arrel queries anything. The most common case is multi-tenancy:
// config/arrel.php
'middleware' => [
\App\Http\Middleware\IdentifyTenant::class,
],config('arrel.authenticated_middleware') runs after Arrel has already resolved the user, and after email verification and the second factor. middleware runs before Sanctum resolves the session, so it does not see an authenticated user yet: anything that needs one must go in authenticated_middleware.
// config/arrel.php
'authenticated_middleware' => [
\App\Http\Middleware\RecordLastSeen::class,
],The full order of the API routes is:
[
...config('arrel.middleware'),
ForceJsonResponse::class,
EnsureFrontendRequestsAreStateful::class,
'web',
'auth:sanctum',
EnsureEmailIsVerified::class,
EnsureTwoFactorIsSetUp::class,
...config('arrel.authenticated_middleware'),
]The endpoints of the custom pages that you write in your application must use this same stack, so that they inherit the same guarantees (see Custom pages).
Next step
With the Panel registered, define your first Resource.