Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
body:has(.s-dashboard) {
|
||||
background-color: var(--layer);
|
||||
--field: var(--field-02);
|
||||
--field-hover: var(--field-hover-02);
|
||||
}
|
||||
|
||||
.input-group-addon label {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.vector-map {
|
||||
height: 350px;
|
||||
color: transparent;
|
||||
}
|
||||
.jvectormap-container {
|
||||
height:100%;
|
||||
width:100%;
|
||||
}
|
||||
.dashboard-widgets .sortable-placeholder {
|
||||
background-color: #fff;
|
||||
border: 1px dashed #4e5d9d;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.dashboard-widgets.cards {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px;
|
||||
}
|
||||
.dashboard-widgets .tile {
|
||||
--layer: var(--background);
|
||||
margin: 5px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-moz-transition-property: background-color, box-shadow, -moz-transform, border;
|
||||
-o-transition-property: background-color, box-shadow, text-shadow, -o-transform, border;
|
||||
-webkit-transition-property: background-color, box-shadow, text-shadow, -webkit-transform, border;
|
||||
transition-property: background-color, box-shadow, text-shadow, transform, border;
|
||||
-moz-transition-duration: 0.2s;
|
||||
-o-transition-duration: 0.2s;
|
||||
-webkit-transition-duration: 0.2s;
|
||||
transition-duration: 0.2s;
|
||||
-moz-transition-timing-function: linear;
|
||||
-o-transition-timing-function: linear;
|
||||
-webkit-transition-timing-function: linear;
|
||||
transition-timing-function: linear;
|
||||
position: relative;
|
||||
}
|
||||
.dashboard-widgets .ui-sortable-helper .tile {
|
||||
box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 1px 0px inset, rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px;
|
||||
max-width: 170px;
|
||||
max-height: 170px;
|
||||
overflow: hidden;
|
||||
transform: rotate(1deg);
|
||||
-webkit-animation: squareToCircle 1.5s .5s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes squareToCircle {
|
||||
0% {border-radius: 0 0 0 0;}
|
||||
|
||||
100% {border-radius: 20px 20px 20px 20px;}
|
||||
}
|
||||
|
||||
.dashboard-widgets .widget-overlay {
|
||||
content: '';
|
||||
width: 101%;
|
||||
height: 101%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: var(--transition-all-expressive);
|
||||
backdrop-filter: blur(0);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
.dashboard-widgets .ui-sortable-helper .widget-overlay {
|
||||
backdrop-filter: blur(8px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dashboard-widgets h4 {
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.dashboard-widgets .dropdown-toggle .ri-more-2-fill {
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
color: var(--icon-secondary);
|
||||
transition: var(--transition-all-productive);
|
||||
width: 30px;
|
||||
margin: -10px;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
border-radius: var(--border-radius-md);
|
||||
}
|
||||
.dashboard-widgets .card-header h4 {
|
||||
width: 90%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.dashboard-widgets .tile table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.dashboard-widgets .tile table td {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.dashboard-widgets .tile table td:last-child {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.chart-wrapper .legend {
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: -17px;
|
||||
width: 100%;
|
||||
-moz-border-radius: 0 0 4px 4px;
|
||||
-webkit-border-radius: 0 0 4px 4px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
color: #fff;
|
||||
-webkit-transition: all 0.3s ease-in-out;
|
||||
}
|
||||
.chart-wrapper:hover .legend {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.chart-wrapper .legend ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.chart-wrapper .legend li {
|
||||
margin: 0 10px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.chart-wrapper .legend li span {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 8px;
|
||||
margin-right: 5px;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.dashboard-widgets .widget {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
#dashboard-widgets .spinner {
|
||||
text-align: center;
|
||||
line-height: 250px;
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
// DashboardBundle
|
||||
// Use absolute path to keep dashboard working when app is in subdir
|
||||
Mautic.widgetUrl = mauticBasePath + '/s/dashboard/widget/';
|
||||
|
||||
/**
|
||||
* @type jQuery DOM element to be replaced with spinner
|
||||
*/
|
||||
Mautic.dashboardSubmitButton = false; // Button text, to be get and shown instead of spinner
|
||||
|
||||
/**
|
||||
* Init dashboard events
|
||||
* @param container
|
||||
*/
|
||||
Mautic.dashboardOnLoad = function (container) {
|
||||
Mautic.loadWidgets();
|
||||
};
|
||||
|
||||
/**
|
||||
* Load all widgets on initial page render
|
||||
*/
|
||||
Mautic.loadWidgets = function () {
|
||||
Mautic.dashboardFilterPreventSubmit();
|
||||
|
||||
jQuery('.widget').each(function() {
|
||||
let widgetId = jQuery(this).attr('data-widget-id');
|
||||
let container = jQuery('.widget[data-widget-id="'+widgetId+'"]');
|
||||
jQuery.ajax({
|
||||
url: Mautic.widgetUrl+widgetId+'?ignoreAjax=true',
|
||||
}).done(function(response) {
|
||||
Mautic.widgetOnLoad(container, response);
|
||||
});
|
||||
});
|
||||
|
||||
jQuery(document).ajaxComplete(function(){
|
||||
Mautic.initDashboardFilter();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Init dashboard filter events after widget load
|
||||
*/
|
||||
Mautic.initDashboardFilter = function () {
|
||||
let form = jQuery('form[name="daterange"]');
|
||||
form.find('button')
|
||||
.replaceWith(Mautic.dashboardSubmitButton);
|
||||
form
|
||||
.unbind('submit')
|
||||
.on('submit', function(e){
|
||||
e.preventDefault();
|
||||
Mautic.dashboardFilterPreventSubmit();
|
||||
jQuery('.widget').each(function() {
|
||||
let widgetId = jQuery(this).attr('data-widget-id');
|
||||
let element = jQuery('.widget[data-widget-id="' + widgetId + '"]');
|
||||
jQuery.ajax({
|
||||
type: 'POST',
|
||||
url: Mautic.widgetUrl + widgetId + '?ignoreAjax=true',
|
||||
data: form.serializeArray(),
|
||||
success: function (response) {
|
||||
Mautic.widgetOnLoad(element, response);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevent filter from submit, show spinner instead of send button
|
||||
*/
|
||||
Mautic.dashboardFilterPreventSubmit = function() {
|
||||
let form = jQuery('form[name="daterange"]');
|
||||
let button = form.find('button:first');
|
||||
Mautic.dashboardSubmitButton = button.clone();
|
||||
button.width(button.width()+'px'); // Keep button width
|
||||
button.html('<i class="ri-loader-3-line ri-spin"></i>');
|
||||
jQuery('.widget').find('.card-body').html('<div class="spinner"><i class="ri-loader-3-line ri-spin"></i></div>');
|
||||
form
|
||||
.unbind('submit')
|
||||
.on('submit', function(e){
|
||||
e.preventDefault();
|
||||
});
|
||||
};
|
||||
|
||||
Mautic.dashboardOnUnload = function(id) {
|
||||
// Trash initialized dashboard vars on app content change.
|
||||
mQuery('.jvectormap-tip').remove();
|
||||
};
|
||||
|
||||
/**
|
||||
* Render widget from XHR to DOM
|
||||
*
|
||||
* @param container
|
||||
* @param response
|
||||
*/
|
||||
Mautic.widgetOnLoad = function(container, response) {
|
||||
if (!response.widgetId) return;
|
||||
// target in DOM
|
||||
var widget = mQuery('.widget[data-widget-id="' + response.widgetId + '"]');
|
||||
// source from response
|
||||
var widgetHtml = mQuery(response.widgetHtml);
|
||||
|
||||
// initialize edit button modal again
|
||||
widgetHtml.find("*[data-toggle='ajaxmodal']").on('click.ajaxmodal', function (event) {
|
||||
event.preventDefault();
|
||||
Mautic.ajaxifyModal(this, event);
|
||||
});
|
||||
|
||||
// Create the new widget wrapper and add it to the 0 position if doesn't exist (probably a new one)
|
||||
if (!widget.length) {
|
||||
widget = mQuery('<div/>')
|
||||
.addClass('widget')
|
||||
.attr('data-widget-id', response.widgetId);
|
||||
mQuery('#dashboard-widgets').prepend(widget);
|
||||
}
|
||||
|
||||
widget.html(widgetHtml)
|
||||
.css('width', response.widgetWidth + '%')
|
||||
.css('height', response.widgetHeight + '%');
|
||||
Mautic.renderCharts(widgetHtml);
|
||||
|
||||
const map = widgetHtml.find('.vector-map').first();
|
||||
if (map.length && !map.hasClass('map-rendered')) {
|
||||
Mautic.initMap(widgetHtml, 'regions');
|
||||
}
|
||||
|
||||
Mautic.initWidgetRemoveEvents();
|
||||
Mautic.initWidgetSorting();
|
||||
Mautic.initDashboardFilter();
|
||||
};
|
||||
|
||||
Mautic.initWidgetRemoveEvents = function () {
|
||||
jQuery('.remove-widget')
|
||||
.unbind('click')
|
||||
.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
element = jQuery(this);
|
||||
let url = element.attr('href');
|
||||
element.closest('.widget').remove();
|
||||
jQuery.ajax({
|
||||
url: url,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Mautic.initWidgetSorting = function () {
|
||||
var widgetsWrapper = mQuery('#dashboard-widgets');
|
||||
var bodyOverflow = {};
|
||||
|
||||
widgetsWrapper.sortable({
|
||||
handle: '.card-header h4',
|
||||
placeholder: 'sortable-placeholder',
|
||||
items: '.widget',
|
||||
opacity: 0.9,
|
||||
scroll: true,
|
||||
scrollSpeed: 10,
|
||||
tolerance: "pointer",
|
||||
cursor: 'move',
|
||||
appendTo: '#dashboard-widgets',
|
||||
|
||||
helper: function(e, ui) {
|
||||
// Ensure the draggable retains it's original size and that the margin doesn't cause things to bounce around
|
||||
ui.children().each(function() {
|
||||
mQuery(this).width(mQuery(this).width());
|
||||
mQuery(this).height(mQuery(this).height());
|
||||
});
|
||||
|
||||
// Fix body overflow that messes sortable up
|
||||
bodyOverflow.overflowX = mQuery('body').css('overflow-x');
|
||||
bodyOverflow.overflowY = mQuery('body').css('overflow-y');
|
||||
mQuery('body').css({
|
||||
overflowX: 'visible',
|
||||
overflowY: 'visible'
|
||||
});
|
||||
|
||||
mQuery("#dashboard-widgets .widget").each(function(i) {
|
||||
var item = mQuery(this);
|
||||
var item_clone = item.clone();
|
||||
|
||||
var canvas = item.find('canvas').first();
|
||||
if (canvas.length) {
|
||||
// Copy the canvas
|
||||
var destCanvas = item_clone.find('canvas').first();
|
||||
var destCtx = destCanvas[0].getContext('2d');
|
||||
destCtx.drawImage(canvas[0], 0, 0);
|
||||
}
|
||||
|
||||
item.data("clone", item_clone);
|
||||
var position = item.position();
|
||||
item_clone
|
||||
.css({
|
||||
left: position.left,
|
||||
top: position.top,
|
||||
width: item.width(),
|
||||
visibility: "visible",
|
||||
position: "absolute",
|
||||
zIndex: 1
|
||||
});
|
||||
|
||||
item.css('visibility', 'hidden');
|
||||
mQuery("#cloned-widgets").append(item_clone);
|
||||
});
|
||||
|
||||
return ui;
|
||||
},
|
||||
start: function(e, ui) {
|
||||
ui.helper.css('visibility', 'visible');
|
||||
ui.helper.data("clone").hide();
|
||||
},
|
||||
sort: function(e, ui) {
|
||||
var tile = ui.item.find('.tile').first();
|
||||
// Prevent margin from pushing the elements out of the way
|
||||
ui.placeholder.css({
|
||||
marginTop: "5px",
|
||||
marginBottom: "5px",
|
||||
marginLeft: 0,
|
||||
marginRight: 0
|
||||
});
|
||||
},
|
||||
stop: function() {
|
||||
// Restore original overflow
|
||||
mQuery('body').css(bodyOverflow);
|
||||
|
||||
mQuery("#dashboard-widgets .widget.exclude-me").each(function() {
|
||||
var item = mQuery(this);
|
||||
var clone = item.data("clone");
|
||||
var position = item.position();
|
||||
|
||||
clone.css("left", position.left);
|
||||
clone.css("top", position.top);
|
||||
clone.show();
|
||||
item.removeClass("exclude-me");
|
||||
});
|
||||
|
||||
mQuery("#dashboard-widgets .widget").css("visibility", "visible");
|
||||
mQuery("#cloned-widgets .widget").remove();
|
||||
|
||||
Mautic.saveWidgetSorting();
|
||||
},
|
||||
change: function(e, ui) {
|
||||
mQuery("#dashboard-widgets .widget:not(.exclude-me)").each(function() {
|
||||
var item = mQuery(this);
|
||||
var clone = item.data("clone");
|
||||
clone.stop(true, false);
|
||||
var position = item.position();
|
||||
clone.animate({
|
||||
left: position.left,
|
||||
top: position.top
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
}).disableSelection();
|
||||
}
|
||||
|
||||
Mautic.saveWidgetSorting = function () {
|
||||
var widgetsWrapper = mQuery('#dashboard-widgets');
|
||||
var widgets = widgetsWrapper.children();
|
||||
var ordering = [];
|
||||
widgets.each(function(index, value) {
|
||||
ordering.push(mQuery(this).attr('data-widget-id'));
|
||||
});
|
||||
|
||||
Mautic.ajaxActionRequest('dashboard:updateWidgetOrdering', {'ordering': ordering}, function(response) {
|
||||
// @todo handle errors
|
||||
});
|
||||
}
|
||||
|
||||
Mautic.updateWidgetForm = function (element) {
|
||||
Mautic.activateLabelLoadingIndicator('widget_type');
|
||||
var formWrapper = mQuery(element).closest('form');
|
||||
var WidgetFormValues = formWrapper.serializeArray();
|
||||
Mautic.ajaxActionRequest('dashboard:updateWidgetForm', WidgetFormValues, function(response) {
|
||||
if (response.formHtml) {
|
||||
var formHtml = mQuery(response.formHtml);
|
||||
formHtml.find('#widget_buttons').addClass('hide hidden');
|
||||
formWrapper.html(formHtml.children());
|
||||
Mautic.onPageLoad('#widget_params');
|
||||
}
|
||||
Mautic.removeLabelLoadingIndicator();
|
||||
});
|
||||
};
|
||||
|
||||
Mautic.exportDashboardLayout = function(text, baseUrl) {
|
||||
var name = prompt(text, "");
|
||||
|
||||
if (name !== null) {
|
||||
if (name) {
|
||||
baseUrl = baseUrl + "?name=" + encodeURIComponent(name);
|
||||
}
|
||||
|
||||
window.location = baseUrl;
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.saveDashboardLayout = function(text) {
|
||||
var name = prompt(text, "");
|
||||
|
||||
if (name) {
|
||||
mQuery.ajax({
|
||||
type: 'POST',
|
||||
url: mauticBaseUrl+'s/dashboard/save',
|
||||
data: {name: name}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.setDateRange = function(option) {
|
||||
var today = new Date();
|
||||
var fromDate, toDate;
|
||||
|
||||
switch(option) {
|
||||
case 'today':
|
||||
fromDate = today;
|
||||
toDate = today;
|
||||
break;
|
||||
case 'yesterday':
|
||||
fromDate = new Date(today.getTime() - (24 * 60 * 60 * 1000));
|
||||
toDate = fromDate;
|
||||
break;
|
||||
default:
|
||||
if (typeof option === 'number') {
|
||||
fromDate = new Date(today.getTime() - (option * 24 * 60 * 60 * 1000));
|
||||
toDate = today;
|
||||
} else {
|
||||
console.error('Invalid option');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('daterange_date_from').value = Mautic.formatDate(fromDate);
|
||||
document.getElementById('daterange_date_to').value = Mautic.formatDate(toDate);
|
||||
document.getElementById('daterange_apply').click();
|
||||
};
|
||||
|
||||
Mautic.formatDate = function(date) {
|
||||
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
||||
];
|
||||
|
||||
return monthNames[date.getMonth()] + " " + date.getDate() + ", " + date.getFullYear();
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_dashboard_index' => [
|
||||
'path' => '/dashboard',
|
||||
'controller' => 'Mautic\DashboardBundle\Controller\DashboardController::indexAction',
|
||||
],
|
||||
'mautic_dashboard_widget' => [
|
||||
'path' => '/dashboard/widget/{widgetId}',
|
||||
'controller' => 'Mautic\DashboardBundle\Controller\DashboardController::widgetAction',
|
||||
],
|
||||
'mautic_dashboard_action' => [
|
||||
'path' => '/dashboard/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\DashboardBundle\Controller\DashboardController::executeAction',
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'mautic_widget_types' => [
|
||||
'path' => '/data',
|
||||
'controller' => 'Mautic\DashboardBundle\Controller\Api\WidgetApiController::getTypesAction',
|
||||
],
|
||||
'mautic_widget_data' => [
|
||||
'path' => '/data/{type}',
|
||||
'controller' => 'Mautic\DashboardBundle\Controller\Api\WidgetApiController::getDataAction',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'menu' => [
|
||||
'main' => [
|
||||
'priority' => 100,
|
||||
'items' => [
|
||||
'mautic.dashboard.menu.index' => [
|
||||
'route' => 'mautic_dashboard_index',
|
||||
'iconClass' => 'ri-funds-fill',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'services' => [
|
||||
'other' => [
|
||||
'mautic.dashboard.widget' => [
|
||||
'class' => Mautic\DashboardBundle\Dashboard\Widget::class,
|
||||
'arguments' => [
|
||||
'mautic.dashboard.model.dashboard',
|
||||
'mautic.helper.user',
|
||||
'request_stack',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'parameters' => [
|
||||
'dashboard_import_dir' => '%mautic.application_dir%/app/assets/dashboards',
|
||||
'dashboard_import_user_dir' => '%mautic.application_dir%/media/dashboards',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
];
|
||||
|
||||
$services->load('Mautic\\DashboardBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\DashboardBundle\\Entity\\', '../Entity/*Repository.php');
|
||||
$services->alias('mautic.dashboard.model.dashboard', Mautic\DashboardBundle\Model\DashboardModel::class);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Controller;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Mautic\DashboardBundle\Form\Type\WidgetType;
|
||||
use Mautic\DashboardBundle\Model\DashboardModel;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
/**
|
||||
* Count how many visitors are currently viewing a page.
|
||||
*/
|
||||
public function viewingVisitorsAction(EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
|
||||
/** @var \Mautic\PageBundle\Entity\PageRepository $pageRepository */
|
||||
$pageRepository = $entityManager->getRepository(Hit::class);
|
||||
$dataArray['viewingVisitors'] = $pageRepository->countVisitors(60, true);
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML of a new widget based on its values.
|
||||
*/
|
||||
public function updateWidgetFormAction(Request $request, FormFactoryInterface $formFactory): JsonResponse
|
||||
{
|
||||
$data = $request->request->all()['widget'] ?? [];
|
||||
$dataArray = ['success' => 0];
|
||||
|
||||
// Clear params if type is not selected
|
||||
if (empty($data['type'])) {
|
||||
unset($data['params']);
|
||||
}
|
||||
|
||||
$widget = new Widget();
|
||||
$form = $formFactory->create(WidgetType::class, $widget);
|
||||
$formHtml = $this->render('@MauticDashboard/Widget/form.html.twig',
|
||||
['form' => $form->submit($data)->createView()]
|
||||
)->getContent();
|
||||
|
||||
$dataArray['formHtml'] = $formHtml;
|
||||
$dataArray['success'] = 1;
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the new ordering of dashboard widgets.
|
||||
*/
|
||||
public function updateWidgetOrderingAction(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->request->all()['ordering'] ?? [];
|
||||
$dashboardModel = $this->getModel('dashboard');
|
||||
\assert($dashboardModel instanceof DashboardModel);
|
||||
$repo = $dashboardModel->getRepository();
|
||||
$repo->updateOrdering(array_flip($data), $this->user->getId());
|
||||
$dataArray = ['success' => 1];
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*/
|
||||
public function deleteAction(Request $request): JsonResponse
|
||||
{
|
||||
$objectId = $request->request->get('widget');
|
||||
$dataArray = ['success' => 0];
|
||||
|
||||
// @todo: build permissions
|
||||
// if (!$this->security->isGranted('dashobard:widgets:delete')) {
|
||||
// return $this->accessDenied();
|
||||
// }
|
||||
|
||||
/** @var DashboardModel $model */
|
||||
$model = $this->getModel('dashboard');
|
||||
$entity = $model->getEntity($objectId);
|
||||
if ($entity) {
|
||||
$model->deleteEntity($entity);
|
||||
$name = $entity->getName();
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\DashboardBundle\DashboardEvents;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Mautic\DashboardBundle\Event\WidgetTypeListEvent;
|
||||
use Mautic\DashboardBundle\Model\DashboardModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Widget>
|
||||
*/
|
||||
class WidgetApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* @var DashboardModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$dashboardModel = $modelFactory->getModel('dashboard');
|
||||
\assert($dashboardModel instanceof DashboardModel);
|
||||
|
||||
$this->model = $dashboardModel;
|
||||
$this->entityClass = Widget::class;
|
||||
$this->entityNameOne = 'widget';
|
||||
$this->entityNameMulti = 'widgets';
|
||||
$this->serializerGroups = [];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of available widget types.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getTypesAction()
|
||||
{
|
||||
$dispatcher = $this->dispatcher;
|
||||
$event = new WidgetTypeListEvent();
|
||||
$event->setTranslator($this->translator);
|
||||
$dispatcher->dispatch($event, DashboardEvents::DASHBOARD_ON_MODULE_LIST_GENERATE);
|
||||
$view = $this->view(['success' => 1, 'types' => $event->getTypes()], Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of available widget types.
|
||||
*
|
||||
* @param string $type of the widget
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getDataAction(Request $request, $type)
|
||||
{
|
||||
$start = microtime(true);
|
||||
$timezone = InputHelper::clean($request->get('timezone', null));
|
||||
$from = InputHelper::clean($request->get('dateFrom', null));
|
||||
$to = InputHelper::clean($request->get('dateTo', null));
|
||||
$dataFormat = InputHelper::clean($request->get('dataFormat', null));
|
||||
$unit = InputHelper::clean($request->get('timeUnit', 'Y'));
|
||||
$dataset = InputHelper::clean($request->query->all()['dataset'] ?? $request->request->all()['dataset'] ?? []);
|
||||
$response = ['success' => 0];
|
||||
|
||||
try {
|
||||
DateTimeHelper::validateMysqlDateTimeUnit($unit);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->returnError($e->getMessage(), Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if ($timezone) {
|
||||
$fromDate = new \DateTime($from, new \DateTimeZone($timezone));
|
||||
$toDate = new \DateTime($to, new \DateTimeZone($timezone));
|
||||
} else {
|
||||
$fromDate = new \DateTime($from);
|
||||
$toDate = new \DateTime($to);
|
||||
}
|
||||
|
||||
$params = [
|
||||
'timeUnit' => InputHelper::clean($request->get('timeUnit', 'Y')),
|
||||
'dateFormat' => InputHelper::clean($request->get('dateFormat', null)),
|
||||
'dateFrom' => $fromDate,
|
||||
'dateTo' => $toDate,
|
||||
'limit' => (int) $request->get('limit', null),
|
||||
'filter' => InputHelper::clean($request->query->all()['filter'] ?? $request->request->all()['filter'] ?? []),
|
||||
'dataset' => $dataset,
|
||||
];
|
||||
|
||||
// Merge filters into the root array as well as that's how widget edit forms send them.
|
||||
$params = array_merge($params, $params['filter']);
|
||||
|
||||
$cacheTimeout = (int) $request->get('cacheTimeout', 0);
|
||||
$widgetHeight = (int) $request->get('height', 300);
|
||||
|
||||
$widget = new Widget();
|
||||
$widget->setParams($params);
|
||||
$widget->setType($type);
|
||||
$widget->setHeight($widgetHeight);
|
||||
$widget->setCacheTimeout($cacheTimeout);
|
||||
|
||||
$this->model->populateWidgetContent($widget);
|
||||
$data = $widget->getTemplateData();
|
||||
|
||||
if (!$data) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if ('raw' == $dataFormat) {
|
||||
if (isset($data['chartData']['labels']) && isset($data['chartData']['datasets'])) {
|
||||
$rawData = [];
|
||||
foreach ($data['chartData']['datasets'] as $dataset) {
|
||||
$rawData[$dataset['label']] = [];
|
||||
foreach ($dataset['data'] as $key => $value) {
|
||||
$rawData[$dataset['label']][$data['chartData']['labels'][$key]] = $value;
|
||||
}
|
||||
}
|
||||
$data = $rawData;
|
||||
} elseif (isset($data['raw'])) {
|
||||
$data = $data['raw'];
|
||||
}
|
||||
} else {
|
||||
if (isset($data['raw'])) {
|
||||
unset($data['raw']);
|
||||
}
|
||||
}
|
||||
|
||||
$response['cached'] = $widget->isCached();
|
||||
$response['execution_time'] = microtime(true) - $start;
|
||||
$response['success'] = 1;
|
||||
$response['data'] = $data;
|
||||
|
||||
$view = $this->view($response, Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AbstractFormController;
|
||||
use Mautic\CoreBundle\Form\Type\DateRangeType;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\CoreBundle\Helper\PhpVersionHelper;
|
||||
use Mautic\CoreBundle\Release\ThisRelease;
|
||||
use Mautic\DashboardBundle\Dashboard\Widget as WidgetService;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Mautic\DashboardBundle\Form\Type\UploadType;
|
||||
use Mautic\DashboardBundle\Model\DashboardModel;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
class DashboardController extends AbstractFormController
|
||||
{
|
||||
/**
|
||||
* Generates the default view.
|
||||
*/
|
||||
public function indexAction(Request $request, WidgetService $widget, FormFactoryInterface $formFactory, PathsHelper $pathsHelper, RouterInterface $urlGenerator): Response
|
||||
{
|
||||
$model = $this->getModel('dashboard');
|
||||
\assert($model instanceof DashboardModel);
|
||||
$widgets = $model->getWidgets();
|
||||
|
||||
// Apply the default dashboard if no widget exists
|
||||
if (!count($widgets) && $this->user->getId()) {
|
||||
return $this->applyDashboardFileAction($request, $pathsHelper, $urlGenerator, 'global.default');
|
||||
}
|
||||
|
||||
$action = $this->generateUrl('mautic_dashboard_index');
|
||||
$dateRangeFilter = $request->query->all()['daterange'] ?? $request->request->all()['daterange'] ?? [];
|
||||
|
||||
// Set new date range to the session
|
||||
if ($request->isMethod(Request::METHOD_POST)) {
|
||||
if (!empty($dateRangeFilter['date_from'])) {
|
||||
$from = new \DateTime($dateRangeFilter['date_from']);
|
||||
$request->getSession()->set('mautic.daterange.form.from', $from->format(DateTimeHelper::FORMAT_DB_DATE_ONLY));
|
||||
}
|
||||
|
||||
if (!empty($dateRangeFilter['date_to'])) {
|
||||
$to = new \DateTime($dateRangeFilter['date_to']);
|
||||
$request->getSession()->set('mautic.daterange.form.to', $to->format(DateTimeHelper::FORMAT_DB_DATE_ONLY.' 23:59:59'));
|
||||
}
|
||||
|
||||
$model->clearDashboardCache();
|
||||
}
|
||||
|
||||
// Set new date range to the session, if present in POST
|
||||
$widget->setFilter($request);
|
||||
|
||||
// Load date range from session
|
||||
$filter = $model->getDefaultFilter();
|
||||
|
||||
// Set the final date range to the form
|
||||
$dateRangeFilter['date_from'] = $filter['dateFrom']->format(WidgetService::FORMAT_HUMAN);
|
||||
$dateRangeFilter['date_to'] = $filter['dateTo']->format(WidgetService::FORMAT_HUMAN);
|
||||
$dateRangeForm = $formFactory->create(DateRangeType::class, $dateRangeFilter, ['action' => $action]);
|
||||
|
||||
$model->populateWidgetsContent($widgets, $filter);
|
||||
$releaseMetadata = ThisRelease::getMetadata();
|
||||
|
||||
$model->populateWidgetPreviews($widgets);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'security' => $this->security,
|
||||
'widgets' => $widgets,
|
||||
'dateRangeForm' => $dateRangeForm->createView(),
|
||||
'phpVersion' => [
|
||||
'isOutdated' => version_compare(PHP_VERSION, $releaseMetadata->getShowPHPVersionWarningIfUnder(), 'lt'),
|
||||
'version' => PhpVersionHelper::getCurrentSemver(),
|
||||
],
|
||||
],
|
||||
'contentTemplate' => '@MauticDashboard/Dashboard/index.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_dashboard_index',
|
||||
'mauticContent' => 'dashboard',
|
||||
'route' => $this->generateUrl('mautic_dashboard_index'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function widgetAction(Request $request, WidgetService $widgetService, Environment $twig, $widgetId): JsonResponse
|
||||
{
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
throw new NotFoundHttpException('Not found.');
|
||||
}
|
||||
|
||||
$widgetService->setFilter($request);
|
||||
$widget = $widgetService->get((int) $widgetId);
|
||||
|
||||
if (!$widget) {
|
||||
throw new NotFoundHttpException('Not found.');
|
||||
}
|
||||
|
||||
$content = $twig->render(
|
||||
'@MauticDashboard/Dashboard/widget.html.twig',
|
||||
['widget' => $widget]
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => 1,
|
||||
'widgetId' => $widgetId,
|
||||
'widgetHtml' => $content,
|
||||
'widgetWidth' => $widget->getWidth(),
|
||||
'widgetHeight' => $widget->getHeight(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new dashboard widget and processes post data.
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, FormFactoryInterface $formFactory)
|
||||
{
|
||||
// retrieve the entity
|
||||
$widget = new Widget();
|
||||
|
||||
$model = $this->getModel('dashboard');
|
||||
\assert($model instanceof DashboardModel);
|
||||
$action = $this->generateUrl('mautic_dashboard_action', ['objectAction' => 'new']);
|
||||
|
||||
// get the user form factory
|
||||
$form = $model->createForm($widget, $formFactory, $action);
|
||||
$closeModal = false;
|
||||
$valid = false;
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if ($request->isMethod(Request::METHOD_POST)) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$closeModal = true;
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($widget);
|
||||
}
|
||||
} else {
|
||||
$closeModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($closeModal) {
|
||||
// just close the modal
|
||||
$passthroughVars = [
|
||||
'closeModal' => 1,
|
||||
'mauticContent' => 'widget',
|
||||
];
|
||||
|
||||
$filter = $model->getDefaultFilter();
|
||||
$model->populateWidgetContent($widget, $filter);
|
||||
|
||||
if ($valid && !$cancelled) {
|
||||
$passthroughVars['upWidgetCount'] = 1;
|
||||
$passthroughVars['widgetHtml'] = $this->renderView('@MauticDashboard/Widget/detail.html.twig', [
|
||||
'widget' => $widget,
|
||||
]);
|
||||
$passthroughVars['widgetId'] = $widget->getId();
|
||||
$passthroughVars['widgetWidth'] = $widget->getWidth();
|
||||
$passthroughVars['widgetHeight'] = $widget->getHeight();
|
||||
}
|
||||
|
||||
return new JsonResponse($passthroughVars);
|
||||
} else {
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticDashboard/Widget/form.html.twig',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* edit widget and processes post data.
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, FormFactoryInterface $formFactory, $objectId)
|
||||
{
|
||||
$model = $this->getModel('dashboard');
|
||||
\assert($model instanceof DashboardModel);
|
||||
$widget = $model->getEntity($objectId);
|
||||
$action = $this->generateUrl('mautic_dashboard_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
|
||||
// get the user form factory
|
||||
$form = $model->createForm($widget, $formFactory, $action);
|
||||
$closeModal = false;
|
||||
$valid = false;
|
||||
// /Check for a submitted form and process it
|
||||
if ($request->isMethod(Request::METHOD_POST)) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$closeModal = true;
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($widget);
|
||||
}
|
||||
} else {
|
||||
$closeModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($closeModal) {
|
||||
// just close the modal
|
||||
$passthroughVars = [
|
||||
'closeModal' => 1,
|
||||
'mauticContent' => 'widget',
|
||||
];
|
||||
|
||||
$filter = $model->getDefaultFilter();
|
||||
$model->populateWidgetContent($widget, $filter);
|
||||
|
||||
if ($valid && !$cancelled) {
|
||||
$passthroughVars['upWidgetCount'] = 1;
|
||||
$passthroughVars['widgetHtml'] = $this->renderView('@MauticDashboard/Widget/detail.html.twig', [
|
||||
'widget' => $widget,
|
||||
]);
|
||||
$passthroughVars['widgetId'] = $widget->getId();
|
||||
$passthroughVars['widgetWidth'] = $widget->getWidth();
|
||||
$passthroughVars['widgetHeight'] = $widget->getHeight();
|
||||
}
|
||||
|
||||
return new JsonResponse($passthroughVars);
|
||||
} else {
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticDashboard/Widget/form.html.twig',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes entity if exists.
|
||||
*
|
||||
* @param int $objectId
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId): Response
|
||||
{
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
throw new BadRequestHttpException();
|
||||
}
|
||||
|
||||
$flashes = [];
|
||||
$success = 0;
|
||||
|
||||
/** @var DashboardModel $model */
|
||||
$model = $this->getModel('dashboard');
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if ($entity) {
|
||||
$model->deleteEntity($entity);
|
||||
$name = $entity->getName();
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $name,
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
$success = 1;
|
||||
} else {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.api.client.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'success' => $success,
|
||||
'flashes' => $flashes,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the widgets of current user into a json and stores it for later as a file.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function saveAction(Request $request)
|
||||
{
|
||||
// Accept only AJAX POST requests because those are check for CSRF tokens
|
||||
if (!$request->isMethod(Request::METHOD_POST) || !$request->isXmlHttpRequest()) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$name = $this->getNameFromRequest($request);
|
||||
|
||||
/** @var DashboardModel $dashboardModel */
|
||||
$dashboardModel = $this->getModel('dashboard');
|
||||
try {
|
||||
$dashboardModel->saveSnapshot($name);
|
||||
$type = 'notice';
|
||||
$msg = $this->translator->trans('mautic.dashboard.notice.save', [
|
||||
'%name%' => $name,
|
||||
'%viewUrl%' => $this->generateUrl(
|
||||
'mautic_dashboard_action',
|
||||
[
|
||||
'objectAction' => 'import',
|
||||
]
|
||||
),
|
||||
], 'flashes');
|
||||
} catch (IOException $e) {
|
||||
$type = 'error';
|
||||
$msg = $this->translator->trans('mautic.dashboard.error.save', [
|
||||
'%msg%' => $e->getMessage(),
|
||||
], 'flashes');
|
||||
}
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => $type,
|
||||
'msg' => $msg,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the widgets of current user into a json file and downloads it.
|
||||
*/
|
||||
public function exportAction(Request $request): JsonResponse
|
||||
{
|
||||
$dashboardModel = $this->getModel('dashboard');
|
||||
\assert($dashboardModel instanceof DashboardModel);
|
||||
$filename = InputHelper::filename($this->getNameFromRequest($request), 'json');
|
||||
$response = new JsonResponse($dashboardModel->toArray($filename));
|
||||
$response->setEncodingOptions($response->getEncodingOptions() | JSON_PRETTY_PRINT);
|
||||
$response->headers->set('Content-Type', 'application/force-download');
|
||||
$response->headers->set('Content-Type', 'application/octet-stream');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="'.$filename.'"');
|
||||
$response->headers->set('Expires', '0');
|
||||
$response->headers->set('Cache-Control', 'must-revalidate');
|
||||
$response->headers->set('Pragma', 'public');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the widgets of current user into a json file.
|
||||
*/
|
||||
public function deleteDashboardFileAction(Request $request, PathsHelper $pathsHelper): RedirectResponse
|
||||
{
|
||||
$file = $request->get('file');
|
||||
|
||||
$parts = explode('.', $file);
|
||||
$type = array_shift($parts);
|
||||
$name = implode('.', $parts);
|
||||
|
||||
$dir = $pathsHelper->getSystemPath("dashboard.$type");
|
||||
$path = $dir.'/'.$name.'.json';
|
||||
|
||||
if (file_exists($path) && is_writable($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('mautic_dashboard_action', ['objectAction' => 'import']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies dashboard layout.
|
||||
*
|
||||
* @param string|null $file
|
||||
*/
|
||||
public function applyDashboardFileAction(Request $request, PathsHelper $pathsHelper, RouterInterface $urlGenerator, $file = null): RedirectResponse
|
||||
{
|
||||
if (!$file) {
|
||||
$file = $request->get('file');
|
||||
}
|
||||
|
||||
$parts = explode('.', $file);
|
||||
$type = array_shift($parts);
|
||||
$name = implode('.', $parts);
|
||||
|
||||
$dir = $pathsHelper->getSystemPath("dashboard.$type");
|
||||
$path = $dir.'/'.$name.'.json';
|
||||
|
||||
if (!file_exists($path) || !is_readable($path)) {
|
||||
$this->addFlashMessage('mautic.dashboard.upload.filenotfound', [], 'error', 'validators');
|
||||
|
||||
return $this->redirectToRoute('mautic_dashboard_action', ['objectAction' => 'import']);
|
||||
}
|
||||
|
||||
$widgets = json_decode(file_get_contents($path), true);
|
||||
if (isset($widgets['widgets'])) {
|
||||
$widgets = $widgets['widgets'];
|
||||
}
|
||||
|
||||
if ($widgets) {
|
||||
/** @var DashboardModel $model */
|
||||
$model = $this->getModel('dashboard');
|
||||
|
||||
$model->clearDashboardCache();
|
||||
|
||||
$currentWidgets = $model->getWidgets();
|
||||
|
||||
if (count($currentWidgets)) {
|
||||
foreach ($currentWidgets as $widget) {
|
||||
$model->deleteEntity($widget);
|
||||
}
|
||||
}
|
||||
|
||||
$filter = $model->getDefaultFilter();
|
||||
foreach ($widgets as $widget) {
|
||||
$widget = $model->populateWidgetEntity($widget);
|
||||
$model->saveEntity($widget);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirect($urlGenerator->generate('mautic_dashboard_index'));
|
||||
}
|
||||
|
||||
public function importAction(Request $request, FormFactoryInterface $formFactory, PathsHelper $pathsHelper): Response
|
||||
{
|
||||
$preview = $request->get('preview');
|
||||
|
||||
/** @var DashboardModel $model */
|
||||
$model = $this->getModel('dashboard');
|
||||
|
||||
$directories = [
|
||||
'user' => $pathsHelper->getSystemPath('dashboard.user'),
|
||||
'global' => $pathsHelper->getSystemPath('dashboard.global'),
|
||||
];
|
||||
|
||||
$action = $this->generateUrl('mautic_dashboard_action', ['objectAction' => 'import']);
|
||||
$form = $formFactory->create(UploadType::class, [], ['action' => $action]);
|
||||
|
||||
if ($request->isMethod(Request::METHOD_POST)) {
|
||||
if (!$this->isFormCancelled($form)) {
|
||||
if ($this->isFormValid($form)) {
|
||||
$fileData = $form['file']->getData();
|
||||
if (!empty($fileData)) {
|
||||
$extension = pathinfo($fileData->getClientOriginalName(), PATHINFO_EXTENSION);
|
||||
if ('json' === $extension) {
|
||||
$fileData->move($directories['user'], $fileData->getClientOriginalName());
|
||||
} else {
|
||||
$form->addError(
|
||||
new FormError(
|
||||
$this->translator->trans('mautic.core.not.allowed.file.extension', ['%extension%' => $extension], 'validators')
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$form->addError(
|
||||
new FormError(
|
||||
$this->translator->trans('mautic.dashboard.upload.filenotfound', [], 'validators')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dashboardFiles = ['user' => [], 'gobal' => []];
|
||||
$dashboards = [];
|
||||
|
||||
if (is_readable($directories['user'])) {
|
||||
// User specific layouts
|
||||
chdir($directories['user']);
|
||||
$dashboardFiles['user'] = glob('*.json');
|
||||
}
|
||||
|
||||
if (is_readable($directories['global'])) {
|
||||
// Global dashboards
|
||||
chdir($directories['global']);
|
||||
$dashboardFiles['global'] = glob('*.json');
|
||||
}
|
||||
|
||||
foreach ($dashboardFiles as $type => $dirDashboardFiles) {
|
||||
$tempDashboard = [];
|
||||
foreach ($dirDashboardFiles as $dashId => $dashboard) {
|
||||
$dashboard = str_replace('.json', '', $dashboard);
|
||||
$config = json_decode(
|
||||
file_get_contents($directories[$type].'/'.$dirDashboardFiles[$dashId]),
|
||||
true
|
||||
);
|
||||
|
||||
// Check for name, description, etc
|
||||
$tempDashboard[$dashboard] = [
|
||||
'type' => $type,
|
||||
'name' => $config['name'] ?? $dashboard,
|
||||
'description' => $config['description'] ?? '',
|
||||
'widgets' => $config['widgets'] ?? $config,
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
uasort($tempDashboard,
|
||||
fn ($a, $b): int => strnatcasecmp($a['name'], $b['name'])
|
||||
);
|
||||
|
||||
$dashboards = array_merge(
|
||||
$dashboards,
|
||||
$tempDashboard
|
||||
);
|
||||
}
|
||||
|
||||
if ($preview && isset($dashboards[$preview])) {
|
||||
// @todo check is_writable
|
||||
$widgets = $dashboards[$preview]['widgets'];
|
||||
$filter = $model->getDefaultFilter();
|
||||
$model->populateWidgetsContent($widgets, $filter);
|
||||
} else {
|
||||
$widgets = [];
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'dashboards' => $dashboards,
|
||||
'widgets' => $widgets,
|
||||
'preview' => $preview,
|
||||
],
|
||||
'contentTemplate' => '@MauticDashboard/Dashboard/import.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_dashboard_index',
|
||||
'mauticContent' => 'dashboardImport',
|
||||
'route' => $this->generateUrl(
|
||||
'mautic_dashboard_action',
|
||||
[
|
||||
'objectAction' => 'import',
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets name from request and defaults it to the timestamp if not provided.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getNameFromRequest(Request $request): string
|
||||
{
|
||||
return $request->get('name', (new \DateTime())->format('Y-m-dTH:i:s'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Dashboard;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\DashboardBundle\Model\DashboardModel;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
class Widget
|
||||
{
|
||||
public const FORMAT_HUMAN = 'M j, Y';
|
||||
|
||||
public function __construct(
|
||||
private DashboardModel $dashboardModel,
|
||||
private UserHelper $userHelper,
|
||||
private RequestStack $requestStack,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ready widget to populate in template.
|
||||
*
|
||||
* @return bool|\Mautic\DashboardBundle\Entity\Widget
|
||||
*/
|
||||
public function get(int $widgetId)
|
||||
{
|
||||
/** @var \Mautic\DashboardBundle\Entity\Widget $widget */
|
||||
$widget = $this->dashboardModel->getEntity($widgetId);
|
||||
|
||||
if (null === $widget || !$widget->getId()) {
|
||||
throw new NotFoundHttpException('Not found.');
|
||||
}
|
||||
|
||||
if ($widget->getCreatedBy() !== $this->userHelper->getUser()->getId()) {
|
||||
// Unauthorized access
|
||||
throw new AccessDeniedException();
|
||||
}
|
||||
|
||||
$filter = $this->dashboardModel->getDefaultFilter();
|
||||
|
||||
$this->dashboardModel->populateWidgetContent($widget, $filter);
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set filter from POST to session.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setFilter(Request $request): void
|
||||
{
|
||||
if (!$request->isMethod(Request::METHOD_POST)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateRangeFilter = $request->query->all()['daterange'] ?? $request->request->all()['daterange'] ?? [];
|
||||
|
||||
if (!empty($dateRangeFilter['date_from'])) {
|
||||
$from = new \DateTime($dateRangeFilter['date_from']);
|
||||
$this->requestStack->getSession()->set('mautic.daterange.form.from', $from->format(DateTimeHelper::FORMAT_DB_DATE_ONLY));
|
||||
}
|
||||
|
||||
if (!empty($dateRangeFilter['date_to'])) {
|
||||
$to = new \DateTime($dateRangeFilter['date_to']);
|
||||
$this->requestStack->getSession()->set('mautic.daterange.form.to', $to->format(DateTimeHelper::FORMAT_DB_DATE_ONLY));
|
||||
}
|
||||
|
||||
$this->dashboardModel->clearDashboardCache();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle;
|
||||
|
||||
/**
|
||||
* Events available for DashboardBundle.
|
||||
*/
|
||||
final class DashboardEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.dashboard_on_widget_list_generate event is dispatched when generating a list of available widget types.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DashboardBundle\Event\WidgetTypeListEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const DASHBOARD_ON_MODULE_LIST_GENERATE = 'mautic.dashboard_on_widget_list_generate';
|
||||
|
||||
/**
|
||||
* The mautic.dashboard_on_widget_form_generate event is dispatched when generating the form of a widget type.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DashboardBundle\Event\WidgetFormEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const DASHBOARD_ON_MODULE_FORM_GENERATE = 'mautic.dashboard_on_widget_form_generate';
|
||||
|
||||
/**
|
||||
* The mautic.dashboard_on_widget_detail_generate event is dispatched when generating the detail of a widget type.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DashboardBundle\Event\WidgetDetailEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const DASHBOARD_ON_MODULE_DETAIL_GENERATE = 'mautic.dashboard_on_widget_detail_generate';
|
||||
|
||||
/**
|
||||
* The mautic.dashboard_on_widget_detail_pre_load event is dispatched before detail of a widget type is generate.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DashboardBundle\Event\WidgetDetailEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const DASHBOARD_ON_MODULE_DETAIL_PRE_LOAD = 'mautic.dashboard_on_widget_detail_pre_load';
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DashboardBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticDashboardExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
class Widget extends FormEntity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $width;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $height;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $ordering;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $params = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $template;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $errorMessage;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $cached = false;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $loadTime = 0;
|
||||
|
||||
/**
|
||||
* @var int|null (minutes)
|
||||
*/
|
||||
private $cacheTimeout;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $templateData = [];
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('widgets');
|
||||
$builder->setCustomRepositoryClass(WidgetRepository::class);
|
||||
$builder->addIdColumns('name', false);
|
||||
$builder->addField('type', Types::STRING);
|
||||
$builder->addField('width', Types::INTEGER);
|
||||
$builder->addField('height', Types::INTEGER);
|
||||
$builder->addNullableField('cacheTimeout', Types::INTEGER, 'cache_timeout');
|
||||
$builder->addNullableField('ordering', Types::INTEGER);
|
||||
$builder->addNullableField('params', Types::ARRAY);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('type', new NotBlank([
|
||||
'message' => 'mautic.core.type.required',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = InputHelper::string($name);
|
||||
$this->isChanged('name', $this->name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = InputHelper::string($type);
|
||||
$this->isChanged('type', $this->type);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set width.
|
||||
*
|
||||
* @param int $width
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setWidth($width)
|
||||
{
|
||||
$this->width = (int) $width;
|
||||
$this->isChanged('width', $this->width);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get width.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getWidth()
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set height.
|
||||
*
|
||||
* @param int $height
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setHeight($height)
|
||||
{
|
||||
$this->height = (int) $height;
|
||||
$this->isChanged('height', $this->height);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache timeout.
|
||||
*
|
||||
* @return int (minutes)
|
||||
*/
|
||||
public function getCacheTimeout()
|
||||
{
|
||||
return $this->cacheTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache timeout.
|
||||
*
|
||||
* @param int $cacheTimeout (minutes)
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setCacheTimeout($cacheTimeout)
|
||||
{
|
||||
$this->isChanged('cacheTimeout', $cacheTimeout);
|
||||
$this->cacheTimeout = $cacheTimeout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get height.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getHeight()
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ordering.
|
||||
*
|
||||
* @param int $ordering
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setOrdering($ordering)
|
||||
{
|
||||
$this->ordering = (int) $ordering;
|
||||
$this->isChanged('ordering', $this->ordering);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ordering.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOrdering()
|
||||
{
|
||||
return $this->ordering;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get params.
|
||||
*
|
||||
* @return array $params
|
||||
*/
|
||||
public function getParams()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set params.
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setParams(array $params)
|
||||
{
|
||||
$this->isChanged('params', $params);
|
||||
$this->params = $params;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set template.
|
||||
*
|
||||
* @param string $template
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setTemplate($template)
|
||||
{
|
||||
$this->isChanged('template', $template);
|
||||
$this->template = $template;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTemplate()
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template data.
|
||||
*
|
||||
* @return array $templateData
|
||||
*/
|
||||
public function getTemplateData()
|
||||
{
|
||||
return $this->templateData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set template data.
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setTemplateData(array $templateData)
|
||||
{
|
||||
$this->isChanged('templateData', $templateData);
|
||||
$this->templateData = $templateData;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set errorMessage.
|
||||
*
|
||||
* @param string $errorMessage
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setErrorMessage($errorMessage)
|
||||
{
|
||||
$this->errorMessage = $errorMessage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get errorMessage.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getErrorMessage()
|
||||
{
|
||||
return $this->errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached flag.
|
||||
*
|
||||
* @param bool $cached
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setCached($cached)
|
||||
{
|
||||
$this->cached = $cached;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isCached()
|
||||
{
|
||||
return $this->cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set loadTime.
|
||||
*
|
||||
* @param string|float|int $loadTime
|
||||
*
|
||||
* @return Widget
|
||||
*/
|
||||
public function setLoadTime($loadTime)
|
||||
{
|
||||
$this->loadTime = $loadTime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get loadTime.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLoadTime()
|
||||
{
|
||||
return $this->loadTime;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->getName(),
|
||||
'width' => $this->getWidth(),
|
||||
'height' => $this->getHeight(),
|
||||
'ordering' => $this->getOrdering(),
|
||||
'type' => $this->getType(),
|
||||
'params' => $this->getParams(),
|
||||
'template' => $this->getTemplate(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Widget>
|
||||
*/
|
||||
class WidgetRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Update widget ordering.
|
||||
*
|
||||
* @param array $ordering
|
||||
* @param int $userId
|
||||
*/
|
||||
public function updateOrdering($ordering, $userId): void
|
||||
{
|
||||
$widgets = $this->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'createdBy' => $userId,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($widgets as &$widget) {
|
||||
if (isset($ordering[$widget->getId()])) {
|
||||
$widget->setOrdering((int) $ordering[$widget->getId()]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->saveEntities($widgets);
|
||||
}
|
||||
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['w.ordering', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'w';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Event;
|
||||
|
||||
use Mautic\CacheBundle\Cache\CacheProviderTagAwareInterface;
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Mautic\DashboardBundle\Exception\CouldNotFormatDateTimeException;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class WidgetDetailEvent extends CommonEvent
|
||||
{
|
||||
public const DASHBOARD_CACHE_TAG = 'dashboard_widget';
|
||||
|
||||
protected $type;
|
||||
|
||||
protected $template;
|
||||
|
||||
protected $templateData = [];
|
||||
|
||||
protected $errorMessage;
|
||||
|
||||
protected $uniqueId;
|
||||
|
||||
protected $cacheDir;
|
||||
|
||||
protected $uniqueCacheDir;
|
||||
|
||||
protected $cacheTimeout;
|
||||
|
||||
protected float $startTime;
|
||||
|
||||
protected $loadTime = 0;
|
||||
|
||||
private string $cacheKeyPath = 'dashboard.widget.';
|
||||
|
||||
private bool $isPreview = false;
|
||||
|
||||
public function __construct(private TranslatorInterface $translator, private CorePermissions $security, protected Widget $widget, private ?CacheProviderTagAwareInterface $cacheProvider = null)
|
||||
{
|
||||
$this->startTime = microtime(true);
|
||||
$this->setWidget($widget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Act as widget preview without data.
|
||||
*/
|
||||
public function setPreview(bool $isPreview): void
|
||||
{
|
||||
$this->isPreview = $isPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is preview without data?
|
||||
*/
|
||||
public function isPreview(): bool
|
||||
{
|
||||
return $this->isPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return unique key, uses legacy methods for BC.
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
$cacheKey = [
|
||||
$this->getUniqueWidgetId(),
|
||||
];
|
||||
|
||||
$params = $this->getWidget()->getParams();
|
||||
|
||||
foreach (['dateTo', 'dateFrom'] as $dateParameter) {
|
||||
if (isset($params[$dateParameter])) {
|
||||
try {
|
||||
$date = $this->castDateTimeToString($params[$dateParameter]);
|
||||
$cacheKey[] = $date;
|
||||
} catch (CouldNotFormatDateTimeException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no additional parameters we return uniqueWidgetId as a cache key
|
||||
// Otherwise we return hashed $cacheKey value
|
||||
$cacheKey = (1 == count($cacheKey)) ? $this->getUniqueWidgetId() : substr(md5(implode('', $cacheKey)), 0, 16);
|
||||
|
||||
return $this->cacheKeyPath.$cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cache dir.
|
||||
*
|
||||
* @param string $cacheDir
|
||||
* @param mixed|null $uniqueCacheDir
|
||||
*/
|
||||
public function setCacheDir($cacheDir, $uniqueCacheDir = null): void
|
||||
{
|
||||
$this->cacheDir = $cacheDir;
|
||||
$this->uniqueCacheDir = $uniqueCacheDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cache timeout.
|
||||
*
|
||||
* @param string $cacheTimeout
|
||||
*/
|
||||
public function setCacheTimeout($cacheTimeout): void
|
||||
{
|
||||
$this->cacheTimeout = (int) $cacheTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the widget type.
|
||||
*
|
||||
* @param string $type
|
||||
*/
|
||||
public function setType($type): void
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the widget type.
|
||||
*
|
||||
* @return string $type
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the widget entity.
|
||||
*/
|
||||
public function setWidget(Widget $widget): void
|
||||
{
|
||||
$this->widget = $widget;
|
||||
|
||||
$params = $widget->getParams();
|
||||
|
||||
// Set required params if undefined
|
||||
if (!isset($params['timeUnit'])) {
|
||||
$params['timeUnit'] = null;
|
||||
}
|
||||
|
||||
if (!isset($params['amount'])) {
|
||||
$params['amount'] = null;
|
||||
}
|
||||
|
||||
if (!isset($params['dateFormat'])) {
|
||||
$params['dateFormat'] = null;
|
||||
}
|
||||
|
||||
if (!isset($params['filter'])) {
|
||||
$params['filter'] = [];
|
||||
}
|
||||
|
||||
$widget->setParams($params);
|
||||
|
||||
$this->setType($widget->getType());
|
||||
$this->setCacheTimeout($widget->getCacheTimeout());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the widget entity.
|
||||
*
|
||||
* @return Widget $widget
|
||||
*/
|
||||
public function getWidget()
|
||||
{
|
||||
return $this->widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the widget template.
|
||||
*
|
||||
* @param string $template
|
||||
*/
|
||||
public function setTemplate($template): void
|
||||
{
|
||||
$this->template = $template;
|
||||
$this->widget->setTemplate($template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the widget template.
|
||||
*
|
||||
* @return string $template
|
||||
*/
|
||||
public function getTemplate()
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the widget template data.
|
||||
*
|
||||
* @param bool|null $skipCache
|
||||
*
|
||||
* @throws \Psr\Cache\InvalidArgumentException
|
||||
*/
|
||||
public function setTemplateData(array $templateData, $skipCache = false): void
|
||||
{
|
||||
$this->templateData = $templateData;
|
||||
$this->widget->setTemplateData($templateData);
|
||||
$this->widget->setLoadTime(abs(microtime(true) - $this->startTime));
|
||||
|
||||
if ($this->usesLegacyCache()) {
|
||||
// Store the template data to the cache
|
||||
if (!$skipCache && $this->cacheDir && $this->widget->getCacheTimeout() > 0) {
|
||||
$cache = new CacheStorageHelper(CacheStorageHelper::ADAPTOR_FILESYSTEM, $this->uniqueCacheDir, null, $this->cacheDir);
|
||||
// must pass a DateTime object or a int of seconds to expire as 3rd attribute to set().
|
||||
$expireTime = $this->widget->getCacheTimeout() * 60;
|
||||
|
||||
$cache->set($this->getUniqueWidgetId(), $templateData, (int) $expireTime);
|
||||
}
|
||||
}
|
||||
|
||||
$cItem = $this->cacheProvider->getItem($this->getCacheKey());
|
||||
if ($this->widget->getCacheTimeout()) {
|
||||
$cItem->expiresAfter((int) $this->widget->getCacheTimeout() * 60); // This is in minutes
|
||||
}
|
||||
$cItem->set($templateData);
|
||||
$cItem->tag(self::DASHBOARD_CACHE_TAG);
|
||||
|
||||
$this->cacheProvider->save($cItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the widget template data.
|
||||
*
|
||||
* @return array<mixed> $templateData
|
||||
*/
|
||||
public function getTemplateData()
|
||||
{
|
||||
return $this->templateData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set en error message.
|
||||
*
|
||||
* @param string $errorMessage
|
||||
*/
|
||||
public function setErrorMessage($errorMessage): void
|
||||
{
|
||||
$this->errorMessage = $errorMessage;
|
||||
$this->widget->setErrorMessage($errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an error message.
|
||||
*
|
||||
* @return string $errorMessage
|
||||
*/
|
||||
public function getErrorMessage()
|
||||
{
|
||||
return $this->errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a unique ID from type and widget params.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUniqueWidgetId()
|
||||
{
|
||||
if ($this->uniqueId) {
|
||||
return $this->uniqueId;
|
||||
}
|
||||
|
||||
$params = $this->getWidget()->getParams();
|
||||
// Unset dateFrom and dateTo since they constantly change
|
||||
unset($params['dateFrom'], $params['dateTo']);
|
||||
|
||||
$uniqueSettings = [
|
||||
'params' => $params,
|
||||
'width' => $this->getWidget()->getWidth(),
|
||||
'height' => $this->getWidget()->getHeight(),
|
||||
'locale' => $this->translator->getLocale(),
|
||||
];
|
||||
|
||||
return $this->uniqueId = $this->getType().'_'.substr(md5(json_encode($uniqueSettings)), 0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Psr\Cache\InvalidArgumentException
|
||||
* Checks the cache for the widget data.
|
||||
* If cache exists, it sets the TemplateData.
|
||||
*/
|
||||
public function isCached(): bool
|
||||
{
|
||||
if (!$this->cacheDir && $this->usesLegacyCache()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->usesLegacyCache()) {
|
||||
$cache = new CacheStorageHelper(CacheStorageHelper::ADAPTOR_FILESYSTEM, $this->uniqueCacheDir, null, $this->cacheDir);
|
||||
$data = $cache->get($this->getUniqueWidgetId(), $this->cacheTimeout);
|
||||
|
||||
if ($data) {
|
||||
$this->widget->setCached(true);
|
||||
$this->setTemplateData($data, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$cachedItem = $this->cacheProvider->getItem($this->getCacheKey());
|
||||
if (!$cachedItem->isHit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->widget->setCached(true);
|
||||
$this->setTemplateData($cachedItem->get());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Translator object.
|
||||
*
|
||||
* @return TranslatorInterface
|
||||
*/
|
||||
public function getTranslator()
|
||||
{
|
||||
return $this->translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has at least one permission of defined array of permissions.
|
||||
*/
|
||||
public function hasPermissions(array $permissions): bool
|
||||
{
|
||||
if (!$this->security) {
|
||||
return true;
|
||||
}
|
||||
$perm = $this->security->isGranted($permissions, 'RETURN_ARRAY');
|
||||
|
||||
return in_array(true, $perm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has defined permission to see the widgets.
|
||||
*
|
||||
* @param string $permission
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($permission)
|
||||
{
|
||||
if (!$this->security) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->security->isGranted($permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for cache type. This event should be created by factory thus not legacy approach.
|
||||
*/
|
||||
private function usesLegacyCache(): bool
|
||||
{
|
||||
return is_null($this->cacheProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to cast DateTime objects to strings to use them in the cache key.
|
||||
*
|
||||
* @param mixed|null $value
|
||||
*
|
||||
* @throws CouldNotFormatDateTimeException
|
||||
*/
|
||||
private function castDateTimeToString($value): string
|
||||
{
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
// We use RFC 2822 format because it includes timezone
|
||||
return $value->format('r');
|
||||
}
|
||||
|
||||
try {
|
||||
$value = strval($value);
|
||||
} catch (\Exception) {
|
||||
throw new CouldNotFormatDateTimeException();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
|
||||
class WidgetFormEvent extends CommonEvent
|
||||
{
|
||||
protected $form;
|
||||
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* Set the widget type.
|
||||
*
|
||||
* @param string $type
|
||||
*/
|
||||
public function setType($type): void
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the widget type.
|
||||
*
|
||||
* @return string $type
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the widget form.
|
||||
*
|
||||
* @param string $form
|
||||
*/
|
||||
public function setForm($form): void
|
||||
{
|
||||
$this->form = $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the widget edit form.
|
||||
*/
|
||||
public function getForm()
|
||||
{
|
||||
return $this->form;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class WidgetTypeListEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $widgetTypes = [];
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @var CorePermissions
|
||||
*/
|
||||
protected $security;
|
||||
|
||||
/**
|
||||
* Adds a new widget type to the widget types list.
|
||||
*
|
||||
* @param string $widgetType
|
||||
* @param string $bundle name (widget category)
|
||||
*/
|
||||
public function addType($widgetType, $bundle = 'others'): void
|
||||
{
|
||||
$bundle = 'mautic.'.$bundle.'.dashboard.widgets';
|
||||
$widgetTypeName = 'mautic.widget.'.$widgetType;
|
||||
|
||||
if ($this->translator) {
|
||||
$bundle = $this->translator->trans($bundle);
|
||||
$widgetTypeName = $this->translator->trans($widgetTypeName);
|
||||
}
|
||||
|
||||
if (!isset($this->widgetTypes[$bundle])) {
|
||||
$this->widgetTypes[$bundle] = [];
|
||||
}
|
||||
|
||||
$this->widgetTypes[$bundle][$widgetType] = $widgetTypeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set translator if you want the strings to be translated.
|
||||
*/
|
||||
public function setTranslator(TranslatorInterface $translator): void
|
||||
{
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set security object to check the perimissions.
|
||||
*/
|
||||
public function setSecurity(CorePermissions $security): void
|
||||
{
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has permission to see the widgets.
|
||||
*/
|
||||
public function hasPermissions(array $permissions): bool
|
||||
{
|
||||
if (!$this->security) {
|
||||
return true;
|
||||
}
|
||||
$perm = $this->security->isGranted($permissions, 'RETURN_ARRAY');
|
||||
|
||||
return !in_array(false, $perm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of widget types.
|
||||
*
|
||||
* @return array $widgetTypes
|
||||
*/
|
||||
public function getTypes()
|
||||
{
|
||||
return $this->widgetTypes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\EventListener;
|
||||
|
||||
use Mautic\DashboardBundle\DashboardEvents;
|
||||
use Mautic\DashboardBundle\Event\WidgetDetailEvent;
|
||||
use Mautic\DashboardBundle\Event\WidgetFormEvent;
|
||||
use Mautic\DashboardBundle\Event\WidgetTypeListEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class DashboardSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* Define the name of the bundle/category of the widget(s).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle = 'others';
|
||||
|
||||
/**
|
||||
* Define the widget(s).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $types = [];
|
||||
|
||||
/**
|
||||
* Define permissions to see those widgets.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [];
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
DashboardEvents::DASHBOARD_ON_MODULE_LIST_GENERATE => ['onWidgetListGenerate', 0],
|
||||
DashboardEvents::DASHBOARD_ON_MODULE_FORM_GENERATE => ['onWidgetFormGenerate', 0],
|
||||
DashboardEvents::DASHBOARD_ON_MODULE_DETAIL_PRE_LOAD => ['onWidgetDetailPreLoad', 0],
|
||||
DashboardEvents::DASHBOARD_ON_MODULE_DETAIL_GENERATE => ['onWidgetDetailGenerate', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates widget preview without data.
|
||||
*
|
||||
* @throws \Psr\Cache\InvalidArgumentException
|
||||
*/
|
||||
public function onWidgetDetailPreLoad(WidgetDetailEvent $event): void
|
||||
{
|
||||
$event->setTemplate('@MauticCore/Helper/chart.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds widget new widget types to the list of available widget types.
|
||||
*/
|
||||
public function onWidgetListGenerate(WidgetTypeListEvent $event): void
|
||||
{
|
||||
if ($this->permissions && !$event->hasPermissions($this->permissions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$widgetTypes = array_keys($this->types);
|
||||
|
||||
foreach ($widgetTypes as $type) {
|
||||
$event->addType($type, $this->bundle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a widget edit form when needed.
|
||||
*/
|
||||
public function onWidgetFormGenerate(WidgetFormEvent $event): void
|
||||
{
|
||||
if (isset($this->types[$event->getType()])) {
|
||||
$event->setForm($this->types[$event->getType()]);
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a widget detail when needed.
|
||||
*/
|
||||
public function onWidgetDetailGenerate(WidgetDetailEvent $event): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a widget detail when needed.
|
||||
*/
|
||||
public function checkPermissions(WidgetDetailEvent $event): void
|
||||
{
|
||||
$widgetTypes = array_keys($this->types);
|
||||
if ($this->permissions && !$event->hasPermissions($this->permissions) && in_array($event->getType(), $widgetTypes)) {
|
||||
$translator = $event->getTranslator();
|
||||
$event->setErrorMessage($translator->trans('mautic.dashboard.missing.permission', ['%section%' => $this->bundle]));
|
||||
$event->stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DashboardBundle\Exception;
|
||||
|
||||
class CouldNotFormatDateTimeException extends \Exception
|
||||
{
|
||||
public function __construct(
|
||||
string $message = 'Can\'t format date object to string',
|
||||
int $code = 0,
|
||||
?\Throwable $throwable = null,
|
||||
) {
|
||||
parent::__construct($message, $code, $throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DashboardBundle\Factory;
|
||||
|
||||
use Mautic\CacheBundle\Cache\CacheProviderTagAwareInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Mautic\DashboardBundle\Event\WidgetDetailEvent;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class WidgetDetailEventFactory
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private CacheProviderTagAwareInterface $cacheProvider,
|
||||
private CorePermissions $corePermissions,
|
||||
private UserHelper $userHelper,
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private PathsHelper $pathsHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(Widget $widget): WidgetDetailEvent
|
||||
{
|
||||
$cacheDir = $this->coreParametersHelper->get('cached_data_dir', $this->pathsHelper->getSystemPath('cache', true));
|
||||
$event = new WidgetDetailEvent($this->translator, $this->corePermissions, $widget, $this->cacheProvider);
|
||||
$event->setCacheDir($cacheDir, $this->userHelper->getUser()->getId());
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class UploadType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'file',
|
||||
FileType::class,
|
||||
[
|
||||
'label' => 'mautic.lead.import.file',
|
||||
'attr' => [
|
||||
'accept' => '.json',
|
||||
'class' => 'form-control',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'start',
|
||||
SubmitType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary',
|
||||
'icon' => 'ri-upload-line',
|
||||
'onclick' => "mQuery(this).prop('disabled', true); mQuery('form[name=\'dashboard_upload\']').submit();",
|
||||
],
|
||||
'label' => 'mautic.lead.import.upload',
|
||||
]);
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'dashboard_upload';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\DashboardBundle\DashboardEvents;
|
||||
use Mautic\DashboardBundle\Event\WidgetFormEvent;
|
||||
use Mautic\DashboardBundle\Event\WidgetTypeListEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class WidgetType extends AbstractType
|
||||
{
|
||||
public function __construct(protected EventDispatcherInterface $dispatcher, protected CorePermissions $security)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.dashboard.widget.form.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control form-control-widget'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$event = new WidgetTypeListEvent();
|
||||
$event->setSecurity($this->security);
|
||||
$this->dispatcher->dispatch($event, DashboardEvents::DASHBOARD_ON_MODULE_LIST_GENERATE);
|
||||
|
||||
$types = array_map(fn ($category): array => array_flip($category), $event->getTypes());
|
||||
|
||||
$builder->add(
|
||||
'type',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'label' => 'mautic.dashboard.widget.form.type',
|
||||
'choices' => $types,
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'placeholder' => 'mautic.core.select',
|
||||
'attr' => [
|
||||
'class' => 'form-control form-control-widget',
|
||||
'onchange' => 'Mautic.updateWidgetForm(this)',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'width',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'label' => 'mautic.dashboard.widget.form.width',
|
||||
'choices' => [
|
||||
'25%' => '25',
|
||||
'50%' => '50',
|
||||
'75%' => '75',
|
||||
'100%' => '100',
|
||||
],
|
||||
'empty_data' => '100',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control form-control-widget'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'height',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'label' => 'mautic.dashboard.widget.form.height',
|
||||
'choices' => [
|
||||
'mautic.dashboard.widget.size.extra_small' => '215',
|
||||
'mautic.dashboard.widget.size.small' => '330',
|
||||
'mautic.dashboard.widget.size.medium' => '445',
|
||||
'mautic.dashboard.widget.size.large' => '560',
|
||||
'mautic.dashboard.widget.size.extra_large' => '675',
|
||||
],
|
||||
'empty_data' => '330',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control form-control-widget'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
// function to add a form for specific widget type dynamically
|
||||
$func = function (FormEvent $e): void {
|
||||
$data = $e->getData();
|
||||
$form = $e->getForm();
|
||||
$event = new WidgetFormEvent();
|
||||
$type = null;
|
||||
$params = [];
|
||||
|
||||
// $data is object on load, array on save (??)
|
||||
if (is_array($data)) {
|
||||
if (isset($data['type'])) {
|
||||
$type = $data['type'];
|
||||
}
|
||||
if (isset($data['params'])) {
|
||||
$params = $data['params'];
|
||||
}
|
||||
} else {
|
||||
$type = $data->getType();
|
||||
$params = $data->getParams();
|
||||
}
|
||||
|
||||
$event->setType($type);
|
||||
$this->dispatcher->dispatch($event, DashboardEvents::DASHBOARD_ON_MODULE_FORM_GENERATE);
|
||||
$widgetForm = $event->getForm();
|
||||
$form->setData($params);
|
||||
|
||||
if (isset($widgetForm['formAlias'])) {
|
||||
$form->add('params', $widgetForm['formAlias'], [
|
||||
'label' => false,
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
$builder->add(
|
||||
'id',
|
||||
HiddenType::class,
|
||||
[
|
||||
'mapped' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class,
|
||||
[
|
||||
'apply_text' => false,
|
||||
'save_text' => 'mautic.core.form.save',
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
|
||||
// Register the function above as EventListener on PreSet and PreBind
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, $func);
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, $func);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class MauticDashboardBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CacheBundle\Cache\CacheProviderTagAwareInterface;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\Filesystem;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\DashboardBundle\DashboardEvents;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Mautic\DashboardBundle\Entity\WidgetRepository;
|
||||
use Mautic\DashboardBundle\Event\WidgetDetailEvent;
|
||||
use Mautic\DashboardBundle\Factory\WidgetDetailEventFactory;
|
||||
use Mautic\DashboardBundle\Form\Type\WidgetType;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Widget>
|
||||
*/
|
||||
class DashboardModel extends FormModel
|
||||
{
|
||||
public function __construct(
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
private PathsHelper $pathsHelper,
|
||||
private WidgetDetailEventFactory $widgetEventFactory,
|
||||
private Filesystem $filesystem,
|
||||
private RequestStack $requestStack,
|
||||
EntityManagerInterface $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
private CacheProviderTagAwareInterface $cacheProvider,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function getRepository(): WidgetRepository
|
||||
{
|
||||
return $this->em->getRepository(Widget::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'dashboard:widgets';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity or generate a new one if id is empty.
|
||||
*/
|
||||
public function getEntity($id = null): ?Widget
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Widget();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load widgets for the current user from database.
|
||||
*
|
||||
* @param bool $ignorePaginator
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getWidgets($ignorePaginator = false)
|
||||
{
|
||||
return $this->getEntities([
|
||||
'orderBy' => 'w.ordering',
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'w.createdBy',
|
||||
'expr' => 'eq',
|
||||
'value' => $this->userHelper->getUser()->getId(),
|
||||
],
|
||||
],
|
||||
],
|
||||
'ignore_paginator' => $ignorePaginator,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array that represents the dashboard and all its widgets.
|
||||
* Useful for dashboard exports.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function toArray($name): array
|
||||
{
|
||||
return [
|
||||
'name' => $name,
|
||||
'description' => $this->generateDescription(),
|
||||
'widgets' => array_map(
|
||||
fn ($widget) => $widget->toArray(),
|
||||
$this->getWidgets(true)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the dashboard snapshot to the user folder.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public function saveSnapshot($name): void
|
||||
{
|
||||
$dir = $this->pathsHelper->getSystemPath('dashboard.user');
|
||||
$filename = InputHelper::filename($name, 'json');
|
||||
$path = $dir.'/'.$filename;
|
||||
$this->filesystem->dumpFile($path, json_encode($this->toArray($name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a translatable description for a dashboard.
|
||||
*/
|
||||
public function generateDescription(): string
|
||||
{
|
||||
return $this->translator->trans(
|
||||
'mautic.dashboard.generated_by',
|
||||
[
|
||||
'%name%' => $this->userHelper->getUser()->getName(),
|
||||
'%date%' => (new \DateTime())->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill widgets with their empty content.
|
||||
*
|
||||
* @param array<mixed> $widgets
|
||||
*/
|
||||
public function populateWidgetPreviews(&$widgets): void
|
||||
{
|
||||
if (count($widgets)) {
|
||||
foreach ($widgets as &$widget) {
|
||||
$this->populateWidgetPreview($widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill widgets with their content.
|
||||
*
|
||||
* @param array $widgets
|
||||
* @param array $filter
|
||||
*/
|
||||
public function populateWidgetsContent(&$widgets, $filter = []): void
|
||||
{
|
||||
if (count($widgets)) {
|
||||
foreach ($widgets as &$widget) {
|
||||
if (!($widget instanceof Widget)) {
|
||||
$widget = $this->populateWidgetEntity($widget);
|
||||
}
|
||||
$this->populateWidgetContent($widget, $filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Widget object from an array data.
|
||||
*/
|
||||
public function populateWidgetEntity(array $data): Widget
|
||||
{
|
||||
$entity = new Widget();
|
||||
|
||||
foreach ($data as $property => $value) {
|
||||
$method = 'set'.ucfirst($property);
|
||||
if (method_exists($entity, $method)) {
|
||||
$entity->$method($value);
|
||||
}
|
||||
unset($data[$property]);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate widget preview.
|
||||
*/
|
||||
public function populateWidgetPreview(Widget $widget): void
|
||||
{
|
||||
$event = $this->widgetEventFactory->create($widget);
|
||||
|
||||
$this->dispatcher->dispatch($event, DashboardEvents::DASHBOARD_ON_MODULE_DETAIL_PRE_LOAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load widget content from the onWidgetDetailGenerate event.
|
||||
*
|
||||
* @param array $filter
|
||||
*/
|
||||
public function populateWidgetContent(Widget $widget, $filter = []): void
|
||||
{
|
||||
$defaultTimeout = $this->coreParametersHelper->get('cached_data_timeout');
|
||||
|
||||
// Timeout 0 will be interpreted as endless cache, so we set it to -1 which will be interpreted as no cache
|
||||
if (0 === $defaultTimeout) {
|
||||
$defaultTimeout = -1;
|
||||
}
|
||||
|
||||
if (null === $widget->getCacheTimeout() || -1 === $widget->getCacheTimeout()) {
|
||||
$widget->setCacheTimeout($defaultTimeout);
|
||||
}
|
||||
|
||||
// Merge global filter with widget params
|
||||
$widgetParams = $widget->getParams();
|
||||
$resultParams = array_merge($widgetParams, $filter);
|
||||
|
||||
// Add the user timezone
|
||||
if (empty($resultParams['timezone'])) {
|
||||
$resultParams['timezone'] = $this->userHelper->getUser()->getTimezone();
|
||||
}
|
||||
|
||||
// Clone the objects in param array to avoid reference issues if some subscriber changes them
|
||||
foreach ($resultParams as &$param) {
|
||||
if (is_object($param)) {
|
||||
$param = clone $param;
|
||||
}
|
||||
}
|
||||
|
||||
$widget->setParams($resultParams);
|
||||
|
||||
$this->dispatcher->dispatch(
|
||||
$this->widgetEventFactory->create($widget),
|
||||
DashboardEvents::DASHBOARD_ON_MODULE_DETAIL_GENERATE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the temporary widget cache.
|
||||
*/
|
||||
public function clearDashboardCache(): void
|
||||
{
|
||||
$cacheDir = $this->coreParametersHelper->get('cached_data_dir', $this->pathsHelper->getSystemPath('cache', true));
|
||||
$cacheStorage = new CacheStorageHelper(CacheStorageHelper::ADAPTOR_FILESYSTEM, $this->userHelper->getUser()->getId(), null, $cacheDir);
|
||||
|
||||
$cacheStorage->clear();
|
||||
|
||||
$this->cacheProvider->invalidateTags([WidgetDetailEvent::DASHBOARD_CACHE_TAG]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Widget $entity
|
||||
* @param string|null $action
|
||||
* @param array $options
|
||||
*
|
||||
* @return \Symfony\Component\Form\FormInterface<mixed>
|
||||
*
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof Widget) {
|
||||
throw new MethodNotAllowedHttpException(['Widget'], 'Entity must be of class Widget()');
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(WidgetType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create/edit entity.
|
||||
*
|
||||
* @param object $entity
|
||||
* @param bool $unlock
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
// Set widget name from widget type if empty
|
||||
if (!$entity->getName()) {
|
||||
$entity->setName($this->translator->trans('mautic.widget.'.$entity->getType()));
|
||||
}
|
||||
|
||||
$entity->setDateModified(new \DateTime());
|
||||
|
||||
parent::saveEntity($entity, $unlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate default date range filter and time unit.
|
||||
*/
|
||||
public function getDefaultFilter(): array
|
||||
{
|
||||
$dateRangeDefault = $this->coreParametersHelper->get('default_daterange_filter', '-1 month');
|
||||
$dateRangeStart = new \DateTime();
|
||||
$dateRangeStart->modify($dateRangeDefault);
|
||||
|
||||
$session = $this->requestStack->getSession();
|
||||
$today = new \DateTime();
|
||||
$dateFrom = new \DateTime($session->get('mautic.daterange.form.from', $dateRangeStart->format('Y-m-d 00:00:00')));
|
||||
$dateTo = new \DateTime($session->get('mautic.daterange.form.to', $today->format('Y-m-d 23:59:59')));
|
||||
|
||||
return [
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo->modify('23:59:59'), // till end of the 'to' date selected
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block headerTitle %}{{ 'mautic.dashboard.import'|trans }}{% endblock %}
|
||||
|
||||
{% block mauticContent %}dashboardImport{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% if dashboards %}
|
||||
<div class="col-sm-6">
|
||||
<div class="ml-sm mt-sm pa-sm">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{{ 'mautic.dashboard.predefined'|trans }}</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="list-group">
|
||||
{% for dashboard, config in dashboards %}
|
||||
<div class="list-group-item {{ (dashboard == preview) ? 'active' : '' }}">
|
||||
<h4 class="list-group-item-heading">{{ config.name|purify }}</h4>
|
||||
{% if config.description is not empty %}<p class="small">{{ config.description|purify }}</p>{% endif %}
|
||||
<p class="list-group-item-heading">
|
||||
<a href="{{ path('mautic_dashboard_action', {'objectAction': 'import', 'preview': dashboard}) }}">
|
||||
{{ 'mautic.dashboard.preview'|trans }}
|
||||
</a>·
|
||||
<a href="{{ path('mautic_dashboard_action', {'objectAction': 'applyDashboardFile', 'file': config.type~'.'~dashboard}) }}">
|
||||
{{ 'mautic.core.form.apply'|trans }}
|
||||
</a>{% if 'user' == config.type %}·
|
||||
<a href="{{ path('mautic_dashboard_action', {'objectAction': 'deleteDashboardFile', 'file': config.type~'.'~dashboard}) }}" data-toggle="confirmation" data-message="{{ 'mautic.dashboard.delete_layout'|trans|e }}" data-confirm-text="{{ 'mautic.core.form.delete'|trans|e }}" data-confirm-callback="executeAction" data-cancel-text="{{ 'mautic.core.form.cancel'|trans|e }}">
|
||||
{{ 'mautic.core.form.delete'|trans }}
|
||||
</a>{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6">
|
||||
<div class="mr-sm mt-sm pa-sm">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{{ 'mautic.dashboard.import.start.instructions'|trans }}</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="input-group well mt-lg">
|
||||
{{ form_widget(form.file) }}
|
||||
<span class="input-group-btn">
|
||||
{{ form_widget(form.start) }}
|
||||
</span>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if widgets %}
|
||||
<div class="col-md-12">
|
||||
<h2>{{ 'mautic.dashboard.widgets.preview'|trans }}</h2>
|
||||
</div>
|
||||
<div id="dashboard-widgets" class="dashboard-widgets cards">
|
||||
{% for widget in widgets %}
|
||||
<div class="card-flex widget" data-widget-id="{{ widget.id }}" style="width: {{ widget.width|default('100') }}%; height: {{ widget.height|default('300') }}px;">
|
||||
{{ include('@MauticDashboard/Widget/detail.html.twig', {widget}) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,120 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block headerTitle %}{{ 'mautic.dashboard.header.index'|trans }}{% endblock %}
|
||||
|
||||
{% block mauticContent %}dashboard{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{{ include('@MauticCore/Helper/page_actions.html.twig', {
|
||||
'routeBase': 'dashboard',
|
||||
'langVar': 'dashboard',
|
||||
'customButtons': [
|
||||
{
|
||||
'attr': {
|
||||
'class': 'btn btn-primary btn-nospin',
|
||||
'data-toggle': 'ajaxmodal',
|
||||
'data-target': '#MauticSharedModal',
|
||||
'href': path('mautic_dashboard_action', {'objectAction': 'new'}),
|
||||
'data-header': 'mautic.dashboard.widget.add'|trans,
|
||||
},
|
||||
'iconClass': 'ri-add-fill',
|
||||
'btnText': 'mautic.dashboard.widget.add',
|
||||
'primary': true,
|
||||
},
|
||||
{
|
||||
'attr': {
|
||||
'class': 'btn btn-ghost btn-nospin',
|
||||
'href': 'javascript:void(0)',
|
||||
'onclick': "Mautic.saveDashboardLayout('"~'mautic.dashboard.confirmation_layout_name'|trans~"');",
|
||||
'data-toggle': '',
|
||||
},
|
||||
'iconClass': 'ri-save-line',
|
||||
'btnText': 'mautic.dashboard.save_as_predefined',
|
||||
},
|
||||
{
|
||||
'attr': {
|
||||
'class': 'btn btn-ghost btn-nospin',
|
||||
'href': 'javascript:void(0)',
|
||||
'onclick': "Mautic.exportDashboardLayout('"~'mautic.dashboard.confirmation_layout_name'|trans~"', '"~path('mautic_dashboard_action', {'objectAction': 'export'})~"');",
|
||||
'data-toggle': '',
|
||||
},
|
||||
'iconClass': 'ri-export-line',
|
||||
'btnText': 'mautic.dashboard.export.widgets',
|
||||
},
|
||||
{
|
||||
'attr': {
|
||||
'class': 'btn btn-ghost',
|
||||
'href': path('mautic_dashboard_action', {'objectAction': 'import'}),
|
||||
'data-header': 'mautic.dashboard.widget.import'|trans,
|
||||
},
|
||||
'iconClass': 'ri-import-line',
|
||||
'btnText': 'mautic.dashboard.widget.import',
|
||||
},
|
||||
],
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if true == phpVersion['isOutdated'] %}
|
||||
<div class="pt-md pl-md col-md-12">
|
||||
<div class="pt-md pl-md alert alert-warning">
|
||||
<h3>{{ 'mautic.dashboard.phpversionwarning.title'|trans }}</h3>
|
||||
<p>{{ 'mautic.dashboard.phpversionwarning.body'|trans({'%phpversion%': phpVersion['version']})|purify }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row pt-md ml-0">
|
||||
<div class="col-sm-12">
|
||||
<div class="d-flex fd-row fw-nowrap gap-sm ai-center jc-space-between">
|
||||
{{ include('@MauticCore/Helper/graph_dateselect.html.twig', {'dateRangeForm': dateRangeForm}) }}
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn btn-ghost btn-sm btn-nospin" data-toggle="dropdown" aria-expanded="false">
|
||||
{{ 'mautic.core.quick_filters'|trans }}
|
||||
<i class="ri-arrow-down-s-line"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#" onclick="Mautic.setDateRange('today'); event.preventDefault();">{{ 'mautic.dashboard.date.today'|trans }}</a></li>
|
||||
<li><a href="#" onclick="Mautic.setDateRange('yesterday'); event.preventDefault();">{{ 'mautic.dashboard.date.yesterday'|trans }}</a></li>
|
||||
<li><a href="#" onclick="Mautic.setDateRange(7); event.preventDefault();">{{ 'mautic.dashboard.date.last_7_days'|trans }}</a></li>
|
||||
<li><a href="#" onclick="Mautic.setDateRange(30); event.preventDefault();">{{ 'mautic.dashboard.date.last_30_days'|trans }}</a></li>
|
||||
<li><a href="#" onclick="Mautic.setDateRange(90); event.preventDefault();">{{ 'mautic.dashboard.date.last_90_days'|trans }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if widgets|length > 0 %}
|
||||
<div id="dashboard-widgets" class="dashboard-widgets cards">
|
||||
{% for widget in widgets %}
|
||||
<div class="card-flex widget" data-widget-id="{{ widget.id }}" style="width: {{ widget.width|default(100) }}%; height: {{ widget.height|default(300) }}px">
|
||||
<div class="spinner"><i class="ri-loader-3-line ri-spin"></i></div>
|
||||
{{ include('@MauticDashboard/Dashboard/widget.html.twig', {'widget': widget}) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="cloned-widgets" class="dashboard-widgets cards"></div>
|
||||
{% else %}
|
||||
<div class="well well col-md-6 col-md-offset-3 mt-md">
|
||||
<div class="row">
|
||||
<div class="mautibot-image col-xs-3 text-center">
|
||||
<img class="img-responsive" style="max-height: 125px; margin-left: auto; margin-right: auto;" src="{{ mautibotGetImage('wave') }}" />
|
||||
</div>
|
||||
<div class="col-xs-9">
|
||||
<h4><i class="ri-double-quotes-l"></i> {{ 'mautic.dashboard.nowidgets.tip.header'|trans }} <i class="ri-double-quotes-r"></i></h4>
|
||||
<p class="mt-md">{{ 'mautic.dashboard.nowidgets.tip'|trans }}</p>
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
label: 'mautic.dashboard.apply_default',
|
||||
variant: 'success',
|
||||
href: path('mautic_dashboard_action', {'objectAction': 'applyDashboardFile', 'file': 'default.json'})
|
||||
}
|
||||
]
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% set system_user = 'mautic.core.system'|trans %}
|
||||
|
||||
{% if logs is defined and logs is iterable %}
|
||||
<div class="pt-md pr-md pb-md pl-md">
|
||||
<ul class="media-list media-list-feed">
|
||||
{% for log in logs %}
|
||||
<li class="media">
|
||||
<div class="media-object pull-left">
|
||||
<span class="figure featured {% if 'create' == log.action %}bg-success{% endif %}">
|
||||
<span class="fa {{ icons[log.bundle]|default('') }}"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
{% if log.userId is not defined or log.userId == 0 %}
|
||||
{{ system_user }}
|
||||
{% else %}
|
||||
<a href="{{ path('mautic_user_action', {'objectAction': 'edit', 'objectId': log.userId}) }}" data-toggle="ajax">
|
||||
{{ log.userName }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ ('mautic.dashboard.'~log.action~'.past.tense')|trans }}
|
||||
|
||||
{% if log.route is defined and log.route is not empty and log.userId != 0 %}
|
||||
<a href="{{ log.route }}" data-toggle="ajax">
|
||||
{{ log.objectName }}
|
||||
</a>
|
||||
{% elseif log.objectName is defined %}
|
||||
{{ log.objectName }}
|
||||
{% endif %}
|
||||
<p class="fs-12 dark-sm"><small> {{ dateToFull(log.dateAdded) }}</small></p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% if upcomingEmails %}
|
||||
<ul class="list-group mb-0 bdr-w-0">
|
||||
{% for email in upcomingEmails %}
|
||||
<li class="mb-md">
|
||||
<div class="box-layout">
|
||||
{% if icons.email is defined and icons.email is not empty %}
|
||||
<div class="col-md-1 va-m">
|
||||
<h3><span class="fa {{ icons.email }} fw-sb text-success"></span></h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-8 va-m">
|
||||
<h5 class="fw-sb text-primary">
|
||||
<a href="{{ path('mautic_campaign_action', {'objectAction': 'view', 'objectId': email.campaign_id}) }}" data-toggle="ajax">
|
||||
{{ email.campaign_name }}
|
||||
</a>
|
||||
</h5>
|
||||
<span class="text-white dark-sm">
|
||||
{{ email.event_name }}
|
||||
{{'mautic.core.send.email.to'|trans|lower}}
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
label: email.lead_name ?: ('mautic.lead.lead'|trans ~ ' #' ~ email.lead_id),
|
||||
variant: 'tertiary',
|
||||
size: 'xs',
|
||||
href: path('mautic_contact_action', {'objectAction': 'view', 'objectId': email.lead_id}),
|
||||
attributes: {
|
||||
'data-toggle': 'ajax',
|
||||
'class': 'pr-sm pl-sm fs-12 ml-3'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-4 va-m text-right small">
|
||||
{{ dateToFull(email.trigger_date) }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">{{ 'mautic.note.no.upcoming.emails'|trans }}</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1 @@
|
||||
{{ include('@MauticDashboard/Widget/detail.html.twig', {'widget': widget}) }}
|
||||
@@ -0,0 +1,59 @@
|
||||
<div class="tile" style="height: {{ widget.height|default('310') - 10 }}px;">
|
||||
<div class="card-header d-flex jc-space-between ai-center">
|
||||
<h4 class="fw-sb">{{ widget.name|purify }}</h4>
|
||||
{% if widget.id %}
|
||||
<div class="dropdown">
|
||||
<a class="btn btn-ghost btn--icon-only btn-xs dropdown-toggle" data-toggle="dropdown" href="#" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="ri-more-2-fill"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li class="small fw-sb text-secondary ma-md ellipsis">
|
||||
{% if widget.isCached %}
|
||||
<i class="ri-history-line mr-xs"></i>
|
||||
{{ 'mautic.dashboard.widget.data.loaded.from.cache'|trans }}
|
||||
{% else %}
|
||||
<i class="ri-flashlight-line mr-xs"></i>
|
||||
{{ 'mautic.dashboard.widget.data.loaded.from.database'|trans }}
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url('mautic_dashboard_action', {'objectAction': 'edit', 'objectId': widget.id}) }}"
|
||||
data-toggle="ajaxmodal"
|
||||
data-target="#MauticSharedModal"
|
||||
data-header="{{ 'mautic.dashboard.widget.header.edit'|trans }}">
|
||||
<span>
|
||||
<i class="ri-edit-line"></i>
|
||||
{{'mautic.core.form.edit'|trans}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url('mautic_dashboard_action', {'objectAction': 'delete', 'objectId': widget.id}) }}"
|
||||
data-header="{{ 'mautic.dashboard.widget.header.delete'|trans }}"
|
||||
class="remove-widget danger">
|
||||
<span>
|
||||
<i class="ri-close-line"></i>
|
||||
{{'mautic.core.remove'|trans}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr class="bdr-b">
|
||||
<div class="card-body d-flex fd-column fg-1">
|
||||
{% if widget.errorMessage %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ widget.errorMessage|trans }}
|
||||
</div>
|
||||
{% elseif widget.template %}
|
||||
<!-- start: {{ widget.template }} -->
|
||||
{{ include(widget.template, widget.templateData) }}
|
||||
<!-- end: {{ widget.template }} -->
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="widget-overlay d-flex ai-center jc-center">
|
||||
<i class="ri-drag-drop-fill ri-4x text-interactive"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
{{ form_start(form) }}
|
||||
<div class="row form-group">
|
||||
<div class="col-xs-6">
|
||||
{{ form_label(form.name) }}
|
||||
{{ form_widget(form.name) }}
|
||||
<div class="has-error">{{ form_errors(form.name) }}</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
{{ form_label(form.type) }}
|
||||
{{ form_widget(form.type) }}
|
||||
<div class="has-error">{{ form_errors(form.type) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<div class="col-xs-6">
|
||||
{{ form_label(form.width) }}
|
||||
{{ form_widget(form.width) }}
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
{{ form_label(form.height) }}
|
||||
{{ form_widget(form.height) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_row(form.buttons) }}
|
||||
{{ form_end(form) }}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DashboardBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\ReportBundle\Entity\Report;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class DashboardControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testWidgetWithReport(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy([]);
|
||||
|
||||
$report = new Report();
|
||||
$report->setName('Lead and points');
|
||||
$report->setSource('lead.pointlog');
|
||||
$this->em->persist($report);
|
||||
$this->em->flush();
|
||||
|
||||
$widget = new Widget();
|
||||
$widget->setName('Line graph report');
|
||||
$widget->setType('report');
|
||||
$widget->setParams(['graph' => sprintf('%s:mautic.lead.graph.line.leads', $report->getId())]);
|
||||
$widget->setWidth(100);
|
||||
$widget->setHeight(200);
|
||||
$widget->setCreatedBy($user);
|
||||
$this->em->persist($widget);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($widget);
|
||||
|
||||
$this->client->xmlHttpRequest('GET', sprintf('/s/dashboard/widget/%s', $widget->getId()));
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$content = $response->getContent();
|
||||
Assert::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
Assert::assertIsArray($data);
|
||||
Assert::assertArrayHasKey('success', $data);
|
||||
Assert::assertSame(1, $data['success']);
|
||||
Assert::assertArrayHasKey('widgetId', $data);
|
||||
Assert::assertSame((string) $widget->getId(), $data['widgetId']);
|
||||
Assert::assertArrayHasKey('widgetWidth', $data);
|
||||
Assert::assertSame($widget->getWidth(), $data['widgetWidth']);
|
||||
Assert::assertArrayHasKey('widgetHeight', $data);
|
||||
Assert::assertSame($widget->getHeight(), $data['widgetHeight']);
|
||||
Assert::assertArrayHasKey('widgetHtml', $data);
|
||||
Assert::assertStringContainsString('View Full Report', $data['widgetHtml']);
|
||||
}
|
||||
|
||||
public function testWidgetWithBestHours(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy([]);
|
||||
$segment = $this->createSegment('A', 'a');
|
||||
$widget = new Widget();
|
||||
$widget->setName('Best email read hours');
|
||||
$widget->setType('emails.best.hours');
|
||||
$widget->setParams(['timeFormat' => 24, 'segmentId' => $segment->getId()]);
|
||||
$widget->setWidth(100);
|
||||
$widget->setHeight(200);
|
||||
$widget->setCreatedBy($user);
|
||||
$this->em->persist($widget);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($widget);
|
||||
|
||||
$this->client->xmlHttpRequest('GET', "/s/dashboard/widget/{$widget->getId()}");
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$content = $this->client->getResponse()->getContent();
|
||||
Assert::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
Assert::assertIsArray($data);
|
||||
Assert::assertArrayHasKey('success', $data);
|
||||
Assert::assertSame(1, $data['success']);
|
||||
Assert::assertArrayHasKey('widgetId', $data);
|
||||
Assert::assertSame((string) $widget->getId(), $data['widgetId']);
|
||||
Assert::assertArrayHasKey('widgetWidth', $data);
|
||||
Assert::assertSame($widget->getWidth(), $data['widgetWidth']);
|
||||
Assert::assertArrayHasKey('widgetHeight', $data);
|
||||
Assert::assertSame($widget->getHeight(), $data['widgetHeight']);
|
||||
Assert::assertArrayHasKey('widgetHtml', $data);
|
||||
Assert::assertStringContainsString('Best email read hours', $data['widgetHtml']);
|
||||
}
|
||||
|
||||
public function testWidgetWithSegmentBuildTime(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy([]);
|
||||
$this->createSegment('A', 'a', 3, $user);
|
||||
$this->createSegment('B', 'b', 60, $user);
|
||||
$this->createSegment('C', 'c', 66, $user);
|
||||
$this->createSegment('D', 'd', 0.4, $user);
|
||||
|
||||
$widget = new Widget();
|
||||
$widget->setName('Segments build time');
|
||||
$widget->setType('segments.build.time');
|
||||
$widget->setParams(['order' => 'DESC', 'segments' => []]);
|
||||
$widget->setWidth(100);
|
||||
$widget->setHeight(300);
|
||||
$widget->setCreatedBy($user);
|
||||
$this->em->persist($widget);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($widget);
|
||||
|
||||
$this->client->xmlHttpRequest('GET', sprintf('/s/dashboard/widget/%s', $widget->getId()));
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$content = $response->getContent();
|
||||
Assert::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
Assert::assertIsArray($data);
|
||||
Assert::assertArrayHasKey('success', $data);
|
||||
Assert::assertSame(1, $data['success']);
|
||||
Assert::assertArrayHasKey('widgetHtml', $data);
|
||||
$tableArray = $this->widgetHtmlWithTableToArray($data['widgetHtml']);
|
||||
|
||||
$this->assertSame([
|
||||
['C', 'Admin User', '1 minute 6 seconds'],
|
||||
['B', 'Admin User', '1 minute'],
|
||||
['A', 'Admin User', '3 seconds'],
|
||||
['D', 'Admin User', 'Less than 1 second'],
|
||||
], $tableArray);
|
||||
}
|
||||
|
||||
public function testAuditLogWidgetWithDeletedContact(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$widget = new Widget();
|
||||
$widget->setName('Recent activity');
|
||||
$widget->setType('recent.activity');
|
||||
$widget->setWidth(100);
|
||||
$widget->setHeight(300);
|
||||
$widget->setCreatedBy($user);
|
||||
$this->em->persist($widget);
|
||||
$this->em->flush();
|
||||
$contact = new Lead();
|
||||
$contact->setFirstName('John');
|
||||
$contactModel = self::getContainer()->get('mautic.lead.model.lead');
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
$contactModel->saveEntity($contact);
|
||||
$contactModel->deleteEntity($contact);
|
||||
$this->em->clear();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, "/s/dashboard/widget/{$widget->getId()}");
|
||||
$this->assertResponseIsSuccessful();
|
||||
$printResponse = fn () => print_r(json_decode($this->client->getResponse()->getContent(), true), true);
|
||||
Assert::assertStringContainsString('created', $printResponse());
|
||||
Assert::assertStringContainsString('deleted', $printResponse());
|
||||
}
|
||||
|
||||
private function createSegment(string $name, string $alias, float $lastBuildTime = 0, ?User $user = null): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name);
|
||||
$segment->setPublicName($name);
|
||||
$segment->setAlias($alias);
|
||||
$segment->setLastBuiltTime($lastBuildTime);
|
||||
|
||||
if ($user) {
|
||||
$segment->setCreatedBy($user);
|
||||
$segment->setCreatedByUser($user->getName());
|
||||
}
|
||||
|
||||
$this->em->persist($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<int,string>>
|
||||
*/
|
||||
private function widgetHtmlWithTableToArray(string $widgetHtml): array
|
||||
{
|
||||
$doc = new \DOMDocument();
|
||||
$doc->loadHTML($widgetHtml);
|
||||
$crawler = new Crawler($doc);
|
||||
$crawlerTable = $crawler->filter('table')->first();
|
||||
|
||||
return array_slice($crawlerTable->filter('tr')->each(fn ($tr) => $tr->filter('td')->each(fn ($td) => trim($td->text()))), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DashboardBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\DashboardBundle\Controller\DashboardController;
|
||||
use Mautic\DashboardBundle\Dashboard\Widget;
|
||||
use Mautic\DashboardBundle\Model\DashboardModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
class DashboardControllerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|Request
|
||||
*/
|
||||
private MockObject $requestMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|CorePermissions
|
||||
*/
|
||||
private MockObject $securityMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Translator
|
||||
*/
|
||||
private MockObject $translatorMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|ModelFactory<DashboardModel>
|
||||
*/
|
||||
private MockObject $modelFactoryMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|DashboardModel
|
||||
*/
|
||||
private MockObject $dashboardModelMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|RouterInterface
|
||||
*/
|
||||
private MockObject $routerMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&FlashBag
|
||||
*/
|
||||
private MockObject $flashBagMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|Container
|
||||
*/
|
||||
private MockObject $containerMock;
|
||||
|
||||
private DashboardController $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->requestMock = $this->createMock(Request::class);
|
||||
$this->dashboardModelMock = $this->createMock(DashboardModel::class);
|
||||
$this->routerMock = $this->createMock(RouterInterface::class);
|
||||
$this->containerMock = $this->createMock(Container::class);
|
||||
|
||||
$doctrine = $this->createMock(ManagerRegistry::class);
|
||||
$this->modelFactoryMock = $this->createMock(ModelFactory::class);
|
||||
$userHelper = $this->createMock(UserHelper::class);
|
||||
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->translatorMock = $this->createMock(Translator::class);
|
||||
$this->flashBagMock = $this->createMock(FlashBag::class);
|
||||
$requestStack = new RequestStack();
|
||||
$this->securityMock = $this->createMock(CorePermissions::class);
|
||||
|
||||
$requestStack->push($this->requestMock);
|
||||
$this->controller = new DashboardController(
|
||||
$doctrine,
|
||||
$this->modelFactoryMock,
|
||||
$userHelper,
|
||||
$coreParametersHelper,
|
||||
$dispatcher,
|
||||
$this->translatorMock,
|
||||
$this->flashBagMock,
|
||||
$requestStack,
|
||||
$this->securityMock
|
||||
);
|
||||
$this->controller->setContainer($this->containerMock);
|
||||
}
|
||||
|
||||
public function testSaveWithGetWillCallAccessDenied(): void
|
||||
{
|
||||
$this->requestMock->expects($this->once())
|
||||
->method('isMethod')
|
||||
->willReturn(true);
|
||||
|
||||
$this->requestMock->expects(self::once())
|
||||
->method('isXmlHttpRequest')
|
||||
->willReturn(false);
|
||||
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
$this->controller->saveAction($this->requestMock);
|
||||
}
|
||||
|
||||
public function testSaveWithPostNotAjaxWillCallAccessDenied(): void
|
||||
{
|
||||
$this->requestMock->expects($this->once())
|
||||
->method('isMethod')
|
||||
->willReturn(true);
|
||||
|
||||
$this->requestMock->method('isXmlHttpRequest')
|
||||
->willReturn(false);
|
||||
|
||||
$this->translatorMock->expects($this->once())
|
||||
->method('trans')
|
||||
->with('mautic.core.url.error.401');
|
||||
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
$this->controller->saveAction($this->requestMock);
|
||||
}
|
||||
|
||||
public function testSaveWithPostAjaxWillSave(): void
|
||||
{
|
||||
$this->requestMock->expects($this->once())
|
||||
->method('isMethod')
|
||||
->willReturn(true);
|
||||
|
||||
$this->requestMock->method('isXmlHttpRequest')->willReturn(true);
|
||||
$this->requestMock->method('get')->willReturn('mockName');
|
||||
|
||||
$this->containerMock->expects($this->exactly(2))
|
||||
->method('get')->willReturnCallback(function (...$parameters) {
|
||||
$this->assertSame('router', $parameters[0]);
|
||||
|
||||
return $this->routerMock;
|
||||
});
|
||||
|
||||
$this->routerMock->expects($this->any())
|
||||
->method('generate')
|
||||
->willReturn('https://some.url');
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('getModel')
|
||||
->with('dashboard')
|
||||
->willReturn($this->dashboardModelMock);
|
||||
|
||||
$this->dashboardModelMock->expects($this->once())
|
||||
->method('saveSnapshot')
|
||||
->with('mockName');
|
||||
|
||||
$this->translatorMock->expects($this->once())
|
||||
->method('trans')
|
||||
->with('mautic.dashboard.notice.save');
|
||||
|
||||
$this->controller->saveAction($this->requestMock);
|
||||
}
|
||||
|
||||
public function testSaveWithPostAjaxWillNotBeAbleToSave(): void
|
||||
{
|
||||
$this->requestMock->expects($this->once())
|
||||
->method('isMethod')
|
||||
->willReturn(true);
|
||||
|
||||
$this->requestMock->method('isXmlHttpRequest')
|
||||
->willReturn(true);
|
||||
|
||||
$this->routerMock->expects($this->any())
|
||||
->method('generate')
|
||||
->willReturn('https://some.url');
|
||||
|
||||
$this->requestMock->method('get')->willReturn('mockName');
|
||||
|
||||
$this->containerMock->expects($this->once())
|
||||
->method('get')
|
||||
->with('router')
|
||||
->willReturn($this->routerMock);
|
||||
|
||||
$this->modelFactoryMock->expects($this->once())
|
||||
->method('getModel')
|
||||
->with('dashboard')
|
||||
->willReturn($this->dashboardModelMock);
|
||||
|
||||
$this->dashboardModelMock->expects($this->once())
|
||||
->method('saveSnapshot')
|
||||
->will($this->throwException(new IOException('some error message')));
|
||||
|
||||
$this->translatorMock->expects($this->once())
|
||||
->method('trans')
|
||||
->with('mautic.dashboard.error.save');
|
||||
|
||||
$this->controller->saveAction($this->requestMock);
|
||||
}
|
||||
|
||||
public function testWidgetDirectRequest(): void
|
||||
{
|
||||
$this->requestMock->method('isXmlHttpRequest')
|
||||
->willReturn(false);
|
||||
|
||||
$this->expectException(NotFoundHttpException::class);
|
||||
$this->controller->widgetAction($this->requestMock, $this->createMock(Widget::class), $this->createMock(Environment::class), 1);
|
||||
}
|
||||
|
||||
public function testWidgetNotFound(): void
|
||||
{
|
||||
$widgetId = '1';
|
||||
$twig = $this->createMock(Environment::class);
|
||||
|
||||
$this->requestMock->method('isXmlHttpRequest')
|
||||
->willReturn(true);
|
||||
|
||||
$widgetService = $this->createMock(Widget::class);
|
||||
$widgetService->expects(self::once())
|
||||
->method('setFilter')
|
||||
->with($this->requestMock);
|
||||
$widgetService->expects(self::once())
|
||||
->method('get')
|
||||
->with((int) $widgetId)
|
||||
->willReturn(null);
|
||||
|
||||
$this->containerMock->expects(self::never())
|
||||
->method('get');
|
||||
|
||||
$this->expectException(NotFoundHttpException::class);
|
||||
$this->controller->widgetAction($this->requestMock, $widgetService, $twig, $widgetId);
|
||||
}
|
||||
|
||||
public function testWidget(): void
|
||||
{
|
||||
$widgetId = '1';
|
||||
$widget = new \Mautic\DashboardBundle\Entity\Widget();
|
||||
$renderedContent = 'lfsadkdhfůasfjds';
|
||||
$twig = $this->createMock(Environment::class);
|
||||
|
||||
$twig->expects(self::once())
|
||||
->method('render')
|
||||
->willReturn($renderedContent);
|
||||
|
||||
$this->requestMock->method('isXmlHttpRequest')
|
||||
->willReturn(true);
|
||||
|
||||
$widgetService = $this->createMock(Widget::class);
|
||||
$widgetService->expects(self::once())
|
||||
->method('setFilter')
|
||||
->with($this->requestMock);
|
||||
$widgetService->expects(self::once())
|
||||
->method('get')
|
||||
->with((int) $widgetId)
|
||||
->willReturn($widget);
|
||||
|
||||
$response = $this->controller->widgetAction($this->requestMock, $widgetService, $twig, $widgetId);
|
||||
|
||||
self::assertSame('{"success":1,"widgetId":"1","widgetHtml":"lfsadkdhf\u016fasfjds","widgetWidth":null,"widgetHeight":null}', $response->getContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DashboardBundle\Tests\Dashboard;
|
||||
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\DashboardBundle\Dashboard\Widget;
|
||||
use Mautic\DashboardBundle\Entity\Widget as WidgetEntity;
|
||||
use Mautic\DashboardBundle\Model\DashboardModel;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
class WidgetTest extends TestCase
|
||||
{
|
||||
private const USER_ID = 1;
|
||||
|
||||
/**
|
||||
* @var DashboardModel&MockObject
|
||||
*/
|
||||
private MockObject $dashboardModel;
|
||||
|
||||
/**
|
||||
* @var UserHelper&MockObject
|
||||
*/
|
||||
private MockObject $userHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject&RequestStack
|
||||
*/
|
||||
private MockObject $requestStack;
|
||||
|
||||
/**
|
||||
* @var User&MockObject
|
||||
*/
|
||||
private MockObject $user;
|
||||
|
||||
private Widget $widget;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->dashboardModel = $this->createMock(DashboardModel::class);
|
||||
$this->userHelper = $this->createMock(UserHelper::class);
|
||||
$this->requestStack = $this->createMock(RequestStack::class);
|
||||
|
||||
$this->user = $this->createMock(User::class);
|
||||
$this->user
|
||||
->method('getId')
|
||||
->willReturn(self::USER_ID);
|
||||
|
||||
$this->widget = new Widget(
|
||||
$this->dashboardModel,
|
||||
$this->userHelper,
|
||||
$this->requestStack
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetSuccess(): void
|
||||
{
|
||||
$widgetId = 2;
|
||||
$widget = $this->createMock(WidgetEntity::class);
|
||||
$widget->expects(self::once())
|
||||
->method('getId')
|
||||
->willReturn($widgetId);
|
||||
$widget->expects(self::once())
|
||||
->method('getCreatedBy')
|
||||
->willReturn(self::USER_ID);
|
||||
|
||||
$widget->setCreatedBy(self::USER_ID);
|
||||
$filter = [
|
||||
'dateFrom' => new \DateTime(),
|
||||
'dateTo' => new \DateTime(),
|
||||
];
|
||||
|
||||
$this->dashboardModel->expects(self::once())
|
||||
->method('getEntity')
|
||||
->with($widgetId)
|
||||
->willReturn($widget);
|
||||
|
||||
$this->userHelper->expects(self::once())
|
||||
->method('getUser')
|
||||
->willReturn($this->user);
|
||||
|
||||
$this->dashboardModel->expects(self::once())
|
||||
->method('getDefaultFilter')
|
||||
->willReturn($filter);
|
||||
|
||||
$this->dashboardModel->expects(self::once())
|
||||
->method('populateWidgetContent')
|
||||
->with($widget, $filter);
|
||||
|
||||
$this->widget->get($widgetId);
|
||||
}
|
||||
|
||||
public function testGetNotFoundHttpException(): void
|
||||
{
|
||||
$widgetId = 2;
|
||||
$widget = null;
|
||||
|
||||
$this->dashboardModel->expects(self::once())
|
||||
->method('getEntity')
|
||||
->with($widgetId)
|
||||
->willReturn($widget);
|
||||
|
||||
$this->expectException(NotFoundHttpException::class);
|
||||
$this->widget->get($widgetId);
|
||||
}
|
||||
|
||||
public function testGetNotFoundHttpExceptionEmptyEntity(): void
|
||||
{
|
||||
$widgetId = 2;
|
||||
$widget = $this->createMock(WidgetEntity::class);
|
||||
$widget->expects(self::once())
|
||||
->method('getId')
|
||||
->willReturn(null);
|
||||
|
||||
$widget->setCreatedBy(self::USER_ID);
|
||||
|
||||
$this->dashboardModel->expects(self::once())
|
||||
->method('getEntity')
|
||||
->with($widgetId)
|
||||
->willReturn($widget);
|
||||
|
||||
$this->expectException(NotFoundHttpException::class);
|
||||
$this->widget->get($widgetId);
|
||||
}
|
||||
|
||||
public function testGetAccessDeniedException(): void
|
||||
{
|
||||
$widgetId = 2;
|
||||
$widget = $this->createMock(WidgetEntity::class);
|
||||
$widget->expects(self::once())
|
||||
->method('getId')
|
||||
->willReturn($widgetId);
|
||||
$widget->expects(self::once())
|
||||
->method('getCreatedBy')
|
||||
->willReturn(self::USER_ID + 1);
|
||||
|
||||
$widget->setCreatedBy(self::USER_ID);
|
||||
|
||||
$this->dashboardModel->expects(self::once())
|
||||
->method('getEntity')
|
||||
->with($widgetId)
|
||||
->willReturn($widget);
|
||||
|
||||
$this->userHelper->expects(self::once())
|
||||
->method('getUser')
|
||||
->willReturn($this->user);
|
||||
|
||||
$this->expectException(AccessDeniedException::class);
|
||||
$this->widget->get($widgetId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Tests\Entity;
|
||||
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
|
||||
class WidgetTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testWidgetNameXssAttempt(): void
|
||||
{
|
||||
$widget = new Widget();
|
||||
$widget->setName('csrf<script>console.log(\'name\');</script>');
|
||||
$this->assertEquals('csrfconsole.log(\'name\');', $widget->getName());
|
||||
}
|
||||
|
||||
public function testWidgetWidthXssAttempt(): void
|
||||
{
|
||||
$widget = new Widget();
|
||||
$widget->setWidth('100<script>console.log(\'yellow\');</script>');
|
||||
$this->assertEquals(100, $widget->getWidth());
|
||||
}
|
||||
|
||||
public function testWidgetHeightXssAttempt(): void
|
||||
{
|
||||
$widget = new Widget();
|
||||
$widget->setHeight('100<script>console.log(\'yellow\');</script>');
|
||||
$this->assertEquals(100, $widget->getHeight());
|
||||
}
|
||||
|
||||
public function testWidgetOrderingSqliAttempt(): void
|
||||
{
|
||||
$widget = new Widget();
|
||||
$widget->setOrdering('3;DROP grep;');
|
||||
$this->assertEquals(3, $widget->getOrdering());
|
||||
}
|
||||
|
||||
public function testWidgetTypeXssAttempt(): void
|
||||
{
|
||||
$widget = new Widget();
|
||||
$widget->setType('map.of.leads<script>console.log(\'yellow\');</script>');
|
||||
$this->assertEquals('map.of.leadsconsole.log(\'yellow\');', $widget->getType());
|
||||
}
|
||||
|
||||
public function testToArrayEmpty(): void
|
||||
{
|
||||
$widget = new Widget();
|
||||
$expected = [
|
||||
'name' => null,
|
||||
'width' => null,
|
||||
'height' => null,
|
||||
'ordering' => null,
|
||||
'type' => null,
|
||||
'params' => [],
|
||||
'template' => null,
|
||||
];
|
||||
$this->assertEquals($expected, $widget->toArray());
|
||||
}
|
||||
|
||||
public function testToArrayFilled(): void
|
||||
{
|
||||
$widget = new Widget();
|
||||
$widget->setName('The itsy bitsy spider');
|
||||
$widget->setWidth(4);
|
||||
$widget->setHeight(5);
|
||||
$widget->setOrdering(6);
|
||||
$widget->setType('climed up');
|
||||
$widget->setParams([]);
|
||||
$widget->setTemplate('the water spout');
|
||||
$expected = [
|
||||
'name' => 'The itsy bitsy spider',
|
||||
'width' => 4,
|
||||
'height' => 5,
|
||||
'ordering' => 6,
|
||||
'type' => 'climed up',
|
||||
'params' => [],
|
||||
'template' => 'the water spout',
|
||||
];
|
||||
$this->assertEquals($expected, $widget->toArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DashboardBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\DashboardBundle\Entity\Widget;
|
||||
use Mautic\DashboardBundle\Event\WidgetDetailEvent;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class WidgetDetailEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private WidgetDetailEvent $widgetDetailEvent;
|
||||
private MockObject $translator;
|
||||
private MockObject $security;
|
||||
private MockObject $widget;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
$this->security = $this->createMock(CorePermissions::class);
|
||||
$this->widget = $this->createMock(Widget::class);
|
||||
|
||||
$this->widgetDetailEvent = new WidgetDetailEvent(
|
||||
$this->translator,
|
||||
$this->security,
|
||||
$this->widget
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetCacheKey(): void
|
||||
{
|
||||
$this->widget
|
||||
->method('getParams')
|
||||
->willReturn(['dateFrom' => '', 'dateTo' => '']);
|
||||
|
||||
$this->translator->expects($this->once())
|
||||
->method('getLocale')
|
||||
->willReturn('en');
|
||||
$this->assertStringContainsString('dashboard.widget.', $this->widgetDetailEvent->getCacheKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DashboardBundle\Tests\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CacheBundle\Cache\CacheProviderTagAwareInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\Filesystem;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\DashboardBundle\Factory\WidgetDetailEventFactory;
|
||||
use Mautic\DashboardBundle\Model\DashboardModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
final class DashboardModelTest extends TestCase
|
||||
{
|
||||
private MockObject&CoreParametersHelper $coreParametersHelper;
|
||||
private MockObject&Session $session;
|
||||
private DashboardModel $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$this->session = $this->createMock(Session::class);
|
||||
$requestStack = $this->createMock(RequestStack::class);
|
||||
$requestStack->method('getSession')
|
||||
->willReturn($this->session);
|
||||
|
||||
$this->model = new DashboardModel(
|
||||
$this->coreParametersHelper,
|
||||
$this->createMock(PathsHelper::class),
|
||||
$this->createMock(WidgetDetailEventFactory::class),
|
||||
$this->createMock(Filesystem::class),
|
||||
$requestStack,
|
||||
$this->createMock(EntityManagerInterface::class),
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(UrlGeneratorInterface::class),
|
||||
$this->createMock(Translator::class),
|
||||
$this->createMock(UserHelper::class),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(CacheProviderTagAwareInterface::class),
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetDefaultFilterFromSession(): void
|
||||
{
|
||||
$dateFromStr = '-1 month';
|
||||
$dateFrom = new \DateTime($dateFromStr);
|
||||
$dateTo = new \DateTime('23:59:59'); // till end of the 'to' date selected
|
||||
|
||||
$this->coreParametersHelper->expects(self::once())
|
||||
->method('get')
|
||||
->with('default_daterange_filter', $dateFromStr)
|
||||
->willReturn($dateFromStr);
|
||||
|
||||
$this->session->expects($this->exactly(2))
|
||||
->method('get')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
$dateFrom->format(\DateTimeInterface::ATOM),
|
||||
$dateTo->format(\DateTimeInterface::ATOM)
|
||||
);
|
||||
|
||||
$filter = $this->model->getDefaultFilter();
|
||||
|
||||
Assert::assertSame(
|
||||
$dateFrom->format(\DateTimeInterface::ATOM),
|
||||
$filter['dateFrom']->format(\DateTimeInterface::ATOM)
|
||||
);
|
||||
|
||||
Assert::assertSame(
|
||||
$dateTo->format(\DateTimeInterface::ATOM),
|
||||
$filter['dateTo']->format(\DateTimeInterface::ATOM)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mautic.dashboard.notice.save="Dashboard snapshot '%name%'' is saved. <a href='%viewUrl%'>View all snapshots</a>."
|
||||
mautic.dashboard.error.save="The dashboard snapshot could not be saved because %msg%"
|
||||
@@ -0,0 +1,71 @@
|
||||
mautic.dashboard.confirmation_layout_name="Enter a name for this dashboard:"
|
||||
mautic.dashboard.create.past.tense="created"
|
||||
mautic.dashboard.delete_layout="Delete this layout?"
|
||||
mautic.dashboard.delete.past.tense="deleted"
|
||||
mautic.dashboard.generated_by="Generated by %name% on %date%"
|
||||
mautic.dashboard.header.index="Dashboard"
|
||||
mautic.dashboard.identified.past.tense="identified"
|
||||
mautic.dashboard.ipadded.past.tense="added IP"
|
||||
mautic.dashboard.label.downloads="Downloads"
|
||||
mautic.dashboard.label.hits="Hits"
|
||||
mautic.dashboard.label.lang="Lang"
|
||||
mautic.dashboard.label.title="Title"
|
||||
mautic.dashboard.apply_default="Apply the default dashboard"
|
||||
mautic.dashboard.menu.index="Dashboard"
|
||||
mautic.dashboard.update.past.tense="updated"
|
||||
mautic.dashboard.date.today="Today"
|
||||
mautic.dashboard.date.yesterday="Yesterday"
|
||||
mautic.dashboard.date.last_7_days="Last 7 days"
|
||||
mautic.dashboard.date.last_30_days="Last 30 days"
|
||||
mautic.dashboard.date.last_90_days="Last 90 days"
|
||||
mautic.note.no.upcoming.emails="No emails are scheduled to be sent."
|
||||
mautic.dashboard.label.created.leads="Created leads"
|
||||
mautic.dashboard.label.url="URL"
|
||||
mautic.dashboard.label.unique.hit.count="Unique hit count"
|
||||
mautic.dashboard.label.total.hit.count="Total hit count"
|
||||
mautic.dashboard.label.email.id="Email ID"
|
||||
mautic.dashboard.label.email.name="Email name"
|
||||
mautic.dashboard.label.contact.id="Contact ID"
|
||||
mautic.dashboard.label.contact.email.address="Contact email address"
|
||||
mautic.dashboard.label.contact.open="Contact open"
|
||||
mautic.dashboard.label.contact.click="Clicks"
|
||||
mautic.dashboard.label.contact.links.clicked="Links clicked"
|
||||
mautic.dashboard.label.segment.id="Segment ID"
|
||||
mautic.dashboard.label.segment.name="Segment name"
|
||||
mautic.dashboard.label.company.id="Company ID"
|
||||
mautic.dashboard.label.company.name="Company name"
|
||||
mautic.dashboard.label.campaign.id="Company ID"
|
||||
mautic.dashboard.label.campaign.name="Campaign name"
|
||||
mautic.dashboard.widget.add="Add widget"
|
||||
mautic.dashboard.widget.no_data="No data found"
|
||||
mautic.dashboard.export.widgets="Export"
|
||||
mautic.dashboard.save_as_predefined="Save as pre-defined"
|
||||
mautic.dashboard.widget.import="Import or select pre-defined"
|
||||
mautic.dashboard.widget.form.name="Name"
|
||||
mautic.dashboard.widget.form.type="Type"
|
||||
mautic.dashboard.widget.form.width="Width"
|
||||
mautic.dashboard.widget.form.height="Height"
|
||||
mautic.dashboard.widget.form.ordering="Place before"
|
||||
mautic.dashboard.widget.ordering.last="Last"
|
||||
mautic.dashboard.widget.header.edit="Edit widget"
|
||||
mautic.dashboard.widget.header.new="New widget"
|
||||
mautic.dashboard.widget.header.delete="Delete widget"
|
||||
mautic.dashboard.widget.data.loaded.from.cache="Showing previously stored data"
|
||||
mautic.dashboard.widget.data.loaded.from.database="Just retrieved latest data"
|
||||
mautic.dashboard.widget.size.extra_small="Extra small (215px)"
|
||||
mautic.dashboard.widget.size.small="Small (330px)"
|
||||
mautic.dashboard.widget.size.medium="Medium (445px)"
|
||||
mautic.dashboard.widget.size.large="Large (560px)"
|
||||
mautic.dashboard.widget.size.extra_large="Extra large (675px)"
|
||||
mautic.dashboard.import="Import a pre-defined dashboard"
|
||||
mautic.dashboard.predefined="Pre-defined dashboards"
|
||||
mautic.dashboard.import.start.instructions="Upload a pre-defined dashboard"
|
||||
mautic.dashboard.widgets.preview="Preview of the selected pre-defined dashboard"
|
||||
mautic.dashboard.date.from="From"
|
||||
mautic.dashboard.date.to="To"
|
||||
mautic.dashboard.nowidgets.tip.header="Hello there!"
|
||||
mautic.dashboard.nowidgets.tip="There are no widgets in your dashboard but don't panic! You can create the widgets with the \"Add widget\" button above, \"Import\" a dashboard from a friend or"
|
||||
mautic.dashboard.missing.permission="You do not have the permission to see the data from %section% section."
|
||||
mautic.dashboard.preview="Preview"
|
||||
mautic.dashboard.phpversionwarning.title="Please update your PHP version"
|
||||
mautic.dashboard.phpversionwarning.body="You are currently using PHP %phpversion%, an outdated and insecure version of PHP. Future versions of Mautic won't support this version anymore.<br>For an overview of supported PHP versions per Mautic version, please visit <a target=\"_blank\" href=\"https://www.mautic.org/download/requirements\">the Mautic website</a>."
|
||||
@@ -0,0 +1 @@
|
||||
mautic.dashboard.upload.filenotfound="File not found"
|
||||
Reference in New Issue
Block a user