VOOZH about

URL: https://lean-admin.dev

⇱ Lean Admin — Laravel package for building custom admin panels


Laravel package for building
custom admin panels

Lean is an admin panel package focused on building fully customized admin panels for customers.

Coming soon, join our waiting list.

Important: Confirm subscription in your inbox (check spam if you can't find the email).
 
1classReportResourceextendsLeanResource
2{
3public $model =Report::class;
4 
5public $lang = [
6'create.title'=>'Vytvořit hlášení',
7 ];
8 
9publicfunctionfields()
10 {
11return [
12Status::make('status')
13->sortable()
14->options(Report::STATUSES)
15->colors([
16'reported'=>'warning',
17'acknowledged'=>'info',
18'resolved'=>'success',
19 ])
20->filterable()
21->searchable()
22->placeholder('Select a status'),
23 ];
24 }
25}

Productive & Modern Tech Stack

Localization

Every language string used by the package can be translated for each resource individually.

Fields

Lean ships with many fields out of the box, making it easy to implement your existing data model.

Fast development

Thanks to our bleeding-edge tech stack, you can build a custom-feeling admin panel extremely quickly.

Resources

Each model gets a resource to tell Lean what fields, actions, titles, labels, and translation strings to use.

Clean code

We don't generate any files. We don't pollute your codebase with repetitive boilerplate code. It's just resources + whatever you choose to customize.

Pages & Actions

Completely custom pages & resource actions for when CRUD isn't enough.

Modern design

Lean ships with a modern & unopinionated theme. You're of course free to customize the UI to match your brand. But if you're short on time — the default one will work great as well.

Responsive

Lean is an admin panel for humans. Humans use mobiles, therefore Lean has perfect mobile support.

Let's build an admin panel together

Rather than showing you fluffy marketing copy, let's look
at actual code that you'll be writing with Lean.

Start with resources

Resources tell Lean how to talk to your database. Each resource has a model, searchable columns, a title column, icon, labels, and more.

 
1classReportResourceextendsLeanResource
2{
3publicstaticstring $model =Report::class;
4 
5publicstaticarray $searchable = [
6'id',
7'description',
8'status',
9 ];
10 
11publicstaticstring $title ='description';
12publicstaticstring $icon ='heroicon-o-exclamation';
13 
18 
23 
28 
29publicstaticfunctiontitleFor(Model $model):string
30 {
31returnStr::of($model->description)->limit(40, '...');
32 }
33}

Then add fields

Use fields to represent database columns, relationships, files, or any other data.

 
1return [
2ID::make(),
3Textarea::make('description'),
4Status::make('status')->options(Report::statuses()),
5Image::make('image_path'),
6BelongsTo::make('category'),
7BelongsTo::make('location'),
8Text::make('concrete_location'),
9BelongsTo::make('assignee'),
10BelongsTo::make('reporter'),
11UpdatedAt::make(),
12CreatedAt::make(),
13];

Configure fields

Fields can be heavily customized both in appearance and behavior using a fluent API.

 
1return [
2ID::make(),
3 
4Textarea::make('description')
5->sortable()
6->expanded()
7->resolveValueUsing(function (Field $field, mixed $value) {
8if ($field->action->index()) { // If we're on index...
9// Add a link to the field. Shift+click opens the Report detail in a modal.
10 $field->link(fn () => static::route('show', $field->model), modal: ['show', '$model']);
11 
12returnstatic::titleFor($field->model);
13 }
14 
15// Otherwise return the normal value
16return $value;
17 })
18->filterable(),
19 
20Status::make('status')
21->sortable()
22->options(Report::statuses())
23->colors([ // Set the colors for individual values
24'reported'=>'warning', // These are defined in config/lean.php
25'acknowledged'=>'info',
26'resolved'=>'success',
27 ])
28->filterable()
29->searchable() // Make the select searchable
30->placeholder('Select a status'),
31 
32Image::make('image_path')
33->display('show', 'edit')
34->resolveUrlUsing(fn (Image $image, string $path) => asset("storage/$path"))
35->readonly(),
36 
37BelongsTo::make('category')
38->filterable()
39->createButton() // Show a create button next to the select
40->sortable(),
41 
42BelongsTo::make('location')
52->filterable()
53->sortable(),
54 
55Text::make('concrete_location')
56->filterable()
57->display('write')
58->datalist(
59// Datalist suggests values but lets the user enter a custom text
60fn () => ConcreteLocation::cursor()->pluck('name')->toArray()
61 )
62->sortable(),
63 
64BelongsTo::make('assignee')
65->filterable()
66->sortable(),
67 
68BelongsTo::make('reporter')
69->filterable()
70->sortable(),
71 
72UpdatedAt::make()->filterable(),
73CreatedAt::make()->filterable(),
74];

Configure actions

You can customize how each action (index/create/show/edit) behaves.
You can also disable, replace, or duplicate each action.
In the example below, we're using a custom Create action and three separate Index actions.

 
1// Unprocessed reports = reports which still need to be acknowledged
2Index::make('unprocessed')
3->access(fn (User $user) => $user->isSuperAdmin()) // Only let the superadmin see this
4->header(buttons: false) // Hide buttons (e.g. 'Create Report') from the header
5->title('Unprocessed Reports')
6->search(false) // Remove the search bar
7->filters(false) // Remove filters
8->advanced(false) // Remove all advanced search features
9->fields(except: ['status', 'reported_by'])
10->menuLink(fn (MenuLink $link) => $link->icon('heroicon-o-bell'))
11->buttons([
12 $acknowledge, // Sets the status to 'acknowledged'
13 $delegate, // Delegates the task to another administrator
14'delete', // Default delete button to discard spam reports
15 ])
16->scope(fn (Builder $query) => $query->where('status', 'reported'))
17->perPage(15)
18->bulkActions([ // Set the bulk actions that can be run on selected records
19BulkAcknowledge::make(),
20BulkDelegate::make(),
21 ]),
22 
23// My reports = reports assigned to the current user
24Index::make('my')
25->fields(except: ['assignee']) // We don't need to show the assignee
26->header(buttons: false)
27->title('My Reports')
28->filters(false)
29->advanced(false)
30->search(false)
31->buttons([
32 $acknowledge,
33 $resolve,
34'show',
35'edit'
36 ])
37->menuLink(fn (MenuLink $link) => $link->icon('heroicon-o-inbox'))
38->scope(fn (Builder $query) => $query
39->where('assignee_id', auth()->id())
40->where('status', '!=', 'resolved')
41 )
42->bulkActions([
43BulkAcknowledge::make(),
44BulkResolve::make(),
45 ]),
46 
47Index::make()
48->access(fn (User $user) => $user->isSuperAdmin())
49->fields(except: 'id')
50->menuLink(fn (MenuLink $link) => $link->label('All Reports'),
51 
52Show::make(), // These were not modified
53 
54Edit::make(), // These were not modified
55 
56// We're using a custom Livewire component for creating reports with better UX
-Create::make(),
+CreateReport::make('create'),

Buttons

The example above uses $acknowledge, $delegate, and $resolve in the buttons() sections. You may wonder, how are buttons like that defined? And how do they handle behavior?
Simple, you can just use our Element classes to create renderable HTML tags with PHP behavior:

 
1Button::make('Acknowledge')
2->click(function (Report $report) {
3 $report->update([
4'status'=>'acknowledged',
5 ]);
6 
7Lean::notify('Report acknowledged!');
8 })
9->if(fn () => $report->status ==='reported');
10 
11Button::make('Delegate')
12->success()
13->xClick('$index.bulkAction("bulk-delegate", [$model])');
14 
15Button::make('Resolve')
16->success()
17->click(function (Report $report) {
18 $report->update([
19'status'=>'resolved',
20 ]);
21 
22Lean::notify('Report resolved!');
23 })
24->if(fn () => $report->status ==='acknowledged');

Bulk Actions

In the example above, you can see that Acknowledge and Resolve are simple button clicks. Here's what they trigger.

 
1classBulkDelegateextendsBulkAction
2{
12 
13publicfunctionfields(FieldCollection $resource):array
14 {
15return [
16 $resource['assignee']->placeholder(text: '--- Select an administrator ---', enabled: true),
17 ];
18 }
19 
20publicfunctionabove()
21 {
22 $count =count($this->modelIds);
23 
24returnAlertPanel::make("$count reports will be delegated.")
25->icon('heroicon-o-user-add')
26->class('mt-3')
27->success();
28 }
29 
30publicfunctionalpine():string
31 {
32return<<<'JS'
33 {
34confirmed() {
35// This action affects a column by which the user might be filtering results.
36// Therefore, to avoid bugs, we clear the selection after the action runs.
37this.index.selected = [];
38 }
39 }
40JS;
41 }
42 
43publicfunctionbuttons(Button $cancel, Button $confirm):array
44 {
45return [
46 $cancel,
47 $confirm->success(),
48 ];
49 }
50 
51publicstaticfunctionhandle(LazyCollection $models, FieldCollection $fields, string $resource)
52 {
53 $ids = $models->pluck('id')->toArray();
54 
55 $resource::updateMany($models, [
56'assignee_id'=> $fields['assignee']->getStoreValue(),
57'status'=>'acknowledged',
58 ]);
59 
60return $ids;
61 }
62 
63publicfunctionafter($ids)
64 {
65Lean::notify(count($ids) .' reports delegated!');
66 
67returnLean::modal()->confirm($ids);
68 }
69}

Custom Actions

A few sections above, we instructed Lean to use a custom Livewire component for the Create action. Presumably we'll have to make some changes to this component — to make it work with Lean.

How hard is it? See for yourself.

 
1classCreateextendsComponent
2{
-useWithFileUploads;
+useWithFileUploads, WithResourceAction;
5 
21 
22publicfunctionmount()
23 {
24$this->resourceAction(resource: 'reports', alias: 'create');
25 
26Lean::setTitle($this->resource()::trans('create.title'));
27 
28$this->location =request()->query('location');
29 
30// ...
31 }
32 
33// ...
34 
35publicfunctionsubmit()
36 {
37$this->validate();
38 
49 
50if ($this->inLean()) {
51Lean::notifyOnNextPage($this->resource()::trans('notifications.created'));
52 
53returnredirect(
54$this->resource()::route('show', $this->report)
55 );
56 }
57 }
58}
 
-<div>
+<div@leanAction>
3 
+ <x-lean::assetname="filepond">
5 <linkhref="https://unpkg.com/filepond/dist/filepond.css"rel="stylesheet">
6 <scriptsrc="https://unpkg.com/filepond/dist/filepond.js"></script>
+ </x-lean::asset>
8 
9 <x-slotname="header">
10 Create Report
11 </x-slot>
12 
- <divclass="shadow-sm sm:rounded-lg">
+ <divclass="@unless($this->inLean()) shadow-sm sm:rounded-lg border-gray-200 @endunless">
15{{-- We can make styles Lean-specific (or only used outside of Lean) by using $this->inLean() --}}
16 </div>
17</div>

Here's what we have now, but there's more

👁 Image

Mobile support

Not only is Lean fully responsive, it has a dedicated mobile menu and is installable as an app (PWA) on any smartphone or computer.

👁 Sidebar on mobile
👁 Create user on mobile
👁 Delegate on mobile

Dark mode

👁 Dark mode

Magic modals

When you shift+click any link in Lean, it will open in a special modal view. The modal is fully navigable, and when you make any change to a resource, it will be reflected everywhere else on the page.

👁 Modals

Advanced datatables

Lean has extremely advanced (yet easy to use) filters for most fields. The filters are also fully configurable and customizable.

👁 Filters

Make it local

We've taken a completely different approach to localization than other admin panels. We don't have any complex inflection systems or anything like that. We simply let you and your translators specify language strings for each resource individually.

 
1publicstaticarray $lang = [
2'create.submit'=>'Vytvořit administrátora',
3'create.title'=>'Vytvořit administrátora',
4'edit.submit'=>'Upravit administrátora',
5'edit.title'=>'Upravit administrátora :title',
6];
7 
8publicstaticfunctionlabel():string
9{
10return'Administrátor';
11}
12 
13publicstaticfunctionpluralLabel():string
14{
15return'Administrátoři';
16}

Custom pages

Build pages from scratch and add them to your admin panel. CRUD actions work well for many things — and save a huge amount of time — but admin panels contain more than that.

 
1classStatisticsextendsLeanPage
2{
3useWithNotifications;
4 
5publicstaticfunctionmenu(MenuLink $link):MenuLink
6 {
7return $link
8->label('Statistics')
9->icon('heroicon-o-chart-bar');
10 }
11 
12publicfunctionrefresh()
13 {
14Cache::forget('stats');
15 
16$this->notify('Data was refreshed!');
17 }
18 
19publicfunctionrender()
20 {
21Lean::fullWidth();
22 
23returnview('lean.pages.stats', [
24'unresolved'=>Report::where('status', 'accepted'),
25'resolved'=>Report::where('status', 'resolved'),
26'total'=>Report::where('status', '!=', 'reported'),
27 ]);
28 }
29}

Ergonomic config file

You can control the theme, font, and similar things in the comfort of your config/lean.php file.

 
1'colors'=> [
2/**
3 * The theme color of your admin panel.
4 *
-'theme'=>'blue',
+'theme'=>'purple',
12 
13/**
14 * The theme code of your admin panel.
15 *
16 * This is used in the manifest and meta tag.
23 */
-'theme_code'=>'default',
+'theme_code'=>'#2b9dff',
26 
27'info'=>'blue',
28'danger'=>'red',
29'success'=>'green',
30'warning'=>'yellow',
31],

Preconfigured fields

Don't waste time specifying the same config again and again. You can configure fields upfront and they'll respect those settings — unless you override them.

 
1Pikaday::make('updated_at')->display('show', 'edit')
-->phpFormat('d.m.Y')
-->jsFormat('DD.MM.YYYY');
4 
5Pikaday::make('created_at')->display('show', 'create')
-->phpFormat('d.m.Y')
-->jsFormat('DD.MM.YYYY');
8 
9Pikaday::make('last_visited_at')->display('show')
-->phpFormat('d.m.Y')
-->jsFormat('DD.MM.YYYY');
12 
13Pikaday::preconfigure(function (Pikaday $pikaday) {
14 $pikaday
+->phpFormat('d.m.Y')
+->jsFormat('DD.MM.YYYY');
17});
18 
19Pikaday::make('updated_at')->display('show', 'edit');
20Pikaday::make('created_at')->display('show', 'create');
21Pikaday::make('last_visited_at')->display('show');

Publishing Laravel assets is cool, most packages can do that.
But can they publish JS and CSS assets?

1. Publish assets

 
1$ php artisan lean:publish
2What asset type do you want to publish?:
3 [js ] JavaScript files
4 [css ] CSS files
5 [assets ] Frontend assets
6 [config ] Configuration file
7 [views ] Blade views
8 [colors ] Color definitions
9 [translations] Translation files
10> css
11Path [All]:
12 [* ] All
13 [base.css ] base.css
14 [buttons.css] buttons.css
15 [inputs.css ] inputs.css
16 [main.css ] main.css
17> buttons.css
18 
19Anything else? (yes/no) [no]:
20> no

2. Make your changes

 
5/* ... */
6 
7.btn {
8/* Base button style */
9 @applyinline-flexitems-center
-px-4 py-2
+px-3 py-1.5
12borderborder-transparent
13text-basefont-almost-boldrounded-md
14focus:outline-none;
15}

3. Recompile using artisan 😎

 
1$ php artisan lean:build
2ℹ Compiling Mix
3Laravel Mix v6.0.25
4✔ Compiled Successfully in 21085ms

Perfect Turbolinks integration

We have an internal Turbolinks fork which provides a flawlessly smooth experience with perfect cache invalidation and handling of all other edge cases.

Turbolinks is enabled by default, but if you don't want to use it, simply comment out these two lines of Blade in your Lean layout.

 
1<html>
2 <head>
3 <x-lean::layout.meta />
4 <x-lean::layout.styles />
5 <x-lean::layout.pwafor="head" />
6 <x-lean::layout.scriptsfor="head" />
7 <x-lean::layout.darkmodefor="head" />
+ <x-lean::layout.turbolinksfor="head" />
9 </head>
10 
15 ...
16 
17 <x-lean::notifications />
18 <x-lean::console-log />
19 
20 <x-lean::layout.scriptsfor="body" />
21 <x-lean::layout.pwafor="body" />
+ <x-lean::layout.turbolinksfor="body" />
23 </body>
24</html>

Coming soon

Join the waiting list

Lean is launching soon

Join the our waiting list to be notified when Lean is released

Coming soon, join our waiting list.

Important: Confirm subscription in your inbox (check spam if you can't find the email).

FAQ

Questions & Answers

As much as you want. You can change field views, field behavior, CRUD actions (e.g. custom CreateOrder action), language strings, pieces of the layout, create custom Pages when CRUD resources aren't enough, and much more.
Yes. Lean lets you translate the global language strings as well as every language string individually for each resource.
Lean is based on Livewire, Tailwind CSS, and Alpine.js. This stack lets anyone be productive with it. Knowing Blade is all you need to customize the views.

Lean uses cutting-edge framework features, so only Laravel 8 is supported. It also requires PHP 8.0.
The package's API is very expressive (see the docs for yourself) and very strongly typed. Basically, anything that can be typed — is typed. This saves both us & you a lot of time fixing strange bugs, because most things that would've caused issues get caught by psalm or PHPStan.

The code is aggressively tested with PHPUnit, psalm, php-cs-fixer, and PHPStan in our CI pipeline.
The price will be announced in the email sent on launch day. As for licensing, Lean uses yearly licenses in the form of one-time purchases. In other words, you'll pay for the license and will receive updates for one year. After the license expires, you'll be able to keep using the last version that was released while your license was active. If you extend your license, you'll get another year of updates.
The app whose code was used in the examples above will be a public GitHub repository when Lean is launched.