Widgety dashboardu#
Dashboard administrace (:Admin:System:Page:dashboard) je plocha s widgety. Šablona stránky
vykreslí jen {control adminSystemDashboardWidgets}, všechno ostatní dělá orchestrátor
DashboardWidgets a widgety jednotlivých modulů. Kód:
app/UI/Admin/System/Components/DashboardWidgetsComponent/.
Jak je plocha poskládaná#
| Část | Role |
|---|---|
DashboardWidgets (komponenta) |
rozložení, karta widgetu (hlavička s ikonou, titulek, přesun, menu „…“), panel „Přidat widget“, signály |
widget (BaseWidget) |
obsah jedné karty, čte data přes managery svého modulu |
DashboardWidgetType |
deklarace typu: klíč, texty, práva, velikosti, nastavení |
DashboardWidgetRegistry |
sebere typy ze všech widgetů v DI |
DashboardLayoutGrid |
čisté funkce mřížky: kolize, volná místa, normalizace nastavení |
cms_system_dashboard_widgets |
uložené rozložení |
dashboard-widgets.js |
přetahování, panel, krok nastavení, dávkové načítání obsahu |
Rozložení patří dvojici (stránka administrace, přihlášený uživatel). Jeden řádek tabulky
cms_system_dashboard_widgets je jeden widget: page_id, user_id, widget_type, buňka
grid_col / grid_row, velikost col_span / row_span a nastavení jako JSON
(settings). Mřížka má 4 sloupce (DashboardLayoutGrid::COLUMNS), pod 1200 px se widgety
přeskládají do 2 a 1 sloupce. Uživatel bez uloženého rozložení vidí prázdnou plochu se 4 řádky
volných míst „+“; výchozí sestava widgetů neexistuje. Na jedné ploše je nejvýš
MAX_WIDGETS (30) widgetů.
Server nevěří ničemu, co pošle prohlížeč
Každý signál (saveLayout!, addWidget!, removeWidget!, saveSettings!) se znovu
ověří proti uloženému rozložení: typ existuje a uživatel má jeho práva (i při přidání),
velikost je v rozsahu typu, widget leží uvnitř 4 sloupců a nepřekrývá jiný, id patří
do vlastního rozložení, počet widgetů a délka i tvar JSON jsou v mezích. Jedna neplatná
část zamítne celý požadavek a nic se nezapíše. Vlastníka požadavek vůbec nenese, Api ho
bere z identity. Zápisové signály jsou jen POST + AJAX.
Widget, na který uživatel ztratil právo, se nevykreslí, nenačte, nenabídne ani nepřijme, ale
v rozložení zůstane uložený (visible: false). Každé uložení ho zachová a s vráceným právem
se objeví na svém místě.
Načítání obsahu#
Stránka vykreslí jen karty s kostrou; obsah si skript dotáhne signálem loadWidgets! (GET +
AJAX, po dávkách 4 karet, nejvýš 8 na požadavek). Pomalý nebo padající widget proto nezdrží
ani nerozbije ostatní:
- Výjimka při vykreslení se zapíše do
log/dashboard.log, karta dostane staverrors tlačítkem „Zkusit znovu“ (dávka o jedné kartě) a ostatní karty dávky se vykreslí. - Požadavek nese jen id řádků. Typ, velikost a nastavení se berou z uloženého řádku, práva
se ověří u každé karty znovu (
forbidden, cizí nebo neznámý řádeknotfound). - Počty sdílí
DashboardCountManager: stejný dotaz (manager + filtry) se v rámci požadavku spočítá jednou, i když stejný info box leží na ploše dvakrát. Mezi požadavky se nic necachuje. Časová okna („dnes“, „7 dní“) stavějte zDashboardCountManager::at(), jinak se kvůli mikrosekundámnew DateTime()dva stejné dotazy nikdy nepotkají.
Widget = komponenta ve složce Widgets/#
app/UI/Admin/<Modul>/Widgets/<Nazev>Widget/
├── <Nazev>.php extends App\Core\Base\BaseWidget
├── <Nazev>Factory.php extends App\Core\Base\BaseWidgetFactory
├── Traits/InitTrait.php čtení dat (volitelné, kvůli délce)
└── Templates/Default/Style1.latte
Továrnu najde DI search jako u každé komponenty a DashboardWidgetRegistry sebere všechny
potomky BaseWidgetFactory. Třídu widgetu odvodí ze jména továrny (<Nazev>Factory →
<Nazev>) a přečte její statické buildTypes(). Registrace v configu není potřeba.
BaseWidget je běžná komponenta (setTemplateFile(__DIR__, …), podkomponenty). Navíc:
| Člen | K čemu |
|---|---|
$translatorDomain |
výchozí doména podle konvence admin.<modul>.widgets.<nazev> (OrdersToShip → admin.eshop.widgets.ordersToShip) |
getInstance() |
typ, ověřené nastavení, velikost a titulek karty |
resolvePeriodDays(), resolveStyle(), resolveLimit(), resolveCurrencySetting() |
nastavení instance, jinak výchozí hodnota typu |
resolveOpenLink() |
hlavní odkaz karty (první položka menu „…“), postav ho přes buildOpenLink() |
resolveTone(), isCalm() |
význam karty podle aktuálních dat (dwg__item--<tone>, klidový stav) |
logWidgetFailure() |
zápis do logu dashboard; nikdy nevyhodí |
$dwgBlocks, $dwgCommon v šabloně |
sdílené Latte bloky (Widgets/Templates/Blocks.latte: řádky, prázdný stav, sparkline…) a jejich texty |
Orchestrátor hlídá jen práva typu
Než widget vykreslí, ověří privilege a statisticsPrivilege jeho typu. Když widget
odkazuje na jiné stránky nebo počítá data jiného výpisu, ověří si cíl sám přes
isTargetAllowed(). buildOpenLink() to dělá a pro cíl bez práva vrátí null.
Chyba čtení není nula
Při selhání čtení vraťte null a v šabloně ukažte pomlčku nebo chybovou větu
(dwgPeriodFailed). Nula by tvrdila „nic nečeká“, přitom data jen nejdou přečíst.
Typ widgetu: DashboardWidgetType#
Widget deklaruje typy, které umí vykreslit, ve statické buildTypes(). Obvykle jeden;
InfoBox jich deklaruje víc, jeden na kartu. Typ se staví jen přes
DashboardWidgetType::create(...) (konstruktor je soukromý, jinak by ho DI search zkoušel
autowirovat). Nekonzistentní definice vyhodí InvalidArgumentException hned při sběru
registru.
public static function buildTypes(): array
{
return [
DashboardWidgetType::create(
key: 'ordersToShip',
titleKey: 'admin.eshop.widgets.ordersToShip.title',
descriptionKey: 'admin.eshop.widgets.ordersToShip.description',
privilege: ':Admin:Eshop:Order:show',
minColSpan: 2, maxColSpan: 4, minRowSpan: 1, maxRowSpan: 3,
defaultColSpan: 2, defaultRowSpan: 2,
icon: 'fa-solid fa-box-open',
tone: 'neutral',
chipTone: 'red',
position: 320,
purpose: 'lists',
preview: 'table',
recommended: true,
limits: [3, 5, 10],
defaultLimit: 5,
),
];
}
| Parametr | Význam |
|---|---|
key |
jedinečný klíč typu (ukládá se do widget_type), [A-Za-z][A-Za-z0-9_]*, nejvýš 64 znaků |
titleKey, descriptionKey |
plné klíče překladu; vedle titulku volitelně ….keywords (synonyma pro hledání v panelu) |
privilege |
admin klíč presenteru, jehož data widget čte (typicky …:show); null jen pro widget, který žádná data nečte (nadpis, rychlé odkazy) |
statisticsPrivilege |
:Admin:<Modul>:<Entita>:statistics pro finanční a statistické souhrny; s ním musí uživatel mít oba klíče |
minColSpan … defaultRowSpan |
povolené a výchozí rozměry v buňkách; šířka nejvýš 4, výška nejvýš 4 (MAX_ROW_SPAN), výchozí uvnitř rozsahu |
icon |
třídy Font Awesome ikonky v hlavičce karty |
tone |
význam karty: urgent, review, notice, neutral |
chipTone |
barva ikonky: green, blue, amber, red, violet, neutral |
position |
pořadí v panelu (vzestupně, pak podle klíče); info boxy začínají na 100 |
hasPeriod |
nastavení period (7, 30, 90, 365, custom) + days (nejvýš 365) |
hasText |
nastavení text (povinné, 80 znaků) a description (160 znaků); karta je pak nadpis bez těla a musí mít výšku 1 |
styles |
klíč stylu => klíč překladu popisku, aspoň dva; výchozí je první |
limits, defaultLimit |
povolené počty řádků seznamu (aspoň dva, 1–100), výchozí defaultLimit nebo první |
hasCurrency, defaultCurrency |
nastavení currency: auto (měna s nejvyšším součtem), all (každá měna zvlášť) nebo kód měny z číselníku |
hasLinks |
nastavení links: seznam položek admin menu {id, fg?, bg?} v pořadí uživatele, barvy #rrggbb, nejvýš 24 (MAX_LINKS) |
module |
modul v panelu; nech null a registr ho odvodí z klíče privilege (:Admin:Eshop:… → eshop), bez práva z namespace widgetu |
purpose |
skupina v panelu: waiting, stats, lists, actions, general |
preview |
miniatura na kartě panelu: columns, area, sparkline, ranking, number, list, stack, donut, table, status, actions, heading, tiles, triple |
recommended |
nabídnout v bloku „Doporučené“ nahoře v panelu |
variant |
rozliší typy jedné třídy (u info boxů klíč karty) |
Nastavení instance ověřuje DashboardLayoutGrid::normalizeSettings(): povolené jsou jen klíče,
které typ deklaruje, a jen hodnoty z deklarace. Cokoli jiného zamítne celý zápis. Uložená
velikost, která po změně typu přestane platit, se při čtení ořízne do nového rozsahu, takže
změna deklarace nerozbije uložená rozložení.
Práva#
| Klíč | Kdy | Co hlídá |
|---|---|---|
privilege |
každý widget, který čte data | stejný klíč jako výpis, ze kterého data pochází |
statisticsPrivilege |
widget se souhrny, grafy a penězi | :Admin:<Modul>:<Entita>:statistics; klíč nemá stránku ani akci, čte ho jen dashboard |
Kontrola je fail-closed: klíč, který v cms_system_user_role_privileges neexistuje, skryje
widget všem, i superadminovi. DashboardWidgetRegistryTest proto ověřuje každý deklarovaný
klíč proti databázi. Nový klíč …:statistics přidejte SQL migrací a přidělte ho jen rolím,
které už dnes vidí odpovídající čísla (vzor 2026-10-29_24_metric_api_statistics_privileges.sql).
Který klíč patří ke kterému zdroji, říká MetricSourceCatalog::getStatisticsPrivilege().
Data pro grafy: Metric API#
Grafy v čase, součty za období a žebříčky čtou widgety jedinou cestou: admin
MetricManager → Api api/system/metric/series a api/system/metric/total
(:Api:System:Metric:series / :total). Akce nemají action*(), zvenku je zavolat nejde.
Zdroje dat deklaruje každý modul ve své Api vrstvě:
app/UI/Api/<Modul>/Metrics/<Modul>MetricSources.php implementuje
MetricSourceProviderInterface a buildSources() vrací MetricSource::create(...): tabulku,
sloupec data, pevné podmínky, sčitatelné hodnoty, měnu, dimenze pro groupBy, filtry a čtecí
klíč Api (:Api:Eshop:Order:getAll). DI search třídu zaregistruje a MetricSourceRegistry
ji najde podle rozhraní. Požadavek pojmenovává jen aliasy ze zdroje, názvy sloupců z něj nikdy
nepřijdou.
MetricSource::create(
key: 'comment',
table: 'cms_mod_base_comments',
tableAlias: 'c',
dateColumn: 'c.insert_datetime',
privilege: ':Api:Base:Comment:getAll',
conditions: ['c.deleted = 0'],
dimensions: [MetricDimension::create('approved', 'c.approved')],
filters: ['approved' => 'c.approved'],
),
Dotaz ověří MetricQuery dřív, než se odešle: metrika count nebo sum:<alias>, granularita
day / week / month / year, období nejvýš 1830 dní, limit 1–50. Neplatný dotaz je
programátorská chyba widgetu (MetricQueryException). Selhání nebo chybějící právo
MetricManager zaloguje a vrátí null.
Měny se nikdy nesčítají
Peněžní metrika vrátí jednu řadu nebo položku na měnu, žebříčky se řadí uvnitř každé
měny. Widget s penězi deklaruje hasCurrency a čte přes MoneySeriesWidgetTrait,
počty v čase přes MetricSeriesWidgetTrait.
Info boxy a rychlé odkazy#
Dřívější pevný dashboard (pásmo „Co čeká na vyřízení“ a dlaždice) žije dál jako widgety:
- Info box (
System/Widgets/InfoBoxWidget/InfoBox) deklaruje jeden typ na každou kartu zDashboardAttentionCards::definitions()(orders→ typattentionOrders). Právo typu je právo cílového výpisu, nula se ukáže jako klidový stav. - Rychlé odkazy (
System/Widgets/TilesWidget/Tiles, typtiles) obalí komponentuDashboardTiles, která právo ověří u každé dlaždice. Výběr, pořadí a barvy odkazů si uživatel nastaví ve widgetu (nastavenílinks). Bez vlastního výběru ukáže celoprojektové dlaždice (sloupcedashboard_tile/dashboard_orderu položek admin menu) a nový widget je má předvybrané. Samostatná obrazovka „Dlaždice dashboardu“ je zrušená.
Postup: přidávám kartu „Co čeká na vyřízení“#
- Do
DashboardAttentionCards::definitions()přidejteself::define(key, privilege, icon, tone, params).privilegeje klíč cílového výpisu (typicky…:show) a zároveň cíl odkazu. Pořadí v poli je pořadí naléhavosti. tonepodle významu:urgent(někdo čeká na peníze nebo zboží),review(čeká na rozhodnutí administrátora),notice(provozní upozornění).- Má-li cílový grid odpovídající filtr, předejte ho v
paramsjako['grid-filters' => ['approved' => 0]]; karta pak otevře jen to, co je potřeba vyřídit. Pro podmínku nad víc sloupci použijte v griduPresetSelectFilter(Práce s tabulkami). - Počet doplňte do
matchvDashboardAttentionManager::countCard()($this->countBy($manager, [filtry])), manager do jeho konstruktoru. - Překlady ve všech jazycích:
admin.system.components.dashboardAttention.cards.<key>se třemi tvary ('{1} … |[2,4] … |]4,Inf[ …') aadmin.system.widgets.infoBox.types.attention<Key>.title/.description(.keywords). - Ověřte s rolí bez práva na cílový výpis: info box v panelu chybí a dotaz se nespustil.
Chyba v počtu karty se projeví jen pomlčkou
countCard() výjimku zapíše do logu dashboard a vrátí null, aby dashboard nespadl.
Info box, který nikdy nic neukáže, proto může být rozbitý; hledejte v
log/dashboard.log.
Stav platby je na dashboardu kopie číselníku
Administrace na Api enumy nesahá, takže DashboardAttentionManager::PAYMENT_STATUS_START
je opsaná hodnota PaymentStatus::START. Změna čísla v číselníku bez změny konstanty
rozbije kartu „uvízlé platby“ bez chybové hlášky.
Postup: přidávám nový widget#
- Složka.
app/UI/Admin/<Modul>/Widgets/<Nazev>Widget/. Zkopírujte podobný widget: seznamEshop/Widgets/OrdersToShipWidget, graf v časeEshop/Widgets/OrdersOverTimeWidget, penízeEshop/Widgets/RevenueWidget. - Třída
<Nazev> extends BaseWidget. Managery svého modulu přijme v konstruktoru a zavoláparent::__construct(). Vrender()připraví data a vykreslísetTemplateFile(__DIR__, 'Style1.latte'). - Továrna
<Nazev>Factory extends BaseWidgetFactory,create()vrací$this->initialized(new <Nazev>(…)). - Typ v
buildTypes()(tabulka výše). Klíč musí být jedinečný v celém registru.purposeapreviewvyplňte vždy, registr je u každého typu vyžaduje. - Data čtěte jen přes managery modulu, souhrny v čase přes
MetricManager. Chybu zalogujte (logWidgetFailure()) a vraťtenull, nikdy výjimku. - Šablona
Templates/Default/Style1.lattese vykreslí uvnitř.dwg__body. Začněte{import $dwgBlocks}a použijte sdílené bloky (dwgRows, prázdný a chybový stav). - Odkaz na výpis vraťte z
resolveOpenLink()přesbuildOpenLink(':Admin:…:show', $args). Předfiltrovaný výpis otevřete přes['grid-filters' => […]]se stejnou podmínkou, jakou widget počítá. - Práva. Klíč
privilege(a u statistikstatisticsPrivilege) musí existovat vcms_system_user_role_privileges; nový klíč přidejte SQL migrací. - Překlady ve všech jazycích v
app/Locale/<modul>/<jazyk>/admin.<locale>.neonpod<modul>.widgets.<nazev>:title,description, volitelněkeywords, popisky stylů a texty šablony. - Testy. Doplňte třídu do
WIDGET_CLASSESa typ doSINGLE_TYPES(u statistik i doSTATISTICS_TYPES) vtests/Unit/Admin/DashboardWidgetRegistryTest.php. Test ověří složku, překlady ve všech jazycích, jedinečné názvy a klíče práv v databázi. - Ověřte po smazání
temp/cache(DI kontejner a RBAC se cachují): widget je v panelu, přidá se, přetáhne, změní velikost a nastavení. S rolí bez práva v panelu chybí.
Nový widget bez temp/cache neexistuje
Registr čte továrny z DI kontejneru. Dokud se kontejner nepřegeneruje, widget v panelu není a uložení jeho typu se zamítne jako neznámý typ.
Kam sáhnout#
| Chci | Kde |
|---|---|
| orchestrátor plochy a signály | app/UI/Admin/System/Components/DashboardWidgetsComponent/ (LayoutTrait, LayoutSaveTrait, WidgetLoadTrait) |
| základ widgetu a továrny | app/Core/Base/BaseWidget.php, BaseWidgetFactory.php |
| sdílené bloky a grafy | app/Core/Base/Widgets/Templates/, app/Core/Base/Widgets/Chart/ |
| deklaraci typu | app/UI/Admin/System/Models/Helpers/DashboardWidgetType.php |
| mřížku a nastavení | app/UI/Admin/System/Models/Helpers/DashboardLayoutGrid.php |
| registr | app/UI/Admin/System/Models/Helpers/DashboardWidgetRegistry.php |
| počty se sdílenou pamětí | app/UI/Admin/System/Models/Managers/DashboardCountManager.php |
| karty info boxů | app/UI/Admin/System/Models/Helpers/DashboardAttentionCards.php, Models/Managers/DashboardAttentionManager.php |
| Metric API | app/Core/Metrics/, app/UI/Api/System/Presenters/MetricPresenter.php, app/UI/Api/<Modul>/Metrics/ |
klíče …:statistics |
app/UI/Admin/System/Models/Helpers/MetricSourceCatalog.php |
| společné traity grafů | app/Core/Traits/Admin/Components/MetricSeriesWidgetTrait.php, MoneySeriesWidgetTrait.php |
| front-end plochy | www/themes/admin/*/desktop/assets/js/components/dashboard-widgets.js |
Navazující kapitoly: Admin presenter · Komponenty · Práce s tabulkami · RBAC · Překlady