New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide an API for plugins to display (multiple) reports in a page #8442
Conversation
@@ -2,6 +2,32 @@ | |||
|
|||
This is a changelog for Piwik platform developers. All changes for our HTTP API's, Plugins, Themes, etc will be listed here. | |||
|
|||
## Piwik 3.0.0 | |||
|
|||
### Breaking Changes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need to optimize the changelog at a later point once we have the final API's and some guides on developer.piwik.org . I expect some of those API's to change (once we work a bit more with it etc) before releasing Piwik 3.0
FYI: I'd merge this by Tuesday or Wednesday (11th/12th) if there are no objections. |
Great work on this one and on the informative PR description. Really like the direction this is going! 👍 |
0b2f3ec
to
fe5677e
Compare
Provide an API for plugins to display (multiple) reports in a page
fixes #7822
fixes #8462
This PR brings many changes and it was not really possible to do smaller PRs as it all affected each other:
API.getWidgetMetadata()
,API.getReportPagesMetadata()
CHANGELOG.md
...#/idSite=...
we now have...#?idSite=
which is more correct (as those are query parameters) and easier to use as it is supported by Angular.Overall I'd say we got rid of some duplicated code and different UI behaviour. Data is reused where possible so that the order of reports and widgets etc is more consistent and when updated in one position, no update somewhere else is needed. Eg the menu items in "Widgets & Dashboard" selector were completely different to the position of reports in the UI as we forgot to update it.
Pages
We got rid of many controllers and actions that were responsible to render reporting pages. From now on any page is constructed pased on widgets. A page is identifed by a
categoryId
(eg 'General_Actions') and asubcategoryId
(eg 'General_Pages').On initial reporting page load
There is a new API
API.getReportPagesMetadata
that returns all reporting pages including the widgets on each page and the structure of each page looks like this:It defines a page for
Actions => Downloads
and only one widget shall be shown within that page.The frontend fetches the data of all pages and creates the reporting menu based on this information as well as the currently selected reporting page. It's quite a lot of data to load but with gzipped it's not that much. On my local server it takes about 300ms to generate and load this data, on a fast server it should be even faster. We only load this list on the initial page load and as long as one doesn't change the date or idSite this list does not need to be reloaded. The good thing is, from then on we have the information of all pages. Meaning when clicking on a menu item, we can build the new page very fast and at least the headlines of the reports will be visible immediately.
Having everything based on the above API will make it possible to have a very flexible UI that can be entirely changed eg by a measurable type like "Mobile app".
To build this list, the system looks for all
Widget
classes provided by plugins, it triggers an eventWidget.addWidgetConfigs
and it asks each report to add widgets.Reporting menu
As you may notice for each page there is a
category
andsubcategory
. Based on this we can build the menu. Eachcategory
represents a main menu item, and eachsubcategory
represents a submenu item of a particular main menu item. They are sorted depending on the specifiedorder
. Theid
of a category or a subcategory will be used in the URL. Egcategory=General_Actions&subcategory=General_Downloads
will be used to render that page. If there's no categroy or subcategory given, we cannot render a page. If both are not given, we select the inital page which is the page having the lowest category and subcategory order (in our case currently "Dashboard" page).For the rendering responsible is the new angular directive
<div piwik-reporting-menu/>
. As soon as someone clicks on a menu item, the URL will be changed and a new page will be rendered immediately because AngularJS triggers a$locationChangeSuccess
event.A typical URL for a reporting page looks like this:
Reporting page
The new angular directive
<div piwik-reporting-page/>
is listening to changes in the URL. As soon as there is a change it renders the requested page depending on thecategory
andsubcategory
URL parameter. As mentioned the data for each page is already present so we can replace the old page immediately with the new one. Each widget containing the actual data still needs a bit of time to render. As the widgets can now be rendered in parallel (eg some pages have 5 or more widgets on one page) it should be faster to load a particular page.The reporting page directive is responsible for rendering a particular page. Eg if an evolution and sparkline widget is defined for a page, it will make sure to show these widgets on the top. If there's only one widget it will show that widget with full width (eg "Action pages") unless specified differently via CSS. If there's more than one widget, it will put these widgets automatically into two columns and also take care of proper headline spacing etc.
It is no longer possible to add any menu items via
ReportingMenu::add(...)
. The reporting menu and reporting pages are fully based on the APIAPI.getReportPagesMetadata
. If one wants to have something shown in a reporting page, one needs to create a widget (eg via a Report or viaWidget
class).Widgets within a page
Each widget that is defined by a widget class or by any report will be shown on a reporting page as long as it defines a
categoryId
andsubcategoryId
. If a widget does not define asubcategoryId
it won't be visible in a reporting page but it would be still possible to put it on a dashboard or to export it unless$widget->setIsNotWidgetizable()
is called.setIsNotWidgetizable()
makes it eg possible to put a widget on a reporting page but not have it available in the list of exportable widgets. This is useful eg forManage Goals
and many other widgets.The order of each widget defines the position of a widget within a page. If a widget is based on a report that has a related report, and this related report would be visible on that page again (eg
Devices.getOs
andDevices.getOsFamilies
are related reports and they would be both visible on one page), then we show only one of those reports (the one having the lower order).New widget API
Instead of having one
Widgets
class per plugin that defines multiple widgets we now have oneWidget
class per widget and it is basically configured like this:They can be located in
Widgets
directory and we can now generate more than one widget (as it is one file per widget). Internally we work withWidgetConfig
which is a rather stupid data object. The actual widget is only needed to render it. Dependencies can be defined in__construct
, the dependencies will be only created if needed to keep gathering widget config information fast. To also keep widget generation fast and cachable we always work with translation keys (eg->setName('Live_VisitorProfile')
instead of->setName(Piwik::translate('Live_VisitorProfile'))
).WidgetConfig
can be also used in aWidget.addWidgetConfigs
event:If one wants to generate a widget from a report all one has to do is to define a
subcategoryId
like this:It is the same as doing this manually:
Many widgets can be generated from one report and they can be customized. Eg
Widget middleware parameters
There are widgets that should be only shown if an archives contains specific data. For example the "Goals Conversion Overview" widget should be only shown if there are actually conversions for the current selected date/period. As mentioned earlier when requesting the initial page we collect all information about all pages and all widgets. Loading this list would be very slow if we had to archive data and fetch reports in order to build this list. Also as we only load this list initially, a widget might not be added to this list if at the point of the initial page load there are no conversions, but at a later point there might be conversions. Also the list of available widgets wouldn't be cachable if it would depend on archived data since we'd have to invalidate the cache whenever new data is tracked or archived.
To have the gathering of all widgets fast I introduced "Middleware parameters". I named it this way as it is kinda used similar in other frameworks like Slim and Laravel. If a widget should be only shown based on archived data, one can set middleware parameters like this:
Whenever a page is requested that contains this widget, we will first perform a request to the specified URL (idSite, period, date is added automatically). If the response contains a JSON
true
we will display the widget, otherwise not. If one clicks on the same page again, we will again perform this check. Meaning even if initially there are no conversions, if one clicks on the link again the widget might be shown if there were conversions meanwhile.Widget container
Sometimes the structure of the UI might be a bit more complex. As mentioned before: As soon as there is more than one widget on a page, they are shown in two columns. If you want to have two independent widgets below each other (and not next to each other) you need to group them as done for example in the
getContinent
report:Any other report can add reports to this container via the
$containerId
as well:Container can as well have a layout in case they are not supposed to be shown below each other. For example you could set a layout
ByDimension
which will show only one of the widgets at a time on the right side and a menu to select a widget on the left side.Widget containers can contain any number of widgets and even widget container itself.
Changing the order of categories and subcategories
As seen earlier in the output a category and subcategory have an
order
to find the correct position:A report consists of a category and optionally a subcategory. The order of the category / subcategory is now used to sort reports (eg for
API.getReportMetadata
). This was hard coded in the past.A widget conists of a category and optionally a subcategory as well. We use this to sort eg the output of
API.getReportPagesMetadata
andAPI.getWidgetMetadata
. Meaning the lower the order of the category, the further left the menu item for that category will be shown. Same for subcategories.By default each category and subcategory have an order of
99
. It is possible to customize this behavior though by defining a category class:or by defining a subcategory class:
Those classes can be placed in a plugin in a
Categories
folder. I did this for all of our plugins to make sure we have the same order in reporting menu as before. A plugin developer would not have to take care of this as those menu items would be just shown after our core menu items (because orderId is99
by default).Sparklines Visualization
The new sparklines visualization shows one or multiple sparklines. Each sparkline item consists of a sparkline graph, at least one metric and one description. Optionally it can show an evolution beside each item. For more documentation see the
Sparklines
andSparklines\Config
PHP docs. An example usage in a report looks like this:Any report can listen to the
ViewDataTable.configure
event and add more sparklines.New angular directives
Besides the mentioned directives
piwik-reporting-menu
andpiwik-reporting-page
there are following new directives:<div piwik-activity-indicator loading="true/false"/>
Shows our general loading message and the typical spinning wheel.<div piwik-dashboard dashboard-id="5"/>
Shows the dashboard having the specified ID.<div piwik-popover-handler/>
If present on any page will listen to the popover URL parameter and open or close a popover. Only needed on a page initially.<div piwik-widget="widget" widetized="true/false"/>
renders any kind of widget as returned by the widget metadata API.<div piwik-widget-container="containerWidget"/>
renders a container widget as returned by the widget metadata API. All widgets within this container are rendered below each other.<div piwik-widget-by-dimension-container="containerWidget"/>
renders a container widget as returned by the widget metadata API. It shows a menu for each widget on the left and the select widget on the right.<div piwik-widget-loader="{module: '', action: '', ...}"/>
similar to ng-include can request any action and shows the returned response of this action within this<div/>
.A reporting page looks pretty much like this:
Furthermore we got kinda rid of the
broadcast
JS object. It is only used by Piwik Overlays now. It's still used to get params though. However, it is no longer used to fetch pages, to handle the history etc. Instead we use Angulars builtin$location
where possible. This is the first step towards using Angulars builtin$routing
but to actually use it, we need to do more work.Other new API's
There's as well a new API method
API.getWidgetMetadata
. It is only kinda similar toAPI.getReportPagesMetadata
.API.getReportPagesMetadata
returns information about all reporting UI pages. They are grouped bycategory
andsubcategory
and only widgets will be shown in this list, if they have actually both a category and a subcategory defined. A widget that has no subcategory defined won't be displayed in a reporting page.API.getWidgetMetadata
is a simple list of all available widgets that can be added to the dashboard or exported via an iframe etc. It also contains widgets that have nosubcategory
defined. Acategory
is mandatory though.If a widget should be shown in a reporting page but not added to the widget metadata, one can call
$widgetConfig->setIsNotWidgetizable()
.If a widget shall be not shown in a reporting page but widgetizable one should simply not set a
subcategoryId
Small overview of new classes
The diff looks really big but it is not that much actually. New are mainly the following classes. Otherwise there are more or less only changes in the plugin to use the new page layout rendering mechanism instead of the old controller logic.
Piwik\Category\CategoryList
Holds a list of all available categoriesPiwik\Category\Category
Defines a single category. A category holds a list of subcategoriesPiwik\Category\Subcategory
(API) Defines a single subcategory.Piwik\Plugin\Categories
Finds all categories and subcategories implemented by pluginsPiwik\Widget\Widget
(API) Defines a standalone widgetPiwik\Widget\WidgetConfig
(API) Data object that defines a widget configuration.Piwik\Widget\WidgetContainerConfig
(API) Data object that defines a widget container configuration.Piwik\Widget\WidgetsList
(API) Holds a list of all configured widgets and widget containerPiwik\Plugin\Widgets
Finds all widgets and widget configs implemented by pluginsPiwik\Report\ReportWidgetConfig
(API) Data object for a widget configuration that is based on a report or created by a report. Allows you to set eg a viewDataTable to render etcPiwik\Report\ReportWidgetFactory
(API) Factory that lets you easily created widgets from a reportPiwik\Plugin\Reports
Finds a specific report or all reports implemented by pluginsCode organization
I made a few changes to the code organization. Eg
Widgets
is no longer underPiwik\Plugin\Widget
, it is underPiwik\Widget\Widget
and everything under this namespace contains so far only code related to Widgets (not really any other dependencies). All category related classes are underPiwik\Category
. They do not really know anything about Piwik and certainly not know anything about Widgets or Reports.I moved some code that finds all components (reports/widgets/categories) that are defined by plugins to
Piwik\Plugin
. For example movedPiwik\Plugin\Report::factory()
andPiwik\Plugin\Report::getAllReports()
to a different class namedPiwik\Plugin\Reports
. Same forPiwik\Plugin\Categories
andPiwik\Plugin\Widgets
. For example:They are under
Piwik\Plugin
as they find only concrete Report, Widget, Category, ... classes implemented by plugins. It would be nice to move most other classes at some point out ofPiwik\Plugin
and eg have instead of aPiwik\Plugin\Report
aPiwik\Report\Report
class.Better widgets list
See following image. We make sure it is more intuitive to find a widget by reusing the structure of the menu. As soon as a subcategory contains 3 or more widgets, we add a new menu item to the list. Eg
Visitors - Times
orEcommerce - Products
, otherwise they are listed in the related category egVisitors
.Todo
TODO
in the code but these can be ignoredAPI.getReportMetadata
to instead of returncategory: 'Visits', 'subcategory': 'Engagement'
the same structure as here with name, id and order:category: {id: '...', name: '...', order: '...'}
UI tests: