Referensi Tables
PandaPanel\Tables\TableSchema dan seluruh komponen yang berada di dalamnya mencakup:
- columns;
- filters;
- groups;
- tabs;
- summaries;
- actions;
- selection;
- editable cells;
- serta
TableQueryyang mengubah state URL menjadi query Eloquent yang sudah dibatasi.
Pada PandaBear, URL merupakan state dari Table.
State tersebut mencakup:
page
per-page
search
sort
direction
filters
columns
group2
3
4
5
6
7
8
Dengan pendekatan ini:
- tombol Back bekerja;
- tombol Forward bekerja;
- refresh mempertahankan Table;
- bookmark mempertahankan filter;
- URL Table dapat dibagikan kepada user lain.
Namespaces
| Class | Kegunaan |
|---|---|
PandaPanel\Tables\TableSchema | Declarative description dari sebuah Table |
PandaPanel\Tables\TableQuery | Menerapkan URL state ke Eloquent query |
PandaPanel\Tables\ArrayTableData | Table Schema yang sama untuk record non-database |
PandaPanel\Tables\Group | Mengelompokkan row menjadi beberapa band |
PandaPanel\Tables\Tab | Filter tab di atas List Page |
PandaPanel\Tables\Columns\Column | Base class seluruh Column |
PandaPanel\Tables\Columns\EditableColumn | Base class seluruh writable Column |
PandaPanel\Tables\Columns\Concerns\HasRelationshipState | Aggregate dan relation sorting untuk seluruh Column |
PandaPanel\Tables\Filters\Filter | Base class seluruh Filter |
PandaPanel\Tables\Filters\Constraints\Constraint | Satu Column yang dapat dibatasi Query Builder Filter |
PandaPanel\Tables\Summaries\Summarizer | Satu nilai summary di bawah Column |
PandaPanel\Tables\Enums\* | Closed set enum yang dikirim ke Vue |
Contoh Table yang Berfungsi
<?php
namespace App\Panels\Admin\Resources\Users\Tables;
use PandaPanel\Actions\DeleteAction;
use PandaPanel\Actions\EditAction;
use PandaPanel\Tables\Columns\BooleanColumn;
use PandaPanel\Tables\Columns\DateColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\SortDirection;
use PandaPanel\Tables\Filters\TernaryFilter;
use PandaPanel\Tables\TableSchema;
final class UsersTable
{
public static function configure(
TableSchema $table
): TableSchema {
return $table
->columns([
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('email')
->searchable(),
BooleanColumn::make('is_admin')
->label('Admin'),
DateColumn::make('created_at')
->label('Joined')
->sortable(),
])
->filters([
TernaryFilter::make('is_admin')
->label('Administrator'),
])
->defaultSort(
'created_at',
SortDirection::Descending
)
->recordActions([
EditAction::make(
UserResource::class
),
DeleteAction::make(
UserResource::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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Gunakan dari Resource:
public static function table(
TableSchema $table
): TableSchema {
return UsersTable::configure(
$table
);
}2
3
4
5
6
7
TableSchema
TableSchema merupakan:
final class TableSchemaSeluruh builder method mengembalikan:
selfContent
API:
public static function make(): self;
public function columns(
array $columns
): self;
// array<array-key, Column>
public function filters(
array $filters
): self;
// array<array-key, Filter>
public function groups(
array $groups
): self;
// array<array-key, Group>
public function defaultGroup(
string $name
): self;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Duplicate Column dan Filter
columns() dan filters() menolak duplicate name melalui:
PandaPanel\Exceptions\PanelSchemaExceptionHal ini penting karena nama Column menjadi key untuk:
- cell;
- visibility state;
- search;
- sort.
Sedangkan nama Filter menjadi key pada query string.
Contohnya:
filters[status]=liveJika dua Filter menggunakan nama:
statustidak ada jawaban yang jelas Filter mana yang seharusnya menerima value tersebut.
Karena itu duplicate ditolak pada schema.
Sorting, Searching, dan Paging
API:
public function defaultSort(
string $column,
SortDirection $direction =
SortDirection::Descending
): self;
public function defaultSortOptionLabel(
string $label
): self;
// null
public function perPageOptions(
array $options
): self;
// [10, 25, 50, 100]
public function defaultPerPage(
int $perPage
): self;
// 25
public function searchPlaceholder(
string $placeholder
): self;
// 'Search...'
public function searchDebounce(
int $milliseconds
): self;
// 300
public function searchOnBlur(
bool $onBlur = true
): self;
// false
public function splitSearchTerms(
bool $split = true
): self;
// true2
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
splitSearchTerms()
Default:
trueArtinya setiap kata dalam search harus menemukan match di suatu tempat.
Contoh query:
ada lovelaceJika Table memiliki:
first_name
last_name2
maka pencarian dapat diperlakukan sebagai:
ada
AND
lovelace2
3
sehingga:
first_name = Ada
last_name = Lovelace2
dapat ditemukan.
Jika seluruh input diperlakukan sebagai satu string:
"ada lovelace"tidak ada satu Column pun yang memiliki exact sequence tersebut.
Unknown Default Sort
Jika:
->defaultSort(
'created_at'
)2
3
digunakan tetapi Table tidak memiliki Column:
created_atframework melempar:
PanelSchemaException::unknownDefaultSort()Pemeriksaan dilakukan ketika:
toArray()bukan langsung dari setter.
Alasannya, valid configuration dapat menulis:
->defaultSort(...)sebelum:
->columns(...)Jika tidak diperiksa, Table akan silently menggunakan natural query order tanpa menjelaskan mengapa konfigurasi sort tidak bekerja.
Persistence dan Deferral
API:
public function persistSearchInSession(
bool $persist = true
): self;
// false
public function persistSortInSession(
bool $persist = true
): self;
// false
public function persistFiltersInSession(
bool $persist = true
): self;
// false
public function persistColumnsInSession(
bool $persist = true
): self;
// false
public function deferFilters(
bool $defer = true
): self;
// false
public function deferColumnManager(
bool $defer = true
): self;
// false2
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
Request Selalu Mengalahkan Session
Jika parameter request ada, request memiliki prioritas.
Bahkan ketika parameter tersebut sengaja dikosongkan.
Contohnya session menyimpan:
search = alankemudian user clear search sehingga URL membawa:
?search=request kosong tersebut harus tetap menang.
Jika tidak, Table akan membaca session kembali dan search yang baru saja dihapus muncul lagi.
Karena itu fallback ke session hanya terjadi jika parameter request tidak ada sama sekali.
Filter Bar dan Column Manager
API:
public function filtersTrigger(
string $label,
?string $icon = null
): self;
// 'Filters', null
public function filtersApplyLabel(
string $label
): self;
// 'Apply filters'
public function filtersResetLabel(
string $label
): self;
// 'Clear'
public function showFiltersResetAction(
bool $show = true
): self;
// true
public function reorderableColumns(
bool $reorderable = true
): self;
// false
public function columnManagerTrigger(
string $label,
?string $icon = null
): self;
// 'Columns', null
public function columnManagerInModal(
bool $inModal = true
): self;
// false
public function showColumnManagerReset(
bool $show = true
): self;
// true2
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
Label reset milik Column Manager selalu:
ResetYang dapat dikonfigurasi hanya apakah Action tersebut:
ditampilkan
atau
disembunyikan2
3
Rows, Selection, dan Actions
API:
public function selectable(
bool $selectable = true
): self;
// false
public function reorderable(
string $column
): self;
// juga melakukan
// defaultSort($column, Ascending)
public function recordActions(
array $actions
): self;
public function recordActionsPosition(
RecordActionsPosition $position
): self;
// AfterColumns
public function recordActionsLabel(
string $label
): self;
// null
public function bulkActions(
array $actions
): self;
// otomatis selectable(true)
// jika tidak kosong
public function headerActions(
array $actions
): self;
public function toolbarActions(
array $actions
): self;
public function emptyStateActions(
array $actions
): self;
public function frozenActions(
bool $frozen = true
): self;
// false2
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
Action Validation
Setiap Action setter memeriksa:
- duplicate Action names;
- inert Action.
Duplicate menghasilkan:
PanelSchemaException::duplicateActions()Sedangkan Action yang tidak memiliki:
- URL;
- handler;
- Form;
- Modal;
menghasilkan:
PanelSchemaException::inertAction()Framework tidak membiarkan button dirender jika ketika diklik tidak dapat melakukan apa pun.
bulkActions() Mengaktifkan Selection
Jika:
->bulkActions([
// ...
])2
3
tidak kosong, Table otomatis menjadi:
selectable(true)Karena Bulk Action tanpa row selection tidak dapat digunakan.
Reorderable Table
$table->reorderable(
'position'
);2
3
selain mengaktifkan drag reorder juga secara otomatis menggunakan:
defaultSort(
'position',
SortDirection::Ascending
)2
3
4
Table reorderable yang disortir berdasarkan Column lain akan menampilkan urutan yang tidak sesuai dengan drag handle.
Empty State
API:
public function emptyState(
string $heading,
?string $description = null,
?string $icon = null
): self;
public function emptyStateComponent(
string $component
): self;
// null2
3
4
5
6
7
8
9
10
Default heading:
No records foundMembaca Schema
API:
public function getColumns(): array;
// list<Column>
public function getColumn(
string $name
): ?Column;
public function columnNames(): array;
public function defaultVisibleColumnNames():
array;
public function toggleableColumnNames():
array;
public function getSortableColumns():
array;
public function getSearchColumns():
array;
// local columns only
public function getSearchRelations():
array;
// dotted paths
public function isSearchable():
bool;
public function getIndividuallySearchableColumns():
array;
public function getIndividuallySearchableColumn(
string $name
): ?Column;
public function getFilters():
array;
public function getFilter(
string $name
): ?Filter;
public function defaultFilters():
array;
public function persistsFiltersInSession():
bool;
public function defersFilters():
bool;
public function persistsSearchInSession():
bool;
public function persistsSortInSession():
bool;
public function persistsColumnsInSession():
bool;
public function hasReorderableColumns():
bool;
public function shouldSplitSearchTerms():
bool;
public function getGroups():
array;
public function getGroup(
string $name
): ?Group;
public function getDefaultGroup():
?string;
public function getDefaultSortColumn():
?string;
public function getDefaultSortDirection():
SortDirection;
public function getPerPageOptions():
array;
public function getDefaultPerPage():
int;
public function isSelectable():
bool;
public function isReorderable():
bool;
public function getReorderColumn():
?string;
public function hasFrozenActions():
bool;
public function hasFrozenStart():
bool;
public function getRecordActions():
array;
public function getBulkActions():
array;
public function getHeaderActions():
array;
public function getToolbarActions():
array;
public function getEmptyStateActions():
array;
public function getRecordAction(
string $name
): ?Action;
public function getBulkAction(
string $name
): ?Action;
public function getTableAction(
string $name
): ?Action;
public function hasSummaries():
bool;
public function applyColumnQueries(
Builder $query
): void;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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
Action Lookup adalah Whitelist
Method:
getRecordAction()
getBulkAction()
getTableAction()2
3
merupakan whitelist yang digunakan Action endpoint.
Jika request mengirim:
action = deletetetapi delete tidak ditemukan dalam schema sesuai scope, Action tersebut dianggap:
tidak adaApa pun string yang dikirim client tidak dapat membuat Action baru.
Menghasilkan Payload
API:
public function toArray(): array;
// Table definition
public function toRow(
Model $record,
?Group $group = null
): array;
// one row
public function summaries(
Builder $query,
array $records
): array;
public function groupSummaries(
Builder $query,
array $records,
Group $group
): array;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
toRow()
Satu row memiliki bentuk:
key
group
cells
cellMeta
actions2
3
4
5
Secara konsep:
[
'key' => ...,
'group' => ...,
'cells' => [...],
'cellMeta' => [...],
'actions' => [...],
]2
3
4
5
6
7
Cell metadata dipisahkan dari value.
Dengan begitu sebuah cell value dapat mempertahankan shape yang tepat sesuai renderer Column-nya.
TableQuery
TableQuery merupakan:
final readonly class TableQueryConstructor:
public function __construct(
TableSchema $schema,
Request $request,
?string $namespace = null,
?string $sessionKey = null,
);2
3
4
5
6
API:
public function namespace():
?string;
public function constrain(
Builder $query
): void;
public function paginate(
Builder $query
): LengthAwarePaginator;
public function paginateRelation(
Relation $relation
): LengthAwarePaginator;
public function activeGroup():
?Group;
public function state(): array;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Contoh TableQuery
use PandaPanel\Tables\TableQuery;
use PandaPanel\Tables\TableSchema;
$schema =
UserResource::table(
TableSchema::make()
);
$query =
new TableQuery(
$schema,
$request,
sessionKey:
'panel.admin.table.users',
);
$records =
$query->paginate(
UserResource::query()
);
$rows =
array_map(
fn ($record) =>
$schema->toRow(
$record
),
$records->items()
);
$state =
$query->state();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
Urutan Constraint
constrain() menerapkan operasi dalam urutan:
- Column queries;
- base-query filters;
- global search;
- per-column searches;
- filters;
- sort.
Column query dapat mencakup:
- aggregates;
- relation sorting requirements.
paginate()
paginate():
- memanggil
constrain(); - melakukan pagination;
- menggunakan:
withQueryString()sehingga state URL tetap dibawa ke link pagination.
TableQuery Tidak Memulai Query
TableQuery tidak melakukan:
Model::query()sendiri.
Sebaliknya ia menerima query yang sudah dibuat Resource.
Contoh:
$query->paginate(
UserResource::query()
);2
3
Dengan begitu scope milik Resource tetap aktif:
- tenancy;
- module;
- permission;
- parent Resource;
- per-Panel query modification.
Query Parameters
Semua value dari query string divalidasi terhadap Table Schema sebelum menyentuh Builder.
Unknown parameter tidak diteruskan begitu saja.
| Parameter | Divalidasi terhadap |
|---|---|
page | max(1, (int) $value) |
perPage | perPageOptions(), jika invalid gunakan default |
search | trim dan maksimal 255 karakter |
columnSearch[{column}] | Column dengan searchable(individually: true) |
sort | Column dengan sortable() |
direction | SortDirection; unknown menjadi ascending |
filters[{name}] | sanitize() milik Filter |
group | Group yang dideklarasikan |
columns | declared Column names; non-toggleable Column selalu dimasukkan kembali ke visible |
Query String adalah User Input
Contoh:
?sort=passwordjika password bukan sortable Column:
diabaikanbukan diteruskan ke:
orderBy(
'password'
)2
3
Hal yang sama berlaku untuk Filter, Group, dan visibility Column.
state()
Shape:
[
'search' =>
?string,
'sort' =>
?string,
'direction' =>
'asc'|'desc',
'perPage' =>
int,
'filters' =>
array<string, mixed>,
'filterIndicators' =>
array<string, string>,
'columnSearches' =>
array<string, string>,
'columns' => [
'visible' =>
list<string>,
'order' =>
list<string>,
],
'group' =>
?string,
]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
State ini menjadi representasi backend dari kondisi Table saat ini.
Namespaces
Namespace memindahkan seluruh state Table ke satu nested query-string key.
Contoh:
new TableQuery(
$schema,
$request,
namespace:
'relations.tasks'
);2
3
4
5
6
URL:
?relations[tasks][sort]=title
&relations[tasks][page]=22
Mengapa Namespace Diperlukan
Sebuah View/Edit Record dapat memiliki beberapa relation table sekaligus.
Contoh:
User
├── Orders Table
├── Tasks Table
└── Sessions Table2
3
4
Jika semuanya memakai:
?page=2maka mengubah page Tasks juga mengubah Orders dan Sessions.
Dengan namespace:
relations[orders][page]
relations[tasks][page]
relations[sessions][page]2
3
masing-masing Table memiliki state sendiri.
Column
Base class:
abstract class ColumnSemua setter mengembalikan:
staticIdentity dan Behaviour
API:
public static function make(
string $name
): static;
public function label(
string $label
): static;
// Str::headline($name)
public function sortable(
bool $sortable = true,
?string $column = null
): static;
// false
public function searchable(
bool $searchable = true,
?array $columns = null,
bool $individually = false
): static;
public function sortUsing(
Closure $callback
): static;
public function visible(
bool $visible = true
): static;
// true
public function toggleable(
bool $toggleable = true
): static;
// true2
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
searchable()
Secara sederhana:
TextColumn::make('name')
->searchable();2
mencari Column tersebut.
Tetapi Anda juga dapat menentukan Column lain:
TextColumn::make('full_name')
->searchable(
columns: [
'first_name',
'last_name',
]
);2
3
4
5
6
7
Dotted path berarti relation.
Contoh:
->searchable(
columns: [
'author.name',
]
)2
3
4
5
Jika:
individually: trueColumn juga memiliki search input sendiri.
Presentation
API:
public function alignment(
Alignment|string $alignment
): static;
// Start
public function headerAlignment(
Alignment|string $alignment
): static;
// mengikuti alignment
public function placeholder(
string $placeholder
): static;
// null
public function default(
mixed $default
): static;
// null
public function tooltip(
Closure|string $tooltip
): static;
public function headerTooltip(
string $tooltip
): static;
public function wrapHeader(
bool $wrap = true
): static;
// false
public function width(
string $width
): static;
// null
public function frozen(
ColumnPin|bool $pin = true
): static;
// null
public function extraAttributes(
Closure|array $attributes
): static;
public function formatUsing(
Closure $callback
): static;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
width()
width() menerima CSS length.
Contoh:
->width('12rem')atau:
->width('180px')Value ini menjadi inline style, bukan Tailwind runtime class.
Frozen Columns
->frozen()atau:
->frozen(true)mem-pin Column ke:
StartUntuk sisi lain:
->frozen(
ColumnPin::End
)2
3
Links, Actions, dan Summaries
API:
public function url(
Closure $callback
): static;
public function action(
Action $action
): static;
public function getAction():
?Action;
public function summarize(
array $summarizers
): static;
public function getSummarizers():
array;
public function hasSummaries():
bool;
public function summaryColumn():
string;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Reading Column
API:
public function getName():
string;
public function getLabel():
string;
public function isSortable():
bool;
public function isVisible():
bool;
public function isToggleable():
bool;
public function isSearchable():
bool;
public function isIndividuallySearchable():
bool;
public function hasCustomSort():
bool;
public function getSortColumn():
string;
public function getSearchColumns():
array;
public function getFrozen():
?ColumnPin;
public function resolveValue(
Model $record
): mixed;
public function toCellMeta(
Model $record
): ?array;
public function applyCustomSort(
Builder $query,
SortDirection $direction
): void;
public function toArray():
array;
abstract public function type():
ColumnType;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
Column Types
| Class | ColumnType | Method Khusus | Default Penting |
|---|---|---|---|
TextColumn | Text | limit(int), wrap(bool = true) | — |
NumberColumn | Number | decimals(int), prefix(string), suffix(string) | decimals 0, alignment End |
BadgeColumn | Badge | colors(array), labels(array) | — |
BooleanColumn | Boolean | labels(string $true, string $false) | Yes / No, alignment Center |
DateColumn | Date | format(string), relative(bool = true) | 'M j, Y' |
DateTimeColumn | DateTime | extends DateColumn | 'M j, Y H:i' |
ImageColumn | Image | circular(bool = true), size(int), fallbackUsing(Closure) | 32 px, square |
IconColumn | Icon | icons(array), colors(array), iconUsing(Closure), boolean(string $true = 'check', string $false = 'x') | alignment Center |
ColorColumn | Color | copyable(bool = true) | false |
CustomColumn | Custom | component(string), state(Closure) | — |
TextInputColumn | TextInput | numeric(), maxLength(int) | writable |
SelectColumn | Select | options(array) | writable |
ToggleColumn | Toggle | — | writable, alignment Center |
CheckboxColumn | Checkbox | — | writable, alignment Center |
Contoh Column
use PandaPanel\Tables\Enums\BadgeColor;
BadgeColumn::make('status')
->colors([
'live' =>
BadgeColor::Success,
'draft' =>
BadgeColor::Neutral,
])
->labels([
'live' =>
'Live',
'draft' =>
'Draft',
]);
DateColumn::make('created_at')
->format('Y-m-d')
->relative();
IconColumn::make('is_admin')
->boolean(
'shield',
'x'
);
CustomColumn::make(
'account_age'
)
->component(
'Panels/Admin/Columns/AccountAge'
)
->state(
fn (
Model $record
): int =>
$record
->created_at
->diffInDays()
);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
Custom Column Component
CustomColumn::component() menerima build-time registry key.
Contoh:
Panels/Admin/Columns/AccountAgemerupakan path di bawah:
resources/js/pages/tanpa extension.
Executable Closure tetap dijalankan di server melalui:
state()Frontend hanya menerima hasil state.
EditableColumn
EditableColumn adalah base class untuk empat Column writable:
TextInputColumn
SelectColumn
ToggleColumn
CheckboxColumn2
3
4
Write cell dikirim ke:
panel.{id}.actions.cellCell write diperlakukan seperti Action dalam hal yang penting:
- memiliki record;
- melakukan authorization;
- melakukan validation;
- mengubah data.
EditableColumn API
public function rules(
array $rules
): static;
public function disabledUsing(
Closure $callback
): static;
public function mutateUsing(
Closure $callback
): static;
public function updateUsing(
Closure $callback
): static;
public function writeTo(
string $attribute
): static;
public function getWriteAttribute():
string;
public function isDisabledFor(
Model $record
): bool;
public function validationRules():
array;
public function write(
Model $record,
mixed $value
): void;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
Editable Column Examples
Disabled berdasarkan record:
ToggleColumn::make(
'is_published'
)
->disabledUsing(
fn (
Model $record
): bool =>
$record
->archived_at
!== null
);2
3
4
5
6
7
8
9
10
11
Menulis ke attribute berbeda:
TextInputColumn::make('sku')
->rules([
'string',
'max:32',
])
->writeTo(
'stock_keeping_unit'
);2
3
4
5
6
7
8
Custom update behavior:
SelectColumn::make('status')
->options([
'draft' =>
'Draft',
'live' =>
'Live',
])
->updateUsing(
function (
mixed $value,
Model $record
): void {
app(
PublishPost::class
)->run(
$record,
$value
);
}
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HasRelationshipState
Trait:
PandaPanel\Tables\Columns\Concerns\HasRelationshipStatedigunakan oleh:
ColumnKarena itu seluruh Column memiliki kemampuan relation aggregate dan relation sorting.
API:
public function counts(
string $relation
): static;
public function exists(
string $relation
): static;
public function sum(
string $relation,
string $column
): static;
public function avg(
string $relation,
string $column
): static;
public function min(
string $relation,
string $column
): static;
public function max(
string $relation,
string $column
): static;
public function sortableByRelation(
string $relation,
string $column
): static;
public function getAggregateRelation():
?string;
public function getSortRelation():
?string;
public function aggregateAttribute():
?string;
public function applyQuery(
Builder $query
): void;
public function applyRelationshipSort(
Builder $query,
SortDirection $direction
): void;
public function summaryUsesAggregate():
bool;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
Aggregate Examples
Count relation:
NumberColumn::make('posts')
->counts('posts')
->sortable();2
3
Sum:
NumberColumn::make('revenue')
->sum(
'orders',
'total'
)
->decimals(2);2
3
4
5
6
Sort berdasarkan relation:
TextColumn::make('author')
->sortableByRelation(
'author',
'name'
);2
3
4
5
Aggregate Tidak Melakukan Query per Row
Aggregate ditambahkan ke base query menggunakan mekanisme Eloquent seperti:
withCount()
withSum()2
Kemudian Column membaca generated attribute.
Artinya:
100 rowstidak menghasilkan:
100 relation aggregate queriesRelationshipAggregate
Pilihan:
RelationshipAggregate::Count
RelationshipAggregate::Exists
RelationshipAggregate::Sum
RelationshipAggregate::Avg
RelationshipAggregate::Min
RelationshipAggregate::Max2
3
4
5
6
7
8
9
10
11
Filters
Base class:
abstract class FilterAPI:
public static function make(
string $name
): static;
public function label(
string $label
): static;
// Str::headline($name)
public function column(
string $column
): static;
// default name
public function query(
Closure $callback
): static;
public function modifyBaseQueryUsing(
Closure $callback
): static;
public function default(
mixed $value
): static;
public function hasDefault():
bool;
public function getDefault():
mixed;
public function apply(
Builder $query,
mixed $value
): void;
public function applyBaseQuery(
Builder $query,
mixed $value
): void;
public function indicator(
mixed $value
): ?string;
public function toArray():
array;
abstract public function type():
FilterType;
abstract public function sanitize(
mixed $value
): 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
sanitize() adalah Whitelist
Query string adalah user input.
Setiap Filter menentukan sendiri value apa yang valid melalui:
sanitize()Jika value tidak dapat digunakan:
nulldikembalikan.
Client tidak dapat memaksa arbitrary value langsung masuk ke Builder.
Filter Types
| Class | FilterType | Method Khusus |
|---|---|---|
SelectFilter | Select | options(array), placeholder(string) |
BooleanFilter | Boolean | labels(string $true, string $false), nullable(bool = true) |
TernaryFilter | Ternary | labels(string $true, string $false, ?string $blank = null), nullable(bool = true), queries(Closure $true, Closure $false) |
DateFilter | Date | — |
TrashedFilter | Select | tiga fixed option: without, with, only |
FormFilter | Form | form(Closure), schema() |
QueryBuilderFilter | QueryBuilder | constraints(array), maxRules(int), constraint(string) |
Default blank label TernaryFilter:
AllDefault label BooleanFilter dan TernaryFilter:
Yes
No2
Filter Examples
use PandaPanel\Tables\Filters\SelectFilter;
use PandaPanel\Tables\Filters\TernaryFilter;
use PandaPanel\Tables\Filters\TrashedFilter;
SelectFilter::make('status')
->options([
'draft' =>
'Draft',
'live' =>
'Live',
])
->placeholder(
'Any status'
)
->default(
'live'
);
TernaryFilter::make(
'verified'
)
->labels(
'Verified',
'Unverified'
)
->queries(
function (
Builder $query
): void {
$query
->whereNotNull(
'email_verified_at'
);
},
function (
Builder $query
): void {
$query
->whereNull(
'email_verified_at'
);
},
);
TrashedFilter::make(
'trashed'
);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
TrashedFilter secara visual menggunakan konsep:
Deleted records
Hidden
Included
Only deleted2
3
4
5
Filter ini membutuhkan Resource yang memang mengaktifkan:
$softDeletesagar record deleted dapat dijangkau.
FormFilter
FormFilter membawa satu FormSchema.
Ini memungkinkan satu Filter terdiri dari beberapa field.
Contoh date range:
use PandaPanel\Forms\Components\DatePicker;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Tables\Filters\FormFilter;
FormFilter::make('created')
->form(
fn (
FormSchema $schema
): FormSchema =>
$schema->schema([
DatePicker::make(
'from'
),
DatePicker::make(
'until'
),
])
)
->query(
function (
Builder $query,
mixed $value
): void {
$query
->when(
$value['from']
?? null,
fn (
$q,
$from
) =>
$q->whereDate(
'created_at',
'>=',
$from
)
)
->when(
$value['until']
?? null,
fn (
$q,
$until
) =>
$q->whereDate(
'created_at',
'<=',
$until
)
);
}
);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
Satu Filter:
createdmemiliki dua input:
from
until2
Query Builder Constraints
QueryBuilderFilter dibangun dari beberapa:
ConstraintSetiap Constraint mendeskripsikan:
- Column;
- operators yang didukung;
- bagaimana value divalidasi;
- bagaimana query diterapkan.
Base:
abstract class Constraint
{
public static function make(
string $name
): static;
public function label(
string $label
): static;
public function column(
string $column
): static;
public function getName():
string;
public function getLabel():
string;
public function getColumn():
string;
public function supports(
ConstraintOperator $operator
): bool;
public function accepts(
ConstraintOperator $operator,
mixed $value
): bool;
public function apply(
Builder $query,
ConstraintOperator $operator,
mixed $value
): void;
public function toArray():
array;
abstract public function inputType():
string;
abstract public function operators():
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Constraint types:
TextConstraint
NumberConstraint
DateConstraint
BooleanConstraint2
3
4
QueryBuilderFilter Example
use PandaPanel\Tables\Filters\Constraints\NumberConstraint;
use PandaPanel\Tables\Filters\Constraints\TextConstraint;
use PandaPanel\Tables\Filters\QueryBuilderFilter;
QueryBuilderFilter::make('query')
->maxRules(5)
->constraints([
TextConstraint::make(
'name'
),
NumberConstraint::make(
'orders_count'
)
->label(
'Orders'
),
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Default:
maxRules = 10ConstraintOperator
Available operators:
ConstraintOperator::Contains
ConstraintOperator::DoesNotContain
ConstraintOperator::StartsWith
ConstraintOperator::EndsWith
ConstraintOperator::EqualTo
ConstraintOperator::NotEqualTo
ConstraintOperator::GreaterThan
ConstraintOperator::GreaterThanOrEqual
ConstraintOperator::LessThan
ConstraintOperator::LessThanOrEqual
ConstraintOperator::IsFilled
ConstraintOperator::IsBlank
ConstraintOperator::IsTrue
ConstraintOperator::IsFalse2
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
Masing-masing menyediakan:
label()
needsValue()2
3
Summaries
Base:
abstract class Summarizer
{
public static function make(
string $name = ''
): static;
public function label(
string $label
): static;
public function formatUsing(
Closure $callback
): static;
public function perPage(
bool $perPage = true
): static;
// false
public function isPerPage():
bool;
public function getName():
string;
public function getLabel():
string;
public function summarize(
QueryBuilder $query,
string $column
): mixed;
public function summarizeRecords(
array $records,
string $column
): mixed;
public function format(
mixed $value
): string;
public function toArray(
mixed $value
): array;
abstract public function aggregate():
?string;
}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
Available summarizers:
Count
Sum
Average
Range2
3
4
Summary Example
use PandaPanel\Tables\Summaries\Average;
use PandaPanel\Tables\Summaries\Range;
use PandaPanel\Tables\Summaries\Sum;
NumberColumn::make('total')
->decimals(2)
->summarize([
Sum::make()
->label(
'Total'
),
Average::make()
->label(
'Average'
)
->formatUsing(
fn (
mixed $value
): string =>
number_format(
(float) $value,
2
)
),
Range::make()
->label(
'Range'
),
]);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
Summary Default Menggunakan Seluruh Filtered Query
Summary dihitung database terhadap filtered query.
Bukan dengan menjumlahkan hanya row pada page saat ini.
Contohnya:
Filtered records:
1.000
Per page:
252
3
4
5
Default Summary:
menghitung 1.000 recordbukan hanya:
25 row yang sedang terlihatJika Summary memang dimaksudkan hanya untuk page sekarang, gunakan:
->perPage()Mengapa Default Bukan Page Total
Jika label:
Totalberubah hanya karena user menekan:
Next Pagemaka angka yang sama memiliki arti berbeda.
Karena itu total default berasal dari keseluruhan filtered result.
Group
API:
public static function make(
string $name
): self;
public function label(
string $label
): self;
public function column(
string $column
): self;
// default name
public function direction(
SortDirection $direction
): self;
// Ascending
public function titleUsing(
Closure $callback
): self;
public function descriptionUsing(
Closure $callback
): self;
public function getName():
string;
public function getLabel():
string;
public function getColumn():
string;
public function keyFor(
Model $record
): string;
public function titleFor(
Model $record
): string;
public function descriptionFor(
Model $record
): ?string;
public function applySort(
Builder $query
): void;
public function toArray():
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
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
Group Example
use PandaPanel\Tables\Group;
$table
->groups([
Group::make('status')
->titleUsing(
fn (
Model $record
): string =>
ucfirst(
(string)
$record->status
)
),
Group::make('created_at')
->label(
'Month'
)
->direction(
SortDirection::Descending
),
])
->defaultGroup(
'status'
);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
Group Metadata Di-resolve di Server
Untuk setiap record server menentukan:
- group key;
- group title;
- group description.
Frontend tidak perlu mengetahui bagaimana:
status = pendingdiubah menjadi:
Pending OrdersLogic tersebut tetap berada di backend.
Tab
Tab merupakan filter tab yang berada di atas List Page.
Tab dideklarasikan melalui:
ListRecords::tabs()API:
public static function make(
string $key,
?string $label = null
): self;
public function query(
Closure $callback
): self;
// Closure(Builder): Builder
public function badge(
string|int|Closure|null $badge
): self;
public function icon(
?string $icon
): self;
public function getLabel():
string;
// Str::headline($key)
public function apply(
Builder $query
): Builder;
public function resolveBadge():
string|int|null;
public function toArray(
bool $active
): array;
public readonly string $key;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
Tab Example
public function tabs(): array
{
return [
'all' =>
Tab::make('all')
->badge(
fn (): int =>
User::query()
->count()
),
'admins' =>
Tab::make(
'admins',
'Administrators'
)
->icon(
'shield'
)
->query(
fn (
Builder $query
): Builder =>
$query->where(
'is_admin',
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
27
28
29
30
Tab Bukan Query Baru
Tab hanya merupakan named scope terhadap query Resource yang sudah ada.
Secara konsep:
Resource::query()
↓
Tenant Scope
↓
Permission Scope
↓
Tab::query()2
3
4
5
6
7
Tab tidak memulai:
User::query()sendiri.
Karena itu tenant dan permission scope tetap aktif.
ArrayTableData
ArrayTableData memungkinkan Table Schema yang sama digunakan terhadap data yang tidak berasal dari database.
Contohnya:
- config file;
- API response;
- computed report;
- in-memory collection.
API:
public static function make(
TableSchema $schema,
iterable $records,
Request $request,
?string $namespace = null
): self;
public function paginate():
LengthAwarePaginator;
public function state():
array;
public function pagination(
LengthAwarePaginator $records
): array;
public function rows(
LengthAwarePaginator $records
): array;
public function sortableColumns():
array;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ArrayTableData Example
use Illuminate\Http\Request;
use PandaPanel\Tables\ArrayTableData;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
$schema =
TableSchema::make()
->columns([
TextColumn::make(
'city'
)
->searchable()
->sortable(),
TextColumn::make(
'temperature'
)
->sortable(),
]);
$data =
ArrayTableData::make(
$schema,
collect(
$readings
)
->map(
fn (
array $row
) =>
Reading::make(
$row
)
),
request(),
);
$records =
$data->paginate();
return Inertia::render(
'Panels/Admin/Pages/Weather',
[
'table' =>
$schema->toArray(),
'state' =>
$data->state(),
'rows' =>
$data->rows(
$records
),
'pagination' =>
$data->pagination(
$records
),
]
);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
59
60
61
62
Search, Sort, dan Paging Dilakukan In-Memory
ArrayTableData tetap menggunakan deklarasi Column dari Table.
Perbedaannya hanya sumber row.
Pada Eloquent Table:
Resource::query()
→ database2
Pada ArrayTableData:
iterable records
→ memory2
Schema, serialization, dan row shape tetap sama.
Enums
| Enum | Cases |
|---|---|
ColumnType | Text, Number, Badge, Boolean, Date, DateTime, Image, Icon, Color, Custom, Toggle, Checkbox, TextInput, Select |
SortDirection | Ascending (asc), Descending (desc); fromRequest(), opposite() |
Alignment | Start, Center, End, Justify; fromRequest() |
BadgeColor | Neutral, Success, Warning, Danger, Info; fromValue() |
ColumnPin | Start, End |
FilterType | Select, Boolean, Date, Ternary, Form, QueryBuilder |
ConstraintOperator | empat belas case; memiliki label() dan needsValue() |
RecordActionsPosition | AfterColumns, BeforeColumns, AfterCells |
RelationshipAggregate | Count, Exists, Sum, Avg, Min, Max; attributeFor(), apply() |
Mengapa BadgeColor dan Alignment Closed Set
Enum seperti:
BadgeColor
Alignment2
dipetakan ke literal Tailwind class.
Compiler harus sudah melihat class tersebut ketika build.
Arbitrary runtime string dapat menghasilkan class yang tidak ada pada compiled CSS.
Jika yang Anda butuhkan adalah arbitrary color value, bukan semantic color, gunakan:
Panel::colors()yang menghasilkan CSS custom property.
Endpoint yang Digunakan Table
| Route Name | Digunakan oleh |
|---|---|
panel.{id}.actions.record | Row Action |
panel.{id}.actions.bulk | Bulk Action terhadap selection |
panel.{id}.actions.table | Header, Toolbar, atau Empty-State Action |
panel.{id}.actions.reorder | Drag reorder yang menulis ke Column dari reorderable() |
panel.{id}.actions.cell | Write dari Editable Column |
panel.{id}.actions.form / actions.submit | Action yang memiliki Form |
Ringkasan Table State
URL dapat terlihat seperti:
/admin/users
?page=2
&perPage=25
&search=alan
&sort=created_at
&direction=desc
&filters[is_admin]=12
3
4
5
6
7
State tersebut dibaca oleh:
TableQuerykemudian divalidasi terhadap:
TableSchemasebelum diterapkan ke Builder.
Ringkasan Query Flow
Resource::query()
↓
TableQuery
↓
Column Queries
↓
Base Query Filters
↓
Global Search
↓
Column Searches
↓
Filters
↓
Sort
↓
Pagination
↓
Rows2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TableQuery tidak menggantikan Resource query.
Ia hanya mempersempit query yang sudah diberikan.
Ringkasan Table Security Boundary
Query String
↓
TableSchema
↓
Validate:
- sort column
- search column
- filters
- group
- visible columns
- per page
↓
Sanitized State
↓
Builder2
3
4
5
6
7
8
9
10
11
12
13
14
15
Client tidak dapat mengirim arbitrary database Column name lalu berharap framework meneruskannya langsung ke query.
Ringkasan Column Lifecycle
Model Record
↓
Column::resolveValue()
↓
formatUsing()
↓
Concrete Column formatting
↓
Cell Value
↓
Cell Metadata
↓
Vue Renderer2
3
4
5
6
7
8
9
10
11
12
13
Ringkasan Editable Column
User mengubah cell
↓
POST actions.cell
↓
Resolve Resource
↓
Resolve Record
↓
Resolve Column dari Schema
↓
EditableColumn?
↓
Disabled untuk record?
↓
Authorize
↓
Validation Rules
↓
mutateUsing()
↓
updateUsing()
atau writeTo()
↓
Persist2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Ringkasan Relationship Aggregate
Column
↓
counts / sum / avg / min / max
↓
applyColumnQueries()
↓
withCount / withSum / ...
↓
Single SQL query
↓
Generated aggregate attribute
↓
Column reads value2
3
4
5
6
7
8
9
10
11
12
13
Tidak ada N+1 aggregate query per row.
Ringkasan Filters
URL
filters[status]=live
↓
Filter::sanitize()
↓
valid?
├── no
│ ↓
│ null / ignored
│
└── yes
↓
Filter::apply()
↓
Builder2
3
4
5
6
7
8
9
10
11
12
13
14
15
Ringkasan FormFilter
FormFilter
├── from
└── until
↓
FormSchema
↓
Nested Filter State
↓
Filter query callback
↓
Builder2
3
4
5
6
7
8
9
10
11
Satu logical Filter dapat memiliki beberapa form input.
Ringkasan QueryBuilderFilter
QueryBuilderFilter
↓
Constraints
├── TextConstraint
├── NumberConstraint
├── DateConstraint
└── BooleanConstraint
↓
Operator
↓
Value validation
↓
Builder2
3
4
5
6
7
8
9
10
11
12
13
Ringkasan Summaries
Default:
Filtered Query
↓
Database Aggregate
↓
Summary2
3
4
5
Dengan:
->perPage()menjadi:
Current Page Records
↓
In-memory/Page Aggregate
↓
Summary2
3
4
5
Ringkasan Groups
Query
↓
Group Sort
↓
Record
↓
Group::keyFor()
↓
Group::titleFor()
↓
Group::descriptionFor()
↓
Frontend Band2
3
4
5
6
7
8
9
10
11
12
13
Ringkasan Tabs
Resource::query()
↓
Current Tab
↓
Tab::apply()
↓
TableQuery
↓
Search / Filter / Sort
↓
Pagination2
3
4
5
6
7
8
9
10
11
Tab tidak mengganti Resource scope.
Ringkasan ArrayTableData
Iterable Records
↓
ArrayTableData
↓
Declared Table Columns
↓
In-memory Search
↓
In-memory Sort
↓
Pagination
↓
Table Rows
↓
Same Vue Renderer2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hal yang Perlu Diperhatikan
URL adalah State
Tidak ada state penting Table yang hanya hidup di local component state.
Hal ini memungkinkan:
filter
sort
search
page2
3
4
direpresentasikan sebagai link yang dapat:
- dibookmark;
- dibagikan;
- direfresh;
- dinavigasi dengan Back/Forward.
Semua Query String Divalidasi terhadap Schema
Query string adalah user input.
Unknown:
sort
filter
group
column2
3
4
tidak diteruskan langsung ke Builder.
Session Persistence Tetap Melalui Validation
State yang disimpan session tidak dianggap lebih terpercaya.
Jika session lama menyimpan:
sort = old_columndan Column tersebut sudah dihapus dari schema, value tetap gagal whitelist dan tidak digunakan.
Table Dapat Berjalan Tanpa Session Middleware
Method:
persist*InSession()menjadi no-op jika session tidak tersedia.
Bukan fatal error.
Dengan begitu schema yang sama dapat dipakai dari context seperti:
- console command;
- non-session environment.
bulkActions() Otomatis Mengaktifkan Selection
Mendeklarasikan Bulk Action tetapi lupa:
selectable()tidak menyebabkan Table unusable.
Schema mengaktifkannya secara otomatis.
Aggregate Ditambahkan ke Query
Method seperti:
counts()
sum()
avg()2
3
menggunakan aggregate query seperti:
withCount()
withSum()2
bukan menjalankan query satu per row.
reorderable() juga Mengatur Default Sort
Jika:
->reorderable('position')digunakan, default sort menjadi:
position ASCAgar visual order sama dengan order yang memang dapat diubah user melalui drag.
Prinsip Utama Tables
PandaBear memperlakukan Table bukan hanya sebagai renderer row, tetapi sebagai server-defined query contract.
TableSchema menentukan:
Column mana yang ada
Column mana yang searchable
Column mana yang sortable
Filter mana yang valid
Group mana yang valid
Action mana yang valid
Column mana yang dapat diedit
State mana yang boleh diterima2
3
4
5
6
7
8
Sedangkan TableQuery menerima user-controlled URL state dan hanya menerapkan bagian yang lolos schema whitelist.
Secara singkat:
URL menyimpan state Table, tetapi Table Schema menentukan bagian mana dari state tersebut yang valid dan boleh memengaruhi query.
Prinsip berikutnya:
TableQuery tidak pernah memulai query sendiri. Ia selalu mempersempit Resource query yang sudah membawa tenant, parent, module, dan permission scope.
Dengan desain ini, fitur:
search
sort
filters
groups
pagination
column manager
actions
editable cells2
3
4
5
6
7
8
tetap berada dalam boundary Resource dan Schema yang sama.