⌘K

Core

Dashboardy

Skládání widgetů na komponentě přes `WithWidgets` a deklarace dashboardu jako vlastníka, kterého najde menu i router.

Na této stránce

Dostat widgety na obrazovku jde dvěma způsoby a je to totéž dělení, jaké dělá resource: napsat si je na komponentu, nebo deklarovat dashboard, který je jmenuje, a nechat ho vykreslit stránkou. To druhé je to, co dá položku do menu a URL do routeru, aniž by se kdokoli z nich dozvěděl, co je widget.

Dashboard layout (WithWidgets)

Použijte trait WithWidgets na Livewire komponentě pro složení widgetového dashboardu.

use NyonCode\WireCore\Widgets\ChartWidget;
use NyonCode\WireCore\Widgets\Concerns\WithWidgets;
use NyonCode\WireCore\Widgets\Contracts\HasWidgets;
use NyonCode\WireCore\Widgets\Stat;
use NyonCode\WireCore\Widgets\StatsOverviewWidget;
use NyonCode\WireTable\Table;
use NyonCode\WireTable\Widgets\TableWidget;

TableWidget jako jediný není z wire-core. Kreslí řádky tabulky uvnitř karty a engine, který je kreslí, žije ve wire-table — takže tam žije i ta třída. Aplikace bez toho balíčku skládá ostatní druhy widgetů přesně jako níž. Viz Tabulky a vlastní pohledy.

Použití

class Dashboard extends Component implements HasWidgets
{
use WithWidgets;
 
protected function getWidgets(): array
{
return [
StatsOverviewWidget::make()
->columns(4)
->stats([
Stat::make('Users', (string) User::count()),
Stat::make('Orders', (string) Order::count()),
Stat::make('Revenue', "$" . number_format(Order::sum('total'), 2)),
Stat::make('Products', (string) Product::count()),
]),
 
ChartWidget::make()
->heading('Monthly Revenue')
->type('line')
->columnSpan(2)
->labels($this->getMonthLabels())
->datasets($this->getRevenueDatasets()),
 
TableWidget::make()
->heading('Recent Orders')
->limit(5)
->table(fn (Table $table): Table => $this->configureRecentOrdersTable($table)),
];
}
 
protected function getWidgetColumns(): int
{
return 2; // 2-sloupcový grid layout
}
}

Blade šablona

Vykreslete dashboard komponentou <x-wire::widget-grid>. getVisibleWidgets() je veřejná a vrací jen widgety, které projdou svými kontrolami viditelnosti a autorizace; každý widget ctí svůj vlastní columnSpan() a polling interval uvnitř gridu:

<div>
<x-wire::widget-grid :widgets="$this->getVisibleWidgets()" :columns="2" />
</div>

Každý widget je také Htmlable, takže můžete komponentu přeskočit a rozložit je sami: @foreach ($this->getVisibleWidgets() as $widget) {{ $widget }} @endforeach.

WithWidgets API

abstract protected function getWidgets(): array // definovat widgety
protected function getWidgetColumns(): int // sloupce gridu (výchozí: 2)
public function getVisibleWidgets(): array // filtrované podle viditelnosti + autorizace
public function refreshWidget(string $key): void // tik pollingu, zodpovězený tím widgetem
public function filterWidget(string $key, string $value): void // výběr filtru
public function callWidgetAction(string $key, string $name): void // akce v hlavičce
public function loadWidget(string $key): void // první render odloženého widgetu
public array $widgetFilters // jaký filtr který widget ukazuje
public array $loadedWidgets // klíče, které už byly staženy
protected function getDashboardFilters(): array // filtry nad celým dashboardem (výchozí: žádné)
public function dashboardFilter(string $name): ?string // hodnota jednoho filtru, pro getWidgets()
public function setDashboardFilter(string $name, string $value): void // co volá lišta filtrů
public function resetDashboardFilters(): void // všechny filtry zpět na výchozí
public array $dashboardFilters // výběr, v adrese jako ?dashboard[…]

Stav drží hostitel, widgety žádný

Widget je obyčejný objekt, který se z getWidgets() staví znovu při každém requestu, takže si přes round trip nemůže pamatovat nic. Hostitel může, a dělá to: zapíše si, jaký filtr který widget ukazuje a které odložené widgety už byly staženy, a obojí do widgetů vrátí v getVisibleWidgets() — až po orazítkování klíčů a ještě před dotazem na viditelnost, aby closure ve visible(fn () => …) mohla přečíst filtr, který uživatel zvolil.

Všechny čtyři metody výše odpovídají markupem jednoho widgetu, stejnou wire:partial oblastí:

Spouštěč Volá Odpoví
refreshWidget() wire:poll na obalu widgetu ten widget, překreslený
filterWidget() wire:change na selectu widgetu ten widget, znovu vyřešený novým klíčem
callWidgetAction() wire:click na tlačítku v hlavičce ten widget, poté co akce doběhla — nebo plný render, když ho akce odstranila
loadWidget() wire:init na obalu odloženého widgetu ten widget, poprvé vykreslený

Klíč, který nic nepojmenuje — widget skrytý od vykreslení stránky, widget bez filtru, widget, který zmizel — nezařadí žádnou oblast a request spadne zpět na plný render, místo aby odpověděl půlkou stránky.

Rozhraní HasWidgets

interface HasWidgets
{
public function getWidgets(): array;
}

Dashboard (deklarovaný, ne komponenta)

WithWidgets výše dává dashboard dovnitř Livewire komponenty, což stačí, dokud ho nepotřebuje něco dalšího: komponenta se nedá zaregistrovat, vypsat v menu ani použít na druhé stránce a její widgety nejsou odnikud jinud dosažitelné.

Dashboard drží deklaraci sám, stejně jako Resource drží tabulku:

use NyonCode\WireCore\Widgets\Dashboard;
 
final class SalesDashboard extends Dashboard
{
public function widgets(): array
{
return [
StatsOverviewWidget::make()->stats([Stat::make('Tržby', '1.2M')]),
ChartWidget::make()->heading('Posledních 30 dní')->type('line'),
];
}
 
public function columns(): int
{
return 3;
}
}

php artisan make:wire-dashboard Sales vygeneruje dva soubory:

app/Dashboards/SalesDashboard.php deklarace
app/Livewire/Dashboards/ShowSales.php stránka, která ji mountuje

Dva proto, že jeden z nich nejde otevřít. Dashboard, který nedeklaruje žádné stránky, není nikam routovaný — v menu je z něj položka bez odkazu a na každé adrese odpovídá 404 — takže generátor, který psal jen deklaraci, vracel něco, co vypadalo hotově a hotové nebylo.

Stránka je třída z wire-panels, takže ji generují panels: příkaz zavolá make:wire-dashboard-page, pokud je ten balíček nainstalovaný, a jinak řekne, co doinstalovat. --no-page zapíše jen deklaraci; deklarované pages() tam zůstane tak jako tak, protože je nikdo nečte, dokud se neroutuje.

Obě šablony jsou stuby, které jde publikovat a upravit:

php artisan vendor:publish --tag=wire-core::stubs # dashboard
php artisan vendor:publish --tag=wire-panels::stubs # stránka

Publikovaný stubs/wire-core/dashboard.stub má přednost před balíčkovým, stejně jako to dělá Laravelí stub:publish.

Registruje se stejně jako resources:

// config/wire-core.php
'dashboards' => [
App\Dashboards\SalesDashboard::class,
],

Kde je aplikace registruje

Dashboardy ani navigační skupiny nepatří žádnému resource ani dashboardu — jsou to věci, které skládá aplikace. wire-core na to dodává provider, stejně jako to dělá Cashier a Fortify:

php artisan vendor:publish --tag=wire-core::providers

Publikuje se jako app/Providers/WireDashboardServiceProvider.php a je váš k úpravám: zaregistrujte ho v bootstrap/providers.php a deklarujte v něm své dashboardy i skupiny menu na jednom místě.

Jak se vykresluje

WirePanels\Resources\Pages\DashboardPage ho vykreslí přesně tak, jako ListPage vykresluje tabulku resource — skládá WithWidgets, takže mřížka, pravidla viditelnosti i polling jednotlivých widgetů výše zůstávají beze změny:

use NyonCode\WirePanels\Resources\Pages\DashboardPage;
 
class SalesDashboardPage extends DashboardPage
{
protected static ?string $dashboard = SalesDashboard::class;
}

Obě cesty zůstávají rovnocenné: stránka, která si deklaruje vlastní getWidgets() a žádný dashboard nejmenuje, funguje jako dřív. Stránka, která nedeklaruje ani jedno, se odmítne vykreslit místo prázdné mřížky — prázdná se čte jako „žádné widgety“, ne jako chyba.

Jak se dostane do menu

Dashboard se do menu dostane stejně jako resource — implementací ProvidesNavigation:

public static function navigation(): NavigationItem
{
return NavigationItem::make('Prodej')->icon('outline:chart-bar')->group('insights');
}

Workspace přitom vůbec neví, co je dashboard. DashboardRegistry je RegistrySource a workspace vypisuje to, co mu katalog podá — díky tomu může menu míchat resources, dashboardy a cokoli, co aplikace zaregistruje později. Router čte tentýž katalog, takže dashboard, který deklaruje pages(), zaroutuje Route::wireResources() stejně jako resource. Viz Resources.

Dashboard API

Metoda Vrací K čemu
widgets(): array array<int, Widget> Widgety v pořadí rozložení. Povinné
columns(): int int Sloupce mřížky; výchozí 2
customisable(): bool bool Jestli si ho každý uživatel smí přeskládat; false, a to je celé opt-in
savedLayouts(): bool bool Jestli si každý uživatel smí držet víc uspořádání, každé pod jménem; potřebuje i customisable()
defaultLayout(): ?array array|null Co uživatel vidí, než si cokoli uspořádá; zbytek začíná v zásobníku. Null umístí vše deklarované
autosave(): bool bool Jestli se změna v režimu úprav uloží hned; false ponechá Uložit a Zrušit
maxWidgets(): ?int int|null Nejvíc widgetů, které si uživatel smí umístit; null bez limitu
filters(): array array<int, DashboardFilter> Filtry nad celým dashboardem, čtené ve widgets() přes filter()
withFilterState(DashboardFilterState $state): static static Nastaví stránka před widgets(); deklarace ho jen čte
filter(string $name): ?string string|null Chráněná. Hodnota jednoho filtru — jeho výchozí, dokud stránka neřekne jinak
static key(): string string Identita, odvozená z názvu třídy bez Dashboard
static label(): string string Lidský název; výchozí nadpis stránky

Jak nechat každého uživatele přeskládat si ho

customisable() je tady jedna věta a za ní celá funkce: widgety, které si uživatel položil, v jeho pořadí a v jeho velikostech, uložené per uživatel a per dashboard. DashboardPage z toho udělá klíč, pod kterým se layout ukládá, a přidá ovládání Přizpůsobit / Uložit / Zrušit / Obnovit — deklarovaný dashboard tak nepotřebuje vlastní view. Každý widget na něm potřebuje vlastní key().

Tu funkci tvarují defaultLayout(), autosave() a maxWidgets() — co uvidí nováček, jestli se změna uloží hned, kolik widgetů se vejde. Úložiště, zásobník, group(), sizes() i ty tři popisují Widgety → Přizpůsobitelné dashboardy.

Filtry nad celým dashboardem

Vlastní filter() widgetu zužuje ten jeden widget. Dashboard čtený jako jeden obraz chce opak: jeden výběr — „tento měsíc“, „tento zákazník“ — na který odpovídá každý widget, takže dvě čísla vedle sebe odpovídají na tutéž otázku.

use NyonCode\WireCore\Widgets\Dashboard;
use NyonCode\WireCore\Widgets\DashboardFilter;
 
final class ProductionDashboard extends Dashboard
{
public function filters(): array
{
return [
DashboardFilter::make('period')->label('Období')->buttons()
->options(['week' => 'Tento týden', 'month' => 'Tento měsíc', 'all' => 'Vše'])
->default('month'),
DashboardFilter::make('customer')->label('Zákazník')->placeholder('Všichni zákazníci')
->options(fn (): array => Customer::withWorkInProgress()->pluck('name', 'id')->all()),
];
}
 
public function widgets(): array
{
$metrics = ProductionMetrics::for($this->filter('period'), $this->filter('customer'));
 
return [
StatsOverviewWidget::make()->key('made')->heading('Vyrobeno')
->stats([Stat::make('Kusů', (string) $metrics->made())]),
StatsOverviewWidget::make()->key('server')->heading('Účetní server')
->ignoresDashboardFilters()
->stats([Stat::make('Stav', $this->serverStatus())]),
];
}
}

Výběr žije na stránce a v adrese. DashboardPage ho drží v $dashboardFilters, který je v query stringu jako ?dashboard[period]=week — dashboard se tak dá poslat odkazem přesně tak, jak ho odesílatel viděl, a obnovení stránky ho zachová. Drží se jen hodnoty, které se liší od výchozích, takže nefiltrovaný dashboard má čistou adresu. Vlastnost je zamčená: lišta filtrů ji mění přes setDashboardFilter() a cokoli přišlo v adrese se ověří dřív, než to kdokoli přečte.

Hodnota zůstane, jen když ji filtr nabídl. Všechno ostatní — starý odkaz, překlep, id z doby před změnou voleb — spadne na výchozí hodnotu filtru, místo aby se dostalo k dotazu, který ji čte. Výchozí null znamená „nezužuje nic“ a kreslí se jako placeholder.

Stránka předá vyhodnocené hodnoty dashboardu dřív, než běží widgets() (withFilterState()), a filter() je čte; dashboard postavený bez stránky čte své výchozí hodnoty. Ručně psaný hostitel WithWidgets deklaruje getDashboardFilters() a v getWidgets() čte $this->dashboardFilter('period') stejně.

Widget, který zúžit nejde, to řekne. ignoresDashboardFilters() na něj dá značku „Bez filtru“, dokud se kterýkoli filtr liší od výchozího — stav externího serveru nemá zákazníka a čtenář, který ho porovnává s filtrovanými sousedy, to potřebuje vědět. Značka leží přes spodní okraj karty a nezabírá místo, takže kartu nikdy neposune mimo řadu s kartami vedle ní.

Kde se kreslí. DashboardPage dá lištu filtrů vedle ovládání rozložení. Vlastní hostitel ji vloží tam, kam ji chce jeho layout:

@include('wire-core::widgets.partials.widget-filters') {{-- bez filtrů nevykreslí nic --}}
@include('wire-core::widgets.widget-grid')

Filtr deklarovaný buttons() — pár období čitelných na první pohled — je řada tlačítek, ostatní jsou select, kde volby jsou záznamy. Odkaz „Zrušit filtr“ se objeví, jen dokud je něco zúžené.

DashboardFilter API

DashboardFilter::make(string $name)
->label(string|Closure|null $label) // výchozí je jméno jako nadpis
->options(array|Closure $options) // hodnota => popisek; closure se vyhodnotí, až je potřeba
->default(int|string|null $default) // hodnota, když není nic vybráno; null nezužuje nic
->placeholder(string|Closure|null $placeholder) // prázdná volba selectu — výchozí: popisek
->buttons(bool $buttons = true) // řada tlačítek místo selectu
->getOptions(): array // klíčované hodnotou tak, jak ji nese adresa
->getDefault(): ?string
->isButtons(): bool
->resolve(mixed $value): ?string // nabídnutá hodnota, jinak výchozí

Přidání widgetu na dashboard, který nevlastníte

Dashboard z nainstalovaného modulu deklaruje svoje widgety uvnitř balíčku, takže aplikace k nim přidá přes hook widget.configuring:

$manager->hook(Hook::WidgetConfiguring, function (WidgetConfiguringPayload $payload) {
$payload->widgets[] = StatsOverviewWidget::make()->stats([Stat::make('Otevřené', '12')]);
 
return $payload;
}, for: 'sales');

for: pojmenuje klíč, pod kterým se dashboard zaregistroval — DashboardPage jím odpoví. Stránka, která si widgety deklaruje sama, neukazuje nic registrovaného a zúží se podle třídy.

Běží dřív, než se orazítkují klíče, a dřív, než seznam profiltruje viditelnost — a obojí je podstatné: klíč widgetu se odvozuje z jeho indexu v neprofiltrovaném seznamu, takže widget přidaný později by žádný neměl (a poll tick by ho nedosáhl), nebo by si vzal ten, na který už jiný widget slyší; a visible() přidaného widgetu platí, místo aby se přeskočilo.

Související