Skip to content

Repository files navigation

Flexible Dashboard

A companion plugin that adds a rows + columns dashboard to Matomo, plus Markdown text blocks you can drop in beside the normal report widgets to add section headers and explanatory notes.

It runs alongside the built-in Dashboard plugin rather than modifying it, so your standard dashboards keep working and nothing here is overwritten when you upgrade Matomo.

Status. Tested against a live Matomo 5.11/5.12 instance (Docker, PHP 8.4): activation, SPA + standalone rendering, widget placement in any column, Markdown editing with live preview, layout persistence (including quotes and HTML entities in Markdown), importing a core-Dashboard-shaped layout, and the Markdown XSS defenses were all exercised end-to-end in a real browser. The companion dev environment lives in ../matomo-dev-docker-compose.


What you get

  • A new Flexible Dashboard entry in the reporting menu (its own category), separate from the standard Dashboard.
  • Dashboards made of rows. Each row has its own column layout (100, 50-50, 67-33, 33-67, 33-33-33, 40-30-30, 30-40-30, 30-30-40, 25-25-25-25).
  • Any Matomo report widget can be placed in any column (reusing Matomo's own widget rendering).
  • Markdown text blocks for headers / descriptions / links, rendered safely (see security model).
  • Drag-and-drop to reorder blocks within and across columns and rows, and to reorder rows.
  • Create / rename / reset / remove dashboards, copy a dashboard to another user (admins), and "set current layout as the default".

Requirements

  • Matomo 5.x
  • The core Dashboard plugin must be active. This plugin deliberately reuses Matomo's existing client-side widget machinery (widgetsHelper, the $.fn.dashboardWidget jQuery plugin, the WidgetFactory, and @Dashboard/_widgetFactoryTemplate.twig) instead of duplicating it.

Install

  1. Copy the FlexibleDashboard folder into your Matomo plugins/ directory:
    matomo/plugins/FlexibleDashboard/
    
  2. Activate it:
    • Administration → Plugins → FlexibleDashboard → Activate, or
    • CLI: ./console plugin:activate FlexibleDashboard
  3. Reload the reporting UI. A Flexible Dashboard section appears in the left reporting menu.

Activation creates one database table, <prefix>flexible_dashboard (login, iddashboard, name, layout MEDIUMTEXT). Uninstalling drops it. Your core user_dashboard table is never touched.


How to use

  • Add row (toolbar) appends a new full-width row. Hover a row to reveal its controls: change row layout, add widget, add text block, remove row, and a drag handle to reorder rows.
  • Hovering a row also reveals a small Add widget / Add text block control at the bottom of every column, so blocks can be placed directly into any column without dragging.
  • Add text block inserts a Markdown block and opens the editor. The editor has a live, server-rendered preview.
  • Drag a block by its top bar to move it between columns or rows.
  • Manage dashboard (toolbar) → create / rename / reset / remove / copy / set-as-default.

Data model

The layout is stored as JSON:

{
  "config": { "layout": "rows" },
  "rows": [
    {
      "config": { "layout": "100" },
      "columns": [
        [ { "type": "markdown", "id": "md_intro", "markdown": "# Overview" } ]
      ]
    },
    {
      "config": { "layout": "50-50" },
      "columns": [
        [ { "uniqueId": "widget...", "parameters": { "module": "VisitsSummary", "action": "getEvolutionGraph" } } ],
        [ { "uniqueId": "widget...", "parameters": { "module": "VisitsSummary", "action": "get" } } ]
      ]
    }
  ]
}

A block in a column is either:

  • a widget: { uniqueId, parameters, isHidden } (identical to the core Dashboard widget object), or
  • a Markdown block: { type: "markdown", id, markdown }.

Migration / importing a core dashboard

The loader accepts older shapes and normalises them to rows automatically:

  • a bare array of columns (very old core layout) → wrapped as one row;
  • the modern core shape { config, columns: [...] } → wrapped as one row using the same column layout.

So if you paste/copy a standard dashboard's layout into a flexible dashboard, it keeps working as a single row that you can then split into multiple rows. (A one-click "import from standard dashboard" button was intentionally left out to keep the first version small; the conversion logic it would need already exists in FlexibleDashboard::migrateToRows().)


Markdown security model ("Markdown only, safest")

Because dashboards can be copied between users, custom HTML would be a stored-XSS vector. This plugin therefore accepts Markdown only and renders it with an escape-first converter (Markdown/SafeMarkdown.php):

  1. The entire input is htmlspecialchars-escaped first. Any raw HTML a user types (e.g. <script> or <img onerror=…>) becomes inert text, never live markup.
  2. Only a fixed allow-list of tags is ever emitted, all generated by the converter itself: h1h6, p, br, strong, em, code, pre, ul, ol, li, blockquote, hr, a.
  3. Links are restricted to http(s), mailto: and relative/in-page targets (/…, #…). javascript:, data:, vbscript:, file: and protocol-relative //host URLs are downgraded to plain text. Every link gets rel="noopener noreferrer". Image syntax ![alt](url) renders as a plain link to the image — inline images are deliberately not supported, since remote images would leak dashboard viewers' IP addresses.
  4. Input is capped (100 KB per block) to avoid resource abuse.

Rendering always happens on the server (Controller::renderMarkdown), both for the saved block and for the editor's live preview, so the client is never trusted to sanitise. Because the only HTML injected into the page for a block is the server's sanitised output, the saved layout JSON itself is not strip_tags-ed (that would corrupt legitimate Markdown such as a < b); instead it is validated as well-formed JSON within a 2 MB cap before being stored.

If you prefer a different parser later (e.g. a CommonMark library), swap the body of SafeMarkdown::toHtml() — but keep the escape-first + allow-list approach, or re-introduce a sanitiser, before emitting anything.


File map

FlexibleDashboard/
├── plugin.json                         Plugin manifest (requires Matomo 5)
├── FlexibleDashboard.php               Plugin class: events, assets, default layout, migration
├── API.php                             getDashboards / create / remove / copy / reset
├── Controller.php                      index, layout get/save, reset, renderMarkdown, row presets
├── Model.php                           CRUD over the flexible_dashboard table
├── Markdown/SafeMarkdown.php           Escape-first Markdown → safe HTML
├── Categories/FlexibleDashboardCategory.php   Own reporting category
├── config/config.php, config/tracker.php
├── javascripts/
│   ├── flexibleDashboardObject.js      Rows+columns engine ($.fn.flexibleDashboard)
│   ├── markdownBlock.js                Markdown block: render + editor + sortable chrome
│   └── flexibleDashboard.js            Management dialogs + bootstrap (no Vue build needed)
├── stylesheets/flexibleDashboard.less  Rows, blocks, editor, picker, layout chooser
├── templates/
│   ├── embeddedIndex.twig              Toolbar, dialogs, editors, container
│   ├── index.twig                      Standalone wrapper
│   └── _header.twig
└── lang/en.json

Verified on a real Matomo 5 instance

Exercised end-to-end against Matomo 5.11/5.12 (official Docker image, PHP 8.4), driven both via HTTP and via a real browser:

  1. Initialisation without Vue works: the jQuery + MutationObserver bootstrap mounts the dashboard on first page load, on SPA menu navigation, and when switching between dashboards (subcategories).
  2. Core widget machinery renders inside arbitrary columns: report widgets (graphs, sparklines, realtime log, maps) paint correctly in every row layout.
  3. Persistence round-trips exactly: layouts containing quotes, HTML-as-text and entities in Markdown save and reload byte-identical. Layouts saved by this plugin are decoded as plain JSON; the legacy entity/backslash de-escaping is only applied as a fallback when importing a layout stored by the core Dashboard plugin (see FlexibleDashboard::decodeLayout()).
  4. Markdown editor live preview, save-in-place, and the XSS defenses (script tags, javascript:/data:/protocol-relative links, attribute breakout attempts) behave as designed.
  5. Anonymous users still deserve a manual check: session-stored layouts (view/reset) are implemented like core but were not part of the automated test run.

About

A more flexible dashboard for Matomo

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages