ReportAPI module
The purpose of this module is to provide admin reporting dashboards. A reporting dashboard consists of a bunch of widgets which show data filtered by date or a context.
Defining default report dashboards
See Config/reportapi_dashboards.php for default dashboards for different areas of the admin area. A dashboard must define the following
return [
'my_dash' => [
'enabled' => true, // If dash will show.
'layout' => ReportDashboardLayoutType::TWO_COLUMN, // See ReportDashboardLayoutType Enum for options.
'parent' => null, // The parentableContent or owner of the dash
'include_provides' => [ // What widgets are available to the dash. A widget defines what it provides.
ReportDashboardWidgetProvides::GLOBAL, // Global eg Html, group
ReportDashboardWidgetProvides::ANALYTICS, // Analytics widgets. Eg unique visits
ReportDashboardWidgetProvides::PARENTABLE, // Widgets that assume a parentable (or site wide). Eg event attendees
// ReportDashboardWidgetProvides::SUBMISSIONS, // Widgets that provide data about submissions. Eg input aggregate
],
'widgets' => [
// Widget array
]
]
Widgets
Widgets are auto discovered by being placed in Plugins/Widgets/Plugins and implementing WidgetInterface. Any new widget should really extend any of the base widgets:
BaseWidget- The most generic of widgetsBaseCountWidget- A count widget, displaying a number and optionally a trend chartBaseDynamicTableWiodget- A table of recordsBaseTimeSeriesWidget- A time series chart
By extending these widgets you should be able to add new widgets will all the structure handled and consistent. You should only need to define how the data is retrieved and additional settings. Helpers are provided for column selection and anything else that is reused more than once.
Widget default settings
When defining a widget class, you set the default settings. If not overridden these settings will be used with default dashboards. Example below will use defaults for missing settings such as size, variant and subject.
[
'id' => 100,
'type' => AnalyticsCount::MACHINE_NAME,
'settings' => [
'title' => 'Unique visitors',
'region' => ReportDashboardLayoutRegions::MAIN,
'metric' => AnalyticsRemoteFields::UNIQ_VISITORS,
],
],
Examples
At time of writing there is at least 10 different widget plugins across various modules. Look for a widget that is closest to what you want to achieve then follow the same pattern.
Modules with widget plugins
ContentApiAnalyticsApiDocumentsEventsSurveys
For each module, checkout the Plugins/Widgets/Plugins dirs.
Testing widgets
A number of helpers are available via Tests/Traits/WidgetPluginTestTrait.
executeWidgetGetData()will return the response from a widgetgetDataassertTableWidgetResponseCorrect()will check common props on a table widgetassertCountWidgetResponseCorrect()will check common props on a count widget
A Good example of testing a few widget types is WidgetPluginEventsTest
Dynamic table example
$settings = ['columns' => ['first_row_colum_name']];
$this->executeWidgetGetData(
TableWidget::class,
$settings,
fn ($response) => $this->assertTableWidgetResponseCorrect($response, $settings, $expectedCount, ['first_row_colum_name' => $first_row_colum_value])
);
Count example
$settings = ['show_chart' => true];
$this->executeWidgetGetData(
CountWidget::class,
$settings,
fn ($response) => $this->assertCountWidgetResponseCorrect($response, $settings, $expectedCount)
);
Saving dashboards
Saved dashboards use the Report model. These have practically the same structure as a report config. If a report dashboard has a matching report model then the dashboard will use that for its config, otherwise it will fall back to reportapi_dashboards config.
Getting the appropriate dashboard is done via static helper methods on the Report model
// Get a report without parent context (eg homepage) for the current user.
$report = Report::getCurrentUserReport('report_group_name');
// Get a report with a parent context (eg project/team report) for all users.
$contextModel = ModelThatImplementsHasReportDashboardInterface::find(1);
$report = Report::getCurrentParentReport($contextModel);
$groupName = Report::getParentGroup($contextModel);
If $report is null, fall back to config('reportapi_dashboards.GROUP_NAME')
Dashboards with parent context
Dashboards may have a parent context, this enables the parent to be passed down to widgets as a context filter. Widgets should implement the logic to filter by the context.
For a parent to have a report dashboard, the following is required
- Parent context model implements
HasReportDashboardInterface - A
reportapi_dashboards.GROUP_NAMEconfig withGROUP_NAMEbeing a singular version of table name. - Parent dashboard controller has the following methods
reportIndex,reportStore&reportDestroy. It should also implementReportDashboardControllerInterfaceand useReportDashboardControllerTrait. - Routes are defined, this can be done by nesting
ReportDashboardRouteService::registerDashboardRoutes($parentController) - Feature tests implement all assertions in
ReportDashboardFeatureTestTrait
Examples
Model
Models/ParentModel.php
class ParentModel extends BaseModel implements HasReportDashboardInterface
{
public function getActionUrlsAttribute(): array
{
// Add report route.
}
}
Config
Config/reportapi_dashboards.php
return [
'parent_model' => [
// fallback config/default dash.
]
]
Controller
Http/Controllers/DashboardParentModelController.php
class DashboardParentModelController extends BaseDashboardController implements ReportDashboardControllerInterface
{
use ReportDashboardControllerTrait;
public function reportIndex(ParentModel $parentModel): Response
{
return $this->renderDashboard(
$this->getParentReportConfiguration($parentModel),
$this->parentRoute,
$parentModel->title,
$parentModel->append(self::$globalAppends)
);
}
public function reportStore(ReportSaveRequest $request, ParentModel $parentModel): JsonResponse
{
return $this->saveDashboard($request, Report::getParentGroup($parentModel), Report::getCurrentParentReport($parentModel), $parentModel);
}
public function reportDestroy(ParentModel $parentModel): JsonResponse
{
return $this->deleteDashboard(Report::getCurrentParentReport($parentModel));
}
}
Routes
Routes/dash.php
Route::prefix('parent_model/{parent_model}/report')
->group(fn () => ReportDashboardRouteService::registerDashboardRoutes(DashboardParentModelController::class))
Tests
Tests/Feature/ReportDashboardTest.php
class ReportDashboardTest extends TestCase
{
use ReportDashboardFeatureTestTrait;
...
public function test_can_get_index_with_no_saved_report()
{
$this->assertViewNonSavedReportHasNoErrors();
}
...
}
See Comments in ReportDashboardFeatureTestTrait and HomepageReportDashboardTest for something you can duplicate.
Setting a parent context
This is hidden away in reportIndex via getParentReportConfiguration, the context is set on the DTO via setContext(). This context is passed all the way through to a widget.
Widgets need to implement context logic
Widgets have access to:
hasContext()if any context is setgetContext()get array of current context. This is lightweight, does no queries and returns the model class name (type) and model id.getContextLoaded()get array of fully loaded context models with original relationships loaded this is always cached as every widget in a report may request this.