Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
UI vrstva / Widgety dashboardu

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 stav error s 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ý řádek notfound).
  • 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 z DashboardCountManager::at(), jinak se kvůli mikrosekundám new 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 z DashboardAttentionCards::definitions() (orders → typ attentionOrders). Právo typu je právo cílového výpisu, nula se ukáže jako klidový stav.
  • Rychlé odkazy (System/Widgets/TilesWidget/Tiles, typ tiles) obalí komponentu DashboardTiles, 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 (sloupce dashboard_tile / dashboard_order u 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í“#

  1. Do DashboardAttentionCards::definitions() přidejte self::define(key, privilege, icon, tone, params). privilege je klíč cílového výpisu (typicky …:show) a zároveň cíl odkazu. Pořadí v poli je pořadí naléhavosti.
  2. tone podle významu: urgent (někdo čeká na peníze nebo zboží), review (čeká na rozhodnutí administrátora), notice (provozní upozornění).
  3. Má-li cílový grid odpovídající filtr, předejte ho v params jako ['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 gridu PresetSelectFilter (Práce s tabulkami).
  4. Počet doplňte do match v DashboardAttentionManager::countCard() ($this->countBy($manager, [filtry])), manager do jeho konstruktoru.
  5. Překlady ve všech jazycích: admin.system.components.dashboardAttention.cards.<key> se třemi tvary ('{1} … |[2,4] … |]4,Inf[ …') a admin.system.widgets.infoBox.types.attention<Key>.title / .description (.keywords).
  6. 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#

  1. Složka. app/UI/Admin/<Modul>/Widgets/<Nazev>Widget/. Zkopírujte podobný widget: seznam Eshop/Widgets/OrdersToShipWidget, graf v čase Eshop/Widgets/OrdersOverTimeWidget, peníze Eshop/Widgets/RevenueWidget.
  2. Třída <Nazev> extends BaseWidget. Managery svého modulu přijme v konstruktoru a zavolá parent::__construct(). V render() připraví data a vykreslí setTemplateFile(__DIR__, 'Style1.latte').
  3. Továrna <Nazev>Factory extends BaseWidgetFactory, create() vrací $this->initialized(new <Nazev>(…)).
  4. Typ v buildTypes() (tabulka výše). Klíč musí být jedinečný v celém registru. purpose a preview vyplňte vždy, registr je u každého typu vyžaduje.
  5. Data čtěte jen přes managery modulu, souhrny v čase přes MetricManager. Chybu zalogujte (logWidgetFailure()) a vraťte null, nikdy výjimku.
  6. Šablona Templates/Default/Style1.latte se vykreslí uvnitř .dwg__body. Začněte {import $dwgBlocks} a použijte sdílené bloky (dwgRows, prázdný a chybový stav).
  7. Odkaz na výpis vraťte z resolveOpenLink() přes buildOpenLink(':Admin:…:show', $args). Předfiltrovaný výpis otevřete přes ['grid-filters' => […]] se stejnou podmínkou, jakou widget počítá.
  8. Práva. Klíč privilege (a u statistik statisticsPrivilege) musí existovat v cms_system_user_role_privileges; nový klíč přidejte SQL migrací.
  9. Překlady ve všech jazycích v app/Locale/<modul>/<jazyk>/admin.<locale>.neon pod <modul>.widgets.<nazev>: title, description, volitelně keywords, popisky stylů a texty šablony.
  10. Testy. Doplňte třídu do WIDGET_CLASSES a typ do SINGLE_TYPES (u statistik i do STATISTICS_TYPES) v tests/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.
  11. 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

Nastavení cookies