Loading...

This is taking longer than expected.

Back to the help centre

The admin panel

What ships under /admin, who gets in, and how to add a screen of your own.

The admin panel is the back office of your project: users, roles, logs and the optional modules, each as a CRUD generated from a short PHP class. This guide covers what the panel ships with and how to register a resource of your own with Laravel Front, the package that renders it.

How to reach it

The panel lives at /admin. Its routes are in routes/admin.php, and bootstrap/app.php wraps that whole file in the middleware web, auth, security and role:admin, under the admin. route-name prefix. Signed-in users with the admin role see a Go To Admin Panel entry in the user menu at the top right of the application.

Three things follow from that middleware stack:

  • You need a session. A visitor is sent to the login page.
  • security applies the same checks as the rest of the account area: a blocked account is logged out, and an account flagged for a password change is sent to the password screen before anything else.
  • You need the role named admin. The middleware carries that name literally, so the admin_role key in config/app.php must stay admin for the panel to open.

Super-administrators are not a role. config/app.php → sudo is a list of emails, and php artisan db:seed (through AdminSeeder) creates an account for each one and assigns it the admin role. That role is what opens the door; being on the sudo list on top of it unlocks the Dev Zone, the Design screen and the activity log. See /help/sudo-and-super-administrators.

Once inside, every screen checks its own policy, and the policy checks a permission of the shape {action} {resource}. The seeded admin role holds every permission, so a fresh install shows everything. A role you create shows only what you grant it, and a resource its role cannot read disappears from the menu. See /help/roles-and-permissions.

The layout

Sidebar. resources/views/layouts/sidebars/admin.blade.php renders the <x-design::sidebar-menu> component (App\Classes\SidebarMenu) with two kinds of entries: the manual links it declares, in this order — Dashboard at /admin, Space at /admin/space, Media at /admin/media, shown to accounts that can browse other accounts' media, Support inbox at /support, shown when the support tickets module is on and the account may answer tickets, and at the bottom Design at /admin/design and Dev Zone at /admin/dev, both shown only to sudo accounts — and every registered resource that returns true from showOnMenu() and whose model the current user passes the viewAny policy check for. Resources are grouped by their $menu_group; a resource with no group sits in the ungrouped block at the top. The sidebar declares the order of its groups (Logs, Plans, Users); a group it does not name goes after those, alphabetically. Each group remembers whether you left it open in localStorage.

Search. When the resource's model uses the Searchable trait, the top bar shows a search box that filters the index by the model's $searchable columns. Users, roles and announcements are searchable out of the box.

Dashboard. /admin is a Livewire component (App\Livewire\Admin\Dashboard) with a date range (default: the last 30 days) and a metric selector. The metrics always available are New Registered Users, Total Accumulated Users, Activity Log Events, Daily Logged Users and Users by Country. Tracking Events appears when FEATURE_TRACKING_ENABLED is on, Referral Subscriptions when FEATURE_REFERRALS_ENABLED is on, and the billing package adds its own when it is installed. The chosen metric is kept in the URL, so a dashboard view can be bookmarked. Each metric is described in /help/dashboard-metrics-and-visitor-tracking.

The admin dashboard with the date range and the metric selector

What ships with the panel

Every class in app/Front/Resources/ is a resource. All of them extend the local App\Front\Resources\Resource, which sets the defaults $section = 'admin', $showOnMenu = true and the circle-stack icon.

Resource URL Menu group What it manages
User /admin/users Users Accounts: name, the identities enabled in config/auth.php (email, phone, username), PIN when auth.enable_pin is on, password with a generator button, roles, verification, detected location, block date, communication logs. Filter by plan when billing is active. Actions: Act as, Block User, Reset Pin. Email and Phone, when they are sign-in identities, are list columns hidden by default: switch them on in Columns and the Excel export carries them, as it carries every visible column. It never carries the password; importing it at /admin/users/import updates the rows with an ID, changing only the columns the file brings and the form can edit (the last login and the created, updated, verified and blocked dates are ignored), including Email Verified, and creates the rows without one, with the same rules as the form and a random password nobody sees. See Accounts created by an administrator.
Role /admin/roles Users Roles and their permission matrix. Action: Clone.
Permission /admin/permissions hidden Read-only list of permissions; showOnMenu is false and its policy denies create, update and delete. It exists so the role screen can load the list.
Announcement /admin/announcements none Announcements shown to every user. Visible only when FEATURE_ANNOUNCEMENTS is on. See /help/announcements.
Activity /admin/activities Logs The activity log written by the models (event, subject, causer, JSON properties). Index and detail only. Its policy requires the admin role and a sudo email.
CommunicationLog /admin/communication_logs Logs Every email and SMS the application sent or failed to send: recipient, notification class, subject, HTML body. Filter by type and status. No create or edit.
TrackingEventType /admin/tracking-event-types Tracking The named events the tracking module sends. Only when FEATURE_TRACKING_ENABLED is on.
TrackingSession /admin/tracking-sessions Tracking Visitor sessions with campaign, IP, Facebook identifiers and their events. Index and detail only. Same flag.
TrackingEvent /admin/tracking-events Tracking Individual events with payload. Filter by type, date range and session. Same flag.

The users list, the first resource of the Users group

Six screens are plain Livewire components registered in the same file:

URL Component What it does
/admin/dev Admin\DevZone Deploy commands, .env editor, test buttons. Aborts with 405 unless the account is sudo. See /help/the-devzone.
/admin/space Admin\Space How much room the whole installation takes up: every registered file, every table with its data and indexes, and the total, with the data space broken down by area. Open to every account with the admin role. See /help/the-space-pages.
/admin/design Admin\Design The design components of the project, each in its states, and which layer supplies each one. Returns 403 unless the account is sudo. See /help/make-it-your-own.
/admin/media Admin\Media\Directory Files that are not your own, in two folders. General holds what the panel uploads — category images and the files of custom fields — and counts against nobody's space. Users lists every account by its numeric ID; opening one shows that account's own files, and what you upload or delete there counts against that account. Needs the browse media_folder permission; deleting a folder also needs delete media_folder. Every level shows how much it weighs: Storage used at the top, and each card — General, Users, every account, every folder — its own size, 0 B when empty, leaving out deleted folders as the quota does. Following the biggest figure down leads to where the space went. Your own files stay at /app/media. Previewing a file here also shows its collection, disk, ID, UUID and the getMedia() line to use in code, and the disk icon names its disk on hover; at /app/media your customers see only the name, size and file type.
/admin/webhook-events Admin\WebhookEvents Received webhooks with payload, response and error, filtered by source and event type. See /help/audit-trails-and-webhook-events.
/admin/categories/{type} Shared\Category A tree editor for categories of a given type, opened from the Categories action of a resource that uses it. See /help/models-casts-and-building-blocks.

Register a resource of your own

The example below adds a Plan resource. Replace the name with yours.

1. Generate the class

php artisan front:resource Plan -a
composer dump-autoload

-a also creates App\Models\Plan, its migration and App\Policies\PlanPolicy. Use -m for the model and migration only, -p for the policy only, or no flag when the model already exists. The command writes app/Front/Resources/Plan.php from a stub with $base_url = '/admin/plans'; the URL is the plural snake case of the class name under default_base_url (/admin) from the package configuration.

composer dump-autoload matters: the permission seeder discovers resources from Composer's class map, and the project's optimize-autoloader setting only refreshes that map when you dump it.

2. Describe the screen

<?php

namespace App\Front\Resources;

use App\Front\Filters;
use App\Models\Plan as Model;
use WeblaborMx\Front\Inputs;

class Plan extends Resource
{
    public $base_url = '/admin/plans';
    public $model = Model::class;
    public $title = 'name';
    public $icon = 'credit-card';
    public $menu_group = 'Plans';
    public $menu_order = 1;

    public function indexQuery($query)
    {
        return $query->latest();
    }

    public function fields()
    {
        return [
            Inputs\ID::make(),
            Inputs\Text::make('Name')->rules(['required', 'string', 'max:255']),
            Inputs\Money::make('Price')->rules(['required', 'numeric', 'min:0']),
            Inputs\Boolean::make('Active')->default(true),
            Inputs\Textarea::make('Description')->hideFromIndex(),
            Inputs\DateTime::make('Created At')->onlyOnDetail(),
        ];
    }

    public function filters()
    {
        return [
            (new Filters\BooleanFilter)->setTitle('Active'),
            (new Filters\TextFilter)->setTitle('Name'),
        ];
    }
}

The properties the resource understands:

Property Default What it does
$base_url required The URL of the index. Its last segment becomes the route prefix and name.
$model required The Eloquent model behind the screen.
$title 'name' The column shown as the record's label in links, breadcrumbs and relationships.
$search_title $title The column matched by the search box and by autocomplete fields.
$icon 'circle-stack' A Heroicons name for the sidebar.
$menu_group null The sidebar heading the entry goes under. null puts it in the ungrouped block.
$menu_order null Position inside the group. Entries without one go after the ordered ones.
$showOnMenu true Set to false to keep the screen reachable but out of the sidebar. Override showOnMenu() to decide at runtime, as the tracking resources do with their flag.
$section 'admin' The sidebar the resource belongs to; it must match the first URL segment.
$actions all seven Which CRUD operations exist: index, create, store, show, edit, update, destroy. ['index', 'show'] makes a read-only log.
$pagination 50 Rows per page.
$default_sort / $default_sort_direction null / 'desc' Initial order of the index.
$enable_index_sorting true Click a column header to sort.
$enable_column_preferences true Each user can hide, show and reorder index columns; the choice is remembered.
$enable_export / $enable_import true Export the index to a spreadsheet, and import rows from one at {base_url}/import.
$show_create_button_on_index true The Create button on the index.
$custom_fields null The JSON column that keeps custom field values. Set, the index gets a Custom fields button. See /help/add-custom-fields.

Fields. fields() returns inputs from WeblaborMx\Front\Inputs. The first argument is the label; the column is its snake case unless you pass a second argument (Inputs\Text::make('Sent At', 'created_at')). Available inputs: Autocomplete, BelongsTo, BelongsToMany, Boolean, Check, Checkboxes, Code, Date, DateTime, Disabled, File, HasMany, HasOneThrough, Hidden, ID, Image, ImageCropper, ImagePointer, Images, InputGroup, Money, MorphMany, MorphTo, MorphToMany, Number, Password, Percentage, Select, Text, Textarea, Time, ToastEditor (Markdown), Trix. The project adds its own under App\Front\Inputs: Password with a generator button, Pin, PermissionSelector and Categories.

Chain these on any input:

Method Effect
rules(), creationRules(), updateRules() Validation for both forms, for create only, for edit only.
hideFromIndex(), hideFromDetail(), hideWhenCreating(), hideWhenUpdating() Remove the field from one place.
onlyOnIndex(), onlyOnDetail(), onlyOnForms(), onlyOnCreate(), onlyOnEdit(), exceptOnForms() Keep the field in one place only.
show($bool) Show or hide by a runtime condition, such as a feature flag.
default($value) Initial value of the form.
help($text) A hint under the input.
placeholder($text) The input's placeholder.
options($array) The choices of a Select or Checkboxes.
sortable() / unsortable() Whether the index column sorts.
exportable() / unexportable(), importable() / unimportable() Whether the field takes part in export and import.
hideWhenValuesSet() Hide the input when the value arrives in the URL, as when creating from a relationship.
conditional("type=='normal'") Show the input only when another input has a value.

Filters. filters() returns instances of the classes in app/Front/Filters/: TextFilter, SelectFilter (->options([...]), ->multiple()), BooleanFilter (->setTrueValue(...)) and DateFilter. setTitle($title, $field, $slug) names the filter and, optionally, the column and the query-string key; setOperator('>=') changes the comparison and setScope('byPlan') delegates to a model scope instead. A search filter is added for you when the model is searchable.

Actions. actions() returns classes extending App\Front\Actions\Action, one button per record. Set $title and $icon, put $show_on_index = true to offer the button on the list as well as on the detail page, return a boolean from hasPermissions($object) and do the work in handle($object). Return a redirect or nothing. When fields() returns inputs, the action first shows a form and receives its data in $this->data. An action with no fields — or only hidden ones — runs from its button, which sends a POST with the form token; opening its address with GET only shows the action screen and changes nothing, so a link or a prefetch never runs it. index_actions() does the same with classes extending App\Front\Actions\IndexAction, which act on the list rather than on one record. ActingAs, BlockUser, ResetPin and CloneRole in app/Front/Actions/ are working examples. GoToCategories is a ready-made action whose handle() takes no record: it redirects to the categories screen for the type you give it with (new Actions\GoToCategories)->setTitle('Tags')->setType('tag').

Cards. cards() returns cards rendered above the index table; the package ships WeblaborMx\Front\Cards\NumericCard, which you extend with a value() method and which caches its number for five minutes.

Pages. A screen that is not a CRUD — a report, a settings form — extends App\Front\Pages\Page, declares fields() for what it shows and post() for what it saves, and is registered with Route::page('Settings', 'settings'), which mounts App\Front\Pages\Settings at /admin/settings for GET, POST, PUT and DELETE.

Hooks. Override these to run code around the CRUD: indexQuery($query) to change the listing query, indexResult($result) to change the collection, processDataBeforeSaving($data) to edit the input before it is written, beforeUpdate($object, $request), store($object, $request) and update($object, $request) after a save, processAfterSave($object, $request) after either, destroy($object) before deleting (return false to stop it), and beforeRequest() to hijack any request after authorization.

The package's layout hooks are configured in config/front.php. The kit sets scripts_stack to scripts, the Blade stack the application layout already prints, so an input that needs JavaScript pushes it there.

3. Add the route

// routes/admin.php
Route::front('Plan');

Route::front registers the whole CRUD in one line. Inside routes/admin.php the routes take the admin.front.plans name family and the /admin/plans prefix:

Route name Method and URL
admin.front.plans GET /admin/plans
admin.front.plans.create GET /admin/plans/create
admin.front.plans.store POST /admin/plans
admin.front.plans.show GET /admin/plans/{id}
admin.front.plans.edit GET /admin/plans/{id}/edit
admin.front.plans.update PUT /admin/plans/{id}
admin.front.plans.destroy DELETE /admin/plans/{id}
admin.front.plans.import GET /admin/plans/import
admin.front.plans.restore POST /admin/plans/{id}/restore, the soft-delete recovery when the model uses SoftDeletes. A GET answers 405.
admin.front.plans.force-delete DELETE /admin/plans/{id}/force-delete, only for a record already in the trash. The trash asks for confirmation before sending it.
admin.front.plans.index_action, .show_action GET /admin/plans/action/{action} and GET /admin/plans/{id}/action/{action}, the action screens, by action slug; the action runs on the .post route of the same URL.
admin.front.planssort, .up, .down POST /admin/plans/{id}/sortable, /sortable/up and /sortable/down, the order arrows of a sortable model.

A resource that is registered but not linked from anywhere is still reachable by URL and still protected by its policy.

4. Write the policy

<?php

namespace App\Policies;

class PlanPolicy extends BasePolicy
{
    protected string $name = 'plan';
    protected $requiredConfig = 'features.plans_enabled';
}

$name is the singular of the model's table, and it is the second word of every permission the resource needs. $requiredConfig is optional: when the config key it names is false, every check fails, the screen hides from the menu and its routes return 403. Laravel finds App\Policies\PlanPolicy for App\Models\Plan on its own; only a model outside App\Models needs Gate::policy() in AppServiceProvider::boot(), which is how ActivityPolicy is attached.

5. Create the permissions

php artisan db:seed --class=PermissionSeeder
php artisan db:seed --class=RoleSeeder

The first command creates create plan, retrieve plan, update plan and delete plan; the second grants them to the admin role. Until you run both, even the admin sees a 403 on the new screen. The full rules are in /help/roles-and-permissions.

6. Put it in the menu

Nothing to do: the sidebar reads $menu_group, $menu_order and $icon from the class, and hides the entry from anyone whose role lacks retrieve plan. To give a new group a fixed position, add it to the :groups list of resources/views/layouts/sidebars/admin.blade.php; to add a link that is not a resource, add it to :items with a name, url, icon, optional menu_group, order and a show boolean.