Widgets
Dokumentasi ini merupakan referensi untuk seluruh class di dalam PandaPanel\Widgets:
- base
Widget; - empat concrete widget type;
- value object yang digunakan Widget untuk membangun payload;
- serta mekanisme registration dan resolution di sekitarnya.
Gunakan halaman ini ketika Anda sudah memahami apa itu Widget dan membutuhkan detail seperti:
- signature yang tepat;
- default value;
- serialized key;
- lifecycle;
- atau behavior internal.
Untuk pembahasan konsep seperti:
- kapan menggunakan masing-masing Widget;
- kapan sebuah Widget sebaiknya dibuat lazy;
lihat:
Sebuah Widget menghitung seluruh datanya di server kemudian mengirim serialized description ke Vue:
scalar
array
null2
3
Tidak ada:
- Closure;
- executable PHP;
- class name;
yang dikirim ke frontend.
Contoh minimal yang berfungsi
Buat Stats Widget:
php artisan make:panel-widget UserStats --panel=Admin --type=statsKemudian:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use App\Models\User;
use PandaPanel\Widgets\Enums\StatColor;
use PandaPanel\Widgets\StatsWidget;
use PandaPanel\Widgets\Support\Stat;
final class UserStats extends StatsWidget
{
protected static int $sort = 10;
protected static ?string $heading = 'Accounts';
/** @return list<Stat> */
public function stats(): array
{
return [
Stat::make(
'Total users',
User::query()->count()
)
->icon('users'),
Stat::make(
'Verified',
User::query()
->whereNotNull(
'email_verified_at'
)
->count()
)
->icon('shield')
->color(
StatColor::Success
),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Aktifkan discovery pada Panel:
use PandaPanel\Core\Panel;
public function panel(
Panel $panel
): Panel {
return $panel
->path('admin')
->discoverWidgets(
app_path(
'Panels/Admin/Widgets'
)
);
}2
3
4
5
6
7
8
9
10
11
12
13
Sekarang:
GET /adminakan merender Dashboard dengan Widget tersebut.
PandaPanel\Widgets\Widget
Widget merupakan abstract base class dari keempat tipe Widget.
Class ini mengimplementasikan:
PandaPanel\Contracts\WidgetContractStatic configuration
| Property | Type | Default | Dibaca melalui |
|---|---|---|---|
$sort | int | 0 | sort() |
$columnSpan | int|string|array<string, int|string> | 1 | columnSpan() |
$lazy | bool | false | isLazy() |
$heading | ?string | null | heading() |
$description | ?string | null | description() |
$pollingInterval | ?int | null | pollingInterval() |
Contoh:
use PandaPanel\Widgets\StatsWidget;
final class QueueDepth extends StatsWidget
{
protected static int $sort = 5;
protected static int|string|array
$columnSpan = [
'default' => 1,
'md' => 2,
'lg' => 3,
'xl' => 4,
];
protected static bool $lazy = true;
protected static ?string $heading =
'Queue';
protected static ?string $description =
'Jobs waiting, by connection.';
protected static ?int $pollingInterval =
30;
// ...
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Widget diurutkan berdasarkan:
[sort(), id()]Jadi jika dua Widget memiliki $sort yang sama, urutannya fallback ke id secara alfabetis, bukan registration order.
Methods
| Method | Signature | Catatan |
|---|---|---|
type() | abstract public static function type(): WidgetType | Diimplementasikan oleh masing-masing base Widget type |
data() | abstract public function data(): array | Payload yang diterima renderer |
id() | public static function id(): string | Str::kebab(class_basename(static::class)) |
sort() | public static function sort(): int | |
isLazy() | public static function isLazy(): bool | |
heading() | public static function heading(): ?string | |
description() | public static function description(): ?string | |
pollingInterval() | public static function pollingInterval(): ?int | Dalam detik |
canView() | public static function canView(): bool | Default true |
columnSpan() | public static function columnSpan(): array | Dinormalisasi per breakpoint |
filterSchema() | public function filterSchema(): ?FormSchema | Default null |
filtersInModal() | public static function filtersInModal(): bool | Default false |
withPageContext() | public function withPageContext(PageContext $context): static | Dipanggil oleh Page |
withFilters() | public function withFilters(array $filters): static | Dipanggil oleh Page |
context() | protected function context(): PageContext | Throw jika context tidak tersedia |
filter() | protected function filter(string $name, mixed $default = null): mixed | |
filters() | protected function filters(): array | |
toDefinition() | public function toDefinition(): array | Serialized Widget |
toArray() | public function toArray(): array | Alias dari toDefinition() |
id()
public static function id(): string;Id dibuat dari kebab-case basename class.
Contoh:
UserStats::id();
// 'user-stats'
RecentUsers::id();
// 'recent-users'2
3
4
5
Id ini stabil antar-run sehingga:
- urutan Widget;
- key deferred data;
- filter namespace;
semuanya menggunakan identifier yang sama.
canView()
public static function canView(): bool;Method ini diperiksa oleh:
WidgetCollectionsebelum Widget di-construct.
Artinya Widget yang tidak diizinkan:
canView()
↓
false
↓
tidak di-construct
↓
data() tidak dipanggil
↓
query tidak berjalan2
3
4
5
6
7
8
9
Contoh:
use Illuminate\Support\Facades\Auth;
public static function canView(): bool
{
return Auth::user()
?->can(
'viewRevenue'
)
?? false;
}2
3
4
5
6
7
8
9
10
columnSpan()
/**
* @return array{
* default: int|string,
* md: int|string,
* lg: int|string,
* xl: int|string
* }
*/
public static function columnSpan(): array;2
3
4
5
6
7
8
9
Method ini menjalankan $columnSpan melalui:
ColumnSpan::normalize()Contoh:
protected static int|string|array
$columnSpan = 'full';
UserStats::columnSpan();2
3
4
Hasil:
[
'default' => 'full',
'md' => 'full',
'lg' => 'full',
'xl' => 'full',
]2
3
4
5
6
withFilters() dan filter()
/**
* @param array<string, mixed> $filters
*/
public function withFilters(
array $filters
): static;
protected function filter(
string $name,
mixed $default = null
): mixed;
/**
* @return array<string, mixed>
*/
protected function filters(): array;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
filter() mengembalikan default jika value:
nullatau empty string:
''Dengan demikian control yang sudah dibersihkan dibaca sebagai:
absentbukan sebagai empty string literal.
Contoh:
$widget =
(new UserGrowth)
->withFilters([
'months' => '12',
]);
// Di dalam Widget:
$months =
(int)
$this->filter(
'months',
6
);
// 122
3
4
5
6
7
8
9
10
11
12
13
14
15
Value yang diterima Widget sudah dibatasi oleh schema yang mendeklarasikannya.
Key yang tidak pernah dideklarasikan schema tidak akan tersedia, apa pun isi query string.
filterSchema() dan filtersInModal()
public function filterSchema():
?FormSchema;
public static function filtersInModal():
bool;2
3
4
5
Contoh:
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
public function filterSchema():
FormSchema
{
return FormSchema::make()
->schema([
Select::make('months')
->label('Window')
->options([
'6' =>
'Last 6 months',
'12' =>
'Last 12 months',
])
->default('6'),
]);
}
public static function filtersInModal():
bool
{
return true;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
State filter disimpan di query string dengan bentuk:
widgets[{id}][{field}]Contoh:
?widgets[user-growth][months]=12State juga dipersist per Page di session.
Lihat:
withPageContext() dan context()
public function withPageContext(
PageContext $context
): static;
/**
* @throws LogicException
*/
protected function context():
PageContext;2
3
4
5
6
7
8
9
Hanya Resource Page yang memberikan Page Context.
context() melempar exception daripada mengembalikan empty context.
Alasannya:
Widget yang mencoba membaca record tetapi tidak pernah diberikan record berarti berada pada Page yang salah.
Contoh:
public function stats(): array
{
return [
Stat::make(
'Rows on this tab',
$this
->context()
->count()
),
];
}2
3
4
5
6
7
8
9
10
11
toDefinition()
/**
* @return array<string, mixed>
*/
public function toDefinition(): array;2
3
4
Serialized Widget memiliki key berikut:
| Key | Type | Value |
|---|---|---|
id | string | static::id() |
type | string | static::type()->value |
sort | int | |
columnSpan | array{default, md, lg, xl} | |
lazy | bool | |
heading | ?string | |
description | ?string | |
polling | ?int | Detik, dari pollingInterval() |
filters | ?array{inModal: bool, form: array} | null jika tidak ada filterSchema() |
data | ?array | null untuk lazy Widget |
component | string | Hanya CustomWidget |
Contoh:
(new UserStats)
->toDefinition()['id'];
// 'user-stats'2
3
4
PandaPanel\Widgets\StatsWidget
API:
public static function type():
WidgetType;
// WidgetType::Stats
/** @return list<Stat> */
abstract public function stats():
array;
/**
* @return array{
* stats: list<array<string, mixed>>
* }
*/
public function data():
array;2
3
4
5
6
7
8
9
10
11
12
13
14
15
Contoh:
use App\Models\Order;
use PandaPanel\Widgets\Enums\StatColor;
use PandaPanel\Widgets\StatsWidget;
use PandaPanel\Widgets\Support\Stat;
final class OrderStats extends StatsWidget
{
/** @return list<Stat> */
public function stats(): array
{
return [
Stat::make(
'Orders',
Order::query()->count()
)
->icon('receipt'),
Stat::make(
'Revenue',
(float)
Order::query()
->sum('total')
)
->format(
prefix: '£',
decimals: 2
)
->color(
StatColor::Success
),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Lakukan aggregation di database.
Contohnya:
Order::query()->count();Bukan:
Order::query()
->get()
->count();2
3
Meng-hydrate seluruh collection hanya untuk menghitung jumlah adalah salah satu cara paling umum membuat Dashboard menjadi Page paling lambat di application.
PandaPanel\Widgets\TableWidget
Defaults:
protected static int|string|array
$columnSpan = [
'default' => 1,
'md' => 2,
'lg' => 2,
'xl' => 2,
];
protected static string $emptyMessage =
'Nothing to show yet.';
protected static int $perPage = 5;2
3
4
5
6
7
8
9
10
11
12
API:
public static function type():
WidgetType;
// WidgetType::Table
abstract public function table(
TableSchema $table
): TableSchema;
/**
* @return Builder<covariant Model>
*/
abstract public function query():
Builder;
public static function stateNamespace():
string;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Contoh:
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tables\Columns\DateTimeColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\SortDirection;
use PandaPanel\Tables\TableSchema;
use PandaPanel\Widgets\TableWidget;
final class RecentUsers extends TableWidget
{
protected static string
$emptyMessage =
'No one has signed up yet.';
protected static int
$perPage = 5;
public function table(
TableSchema $table
): TableSchema {
return $table
->columns([
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('email')
->searchable(),
DateTimeColumn::make(
'created_at'
)
->label('Joined')
->relative()
->sortable(),
])
->defaultSort(
'created_at',
SortDirection::Descending
);
}
/** @return Builder<User> */
public function query(): Builder
{
return User::query()
->select([
'id',
'name',
'email',
'created_at',
]);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
data() menjalankan schema melalui:
PandaPanel\Tables\TableQueryyang sama dengan yang digunakan Resource Index.
Hasilnya:
| Key | Type | Catatan |
|---|---|---|
columns | array | Dari TableSchema::toArray()['columns'] |
rows | list<array> | Satu TableSchema::toRow() per record |
emptyMessage | string | static::$emptyMessage |
state | array | Search, sort, direction, perPage, filters, columns, group |
pagination | array{page, perPage, total, lastPage, from, to} | |
namespace | string | static::stateNamespace() |
searchable | bool | TableSchema::isSearchable() |
stateNamespace() menghasilkan:
'widgets.'
. Str::kebab(
class_basename(
static::class
)
)2
3
4
5
6
Contoh:
RecentUsers::stateNamespace();
// 'widgets.recent-users'2
3
Sehingga state Table berada di:
?widgets[recent-users][page]=2Namespacing inilah yang memungkinkan beberapa Table Widget berada di satu Dashboard tanpa state saling bertabrakan.
Per-page options dipaksa menjadi:
[
$perPage,
]2
3
Artinya Table Widget tidak memiliki page-size control.
Table Widget merupakan ringkasan yang dapat:
- dicari;
- disortir;
- dipaginate;
bukan Resource Index kedua.
Karena itu Table Widget tidak memiliki:
- Bulk Actions;
- Column Manager;
- Filter Tabs.
PandaPanel\Widgets\ChartWidget
Defaults:
protected static int|string|array
$columnSpan = [
'default' => 1,
'md' => 2,
'lg' => 2,
'xl' => 2,
];
protected static ChartVariant
$variant =
ChartVariant::Bar;
protected static int
$maxHeight = 220;2
3
4
5
6
7
8
9
10
11
12
13
14
API:
public static function type():
WidgetType;
// WidgetType::Chart
/** @return list<string> */
abstract public function labels():
array;
/** @return list<ChartSeries> */
abstract public function series():
array;
public function options():
ChartOptions;2
3
4
5
6
7
8
9
10
11
12
13
14
Contoh:
use PandaPanel\Widgets\ChartWidget;
use PandaPanel\Widgets\Enums\ChartVariant;
use PandaPanel\Widgets\Enums\StatColor;
use PandaPanel\Widgets\Support\ChartOptions;
use PandaPanel\Widgets\Support\ChartSeries;
final class UserGrowth extends ChartWidget
{
protected static ChartVariant
$variant =
ChartVariant::Area;
protected static int
$maxHeight = 200;
/** @return list<string> */
public function labels(): array
{
return [
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
];
}
/** @return list<ChartSeries> */
public function series(): array
{
return [
ChartSeries::make(
'Sign-ups',
[
4,
9,
7,
12,
18,
21,
]
)
->color(
StatColor::Info
),
];
}
public function options():
ChartOptions
{
return ChartOptions::make()
->legend(false)
->curved()
->filled();
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
data() menghasilkan:
variant
labels
series
options
maxHeight2
3
4
5
Setiap point pada seluruh series menggunakan daftar label yang sama.
Chart digambar menggunakan dependency-free inline SVG yang sudah di-compile.
Yang dikirim ke frontend adalah deskripsi Chart, bukan configuration tree untuk third-party chart library.
Jika kebutuhan Chart tidak dapat diekspresikan oleh:
ChartOptionsgunakan:
CustomWidgetPandaPanel\Widgets\CustomWidget
API:
/**
* Path di bawah resources/js/pages/,
* misalnya:
* Panels/Admin/Widgets/ServerHealth
*/
protected static string
$component = '';
public static function type():
WidgetType;
// WidgetType::Custom
/**
* @throws RuntimeException
* jika $component kosong
*/
public static function component():
string;
public function toDefinition():
array;
// parent definition + component2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Contoh:
use Illuminate\Foundation\Application;
use PandaPanel\Widgets\CustomWidget;
final class SystemInfo extends CustomWidget
{
protected static string
$component =
'Panels/Admin/Widgets/SystemInfo';
/**
* @return array<string, mixed>
*/
public function data(): array
{
return [
'laravel' =>
Application::VERSION,
'php' =>
PHP_VERSION,
'environment' =>
app()->environment(),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Vue:
<script setup lang="ts">
defineProps<{
data: {
laravel: string
php: string
environment: string
}
}>()
</script>
<template>
<dl class="space-y-1 text-sm">
<div>
<dt>Laravel</dt>
<dd>{{ data.laravel }}</dd>
</div>
<div>
<dt>PHP</dt>
<dd>{{ data.php }}</dd>
</div>
<div>
<dt>Environment</dt>
<dd>{{ data.environment }}</dd>
</div>
</dl>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Component harus berada di:
resources/js/pages/Panels/**/Widgets/*.vueFrontend me-resolve component menggunakan build-time:
import.meta.globJadi component name yang tidak ikut di-compile tidak dapat diakses, apa pun string yang diterima runtime.
Unknown component:
- merender neutral fallback;
- memberikan warning sekali di development.
Lihat:
PandaPanel\Widgets\Support\Stat
Stat merupakan:
final readonlyFluent method-nya menghasilkan instance baru.
Jadi Stat tidak dimutasi setelah diserahkan Widget.
Constructor:
public function __construct(
public string $label,
public string|int|float $value,
public ?string $description = null,
public ?string $icon = null,
public StatColor $color =
StatColor::Default,
public ?array $trend = null,
public array $chart = [],
public ?string $url = null,
public ?string $prefix = null,
public ?string $suffix = null,
public ?int $decimals = null,
) {}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
API:
| Method | Signature |
|---|---|
make() | public static function make(string $label, string|int|float $value): self |
description() | public function description(string $description): self |
icon() | public function icon(string $icon): self |
color() | public function color(StatColor $color): self |
trend() | public function trend(string $direction, float $value): self |
chart() | public function chart(array $values): self |
url() | public function url(string $url): self |
format() | public function format(?string $prefix = null, ?string $suffix = null, ?int $decimals = null): self |
display() | public function display(): string |
toArray() | public function toArray(): array |
Contoh:
use App\Panels\Admin\Resources\Users\UserResource;
use PandaPanel\Widgets\Enums\StatColor;
use PandaPanel\Widgets\Support\Stat;
Stat::make(
'Revenue',
12045
)
->format(
prefix: '£',
decimals: 2
)
// display: "£12,045.00"
->icon('receipt')
->color(
StatColor::Success
)
->trend(
'up',
12.4
)
// 'up' | 'down' | 'neutral'
->chart([
4,
9,
7,
12,
18,
21,
])
->url(
UserResource::url()
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
display() melakukan format di server karena server mengetahui arti angka tersebut.
Contoh:
Stat::make(
'Revenue',
1204.5
)
->format(
prefix: '£',
decimals: 2
)
->display();
// '£1,204.50'2
3
4
5
6
7
8
9
10
11
String tidak diubah:
Stat::make(
'Uptime',
'99.9%'
)
->display();
// '99.9%'2
3
4
5
6
7
String dibiarkan persis seperti yang diberikan Widget.
Tanpa decimals:
float
→ 2 decimal
int
→ 0 decimal2
3
4
5
toArray() mengirim:
label
value
display
description
icon
color
trend
chart
url2
3
4
5
6
7
8
9
Property berikut tidak diserialisasi secara terpisah:
prefix
suffix
decimals2
3
Ketiganya hanya digunakan untuk menghasilkan:
displayPandaPanel\Widgets\Support\ChartSeries
ChartSeries juga merupakan:
final readonlyConstructor:
/**
* @param list<int|float> $values
*/
public function __construct(
public string $label,
public array $values,
public StatColor $color =
StatColor::Default,
) {}2
3
4
5
6
7
8
9
10
11
Factory:
/**
* @param list<int|float> $values
*/
public static function make(
string $label,
array $values
): self;2
3
4
5
6
7
Color:
public function color(
StatColor $color
): self;2
3
Serialization:
/**
* @return array{
* label: string,
* values: list<int|float>,
* color: string
* }
*/
public function toArray():
array;2
3
4
5
6
7
8
9
Contoh:
use PandaPanel\Widgets\Enums\StatColor;
use PandaPanel\Widgets\Support\ChartSeries;
ChartSeries::make(
'Sign-ups',
[
4,
9,
7,
]
)
->color(
StatColor::Info
)
->toArray();2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hasil:
[
'label' =>
'Sign-ups',
'values' => [
4,
9,
7,
],
'color' =>
'info',
]2
3
4
5
6
7
8
9
10
11
12
13
PandaPanel\Widgets\Support\ChartOptions
ChartOptions merupakan closed set configuration.
Bukan arbitrary options tree.
Object ini mutable dan fluent.
Setiap method mengembalikan instance yang sama.
| Method | Signature | Default |
|---|---|---|
make() | public static function make(): self | |
legend() | public function legend(bool $legend = true): self | true |
grid() | public function grid(bool $grid = true): self | true |
stacked() | public function stacked(bool $stacked = true): self | false |
filled() | public function filled(bool $filled = true): self | false |
curved() | public function curved(bool $curved = true): self | false |
labels() | public function labels(bool $labels = true): self | false |
range() | public function range(?float $min, ?float $max): self | null, null |
format() | public function format(?string $prefix = null, ?string $suffix = null): self | null, null |
toArray() | public function toArray(): array |
Contoh:
use PandaPanel\Widgets\Support\ChartOptions;
ChartOptions::make()
->legend(false)
->stacked()
->range(
0,
100
)
->format(
suffix: '%'
)
->toArray();2
3
4
5
6
7
8
9
10
11
12
13
Hasil:
[
'legend' =>
false,
'grid' =>
true,
'stacked' =>
true,
'filled' =>
false,
'curved' =>
false,
'labels' =>
false,
'min' =>
0.0,
'max' =>
100.0,
'prefix' =>
null,
'suffix' =>
'%',
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
range() berguna jika Chart dibaca terhadap target tetap, bukan relatif terhadap datanya sendiri.
Misalnya:
0–100%Tanpa fixed range, axis yang terus menyesuaikan diri terhadap data dapat membuat setiap minggu terlihat memiliki bentuk yang hampir sama walaupun skala sebenarnya berubah.
PandaPanel\Widgets\Support\ColumnSpan
API:
/**
* @param int|string|array<string, int|string> $span
*
* @return array{
* default: int|string,
* md: int|string,
* lg: int|string,
* xl: int|string
* }
*/
public static function normalize(
int|string|array $span,
string $context = 'A widget'
): array;2
3
4
5
6
7
8
9
10
11
12
13
14
Breakpoint yang tersedia:
default
md
lg
xl2
3
4
Maximum span:
4Value:
fulldibiarkan sebagai:
fullContoh:
use PandaPanel\Widgets\Support\ColumnSpan;
ColumnSpan::normalize(2);2
3
Hasil:
[
'default' => 2,
'md' => 2,
'lg' => 2,
'xl' => 2,
]2
3
4
5
6
Partial breakpoint:
ColumnSpan::normalize([
'default' => 1,
'lg' => 2,
]);2
3
4
Hasil:
[
'default' => 1,
'md' => 1,
'lg' => 2,
'xl' => 2,
]2
3
4
5
6
md mewarisi:
defaultNilai terlalu besar:
ColumnSpan::normalize(99);menjadi:
[
'default' => 4,
'md' => 4,
'lg' => 4,
'xl' => 4,
]2
3
4
5
6
Karena maximum grid span adalah empat.
Dua kondisi menghasilkan:
PandaPanel\Exceptions\PanelSchemaExceptionTypo span:
ColumnSpan::normalize(
'ful'
);2
3
Secara konsep error-nya:
declares a column span of [ful],
which is neither a number nor "full"2
Unknown breakpoint:
ColumnSpan::normalize([
'default' => 1,
'sm' => 2,
]);2
3
4
Error:
declares a column span at [sm]
...
It has:
default, md, lg, xl2
3
4
Angka di luar range dianggap permintaan untuk menggunakan lebih banyak Column daripada yang tersedia.
Framework dapat menjawab secara aman dengan:
4Namun string tidak valid seperti:
fulkemungkinan besar merupakan typo.
Fallback diam-diam ke 1 akan membuat Widget tampil seperempat lebar tanpa memberi tahu mengapa.
PandaPanel\Widgets\Support\WidgetFilters
WidgetFilters merupakan:
final readonlyObject ini dibuat oleh:
Page::resolveFilters()Sebuah Widget hanya menerima bagian filter miliknya melalui:
withFilters()API:
public static function none():
self;
/**
* @param array<string, FormSchema>
* $widgetSchemas
* keyed by widget id
*/
public static function fromRequest(
Request $request,
?FormSchema $dashboardSchema = null,
array $widgetSchemas = [],
?string $sessionKey = null,
): self;
/**
* @return array<string, mixed>
*/
public function for(
string $widgetId
): array;
/**
* @return array<string, mixed>
*/
public function dashboard():
array;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Contoh:
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Widgets\Support\WidgetFilters;
$filters =
WidgetFilters::fromRequest(
request(),
// ?filters[months]=6
// &widgets[user-growth][months]=24
FormSchema::make()
->schema([
Select::make('months')
->options([
'6' => '6',
])
->default('6'),
]),
[
'user-growth' =>
FormSchema::make()
->schema([
Select::make(
'months'
)
->options([
'24' =>
'24',
]),
]),
],
'panel.admin.page.dashboard',
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Dashboard filter:
$filters->dashboard();
// ['months' => '6']2
3
Widget-specific filter:
$filters->for(
'user-growth'
);
// ['months' => '24']2
3
4
5
Widget yang tidak memiliki override:
$filters->for(
'recent-users'
);
// ['months' => '6']2
3
4
5
Widget-specific filter memiliki prioritas terhadap Page filter.
Rules yang digunakan resolver:
| Situasi | Hasil |
|---|---|
| Key tidak dideklarasikan schema | Dibuang |
| Parameter group tidak ada, session memiliki value | Gunakan stored value |
| Parameter group tidak ada, session kosong | Gunakan field default |
| Parameter group ada, tetapi field tidak dikirim | null — filter yang sudah dibersihkan tetap kosong |
PandaPanel\Widgets\PageContext
PageContext merupakan:
finalObject ini menentukan informasi Page apa yang boleh diketahui Widget pada Resource Page.
API:
public static function forRecord(
Model $record
): self;
/**
* @param Closure():
* Builder<covariant Model> $query
*/
public static function forQuery(
Closure $query
): self;
public function record():
?Model;
/**
* @return Builder<covariant Model>|null
*/
public function query():
?Builder;
public function count():
int;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Contoh:
use PandaPanel\Widgets\PageContext;
PageContext::forRecord(
$order
);
PageContext::forQuery(
static fn () =>
OrderResource::query()
);2
3
4
5
6
7
8
9
10
count() di-memoize.
Jika tidak ada query:
0dikembalikan.
Artinya Page dengan empat Widget yang tidak pernah memanggil:
context()->count()tidak menjalankan query count tambahan.
Jika tiga Widget memanggilnya, query count tetap hanya berjalan sekali.
Context per Page:
| Page | Context yang diberikan |
|---|---|
ListRecords | forQuery(), sudah di-scope ke active Tab |
ViewRecord, EditRecord | forRecord() |
ManageRelatedRecords | forRecord() terhadap owner record |
CreateRecord | Tidak ada |
Page, Dashboard | Tidak ada |
PandaPanel\Pages\WidgetCollection
WidgetCollection merupakan:
final readonlyObject ini me-resolve daftar Widget class sebuah Page menjadi props.
Authorization dilakukan:
lebih dulu
dan
sekali2
3
API:
/**
* @param list<class-string<Widget>> $classes
*/
public static function for(
array $classes,
?PageContext $context = null,
?WidgetFilters $filters = null,
): self;
public function merge(
self $other
): self;
/**
* @param list<class-string<Widget>> $classes
*
* @return array<string, FormSchema>
*/
public static function filterSchemas(
array $classes
): array;
/**
* @return list<
* array<string, mixed>
* >
*/
public function definitions():
array;
public function deferred():
mixed;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Contoh:
use PandaPanel\Pages\WidgetCollection;
$collection =
WidgetCollection::for([
UserStats::class,
RecentUsers::class,
]);
$collection
->definitions();
// [
// ['id' => 'recent-users', ...],
// ['id' => 'user-stats', ...],
// ]2
3
4
5
6
7
8
9
10
11
12
13
14
15
Jika tidak ada Widget lazy:
$collection->deferred();
// null2
3
deferred() menghasilkan:
Inertia::defer()dengan shape:
{
widgetId: data
}2
3
untuk Widget lazy.
Jika tidak ada lazy Widget:
nulldikembalikan sehingga Page tidak mengiklankan second request yang sebenarnya tidak diperlukan.
PandaPanel\Core\WidgetRegistry
WidgetRegistry merupakan:
finalSatu registry dibuat per Panel dan di-key berdasarkan Widget id.
API:
/**
* @param class-string<WidgetContract> $widget
*/
public function register(
string $widget
): void;
public function has(
string $id
): bool;
/**
* @return class-string<WidgetContract>|null
*/
public function byId(
string $id
): ?string;
/**
* @return list<
* class-string<WidgetContract>
* >
*/
public function all():
array;
public function count():
int;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Contoh:
use PandaPanel\Core\PanelManager;
$registry =
app(
PanelManager::class
)
->widgets(
panel('admin')
);2
3
4
5
6
7
8
9
Seluruh Widget:
$registry->all();Class names sudah diurutkan.
Lookup:
$registry->byId(
'user-stats'
);
// 'App\Panels\Admin\Widgets\UserStats'2
3
4
5
Check:
$registry->has(
'nope'
);
// false2
3
4
5
Jika dua Widget class pada Panel yang sama memiliki id sama:
PanelRegistrationException::duplicateWidgetId()dilempar.
Karena id berasal dari basename class, maka:
Admin\Widgets\UserStatsdan:
Reports\UserStatspada Panel yang sama akan bertabrakan.
Registration dan placement
Pada Panel
API:
/**
* @param list<class-string> $widgets
*/
public function widgets(
array $widgets
): self;
public function discoverWidgets(
string ...$paths
): self;
/**
* @return list<class-string>
*/
public function getWidgets():
array;
/**
* @return list<string>
*/
public function getWidgetDiscoveryPaths():
array;
/**
* @param class-string<Page> $page
*/
public function dashboard(
string $page
): self;
/**
* @param array<
* array-key,
* class-string<Page>
* > $pages
*/
public function dashboards(
array $pages
): self;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Contoh:
use App\Panels\Admin\Pages\AccountsDashboard;
use App\Panels\Admin\Widgets\RevenueChart;
use PandaPanel\Core\Panel;
use PandaPanel\Pages\Dashboard;
return $panel
->widgets([
RevenueChart::class,
])
->discoverWidgets(
app_path(
'Panels/Admin/Widgets'
)
)
->dashboards([
Dashboard::class,
AccountsDashboard::class,
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Registration eksplisit digabungkan dengan discovery.
Jika class yang sama muncul pada keduanya:
hanya satu kalidashboards() menjadikan entry pertama sebagai root Panel.
Entry sisanya diregistrasikan sebagai ordinary Pages.
Contoh:
[
Dashboard::class,
AccountsDashboard::class,
]2
3
4
berarti:
Dashboard
→ Panel root
AccountsDashboard
→ own route
→ own navigation item
→ own filters2
3
4
5
6
7
Jika dashboards() menerima empty array:
[]Dashboard yang sudah ada tidak diubah.
Pada Page
API:
/**
* @return list<class-string<Widget>>
*/
public function widgets():
array;
public function filterSchema():
?FormSchema;
protected function filterSessionKey():
string;
protected function resolveFilters():
WidgetFilters;
protected function resolveWidgets(
?WidgetFilters $filters = null
): WidgetCollection;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PandaPanel\Pages\Dashboard::widgets() mengoverride default dan mengembalikan seluruh Widget di Panel registry.
filterSessionKey():
panel.{panelId}.page.{slug}Contohnya:
panel.admin.page.dashboardDengan demikian dua Dashboard menyimpan filter masing-masing secara terpisah.
Contoh custom Page:
use PandaPanel\Pages\Page;
use PandaPanel\Widgets\Widget;
final class Reports extends Page
{
/**
* @return list<
* class-string<Widget>
* >
*/
public function widgets():
array
{
return [
RevenueChart::class,
TopProducts::class,
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Sebuah Page merender props:
widgets
widgetData
filters2
3
Pada Resource Page
API pada Resource:
public static function getWidgets():
array;
public static function getHeaderWidgets(
string $page
): array;
public static function getFooterWidgets(
string $page
): array;2
3
4
5
6
7
8
9
10
API pada Page instance:
/**
* @return list<class-string<Widget>>
*/
public function headerWidgets():
array;
/**
* @return list<class-string<Widget>>
*/
public function footerWidgets():
array;
/**
* @return array<string, mixed>
*/
protected function widgetProps(
?PageContext $context = null
): array;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Contoh:
use PandaPanel\Resources\Pages\ListRecords;
use PandaPanel\Resources\Resource;
use PandaPanel\Widgets\Widget;
final class OrderResource extends Resource
{
/**
* @return list<
* class-string<Widget>
* >
*/
public static function getWidgets():
array
{
return [
OrderStats::class,
];
}
}
final class ListOrders extends ListRecords
{
/**
* @return list<
* class-string<Widget>
* >
*/
public function headerWidgets():
array
{
return [
...parent::headerWidgets(),
RevenueChart::class,
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
widgetProps() mengirim:
headerWidgets
footerWidgets
widgetData2
3
widgetData merupakan satu shared deferred prop yang mencakup header dan footer Widget.
Filter Resource Page Widget di-resolve dari:
widgets[{id}]dan disimpan per:
Panel
Resource
Page
Record Key2
3
4
Discovery
API:
/**
* @return list<
* class-string<WidgetContract>
* >
*/
public function widgets(
Panel $panel
): array;2
3
4
5
6
7
8
Contoh:
use PandaPanel\Discovery\PanelDiscoverer;
app(
PanelDiscoverer::class
)
->widgets(
panel('admin')
);2
3
4
5
6
7
8
Discovery men-scan:
getWidgetDiscoveryPaths()untuk class yang mengimplementasikan:
WidgetContractYang dilewati:
- abstract base class;
Support/value objects;- enums;
karena mereka tidak mengimplementasikan contract secara concrete.
Enums
enum PandaPanel\Widgets\Enums\WidgetType:
string
{
case Stats =
'stats';
case Table =
'table';
case Chart =
'chart';
case Custom =
'custom';
}
enum PandaPanel\Widgets\Enums\StatColor:
string
{
case Default =
'default';
case Success =
'success';
case Warning =
'warning';
case Danger =
'danger';
case Info =
'info';
}
enum PandaPanel\Widgets\Enums\ChartVariant:
string
{
case Bar =
'bar';
case Line =
'line';
case Area =
'area';
case Doughnut =
'doughnut';
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Ketiganya merupakan closed set karena frontend memetakan masing-masing case ke:
- literal class;
- atau renderer yang sudah di-compile.
Arbitrary color name dapat menghasilkan CSS class yang tidak pernah masuk bundle.
PandaPanel\Contracts\WidgetContract
Contract:
interface WidgetContract
{
public static function id():
string;
public static function type():
WidgetType;
public static function canView():
bool;
/**
* @return array<string, mixed>
*/
public function toArray():
array;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Contract ini diimplementasikan oleh:
PandaPanel\Widgets\WidgetDiscovery dan registry melakukan type-hint terhadap contract ini.
Untuk penggunaan normal, extend salah satu base Widget daripada mengimplementasikan contract langsung.
Artisan
Command:
php artisan make:panel-widget {name} --panel=Admin [--type=stats] [--force]Options:
| Option | Value | Default |
|---|---|---|
--panel | Nama directory Panel | Wajib |
--type | stats, table, chart, custom | stats |
--force | Overwrite file yang sudah ada | Off |
Contoh:
php artisan make:panel-widget RecentOrders --panel=Admin --type=tableCustom:
php artisan make:panel-widget ServerHealth --panel=Admin --type=customClass dibuat pada:
app/Panels/{Panel}/Widgets/{Name}.phpUntuk:
--type=custommatching Vue component juga dibuat pada:
resources/js/pages/Panels/{Panel}/Widgets/{Name}.vueProperty:
$componentotomatis diisi:
Panels/{Panel}/Widgets/{Name}Custom Widget tanpa component akan merender fallback.
Exceptions
| Exception | Dilempar ketika |
|---|---|
PanelSchemaException::unusableColumnSpan() | $columnSpan berupa string yang bukan numeric dan bukan 'full' |
PanelSchemaException::unknownBreakpoints() | $columnSpan menggunakan breakpoint di luar default, md, lg, xl |
PanelRegistrationException::duplicateWidgetId() | Dua Widget class pada satu Panel memiliki id sama |
RuntimeException | CustomWidget::component() dipanggil ketika $component kosong |
LogicException | Widget::context() dipanggil pada Widget yang dirender tanpa Page Context |
Gotchas
canView()dijalankan sebelum construction.WidgetCollection::for()melewati class sepenuhnya jika authorization gagal. Karena itudata()tidak pernah berjalan dan tidak ada query yang dieksekusi. Menyembunyikan Widget hanya di frontend tetap akan membayar biaya query.widgetDatatidak bernilainullpada response pertama — key-nya belum ada.
Deferred prop belum tersedia sampai follow-up request selesai. Vue component yang membaca prop tersebut harus membuatnya optional, atau Vue dapat memberi warning karena required prop belum tersedia pada first paint.Filter dan Page Context tidak digunakan bersama.
Dashboard atau standalone Page memberikan filter kepada Widget tetapi tidak memberikan context. Resource Page memberikan context tetapi tidak memberikan filter.context()pada Dashboard Widget akan throw, sedangkanfilter()pada Resource Page Widget mengembalikan default.Widget id berasal dari basename class.
Jika class:textUserGrowth1diganti nama, URL tersimpan seperti:
text?widgets[user-growth][...]1tidak lagi cocok. Filter yang tersimpan di session di bawah id lama juga menjadi orphaned state.
Table Widget tidak memiliki page-size control.
data()mengunci:phpperPageOptions([ $perPage, ])1
2
3Jadi jika ingin jumlah row lebih banyak, ubah:
php$perPage1Polling melakukan reload props milik Page, bukan hanya satu Widget.
Data Widget merupakan prop dari Page yang menampungnya. Karena itu$pollingIntervalmenyebabkan partial reload terhadap Page pada setiap interval untuk setiap browser tab yang sedang terbuka.Definition Lazy Widget tetap dikirim langsung.
Yang ditunda hanya:textdata1Sedangkan berikut tetap tersedia pada first paint:
textheading columnSpan filters polling1
2
3
4Stat::display()membiarkan string apa adanya.format()hanya diterapkan pada:textint float1
2Jika Widget sudah membuat formatted string sendiri, framework menghormati value tersebut.
Column span class ditulis lengkap di frontend.
Dynamic interpolation seperti:textmd:col-span-${n}1belum tentu tersedia di compiled bundle.
Karena itu vocabulary span bersifat closed dan nilai numeric di luar range di-clamp.