From b8d0f1ce1b86abd8b040b8e4fc865b7c5cf68e4d Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:13:42 +0100 Subject: [PATCH 001/106] JS: Introduce dashboards behavior --- library/Icinga/Web/JavaScript.php | 3 +- public/js/icinga/behavior/dashboards.js | 181 ++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 public/js/icinga/behavior/dashboards.js diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index 7d0eed8ec1..d5dd1cbae0 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -42,7 +42,8 @@ class JavaScript 'js/icinga/behavior/selectable.js', 'js/icinga/behavior/modal.js', 'js/icinga/behavior/input-enrichment.js', - 'js/icinga/behavior/datetime-picker.js' + 'js/icinga/behavior/datetime-picker.js', + 'js/icinga/behavior/dashboards.js' ]; protected static $vendorFiles = [ diff --git a/public/js/icinga/behavior/dashboards.js b/public/js/icinga/behavior/dashboards.js new file mode 100644 index 0000000000..4c701bf4ef --- /dev/null +++ b/public/js/icinga/behavior/dashboards.js @@ -0,0 +1,181 @@ +/*! Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */ + +;(function (Icinga, $) { + + 'use strict'; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + /** + * Behavior for the enhanced Icinga Web 2 dashboards + * + * @param icinga {Icinga} The current Icinga Object + * + * @constructor + */ + var Dashboard = function (icinga) { + Icinga.EventListener.call(this, icinga); + + /** + * Base route + * + * @type {string} + */ + this.baseUrl = icinga.config.baseUrl + + /** + * Dashlet container id which is currently being dragged + * + * @type {null|string} + */ + this.containerId = null; + + // Register event handlers for drag and drop functionalities + this.on('dragstart', '.dashboard > .dashlet-sortable', this.onDragStart, this); + this.on('dragover', '.dashboard > .dashlet-sortable', this.onDragOver, this); + this.on('dragleave', '.dashboard > .dashlet-sortable', this.onDragLeave, this); + this.on('dragend', '.dashboard > .dashlet-sortable', this.onDragEnd, this); + this.on('drop', '.dashboard > .dashlet-sortable', this.onDrop, this); + }; + + Dashboard.prototype = new Icinga.EventListener(); + + /** + * A user tries to drag an element, so make sure it's sortable and setup the procedure + * + * @param event {Event} The `dragstart` event triggered when starting to drag the element + * with a mouse click and begin to move it + */ + Dashboard.prototype.onDragStart = function (event) { + let _this = event.data.self; + let $target = $(event.target); + + if (! $target.hasClass('dashlet-sortable')) { + return false; + } + + event.originalEvent.dataTransfer.setData('text', $target.attr('id')); + $target.addClass('draggable-element'); + + _this.containerId = $target.attr('id'); + // Prevents child elements from being the target of pointer events + $('.dashboard.content').children('.dashlet-sortable').addClass('drag-active'); + }; + + /** + * Event handler for drag over + * + * Check that the target el is draggable and isn't the el itself + * which is currently being dragged + * + * @param event {Event} The `drag over` event triggered when dragging over another dashlet + */ + Dashboard.prototype.onDragOver = function (event) { + let $target = $(event.target); + let _this = event.data.self; + + if (! $target.hasClass('dashlet-sortable')) { + $target = $target.closest('.dashlet-sortable'); + } + + // Don't show mouse drop cursor if the target element is the draggable element + if ($target.attr('id') !== _this.containerId) { + event.preventDefault(); + event.stopPropagation(); + + $target.addClass('drag-over'); + } + }; + + /** + * The element doesn't get dragged over anymore, so just remove the drag-over class + * + * @param event {Event} The `drag leave` event triggered when dragging over a dashlet + * and leaving without dropping the draggable element + */ + Dashboard.prototype.onDragLeave = function (event) { + let $target = $(event.target); + + $target.removeClass('drag-over'); + }; + + /** + * Remove all class names added dynamically + * + * @param event {Event} The `drag end` event triggered when the draggable element is released + */ + Dashboard.prototype.onDragEnd = function (event) { + let $target = $(event.target); + + $target.removeClass('draggable-element'); + $target.removeClass('drag-over'); + + // The draggable is now released, so we have to remove the class to enable the pointer events again + $('.dashboard.content').children('.dashlet-sortable').removeClass('drag-active'); + }; + + /** + * Event handler for on drop action + * + * @param event {Event} The `ondrop` event triggered when the dashlet has been dropped + */ + Dashboard.prototype.onDrop = function (event) { + let $target = $(event.target); + let _this = event.data.self; + + // Prevents from being dropped in a child elements + if (! $target.hasClass('dashlet-sortable')) { + $target = $target.closest('.dashlet-sortable'); + } + + // Prevent default behaviors to allow the drop event + event.preventDefault(); + + const dragTarget = event.originalEvent.dataTransfer.getData('text'); + const draggable = $('#' + dragTarget); + + // If the target element has been located before the draggable element, + // insert the draggable before the target element otherwise after it + if ($target.nextAll().filter(draggable).length) { + $(draggable).insertBefore($target); + } else { + $(draggable).insertAfter($target); + } + + // Draggable element is now dropped, so drag-over class must also be removed + $target.removeClass('drag-over'); + + _this.postReorderedDashlets(); + }; + + /** + * Set up a request with the reordered containers and post the data to the controller + */ + Dashboard.prototype.postReorderedDashlets = function () { + let _this = this, + $dashboard = $('.dashboard.content'), + $paneAndHome = $dashboard.data('icinga-pane').split('|', 2), + $dashlets = []; + + $dashboard.children('.dashlet-sortable').each(function () { + $dashlets.push($(this).data('icinga-dashlet')); + }); + + let $pane = $paneAndHome.pop(); + let $home = $paneAndHome.pop(); + + let data = {[$home]: { [$pane]: $dashlets }}; + + $.ajax({ + context : _this, + type : 'post', + url : _this.baseUrl + '/dashboards/reorder-dashlets', + headers : {'Accept': 'application/json'}, + contentType : 'application/json', + data : JSON.stringify(data) + }); + }; + + Icinga.Behaviors.Dashboard = Dashboard; + +})(Icinga, jQuery); From f21a7d27635731acd8337a4759ea35757e5c57a7 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:15:34 +0100 Subject: [PATCH 002/106] CSS: Introduce some styling for the enhanced dashboards --- library/Icinga/Web/StyleSheet.php | 3 +- public/css/icinga/dashboards.less | 295 ++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 public/css/icinga/dashboards.less diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index aba89b4cc3..d231bd9bc9 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -79,7 +79,8 @@ class StyleSheet 'css/icinga/modal.less', 'css/icinga/audit.less', 'css/icinga/health.less', - 'css/icinga/php-diff.less' + 'css/icinga/php-diff.less', + 'css/icinga/dashboards.less' ]; /** diff --git a/public/css/icinga/dashboards.less b/public/css/icinga/dashboards.less new file mode 100644 index 0000000000..5e2c3d9564 --- /dev/null +++ b/public/css/icinga/dashboards.less @@ -0,0 +1,295 @@ +.welcome-view { + position: relative; +} + +.dashboard-introduction { + top: 40%; + right: 50%; + position: absolute; + transform: translate(50%,-50%); + color: @text-color; + + h1, p { + color: inherit; + text-align: center; + } + + h1 { + border-bottom: none; + } + + p { + opacity: .9; + width: 58em; + font-size: 1.1em; + line-height: 1.5em; + } + + // Welcome page form controls + form.icinga-form .form-controls { + margin-left: 13em; + margin-top: 1.8em; + justify-content: center; + } +} + +// Collapse/Expand icons wrapper +.dashboard-list-info, .dashlets-list-info { + float: left; + width: 1.5%; + height: 6%; + cursor: pointer; + + .collapse-icon, .expand-icon { + top: 0; + position: absolute; + + &:before { + font-size: 2em; + } + } + + .collapse-icon { + display: block; + } + + .expand-icon { + display: none; + } +} + +.collapsed .dashboard-list-info, .collapsed .dashlets-list-info { + .collapse-icon { + display: none; + } + + .expand-icon { + display: block; + } +} + +// All the dashboard item lists +.home-item-list, .dashboard-item-list, .dashlet-item-list { + padding: 0; + margin-left: 0; + width: 100%; + display: flex; + flex-flow: row wrap; + list-style-type: none; + + label { + width: 49%; + margin: 0 6px 6px 0; + } + + .dashlet-list-item { + display: flex; + flex-direction: column; + border-radius: .4em; + border: 1px solid @gray-light; + background: @gray-lightest; + + section.caption, h1 { + margin-left: .5em; + font-weight: 500; + } + + h1 { + border-bottom: none; + } + + .caption { + height: auto; + min-height: 3em;; + font-size: 1.2em; + color: @text-color-light; + } + } + + .dashlet-list-item:hover { + background-color: @tr-hover-color; + cursor: pointer; + } + + // Simulate a multiselect based on the checkbox state + input[type="checkbox"]:checked + li { + outline-offset: -2px; + outline: 2px solid @icinga-blue; + background-color: @tr-active-color; + } +} + +.dashlet-item-list.empty-list { + margin: 0 0 .9em; +} + +// Dashboard manager +.dashboard-settings { + display: flex; + flex-direction: column; + margin-top: .6em; + border-top: 1px solid @gray-light; +} + +// Add new home button of the dashboard manager +.controls .add-home { + float: right; + outline: none; + cursor: pointer; + font-size: 1.2em; + font-weight: 500; + padding: .5em 1em; + line-height: normal; + text-decoration: none; + border: 2px solid @icinga-blue; + border-radius: 3px; + color: @text-color-on-icinga-blue; + background-color: @icinga-blue; +} + +.collapsed .action-link.add-dashlet, .collapsed .action-link.add-dashboard { + display: none; +} + +.action-link.add-dashboard { + width: 82.6%; + margin-top: 1em; +} + +.action-link.add-dashlet, .action-link.add-dashboard { + order: 1; + font-size: 1.2em; + font-weight: 500; + padding: .9em 1em; + border-radius: .5em; + text-decoration: none; + background-color: @low-sat-blue; +} + +.dashboard-item-list { + width: 100%; + display: flex; + margin-left: 2.2em; + + .dashlet-list-item { + width: 49.4%; + margin: 0 6px 6px 0; + background: transparent; + } +} + +.dashboard-list-control { + width: 80%; + + h1.collapsible-header { + margin-left: 1.5em; + } + + .action-link.add-dashlet { + width: 98.8%; // Dashlet list item 49.4% x 2 + margin-left: 1.8em; + } +} + +.home-list-control, .dashboard-list-control { + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + + h1.collapsible-header { + border-bottom: none; + } + + h1.collapsible-header a, h1.dashlet-header a { + margin-left: .3em; + color: @icinga-blue; + } + + .dashlets-list-info, .dashboard-list-info { + .expand-icon { + margin-top: .9em; + } + + .collapse-icon { + margin-top: .6em; + } + } +} + +.home-list-control { + width: 100%; + + h1.collapsible-header.home { + margin-left: 1.5em; + margin-bottom: 0; + } + + .action-link.add-dashboard { + margin-left: .4em; + } + + .dashboard-list-control { + .dashboard-item-list { + margin-left: 4em; + } + + .dashlets-list-info { + margin-left: 2.2em; + } + + h1.collapsible-header { + margin-left: 3em; + } + + .action-link.add-dashlet { + margin-left: 3.4em; + } + } +} + +// Add dashlets mini wizard +.form-list-control { + span { + font-size: 1.3em; + opacity: .8; + font-weight: 500; + } +} + +.form-list-control.multi-dashlets { + width: 100%; + display: flex; + flex-direction: column; + border-top: 2px solid @gray-light; + + .control-group { + width: 100%; + } +} + +.form-list-control.multi-dashlets:not(.collapsed) span { + margin-left: 1.4em; +} + +// Make the submit buttons a bit bigger than the normal size +.modal-form.icinga-controls .control-group.form-controls input[type="submit"] { + min-width: 7em; + font-size: 1.2em; + margin-right: .4em; +} + +// Drag and drop +.drag-over { + background: fade(@icinga-blue, 20); + border: 5px @body-bg-color dashed; + box-shadow: 4px 4px 4px 4px @black; +} + +.dashlet-sortable.drag-active * { + pointer-events: none; +} + +.draggable-element{ + opacity: .4; +} From 937f0631bc08eba68147a0c1d17d2491b58c72e0 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:16:51 +0100 Subject: [PATCH 003/106] MYSQL: Introduce mysql schema for the enhanced dashboards --- etc/schema/dashboards.sql | 80 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 etc/schema/dashboards.sql diff --git a/etc/schema/dashboards.sql b/etc/schema/dashboards.sql new file mode 100644 index 0000000000..91c03aa091 --- /dev/null +++ b/etc/schema/dashboards.sql @@ -0,0 +1,80 @@ +DROP DATABASE IF EXISTS dashboard; +DROP USER IF EXISTS dashboard; + +CREATE DATABASE dashboard; +USE dashboard; + +CREATE TABLE `dashboard_home` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, + `label` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, + `username` varchar(254) NOT NULL COLLATE utf8mb4_unicode_ci, + `priority` tinyint NOT NULL, + `type` enum('public', 'private', 'shared') DEFAULT 'private', + PRIMARY KEY (`id`), + UNIQUE KEY `idx_dashboard_home_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE TABLE `dashboard` ( + `id` binary(20) NOT NULL, + `home_id` int(10) UNSIGNED NOT NULL, + `username` varchar(254) NOT NULL COLLATE utf8mb4_unicode_ci, + `name` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, + `label` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, + `priority` tinyint NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_dashboard_dashboard_home` (`home_id`), + CONSTRAINT `fk_dashboard_dashboard_home` FOREIGN KEY (`home_id`) REFERENCES `dashboard_home` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE TABLE `dashboard_override` ( + `dashboard_id` binary(20) NOT NULL, + `username` varchar(254) NOT NULL COLLATE utf8mb4_unicode_ci, + `label` varchar(64) DEFAULT NULL COLLATE utf8mb4_unicode_ci, + `disabled` tinyint(1) DEFAULT 0, + `priority` tinyint NOT NULL, + PRIMARY KEY (`dashboard_id`, `username`), + CONSTRAINT `fk_dashboard_override_dashboard` FOREIGN KEY (`dashboard_id`) REFERENCES dashboard (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE TABLE `dashlet` ( + `id` binary(20) NOT NULL, + `dashboard_id` binary(20) NOT NULL, + `name` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, + `label` varchar(254) NOT NULL COLLATE utf8mb4_unicode_ci, + `url` varchar(2048) NOT NULL COLLATE utf8mb4_bin, + `priority` tinyint NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_dashlet_dashboard` (`dashboard_id`), + CONSTRAINT `fk_dashlet_dashboard` FOREIGN KEY (`dashboard_id`) REFERENCES `dashboard` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE TABLE `dashlet_system` ( + `dashlet_id` binary(20) NOT NULL, + `module_dashlet_id` binary(20) NOT NULL, + `username` varchar(254) NOT NULL COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`username`, `dashlet_id`, `module_dashlet_id`), + CONSTRAINT `fk_dashlet_system_dashlet` FOREIGN KEY (`dashlet_id`) REFERENCES `dashlet` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_dashlet_system_module_dashlet` FOREIGN KEY (`dashlet_id`) REFERENCES `dashlet` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE TABLE `module_dashlet` ( + `id` binary(20) NOT NULL, + `name` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, + `label` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, + `module` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, + `pane` varchar(64) DEFAULT NULL COLLATE utf8mb4_unicode_ci, + `url` varchar(2048) NOT NULL COLLATE utf8mb4_bin, + `description` varchar(64) DEFAULT NULL COLLATE utf8mb4_unicode_ci, + `priority` int(10) DEFAULT 0, + PRIMARY KEY (`id`), + INDEX `idx_module_dashlet_name` (`name`), + INDEX `idx_module_dashlet_pane` (`pane`), + INDEX `idx_module_dashlet_module` (`module`), + INDEX `idx_module_dashlet_priority` (`priority`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE USER 'dashboard'@'%' IDENTIFIED BY 'dashboard'; +GRANT ALL PRIVILEGES ON `dashboard`.* TO 'dashboard'@'%' IDENTIFIED BY 'dashboard'; + +FLUSH PRIVILEGES; \ No newline at end of file From f19369ef0358d28304ee7e1bb4d9327e8b4475dc Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:18:08 +0100 Subject: [PATCH 004/106] Introduce some database model for the new dashboards --- library/Icinga/Model/DashboardOverride.php | 53 ++++++++++++++++++ library/Icinga/Model/Dashlet.php | 60 ++++++++++++++++++++ library/Icinga/Model/Home.php | 58 ++++++++++++++++++++ library/Icinga/Model/ModuleDashlet.php | 56 +++++++++++++++++++ library/Icinga/Model/Pane.php | 64 ++++++++++++++++++++++ 5 files changed, 291 insertions(+) create mode 100644 library/Icinga/Model/DashboardOverride.php create mode 100644 library/Icinga/Model/Dashlet.php create mode 100644 library/Icinga/Model/Home.php create mode 100644 library/Icinga/Model/ModuleDashlet.php create mode 100644 library/Icinga/Model/Pane.php diff --git a/library/Icinga/Model/DashboardOverride.php b/library/Icinga/Model/DashboardOverride.php new file mode 100644 index 0000000000..8582da3449 --- /dev/null +++ b/library/Icinga/Model/DashboardOverride.php @@ -0,0 +1,53 @@ + new Expression('COALESCE(COUNT(dashboard_subscribable_dashboard_dashboard_override.dashboard_id), 0)') + ]; + } + + public function getMetaData() + { + return ['priority' => t('Dashboard Priority Order')]; + } + + public function getSearchColumns() + { + return ['name']; + } + + public function getDefaultSort() + { + return 'dashboard.name'; + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('dashboard', Pane::class); + } +} diff --git a/library/Icinga/Model/Dashlet.php b/library/Icinga/Model/Dashlet.php new file mode 100644 index 0000000000..8b02245835 --- /dev/null +++ b/library/Icinga/Model/Dashlet.php @@ -0,0 +1,60 @@ + t('Dashboard Id'), + 'name' => t('Dashlet Name'), + 'label' => t('Dashlet Title'), + 'url' => t('Dashlet Url'), + 'priority' => t('Dashlet Order Priority') + ]; + } + + public function getSearchColumns() + { + return ['name']; + } + + public function getDefaultSort() + { + return 'priority'; + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('dashboard', Pane::class); + //$relations->belongsTo('home', Home::class); + } +} diff --git a/library/Icinga/Model/Home.php b/library/Icinga/Model/Home.php new file mode 100644 index 0000000000..50f68e69b6 --- /dev/null +++ b/library/Icinga/Model/Home.php @@ -0,0 +1,58 @@ + t('Dashboard Home Name'), + 'label' => t('Dashboard Home Title'), + //'priority' => t('Dashboard Order Priority') + ]; + } + + public function getSearchColumns() + { + return ['name']; + } + + public function getDefaultSort() + { + return 'dashboard_home.name'; + } + + public function createRelations(Relations $relations) + { + $relations->hasMany('dashboard', Pane::class); + + //$relations->hasMany('dashlet', Dashlet::class); + } +} diff --git a/library/Icinga/Model/ModuleDashlet.php b/library/Icinga/Model/ModuleDashlet.php new file mode 100644 index 0000000000..aa2bec4e08 --- /dev/null +++ b/library/Icinga/Model/ModuleDashlet.php @@ -0,0 +1,56 @@ + t('Dashlet Name'), + 'label' => t('Dashlet Title'), + 'module' => t('Module Name'), + 'pane' => t('Dashboard Pane Name'), + 'url' => t('Dashlet Url'), + 'description' => t('Dashlet Description'), + 'priority' => t('Dashlet Order Priority') + ]; + } + + public function getSearchColumns() + { + return ['name']; + } + + public function getDefaultSort() + { + return ['module_dashlet.name', 'module_dashlet.priority']; + } +} diff --git a/library/Icinga/Model/Pane.php b/library/Icinga/Model/Pane.php new file mode 100644 index 0000000000..ce1e98c157 --- /dev/null +++ b/library/Icinga/Model/Pane.php @@ -0,0 +1,64 @@ + t('Dashboard Home Id'), + 'name' => t('Dashboard Name'), + 'label' => t('Dashboard Title'), + 'username' => t('Username'), + ]; + } + + public function getSearchColumns() + { + return ['name']; + } + + public function getDefaultSort() + { + return 'dashboard.name'; + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('home', Home::class) + ->setCandidateKey('home_id'); + + $relations->hasMany('dashboard_override', DashboardOverride::class) + ->setJoinType('LEFT'); + $relations->hasMany('dashlet', Dashlet::class) + ->setJoinType('LEFT'); + } +} From 3c55422fa7f4dd9be0772b453cbdf356481fb83d Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:21:11 +0100 Subject: [PATCH 005/106] `loader.js`: Allow to override the actual form action using `formaction` attr --- public/js/icinga/loader.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index f038dc7c8f..8d69fe826a 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -89,6 +89,11 @@ $target = this.getLinkTargetFor($form); } + // Overwrite the URL only if the form is not auto submitted + if ($button.hasAttr('formaction') && ! $autoSubmittedBy) { + url = $button.attr('formaction'); + } + if (! url) { // Use the URL of the target container if the form's action is not set url = $target.closest('.container').data('icinga-url'); From 26f2f735103cb6aaeaa7da2594a956614b6b361c Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:24:47 +0100 Subject: [PATCH 006/106] Introduce common `DataExtractor` trait --- library/Icinga/Common/DataExtractor.php | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 library/Icinga/Common/DataExtractor.php diff --git a/library/Icinga/Common/DataExtractor.php b/library/Icinga/Common/DataExtractor.php new file mode 100644 index 0000000000..ed77115a91 --- /dev/null +++ b/library/Icinga/Common/DataExtractor.php @@ -0,0 +1,37 @@ + $value) { + $func = 'set'. ucfirst($name); + if (method_exists($this, $func)) { + $this->$func($value); + } + } + + return $this; + } + + /** + * Get this class's structure as array + * + * @return array + */ + public function toArray() + { + return []; + } +} From cd54881c165dd6023e1a82f7073621a8ce286b94 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:26:24 +0100 Subject: [PATCH 007/106] ViewScript: Remove index.phtml of the search dashboard controller --- application/views/scripts/search/index.phtml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 application/views/scripts/search/index.phtml diff --git a/application/views/scripts/search/index.phtml b/application/views/scripts/search/index.phtml deleted file mode 100644 index 321597e701..0000000000 --- a/application/views/scripts/search/index.phtml +++ /dev/null @@ -1,7 +0,0 @@ -
-dashboard->getTabs() ?> -
- -
-dashboard ?> -
From 335a07776647ca0c4c90821200eee332eacdd377 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:32:11 +0100 Subject: [PATCH 008/106] Dashbaord: Introduce some common traits used to manage the new dashboards --- .../Web/Dashboard/Common/DashboardManager.php | 498 ++++++++++++++++++ .../Web/Dashboard/Common/DisableWidget.php | 37 ++ .../Web/Dashboard/Common/ModuleDashlet.php | 70 +++ .../Web/Dashboard/Common/OrderWidget.php | 39 ++ 4 files changed, 644 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/Common/DashboardManager.php create mode 100644 library/Icinga/Web/Dashboard/Common/DisableWidget.php create mode 100644 library/Icinga/Web/Dashboard/Common/ModuleDashlet.php create mode 100644 library/Icinga/Web/Dashboard/Common/OrderWidget.php diff --git a/library/Icinga/Web/Dashboard/Common/DashboardManager.php b/library/Icinga/Web/Dashboard/Common/DashboardManager.php new file mode 100644 index 0000000000..5ac600fab9 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Common/DashboardManager.php @@ -0,0 +1,498 @@ +loadHomesFromMenu(); + $this->loadDashboards(); + + $this->initAndGetDefaultHome(); + $this->deployModuleDashlets(); + } + + /** + * Get Database connection + * + * This is needed because we don't want to always initiate a new DB connection when calling $this->getDb(). + * And as we are using PDO transactions to manage the dashboards, this wouldn't work if $this->getDb() + * is called over again after a transaction has been initiated + * + * @return Connection + */ + public static function getConn() + { + if (self::$conn === null) { + self::$conn = (new self())->getDb(); + } + + return self::$conn; + } + + /** + * Generate the sha1 hash of the provided string + * + * @param string $name + * + * @return string + */ + public static function getSHA1($name) + { + return sha1($name, true); + } + + /** + * Load dashboard homes from the navigation menu + * + * @return $this + */ + public function loadHomesFromMenu() + { + $menu = new HomeMenu(); + foreach ($menu->getItem('dashboard')->getChildren() as $home) { + if (! $home instanceof DashboardHome) { + continue; + } + + $this->homes[$home->getName()] = $home; + } + + return $this; + } + + /** + * Load dashboards assigned to the given home or active home being loaded + * + * @param string $name + * + * @return $this + */ + public function loadDashboards($name = null) + { + if ($name && $this->hasHome($name)) { + $home = $this->getHome($name); + } else { + $requestRoute = Url::fromRequest(); + if ($requestRoute->getPath() === Dashboard::BASE_ROUTE) { + $home = $this->initAndGetDefaultHome(); + } else { + $homeParam = $requestRoute->getParam('home'); + if (empty($homeParam) || ! $this->hasHome($homeParam)) { + if (! ($home = $this->rewindHomes())) { + return $this; + } + } else { + $home = $this->getHome($homeParam); + } + } + } + + $this->activateHome($home); + $home->loadDashboardsFromDB(); + + return $this; + } + + /** + * Get a dashboard home by the given name + * + * @param string $name + * + * @return DashboardHome + */ + public function getHome($name) + { + if ($this->hasHome($name)) { + return $this->homes[$name]; + } + + throw new ProgrammingError('Trying to retrieve invalid dashboard home "%s"', $name); + } + + /** + * Get all dashboard homes assigned to the active user + * + * @return DashboardHome[] + */ + public function getHomes() + { + return $this->homes; + } + + /** + * Set this home's dashboards + * + * @param DashboardHome|DashboardHome[] $homes + */ + public function setHomes($homes) + { + if ($homes instanceof DashboardHome) { + $homes = [$homes->getName() => $homes]; + } + + $this->homes = $homes; + + return $this; + } + + /** + * Get whether the given home exist + * + * @param string $name + * + * @return bool + */ + public function hasHome($name) + { + return array_key_exists($name, $this->homes); + } + + /** + * Add a new pane to this home + * + * @param DashboardHome $home + * + * @return $this + */ + public function activateHome(DashboardHome $home) + { + $activeHome = $this->getActiveHome(); + if ($activeHome && $activeHome->getName() !== $home->getName()) { + $activeHome->setActive(false); + } + + $home->setActive(true); + + return $this; + } + + /** + * Checks if this home has any panes + * + * @return ?DashboardHome + */ + public function getActiveHome() + { + $active = null; + foreach ($this->getHomes() as $home) { + if ($home->getActive()) { + $active = $home; + + break; + } + } + + return $active; + } + + /** + * Reset the current position of the internal dashboard homes pointer + * + * @return false|DashboardHome + */ + public function rewindHomes() + { + return reset($this->homes); + } + + /** + * Remove a specific pane form this home + * + * @param DashboardHome|string $home + * + * @return $this + */ + public function removeHome($home) + { + $name = $home instanceof DashboardHome ? $home->getName() : $home; + if (! $this->hasHome($name)) { + throw new ProgrammingError('Trying to remove invalid dashboard home "%s"', $name); + } + + $home = $home instanceof DashboardHome ? $home : $this->getHome($home); + if (! $home->isDisabled()) { + $home->removePanes(); + + self::getConn()->delete(DashboardHome::TABLE, ['id = ?' => $home->getUuid()]); + } + + return $this; + } + + /** + * Remove all panes from this home, unless you specified the panes + * + * @param DashboardHome[] $homes + * + * @return $this + */ + public function removeHomes(array $homes = []) + { + $homes = ! empty($homes) ? $homes : $this->getHomes(); + foreach ($homes as $home) { + $this->removeHome($home); + } + + return $this; + } + + /** + * Manage the given pane(s) + * + * @param DashboardHome $home + * + * @return $this + */ + public function manageHome(DashboardHome $home) + { + $conn = self::getConn(); + + if (! $this->hasHome($home->getName())) { + $conn->insert(DashboardHome::TABLE, [ + 'name' => $home->getName(), + 'label' => $home->getLabel(), + 'username' => self::getUser()->getUsername(), + 'priority' => $home->getPriority(), + 'type' => $home->getType() !== Dashboard::SYSTEM ? $home->getType() : Dashboard::PRIVATE_DS + ]); + + $home->setUuid($conn->lastInsertId()); + } elseif ($home->getName() !== DashboardHome::DEFAULT_HOME) { + $conn->update(DashboardHome::TABLE, ['label' => $home->getLabel()], ['id = ?' => $home->getUuid()]); + } + + return $this; + } + + /** + * Get an array with home name=>title format + * + * @return array + */ + public function getHomeKeyTitleArr() + { + $panes = []; + foreach ($this->getHomes() as $home) { + if ($home->isDisabled()) { + continue; + } + + $panes[$home->getName()] = $home->getLabel(); + } + + return $panes; + } + + /** + * Get and/or init the default dashboard home + * + * @return DashboardHome + */ + public function initAndGetDefaultHome() + { + if ($this->hasHome(DashboardHome::DEFAULT_HOME)) { + return $this->getHome(DashboardHome::DEFAULT_HOME); + } + + $default = new DashboardHome(DashboardHome::DEFAULT_HOME); + $this->manageHome($default); + + $this->homes[$default->getName()] = $default; + + return $default; + } + + /** + * Set this dashboard's user + * + * @param User $user + * + * @return $this + */ + public function setUser(User $user) + { + self::$user = $user; + + return $this; + } + + /** + * Get this dashboard's user + * + * @return User + */ + public static function getUser() + { + if (self::$user === null) { + self::$user = Auth::getInstance()->getUser(); + } + + return self::$user; + } + + /** + * Get system defaults which are normally being + * provided by icingadb or monitoring module + * + * @return Pane[] + */ + public function getSystemDefaults() + { + return $this->defaultPanes; + } + + /** + * Browse all enabled modules configuration file and import all dashboards + * provided by them into the DB table `module_dashlet` + * + * @return void + */ + public function deployModuleDashlets() + { + $moduleManager = Icinga::app()->getModuleManager(); + foreach ($moduleManager->getLoadedModules() as $module) { + foreach ($module->getDashboard() as $dashboardPane) { + foreach ($dashboardPane->getDashlets() as $dashlet) { + $identifier = self::getSHA1( + $module->getName() . $dashboardPane->getName() . $dashlet->getName() + ); + $dashlet->setUuid($identifier); + self::updateOrInsertModuleDashlet($dashlet, $module->getName()); + } + + if (in_array($module->getName(), ['monitoring', 'icingadb'], true)) { + $this->defaultPanes[$dashboardPane->getName()] = $dashboardPane; + } + } + + $priority = 0; + foreach ($module->getDashlet() as $dashlet) { + $identifier = self::getSHA1($module->getName() . $dashlet->getName()); + + $dashlet->setUuid($identifier); + $dashlet->setPriority($priority++); + + self::updateOrInsertModuleDashlet($dashlet, $module->getName()); + } + } + } + + /** + * Get whether the given module Dashlet already exists + * + * @param Dashlet $dashlet + * + * @return bool + */ + public static function moduleDashletExist(Dashlet $dashlet) + { + $query = Model\ModuleDashlet::on(self::getConn())->filter(Filter::equal('id', $dashlet->getUuid())); + $query->getSelectBase()->columns(new Expression('1')); + + return $query->execute()->hasResult(); + } + + /** + * Insert or update the given module dashlet + * + * @param Dashlet $dashlet + * @param string $module + * + * @return void + */ + public static function updateOrInsertModuleDashlet(Dashlet $dashlet, $module) + { + if (! self::moduleDashletExist($dashlet)) { + self::getConn()->insert('module_dashlet', [ + 'id' => $dashlet->getUuid(), + 'name' => $dashlet->getName(), + 'label' => $dashlet->getTitle(), + 'pane' => $dashlet->getPane() ? $dashlet->getPane()->getName() : null, + 'module' => $module, + 'url' => $dashlet->getUrl()->getRelativeUrl(), + 'description' => $dashlet->getDescription(), + 'priority' => $dashlet->getPriority() + ]); + } else { + self::getConn()->update('module_dashlet', [ + 'label' => $dashlet->getTitle(), + 'url' => $dashlet->getUrl()->getRelativeUrl(), + 'description' => $dashlet->getDescription(), + 'priority' => $dashlet->getPriority() + ], ['id = ?' => $dashlet->getUuid()]); + } + } + + /** + * Get module dashlets from the database + * + * @return array + */ + public static function getModuleDashlets(Query $query) + { + $dashlets = []; + foreach ($query as $moduleDashlet) { + $dashlet = new Dashlet($moduleDashlet->name, $moduleDashlet->url); + if ($moduleDashlet->description) { + $dashlet->setDescription(t($moduleDashlet->description)); + } + + $dashlet->fromArray([ + 'label' => t($moduleDashlet->label), + 'priority' => $moduleDashlet->priority, + 'uuid' => $moduleDashlet->id + ]); + + if (($pane = $moduleDashlet->pane)) { + $dashlet->setPane(new Pane($pane)); + } + + $dashlets[$moduleDashlet->module][$dashlet->getName()] = $dashlet; + } + + return $dashlets; + } +} diff --git a/library/Icinga/Web/Dashboard/Common/DisableWidget.php b/library/Icinga/Web/Dashboard/Common/DisableWidget.php new file mode 100644 index 0000000000..15e7c55324 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Common/DisableWidget.php @@ -0,0 +1,37 @@ +disabled = $disable; + + return $this; + } + + /** + * Get whether this widget is disabled + * + * @return bool + */ + public function isDisabled() + { + return $this->disabled; + } +} diff --git a/library/Icinga/Web/Dashboard/Common/ModuleDashlet.php b/library/Icinga/Web/Dashboard/Common/ModuleDashlet.php new file mode 100644 index 0000000000..c1299d4390 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Common/ModuleDashlet.php @@ -0,0 +1,70 @@ +module; + } + + /** + * Set the name of the module which provides this dashlet + * + * @param string $module + * + * @return $this + */ + public function setModule($module) + { + $this->module = $module; + + return $this; + } + + /** + * Get whether this widget originates from a module + * + * @return bool + */ + public function isModuleDashlet() + { + return $this->moduleDashlet; + } + + /** + * Set whether this dashlet widget is provided by a module + * + * @param bool $moduleDashlet + * + * @return $this + */ + public function setModuleDashlet(bool $moduleDashlet) + { + $this->moduleDashlet = $moduleDashlet; + + return $this; + } +} diff --git a/library/Icinga/Web/Dashboard/Common/OrderWidget.php b/library/Icinga/Web/Dashboard/Common/OrderWidget.php new file mode 100644 index 0000000000..b726a20d40 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Common/OrderWidget.php @@ -0,0 +1,39 @@ +order = $order; + + return $this; + } + + /** + * Get the priority order of this widget + * + * @return int + */ + public function getPriority() + { + return $this->order; + } +} From 644a75d57c41aaeb1976ecce3fecf5ee0795ff75 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:33:40 +0100 Subject: [PATCH 009/106] Introduce a new `Dashlet` widget --- library/Icinga/Web/Dashboard/Dashlet.php | 334 +++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/Dashlet.php diff --git a/library/Icinga/Web/Dashboard/Dashlet.php b/library/Icinga/Web/Dashboard/Dashlet.php new file mode 100644 index 0000000000..8c3848c5d6 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Dashlet.php @@ -0,0 +1,334 @@ + 'container dashlet-sortable', + 'draggable' => 'true' + ]; + + /** + * The url of this Dashlet + * + * @var Url|null + */ + protected $url; + + /** + * Not translatable name of this dashlet + * + * @var string + */ + protected $name; + + /** + * The title being displayed on top of the dashlet + * @var + */ + protected $title; + + /** + * The pane this dashlet belongs to + * + * @var Pane + */ + protected $pane; + + /** + * The progress label being used + * + * @var string + */ + protected $progressLabel; + + /** + * Unique identifier of this dashlet + * + * @var string + */ + protected $uuid; + + /** + * The dashlet's description + * + * @var string + */ + protected $description; + + /** + * Create a new dashlet displaying the given url in the provided pane + * + * @param string $title The title to use for this dashlet + * @param Url|string $url The url this dashlet uses for displaying information + * @param Pane|null $pane The pane this Dashlet will be added to + */ + public function __construct($title, $url, Pane $pane = null) + { + $this->name = $title; + $this->title = $title; + $this->pane = $pane; + $this->url = $url; + } + + /** + * Set the identifier of this dashlet + * + * @param string $id + * + * @return $this + */ + public function setUuid($id) + { + $this->uuid = $id; + + return $this; + } + + /** + * Get the unique identifier of this dashlet + * + * @return string + */ + public function getUuid() + { + return $this->uuid; + } + + /** + * Setter for this name + * + * @param $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Getter for this name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Retrieve the dashlets title + * + * @return string + */ + public function getTitle() + { + return $this->title !== null ? $this->title : $this->getName(); + } + + /** + * Set the title of this dashlet + * + * @param string $title + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * Retrieve the dashlets url + * + * @return Url|null + */ + public function getUrl() + { + if ($this->url !== null && ! $this->url instanceof Url) { + if (Icinga::app()->isCli()) { + $this->url = Url::fromPath($this->url, [], new Request()); + } else { + $this->url = Url::fromPath($this->url); + } + } + + return $this->url; + } + + /** + * Set the dashlets URL + * + * @param string|Url $url The url to use, either as an Url object or as a path + * + * @return $this + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * Set the progress label to use + * + * @param string $label + * + * @return $this + */ + public function setProgressLabel($label) + { + $this->progressLabel = $label; + + return $this; + } + + /** + * Return the progress label to use + * + * @return string + */ + public function getProgressLabel() + { + if ($this->progressLabel === null) { + return $this->progressLabel = t('Loading'); + } + + return $this->progressLabel; + } + + /** + * Get the dashlet's description + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set the dashlet's description + * + * @param string $description + * + * @return $this + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Set the Pane of this dashlet + * + * @param Pane $pane + * + * @return Dashlet + */ + public function setPane(Pane $pane) + { + $this->pane = $pane; + + return $this; + } + + /** + * Get the pane of this dashlet + * + * @return Pane + */ + public function getPane() + { + return $this->pane; + } + + protected function assemble() + { + if (! $this->getUrl()) { + $this->addHtml(HtmlElement::create('h1', null, t($this->getTitle()))); + $this->addHtml(HtmlElement::create( + 'p', + ['class' => 'error-message'], + sprintf(t('Cannot create dashboard dashlet "%s" without valid URL'), t($this->getTitle())) + )); + } else { + $url = $this->getUrl(); + $url->setParam('showCompact', true); + + $this->setAttribute('data-icinga-url', $url); + $this->setAttribute('data-icinga-dashlet', $this->getName()); + + $this->addHtml(new HtmlElement('h1', null, new Link( + t($this->getTitle()), + $url->getUrlWithout(['showCompact', 'limit'])->getRelativeUrl(), + [ + 'aria-label' => t($this->getTitle()), + 'title' => t($this->getTitle()), + 'data-base-target' => 'col1' + ] + ))); + + $this->addHtml(HtmlElement::create( + 'p', + ['class' => 'progress-label'], + [ + $this->getProgressLabel(), + HtmlElement::create('span', null, '.'), + HtmlElement::create('span', null, '.'), + HtmlElement::create('span', null, '.'), + ] + )); + } + } + + public function toArray() + { + return [ + 'id' => $this->getUuid(), + 'pane' => $this->getPane() ? $this->getPane()->getName() : null, + 'name' => $this->getName(), + 'url' => $this->getUrl()->getRelativeUrl(), + 'label' => $this->getTitle(), + 'order' => $this->getPriority(), + 'disabled' => (int) $this->isDisabled(), + ]; + } +} From f9da975b9a73a09c7c2d9d5f8f648091624a0e8e Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:34:28 +0100 Subject: [PATCH 010/106] Introduce new `Pane` widget --- library/Icinga/Web/Dashboard/Pane.php | 545 ++++++++++++++++++++++++++ 1 file changed, 545 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/Pane.php diff --git a/library/Icinga/Web/Dashboard/Pane.php b/library/Icinga/Web/Dashboard/Pane.php new file mode 100644 index 0000000000..6247b91f1f --- /dev/null +++ b/library/Icinga/Web/Dashboard/Pane.php @@ -0,0 +1,545 @@ +name = $name; + $this->title = $name; + + if (! empty($properties)) { + $this->fromArray($properties); + } + } + + /** + * Set the name of this pane + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Returns the name of this pane + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the title of this pane + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Overwrite the title of this pane + * + * @param string $title The new title to use for this pane + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + public function override(bool $override) + { + $this->override = $override; + + return $this; + } + + public function isOverriding() + { + return $this->override; + } + + /** + * Set this pane's unique identifier + * + * @param string $uuid + * + * @return $this + */ + public function setUuid($uuid) + { + $this->uuid = $uuid; + + return $this; + } + + /** + * Get this pane's unique identifier + * + * @return string + */ + public function getUuid() + { + return $this->uuid; + } + + /** + * Set the owner of this dashboard + * + * @param string $owner + * + * @return $this + */ + public function setOwner($owner) + { + $this->owner = $owner; + + return $this; + } + + /** + * Get owner of this dashboard + * + * @return string + */ + public function getOwner() + { + return $this->owner; + } + + /** + * Set the number of users who have subscribed to this pane if (public) + * + * @param int $acceptance + */ + public function setAcceptance($acceptance) + { + $this->acceptance = $acceptance; + + return $this; + } + + /** + * Get the number of users who have subscribed to this pane if (public) + * + * @return int + */ + public function getAcceptance() + { + return $this->acceptance; + } + + /** + * Get the dashboard home this pane is a part of + * + * This may return null if it wasn't set before (should never happen for DB dashboards) + * + * @return ?DashboardHome + */ + public function getHome() + { + return $this->home; + } + + /** + * Set the dashboard home this pane is a part of + * + * @param DashboardHome $home + */ + public function setHome(DashboardHome $home) + { + $this->home = $home; + + return $this; + } + + /** + * Return true if a dashlet with the given title exists in this pane + * + * @param string $title The title of the dashlet to check for existence + * + * @return bool + */ + public function hasDashlet($title) + { + return array_key_exists($title, $this->dashlets); + } + + /** + * Checks if the current pane has any dashlets + * + * @return bool + */ + public function hasDashlets() + { + return ! empty($this->dashlets); + } + + /** + * Return a dashlet with the given name if existing + * + * @param string $title The title of the dashlet to return + * + * @return Dashlet The dashlet with the given title + * @throws ProgrammingError If the dashlet doesn't exist + */ + public function getDashlet($title) + { + if ($this->hasDashlet($title)) { + return $this->dashlets[$title]; + } + + throw new ProgrammingError('Trying to access invalid dashlet: %s', $title); + } + + /** + * Get all dashlets belongs to this pane + * + * @return Dashlet[] + */ + public function getDashlets() + { + uasort($this->dashlets, function (Dashlet $x, Dashlet $y) { + return $x->getPriority() - $y->getPriority(); + }); + + return $this->dashlets; + } + + /** + * Set dashlets of this pane + * + * @param Dashlet[] $dashlets + * + * @return $this + */ + public function setDashlets(array $dashlets) + { + $this->dashlets = $dashlets; + + return $this; + } + + /** + * Create, add and return a new dashlet + * + * @param string $title + * @param string $url + * + * @return Dashlet + */ + public function createDashlet($title, $url = null) + { + $dashlet = new Dashlet($title, $url, $this); + $this->addDashlet($dashlet); + + return $dashlet; + } + + /** + * Add a dashlet to this pane, optionally creating it if $dashlet is a string + * + * @param string|Dashlet $dashlet + * @param ?string $url + * + * @return $this + * @throws \Icinga\Exception\ConfigurationError + */ + public function addDashlet($dashlet, $url = null) + { + if ($dashlet instanceof Dashlet) { + $this->dashlets[$dashlet->getName()] = $dashlet; + } elseif (is_string($dashlet) && $url !== null) { + $this->createDashlet($dashlet, $url); + } else { + throw new ConfigurationError('Invalid dashlet added: %s', $dashlet); + } + + return $this; + } + + /** + * Add new dashlets to existing dashlets + * + * @param array $dashlets + * @return $this + */ + public function addDashlets(array $dashlets) + { + /* @var $dashlet Dashlet */ + foreach ($dashlets as $dashlet) { + if (array_key_exists($dashlet->getName(), $this->dashlets)) { + if (preg_match('/_(\d+)$/', $dashlet->getName(), $m)) { + $name = preg_replace('/_\d+$/', $m[1]++, $dashlet->getName()); + } else { + $name = $dashlet->getName() . '_2'; + } + $this->dashlets[$name] = $dashlet; + } else { + $this->dashlets[$dashlet->getName()] = $dashlet; + } + } + + return $this; + } + + /** + * Add a dashlet to the current pane @see addDashlet() + * + * @param string $title + * @param Url|string $url + * + * @return $this + */ + public function add($title, $url, $priority = 0) + { + $dashlet = $this->createDashlet($title, $url); + $dashlet->setPriority($priority); + $this->addDashlet($dashlet); + + return $this; + } + + /** + * Remove the dashlet the given dashlet if it exists in this pane + * + * @param Dashlet|string $dashlet + * + * @return $this + */ + public function removeDashlet($dashlet) + { + $name = $dashlet instanceof Dashlet ? $dashlet->getName() : $dashlet; + if (! $this->hasDashlet($name)) { + throw new ProgrammingError('Trying to remove invalid dashlet: %s', $name); + } + + if (! $dashlet instanceof Dashlet) { + $dashlet = $this->getDashlet($dashlet); + } + + Dashboard::getConn()->delete(Dashlet::TABLE, [ + 'id = ?' => $dashlet->getUuid(), + 'dashboard_id = ?' => $this->getUuid() + ]); + + return $this; + } + + /** + * Removes all or a given list of dashlets from this pane + * + * @param array $dashlets Optional list of dashlets + * + * @return $this + */ + public function removeDashlets(array $dashlets = []) + { + if (empty($dashlets)) { + $dashlets = $this->getDashlets(); + } + + foreach ($dashlets as $dashlet) { + $this->removeDashlet($dashlet); + } + + return $this; + } + + /** + * Load all dashlets this dashboard is assigned to + * + * @return $this + */ + public function loadDashletsFromDB() + { + if ($this->isDisabled()) { + return $this; + } + + $this->dashlets = []; + $dashlets = Model\Dashlet::on(Dashboard::getConn()); + $dashlets->filter(Filter::equal('dashboard_id', $this->getUuid())); + + foreach ($dashlets as $dashlet) { + $newDashlet = new Dashlet($dashlet->name, $dashlet->url, $this); + $newDashlet->fromArray([ + 'uuid' => $dashlet->id, + 'title' => t($dashlet->label), + 'priority' => $dashlet->priority, + 'pane' => $this + ]); + + $this->addDashlet($newDashlet); + } + + return $this; + } + + /** + * Manage the given dashlet(s) + * + * If you want to move the dashlet(s) from another to this pane, + * you have to also pass the origin pane + * + * @param Dashlet|Dashlet[] $dashlets + * @param ?Pane $origin + * + * @return $this + */ + public function manageDashlets($dashlets, Pane $origin = null) + { + $user = Dashboard::getUser(); + $conn = Dashboard::getConn(); + + $dashlets = is_array($dashlets) ? $dashlets : [$dashlets]; + $order = count($this->getDashlets()) + 1; + + foreach ($dashlets as $dashlet) { + if (is_array($dashlet)) { + $this->manageDashlets($dashlet, $origin); + } + + if (! $dashlet instanceof Dashlet) { + break; + } + + $uuid = $dashlet->isModuleDashlet() + ? Dashboard::getSHA1($dashlet->getModule() . $this->getName() . $dashlet->getName()) + : Dashboard::getSHA1( + $user->getUsername() . $this->getHome()->getName() . $this->getName() . $dashlet->getName() + ); + + if (! $this->hasDashlet($dashlet->getName()) && (! $origin || ! $origin->hasDashlet($dashlet->getName()))) { + $conn->insert(Dashlet::TABLE, [ + 'id' => $uuid, + 'dashboard_id' => $this->getUuid(), + 'name' => $dashlet->getName(), + 'label' => $dashlet->getTitle(), + 'url' => $dashlet->getUrl()->getRelativeUrl(), + 'priority' => $order++ + ]); + + $dashlet->setUuid($uuid); + } else { + $conn->update(Dashlet::TABLE, [ + 'id' => $uuid, + 'dashboard_id' => $this->getUuid(), + 'label' => $dashlet->getTitle(), + 'url' => $dashlet->getUrl()->getRelativeUrl(), + 'priority' => $this->getPriority() + ], ['id = ?' => $dashlet->getUuid()]); + } + + $dashlet->setPane($this); + } + + return $this; + } + + public function toArray() + { + return [ + 'id' => $this->getUuid(), + 'name' => $this->getName(), + 'label' => $this->getTitle(), + 'home' => $this->getHome() ? $this->getHome()->getName() : null, + 'priority' => $this->getPriority(), + 'disabled' => (int) $this->isDisabled() + ]; + } +} From 8a225f6f4e10a03cff15298e5d7acf132c124fbd Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:35:09 +0100 Subject: [PATCH 011/106] Navigation: Introduce `DashboardHome` navigation item class --- .../Icinga/Web/Navigation/DashboardHome.php | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 library/Icinga/Web/Navigation/DashboardHome.php diff --git a/library/Icinga/Web/Navigation/DashboardHome.php b/library/Icinga/Web/Navigation/DashboardHome.php new file mode 100644 index 0000000000..ccac015e34 --- /dev/null +++ b/library/Icinga/Web/Navigation/DashboardHome.php @@ -0,0 +1,397 @@ +getName() !== self::DEFAULT_HOME && ! $this->isDisabled()) { + $this->setUrl(Url::fromPath(Dashboard::BASE_ROUTE . '/home', [ + 'home' => $this->getName() + ])); + } + } + + /** + * Get this dashboard home's url + * + * Parent class would always report a default url if $this->url isn't + * set, which we do it on purpose. + * + * @return \Icinga\Web\Url + */ + public function getUrl() + { + return $this->url; + } + + /** + * Get whether this home has been activated + * + * @return bool + */ + public function getActive() + { + return $this->active; + } + + /** + * Set whether this home is active + * + * DB dashboard will load only when this home has been activated + * + * @param bool $active + * + * @return $this + */ + public function setActive($active = true) + { + $this->active = $active; + + return $this; + } + + /** + * Get the type of this dashboard home + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set the type of this dashboard home + * + * @param string $type + * + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get the uuid of this dashboard home + * + * @return int + */ + public function getUuid() + { + return $this->uuid; + } + + /** + * Get the uuid of this dashboard home + * + * @param int $uuid + * + * @return $this + */ + public function setUuid(int $uuid) + { + $this->uuid = $uuid; + + return $this; + } + + /** + * Get a pane with the given name if exists + * + * @param string $name + * + * @return Pane + */ + public function getPane($name) + { + if (! $this->hasPane($name)) { + throw new ProgrammingError('Trying to retrieve invalid dashboard pane "%s"', $name); + } + + return $this->panes[$name]; + } + + /** + * Get whether this home has any dashboard panes + * + * @return bool + */ + public function hasPanes() + { + return ! empty($this->panes); + } + + /** + * Get whether the given pane exist + * + * @param string $name + * + * @return bool + */ + public function hasPane($name) + { + return array_key_exists($name, $this->panes); + } + + /** + * Get all dashboards of this home + * + * @param bool $skipDisabled Whether to skip disabled dashboards + * + * @return Pane[] + */ + public function getPanes($skipDisabled = false) + { + // As the panes can also be added individually afterwards, it might be the case that the priority + // order gets mixed up, so we have to sort things here before being able to render them + uasort($this->panes, function (Pane $x, Pane $y) { + return $x->getPriority() - $y->getPriority(); + }); + + return ! $skipDisabled ? $this->panes : array_filter($this->panes, function ($pane) { + return ! $pane->isDisabled(); + }); + } + + /** + * Set dashboards of this home + * + * @param Pane|Pane[] $panes + * + * @return $this + */ + public function setPanes($panes) + { + if ($panes instanceof Pane) { + $panes = [$panes->getName() => $panes]; + } + + $this->panes = $panes; + + return $this; + } + + /** + * Add a new dashboard pane to this home + * + * @param Pane|string $pane + * + * @return $this + */ + public function addPane($pane) + { + if (! $pane instanceof Pane) { + $pane = new Pane($pane); + $pane + ->setHome($this) + ->setTitle($pane->getName()); + } + + $this->panes[$pane->getName()] = $pane; + + return $this; + } + + /** + * Get an array with pane name=>title format + * + * @return string[] + */ + public function getPaneKeyTitleArr() + { + $panes = []; + foreach ($this->getPanes(true) as $pane) { + $panes[$pane->getName()] = $pane->getName(); + } + + return $panes; + } + + /** + * Remove the given pane from this home + * + * @param Pane|string $pane + * + * @return $this + */ + public function removePane($pane) + { + $name = $pane instanceof Pane ? $pane->getName() : $pane; + if (! $this->hasPane($name)) { + throw new ProgrammingError('Trying to remove invalid dashboard pane "%s"', $name); + } + + $pane = $pane instanceof Pane ? $pane : $this->getPane($pane); + if (! $pane->isOverriding()) { + $pane->removeDashlets(); + + Dashboard::getConn()->delete(Pane::TABLE, ['id = ?' => $this->getUuid()]); + } + + return $this; + } + + /** + * Remove all/the given dashboard panes from this home + * + * @param Pane[] $panes + * + * @return $this + */ + public function removePanes(array $panes = []) + { + $panes = ! empty($panes) ? $panes : $this->getPanes(); + foreach ($panes as $pane) { + $this->removePane($pane); + } + + return $this; + } + + /** + * Load all dashboards this user is assigned to from the DB + * + * @return $this + */ + public function loadDashboardsFromDB() + { + // Skip when this home is either disabled or inactive + if (! $this->getActive() || $this->isDisabled()) { + return $this; + } + + $this->panes = []; + $panes = Model\Pane::on(Dashboard::getConn())->utilize('home'); + $panes + ->filter(Filter::equal('home_id', $this->getUuid())) + ->filter(Filter::equal('username', Dashboard::getUser()->getUsername())); + + foreach ($panes as $pane) { + $newPane = new Pane($pane->name); + //$newPane->disable($pane->disable); + $newPane->fromArray([ + 'uuid' => $pane->id, + 'title' => t($pane->label), + 'priority' => $pane->priority, + 'home' => $this + ]); + + $newPane->loadDashletsFromDB(); + + $this->panes[$newPane->getName()] = $newPane; + } + + return $this; + } + + /** + * Manage the given pane(s) + * + * If you want to move the pane(s) from another to this pane, + * you have to also pass the origin home with + * + * @param Pane|Pane[] $panes + * @param DashboardHome|null $origin + * + * @return $this + */ + public function managePanes($panes, DashboardHome $origin = null) + { + $user = Dashboard::getUser(); + $conn = Dashboard::getConn(); + + $panes = is_array($panes) ? $panes : [$panes]; + $order = count($this->getPanes()) + 1; + + foreach ($panes as $pane) { + $uuid = Dashboard::getSHA1($user->getUsername() . $this->getName() . $pane->getName()); + if (! $pane->isOverriding()) { + if (! $this->hasPane($pane->getName()) && (! $origin || ! $origin->hasPane($pane->getName()))) { + $conn->insert(Pane::TABLE, [ + 'id' => $uuid, + 'home_id' => $this->getUuid(), + 'name' => $pane->getName(), + 'label' => $pane->getTitle(), + 'username' => $user->getUsername(), + 'priority' => $order++ + ]); + } else { + $conn->update(Pane::TABLE, [ + 'id' => $uuid, + 'home_id' => $this->getUuid(), + 'label' => $pane->getTitle(), + 'priority' => $pane->getPriority() + ], ['id = ?' => $pane->getUuid()]); + } + + // We may want to update some dashlets later, so we need to set the uuid + // here too in case the pane might be moved from another home + $pane->setUuid($uuid); + } else { + // TODO(TBD) + } + + $pane->setHome($this); + } + + return $this; + } +} From dab899e1befaafc39093873372e6ed69ec5a854f Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:36:03 +0100 Subject: [PATCH 012/106] Introduce a new dashboard manager --- library/Icinga/Web/Dashboard/Dashboard.php | 281 +++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/Dashboard.php diff --git a/library/Icinga/Web/Dashboard/Dashboard.php b/library/Icinga/Web/Dashboard/Dashboard.php new file mode 100644 index 0000000000..2c85187475 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Dashboard.php @@ -0,0 +1,281 @@ + 'dashboard content']; + + /** + * The @see Tabs object for displaying displayable panes + * + * @var Tabs + */ + protected $tabs; + + /** + * The parameter that will be added to identify panes + * + * @var string + */ + private $tabParam = 'pane'; + + /** + * A welcome form rendered when there is no dashboard panes + * + * @var Form + */ + private $welcomeForm; + + /** + * Set the given tab name as active + * + * @param string $name The tab name to activate + * + */ + public function activate($name) + { + $this->getTabs()->activate($name); + } + + /** + * Set this dashboard's tabs + * + * @param Tabs $tabs + * + * @return $this + */ + public function setTabs(Tabs $tabs) + { + $this->tabs = $tabs; + + return $this; + } + + /** + * Return the tab object used to navigate through this dashboard + * + * @return Tabs + */ + public function getTabs() + { + $activeHome = $this->getActiveHome(); + if ($activeHome && $activeHome->getName() !== DashboardHome::DEFAULT_HOME) { + $url = Url::fromPath(self::BASE_ROUTE . '/home')->getUrlWithout(['home', $this->tabParam]); + $url->addParams(['home' => $activeHome->getName()]); + } else { + $url = Url::fromPath(self::BASE_ROUTE)->getUrlWithout($this->tabParam); + } + + if ($this->tabs === null) { + $this->tabs = new Tabs(); + } + + $this->tabs->disableLegacyExtensions(); + if (! $activeHome || $activeHome->isDisabled()) { + return $this->tabs; + } + + foreach ($activeHome->getPanes() as $key => $pane) { + if ($pane->isDisabled()) { + continue; + } + + if (! $this->tabs->get($key)) { + $this->tabs->add( + $key, + [ + 'title' => sprintf( + t('Show %s', 'dashboard.pane.tooltip'), + $pane->getTitle() + ), + 'label' => $pane->getTitle(), + 'url' => clone($url), + 'urlParams' => [$this->tabParam => $key] + ] + ); + } + } + + return $this->tabs; + } + + /** + * Activate the default pane of this dashboard and returns its name + * + * @return ?int|string + */ + private function setDefaultPane() + { + $active = null; + $activeHome = $this->getActiveHome(); + + foreach ($activeHome->getPanes() as $key => $pane) { + if ($pane->isDisabled() === false) { + $active = $key; + break; + } + } + + if ($active !== null) { + $this->activate($active); + } + + return $active; + } + + /** + * @see determineActivePane() + */ + public function getActivePane() + { + return $this->determineActivePane(); + } + + /** + * Determine the active pane either by the selected tab or the current request + * + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\ProgrammingError + * + * @return Pane The currently active pane + */ + public function determineActivePane() + { + $active = $this->getTabs()->getActiveTab(); + $activeHome = $this->getActiveHome(); + + if (! $active) { + if ($active = Url::fromRequest()->getParam($this->tabParam)) { + if ($activeHome->hasPane($active)) { + $this->activate($active); + } else { + throw new ProgrammingError('Try to get an inexistent pane.'); + } + } else { + $active = $this->setDefaultPane(); + } + } else { + $active = $active->getName(); + } + + $panes = $activeHome->getPanes(); + if (isset($panes[$active])) { + return $panes[$active]; + } + + throw new ConfigurationError('Could not determine active pane'); + } + + public function setWelcomeForm(Form $form) + { + $this->welcomeForm = $form; + + return $this; + } + + protected function assemble() + { + $activeHome = $this->getActiveHome(); + if (! $activeHome || $activeHome->getName() === DashboardHome::DEFAULT_HOME && ! $activeHome->hasPanes()) { + $this->setAttribute('class', 'content welcome-view'); + $wrapper = HtmlElement::create('div', ['class' => 'dashboard-introduction']); + + $wrapper->addHtml(HtmlElement::create('h1', null, t('Welcome to Icinga Web 2!'))); + $wrapper->addHtml(HtmlElement::create( + 'p', + null, + t('You will see this screen every time you log in and haven\'t created any dashboards yet.') + )); + + $message = t('At the moment this view is empty, but you can populate it with small portions of information called Dashlets.'); + $wrapper->addHtml(HtmlElement::create('p', null, $message)); + + $message = t( + 'Now you can either customize which dashlets to display, or use the system default dashlets.' + . ' You will be always able to edit them afterwards.' + ); + $wrapper->addHtml(HtmlElement::create('p', null, $message)); + + $wrapper->addHtml($this->welcomeForm); + $this->addHtml($wrapper); + } elseif (! empty($activeHome->getPanes(true))) { + $dashlets = $this->getActivePane()->getDashlets(); + $this->setAttribute('data-icinga-pane', $activeHome->getName() . '|' . $this->getActivePane()->getName()); + + if (empty($dashlets)) { + $this->setAttribute('class', 'content'); + $dashlets = HtmlElement::create('h1', null, t('No dashlet added to this pane.')); + } + + $this->add($dashlets); + } else { + // TODO: What to do with dashboard homes without any dashboards?? + exit(0); + } + } +} From b42fcd96f5deaa6a7a75595edb2c2b6e67baee7b Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:36:42 +0100 Subject: [PATCH 013/106] Module: Use the new dashbaord widget for providing dashbaords and dashlets --- library/Icinga/Application/Modules/Module.php | 92 ++++++++++++------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index a4af342204..09ec732fbd 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -13,11 +13,14 @@ use Icinga\Exception\ProgrammingError; use Icinga\Module\Setup\SetupWizard; use Icinga\Util\File; +use Icinga\Web\Dashboard; use Icinga\Web\Navigation\Navigation; -use Icinga\Web\Widget; use ipl\I18n\GettextTranslator; use ipl\I18n\StaticTranslator; use ipl\I18n\Translation; +use ipl\Web\Url; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use Zend_Controller_Router_Route; use Zend_Controller_Router_Route_Abstract; use Zend_Controller_Router_Route_Regex; @@ -214,9 +217,16 @@ class Module /** * A set of Pane elements * - * @var array + * @var Dashboard\Pane[] */ - protected $paneItems = array(); + protected $paneItems = []; + + /** + * A set of Dashlet elements + * + * @var Dashboard\Dashlet[] + */ + protected $dashletItems = []; /** * A set of objects representing a searchUrl configuration @@ -307,49 +317,38 @@ public function getSearchUrls() /** * Return this module's dashboard * - * @return Navigation + * @return Dashboard\Pane[] */ public function getDashboard() { $this->launchConfigScript(); - return $this->createDashboard($this->paneItems); + return $this->paneItems; } /** * Create and return a new navigation for the given dashboard panes * - * @param DashboardContainer[] $panes + * @param DashboardContainer[] $paneItems * - * @return Navigation + * @return DashboardContainer[] */ - public function createDashboard(array $panes) + public function createDashboard(array $paneItems) { - $navigation = new Navigation(); - foreach ($panes as $pane) { - /** @var DashboardContainer $pane */ + $panes = []; + foreach ($paneItems as $pane) { $dashlets = []; foreach ($pane->getDashlets() as $dashletName => $dashletConfig) { - $dashlets[$dashletName] = [ - 'label' => $this->translate($dashletName), - 'url' => $dashletConfig['url'], - 'priority' => $dashletConfig['priority'] - ]; + $dashlet = new Dashboard\Dashlet($dashletName, $dashletConfig['url']); + $dashlet->fromArray($dashletConfig); + + $dashlets[$dashletName] = $dashlet; } - $navigation->addItem( - $pane->getName(), - array_merge( - $pane->getProperties(), - array( - 'label' => $this->translate($pane->getName()), - 'type' => 'dashboard-pane', - 'children' => $dashlets - ) - ) - ); + $pane->setDashlets($dashlets); + $panes[$pane->getName()] = $pane; } - return $navigation; + return $panes; } /** @@ -358,19 +357,50 @@ public function createDashboard(array $panes) * @param string $name * @param array $properties * - * @return DashboardContainer + * @return Dashboard\Pane */ protected function dashboard($name, array $properties = array()) { if (array_key_exists($name, $this->paneItems)) { - $this->paneItems[$name]->setProperties($properties); + $this->paneItems[$name]->fromArray($properties); } else { - $this->paneItems[$name] = new DashboardContainer($name, $properties); + $this->paneItems[$name] = new Dashboard\Pane($name, $properties); } return $this->paneItems[$name]; } + /** + * Get this module's provided dashlets + * + * @return Dashboard\Dashlet[] + */ + public function getDashlet() + { + $this->launchConfigScript(); + return $this->dashletItems; + } + + /** + * Add or get a dashlet + * + * @param string $name + * @param Url|string $url + * @param string $description + * + * @return Dashboard\Dashlet + */ + protected function provideDashlet($name, $url, $description) + { + if (! array_key_exists($name, $this->dashletItems)) { + $this->dashletItems[$name] = new Dashboard\Dashlet($name, $url); + } + + $this->dashletItems[$name]->setDescription($description); + + return $this->dashletItems[$name]; + } + /** * Return this module's menu * From 42581e8d7f09d5a4eb04edbe2615cd8e09108fe1 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:37:55 +0100 Subject: [PATCH 014/106] Dashboard: Introduce `OverridingWidget` interface --- .../Icinga/Web/Dashboard/OverridingWidget.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/OverridingWidget.php diff --git a/library/Icinga/Web/Dashboard/OverridingWidget.php b/library/Icinga/Web/Dashboard/OverridingWidget.php new file mode 100644 index 0000000000..541a442dd0 --- /dev/null +++ b/library/Icinga/Web/Dashboard/OverridingWidget.php @@ -0,0 +1,24 @@ + Date: Fri, 11 Mar 2022 16:39:19 +0100 Subject: [PATCH 015/106] Introduce `DashletListItem` class --- .../Dashboard/ItemList/DashletListItem.php | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/ItemList/DashletListItem.php diff --git a/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php b/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php new file mode 100644 index 0000000000..a010016bf7 --- /dev/null +++ b/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php @@ -0,0 +1,89 @@ + 'dashlet-list-item']; + + protected $tag = 'li'; + + /** @var Dashlet */ + protected $dashlet; + + protected $renderEditButton; + + public function __construct(Dashlet $dashlet = null, $renderEditButton = false) + { + $this->dashlet = $dashlet; + $this->renderEditButton = $renderEditButton; + } + + /** + * Set whether to render an edit button for this dashlet + * + * @param bool $value + * + * @return $this + */ + protected function setDetailUrl(bool $value) + { + $this->renderEditButton = $value; + + return $this; + } + + protected function assembleTitle() + { + $title = HtmlElement::create('h1', ['class' => 'dashlet-header']); + + if (! $this->dashlet) { + $title->add(t('Custom Url')); + } else { + $title->add($this->dashlet->getTitle()); + + if ($this->renderEditButton) { + $pane = $this->dashlet->getPane(); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-dashlet'); + $url->setParams([ + 'home' => $pane->getHome()->getName(), + 'pane' => $pane->getName(), + 'dashlet' => $this->dashlet->getName() + ]); + + $title->addHtml(new Link(t('Edit'), $url, [ + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ])); + } + } + + return $title; + } + + protected function assembleSummary() + { + $section = HtmlElement::create('section', ['class' => 'caption']); + + if (! $this->dashlet) { + $section->add(t('Create a dashlet with custom url and filter')); + } else { + $section->add($this->dashlet->getDescription() ?: $this->dashlet->getTitle()); + } + + return $section; + } + + protected function assemble() + { + $this->addHtml($this->assembleTitle()); + $this->addHtml($this->assembleSummary()); + } +} From c01d44dbf197c9c2aadc390578cc6684666b252d Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:39:55 +0100 Subject: [PATCH 016/106] Introduce `DashletlistMultiSelect` class --- .../ItemList/DashletListMultiSelect.php | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/ItemList/DashletListMultiSelect.php diff --git a/library/Icinga/Web/Dashboard/ItemList/DashletListMultiSelect.php b/library/Icinga/Web/Dashboard/ItemList/DashletListMultiSelect.php new file mode 100644 index 0000000000..2040d4d71e --- /dev/null +++ b/library/Icinga/Web/Dashboard/ItemList/DashletListMultiSelect.php @@ -0,0 +1,41 @@ +checkbox = $checkbox; + + return $this; + } + + protected function createLabel() + { + $label = HtmlElement::create('label'); + $label->addHtml($this->checkbox); + + return $label; + } + + protected function assemble() + { + parent::assemble(); + + $this->addWrapper($this->createLabel()); + } +} From 1120bc0d60046f06afa42206bc622476a5375959 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:41:06 +0100 Subject: [PATCH 017/106] SearchDashboard: Provide support for the enhanced dashboards --- application/controllers/SearchController.php | 10 ++- library/Icinga/Web/Widget/SearchDashboard.php | 62 ++++++++++++------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/application/controllers/SearchController.php b/application/controllers/SearchController.php index 92aeabe54d..0b8b55983c 100644 --- a/application/controllers/SearchController.php +++ b/application/controllers/SearchController.php @@ -3,23 +3,21 @@ namespace Icinga\Controllers; -use Icinga\Web\Controller\ActionController; -use Icinga\Web\Widget; use Icinga\Web\Widget\SearchDashboard; +use ipl\Web\Compat\CompatController; /** * Search controller */ -class SearchController extends ActionController +class SearchController extends CompatController { public function indexAction() { $searchDashboard = new SearchDashboard(); $searchDashboard->setUser($this->Auth()->getUser()); - $this->view->dashboard = $searchDashboard->search($this->params->get('q')); - // NOTE: This renders the dashboard twice. Remove this once we can catch exceptions thrown in view scripts. - $this->view->dashboard->render(); + $this->controls->setTabs($searchDashboard->getTabs()); + $this->content = $searchDashboard->search($this->getParam('q')); } public function hintAction() diff --git a/library/Icinga/Web/Widget/SearchDashboard.php b/library/Icinga/Web/Widget/SearchDashboard.php index 1ce4c464b2..f6301f0496 100644 --- a/library/Icinga/Web/Widget/SearchDashboard.php +++ b/library/Icinga/Web/Widget/SearchDashboard.php @@ -3,15 +3,23 @@ namespace Icinga\Web\Widget; -use Zend_Controller_Action_Exception; +use Icinga\Exception\Http\HttpNotFoundException; use Icinga\Application\Icinga; +use Icinga\Web\Navigation\DashboardHome; use Icinga\Web\Url; /** * Class SearchDashboard display multiple search views on a single search page */ -class SearchDashboard extends Dashboard +class SearchDashboard extends \Icinga\Web\Dashboard\Dashboard { + /** + * Name for the search home + * + * @var string + */ + const SEARCH_HOME = 'Search Home'; + /** * Name for the search pane * @@ -20,12 +28,23 @@ class SearchDashboard extends Dashboard const SEARCH_PANE = 'search'; /** - * {@inheritdoc} + * Dashboard home of this search dashboard + * + * @var DashboardHome */ + protected $searchHome; + + public function __construct() + { + $this->searchHome = new DashboardHome(self::SEARCH_HOME); + $this->searchHome->setActive(); + } + public function getTabs() { if ($this->tabs === null) { - $this->tabs = new Tabs(); + $this->tabs = new \ipl\Web\Widget\Tabs(); + $this->tabs->add( 'search', array( @@ -35,9 +54,15 @@ public function getTabs() ) ); } + return $this->tabs; } + public function getActiveHome() + { + return $this->searchHome; + } + /** * Load all available search dashlets from modules * @@ -47,18 +72,18 @@ public function getTabs() */ public function search($searchString = '') { - $pane = $this->createPane(self::SEARCH_PANE)->getPane(self::SEARCH_PANE)->setTitle(t('Search')); + $pane = $this->searchHome->addPane(self::SEARCH_PANE)->getPane(self::SEARCH_PANE)->setTitle(t('Search')); $this->activate(self::SEARCH_PANE); $manager = Icinga::app()->getModuleManager(); $searchUrls = array(); foreach ($manager->getLoadedModules() as $module) { - if ($this->getUser()->can($manager::MODULE_PERMISSION_NS . $module->getName())) { + if (self::getUser()->can($manager::MODULE_PERMISSION_NS . $module->getName())) { $moduleSearchUrls = $module->getSearchUrls(); if (! empty($moduleSearchUrls)) { if ($searchString === '') { - $pane->add(t('Ready to search'), 'search/hint'); + $pane->addDashlet(t('Ready to search'), 'search/hint'); return $this; } $searchUrls = array_merge($searchUrls, $moduleSearchUrls); @@ -69,28 +94,21 @@ public function search($searchString = '') usort($searchUrls, array($this, 'compareSearchUrls')); foreach (array_reverse($searchUrls) as $searchUrl) { - $pane->createDashlet( - $searchUrl->title . ': ' . $searchString, - Url::fromPath($searchUrl->url, array('q' => $searchString)) - )->setProgressLabel(t('Searching')); + $title = $searchUrl->title . ': ' . $searchString; + $pane->addDashlet($title, Url::fromPath($searchUrl->url, array('q' => $searchString))); + $pane->getDashlet($title)->setProgressLabel(t('Searching')); } return $this; } - /** - * Renders the output - * - * @return string - * - * @throws Zend_Controller_Action_Exception - */ - public function render() + protected function assemble() { - if (! $this->getPane(self::SEARCH_PANE)->hasDashlets()) { - throw new Zend_Controller_Action_Exception(t('Page not found'), 404); + if (! $this->searchHome->getPane(self::SEARCH_PANE)->hasDashlets()) { + throw new HttpNotFoundException(t('Page not found')); } - return parent::render(); + + $this->add($this->searchHome->getPane(self::SEARCH_PANE)->getDashlets()); } /** From 4addf5291ef8ba4108aedab19900278a2a4fcec2 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:43:05 +0100 Subject: [PATCH 018/106] Menu: Provide own class for dashboard homes navigation items --- library/Icinga/Web/HomeMenu.php | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 library/Icinga/Web/HomeMenu.php diff --git a/library/Icinga/Web/HomeMenu.php b/library/Icinga/Web/HomeMenu.php new file mode 100644 index 0000000000..423048c8ad --- /dev/null +++ b/library/Icinga/Web/HomeMenu.php @@ -0,0 +1,41 @@ +initHome(); + } + + public function initHome() + { + $user = Dashboard::getUser(); + $dashboardItem = $this->getItem('dashboard'); + + $homes = Home::on(Dashboard::getConn()); + $homes->filter(Filter::equal('username', $user->getUsername())); + + foreach ($homes as $home) { + $dashboardHome = new DashboardHome($home->name, [ + 'uuid' => $home->id, + 'label' => t($home->label), + 'priority' => $home->priority, + 'type' => $home->type, + ]); + + $dashboardItem->addChild($dashboardHome); + } + } +} From 06d9e9a57ad04317902e4ba4d082fc56abe89a6c Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:44:31 +0100 Subject: [PATCH 019/106] Controllers: Introduce own dashboards controller --- .../controllers/DashboardsController.php | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 application/controllers/DashboardsController.php diff --git a/application/controllers/DashboardsController.php b/application/controllers/DashboardsController.php new file mode 100644 index 0000000000..af288f8d54 --- /dev/null +++ b/application/controllers/DashboardsController.php @@ -0,0 +1,375 @@ +dashboard = new Dashboard(); + $this->dashboard->setUser($this->Auth()->getUser()); + $this->dashboard->setTabs($this->getTabs()); + $this->dashboard->load(); + } + + public function indexAction() + { + $this->createTabs(); + + $activeHome = $this->dashboard->getActiveHome(); + if (!$activeHome || ! $activeHome->hasPanes()) { + $this->getTabs()->add('dashboard', [ + 'active' => true, + 'title' => $this->translate('Welcome'), + 'url' => Url::fromRequest() + ]); + + // Setup dashboard introduction form + $welcomeForm = new WelcomeForm($this->dashboard); + $welcomeForm->on(WelcomeForm::ON_SUCCESS, function () use ($welcomeForm) { + $this->redirectNow($welcomeForm->getRedirectUrl()); + })->handleRequest(ServerRequest::fromGlobals()); + + $this->dashboard->setWelcomeForm($welcomeForm); + } elseif (empty($activeHome->getPanes(true))) { + // TODO(TBD): What to do when the user has only disabled dashboards? Should we render the welcome screen? + } else { + if ($this->getParam('pane')) { + $pane = $this->getParam('pane'); + $this->getTabs()->activate($pane); + } + } + + $this->content = $this->dashboard; + } + + /** + * Display all the dashboards assigned to a Home set in the `home` request param + * + * If no pane param is submitted, the default pane is displayed (usually the first one) + */ + public function homeAction() + { + $home = $this->params->getRequired('home'); + if (! $this->dashboard->hasHome($home)) { + $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); + } + + $this->createTabs(); + + $activeHome = $this->dashboard->getActiveHome(); + if (! $activeHome || empty($activeHome->getPanes(true))) { + $this->getTabs()->add($home, [ + 'active' => true, + 'title' => $home, + 'url' => Url::fromRequest() + ]); + } elseif (($pane = $this->getParam('param'))) { + $this->dashboard->activate($pane); + } + + $this->content = $this->dashboard; + } + + public function renameHomeAction() + { + $this->setTitle(t('Update Home')); + + $home = $this->params->getRequired('home'); + if (! $this->dashboard->hasHome($home)) { + $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + } + + $homeForm = new HomePaneForm($this->dashboard); + $homeForm->on(HomePaneForm::ON_SUCCESS, function () { + $this->redirectNow('__CLOSE__'); + })->handleRequest(ServerRequest::fromGlobals()); + + $homeForm->load($this->dashboard->getActiveHome()); + $this->addContent($homeForm); + } + + public function removeHomeAction() + { + $this->setTitle(t('Remove Home')); + + $home = $this->params->getRequired('home'); + if (! $this->dashboard->hasHome($home)) { + $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + } + + $homeForm = (new RemoveHomePaneForm($this->dashboard)) + ->on(RemoveHomePaneForm::ON_SUCCESS, function () { + $this->redirectNow('__CLOSE__'); + }) + ->handleRequest(ServerRequest::fromGlobals()); + + $this->addContent($homeForm); + } + + public function editPaneAction() + { + $this->setTitle(t('Update Pane')); + + $pane = $this->params->getRequired('pane'); + $home = $this->params->getRequired('home'); + + if (! $this->dashboard->hasHome($home)) { + $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + } + + if (! $this->dashboard->getActiveHome()->hasPane($pane)) { + $this->httpNotFound(sprintf($this->translate('Pane "%s" not found'), $pane)); + } + + $paneForm = new HomePaneForm($this->dashboard); + $paneForm->on(HomePaneForm::ON_SUCCESS, function () use ($pane) { + $this->redirectNow('__CLOSE__'); + })->handleRequest(ServerRequest::fromGlobals()); + + $paneForm->load($this->dashboard->getActiveHome()->getPane($pane)); + + $this->addContent($paneForm); + } + + public function removePaneAction() + { + $this->setTitle(t('Remove Pane')); + + $home = $this->params->getRequired('home'); + $paneParam = $this->params->getRequired('pane'); + + if (! $this->dashboard->hasHome($home)) { + $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + } + + if (! $this->dashboard->getActiveHome()->hasPane($paneParam)) { + $this->httpNotFound(sprintf($this->translate('Pane "%s" not found'), $paneParam)); + } + + $paneForm = new RemoveHomePaneForm($this->dashboard); + $paneForm->populate(['org_name' => $paneParam]); + $paneForm->on(RemoveHomePaneForm::ON_SUCCESS, function () { + $this->redirectNow('__CLOSE__'); + })->handleRequest(ServerRequest::fromGlobals()); + + $paneForm->getElement('btn_remove')->setLabel(t('Remove Pane')); + $paneForm->prependHtml(HtmlElement::create('h1', null, sprintf( + t('Please confirm removal of dashboard pane "%s"'), + $paneParam + ))); + + $this->addContent($paneForm); + } + + public function newDashletAction() + { + $this->setTitle(t('Add Dashlet To Dashboard')); + + $dashletForm = new DashletForm($this->dashboard); + $dashletForm->populate($this->getRequest()->getPost()); + $dashletForm->on(DashletForm::ON_SUCCESS, function () use ($dashletForm) { + $this->redirectNow('__CLOSE__'); + })->handleRequest(ServerRequest::fromGlobals()); + + $params = $this->getAllParams(); + if ($this->getParam('url')) { + $params['url'] = rawurldecode($this->getParam('url')); + } + + $dashletForm->populate($params); + + $this->addContent($dashletForm); + } + + public function editDashletAction() + { + $this->setTitle(t('Edit Dashlet')); + + $pane = $this->validateDashletParams(); + $dashlet = $pane->getDashlet($this->getParam('dashlet')); + + $dashletForm = new DashletForm($this->dashboard); + $dashletForm->on(DashletForm::ON_SUCCESS, function () use ($dashletForm, $pane) { + $this->redirectNow('__CLOSE__'); + })->handleRequest(ServerRequest::fromGlobals()); + + $dashletForm->getElement('submit')->setLabel(t('Update Dashlet')); + + $dashletForm->load($dashlet); + $this->addContent($dashletForm); + } + + public function removeDashletAction() + { + $this->validateDashletParams(); + $this->setTitle(t('Remove Dashlet')); + + $removeForm = (new RemoveDashletForm($this->dashboard)) + ->on(RemoveDashletForm::ON_SUCCESS, function () { + $this->redirectNow('__CLOSE__'); + }) + ->handleRequest(ServerRequest::fromGlobals()); + + $this->addContent($removeForm); + } + + public function reorderDashletsAction() + { + $this->assertHttpMethod('post'); + if (! $this->getRequest()->isApiRequest()) { + $this->httpBadRequest('No API request'); + } + + if ( + ! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches) + || $matches[1] !== 'application/json' + ) { + $this->httpBadRequest('No JSON content'); + } + + $dashboards = $this->getRequest()->getPost(); + foreach ($dashboards as $home => $panes) { + if (! $this->dashboard->hasHome($home)) { + $this->httpNotFound(sprintf($this->translate('Dashboard home "%s" not found'), $home)); + } + + $home = $this->dashboard->getHome($home); + $home->setActive(); + $home->loadDashboardsFromDB(); + + foreach ($panes as $pane => $dashlets) { + if (! $home->hasPane($pane)) { + $this->httpNotFound(sprintf($this->translate('Dashboard pane "%s" not found'), $pane)); + } + + $pane = $home->getPane($pane); + foreach ($dashlets as $order => $dashlet) { + if (! $pane->hasDashlet($dashlet)) { + $this->httpNotFound(sprintf($this->translate('Dashlet "%s" not found'), $pane)); + } + + Dashboard::getConn()->update(Dashlet::TABLE, ['priority' => $order], [ + 'id = ?' => $pane->getDashlet($dashlet)->getUuid(), + 'dashboard_id = ?' => $pane->getUuid() + ]); + } + } + } + + exit; + } + + /** + * Provides a mini wizard which guides a new user through the dashboard creation + * process and helps them get a first impression of Icinga Web 2. + */ + public function setupDashboardAction() + { + if (isset($this->getRequest()->getPost()['btn_next'])) { + // Set compact view to prevent the controls from being + // rendered in the modal view when redirecting + $this->view->compact = true; + + $this->getResponse()->setHeader('X-Icinga-Title', 'Configure Dashlets', true); + } else { + $this->setTitle($this->translate('Add Dashlet')); + } + + $query = ModuleDashlet::on(Dashboard::getConn()); + + $setupForm = new SetupNewDashboard($this->dashboard); + $setupForm->initDashlets(Dashboard::getModuleDashlets($query)); + $setupForm->on(SetupNewDashboard::ON_SUCCESS, function () use ($setupForm) { + if ($setupForm->getPopulatedValue('btn_cancel')) { + $this->redirectNow('__CLOSE__'); + } + + $this->redirectNow($setupForm->getRedirectUrl()); + })->handleRequest(ServerRequest::fromGlobals()); + + $this->addContent($setupForm); + } + + public function settingsAction() + { + $this->createTabs(); + // TODO(yh): This may raise an exception when the given tab name doesn't exist. + // But as ipl::Tabs() doesn't offer the possibility to check this beforehand, just ignore it for now!! + $this->dashboard->activate('dashboard_settings'); + + $this->addControl(new ActionLink( + $this->translate('Add new Home'), + Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'), + 'plus', + [ + 'class' => 'add-home', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ] + )); + + $this->content = new Settings($this->dashboard); + } + + /** + * Create tab aggregation + */ + private function createTabs() + { + $tabs = $this->dashboard->getTabs(); + $activeHome = $this->dashboard->getActiveHome(); + if ($activeHome && $activeHome->hasPanes()) { + $tabs->extend(new DashboardSettings()); + } + + return $tabs; + } + + private function validateDashletParams() + { + $home = $this->params->getRequired('home'); + $pane = $this->params->getRequired('pane'); + $dashlet = $this->params->getRequired('dashlet'); + + if (! $this->dashboard->hasHome($home)) { + $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + } + + if (! $this->dashboard->getActiveHome()->hasPane($pane)) { + $this->httpNotFound(sprintf($this->translate('Pane "%s" not found'), $pane)); + } + + $pane = $this->dashboard->getActiveHome()->getPane($pane); + if (! $pane->hasDashlet($dashlet)) { + $this->httpNotFound(sprintf($this->translate('Dashlet "%s" not found'), $dashlet)); + } + + return $pane; + } +} From ef528a0f3f9d3df621dbab55e7311c3864a869d7 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:45:07 +0100 Subject: [PATCH 020/106] Make use of the new controller by default --- application/forms/Authentication/LoginForm.php | 3 ++- application/layouts/scripts/body.phtml | 3 ++- application/views/scripts/layout/menu.phtml | 2 +- library/Icinga/Web/Menu.php | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/application/forms/Authentication/LoginForm.php b/application/forms/Authentication/LoginForm.php index 8a71ecf554..fd7173b7ea 100644 --- a/application/forms/Authentication/LoginForm.php +++ b/application/forms/Authentication/LoginForm.php @@ -11,6 +11,7 @@ use Icinga\Authentication\User\ExternalBackend; use Icinga\Common\Database; use Icinga\User; +use Icinga\Web\Dashboard\Dashboard; use Icinga\Web\Form; use Icinga\Web\RememberMe; use Icinga\Web\Url; @@ -27,7 +28,7 @@ class LoginForm extends Form /** * Redirect URL */ - const REDIRECT_URL = 'dashboard'; + const REDIRECT_URL = Dashboard::BASE_ROUTE; public static $defaultElementDecorators = [ ['ViewHelper', ['separator' => '']], diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml index 87b570bfe5..fea90ee7bc 100644 --- a/application/layouts/scripts/body.phtml +++ b/application/layouts/scripts/body.phtml @@ -1,5 +1,6 @@ layout()->inlineLayout) {
qlink( '', - Auth::getInstance()->isAuthenticated() ? 'dashboard' : '', + Auth::getInstance()->isAuthenticated() ? Dashboard::BASE_ROUTE : '', null, array( 'aria-hidden' => 'true', diff --git a/application/views/scripts/layout/menu.phtml b/application/views/scripts/layout/menu.phtml index c82d5cc846..6aa04e1b0a 100644 --- a/application/views/scripts/layout/menu.phtml +++ b/application/views/scripts/layout/menu.phtml @@ -5,7 +5,7 @@ use Icinga\Web\Widget\SearchDashboard; $searchDashboard = new SearchDashboard(); $searchDashboard->setUser($this->Auth()->getUser()); -if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?> +if ($searchDashboard->search('dummy')->getActiveHome()->getPane('search')->hasDashlets()): ?>
From ff90450b7093632cae235fb269662253ae8927f7 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:47:44 +0100 Subject: [PATCH 022/106] DashboardSettings: Don't add `Add Dashlet` as a dropdown menu anymore --- .../Widget/Tabextension/DashboardSettings.php | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php b/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php index fc7412aec5..ff05fd7391 100644 --- a/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php +++ b/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php @@ -4,13 +4,29 @@ namespace Icinga\Web\Widget\Tabextension; use Icinga\Web\Url; +use Icinga\Web\Dashboard\Dashboard; use Icinga\Web\Widget\Tabs; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; /** * Dashboard settings */ class DashboardSettings implements Tabextension { + /** @var array|null url params to be attached to the dropdown menus. */ + private $urlParam; + + /** + * DashboardSettings constructor. + * + * @param array $urlParam + */ + public function __construct(array $urlParam = []) + { + $this->urlParam = $urlParam; + } + /** * Apply this tabextension to the provided tabs * @@ -18,22 +34,15 @@ class DashboardSettings implements Tabextension */ public function apply(Tabs $tabs) { - $tabs->addAsDropdown( - 'dashboard_add', - array( - 'icon' => 'dashboard', - 'label' => t('Add Dashlet'), - 'url' => Url::fromPath('dashboard/new-dashlet') - ) - ); - - $tabs->addAsDropdown( + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/settings'); + $url = empty($this->urlParam) ? $url : $url->addParams($this->urlParam); + $tabs->add( 'dashboard_settings', - array( - 'icon' => 'dashboard', - 'label' => t('Settings'), - 'url' => Url::fromPath('dashboard/settings') - ) + [ + 'icon' => 'service', + 'url' => (string) $url, + 'priority' => -100 + ] ); } } From 22450ab88ad7df640f03e74e32bdf01906711e71 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:48:59 +0100 Subject: [PATCH 023/106] DashletForm: Add support for the new dashboards and make use of ipl form --- application/forms/Dashboard/DashletForm.php | 394 +++++++++++++------- 1 file changed, 264 insertions(+), 130 deletions(-) diff --git a/application/forms/Dashboard/DashletForm.php b/application/forms/Dashboard/DashletForm.php index 1af65a954a..922670ebfa 100644 --- a/application/forms/Dashboard/DashletForm.php +++ b/application/forms/Dashboard/DashletForm.php @@ -1,171 +1,305 @@ setName('form_dashboard_addurl'); - if (! $this->getSubmitLabel()) { - $this->setSubmitLabel($this->translate('Add To Dashboard')); - } - $this->setAction(Url::fromRequest()); + $this->dashboard = $dashboard; + + $this->setAction((string) Url::fromRequest()); } - /** - * Build AddUrl form elements - * - * @see Form::createElements() - */ - public function createElements(array $formData) + public function hasBeenSubmitted() { - $groupElements = array(); - $panes = array(); + return $this->hasBeenSent() && $this->getPopulatedValue('submit'); + } - if ($this->dashboard) { - $panes = $this->dashboard->getPaneKeyTitleArray(); - } + protected function assemble() + { + $requestUrl = Url::fromRequest(); + + $homes = $this->dashboard->getHomeKeyTitleArr(); + $activeHome = $this->dashboard->getActiveHome(); + $currentHome = $requestUrl->getParam('home', reset($homes)); + $populatedHome = $this->getPopulatedValue('home', $currentHome); + + $panes = []; + if ($currentHome === $populatedHome && $this->getPopulatedValue('create_new_home') !== 'y') { + if (! $currentHome || ! $activeHome) { + // Home param isn't passed through, so let's try to load based on the first home + $firstHome = $this->dashboard->rewindHomes(); + if ($firstHome) { + $this->dashboard->loadDashboards($firstHome->getName()); - $sectionNameValidator = ['Callback', true, [ - 'callback' => function ($value) { - if (strpos($value, '[') === false && strpos($value, ']') === false) { - return true; + $panes = $firstHome->getPaneKeyTitleArr(); } - }, - 'messages' => [ - 'callbackValue' => $this->translate('Brackets ([, ]) cannot be used here') - ] - ]]; - - $this->addElement( - 'hidden', - 'org_pane', - array( - 'required' => false - ) - ); - - $this->addElement( - 'hidden', - 'org_dashlet', - array( - 'required' => false - ) - ); - - $this->addElement( - 'textarea', - 'url', - array( + } else { + $panes = $activeHome->getPaneKeyTitleArr(); + } + } elseif ($this->dashboard->hasHome($populatedHome)) { + $this->dashboard->loadDashboards($populatedHome); + + $panes = $this->dashboard->getActiveHome()->getPaneKeyTitleArr(); + } + + $this->addElement('hidden', 'org_pane', ['required' => false]); + $this->addElement('hidden', 'org_home', ['required' => false]); + $this->addElement('hidden', 'org_dashlet', ['required' => false]); + + $this->addElement('checkbox', 'create_new_home', [ + 'class' => 'autosubmit', + 'required' => false, + 'disabled' => empty($homes) ?: null, + 'label' => t('New Dashboard Home'), + 'description' => t('Check this box if you want to add the dashboard to a new dashboard home.'), + ]); + + if (empty($homes) || $this->getPopulatedValue('create_new_home') === 'y') { + // $el->attrs->set() has no effect here anymore, so we need to register a proper callback + $this->getElement('create_new_home') + ->getAttributes() + ->registerAttributeCallback('checked', function () { return true; }); + + $this->addElement('text', 'home', [ 'required' => true, - 'label' => $this->translate('Url'), - 'description' => $this->translate( - 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters.' - ), - 'validators' => array(new UrlValidator(), new InternalUrlValidator()) - ) - ); - $this->addElement( - 'text', - 'dashlet', - array( + 'label' => t('Dashboard Home'), + 'description' => t('Enter a title for the new dashboard home.') + ]); + } else { + $this->addElement('select', 'home', [ 'required' => true, - 'label' => $this->translate('Dashlet Title'), - 'description' => $this->translate('Enter a title for the dashlet.'), - 'validators' => [$sectionNameValidator] - ) - ); - $this->addElement( - 'note', - 'note', - array( - 'decorators' => array( - array('HtmlTag', array('tag' => 'hr')) - ) - ) - ); - $this->addElement( - 'checkbox', - 'create_new_pane', - array( - 'autosubmit' => true, - 'required' => false, - 'label' => $this->translate('New dashboard'), - 'description' => $this->translate('Check this box if you want to add the dashlet to a new dashboard') - ) - ); - if (empty($panes) || ((isset($formData['create_new_pane']) && $formData['create_new_pane'] != false))) { - $this->addElement( - 'text', - 'pane', - array( - 'required' => true, - 'label' => $this->translate('New Dashboard Title'), - 'description' => $this->translate('Enter a title for the new dashboard'), - 'validators' => [$sectionNameValidator] - ) - ); + 'class' => 'autosubmit', + 'value' => $currentHome, + 'multiOptions' => $homes, + 'label' => t('Dashboard Home'), + 'descriptions' => t('Select a home you want to add the dashboard pane to.') + ]); + } + + $disable = empty($panes) || $this->getPopulatedValue('create_new_home') === 'y'; + $this->addElement('checkbox', 'create_new_pane', [ + 'required' => false, + 'class' => 'autosubmit', + 'disabled' => $disable ?: null, + 'label' => t('New Dashboard'), + 'description' => t('Check this box if you want to add the dashlet to a new dashboard.'), + ]); + + // Pane element's values are depending on the home element's value + if (! in_array($this->getPopulatedValue('pane'), $panes)) { + $this->clearPopulatedValue('pane'); + } + + if ($disable || $this->getValue('create_new_pane') === 'y') { + // $el->attrs->set() has no effect here anymore, so we need to register a proper callback + $this->getElement('create_new_pane') + ->getAttributes() + ->registerAttributeCallback('checked', function () { return true; }); + + $this->addElement('text', 'pane', [ + 'required' => true, + 'label' => t('New Dashboard Title'), + 'description' => t('Enter a title for the new dashboard.'), + ]); } else { - $this->addElement( - 'select', - 'pane', - array( - 'required' => true, - 'label' => $this->translate('Dashboard'), - 'multiOptions' => $panes, - 'description' => $this->translate('Select a dashboard you want to add the dashlet to') - ) - ); + $this->addElement('select', 'pane', [ + 'required' => true, + 'value' => reset($panes), + 'multiOptions' => $panes, + 'label' => t('Dashboard'), + 'description' => t('Select a dashboard you want to add the dashlet to.'), + ]); } - } - /** - * @param \Icinga\Web\Widget\Dashboard $dashboard - */ - public function setDashboard(Dashboard $dashboard) - { - $this->dashboard = $dashboard; - } + $this->addHtml(new HtmlElement('hr')); - /** - * @return \Icinga\Web\Widget\Dashboard - */ - public function getDashboard() - { - return $this->dashboard; + $this->addElement('textarea', 'url', [ + 'required' => true, + 'label' => t('Url'), + 'description' => t( + 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters.' + ), + ]); + + $this->addElement('text', 'dashlet', [ + 'required' => true, + 'label' => t('Dashlet Title'), + 'description' => t('Enter a title for the dashlet.'), + ]); + + $url = (string) Url::fromPath(Dashboard::BASE_ROUTE . '/browse'); + + $element = $this->createElement('submit', 'submit', ['label' => t('Add to Dashboard')]); + $this->registerElement($element)->decorate($element); + + // We might need this later to allow the user to browse dashlets when creating a dashlet + $this->addElement('submit', 'btn_browse', [ + 'label' => t('Browse Dashlets'), + 'href' => $url, + 'formaction' => $url, + ]); + + $this->getElement('btn_browse')->setWrapper($element->getWrapper()); } /** + * Populate form data from config + * * @param Dashlet $dashlet */ public function load(Dashlet $dashlet) { + $home = Url::fromRequest()->getParam('home'); $this->populate(array( - 'pane' => $dashlet->getPane()->getTitle(), + 'org_home' => $home, 'org_pane' => $dashlet->getPane()->getName(), - 'dashlet' => $dashlet->getTitle(), + 'pane' => $dashlet->getPane()->getTitle(), 'org_dashlet' => $dashlet->getName(), + 'dashlet' => $dashlet->getTitle(), 'url' => $dashlet->getUrl()->getRelativeUrl() )); } + + protected function onSuccess() + { + $conn = Dashboard::getConn(); + $dashboard = $this->dashboard; + + if (Url::fromRequest()->getPath() === Dashboard::BASE_ROUTE . '/new-dashlet') { + $home = new DashboardHome($this->getValue('home')); + if ($dashboard->hasHome($home->getName())) { + $home = $dashboard->getHome($home->getName()); + if ($home->getName() !== $dashboard->getActiveHome()->getName()) { + $home->setActive(); + $home->loadDashboardsFromDB(); + } + } + + $pane = new Pane($this->getValue('pane')); + if ($home->hasPane($pane->getName())) { + $pane = $home->getPane($pane->getName()); + } + + $dashlet = new Dashlet($this->getValue('dashlet'), $this->getValue('url'), $pane); + if ($pane->hasDashlet($dashlet->getName())) { + Notification::error(sprintf( + t('Dashlet "%s" already exists within the "%s" dashboard pane'), + $dashlet->getTitle(), + $pane->getTitle() + )); + + return; + } + + $conn->beginTransaction(); + + try { + $dashboard->manageHome($home); + $home->managePanes($pane); + $pane->manageDashlets($dashlet); + + $conn->commitTransaction(); + } catch (\Exception $err) { // This error handling is just for debugging purpose! Will be removed! + Logger::error($err); + $conn->rollBackTransaction(); + + throw $err; + } + + Notification::success(sprintf(t('Created dashlet "%s" successfully'), $dashlet->getTitle())); + } else { + $orgHome = $dashboard->getHome($this->getValue('org_home')); + $orgPane = $orgHome->getPane($this->getValue('org_pane')); + $orgDashlet = $orgPane->getDashlet($this->getValue('org_dashlet')); + + $currentHome = new DashboardHome($this->getValue('home')); + if ($dashboard->hasHome($currentHome->getName())) { + $currentHome = $dashboard->getHome($currentHome->getName()); + $activeHome = $dashboard->getActiveHome(); + if ($currentHome->getName() !== $activeHome->getName()) { + $currentHome->setActive(); + $currentHome->loadDashboardsFromDB(); + } + } + + $currentPane = new Pane($this->getValue('pane')); + if ($currentHome->hasPane($currentPane->getName())) { + $currentPane = $currentHome->getPane($currentPane->getName()); + } + + $currentPane->setHome($currentHome); + + $currentDashlet = clone $orgDashlet; + $currentDashlet + ->setPane($currentPane) + ->setUrl($this->getValue('url')) + ->setTitle($this->getValue('dashlet')); + + if ( + $orgPane->getName() !== $currentPane->getName() + && $currentPane->hasDashlet($currentDashlet->getName()) + ) { + Notification::error(sprintf( + t('Failed to move dashlet "%s": Dashlet already exists within the "%s" dashboard pane'), + $currentDashlet->getTitle(), + $currentPane->getTitle() + )); + + return; + } + + $paneDiff = array_filter(array_diff_assoc($currentPane->toArray(), $orgPane->toArray())); + $dashletDiff = array_filter( + array_diff_assoc($currentDashlet->toArray(), $orgDashlet->toArray()), + function ($val) { + return $val !== null; + } + ); + + // Prevent meaningless updates when there weren't any changes, + // e.g. when the user just presses the update button without changing anything + if (empty($dashletDiff) && empty($paneDiff)) { + return; + } + + $conn->beginTransaction(); + + try { + $dashboard->manageHome($currentHome); + $currentHome->managePanes($currentPane, $orgHome); + $currentPane->manageDashlets($currentDashlet, $orgPane); + + $conn->commitTransaction(); + } catch (\Exception $err) { + Logger::error($err); + $conn->rollBackTransaction(); + + throw $err; + } + + Notification::success(sprintf(t('Updated dashlet "%s" successfully'), $currentDashlet->getTitle())); + } + } } From dcaf62573cf6323e30238eb093eb61c3b4e29319 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:51:07 +0100 Subject: [PATCH 024/106] Forms: Introduce `RemoveDashletForm` class --- .../forms/Dashboard/RemoveDashletForm.php | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 application/forms/Dashboard/RemoveDashletForm.php diff --git a/application/forms/Dashboard/RemoveDashletForm.php b/application/forms/Dashboard/RemoveDashletForm.php new file mode 100644 index 0000000000..a5dbd2b252 --- /dev/null +++ b/application/forms/Dashboard/RemoveDashletForm.php @@ -0,0 +1,46 @@ +dashboard = $dashboard; + + $this->setAction((string) Url::fromRequest()); + } + + protected function assemble() + { + $this->addHtml(HtmlElement::create('h1', null, sprintf( + t('Please confirm removal of dashlet "%s"'), + Url::fromRequest()->getParam('dashlet') + ))); + + $this->addElement('submit', 'remove_dashlet', ['label' => t('Remove Dashlet')]); + } + + protected function onSuccess() + { + $requestUrl = Url::fromRequest(); + $home = $this->dashboard->getActiveHome(); + $pane = $home->getPane($requestUrl->getParam('pane')); + + $dashlet = $requestUrl->getParam('dashlet'); + $pane->removeDashlet($dashlet); + + Notification::success(sprintf(t('Removed dashlet "%s" successfully'), $dashlet)); + } +} From f55f496078758765ff61962b797fca1eb97214f9 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:51:43 +0100 Subject: [PATCH 025/106] Forms: Introduce `HomePaneForm` class --- application/forms/Dashboard/HomePaneForm.php | 148 +++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 application/forms/Dashboard/HomePaneForm.php diff --git a/application/forms/Dashboard/HomePaneForm.php b/application/forms/Dashboard/HomePaneForm.php new file mode 100644 index 0000000000..d6b15906b3 --- /dev/null +++ b/application/forms/Dashboard/HomePaneForm.php @@ -0,0 +1,148 @@ +dashboard = $dashboard; + + // We need to set this explicitly needed for modals + $this->setAction((string) Url::fromRequest()); + } + + /** + * Populate form data from config + * + * @param DashboardHome|Pane $widget + */ + public function load($widget) + { + $title = $widget instanceof Pane ? $widget->getTitle() : $widget->getLabel(); + $this->populate([ + 'org_title' => $title, + 'title' => $title, + 'org_name' => $widget->getName() + ]); + } + + protected function assemble() + { + $this->addElement('hidden', 'org_name', ['required' => false]); + $this->addElement('hidden', 'org_title', ['required' => false]); + + $titleDesc = t('Edit the title of this dashboard home'); + $buttonLabel = t('Update Home'); + if (Url::fromRequest()->getPath() === Dashboard::BASE_ROUTE . '/edit-pane') { + $titleDesc = t('Edit the title of this dashboard pane'); + $buttonLabel = t('Update Pane'); + + $homes = $this->dashboard->getHomeKeyTitleArr(); + $this->addElement('checkbox', 'create_new_home', [ + 'required' => false, + 'class' => 'autosubmit', + 'disabled' => empty($homes) ?: null, + 'label' => t('New Dashboard Home'), + 'description' => t('Check this box if you want to move the pane to a new dashboard home.'), + ]); + + $activeHome = $this->dashboard->getActiveHome(); + $populatedHome = $this->getPopulatedValue('home', $activeHome->getName()); + + if (empty($homes) || $this->getPopulatedValue('create_new_home') === 'y') { + $this->getElement('create_new_home')->addAttributes(['checked' => 'checked']); + + $this->addElement('text', 'home', [ + 'required' => true, + 'label' => t('Dashboard Home'), + 'description' => t('Enter a title for the new dashboard home.'), + ]); + } else { + $this->addElement('select', 'home', [ + 'required' => true, + 'class' => 'autosubmit', + 'value' => $populatedHome, + 'multiOptions' => $homes, + 'label' => t('Move to Home'), + 'description' => t('Select a dashboard home you want to move the dashboard to.'), + ]); + } + } + + $this->addElement('text', 'title', [ + 'required' => true, + 'label' => t('Title'), + 'description' => $titleDesc + ]); + + $this->addElement('submit', 'btn_update', ['label' => $buttonLabel]); + } + + protected function onSuccess() + { + $requestUrl = Url::fromRequest(); + if ($requestUrl->getPath() === Dashboard::BASE_ROUTE . '/edit-pane') { + $orgHome = $this->dashboard->getHome($requestUrl->getParam('home')); + + $currentHome = new DashboardHome($this->getValue('home')); + if ($this->dashboard->hasHome($currentHome->getName())) { + $currentHome = $this->dashboard->getHome($currentHome->getName()); + $activeHome = $this->dashboard->getActiveHome(); + if ($currentHome->getName() !== $activeHome->getName()) { + $currentHome->setActive(); + $currentHome->loadDashboardsFromDB(); + } + } + + $currentPane = $orgHome->getPane($this->getValue('org_name')); + $currentPane + ->setHome($currentHome) + ->setTitle($this->getValue('title')); + + if ($orgHome->getName() !== $currentHome->getName() && $currentHome->hasPane($currentPane->getName())) { + Notification::error(sprintf( + t('Failed to move dashboard "%s": Dashbaord pane already exists within the "%s" dashboard home'), + $currentPane->getTitle(), + $currentHome->getLabel() + )); + + return; + } + + $conn = Dashboard::getConn(); + $conn->beginTransaction(); + + try { + $this->dashboard->manageHome($currentHome); + $currentHome->managePanes($currentPane, $orgHome); + + $conn->commitTransaction(); + } catch (\Exception $err) { + Logger::error($err); + $conn->rollBackTransaction(); + } + + Notification::success(sprintf(t('Updated dashboard pane "%s" successfully'), $currentPane->getTitle())); + } else { + $home = $this->dashboard->getActiveHome(); + $home->setLabel($this->getValue('title')); + + $this->dashboard->manageHome($home); + Notification::success(sprintf(t('Updated dashboard home "%s" successfully'), $home->getLabel())); + } + } +} From c47a84730537dab66756c28c60e0db167b6b1031 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:52:28 +0100 Subject: [PATCH 026/106] Forms: Introduce `RemoveHomePaneForm` class --- .../forms/Dashboard/RemoveHomePaneForm.php | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 application/forms/Dashboard/RemoveHomePaneForm.php diff --git a/application/forms/Dashboard/RemoveHomePaneForm.php b/application/forms/Dashboard/RemoveHomePaneForm.php new file mode 100644 index 0000000000..c29818f1c5 --- /dev/null +++ b/application/forms/Dashboard/RemoveHomePaneForm.php @@ -0,0 +1,45 @@ +dashboard = $dashboard; + + $this->setAction((string) Url::fromRequest()); + } + + protected function assemble() + { + $this->addElement('submit', 'btn_remove', ['label' => t('Remove Home')]); + } + + protected function onSuccess() + { + $requestUrl = Url::fromRequest(); + $home = $this->dashboard->getActiveHome(); + + if ($requestUrl->getPath() === Dashboard::BASE_ROUTE . '/remove-home') { + $this->dashboard->removeHome($home); + + Notification::success(sprintf(t('Removed dashboard home "%s" successfully'), $home->getLabel())); + } else { + $pane = $home->getPane($requestUrl->getParam('pane')); + $home->removePane($pane); + + Notification::success(sprintf(t('Removed dashboard pane "%s" successfully'), $pane->getTitle())); + } + } +} From b28cc417883cb415c29f1c4131647c9741355168 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:52:57 +0100 Subject: [PATCH 027/106] Forms: Introduce `WelcomeForm` class --- application/forms/Dashboard/WelcomeForm.php | 58 +++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 application/forms/Dashboard/WelcomeForm.php diff --git a/application/forms/Dashboard/WelcomeForm.php b/application/forms/Dashboard/WelcomeForm.php new file mode 100644 index 0000000000..37173c1e7c --- /dev/null +++ b/application/forms/Dashboard/WelcomeForm.php @@ -0,0 +1,58 @@ +dashboard = $dashboard; + $this->setRedirectUrl(Url::fromPath(Dashboard::BASE_ROUTE)); + } + + public function hasBeenSubmitted() + { + return parent::hasBeenSubmitted() || $this->getPressedSubmitElement(); + } + + protected function assemble() + { + $element = $this->createElement('submit', 'btn_use_defaults', ['label' => t('Use System Defaults')]); + $this->registerElement($element)->decorate($element); + + $this->addElement('submit', 'btn_customize_dashlets', [ + 'label' => t('Add Dashlets Now'), + 'href' => Url::fromPath(Dashboard::BASE_ROUTE . '/setup-dashboard'), + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ]); + + $this->getElement('btn_customize_dashlets')->setWrapper($element->getWrapper()); + } + + protected function onSuccess() + { + if ($this->getPopulatedValue('btn_use_defaults')) { + $order = 0; + $home = $this->dashboard->getHome(DashboardHome::DEFAULT_HOME); + foreach ($this->dashboard->getSystemDefaults() as $pane) { + $pane->setPriority($order++); + $home->managePanes($pane); + + $dashlets = $pane->getDashlets(); + $pane->setDashlets([]); + $pane->manageDashlets($dashlets); + } + } + } +} From a121c9571d271e445e481b3b3232209475444186 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:53:32 +0100 Subject: [PATCH 028/106] Dashboard: Introduce `DashbaordList` widget --- .../Web/Dashboard/ItemList/DashboardList.php | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/ItemList/DashboardList.php diff --git a/library/Icinga/Web/Dashboard/ItemList/DashboardList.php b/library/Icinga/Web/Dashboard/ItemList/DashboardList.php new file mode 100644 index 0000000000..83c9c8b33a --- /dev/null +++ b/library/Icinga/Web/Dashboard/ItemList/DashboardList.php @@ -0,0 +1,73 @@ + 'dashboard-item-list']; + + protected $tag = 'ul'; + + /** @var Pane */ + protected $pane; + + public function __construct(Pane $pane) + { + $this->pane = $pane; + + $this->getAttributes()->add('class', $pane->getName()); + } + + protected function assemble() + { + // TODO: How should disabled dashboard panes look like? + $wrapper = HtmlElement::create('div', [ + 'class' => 'dashboard-list-control collapsible', + 'data-toggle-element' => '.dashlets-list-info' + ]); + + $wrapper->addHtml(HtmlElement::create('div', ['class' => 'dashlets-list-info'], [ + new Icon('angle-down', ['class' => 'expand-icon', 'title' => t('Expand')]), + new Icon('angle-up', ['class' => 'collapse-icon', 'title' => t('Collapse')]) + ])); + + $header = HtmlElement::create('h1', ['class' => 'collapsible-header'], $this->pane->getTitle()); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-pane')->setParams([ + 'home' => $this->pane->getHome()->getName(), + 'pane' => $this->pane->getName() + ]); + + $header->addHtml(new Link(t('Edit'), $url, [ + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ])); + + $wrapper->addHtml($header); + + foreach ($this->pane->getDashlets() as $dashlet) { + $this->addHtml(new DashletListItem($dashlet, true)); + } + + $wrapper->addHtml(new ActionLink( + t('Add Dashlet'), + Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet')->addParams(['pane' => $this->pane->getName()]), + 'plus', + [ + 'class' => 'add-dashlet', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ] + )); + + $this->addWrapper($wrapper); + } +} From 3dcf9fa02a21a3eb444dd4c005f13e1e063bd381 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:54:13 +0100 Subject: [PATCH 029/106] Dashboard: Introduce `DashboardHomeList` widget --- .../Dashboard/ItemList/DashboardHomeList.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php diff --git a/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php b/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php new file mode 100644 index 0000000000..5e1c39ddf7 --- /dev/null +++ b/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php @@ -0,0 +1,72 @@ + 'home-item-list']; + + protected $tag = 'ul'; + + /** @var DashboardHome */ + protected $home; + + public function __construct(DashboardHome $home) + { + $this->home = $home; + $this->home->setActive(); + $this->home->loadDashboardsFromDB(); + + $this->getAttributes()->add('class', $home->getName()); + } + + protected function assemble() + { + $wrapper = HtmlElement::create('div', [ + 'class' => 'home-list-control collapsible', + 'data-toggle-element' => '.dashboard-list-info' + ]); + + $wrapper->addHtml(HtmlElement::create('div', ['class' => 'dashboard-list-info'], [ + new Icon('angle-down', ['class' => 'expand-icon', 'title' => t('Expand')]), + new Icon('angle-up', ['class' => 'collapse-icon', 'title' => t('Collapse')]) + ])); + + $header = HtmlElement::create('h1', ['class' => 'collapsible-header home'], $this->home->getLabel()); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/rename-home')->setParams(['home' => $this->home->getName()]); + + $header->addHtml(new Link(t('Edit'), $url, [ + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ])); + + $wrapper->addHtml($header); + + // List all dashboard panes + foreach ($this->home->getPanes() as $pane) { + $pane->setHome($this->home); // In case it's not set + + $this->addHtml(new DashboardList($pane)); + } + + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'); + $url->setParams(['home' => $this->home->getName()]); + + $wrapper->addHtml(new ActionLink(t('Add Dashboard'), $url, 'plus', [ + 'class' => 'add-dashboard', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ])); + + $this->addWrapper($wrapper); + } +} From 9e4d09fd7eb61233009233f0b874a4df64aed600 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:55:08 +0100 Subject: [PATCH 030/106] Dashboard: Introduce `Settings` class to manage to the new dashbaords --- library/Icinga/Web/Dashboard/Settings.php | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 library/Icinga/Web/Dashboard/Settings.php diff --git a/library/Icinga/Web/Dashboard/Settings.php b/library/Icinga/Web/Dashboard/Settings.php new file mode 100644 index 0000000000..c85502552e --- /dev/null +++ b/library/Icinga/Web/Dashboard/Settings.php @@ -0,0 +1,59 @@ + ['dashboard-settings content']]; + + protected $tag = 'div'; + + /** @var Dashboard */ + protected $dashboard; + + public function __construct(Dashboard $dashboard) + { + $this->dashboard = $dashboard; + } + + protected function assemble() + { + // TODO: What we should with disabled homes?? + $activeHome = $this->dashboard->getActiveHome(); + + if (empty($this->dashboard->getHomes())) { + // TODO: No dashboard homes :( what should we render now?? + } elseif (count($this->dashboard->getHomes()) === 1 && $activeHome->getName() === DashboardHome::DEFAULT_HOME) { + foreach ($activeHome->getPanes() as $pane) { + $pane->setHome($activeHome); + + $this->addHtml(new DashboardList($pane)); + } + + $this->addHtml(new ActionLink( + t('Add Dashboard'), + Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'), + 'plus', + [ + 'class' => 'add-dashboard', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ] + )); + } else { + // Make a list of dashbaord homes + foreach ($this->dashboard->getHomes() as $home) { + $this->addHtml(new DashboardHomeList($home)); + } + } + } +} From 731cd2aef834255658123543c4ed44517ac85123 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 11 Mar 2022 16:55:55 +0100 Subject: [PATCH 031/106] Dashboard: Provide mini welcome wizard for new users --- .../controllers/DashboardsController.php | 138 +++++---- application/forms/Dashboard/DashletForm.php | 113 +++---- application/forms/Dashboard/HomePaneForm.php | 36 +-- application/forms/Dashboard/WelcomeForm.php | 24 +- etc/schema/dashboards.sql | 9 +- library/Icinga/Application/Modules/Module.php | 6 +- library/Icinga/Common/DataExtractor.php | 2 +- library/Icinga/Model/DashboardOverride.php | 2 +- library/Icinga/Model/Dashlet.php | 14 +- library/Icinga/Model/Home.php | 8 +- library/Icinga/Model/ModuleDashlet.php | 22 +- library/Icinga/Model/Pane.php | 8 +- library/Icinga/Model/SystemDashlet.php | 36 +++ .../Web/Dashboard/Common/DashboardManager.php | 145 +++++---- .../Web/Dashboard/Common/ItemListControl.php | 40 +++ library/Icinga/Web/Dashboard/Dashboard.php | 13 +- library/Icinga/Web/Dashboard/Dashlet.php | 34 +-- .../Dashboard/ItemList/DashboardHomeList.php | 67 +++-- .../Web/Dashboard/ItemList/DashboardList.php | 69 ++--- .../Dashboard/ItemList/DashletListItem.php | 25 +- .../Icinga/Web/Dashboard/OverridingWidget.php | 2 +- library/Icinga/Web/Dashboard/Pane.php | 171 +++++------ library/Icinga/Web/Dashboard/Settings.php | 12 +- .../Web/Dashboard/Setup/SetupNewDashboard.php | 279 ++++++++++++++++++ library/Icinga/Web/HomeMenu.php | 8 +- .../Icinga/Web/Navigation/DashboardHome.php | 71 +++-- public/css/icinga/dashboards.less | 57 ++-- public/js/icinga/behavior/dashboards.js | 216 +++++++++++--- 28 files changed, 1106 insertions(+), 521 deletions(-) create mode 100644 library/Icinga/Model/SystemDashlet.php create mode 100644 library/Icinga/Web/Dashboard/Common/ItemListControl.php create mode 100644 library/Icinga/Web/Dashboard/Setup/SetupNewDashboard.php diff --git a/application/controllers/DashboardsController.php b/application/controllers/DashboardsController.php index af288f8d54..9cd791b217 100644 --- a/application/controllers/DashboardsController.php +++ b/application/controllers/DashboardsController.php @@ -12,10 +12,8 @@ use Icinga\Forms\Dashboard\WelcomeForm; use Icinga\Model\ModuleDashlet; use Icinga\Web\Dashboard\Dashboard; -use Icinga\Web\Dashboard\Dashlet; use Icinga\Web\Dashboard\Settings; use Icinga\Web\Dashboard\Setup\SetupNewDashboard; -use Icinga\Web\Navigation\DashboardHome; use Icinga\Web\Widget\Tabextension\DashboardSettings; use ipl\Html\HtmlElement; use ipl\Web\Compat\CompatController; @@ -42,11 +40,11 @@ public function indexAction() $this->createTabs(); $activeHome = $this->dashboard->getActiveHome(); - if (!$activeHome || ! $activeHome->hasPanes()) { + if (! $activeHome || ! $activeHome->hasPanes()) { $this->getTabs()->add('dashboard', [ 'active' => true, - 'title' => $this->translate('Welcome'), - 'url' => Url::fromRequest() + 'title' => t('Welcome'), + 'url' => Url::fromRequest() ]); // Setup dashboard introduction form @@ -58,11 +56,8 @@ public function indexAction() $this->dashboard->setWelcomeForm($welcomeForm); } elseif (empty($activeHome->getPanes(true))) { // TODO(TBD): What to do when the user has only disabled dashboards? Should we render the welcome screen? - } else { - if ($this->getParam('pane')) { - $pane = $this->getParam('pane'); - $this->getTabs()->activate($pane); - } + } elseif (($pane = $this->getParam('pane'))) { + $this->getTabs()->activate($pane); } $this->content = $this->dashboard; @@ -85,11 +80,11 @@ public function homeAction() $activeHome = $this->dashboard->getActiveHome(); if (! $activeHome || empty($activeHome->getPanes(true))) { $this->getTabs()->add($home, [ - 'active' => true, - 'title' => $home, - 'url' => Url::fromRequest() + 'active' => true, + 'title' => $home, + 'url' => Url::fromRequest() ]); - } elseif (($pane = $this->getParam('param'))) { + } elseif (($pane = $this->getParam('pane'))) { $this->dashboard->activate($pane); } @@ -102,13 +97,14 @@ public function renameHomeAction() $home = $this->params->getRequired('home'); if (! $this->dashboard->hasHome($home)) { - $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } - $homeForm = new HomePaneForm($this->dashboard); - $homeForm->on(HomePaneForm::ON_SUCCESS, function () { - $this->redirectNow('__CLOSE__'); - })->handleRequest(ServerRequest::fromGlobals()); + $homeForm = (new HomePaneForm($this->dashboard)) + ->on(HomePaneForm::ON_SUCCESS, function () { + $this->redirectNow('__CLOSE__'); + }) + ->handleRequest(ServerRequest::fromGlobals()); $homeForm->load($this->dashboard->getActiveHome()); $this->addContent($homeForm); @@ -120,7 +116,7 @@ public function removeHomeAction() $home = $this->params->getRequired('home'); if (! $this->dashboard->hasHome($home)) { - $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } $homeForm = (new RemoveHomePaneForm($this->dashboard)) @@ -140,17 +136,18 @@ public function editPaneAction() $home = $this->params->getRequired('home'); if (! $this->dashboard->hasHome($home)) { - $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } if (! $this->dashboard->getActiveHome()->hasPane($pane)) { - $this->httpNotFound(sprintf($this->translate('Pane "%s" not found'), $pane)); + $this->httpNotFound(sprintf(t('Pane "%s" not found'), $pane)); } - $paneForm = new HomePaneForm($this->dashboard); - $paneForm->on(HomePaneForm::ON_SUCCESS, function () use ($pane) { - $this->redirectNow('__CLOSE__'); - })->handleRequest(ServerRequest::fromGlobals()); + $paneForm = (new HomePaneForm($this->dashboard)) + ->on(HomePaneForm::ON_SUCCESS, function () { + $this->redirectNow('__CLOSE__'); + }) + ->handleRequest(ServerRequest::fromGlobals()); $paneForm->load($this->dashboard->getActiveHome()->getPane($pane)); @@ -165,16 +162,16 @@ public function removePaneAction() $paneParam = $this->params->getRequired('pane'); if (! $this->dashboard->hasHome($home)) { - $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } if (! $this->dashboard->getActiveHome()->hasPane($paneParam)) { - $this->httpNotFound(sprintf($this->translate('Pane "%s" not found'), $paneParam)); + $this->httpNotFound(sprintf(t('Pane "%s" not found'), $paneParam)); } $paneForm = new RemoveHomePaneForm($this->dashboard); $paneForm->populate(['org_name' => $paneParam]); - $paneForm->on(RemoveHomePaneForm::ON_SUCCESS, function () { + $paneForm->on(RemoveHomePaneForm::ON_SUCCESS, function () { $this->redirectNow('__CLOSE__'); })->handleRequest(ServerRequest::fromGlobals()); @@ -193,7 +190,7 @@ public function newDashletAction() $dashletForm = new DashletForm($this->dashboard); $dashletForm->populate($this->getRequest()->getPost()); - $dashletForm->on(DashletForm::ON_SUCCESS, function () use ($dashletForm) { + $dashletForm->on(DashletForm::ON_SUCCESS, function () { $this->redirectNow('__CLOSE__'); })->handleRequest(ServerRequest::fromGlobals()); @@ -214,10 +211,11 @@ public function editDashletAction() $pane = $this->validateDashletParams(); $dashlet = $pane->getDashlet($this->getParam('dashlet')); - $dashletForm = new DashletForm($this->dashboard); - $dashletForm->on(DashletForm::ON_SUCCESS, function () use ($dashletForm, $pane) { - $this->redirectNow('__CLOSE__'); - })->handleRequest(ServerRequest::fromGlobals()); + $dashletForm = (new DashletForm($this->dashboard)) + ->on(DashletForm::ON_SUCCESS, function () { + $this->redirectNow('__CLOSE__'); + }) + ->handleRequest(ServerRequest::fromGlobals()); $dashletForm->getElement('submit')->setLabel(t('Update Dashlet')); @@ -231,7 +229,7 @@ public function removeDashletAction() $this->setTitle(t('Remove Dashlet')); $removeForm = (new RemoveDashletForm($this->dashboard)) - ->on(RemoveDashletForm::ON_SUCCESS, function () { + ->on(RemoveDashletForm::ON_SUCCESS, function () { $this->redirectNow('__CLOSE__'); }) ->handleRequest(ServerRequest::fromGlobals()); @@ -239,45 +237,61 @@ public function removeDashletAction() $this->addContent($removeForm); } - public function reorderDashletsAction() + /** + * Handles all widgets drag and drop requests + */ + public function reorderWidgetsAction() { $this->assertHttpMethod('post'); if (! $this->getRequest()->isApiRequest()) { $this->httpBadRequest('No API request'); } - if ( - ! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches) - || $matches[1] !== 'application/json' - ) { + if (! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches) + || $matches[1] !== 'application/json') { $this->httpBadRequest('No JSON content'); } $dashboards = $this->getRequest()->getPost(); - foreach ($dashboards as $home => $panes) { + $widgetType = array_pop($dashboards); + + foreach ($dashboards as $key => $panes) { + $home = $widgetType === 'Homes' ? $panes : $key; if (! $this->dashboard->hasHome($home)) { - $this->httpNotFound(sprintf($this->translate('Dashboard home "%s" not found'), $home)); + $this->httpNotFound(sprintf(t('Dashboard home "%s" not found'), $home)); } $home = $this->dashboard->getHome($home); + if ($widgetType === 'Homes') { + $home->setPriority($key); + $this->dashboard->manageHome($home); + + continue; + } + $home->setActive(); - $home->loadDashboardsFromDB(); + $home->loadPanesFromDB(); - foreach ($panes as $pane => $dashlets) { + foreach ($panes as $innerKey => $value) { + $pane = $widgetType === 'Dashboards' ? $value : $innerKey; if (! $home->hasPane($pane)) { - $this->httpNotFound(sprintf($this->translate('Dashboard pane "%s" not found'), $pane)); + $this->httpNotFound(sprintf(t('Dashboard pane "%s" not found'), $pane)); } $pane = $home->getPane($pane); - foreach ($dashlets as $order => $dashlet) { - if (! $pane->hasDashlet($dashlet)) { - $this->httpNotFound(sprintf($this->translate('Dashlet "%s" not found'), $pane)); + if ($widgetType === 'Dashboards') { + $pane->setPriority($innerKey); + $home->managePanes($pane); + } else { + foreach ($value as $order => $dashlet) { + if (! $pane->hasDashlet($dashlet)) { + $this->httpNotFound(sprintf(t('Dashlet "%s" not found'), $dashlet)); + } + + $dashlet = $pane->getDashlet($dashlet); + $dashlet->setPriority($order); + $pane->manageDashlets($dashlet); } - - Dashboard::getConn()->update(Dashlet::TABLE, ['priority' => $order], [ - 'id = ?' => $pane->getDashlet($dashlet)->getUuid(), - 'dashboard_id = ?' => $pane->getUuid() - ]); } } } @@ -296,9 +310,9 @@ public function setupDashboardAction() // rendered in the modal view when redirecting $this->view->compact = true; - $this->getResponse()->setHeader('X-Icinga-Title', 'Configure Dashlets', true); + $this->getResponse()->setHeader('X-Icinga-Title', t('Configure Dashlets'), true); } else { - $this->setTitle($this->translate('Add Dashlet')); + $this->setTitle(t('Add Dashlet')); } $query = ModuleDashlet::on(Dashboard::getConn()); @@ -324,13 +338,13 @@ public function settingsAction() $this->dashboard->activate('dashboard_settings'); $this->addControl(new ActionLink( - $this->translate('Add new Home'), + t('Add new Home'), Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'), 'plus', [ - 'class' => 'add-home', - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true + 'class' => 'add-home', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true ] )); @@ -358,16 +372,16 @@ private function validateDashletParams() $dashlet = $this->params->getRequired('dashlet'); if (! $this->dashboard->hasHome($home)) { - $this->httpNotFound(sprintf($this->translate('Home "%s" not found'), $home)); + $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } if (! $this->dashboard->getActiveHome()->hasPane($pane)) { - $this->httpNotFound(sprintf($this->translate('Pane "%s" not found'), $pane)); + $this->httpNotFound(sprintf(t('Pane "%s" not found'), $pane)); } $pane = $this->dashboard->getActiveHome()->getPane($pane); if (! $pane->hasDashlet($dashlet)) { - $this->httpNotFound(sprintf($this->translate('Dashlet "%s" not found'), $dashlet)); + $this->httpNotFound(sprintf(t('Dashlet "%s" not found'), $dashlet)); } return $pane; diff --git a/application/forms/Dashboard/DashletForm.php b/application/forms/Dashboard/DashletForm.php index 922670ebfa..53b5abb8d9 100644 --- a/application/forms/Dashboard/DashletForm.php +++ b/application/forms/Dashboard/DashletForm.php @@ -4,6 +4,7 @@ namespace Icinga\Forms\Dashboard; +use Exception; use Icinga\Application\Logger; use Icinga\Web\Dashboard\Dashboard; use Icinga\Web\Navigation\DashboardHome; @@ -64,47 +65,49 @@ protected function assemble() $panes = $this->dashboard->getActiveHome()->getPaneKeyTitleArr(); } - $this->addElement('hidden', 'org_pane', ['required' => false]); - $this->addElement('hidden', 'org_home', ['required' => false]); - $this->addElement('hidden', 'org_dashlet', ['required' => false]); + $this->addElement('hidden', 'org_pane', ['required' => false]); + $this->addElement('hidden', 'org_home', ['required' => false]); + $this->addElement('hidden', 'org_dashlet', ['required' => false]); $this->addElement('checkbox', 'create_new_home', [ - 'class' => 'autosubmit', - 'required' => false, - 'disabled' => empty($homes) ?: null, - 'label' => t('New Dashboard Home'), - 'description' => t('Check this box if you want to add the dashboard to a new dashboard home.'), + 'class' => 'autosubmit', + 'required' => false, + 'disabled' => empty($homes) ?: null, + 'label' => t('New Dashboard Home'), + 'description' => t('Check this box if you want to add the dashboard to a new dashboard home.'), ]); if (empty($homes) || $this->getPopulatedValue('create_new_home') === 'y') { // $el->attrs->set() has no effect here anymore, so we need to register a proper callback $this->getElement('create_new_home') ->getAttributes() - ->registerAttributeCallback('checked', function () { return true; }); + ->registerAttributeCallback('checked', function () { + return true; + }); $this->addElement('text', 'home', [ - 'required' => true, - 'label' => t('Dashboard Home'), - 'description' => t('Enter a title for the new dashboard home.') + 'required' => true, + 'label' => t('Dashboard Home'), + 'description' => t('Enter a title for the new dashboard home.') ]); } else { $this->addElement('select', 'home', [ - 'required' => true, - 'class' => 'autosubmit', - 'value' => $currentHome, - 'multiOptions' => $homes, - 'label' => t('Dashboard Home'), - 'descriptions' => t('Select a home you want to add the dashboard pane to.') + 'required' => true, + 'class' => 'autosubmit', + 'value' => $currentHome, + 'multiOptions' => $homes, + 'label' => t('Dashboard Home'), + 'descriptions' => t('Select a home you want to add the dashboard pane to.') ]); } $disable = empty($panes) || $this->getPopulatedValue('create_new_home') === 'y'; $this->addElement('checkbox', 'create_new_pane', [ - 'required' => false, - 'class' => 'autosubmit', - 'disabled' => $disable ?: null, - 'label' => t('New Dashboard'), - 'description' => t('Check this box if you want to add the dashlet to a new dashboard.'), + 'required' => false, + 'class' => 'autosubmit', + 'disabled' => $disable ?: null, + 'label' => t('New Dashboard'), + 'description' => t('Check this box if you want to add the dashlet to a new dashboard.'), ]); // Pane element's values are depending on the home element's value @@ -116,37 +119,39 @@ protected function assemble() // $el->attrs->set() has no effect here anymore, so we need to register a proper callback $this->getElement('create_new_pane') ->getAttributes() - ->registerAttributeCallback('checked', function () { return true; }); + ->registerAttributeCallback('checked', function () { + return true; + }); $this->addElement('text', 'pane', [ - 'required' => true, - 'label' => t('New Dashboard Title'), - 'description' => t('Enter a title for the new dashboard.'), + 'required' => true, + 'label' => t('New Dashboard Title'), + 'description' => t('Enter a title for the new dashboard.'), ]); } else { $this->addElement('select', 'pane', [ - 'required' => true, - 'value' => reset($panes), - 'multiOptions' => $panes, - 'label' => t('Dashboard'), - 'description' => t('Select a dashboard you want to add the dashlet to.'), + 'required' => true, + 'value' => reset($panes), + 'multiOptions' => $panes, + 'label' => t('Dashboard'), + 'description' => t('Select a dashboard you want to add the dashlet to.'), ]); } $this->addHtml(new HtmlElement('hr')); $this->addElement('textarea', 'url', [ - 'required' => true, - 'label' => t('Url'), - 'description' => t( + 'required' => true, + 'label' => t('Url'), + 'description' => t( 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters.' ), ]); $this->addElement('text', 'dashlet', [ - 'required' => true, - 'label' => t('Dashlet Title'), - 'description' => t('Enter a title for the dashlet.'), + 'required' => true, + 'label' => t('Dashlet Title'), + 'description' => t('Enter a title for the dashlet.'), ]); $url = (string) Url::fromPath(Dashboard::BASE_ROUTE . '/browse'); @@ -156,9 +161,9 @@ protected function assemble() // We might need this later to allow the user to browse dashlets when creating a dashlet $this->addElement('submit', 'btn_browse', [ - 'label' => t('Browse Dashlets'), - 'href' => $url, - 'formaction' => $url, + 'label' => t('Browse Dashlets'), + 'href' => $url, + 'formaction' => $url, ]); $this->getElement('btn_browse')->setWrapper($element->getWrapper()); @@ -173,12 +178,12 @@ public function load(Dashlet $dashlet) { $home = Url::fromRequest()->getParam('home'); $this->populate(array( - 'org_home' => $home, - 'org_pane' => $dashlet->getPane()->getName(), - 'pane' => $dashlet->getPane()->getTitle(), - 'org_dashlet' => $dashlet->getName(), - 'dashlet' => $dashlet->getTitle(), - 'url' => $dashlet->getUrl()->getRelativeUrl() + 'org_home' => $home, + 'org_pane' => $dashlet->getPane()->getName(), + 'pane' => $dashlet->getPane()->getTitle(), + 'org_dashlet' => $dashlet->getName(), + 'dashlet' => $dashlet->getTitle(), + 'url' => $dashlet->getUrl()->getRelativeUrl() )); } @@ -193,7 +198,7 @@ protected function onSuccess() $home = $dashboard->getHome($home->getName()); if ($home->getName() !== $dashboard->getActiveHome()->getName()) { $home->setActive(); - $home->loadDashboardsFromDB(); + $home->loadPanesFromDB(); } } @@ -221,7 +226,7 @@ protected function onSuccess() $pane->manageDashlets($dashlet); $conn->commitTransaction(); - } catch (\Exception $err) { // This error handling is just for debugging purpose! Will be removed! + } catch (Exception $err) { // This error handling is just for debugging purpose! Will be removed! Logger::error($err); $conn->rollBackTransaction(); @@ -240,7 +245,7 @@ protected function onSuccess() $activeHome = $dashboard->getActiveHome(); if ($currentHome->getName() !== $activeHome->getName()) { $currentHome->setActive(); - $currentHome->loadDashboardsFromDB(); + $currentHome->loadPanesFromDB(); } } @@ -257,10 +262,8 @@ protected function onSuccess() ->setUrl($this->getValue('url')) ->setTitle($this->getValue('dashlet')); - if ( - $orgPane->getName() !== $currentPane->getName() - && $currentPane->hasDashlet($currentDashlet->getName()) - ) { + if ($orgPane->getName() !== $currentPane->getName() + && $currentPane->hasDashlet($currentDashlet->getName())) { Notification::error(sprintf( t('Failed to move dashlet "%s": Dashlet already exists within the "%s" dashboard pane'), $currentDashlet->getTitle(), @@ -292,7 +295,7 @@ function ($val) { $currentPane->manageDashlets($currentDashlet, $orgPane); $conn->commitTransaction(); - } catch (\Exception $err) { + } catch (Exception $err) { Logger::error($err); $conn->rollBackTransaction(); diff --git a/application/forms/Dashboard/HomePaneForm.php b/application/forms/Dashboard/HomePaneForm.php index d6b15906b3..84f1d5db35 100644 --- a/application/forms/Dashboard/HomePaneForm.php +++ b/application/forms/Dashboard/HomePaneForm.php @@ -53,11 +53,11 @@ protected function assemble() $homes = $this->dashboard->getHomeKeyTitleArr(); $this->addElement('checkbox', 'create_new_home', [ - 'required' => false, - 'class' => 'autosubmit', - 'disabled' => empty($homes) ?: null, - 'label' => t('New Dashboard Home'), - 'description' => t('Check this box if you want to move the pane to a new dashboard home.'), + 'required' => false, + 'class' => 'autosubmit', + 'disabled' => empty($homes) ?: null, + 'label' => t('New Dashboard Home'), + 'description' => t('Check this box if you want to move the pane to a new dashboard home.'), ]); $activeHome = $this->dashboard->getActiveHome(); @@ -67,26 +67,26 @@ protected function assemble() $this->getElement('create_new_home')->addAttributes(['checked' => 'checked']); $this->addElement('text', 'home', [ - 'required' => true, - 'label' => t('Dashboard Home'), - 'description' => t('Enter a title for the new dashboard home.'), + 'required' => true, + 'label' => t('Dashboard Home'), + 'description' => t('Enter a title for the new dashboard home.'), ]); } else { $this->addElement('select', 'home', [ - 'required' => true, - 'class' => 'autosubmit', - 'value' => $populatedHome, - 'multiOptions' => $homes, - 'label' => t('Move to Home'), - 'description' => t('Select a dashboard home you want to move the dashboard to.'), + 'required' => true, + 'class' => 'autosubmit', + 'value' => $populatedHome, + 'multiOptions' => $homes, + 'label' => t('Move to Home'), + 'description' => t('Select a dashboard home you want to move the dashboard to.'), ]); } } $this->addElement('text', 'title', [ - 'required' => true, - 'label' => t('Title'), - 'description' => $titleDesc + 'required' => true, + 'label' => t('Title'), + 'description' => $titleDesc ]); $this->addElement('submit', 'btn_update', ['label' => $buttonLabel]); @@ -104,7 +104,7 @@ protected function onSuccess() $activeHome = $this->dashboard->getActiveHome(); if ($currentHome->getName() !== $activeHome->getName()) { $currentHome->setActive(); - $currentHome->loadDashboardsFromDB(); + $currentHome->loadPanesFromDB(); } } diff --git a/application/forms/Dashboard/WelcomeForm.php b/application/forms/Dashboard/WelcomeForm.php index 37173c1e7c..41673d8238 100644 --- a/application/forms/Dashboard/WelcomeForm.php +++ b/application/forms/Dashboard/WelcomeForm.php @@ -31,10 +31,10 @@ protected function assemble() $this->registerElement($element)->decorate($element); $this->addElement('submit', 'btn_customize_dashlets', [ - 'label' => t('Add Dashlets Now'), - 'href' => Url::fromPath(Dashboard::BASE_ROUTE . '/setup-dashboard'), - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true + 'label' => t('Add Dashlets Now'), + 'href' => Url::fromPath(Dashboard::BASE_ROUTE . '/setup-dashboard'), + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true ]); $this->getElement('btn_customize_dashlets')->setWrapper($element->getWrapper()); @@ -43,15 +43,17 @@ protected function assemble() protected function onSuccess() { if ($this->getPopulatedValue('btn_use_defaults')) { - $order = 0; $home = $this->dashboard->getHome(DashboardHome::DEFAULT_HOME); - foreach ($this->dashboard->getSystemDefaults() as $pane) { - $pane->setPriority($order++); - $home->managePanes($pane); + $conn = Dashboard::getConn(); + $conn->beginTransaction(); - $dashlets = $pane->getDashlets(); - $pane->setDashlets([]); - $pane->manageDashlets($dashlets); + try { + $home->managePanes($this->dashboard->getSystemDefaults(), null, true); + + $conn->commitTransaction(); + } catch (\Exception $err) { + $conn->rollBackTransaction(); + throw $err; } } } diff --git a/etc/schema/dashboards.sql b/etc/schema/dashboards.sql index 91c03aa091..dd44013be9 100644 --- a/etc/schema/dashboards.sql +++ b/etc/schema/dashboards.sql @@ -50,12 +50,11 @@ CREATE TABLE `dashlet` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE `dashlet_system` ( + `id` int(10) NOT NULL PRIMARY KEY AUTO_INCREMENT, `dashlet_id` binary(20) NOT NULL, `module_dashlet_id` binary(20) NOT NULL, - `username` varchar(254) NOT NULL COLLATE utf8mb4_unicode_ci, - PRIMARY KEY (`username`, `dashlet_id`, `module_dashlet_id`), CONSTRAINT `fk_dashlet_system_dashlet` FOREIGN KEY (`dashlet_id`) REFERENCES `dashlet` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `fk_dashlet_system_module_dashlet` FOREIGN KEY (`dashlet_id`) REFERENCES `dashlet` (`id`) ON DELETE CASCADE ON UPDATE CASCADE + CONSTRAINT `fk_dashlet_system_module_dashlet` FOREIGN KEY (`module_dashlet_id`) REFERENCES `module_dashlet` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE `module_dashlet` ( @@ -65,7 +64,7 @@ CREATE TABLE `module_dashlet` ( `module` varchar(64) NOT NULL COLLATE utf8mb4_unicode_ci, `pane` varchar(64) DEFAULT NULL COLLATE utf8mb4_unicode_ci, `url` varchar(2048) NOT NULL COLLATE utf8mb4_bin, - `description` varchar(64) DEFAULT NULL COLLATE utf8mb4_unicode_ci, + `description` text DEFAULT NULL COLLATE utf8mb4_unicode_ci, `priority` int(10) DEFAULT 0, PRIMARY KEY (`id`), INDEX `idx_module_dashlet_name` (`name`), @@ -77,4 +76,4 @@ CREATE TABLE `module_dashlet` ( CREATE USER 'dashboard'@'%' IDENTIFIED BY 'dashboard'; GRANT ALL PRIVILEGES ON `dashboard`.* TO 'dashboard'@'%' IDENTIFIED BY 'dashboard'; -FLUSH PRIVILEGES; \ No newline at end of file +FLUSH PRIVILEGES; diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 09ec732fbd..67d24ba4f5 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -375,9 +375,13 @@ protected function dashboard($name, array $properties = array()) * * @return Dashboard\Dashlet[] */ - public function getDashlet() + public function getDashlets() { $this->launchConfigScript(); + uasort($this->dashletItems, function (Dashboard\Dashlet $x, Dashboard\Dashlet $y) { + return $x->getPriority() - $y->getPriority(); + }); + return $this->dashletItems; } diff --git a/library/Icinga/Common/DataExtractor.php b/library/Icinga/Common/DataExtractor.php index ed77115a91..54e63e23d5 100644 --- a/library/Icinga/Common/DataExtractor.php +++ b/library/Icinga/Common/DataExtractor.php @@ -16,7 +16,7 @@ trait DataExtractor public function fromArray(array $data) { foreach ($data as $name => $value) { - $func = 'set'. ucfirst($name); + $func = 'set' . ucfirst($name); if (method_exists($this, $func)) { $this->$func($value); } diff --git a/library/Icinga/Model/DashboardOverride.php b/library/Icinga/Model/DashboardOverride.php index 8582da3449..d498539ddf 100644 --- a/library/Icinga/Model/DashboardOverride.php +++ b/library/Icinga/Model/DashboardOverride.php @@ -27,7 +27,7 @@ public function getColumns() 'username', 'disabled', 'priority', - 'acceptance' => new Expression('COALESCE(COUNT(dashboard_subscribable_dashboard_dashboard_override.dashboard_id), 0)') + 'acceptance' => new Expression('0') ]; } diff --git a/library/Icinga/Model/Dashlet.php b/library/Icinga/Model/Dashlet.php index 8b02245835..eee06a1a22 100644 --- a/library/Icinga/Model/Dashlet.php +++ b/library/Icinga/Model/Dashlet.php @@ -34,11 +34,11 @@ public function getColumns() public function getMetaData() { return [ - 'dashboard_id' => t('Dashboard Id'), - 'name' => t('Dashlet Name'), - 'label' => t('Dashlet Title'), - 'url' => t('Dashlet Url'), - 'priority' => t('Dashlet Order Priority') + 'dashboard_id' => t('Dashboard Id'), + 'name' => t('Dashlet Name'), + 'label' => t('Dashlet Title'), + 'url' => t('Dashlet Url'), + 'priority' => t('Dashlet Order Priority') ]; } @@ -56,5 +56,9 @@ public function createRelations(Relations $relations) { $relations->belongsTo('dashboard', Pane::class); //$relations->belongsTo('home', Home::class); + + $relations->belongsToMany('module_dashlet', ModuleDashlet::class) + ->through(SystemDashlet::class) + ->setJoinType('LEFT'); } } diff --git a/library/Icinga/Model/Home.php b/library/Icinga/Model/Home.php index 50f68e69b6..ce9bd2c790 100644 --- a/library/Icinga/Model/Home.php +++ b/library/Icinga/Model/Home.php @@ -33,9 +33,9 @@ public function getColumns() public function getMetaData() { return [ - 'name' => t('Dashboard Home Name'), - 'label' => t('Dashboard Home Title'), - //'priority' => t('Dashboard Order Priority') + 'name' => t('Dashboard Home Name'), + 'label' => t('Dashboard Home Title'), + 'priority' => t('Dashboard Order Priority') ]; } @@ -46,7 +46,7 @@ public function getSearchColumns() public function getDefaultSort() { - return 'dashboard_home.name'; + return 'name'; } public function createRelations(Relations $relations) diff --git a/library/Icinga/Model/ModuleDashlet.php b/library/Icinga/Model/ModuleDashlet.php index aa2bec4e08..dac1c48734 100644 --- a/library/Icinga/Model/ModuleDashlet.php +++ b/library/Icinga/Model/ModuleDashlet.php @@ -5,6 +5,7 @@ namespace Icinga\Model; use ipl\Orm\Model; +use ipl\Orm\Relations; class ModuleDashlet extends Model { @@ -34,13 +35,13 @@ public function getColumns() public function getMetaData() { return [ - 'name' => t('Dashlet Name'), - 'label' => t('Dashlet Title'), - 'module' => t('Module Name'), - 'pane' => t('Dashboard Pane Name'), - 'url' => t('Dashlet Url'), - 'description' => t('Dashlet Description'), - 'priority' => t('Dashlet Order Priority') + 'name' => t('Dashlet Name'), + 'label' => t('Dashlet Title'), + 'module' => t('Module Name'), + 'pane' => t('Dashboard Pane Name'), + 'url' => t('Dashlet Url'), + 'description' => t('Dashlet Description'), + 'priority' => t('Dashlet Order Priority') ]; } @@ -53,4 +54,11 @@ public function getDefaultSort() { return ['module_dashlet.name', 'module_dashlet.priority']; } + + public function createRelations(Relations $relations) + { + $relations->belongsToMany('dashlet', Dashlet::class) + ->through(SystemDashlet::class) + ->setJoinType('LEFT'); + } } diff --git a/library/Icinga/Model/Pane.php b/library/Icinga/Model/Pane.php index ce1e98c157..cb80fe35c1 100644 --- a/library/Icinga/Model/Pane.php +++ b/library/Icinga/Model/Pane.php @@ -34,10 +34,10 @@ public function getColumns() public function getMetaData() { return [ - 'home_id' => t('Dashboard Home Id'), - 'name' => t('Dashboard Name'), - 'label' => t('Dashboard Title'), - 'username' => t('Username'), + 'home_id' => t('Dashboard Home Id'), + 'name' => t('Dashboard Name'), + 'label' => t('Dashboard Title'), + 'username' => t('Username'), ]; } diff --git a/library/Icinga/Model/SystemDashlet.php b/library/Icinga/Model/SystemDashlet.php new file mode 100644 index 0000000000..96bd8a717b --- /dev/null +++ b/library/Icinga/Model/SystemDashlet.php @@ -0,0 +1,36 @@ +belongsTo('dashlet', Dashlet::class); + $relations->belongsTo('module_dashlet', ModuleDashlet::class); + } +} diff --git a/library/Icinga/Web/Dashboard/Common/DashboardManager.php b/library/Icinga/Web/Dashboard/Common/DashboardManager.php index 5ac600fab9..77237fcf2a 100644 --- a/library/Icinga/Web/Dashboard/Common/DashboardManager.php +++ b/library/Icinga/Web/Dashboard/Common/DashboardManager.php @@ -27,31 +27,31 @@ trait DashboardManager /** @var User */ private static $user; - + /** @var Connection */ private static $conn; /** - * A list of @see DashboardHome + * A list of default panes loaded from monitoring|icingadb module * - * @var DashboardHome[] + * @var Pane[] */ - private $homes = []; + private static $defaultPanes = []; /** - * A list of default panes loaded from monitoring|icingadb module + * A list of @see DashboardHome * - * @var Pane[] + * @var DashboardHome[] */ - private $defaultPanes = []; + private $homes = []; public function load() { $this->loadHomesFromMenu(); $this->loadDashboards(); - $this->initAndGetDefaultHome(); - $this->deployModuleDashlets(); + $this->initGetDefaultHome(); + self::deployModuleDashlets(); } /** @@ -106,7 +106,7 @@ public function loadHomesFromMenu() /** * Load dashboards assigned to the given home or active home being loaded * - * @param string $name + * @param ?string $name Name of the dashboard home you want to load the dashboards for * * @return $this */ @@ -117,11 +117,12 @@ public function loadDashboards($name = null) } else { $requestRoute = Url::fromRequest(); if ($requestRoute->getPath() === Dashboard::BASE_ROUTE) { - $home = $this->initAndGetDefaultHome(); + $home = $this->initGetDefaultHome(); } else { $homeParam = $requestRoute->getParam('home'); if (empty($homeParam) || ! $this->hasHome($homeParam)) { if (! ($home = $this->rewindHomes())) { + // No dashboard homes return $this; } } else { @@ -131,7 +132,7 @@ public function loadDashboards($name = null) } $this->activateHome($home); - $home->loadDashboardsFromDB(); + $home->loadPanesFromDB(); return $this; } @@ -163,9 +164,11 @@ public function getHomes() } /** - * Set this home's dashboards + * Set this user's dashboard homes * * @param DashboardHome|DashboardHome[] $homes + * + * @return $this */ public function setHomes($homes) { @@ -181,7 +184,7 @@ public function setHomes($homes) /** * Get whether the given home exist * - * @param string $name + * @param string $name * * @return bool */ @@ -191,7 +194,7 @@ public function hasHome($name) } /** - * Add a new pane to this home + * Activates the given home and deactivates all other active homes * * @param DashboardHome $home * @@ -210,22 +213,19 @@ public function activateHome(DashboardHome $home) } /** - * Checks if this home has any panes + * Get the active home currently being loaded * * @return ?DashboardHome */ public function getActiveHome() { - $active = null; foreach ($this->getHomes() as $home) { if ($home->getActive()) { - $active = $home; - - break; + return $home; } } - return $active; + return null; } /** @@ -239,7 +239,7 @@ public function rewindHomes() } /** - * Remove a specific pane form this home + * Remove the given home * * @param DashboardHome|string $home * @@ -263,9 +263,9 @@ public function removeHome($home) } /** - * Remove all panes from this home, unless you specified the panes + * Remove all|given list of dashboard homes * - * @param DashboardHome[] $homes + * @param DashboardHome[] $homes Optional list of dashboard homes * * @return $this */ @@ -280,7 +280,7 @@ public function removeHomes(array $homes = []) } /** - * Manage the given pane(s) + * Manage the given home * * @param DashboardHome $home * @@ -289,19 +289,23 @@ public function removeHomes(array $homes = []) public function manageHome(DashboardHome $home) { $conn = self::getConn(); - if (! $this->hasHome($home->getName())) { $conn->insert(DashboardHome::TABLE, [ - 'name' => $home->getName(), - 'label' => $home->getLabel(), - 'username' => self::getUser()->getUsername(), - 'priority' => $home->getPriority(), - 'type' => $home->getType() !== Dashboard::SYSTEM ? $home->getType() : Dashboard::PRIVATE_DS + 'name' => $home->getName(), + 'label' => $home->getLabel(), + 'username' => self::getUser()->getUsername(), + 'priority' => count($this->getHomes()) + 1, + 'type' => $home->getType() !== Dashboard::SYSTEM ? $home->getType() : Dashboard::PRIVATE_DS ]); $home->setUuid($conn->lastInsertId()); } elseif ($home->getName() !== DashboardHome::DEFAULT_HOME) { - $conn->update(DashboardHome::TABLE, ['label' => $home->getLabel()], ['id = ?' => $home->getUuid()]); + $conn->update(DashboardHome::TABLE, [ + 'label' => $home->getLabel(), + 'priority' => $home->getPriority() + ], ['id = ?' => $home->getUuid()]); + } else { + $conn->update(DashboardHome::TABLE, ['priority' => $home->getPriority()], ['id = ?' => $home->getUuid()]); } return $this; @@ -327,11 +331,11 @@ public function getHomeKeyTitleArr() } /** - * Get and/or init the default dashboard home + * Get and|or init the default dashboard home * * @return DashboardHome */ - public function initAndGetDefaultHome() + public function initGetDefaultHome() { if ($this->hasHome(DashboardHome::DEFAULT_HOME)) { return $this->getHome(DashboardHome::DEFAULT_HOME); @@ -379,9 +383,9 @@ public static function getUser() * * @return Pane[] */ - public function getSystemDefaults() + public static function getSystemDefaults() { - return $this->defaultPanes; + return self::$defaultPanes; } /** @@ -390,32 +394,39 @@ public function getSystemDefaults() * * @return void */ - public function deployModuleDashlets() + public static function deployModuleDashlets() { $moduleManager = Icinga::app()->getModuleManager(); foreach ($moduleManager->getLoadedModules() as $module) { foreach ($module->getDashboard() as $dashboardPane) { + $priority = 0; foreach ($dashboardPane->getDashlets() as $dashlet) { - $identifier = self::getSHA1( - $module->getName() . $dashboardPane->getName() . $dashlet->getName() - ); - $dashlet->setUuid($identifier); - self::updateOrInsertModuleDashlet($dashlet, $module->getName()); + $uuid = self::getSHA1($module->getName() . $dashboardPane->getName() . $dashlet->getName()); + $dashlet + ->setUuid($uuid) + ->setPriority($priority++) + ->setModule($module->getName()) + ->setModuleDashlet(true); + + self::updateOrInsertModuleDashlet($dashlet); } if (in_array($module->getName(), ['monitoring', 'icingadb'], true)) { - $this->defaultPanes[$dashboardPane->getName()] = $dashboardPane; + self::$defaultPanes[$dashboardPane->getName()] = $dashboardPane; } } $priority = 0; - foreach ($module->getDashlet() as $dashlet) { + foreach ($module->getDashlets() as $dashlet) { $identifier = self::getSHA1($module->getName() . $dashlet->getName()); - $dashlet->setUuid($identifier); - $dashlet->setPriority($priority++); + $dashlet + ->setUuid($identifier) + ->setPriority($priority++) + ->setModule($module->getName()) + ->setModuleDashlet(true); - self::updateOrInsertModuleDashlet($dashlet, $module->getName()); + self::updateOrInsertModuleDashlet($dashlet); } } } @@ -439,29 +450,33 @@ public static function moduleDashletExist(Dashlet $dashlet) * Insert or update the given module dashlet * * @param Dashlet $dashlet - * @param string $module + * @param string $module * * @return void */ - public static function updateOrInsertModuleDashlet(Dashlet $dashlet, $module) + public static function updateOrInsertModuleDashlet(Dashlet $dashlet) { + if (! $dashlet->isModuleDashlet()) { + return; + } + if (! self::moduleDashletExist($dashlet)) { self::getConn()->insert('module_dashlet', [ - 'id' => $dashlet->getUuid(), - 'name' => $dashlet->getName(), - 'label' => $dashlet->getTitle(), - 'pane' => $dashlet->getPane() ? $dashlet->getPane()->getName() : null, - 'module' => $module, - 'url' => $dashlet->getUrl()->getRelativeUrl(), - 'description' => $dashlet->getDescription(), - 'priority' => $dashlet->getPriority() + 'id' => $dashlet->getUuid(), + 'name' => $dashlet->getName(), + 'label' => $dashlet->getTitle(), + 'pane' => $dashlet->getPane() ? $dashlet->getPane()->getName() : null, + 'module' => $dashlet->getModule(), + 'url' => $dashlet->getUrl()->getRelativeUrl(), + 'description' => $dashlet->getDescription(), + 'priority' => $dashlet->getPriority() ]); } else { self::getConn()->update('module_dashlet', [ - 'label' => $dashlet->getTitle(), - 'url' => $dashlet->getUrl()->getRelativeUrl(), - 'description' => $dashlet->getDescription(), - 'priority' => $dashlet->getPriority() + 'label' => $dashlet->getTitle(), + 'url' => $dashlet->getUrl()->getRelativeUrl(), + 'description' => $dashlet->getDescription(), + 'priority' => $dashlet->getPriority() ], ['id = ?' => $dashlet->getUuid()]); } } @@ -481,16 +496,16 @@ public static function getModuleDashlets(Query $query) } $dashlet->fromArray([ - 'label' => t($moduleDashlet->label), - 'priority' => $moduleDashlet->priority, - 'uuid' => $moduleDashlet->id + 'label' => t($moduleDashlet->label), + 'priority' => $moduleDashlet->priority, + 'uuid' => $moduleDashlet->id ]); if (($pane = $moduleDashlet->pane)) { $dashlet->setPane(new Pane($pane)); } - $dashlets[$moduleDashlet->module][$dashlet->getName()] = $dashlet; + $dashlets[$moduleDashlet->module][$dashlet->getName()] = $dashlet; } return $dashlets; diff --git a/library/Icinga/Web/Dashboard/Common/ItemListControl.php b/library/Icinga/Web/Dashboard/Common/ItemListControl.php new file mode 100644 index 0000000000..f211643fcc --- /dev/null +++ b/library/Icinga/Web/Dashboard/Common/ItemListControl.php @@ -0,0 +1,40 @@ +collapsibleControlClass = $class; + + return $this; + } + + protected function assemble() + { + $this->addHtml(HtmlElement::create('div', ['class' => $this->collapsibleControlClass], [ + new Icon('angle-down', ['class' => 'expand-icon', 'title' => t('Expand')]), + new Icon('angle-up', ['class' => 'collapse-icon', 'title' => t('Collapse')]) + ])); + + $this->getAttributes()->registerAttributeCallback('draggable', function () { + return 'true'; + }); + } +} diff --git a/library/Icinga/Web/Dashboard/Dashboard.php b/library/Icinga/Web/Dashboard/Dashboard.php index 2c85187475..7b5fe43118 100644 --- a/library/Icinga/Web/Dashboard/Dashboard.php +++ b/library/Icinga/Web/Dashboard/Dashboard.php @@ -149,7 +149,7 @@ public function getTabs() $this->tabs->add( $key, [ - 'title' => sprintf( + 'title' => sprintf( t('Show %s', 'dashboard.pane.tooltip'), $pane->getTitle() ), @@ -199,10 +199,10 @@ public function getActivePane() /** * Determine the active pane either by the selected tab or the current request * - * @throws \Icinga\Exception\ConfigurationError + * @return Pane The currently active pane * @throws \Icinga\Exception\ProgrammingError * - * @return Pane The currently active pane + * @throws \Icinga\Exception\ConfigurationError */ public function determineActivePane() { @@ -241,7 +241,7 @@ public function setWelcomeForm(Form $form) protected function assemble() { $activeHome = $this->getActiveHome(); - if (! $activeHome || $activeHome->getName() === DashboardHome::DEFAULT_HOME && ! $activeHome->hasPanes()) { + if (! $activeHome || ($activeHome->getName() === DashboardHome::DEFAULT_HOME && ! $activeHome->hasPanes())) { $this->setAttribute('class', 'content welcome-view'); $wrapper = HtmlElement::create('div', ['class' => 'dashboard-introduction']); @@ -252,7 +252,10 @@ protected function assemble() t('You will see this screen every time you log in and haven\'t created any dashboards yet.') )); - $message = t('At the moment this view is empty, but you can populate it with small portions of information called Dashlets.'); + $message = t( + 'At the moment this view is empty, but you can populate it with small portions of' + . ' information called Dashlets.' + ); $wrapper->addHtml(HtmlElement::create('p', null, $message)); $message = t( diff --git a/library/Icinga/Web/Dashboard/Dashlet.php b/library/Icinga/Web/Dashboard/Dashlet.php index 8c3848c5d6..a0c2d0c97e 100644 --- a/library/Icinga/Web/Dashboard/Dashlet.php +++ b/library/Icinga/Web/Dashboard/Dashlet.php @@ -33,7 +33,7 @@ class Dashlet extends BaseHtmlElement protected $tag = 'div'; protected $defaultAttributes = [ - 'class' => 'container dashlet-sortable', + 'class' => 'container widget-sortable', 'draggable' => 'true' ]; @@ -88,9 +88,9 @@ class Dashlet extends BaseHtmlElement /** * Create a new dashlet displaying the given url in the provided pane * - * @param string $title The title to use for this dashlet - * @param Url|string $url The url this dashlet uses for displaying information - * @param Pane|null $pane The pane this Dashlet will be added to + * @param string $title The title to use for this dashlet + * @param Url|string $url The url this dashlet uses for displaying information + * @param Pane|null $pane The pane this Dashlet will be added to */ public function __construct($title, $url, Pane $pane = null) { @@ -193,7 +193,7 @@ public function getUrl() /** * Set the dashlets URL * - * @param string|Url $url The url to use, either as an Url object or as a path + * @param string|Url $url The url to use, either as an Url object or as a path * * @return $this */ @@ -207,7 +207,7 @@ public function setUrl($url) /** * Set the progress label to use * - * @param string $label + * @param string $label * * @return $this */ @@ -245,7 +245,7 @@ public function getDescription() /** * Set the dashlet's description * - * @param string $description + * @param string $description * * @return $this */ @@ -300,9 +300,9 @@ protected function assemble() t($this->getTitle()), $url->getUrlWithout(['showCompact', 'limit'])->getRelativeUrl(), [ - 'aria-label' => t($this->getTitle()), - 'title' => t($this->getTitle()), - 'data-base-target' => 'col1' + 'aria-label' => t($this->getTitle()), + 'title' => t($this->getTitle()), + 'data-base-target' => 'col1' ] ))); @@ -322,13 +322,13 @@ protected function assemble() public function toArray() { return [ - 'id' => $this->getUuid(), - 'pane' => $this->getPane() ? $this->getPane()->getName() : null, - 'name' => $this->getName(), - 'url' => $this->getUrl()->getRelativeUrl(), - 'label' => $this->getTitle(), - 'order' => $this->getPriority(), - 'disabled' => (int) $this->isDisabled(), + 'id' => $this->getUuid(), + 'pane' => $this->getPane() ? $this->getPane()->getName() : null, + 'name' => $this->getName(), + 'url' => $this->getUrl()->getRelativeUrl(), + 'label' => $this->getTitle(), + 'order' => $this->getPriority(), + 'disabled' => (int) $this->isDisabled(), ]; } } diff --git a/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php b/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php index 5e1c39ddf7..17bd54d295 100644 --- a/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php +++ b/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php @@ -1,22 +1,19 @@ 'home-item-list']; - - protected $tag = 'ul'; - /** @var DashboardHome */ protected $home; @@ -24,49 +21,57 @@ public function __construct(DashboardHome $home) { $this->home = $home; $this->home->setActive(); - $this->home->loadDashboardsFromDB(); - - $this->getAttributes()->add('class', $home->getName()); + $this->home->loadPanesFromDB(); + + $this->setCollapsibleControlClass('dashboard-list-info'); + $this->getAttributes() + ->registerAttributeCallback('class', function () { + return 'home-list-control collapsible widget-sortable'; + }) + ->registerAttributeCallback('data-toggle-element', function () { + return '.dashboard-list-info'; + }) + ->registerAttributeCallback('data-icinga-home', function () { + return $this->home->getName(); + }) + ->registerAttributeCallback('id', function () { + return 'home_' . $this->home->getPriority(); + }); } protected function assemble() { - $wrapper = HtmlElement::create('div', [ - 'class' => 'home-list-control collapsible', - 'data-toggle-element' => '.dashboard-list-info' - ]); - - $wrapper->addHtml(HtmlElement::create('div', ['class' => 'dashboard-list-info'], [ - new Icon('angle-down', ['class' => 'expand-icon', 'title' => t('Expand')]), - new Icon('angle-up', ['class' => 'collapse-icon', 'title' => t('Collapse')]) - ])); + // TODO: How should disabled homes look like? + parent::assemble(); $header = HtmlElement::create('h1', ['class' => 'collapsible-header home'], $this->home->getLabel()); - $url = Url::fromPath(Dashboard::BASE_ROUTE . '/rename-home')->setParams(['home' => $this->home->getName()]); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/rename-home')->setParams([ + 'home' => $this->home->getName() + ]); $header->addHtml(new Link(t('Edit'), $url, [ 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ])); - $wrapper->addHtml($header); + $this->addHtml($header); + + $list = HtmlElement::create('ul', ['class' => 'dashboard-item-list']); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'); + $url->setParams(['home' => $this->home->getName()]); // List all dashboard panes foreach ($this->home->getPanes() as $pane) { $pane->setHome($this->home); // In case it's not set - $this->addHtml(new DashboardList($pane)); + $list->addHtml(new DashboardList($pane)); } - $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'); - $url->setParams(['home' => $this->home->getName()]); - - $wrapper->addHtml(new ActionLink(t('Add Dashboard'), $url, 'plus', [ - 'class' => 'add-dashboard', - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true + $this->addHtml($list); + $this->addHtml(new ActionLink(t('Add Dashboard'), $url, 'plus', [ + 'class' => 'add-dashboard', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true ])); - - $this->addWrapper($wrapper); } } diff --git a/library/Icinga/Web/Dashboard/ItemList/DashboardList.php b/library/Icinga/Web/Dashboard/ItemList/DashboardList.php index 83c9c8b33a..17dbba40ea 100644 --- a/library/Icinga/Web/Dashboard/ItemList/DashboardList.php +++ b/library/Icinga/Web/Dashboard/ItemList/DashboardList.php @@ -1,22 +1,19 @@ 'dashboard-item-list']; - - protected $tag = 'ul'; - /** @var Pane */ protected $pane; @@ -24,26 +21,31 @@ public function __construct(Pane $pane) { $this->pane = $pane; - $this->getAttributes()->add('class', $pane->getName()); + $this->setCollapsibleControlClass('dashlets-list-info'); + $this->getAttributes() + ->registerAttributeCallback('class', function () { + return 'dashboard-list-control collapsible widget-sortable'; + }) + ->registerAttributeCallback('data-toggle-element', function () { + return '.dashlets-list-info'; + }) + ->registerAttributeCallback('data-icinga-pane', function () { + return $this->pane->getHome()->getName() . '|' . $this->pane->getName(); + }) + ->registerAttributeCallback('id', function () { + return 'pane_' . $this->pane->getPriority(); + }); } protected function assemble() { // TODO: How should disabled dashboard panes look like? - $wrapper = HtmlElement::create('div', [ - 'class' => 'dashboard-list-control collapsible', - 'data-toggle-element' => '.dashlets-list-info' - ]); - - $wrapper->addHtml(HtmlElement::create('div', ['class' => 'dashlets-list-info'], [ - new Icon('angle-down', ['class' => 'expand-icon', 'title' => t('Expand')]), - new Icon('angle-up', ['class' => 'collapse-icon', 'title' => t('Collapse')]) - ])); + parent::assemble(); $header = HtmlElement::create('h1', ['class' => 'collapsible-header'], $this->pane->getTitle()); $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-pane')->setParams([ - 'home' => $this->pane->getHome()->getName(), - 'pane' => $this->pane->getName() + 'home' => $this->pane->getHome()->getName(), + 'pane' => $this->pane->getName() ]); $header->addHtml(new Link(t('Edit'), $url, [ @@ -51,23 +53,24 @@ protected function assemble() 'data-no-icinga-ajax' => true ])); - $wrapper->addHtml($header); + $this->addHtml($header); + + $list = HtmlElement::create('ul', ['class' => 'dashlet-item-list']); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'); + $url->setParams([ + 'home' => $this->pane->getHome(), + 'pane' => $this->pane->getName() + ]); foreach ($this->pane->getDashlets() as $dashlet) { - $this->addHtml(new DashletListItem($dashlet, true)); + $list->addHtml(new DashletListItem($dashlet, true)); } - $wrapper->addHtml(new ActionLink( - t('Add Dashlet'), - Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet')->addParams(['pane' => $this->pane->getName()]), - 'plus', - [ - 'class' => 'add-dashlet', - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ] - )); - - $this->addWrapper($wrapper); + $this->addHtml($list); + $this->addHtml(new ActionLink(t('Add Dashlet'), $url, 'plus', [ + 'class' => 'add-dashlet', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ])); } } diff --git a/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php b/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php index a010016bf7..fb6f266e27 100644 --- a/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php +++ b/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php @@ -1,5 +1,7 @@ 'dashlet-list-item']; + protected $defaultAttributes = ['class' => 'dashlet-list-item',]; protected $tag = 'li'; /** @var Dashlet */ protected $dashlet; + /** @var bool Whether to render an edit button for the dashlet */ protected $renderEditButton; public function __construct(Dashlet $dashlet = null, $renderEditButton = false) { $this->dashlet = $dashlet; $this->renderEditButton = $renderEditButton; + + if ($this->dashlet) { + $this->getAttributes() + ->set('draggable', 'true') + ->add('class', 'widget-sortable'); + + $this->getAttributes() + ->registerAttributeCallback('data-icinga-dashlet', function () { + return $this->dashlet->getName(); + }) + ->registerAttributeCallback('id', function () { + return 'dashlet_' . $this->dashlet->getPriority(); + }); + } } /** @@ -53,9 +70,9 @@ protected function assembleTitle() $pane = $this->dashlet->getPane(); $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-dashlet'); $url->setParams([ - 'home' => $pane->getHome()->getName(), - 'pane' => $pane->getName(), - 'dashlet' => $this->dashlet->getName() + 'home' => $pane->getHome()->getName(), + 'pane' => $pane->getName(), + 'dashlet' => $this->dashlet->getName() ]); $title->addHtml(new Link(t('Edit'), $url, [ diff --git a/library/Icinga/Web/Dashboard/OverridingWidget.php b/library/Icinga/Web/Dashboard/OverridingWidget.php index 541a442dd0..b9c0524484 100644 --- a/library/Icinga/Web/Dashboard/OverridingWidget.php +++ b/library/Icinga/Web/Dashboard/OverridingWidget.php @@ -9,7 +9,7 @@ interface OverridingWidget /** * Set whether this widget overrides another widget * - * @param bool $override + * @param bool $override * * @return $this */ diff --git a/library/Icinga/Web/Dashboard/Pane.php b/library/Icinga/Web/Dashboard/Pane.php index 6247b91f1f..974ddd752f 100644 --- a/library/Icinga/Web/Dashboard/Pane.php +++ b/library/Icinga/Web/Dashboard/Pane.php @@ -84,12 +84,12 @@ class Pane implements OverridingWidget /** * Create a new pane * - * @param string $name The pane to create - * @param array $properties + * @param string $name The pane to create + * @param array $properties */ public function __construct($name, array $properties = []) { - $this->name = $name; + $this->name = $name; $this->title = $name; if (! empty($properties)) { @@ -100,7 +100,7 @@ public function __construct($name, array $properties = []) /** * Set the name of this pane * - * @param string $name + * @param string $name */ public function setName($name) { @@ -130,7 +130,7 @@ public function getTitle() /** * Overwrite the title of this pane * - * @param string $title The new title to use for this pane + * @param string $title The new title to use for this pane * * @return $this */ @@ -239,6 +239,8 @@ public function getHome() * Set the dashboard home this pane is a part of * * @param DashboardHome $home + * + * @return $this */ public function setHome(DashboardHome $home) { @@ -248,15 +250,15 @@ public function setHome(DashboardHome $home) } /** - * Return true if a dashlet with the given title exists in this pane + * Return true if a dashlet with the given name exists in this pane * - * @param string $title The title of the dashlet to check for existence + * @param string $name The title of the dashlet to check for existence * * @return bool */ - public function hasDashlet($title) + public function hasDashlet($name) { - return array_key_exists($title, $this->dashlets); + return array_key_exists($name, $this->dashlets); } /** @@ -270,20 +272,19 @@ public function hasDashlets() } /** - * Return a dashlet with the given name if existing + * Get a dashlet with the given name if existing * - * @param string $title The title of the dashlet to return + * @param string $name * - * @return Dashlet The dashlet with the given title - * @throws ProgrammingError If the dashlet doesn't exist + * @return Dashlet */ - public function getDashlet($title) + public function getDashlet($name) { - if ($this->hasDashlet($title)) { - return $this->dashlets[$title]; + if ($this->hasDashlet($name)) { + return $this->dashlets[$name]; } - throw new ProgrammingError('Trying to access invalid dashlet: %s', $title); + throw new ProgrammingError('Trying to access invalid dashlet: %s', $name); } /** @@ -317,14 +318,14 @@ public function setDashlets(array $dashlets) /** * Create, add and return a new dashlet * - * @param string $title - * @param string $url + * @param string $name + * @param string $url * * @return Dashlet */ - public function createDashlet($title, $url = null) + public function createDashlet($name, $url = null) { - $dashlet = new Dashlet($title, $url, $this); + $dashlet = new Dashlet($name, $url, $this); $this->addDashlet($dashlet); return $dashlet; @@ -344,7 +345,7 @@ public function addDashlet($dashlet, $url = null) if ($dashlet instanceof Dashlet) { $this->dashlets[$dashlet->getName()] = $dashlet; } elseif (is_string($dashlet) && $url !== null) { - $this->createDashlet($dashlet, $url); + $this->createDashlet($dashlet, $url); } else { throw new ConfigurationError('Invalid dashlet added: %s', $dashlet); } @@ -353,49 +354,27 @@ public function addDashlet($dashlet, $url = null) } /** - * Add new dashlets to existing dashlets - * - * @param array $dashlets - * @return $this - */ - public function addDashlets(array $dashlets) - { - /* @var $dashlet Dashlet */ - foreach ($dashlets as $dashlet) { - if (array_key_exists($dashlet->getName(), $this->dashlets)) { - if (preg_match('/_(\d+)$/', $dashlet->getName(), $m)) { - $name = preg_replace('/_\d+$/', $m[1]++, $dashlet->getName()); - } else { - $name = $dashlet->getName() . '_2'; - } - $this->dashlets[$name] = $dashlet; - } else { - $this->dashlets[$dashlet->getName()] = $dashlet; - } - } - - return $this; - } - - /** - * Add a dashlet to the current pane @see addDashlet() + * Add a dashlet to the current pane * - * @param string $title + * @param string $name * @param Url|string $url * * @return $this + * @see addDashlet() */ - public function add($title, $url, $priority = 0) + public function add($name, $url, $priority = 0, $description = null) { - $dashlet = $this->createDashlet($title, $url); - $dashlet->setPriority($priority); + $dashlet = $this->createDashlet($name, $url); + $dashlet + ->setDescription($description) + ->setPriority($priority); $this->addDashlet($dashlet); return $this; } /** - * Remove the dashlet the given dashlet if it exists in this pane + * Remove the given dashlet if it exists in this pane * * @param Dashlet|string $dashlet * @@ -413,8 +392,8 @@ public function removeDashlet($dashlet) } Dashboard::getConn()->delete(Dashlet::TABLE, [ - 'id = ?' => $dashlet->getUuid(), - 'dashboard_id = ?' => $this->getUuid() + 'id = ?' => $dashlet->getUuid(), + 'dashboard_id = ?' => $this->getUuid() ]); return $this; @@ -429,10 +408,7 @@ public function removeDashlet($dashlet) */ public function removeDashlets(array $dashlets = []) { - if (empty($dashlets)) { - $dashlets = $this->getDashlets(); - } - + $dashlets = ! empty($dashlets) ? $dashlets : $this->getDashlets(); foreach ($dashlets as $dashlet) { $this->removeDashlet($dashlet); } @@ -452,16 +428,17 @@ public function loadDashletsFromDB() } $this->dashlets = []; - $dashlets = Model\Dashlet::on(Dashboard::getConn()); + $dashlets = Model\Dashlet::on(Dashboard::getConn())->with('module_dashlet'); $dashlets->filter(Filter::equal('dashboard_id', $this->getUuid())); foreach ($dashlets as $dashlet) { $newDashlet = new Dashlet($dashlet->name, $dashlet->url, $this); $newDashlet->fromArray([ - 'uuid' => $dashlet->id, - 'title' => t($dashlet->label), - 'priority' => $dashlet->priority, - 'pane' => $this + 'uuid' => $dashlet->id, + 'title' => t($dashlet->label), + 'priority' => $dashlet->priority, + 'pane' => $this, + 'description' => $dashlet->module_dashlet->description ]); $this->addDashlet($newDashlet); @@ -477,15 +454,20 @@ public function loadDashletsFromDB() * you have to also pass the origin pane * * @param Dashlet|Dashlet[] $dashlets - * @param ?Pane $origin + * @param ?Pane $origin * * @return $this */ public function manageDashlets($dashlets, Pane $origin = null) { + if (! $this->getHome()) { + throw new \LogicException( + 'Dashlets cannot be managed. Please make sure to set the current dashboard home beforehand.' + ); + } + $user = Dashboard::getUser(); $conn = Dashboard::getConn(); - $dashlets = is_array($dashlets) ? $dashlets : [$dashlets]; $order = count($this->getDashlets()) + 1; @@ -498,31 +480,40 @@ public function manageDashlets($dashlets, Pane $origin = null) break; } - $uuid = $dashlet->isModuleDashlet() - ? Dashboard::getSHA1($dashlet->getModule() . $this->getName() . $dashlet->getName()) - : Dashboard::getSHA1( - $user->getUsername() . $this->getHome()->getName() . $this->getName() . $dashlet->getName() - ); + $uuid = Dashboard::getSHA1( + $user->getUsername() . $this->getHome()->getName() . $this->getName() . $dashlet->getName() + ); if (! $this->hasDashlet($dashlet->getName()) && (! $origin || ! $origin->hasDashlet($dashlet->getName()))) { $conn->insert(Dashlet::TABLE, [ - 'id' => $uuid, - 'dashboard_id' => $this->getUuid(), - 'name' => $dashlet->getName(), - 'label' => $dashlet->getTitle(), - 'url' => $dashlet->getUrl()->getRelativeUrl(), - 'priority' => $order++ + 'id' => $uuid, + 'dashboard_id' => $this->getUuid(), + 'name' => $dashlet->getName(), + 'label' => $dashlet->getTitle(), + 'url' => $dashlet->getUrl()->getRelativeUrl(), + 'priority' => $order++ ]); - $dashlet->setUuid($uuid); - } else { + if ($dashlet->isModuleDashlet()) { + $systemUuid = Dashboard::getSHA1($dashlet->getModule() . $this->getName() . $dashlet->getName()); + $conn->insert('dashlet_system', [ + 'dashlet_id' => $uuid, + 'module_dashlet_id' => $systemUuid + ]); + } + } elseif (! $this->hasDashlet($dashlet->getName()) + || ! $origin + || ! $origin->hasDashlet($dashlet->getName())) { $conn->update(Dashlet::TABLE, [ - 'id' => $uuid, - 'dashboard_id' => $this->getUuid(), - 'label' => $dashlet->getTitle(), - 'url' => $dashlet->getUrl()->getRelativeUrl(), - 'priority' => $this->getPriority() + 'id' => $uuid, + 'dashboard_id' => $this->getUuid(), + 'label' => $dashlet->getTitle(), + 'url' => $dashlet->getUrl()->getRelativeUrl(), + 'priority' => $dashlet->getPriority() ], ['id = ?' => $dashlet->getUuid()]); + } else { + // This should have already been handled by the caller + break; } $dashlet->setPane($this); @@ -534,12 +525,12 @@ public function manageDashlets($dashlets, Pane $origin = null) public function toArray() { return [ - 'id' => $this->getUuid(), - 'name' => $this->getName(), - 'label' => $this->getTitle(), - 'home' => $this->getHome() ? $this->getHome()->getName() : null, - 'priority' => $this->getPriority(), - 'disabled' => (int) $this->isDisabled() + 'id' => $this->getUuid(), + 'name' => $this->getName(), + 'label' => $this->getTitle(), + 'home' => $this->getHome() ? $this->getHome()->getName() : null, + 'priority' => $this->getPriority(), + 'disabled' => (int) $this->isDisabled() ]; } } diff --git a/library/Icinga/Web/Dashboard/Settings.php b/library/Icinga/Web/Dashboard/Settings.php index c85502552e..09cf65a529 100644 --- a/library/Icinga/Web/Dashboard/Settings.php +++ b/library/Icinga/Web/Dashboard/Settings.php @@ -13,7 +13,7 @@ class Settings extends BaseHtmlElement { - protected $defaultAttributes = ['class' => ['dashboard-settings content']]; + protected $defaultAttributes = ['class' => 'content dashboard-settings']; protected $tag = 'div'; @@ -27,7 +27,7 @@ public function __construct(Dashboard $dashboard) protected function assemble() { - // TODO: What we should with disabled homes?? + // TODO: What we should do with disabled homes?? $activeHome = $this->dashboard->getActiveHome(); if (empty($this->dashboard->getHomes())) { @@ -44,13 +44,13 @@ protected function assemble() Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'), 'plus', [ - 'class' => 'add-dashboard', - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true + 'class' => 'add-dashboard', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true ] )); } else { - // Make a list of dashbaord homes + // Make a list of dashboard homes foreach ($this->dashboard->getHomes() as $home) { $this->addHtml(new DashboardHomeList($home)); } diff --git a/library/Icinga/Web/Dashboard/Setup/SetupNewDashboard.php b/library/Icinga/Web/Dashboard/Setup/SetupNewDashboard.php new file mode 100644 index 0000000000..5e8deed1f8 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Setup/SetupNewDashboard.php @@ -0,0 +1,279 @@ +dashboard = $dashboard; + + $this->setRedirectUrl((string) Url::fromPath(Dashboard::BASE_ROUTE)); + $this->setAction($this->getRedirectUrl() . '/setup-dashboard'); + } + + /** + * Initialize module dashlets + * + * @param array $dashlets + * + * @return $this + */ + public function initDashlets(array $dashlets) + { + $this->dashlets = $dashlets; + + return $this; + } + + public function hasBeenSubmitted() + { + return $this->hasBeenSent() + && ($this->getPopulatedValue('btn_cancel') + || $this->getPopulatedValue('submit')); + } + + protected function assemble() + { + $this->getAttributes()->add('class', 'modal-form'); + + if ($this->getPopulatedValue('btn_next')) { // Configure Dashlets + $this->dumpArbitaryDashlets(); + + $this->addElement('text', 'pane', [ + 'required' => true, + 'label' => t('Dashboard Title'), + 'description' => t('Enter a title for the new dashboard you want to add the dashlets to'), + ]); + + if (empty($this->dashlets) + || (count(array_keys($this->dashlets)) == 1 // Only one module + && count(reset($this->dashlets)) == 1 // Only one module dashlet + ) + ) { + $this->addHtml(HtmlElement::create('hr')); + + $this->addElement('text', 'dashlet', [ + 'required' => true, + 'label' => t('Dashlet Title'), + 'description' => t('Enter a title for the dashlet'), + ]); + + $this->addElement('textarea', 'url', [ + 'required' => true, + 'label' => t('Url'), + 'description' => t( + 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters' + ) + ]); + + foreach ($this->dashlets as $_ => $dashlets) { + /** @var Dashlet $dashlet */ + foreach ($dashlets as $dashlet) { + $this->getElement('dashlet')->getAttributes()->set('value', $dashlet->getTitle()); + $this->getElement('url')->getAttributes()->set('value', $dashlet->getUrl()->getRelativeUrl()); + } + } + } else { + foreach ($this->dashlets as $module => $dashlets) { + /** @var Dashlet $dashlet */ + foreach ($dashlets as $dashlet) { + $listControl = $this->createFormListControl(); + $listControl->getAttributes()->add('class', 'multi-dashlets'); + + $listControl->addHtml(HtmlElement::create('div', ['class' => 'dashlets-list-info'], [ + new Icon('angle-down', ['class' => 'expand-icon', 'title' => t('Expand')]), + new Icon('angle-up', ['class' => 'collapse-icon', 'title' => t('Collapse')]) + ])); + + $dashletName = $this->createElement('text', $module . $dashlet->getName(), [ + 'required' => true, + 'label' => t('Dashlet Title'), + 'value' => $dashlet->getTitle(), + 'description' => t('Enter a title for the dashlet'), + ]); + + $dashletUrl = $this->createElement('textarea', $module . $dashlet->getName() . '_url', [ + 'required' => true, + 'label' => t('Url'), + 'value' => $dashlet->getUrl()->getRelativeUrl(), + 'description' => t( + 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters' + ) + ]); + + $this->registerElement($dashletName)->decorate($dashletName); + $this->registerElement($dashletUrl)->decorate($dashletUrl); + + $listControl->addHtml(HtmlElement::create('span', null, t($dashlet->getTitle()))); + + $listControl->addHtml($dashletName); + $listControl->addHtml($dashletUrl); + + $this->addHtml($listControl); + } + } + } + + $submitButton = $this->createElement('submit', 'submit', ['label' => t('Add Dashlets')]); + $this->registerElement($submitButton)->decorate($submitButton); + } else { // Select Dashlets + $list = HtmlElement::create('ul', ['class' => 'dashlet-item-list empty-list']); + $multi = new DashletListMultiSelect(); + $multi->setCheckBox($this->createElement('checkbox', 'custom_url', ['class' => 'sr-only'])); + + $listControl = $this->createFormListControl(); + $listControl->getAttributes()->remove('class', 'collapsible'); + + $this->addHtml($listControl->addHtml($list->addHtml($multi))); + + foreach ($this->dashlets as $module => $dashlets) { + $listControl = $this->createFormListControl(); + $listControl->addHtml(HtmlElement::create('div', ['class' => 'dashlets-list-info'], [ + new Icon('angle-down', ['class' => 'expand-icon', 'title' => t('Expand')]), + new Icon('angle-up', ['class' => 'collapse-icon', 'title' => t('Collapse')]) + ])); + + $list = HtmlElement::create('ul', ['class' => 'dashlet-item-list ' . $module]); + $listControl->addHtml(HtmlElement::create('span', null, ucfirst($module))); + + /** @var Dashlet $dashlet */ + foreach ($dashlets as $dashlet) { + $multi = new DashletListMultiSelect($dashlet); + $multi->setCheckBox( + $this->createElement('checkbox', $module . '|' . $dashlet->getName(), ['class' => 'sr-only']) + ); + + $list->addHtml($multi); + } + + $this->addHtml($listControl->addHtml($list)); + } + + $submitButton = $this->createElement('submit', 'btn_next', [ + 'class' => 'autosubmit', + 'label' => t('Next'), + ]); + $this->registerElement($submitButton)->decorate($submitButton); + } + + $this->addElement('submit', 'btn_cancel', ['label' => t('Cancel')]); + $this->getElement('btn_cancel')->setWrapper($submitButton->getWrapper()); + } + + protected function onSuccess() + { + if ($this->getPopulatedValue('submit')) { + $conn = Dashboard::getConn(); + $pane = new Pane($this->getPopulatedValue('pane')); + + $conn->beginTransaction(); + + try { + $this->dashboard->getHome(DashboardHome::DEFAULT_HOME)->managePanes($pane); + + // If element name "dashlet" and "url" are set we need to only store one dashlet + if (($name = $this->getPopulatedValue('dashlet')) && ($url = $this->getPopulatedValue('url'))) { + $dashlet = new Dashlet($name, $url, $pane); + $pane->manageDashlets($dashlet); + } else { + foreach ($this->dashlets as $module => $dashlets) { + $moduleDashlets = []; + + /** @var Dashlet $dashlet */ + foreach ($dashlets as $dashlet) { + $element = str_replace(' ', '_', $module . $dashlet->getName()); + if (! $this->getPopulatedValue($element)) { + continue; + } + + $title = $this->getPopulatedValue($element); + $url = $this->getPopulatedValue($element . '_url'); + + $dashlet + ->setUrl($url) + ->setTitle($title) + ->setModule($module) + ->setModuleDashlet(true); + + if ($dashlet->getPane()) { + $paneName = $dashlet->getPane()->getName(); + $dashlet->setUuid(Dashboard::getSHA1($module . $paneName . $dashlet->getName())); + } else { + $dashlet->setUuid(Dashboard::getSHA1($module . $dashlet->getName())); + } + + $moduleDashlets[$dashlet->getName()] = $dashlet; + } + + $pane->manageDashlets($moduleDashlets); + } + } + + $conn->commitTransaction(); + } catch (\Exception $err) { + $conn->rollBackTransaction(); + throw $err; + } + + Notification::success(t('Created dashboard successfully')); + } + } + + /** + * Dump all module dashlets which are not selected by the user + * from the member variable + * + * @return $this + */ + private function dumpArbitaryDashlets() + { + $choosenDashlets = []; + foreach ($this->dashlets as $module => $dashlets) { + /** @var Dashlet $dashlet */ + foreach ($dashlets as $dashlet) { + $element = str_replace(' ', '_', $module . '|' . $dashlet->getName()); + if ($this->getPopulatedValue($element) === 'y') { + $choosenDashlets[$module][$dashlet->getName()] = $dashlet; + } + } + } + + $this->dashlets = $choosenDashlets; + + return $this; + } + + /** + * Create collapsible form list control + * + * @return ValidHtml + */ + private function createFormListControl() + { + return HtmlElement::create('div', [ + 'class' => ['control-group', 'form-list-control', 'collapsible'], + 'data-toggle-element' => '.dashlets-list-info' + ]); + } +} diff --git a/library/Icinga/Web/HomeMenu.php b/library/Icinga/Web/HomeMenu.php index 423048c8ad..a7704ea466 100644 --- a/library/Icinga/Web/HomeMenu.php +++ b/library/Icinga/Web/HomeMenu.php @@ -29,10 +29,10 @@ public function initHome() foreach ($homes as $home) { $dashboardHome = new DashboardHome($home->name, [ - 'uuid' => $home->id, - 'label' => t($home->label), - 'priority' => $home->priority, - 'type' => $home->type, + 'uuid' => $home->id, + 'label' => t($home->label), + 'priority' => $home->priority, + 'type' => $home->type, ]); $dashboardItem->addChild($dashboardHome); diff --git a/library/Icinga/Web/Navigation/DashboardHome.php b/library/Icinga/Web/Navigation/DashboardHome.php index ccac015e34..399a7f4d25 100644 --- a/library/Icinga/Web/Navigation/DashboardHome.php +++ b/library/Icinga/Web/Navigation/DashboardHome.php @@ -1,5 +1,7 @@ getName() !== self::DEFAULT_HOME && ! $this->isDisabled()) { $this->setUrl(Url::fromPath(Dashboard::BASE_ROUTE . '/home', [ - 'home' => $this->getName() + 'home' => $this->getName() ])); } } @@ -283,7 +285,10 @@ public function removePane($pane) if (! $pane->isOverriding()) { $pane->removeDashlets(); - Dashboard::getConn()->delete(Pane::TABLE, ['id = ?' => $this->getUuid()]); + Dashboard::getConn()->delete(Pane::TABLE, [ + 'id = ?' => $pane->getUuid(), + 'home_id = ?' => $this->getUuid() + ]); } return $this; @@ -311,7 +316,7 @@ public function removePanes(array $panes = []) * * @return $this */ - public function loadDashboardsFromDB() + public function loadPanesFromDB() { // Skip when this home is either disabled or inactive if (! $this->getActive() || $this->isDisabled()) { @@ -328,10 +333,10 @@ public function loadDashboardsFromDB() $newPane = new Pane($pane->name); //$newPane->disable($pane->disable); $newPane->fromArray([ - 'uuid' => $pane->id, - 'title' => t($pane->label), - 'priority' => $pane->priority, - 'home' => $this + 'uuid' => $pane->id, + 'title' => $pane->label, + 'priority' => $pane->priority, + 'home' => $this ]); $newPane->loadDashletsFromDB(); @@ -345,15 +350,16 @@ public function loadDashboardsFromDB() /** * Manage the given pane(s) * - * If you want to move the pane(s) from another to this pane, - * you have to also pass the origin home with + * If you want to move the pane(s) from another to this home, + * you have to also pass through the origin home with * - * @param Pane|Pane[] $panes - * @param DashboardHome|null $origin + * @param Pane|Pane[] $panes + * @param ?DashboardHome $origin + * @param bool $mngPaneDashlets * * @return $this */ - public function managePanes($panes, DashboardHome $origin = null) + public function managePanes($panes, DashboardHome $origin = null, $mngPaneDashlets = false) { $user = Dashboard::getUser(); $conn = Dashboard::getConn(); @@ -366,30 +372,41 @@ public function managePanes($panes, DashboardHome $origin = null) if (! $pane->isOverriding()) { if (! $this->hasPane($pane->getName()) && (! $origin || ! $origin->hasPane($pane->getName()))) { $conn->insert(Pane::TABLE, [ - 'id' => $uuid, - 'home_id' => $this->getUuid(), - 'name' => $pane->getName(), - 'label' => $pane->getTitle(), - 'username' => $user->getUsername(), - 'priority' => $order++ + 'id' => $uuid, + 'home_id' => $this->getUuid(), + 'name' => $pane->getName(), + 'label' => $pane->getTitle(), + 'username' => $user->getUsername(), + 'priority' => $order++ ]); - } else { + } elseif (! $this->hasPane($pane->getName()) || ! $origin || ! $origin->hasPane($pane->getName())) { $conn->update(Pane::TABLE, [ - 'id' => $uuid, - 'home_id' => $this->getUuid(), - 'label' => $pane->getTitle(), - 'priority' => $pane->getPriority() - ], ['id = ?' => $pane->getUuid()]); + 'id' => $uuid, + 'home_id' => $this->getUuid(), + 'label' => $pane->getTitle(), + 'priority' => $pane->getPriority() + ], [ + 'id = ?' => $pane->getUuid(), + 'home_id = ?' => $this->getUuid() + ]); + } else { + // Failed to move the pane! Should have been handled already by the caller + break; } - // We may want to update some dashlets later, so we need to set the uuid - // here too in case the pane might be moved from another home $pane->setUuid($uuid); } else { // TODO(TBD) } $pane->setHome($this); + if ($mngPaneDashlets) { + // Those dashboard panes are usually system defaults and go up when + // the user is clicking on the "Use System Defaults" button + $dashlets = $pane->getDashlets(); + $pane->setDashlets([]); + $pane->manageDashlets($dashlets); + } } return $this; diff --git a/public/css/icinga/dashboards.less b/public/css/icinga/dashboards.less index 5e2c3d9564..b18c2454bc 100644 --- a/public/css/icinga/dashboards.less +++ b/public/css/icinga/dashboards.less @@ -6,7 +6,7 @@ top: 40%; right: 50%; position: absolute; - transform: translate(50%,-50%); + transform: translate(50%, -50%); color: @text-color; h1, p { @@ -68,10 +68,14 @@ } } +.collapsed .dashlet-list-item { + margin-top: 1.3em; +} + // All the dashboard item lists -.home-item-list, .dashboard-item-list, .dashlet-item-list { +.dashboard-item-list, .dashlet-item-list { padding: 0; - margin-left: 0; + margin: 1em 0 0; width: 100%; display: flex; flex-flow: row wrap; @@ -79,7 +83,11 @@ label { width: 49%; - margin: 0 6px 6px 0; + margin-bottom: .5em; + } + + label:nth-child(odd) { + margin-right: .5em; } .dashlet-list-item { @@ -99,9 +107,9 @@ } .caption { - height: auto; - min-height: 3em;; - font-size: 1.2em; + height: 3em; + overflow-y: auto; + font-size: 1.1em; color: @text-color-light; } } @@ -142,7 +150,7 @@ line-height: normal; text-decoration: none; border: 2px solid @icinga-blue; - border-radius: 3px; + border-radius: .4em; color: @text-color-on-icinga-blue; background-color: @icinga-blue; } @@ -152,30 +160,34 @@ } .action-link.add-dashboard { - width: 82.6%; - margin-top: 1em; + width: 81.5%; + margin-top: .5em; } .action-link.add-dashlet, .action-link.add-dashboard { order: 1; font-size: 1.2em; font-weight: 500; - padding: .9em 1em; - border-radius: .5em; + padding: .6em 1em; + border-radius: .3em; text-decoration: none; background-color: @low-sat-blue; } -.dashboard-item-list { +.dashboard-list-control .dashlet-item-list { width: 100%; display: flex; margin-left: 2.2em; .dashlet-list-item { width: 49.4%; - margin: 0 6px 6px 0; + margin-bottom: .5em; background: transparent; } + + .dashlet-list-item:nth-child(odd) { + margin-right: .5em; + } } .dashboard-list-control { @@ -186,7 +198,7 @@ } .action-link.add-dashlet { - width: 98.8%; // Dashlet list item 49.4% x 2 + width: 99.5%; margin-left: 1.8em; } } @@ -203,6 +215,7 @@ h1.collapsible-header a, h1.dashlet-header a { margin-left: .3em; + font-weight: normal; color: @icinga-blue; } @@ -226,11 +239,16 @@ } .action-link.add-dashboard { - margin-left: .4em; + width: 81.6%; + margin-left: 1.3em; } .dashboard-list-control { - .dashboard-item-list { + .dashboard-item-list, .dashlet-item-list { + margin: 0; + } + + .dashlet-item-list { margin-left: 4em; } @@ -244,6 +262,7 @@ .action-link.add-dashlet { margin-left: 3.4em; + width: 99.2%; } } } @@ -286,10 +305,10 @@ box-shadow: 4px 4px 4px 4px @black; } -.dashlet-sortable.drag-active * { +.drag-active * { pointer-events: none; } -.draggable-element{ +.draggable-element { opacity: .4; } diff --git a/public/js/icinga/behavior/dashboards.js b/public/js/icinga/behavior/dashboards.js index 4c701bf4ef..392af2a43a 100644 --- a/public/js/icinga/behavior/dashboards.js +++ b/public/js/icinga/behavior/dashboards.js @@ -4,6 +4,13 @@ 'use strict'; + /** + * Possible type of widgets this behavior is being applied to + * + * @type {object} + */ + const WIDGET_TYPES = { Dashlets : 'Dashlets', Dashboards : 'Dashboards', DashboardHomes : 'Homes' }; + Icinga.Behaviors = Icinga.Behaviors || {}; /** @@ -16,26 +23,28 @@ var Dashboard = function (icinga) { Icinga.EventListener.call(this, icinga); + this.icinga = icinga; + /** - * Base route + * Type of the widget which is currently being sorted * * @type {string} */ - this.baseUrl = icinga.config.baseUrl + this.sortedWidgetType = WIDGET_TYPES.Dashlets; /** - * Dashlet container id which is currently being dragged + * Widget container id which is currently being dragged * * @type {null|string} */ this.containerId = null; // Register event handlers for drag and drop functionalities - this.on('dragstart', '.dashboard > .dashlet-sortable', this.onDragStart, this); - this.on('dragover', '.dashboard > .dashlet-sortable', this.onDragOver, this); - this.on('dragleave', '.dashboard > .dashlet-sortable', this.onDragLeave, this); - this.on('dragend', '.dashboard > .dashlet-sortable', this.onDragEnd, this); - this.on('drop', '.dashboard > .dashlet-sortable', this.onDrop, this); + this.on('dragstart', '.widget-sortable', this.onDragStart, this); + this.on('dragover', '.widget-sortable', this.onDragOver, this); + this.on('dragleave', '.widget-sortable', this.onDragLeave, this); + this.on('dragend', '.widget-sortable', this.onDragEnd, this); + this.on('drop', '.widget-sortable', this.onDrop, this); }; Dashboard.prototype = new Icinga.EventListener(); @@ -50,16 +59,16 @@ let _this = event.data.self; let $target = $(event.target); - if (! $target.hasClass('dashlet-sortable')) { + if (! _this.isDraggable($target)) { return false; } - event.originalEvent.dataTransfer.setData('text', $target.attr('id')); + _this.containerId = $target.attr('id'); $target.addClass('draggable-element'); - _this.containerId = $target.attr('id'); + let $parent = $target.parent()[0]; // Prevents child elements from being the target of pointer events - $('.dashboard.content').children('.dashlet-sortable').addClass('drag-active'); + $($parent).children('.widget-sortable').addClass('drag-active'); }; /** @@ -74,8 +83,9 @@ let $target = $(event.target); let _this = event.data.self; - if (! $target.hasClass('dashlet-sortable')) { - $target = $target.closest('.dashlet-sortable'); + // Moving an element arbitrarily elsewhere isn't allowed + if (! _this.isDraggable($target) || ! _this.isDraggableSiblingOf($target)) { + return false; } // Don't show mouse drop cursor if the target element is the draggable element @@ -95,6 +105,11 @@ */ Dashboard.prototype.onDragLeave = function (event) { let $target = $(event.target); + let _this = event.data.self; + + if (! _this.isDraggable($target) || ! _this.isDraggableSiblingOf($target)) { + return false; + } $target.removeClass('drag-over'); }; @@ -106,12 +121,18 @@ */ Dashboard.prototype.onDragEnd = function (event) { let $target = $(event.target); + let _this = event.data.self; + + if (! _this.isDraggable($target) || ! _this.isDraggableSiblingOf($target)) { + return false; + } $target.removeClass('draggable-element'); $target.removeClass('drag-over'); + let $parent = $target.parent()[0]; // The draggable is now released, so we have to remove the class to enable the pointer events again - $('.dashboard.content').children('.dashlet-sortable').removeClass('drag-active'); + $($parent).children('.widget-sortable').removeClass('drag-active'); }; /** @@ -123,57 +144,162 @@ let $target = $(event.target); let _this = event.data.self; - // Prevents from being dropped in a child elements - if (! $target.hasClass('dashlet-sortable')) { - $target = $target.closest('.dashlet-sortable'); + // Don't allow to drop an element arbitrarily elsewhere + if (! _this.isDraggable($target) || ! _this.isDraggableSiblingOf($target)) { + return false; } // Prevent default behaviors to allow the drop event event.preventDefault(); + event.stopPropagation(); - const dragTarget = event.originalEvent.dataTransfer.getData('text'); - const draggable = $('#' + dragTarget); + const draggable = $target.parent().children('#' + _this.containerId); // If the target element has been located before the draggable element, // insert the draggable before the target element otherwise after it if ($target.nextAll().filter(draggable).length) { - $(draggable).insertBefore($target); + draggable.insertBefore($target); } else { - $(draggable).insertAfter($target); + draggable.insertAfter($target); } // Draggable element is now dropped, so drag-over class must also be removed $target.removeClass('drag-over'); - _this.postReorderedDashlets(); + if ($target.data('icinga-pane')) { + _this.sortedWidgetType = WIDGET_TYPES.Dashboards; + } else if ($target.data('icinga-home')) { + _this.sortedWidgetType = WIDGET_TYPES.DashboardHomes; + } + + _this.sendReorderedWidgets($target); + }; + + /** + * Get whether the given element is draggable + * + * @param $target {jQuery} + * + * @returns {boolean} + */ + Dashboard.prototype.isDraggable = function ($target) { + return $target.attr('draggable'); + }; + + /** + * Get whether the given element is sibling of the element currently being dragged + * + * @param $target {jQuery} + * + * @returns {number} + */ + Dashboard.prototype.isDraggableSiblingOf = function ($target) { + return $target.parent().children('#' + this.containerId).length; }; /** * Set up a request with the reordered containers and post the data to the controller + * + * @param $target {jQuery} */ - Dashboard.prototype.postReorderedDashlets = function () { + Dashboard.prototype.sendReorderedWidgets = function ($target) { let _this = this, - $dashboard = $('.dashboard.content'), - $paneAndHome = $dashboard.data('icinga-pane').split('|', 2), - $dashlets = []; - - $dashboard.children('.dashlet-sortable').each(function () { - $dashlets.push($(this).data('icinga-dashlet')); - }); - - let $pane = $paneAndHome.pop(); - let $home = $paneAndHome.pop(); - - let data = {[$home]: { [$pane]: $dashlets }}; - - $.ajax({ - context : _this, - type : 'post', - url : _this.baseUrl + '/dashboards/reorder-dashlets', - headers : {'Accept': 'application/json'}, - contentType : 'application/json', - data : JSON.stringify(data) - }); + data = {}; + + switch (_this.sortedWidgetType) { + case WIDGET_TYPES.DashboardHomes: { + let $homes = []; + $target.parent().children('.home-list-control.widget-sortable').each(function () { + let home = $(this); + if (typeof home.data('icinga-home') === 'undefined') { + _this.icinga.logger.error( + '[Dashboards] Dashboard home widget has no "icingaHome" data attribute registered: ', + home[0] + ); + return; + } + + $homes.push(home.data('icinga-home')); + }); + + data = { ...$homes }; + break; + } + case WIDGET_TYPES.Dashboards: { + let $home, $panes = []; + $target.parent().children('.dashboard-list-control.widget-sortable').each(function () { + let pane = $(this); + if (typeof pane.data('icinga-pane') === 'undefined') { + _this.icinga.logger.error( + '[Dashboards] Dashboard widget has no "icingaPane" data attribute registered: ', + pane[0] + ); + return; + } + + pane = pane.data('icinga-pane').split('|', 2); + if (! $home) { + $home = pane.shift(); + } + + $panes.push(pane.pop()); + }); + + data[$home] = $panes; + break; + } + case WIDGET_TYPES.Dashlets: { + let $home, $pane, $dashlets = []; + $target.parent().children('.widget-sortable').each(function () { + let dashlet = $(this); + if (typeof dashlet.data('icinga-dashlet') === 'undefined') { + _this.icinga.logger.error( + '[Dashboards] Dashlet widget has no "icingaDashlet" data attribute registered: ', + dashlet[0] + ); + return; + } + + if (! $home && ! $pane) { + let pane = dashlet.parent(); + if (typeof pane.data('icinga-pane') === 'undefined') { + // Nested parents + pane = pane.parent(); + if (typeof pane.data('icinga-pane') === 'undefined') { + _this.icinga.logger.error( + '[Dashboards] Dashlet parent widget has no "icingaPane" data attribute registered: ', + pane[0] + ); + return; + } + } + + pane = pane.data('icinga-pane').split('|', 2); + $home = pane.shift(); + $pane = pane.shift(); + } + + $dashlets.push(dashlet.data('icinga-dashlet')); + }); + + if ($home && $pane) { + data[$home] = { [$pane] : $dashlets }; + } + } + } + + if (Object.keys(data).length) { + data.Type = _this.sortedWidgetType; + + $.ajax({ + context : _this, + type : 'post', + url : _this.icinga.config.baseUrl + '/dashboards/reorder-widgets', + headers : { 'Accept' : 'application/json' }, + contentType : 'application/json', + data : JSON.stringify(data), + }); + } }; Icinga.Behaviors.Dashboard = Dashboard; From a2623ef7cf2eeaccbd329a7d169e815cad2815d0 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 28 Mar 2022 10:58:39 +0200 Subject: [PATCH 032/106] Forms: Introduce NewHomePaneForm class --- .../forms/Dashboard/NewHomePaneForm.php | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 application/forms/Dashboard/NewHomePaneForm.php diff --git a/application/forms/Dashboard/NewHomePaneForm.php b/application/forms/Dashboard/NewHomePaneForm.php new file mode 100644 index 0000000000..5066fdd9f9 --- /dev/null +++ b/application/forms/Dashboard/NewHomePaneForm.php @@ -0,0 +1,92 @@ +dashboard = $dashboard; + + $requestUrl = Url::fromRequest(); + + // We need to set this explicitly needed for modals + $this->setAction((string) $requestUrl); + + if ($requestUrl->hasParam('home')) { + $this->populate(['home' => $requestUrl->getParam('home')]); + } + } + + public function hasBeenSubmitted() + { + return $this->hasBeenSent() + && ($this->getPopulatedValue('btn_cancel') + || $this->getPopulatedValue('submit')); + } + + protected function assemble() + { + $populatedHome = Url::fromRequest()->getParam('home'); + $this->addElement('text', 'pane', [ + 'required' => true, + 'label' => t('Title'), + 'description' => t('Add new dashboard to this home.') + ]); + + $this->addElement('select', 'home', [ + 'required' => true, + 'class' => 'autosubmit', + 'value' => $populatedHome, + 'multiOptions' => $this->dashboard->getHomeKeyTitleArr(), + 'label' => t('Assign to Home'), + 'description' => t('A dashboard home you want to assign the new dashboard to.'), + ]); + + $submitButton = $this->createElement('submit', 'submit', [ + 'class' => 'autosubmit', + 'label' => t('Add Dashboard'), + ]); + $this->registerElement($submitButton)->decorate($submitButton); + + $this->addElement('submit', 'btn_cancel', ['label' => t('Cancel')]); + $this->getElement('btn_cancel')->setWrapper($submitButton->getWrapper()); + } + + protected function onSuccess() + { + $requestUrl = Url::fromRequest(); + $dashboard = $this->dashboard; + $conn = Dashboard::getConn(); + + if ($requestUrl->getPath() === Dashboard::BASE_ROUTE . '/new-pane') { + $home = $this->getPopulatedValue('home'); + $home = $dashboard->getHome($home); + + $conn->beginTransaction(); + + try { + $pane = new Pane($this->getPopulatedValue('pane')); + $home->managePanes($pane); + + $conn->commitTransaction(); + } catch (\Exception $err) { + $conn->rollBackTransaction(); + throw $err; + } + + Notification::success('Added dashboard successfully'); + } else { + + } + } +} From 532ed027f7d35a22933bd0017d2a40a38c5c7031 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 28 Mar 2022 19:00:22 +0200 Subject: [PATCH 033/106] modal.js: Register onModalClose event also on `.modal-cancel` selector --- public/js/icinga/behavior/modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/icinga/behavior/modal.js b/public/js/icinga/behavior/modal.js index a575e01eab..d58d903a3d 100644 --- a/public/js/icinga/behavior/modal.js +++ b/public/js/icinga/behavior/modal.js @@ -23,7 +23,7 @@ this.on('change', '#modal form input.autosubmit', this.onFormAutoSubmit, this); this.on('click', '[data-icinga-modal]', this.onModalToggleClick, this); this.on('mousedown', '#layout > #modal', this.onModalLeave, this); - this.on('click', '.modal-header > button', this.onModalClose, this); + this.on('click', '.modal-header > button, .modal-cancel', this.onModalClose, this); this.on('keydown', this.onKeyDown, this); }; From d6a2f1e775e5799e4819990d8fce219d508c7e05 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 28 Mar 2022 10:59:14 +0200 Subject: [PATCH 034/106] Do some code refactoring --- .../controllers/DashboardsController.php | 249 +++++++---- .../forms/Dashboard/BaseDashboardForm.php | 113 +++++ application/forms/Dashboard/DashletForm.php | 269 ++++++------ application/forms/Dashboard/HomePaneForm.php | 133 +++--- .../forms/Dashboard/NewHomePaneForm.php | 70 +-- .../forms/Dashboard/RemoveDashletForm.php | 22 +- .../forms/Dashboard/RemoveHomePaneForm.php | 36 +- application/forms/Dashboard/WelcomeForm.php | 6 +- application/views/scripts/layout/menu.phtml | 2 +- library/Icinga/Common/DataExtractor.php | 10 +- .../Web/Dashboard/Common/BaseDashboard.php | 267 +++++++++++ .../Dashboard/Common/DashboardControls.php | 126 ++++++ .../Web/Dashboard/Common/DashboardEntry.php | 131 ++++++ .../Web/Dashboard/Common/DashboardManager.php | 217 ++------- .../Web/Dashboard/Common/DisableWidget.php | 37 -- .../Web/Dashboard/Common/ItemListControl.php | 67 ++- .../Web/Dashboard/Common/OrderWidget.php | 39 -- .../{ => Common}/OverridingWidget.php | 2 +- .../Icinga/Web/Dashboard/Common/Sortable.php | 23 + library/Icinga/Web/Dashboard/Dashboard.php | 55 ++- .../Icinga/Web/Dashboard/DashboardHome.php | 240 ++++++++++ library/Icinga/Web/Dashboard/Dashlet.php | 186 ++------ .../Dashboard/ItemList/DashboardHomeList.php | 62 ++- .../Web/Dashboard/ItemList/DashboardList.php | 66 ++- .../Dashboard/ItemList/DashletListItem.php | 55 ++- .../ItemList/DashletListMultiSelect.php | 2 + library/Icinga/Web/Dashboard/Pane.php | 370 +++------------- library/Icinga/Web/Dashboard/Settings.php | 29 +- .../Web/Dashboard/Setup/SetupNewDashboard.php | 46 +- library/Icinga/Web/HomeMenu.php | 29 +- .../Icinga/Web/Navigation/DashboardHome.php | 414 ------------------ .../Web/Navigation/DashboardHomeItem.php | 42 ++ library/Icinga/Web/Widget/SearchDashboard.php | 16 +- .../Widget/Tabextension/DashboardSettings.php | 14 +- public/css/icinga/dashboards.less | 101 +++-- public/js/icinga/behavior/dashboards.js | 374 ++++++---------- 36 files changed, 1959 insertions(+), 1961 deletions(-) create mode 100644 application/forms/Dashboard/BaseDashboardForm.php create mode 100644 library/Icinga/Web/Dashboard/Common/BaseDashboard.php create mode 100644 library/Icinga/Web/Dashboard/Common/DashboardControls.php create mode 100644 library/Icinga/Web/Dashboard/Common/DashboardEntry.php delete mode 100644 library/Icinga/Web/Dashboard/Common/DisableWidget.php delete mode 100644 library/Icinga/Web/Dashboard/Common/OrderWidget.php rename library/Icinga/Web/Dashboard/{ => Common}/OverridingWidget.php (91%) create mode 100644 library/Icinga/Web/Dashboard/Common/Sortable.php create mode 100644 library/Icinga/Web/Dashboard/DashboardHome.php delete mode 100644 library/Icinga/Web/Navigation/DashboardHome.php create mode 100644 library/Icinga/Web/Navigation/DashboardHomeItem.php diff --git a/application/controllers/DashboardsController.php b/application/controllers/DashboardsController.php index 9cd791b217..d576a9535b 100644 --- a/application/controllers/DashboardsController.php +++ b/application/controllers/DashboardsController.php @@ -7,15 +7,19 @@ use GuzzleHttp\Psr7\ServerRequest; use Icinga\Forms\Dashboard\DashletForm; use Icinga\Forms\Dashboard\HomePaneForm; +use Icinga\Forms\Dashboard\NewHomePaneForm; use Icinga\Forms\Dashboard\RemoveDashletForm; use Icinga\Forms\Dashboard\RemoveHomePaneForm; use Icinga\Forms\Dashboard\WelcomeForm; use Icinga\Model\ModuleDashlet; +use Icinga\Util\Json; use Icinga\Web\Dashboard\Dashboard; +use Icinga\Web\Dashboard\DashboardHome; +use Icinga\Web\Dashboard\Pane; use Icinga\Web\Dashboard\Settings; use Icinga\Web\Dashboard\Setup\SetupNewDashboard; +use Icinga\Web\Notification; use Icinga\Web\Widget\Tabextension\DashboardSettings; -use ipl\Html\HtmlElement; use ipl\Web\Compat\CompatController; use ipl\Web\Url; use ipl\Web\Widget\ActionLink; @@ -40,7 +44,7 @@ public function indexAction() $this->createTabs(); $activeHome = $this->dashboard->getActiveHome(); - if (! $activeHome || ! $activeHome->hasPanes()) { + if (! $activeHome || ! $activeHome->hasEntries()) { $this->getTabs()->add('dashboard', [ 'active' => true, 'title' => t('Welcome'), @@ -54,8 +58,6 @@ public function indexAction() })->handleRequest(ServerRequest::fromGlobals()); $this->dashboard->setWelcomeForm($welcomeForm); - } elseif (empty($activeHome->getPanes(true))) { - // TODO(TBD): What to do when the user has only disabled dashboards? Should we render the welcome screen? } elseif (($pane = $this->getParam('pane'))) { $this->getTabs()->activate($pane); } @@ -71,38 +73,43 @@ public function indexAction() public function homeAction() { $home = $this->params->getRequired('home'); - if (! $this->dashboard->hasHome($home)) { + if (! $this->dashboard->hasEntry($home)) { $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } - $this->createTabs(); - $activeHome = $this->dashboard->getActiveHome(); - if (! $activeHome || empty($activeHome->getPanes(true))) { - $this->getTabs()->add($home, [ + if (! $activeHome->getEntries()) { + $this->getTabs()->add($activeHome->getName(), [ 'active' => true, - 'title' => $home, + 'title' => $activeHome->getTitle(), 'url' => Url::fromRequest() ]); + + // Not to render the cog icon before the above tab + $this->createTabs(); } elseif (($pane = $this->getParam('pane'))) { + $this->createTabs(); + $this->dashboard->activate($pane); } $this->content = $this->dashboard; } - public function renameHomeAction() + public function editHomeAction() { $this->setTitle(t('Update Home')); $home = $this->params->getRequired('home'); - if (! $this->dashboard->hasHome($home)) { + if (! $this->dashboard->hasEntry($home)) { $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } $homeForm = (new HomePaneForm($this->dashboard)) ->on(HomePaneForm::ON_SUCCESS, function () { - $this->redirectNow('__CLOSE__'); + $this->getResponse()->setAutoRefreshInterval(1); + + $this->redirectNow(Url::fromPath(Dashboard::BASE_ROUTE . '/settings')); }) ->handleRequest(ServerRequest::fromGlobals()); @@ -115,19 +122,41 @@ public function removeHomeAction() $this->setTitle(t('Remove Home')); $home = $this->params->getRequired('home'); - if (! $this->dashboard->hasHome($home)) { + if (! $this->dashboard->hasEntry($home)) { $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } $homeForm = (new RemoveHomePaneForm($this->dashboard)) ->on(RemoveHomePaneForm::ON_SUCCESS, function () { - $this->redirectNow('__CLOSE__'); + $this->getResponse()->setAutoRefreshInterval(1); + + $this->redirectNow(Url::fromPath(Dashboard::BASE_ROUTE . '/settings')); }) ->handleRequest(ServerRequest::fromGlobals()); $this->addContent($homeForm); } + public function newPaneAction() + { + $this->setTitle(t('Add new Dashboard')); + + $home = $this->params->getRequired('home'); + if (! $this->dashboard->hasEntry($home)) { + $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); + } + + $paneForm = (new NewHomePaneForm($this->dashboard)) + ->on(NewHomePaneForm::ON_SUCCESS, function () { + $this->getResponse()->setAutoRefreshInterval(1); + + $this->redirectNow(Url::fromPath(Dashboard::BASE_ROUTE . '/settings')); + }) + ->handleRequest(ServerRequest::fromGlobals()); + + $this->addContent($paneForm); + } + public function editPaneAction() { $this->setTitle(t('Update Pane')); @@ -135,21 +164,23 @@ public function editPaneAction() $pane = $this->params->getRequired('pane'); $home = $this->params->getRequired('home'); - if (! $this->dashboard->hasHome($home)) { + if (! $this->dashboard->hasEntry($home)) { $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } - if (! $this->dashboard->getActiveHome()->hasPane($pane)) { + if (! $this->dashboard->getActiveHome()->hasEntry($pane)) { $this->httpNotFound(sprintf(t('Pane "%s" not found'), $pane)); } $paneForm = (new HomePaneForm($this->dashboard)) ->on(HomePaneForm::ON_SUCCESS, function () { - $this->redirectNow('__CLOSE__'); + $this->getResponse()->setAutoRefreshInterval(1); + + $this->redirectNow(Url::fromPath(Dashboard::BASE_ROUTE . '/settings')); }) ->handleRequest(ServerRequest::fromGlobals()); - $paneForm->load($this->dashboard->getActiveHome()->getPane($pane)); + $paneForm->load($this->dashboard->getActiveHome()->getEntry($pane)); $this->addContent($paneForm); } @@ -161,25 +192,21 @@ public function removePaneAction() $home = $this->params->getRequired('home'); $paneParam = $this->params->getRequired('pane'); - if (! $this->dashboard->hasHome($home)) { + if (! $this->dashboard->hasEntry($home)) { $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } - if (! $this->dashboard->getActiveHome()->hasPane($paneParam)) { + if (! $this->dashboard->getActiveHome()->hasEntry($paneParam)) { $this->httpNotFound(sprintf(t('Pane "%s" not found'), $paneParam)); } $paneForm = new RemoveHomePaneForm($this->dashboard); $paneForm->populate(['org_name' => $paneParam]); $paneForm->on(RemoveHomePaneForm::ON_SUCCESS, function () { - $this->redirectNow('__CLOSE__'); - })->handleRequest(ServerRequest::fromGlobals()); + $this->getResponse()->setAutoRefreshInterval(1); - $paneForm->getElement('btn_remove')->setLabel(t('Remove Pane')); - $paneForm->prependHtml(HtmlElement::create('h1', null, sprintf( - t('Please confirm removal of dashboard pane "%s"'), - $paneParam - ))); + $this->redirectNow(Url::fromPath(Dashboard::BASE_ROUTE . '/settings')); + })->handleRequest(ServerRequest::fromGlobals()); $this->addContent($paneForm); } @@ -191,15 +218,10 @@ public function newDashletAction() $dashletForm = new DashletForm($this->dashboard); $dashletForm->populate($this->getRequest()->getPost()); $dashletForm->on(DashletForm::ON_SUCCESS, function () { - $this->redirectNow('__CLOSE__'); - })->handleRequest(ServerRequest::fromGlobals()); + $this->getResponse()->setAutoRefreshInterval(1); - $params = $this->getAllParams(); - if ($this->getParam('url')) { - $params['url'] = rawurldecode($this->getParam('url')); - } - - $dashletForm->populate($params); + $this->redirectNow(Url::fromPath(Dashboard::BASE_ROUTE . '/settings')); + })->handleRequest(ServerRequest::fromGlobals()); $this->addContent($dashletForm); } @@ -209,11 +231,13 @@ public function editDashletAction() $this->setTitle(t('Edit Dashlet')); $pane = $this->validateDashletParams(); - $dashlet = $pane->getDashlet($this->getParam('dashlet')); + $dashlet = $pane->getEntry($this->getParam('dashlet')); $dashletForm = (new DashletForm($this->dashboard)) ->on(DashletForm::ON_SUCCESS, function () { - $this->redirectNow('__CLOSE__'); + $this->getResponse()->setAutoRefreshInterval(1); + + $this->redirectNow(Url::fromPath(Dashboard::BASE_ROUTE . '/settings')); }) ->handleRequest(ServerRequest::fromGlobals()); @@ -225,12 +249,14 @@ public function editDashletAction() public function removeDashletAction() { - $this->validateDashletParams(); $this->setTitle(t('Remove Dashlet')); + $this->validateDashletParams(); $removeForm = (new RemoveDashletForm($this->dashboard)) ->on(RemoveDashletForm::ON_SUCCESS, function () { - $this->redirectNow('__CLOSE__'); + $this->getResponse()->setAutoRefreshInterval(1); + + $this->redirectNow(Url::fromPath(Dashboard::BASE_ROUTE . '/settings')); }) ->handleRequest(ServerRequest::fromGlobals()); @@ -243,60 +269,109 @@ public function removeDashletAction() public function reorderWidgetsAction() { $this->assertHttpMethod('post'); - if (! $this->getRequest()->isApiRequest()) { - $this->httpBadRequest('No API request'); + $dashboards = $this->getRequest()->getPost(); + if (! isset($dashboards['dashboardData'])) { + $this->httpBadRequest(t('Invalid request data')); } - if (! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches) - || $matches[1] !== 'application/json') { - $this->httpBadRequest('No JSON content'); + $dashboards = Json::decode($dashboards['dashboardData'], true); + $originals = $dashboards['originals']; + unset($dashboards['Type']); + unset($dashboards['originals']); + + $orgHome = null; + $orgPane = null; + if ($originals && isset($originals['originalHome'])) { + /** @var DashboardHome $orgHome */ + $orgHome = $this->dashboard->getEntry($originals['originalHome']); + $orgHome->setActive()->loadDashboardEntries(); + + if (isset($originals['originalPane'])) { + $orgPane = $orgHome->getEntry($originals['originalPane']); + $orgHome->setEntries([$orgPane->getName() => $orgPane]); + } } - $dashboards = $this->getRequest()->getPost(); - $widgetType = array_pop($dashboards); + $reroutePath = $dashboards['redirectPath']; + unset($dashboards['redirectPath']); - foreach ($dashboards as $key => $panes) { - $home = $widgetType === 'Homes' ? $panes : $key; - if (! $this->dashboard->hasHome($home)) { - $this->httpNotFound(sprintf(t('Dashboard home "%s" not found'), $home)); + foreach ($dashboards as $home => $value) { + if (! $this->dashboard->hasEntry($home)) { + Notification::error(sprintf(t('Dashboard home "%s" not found'), $home)); + break; } - $home = $this->dashboard->getHome($home); - if ($widgetType === 'Homes') { - $home->setPriority($key); - $this->dashboard->manageHome($home); + /** @var DashboardHome $home */ + $home = $this->dashboard->getEntry($home); + if (! is_array($value)) { + $this->dashboard->reorderWidget($home, (int) $value); - continue; + Notification::success(sprintf(t('Updated dashboard home "%s" successfully'), $home->getTitle())); + break; } - $home->setActive(); - $home->loadPanesFromDB(); + $home->setActive()->loadDashboardEntries(); + foreach ($value as $pane => $indexOrValues) { + if (! $home->hasEntry($pane) && (! $orgHome || ! $orgHome->hasEntry($pane))) { + Notification::error(sprintf(t('Dashboard pane "%s" not found'), $pane)); + break; + } + + /** @var Pane $pane */ + $pane = $home->hasEntry($pane) ? $home->getEntry($pane) : $orgHome->getEntry($pane); + if (! is_array($indexOrValues)) { + if ($orgHome && $orgHome->hasEntry($pane->getName()) && $home->hasEntry($pane->getName())) { + Notification::error(sprintf( + t('Dashboard "%s" already exists within "%s" home'), + $pane->getTitle(), + $home->getTitle() + )); + break; + } - foreach ($panes as $innerKey => $value) { - $pane = $widgetType === 'Dashboards' ? $value : $innerKey; - if (! $home->hasPane($pane)) { - $this->httpNotFound(sprintf(t('Dashboard pane "%s" not found'), $pane)); + // Perform DB updates + $home->reorderWidget($pane, (int) $indexOrValues, $orgHome); + if ($orgHome) { + // In order to properly update the dashlets id (user + home + pane + dashlet) + $pane->manageEntry($pane->getEntries()); + } + + Notification::success(sprintf( + t('%s dashboard pane "%s" successfully'), + $orgHome ? 'Moved' : 'Updated', + $pane->getTitle() + )); + break; } - $pane = $home->getPane($pane); - if ($widgetType === 'Dashboards') { - $pane->setPriority($innerKey); - $home->managePanes($pane); - } else { - foreach ($value as $order => $dashlet) { - if (! $pane->hasDashlet($dashlet)) { - $this->httpNotFound(sprintf(t('Dashlet "%s" not found'), $dashlet)); - } - - $dashlet = $pane->getDashlet($dashlet); - $dashlet->setPriority($order); - $pane->manageDashlets($dashlet); + foreach ($indexOrValues as $dashlet => $index) { + if (! $pane->hasEntry($dashlet) && (! $orgPane || ! $orgPane->hasEntry($dashlet))) { + Notification::error(sprintf(t('Dashlet "%s" not found'), $dashlet)); + break; + } + + if ($orgPane && $orgPane->hasEntry($dashlet) && $pane->hasEntry($dashlet)) { + Notification::error(sprintf( + t('Dashlet "%s" already exists within "%s" dashboard pane'), + $dashlet, + $pane->getTitle() + )); + break; } + + $dashlet = $pane->hasEntry($dashlet) ? $pane->getEntry($dashlet) : $orgPane->getEntry($dashlet); + $pane->reorderWidget($dashlet, (int) $index, $orgPane); + + Notification::success(sprintf( + t('%s dashlet "%s" successfully'), + $orgPane ? 'Moved' : 'Updated', + $dashlet->getTitle() + )); } } } - exit; + $this->redirectNow($reroutePath); } /** @@ -320,10 +395,6 @@ public function setupDashboardAction() $setupForm = new SetupNewDashboard($this->dashboard); $setupForm->initDashlets(Dashboard::getModuleDashlets($query)); $setupForm->on(SetupNewDashboard::ON_SUCCESS, function () use ($setupForm) { - if ($setupForm->getPopulatedValue('btn_cancel')) { - $this->redirectNow('__CLOSE__'); - } - $this->redirectNow($setupForm->getRedirectUrl()); })->handleRequest(ServerRequest::fromGlobals()); @@ -333,8 +404,12 @@ public function setupDashboardAction() public function settingsAction() { $this->createTabs(); - // TODO(yh): This may raise an exception when the given tab name doesn't exist. - // But as ipl::Tabs() doesn't offer the possibility to check this beforehand, just ignore it for now!! + $activeHome = $this->dashboard->getActiveHome(); + // We can't grant access the user to the dashboard manager if there aren't any dashboards to manage + if (! $activeHome || (! $activeHome->hasEntries() && count($this->dashboard->getEntries()) === 1)) { + $this->redirectNow(Dashboard::BASE_ROUTE); + } + $this->dashboard->activate('dashboard_settings'); $this->addControl(new ActionLink( @@ -358,7 +433,7 @@ private function createTabs() { $tabs = $this->dashboard->getTabs(); $activeHome = $this->dashboard->getActiveHome(); - if ($activeHome && $activeHome->hasPanes()) { + if (($activeHome && $activeHome->hasEntries()) || count($this->dashboard->getEntries()) > 1) { $tabs->extend(new DashboardSettings()); } @@ -371,16 +446,16 @@ private function validateDashletParams() $pane = $this->params->getRequired('pane'); $dashlet = $this->params->getRequired('dashlet'); - if (! $this->dashboard->hasHome($home)) { + if (! $this->dashboard->hasEntry($home)) { $this->httpNotFound(sprintf(t('Home "%s" not found'), $home)); } - if (! $this->dashboard->getActiveHome()->hasPane($pane)) { + if (! $this->dashboard->getActiveHome()->hasEntry($pane)) { $this->httpNotFound(sprintf(t('Pane "%s" not found'), $pane)); } - $pane = $this->dashboard->getActiveHome()->getPane($pane); - if (! $pane->hasDashlet($dashlet)) { + $pane = $this->dashboard->getActiveHome()->getEntry($pane); + if (! $pane->hasEntry($dashlet)) { $this->httpNotFound(sprintf(t('Dashlet "%s" not found'), $dashlet)); } diff --git a/application/forms/Dashboard/BaseDashboardForm.php b/application/forms/Dashboard/BaseDashboardForm.php new file mode 100644 index 0000000000..5490caade9 --- /dev/null +++ b/application/forms/Dashboard/BaseDashboardForm.php @@ -0,0 +1,113 @@ +dashboard = $dashboard; + + // This is needed for the modal views + $this->setAction((string) Url::fromRequest()); + } + + public function hasBeenSubmitted() + { + // We don't use addElement() for the form controls, so the form has no way of knowing + // that we do have a submit button and will always be submitted with autosubmit elements + return $this->hasBeenSent() && $this->getPopulatedValue('submit'); + } + + /** + * Populate form data from config + * + * @param BaseDashboard $dashboard + * + * @return void + */ + public function load(BaseDashboard $dashboard) + { + } + + /** + * Create custom form controls + * + * @return HtmlElement + */ + protected function createFormControls() + { + return HtmlElement::create('div', ['class' => 'control-group form-controls']); + } + + /** + * Create a cancel button + * + * @return FormElement + */ + protected function createCancelButton() + { + return $this->createElement('submitButton', 'btn_cancel', ['class' => 'modal-cancel', 'label' => t('Cancel')]); + } + + /** + * Create a remove button + * + * @param Url $action + * @param string $label + * + * @return FormElement + */ + protected function createRemoveButton(Url $action, $label) + { + return $this->createElement('submitButton', 'btn_remove', [ + 'class' => 'remove-button', + 'label' => [new Icon('trash'), $label], + 'formaction' => (string) $action + ]); + } + + /** + * Create and register a submit button + * + * @param string $label + * + * @return FormElement + */ + protected function registerSubmitButton($label) + { + $submitElement = $this->createElement('submit', 'submit', ['class' => 'btn-primary', 'label' => $label]); + $this->registerElement($submitElement); + + return $submitElement; + } +} diff --git a/application/forms/Dashboard/DashletForm.php b/application/forms/Dashboard/DashletForm.php index 53b5abb8d9..5d20634b38 100644 --- a/application/forms/Dashboard/DashletForm.php +++ b/application/forms/Dashboard/DashletForm.php @@ -6,136 +6,93 @@ use Exception; use Icinga\Application\Logger; +use Icinga\Web\Dashboard\Common\BaseDashboard; use Icinga\Web\Dashboard\Dashboard; -use Icinga\Web\Navigation\DashboardHome; +use Icinga\Web\Dashboard\DashboardHome; use Icinga\Web\Notification; use Icinga\Web\Dashboard\Dashlet; use Icinga\Web\Dashboard\Pane; use ipl\Html\HtmlElement; -use ipl\Web\Compat\CompatForm; use ipl\Web\Url; -/** - * Form to add an url a dashboard pane - */ -class DashletForm extends CompatForm +class DashletForm extends BaseDashboardForm { - /** - * @var Dashboard - */ - private $dashboard; - - public function __construct(Dashboard $dashboard) - { - $this->dashboard = $dashboard; - - $this->setAction((string) Url::fromRequest()); - } - - public function hasBeenSubmitted() - { - return $this->hasBeenSent() && $this->getPopulatedValue('submit'); - } - protected function assemble() { $requestUrl = Url::fromRequest(); - $homes = $this->dashboard->getHomeKeyTitleArr(); + $homes = $this->dashboard->getEntryKeyTitleArr(); $activeHome = $this->dashboard->getActiveHome(); $currentHome = $requestUrl->getParam('home', reset($homes)); $populatedHome = $this->getPopulatedValue('home', $currentHome); $panes = []; - if ($currentHome === $populatedHome && $this->getPopulatedValue('create_new_home') !== 'y') { + if ($currentHome === $populatedHome && $populatedHome !== self::CREATE_NEW_HOME) { if (! $currentHome || ! $activeHome) { // Home param isn't passed through, so let's try to load based on the first home - $firstHome = $this->dashboard->rewindHomes(); + $firstHome = $this->dashboard->rewindEntries(); if ($firstHome) { - $this->dashboard->loadDashboards($firstHome->getName()); + $this->dashboard->loadDashboardEntries($firstHome->getName()); - $panes = $firstHome->getPaneKeyTitleArr(); + $panes = $firstHome->getEntryKeyTitleArr(); } } else { - $panes = $activeHome->getPaneKeyTitleArr(); + $panes = $activeHome->getEntryKeyTitleArr(); } - } elseif ($this->dashboard->hasHome($populatedHome)) { - $this->dashboard->loadDashboards($populatedHome); + } elseif ($this->dashboard->hasEntry($populatedHome)) { + $this->dashboard->loadDashboardEntries($populatedHome); - $panes = $this->dashboard->getActiveHome()->getPaneKeyTitleArr(); + $panes = $this->dashboard->getActiveHome()->getEntryKeyTitleArr(); } $this->addElement('hidden', 'org_pane', ['required' => false]); $this->addElement('hidden', 'org_home', ['required' => false]); $this->addElement('hidden', 'org_dashlet', ['required' => false]); - $this->addElement('checkbox', 'create_new_home', [ - 'class' => 'autosubmit', - 'required' => false, - 'disabled' => empty($homes) ?: null, - 'label' => t('New Dashboard Home'), - 'description' => t('Check this box if you want to add the dashboard to a new dashboard home.'), + $this->addElement('select', 'home', [ + 'class' => 'autosubmit', + 'required' => true, + 'disabled' => empty($homes) ?: null, + 'value' => $populatedHome, + 'multiOptions' => array_merge([self::CREATE_NEW_HOME => self::CREATE_NEW_HOME], $homes), + 'label' => t('Select Home'), + 'descriptions' => t('Select a dashboard home you want to add the dashboard pane to.') ]); - if (empty($homes) || $this->getPopulatedValue('create_new_home') === 'y') { - // $el->attrs->set() has no effect here anymore, so we need to register a proper callback - $this->getElement('create_new_home') - ->getAttributes() - ->registerAttributeCallback('checked', function () { - return true; - }); - - $this->addElement('text', 'home', [ + if (empty($homes) || $populatedHome === self::CREATE_NEW_HOME) { + $this->addElement('text', 'new_home', [ 'required' => true, - 'label' => t('Dashboard Home'), + 'label' => t('Home Title'), + 'placeholder' => t('Enter dashboard home title'), 'description' => t('Enter a title for the new dashboard home.') ]); - } else { - $this->addElement('select', 'home', [ - 'required' => true, - 'class' => 'autosubmit', - 'value' => $currentHome, - 'multiOptions' => $homes, - 'label' => t('Dashboard Home'), - 'descriptions' => t('Select a home you want to add the dashboard pane to.') - ]); } - $disable = empty($panes) || $this->getPopulatedValue('create_new_home') === 'y'; - $this->addElement('checkbox', 'create_new_pane', [ - 'required' => false, - 'class' => 'autosubmit', - 'disabled' => $disable ?: null, - 'label' => t('New Dashboard'), - 'description' => t('Check this box if you want to add the dashlet to a new dashboard.'), - ]); - + $populatedPane = $this->getPopulatedValue('pane'); // Pane element's values are depending on the home element's value - if (! in_array($this->getPopulatedValue('pane'), $panes)) { + if ($populatedPane !== self::CREATE_NEW_PANE && ! in_array($populatedPane, $panes)) { $this->clearPopulatedValue('pane'); } - if ($disable || $this->getValue('create_new_pane') === 'y') { - // $el->attrs->set() has no effect here anymore, so we need to register a proper callback - $this->getElement('create_new_pane') - ->getAttributes() - ->registerAttributeCallback('checked', function () { - return true; - }); + $populatedPane = $this->getPopulatedValue('pane', reset($panes)); + $disable = empty($panes) || $populatedHome === self::CREATE_NEW_HOME; + $this->addElement('select', 'pane', [ + 'class' => 'autosubmit', + 'required' => true, + 'disabled' => $disable ?: null, + 'value' => ! $disable ? $populatedPane : self::CREATE_NEW_PANE, // Cheat the browser complains + 'multiOptions' => array_merge([self::CREATE_NEW_PANE => self::CREATE_NEW_PANE], $panes), + 'label' => t('Select Dashboard'), + 'description' => t('Select a dashboard you want to add the dashlet to.'), + ]); - $this->addElement('text', 'pane', [ + if ($disable || $this->getPopulatedValue('pane') === self::CREATE_NEW_PANE) { + $this->addElement('text', 'new_pane', [ 'required' => true, - 'label' => t('New Dashboard Title'), + 'label' => t('Dashboard Title'), + 'placeholder' => t('Enter dashboard title'), 'description' => t('Enter a title for the new dashboard.'), ]); - } else { - $this->addElement('select', 'pane', [ - 'required' => true, - 'value' => reset($panes), - 'multiOptions' => $panes, - 'label' => t('Dashboard'), - 'description' => t('Select a dashboard you want to add the dashlet to.'), - ]); } $this->addHtml(new HtmlElement('hr')); @@ -143,6 +100,7 @@ protected function assemble() $this->addElement('textarea', 'url', [ 'required' => true, 'label' => t('Url'), + 'placeholder' => t('Enter dashlet url'), 'description' => t( 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters.' ), @@ -151,40 +109,24 @@ protected function assemble() $this->addElement('text', 'dashlet', [ 'required' => true, 'label' => t('Dashlet Title'), + 'placeholder' => t('Enter a dashlet title'), 'description' => t('Enter a title for the dashlet.'), ]); - $url = (string) Url::fromPath(Dashboard::BASE_ROUTE . '/browse'); - - $element = $this->createElement('submit', 'submit', ['label' => t('Add to Dashboard')]); - $this->registerElement($element)->decorate($element); + $removeButton = null; + if ($requestUrl->getPath() === Dashboard::BASE_ROUTE . '/edit-dashlet') { + $targetUrl = (clone $requestUrl)->setPath(Dashboard::BASE_ROUTE . '/remove-dashlet'); + $removeButton = $this->createRemoveButton($targetUrl, t('Remove Dashlet')); + } - // We might need this later to allow the user to browse dashlets when creating a dashlet - $this->addElement('submit', 'btn_browse', [ - 'label' => t('Browse Dashlets'), - 'href' => $url, - 'formaction' => $url, + $formControls = $this->createFormControls(); + $formControls->add([ + $this->registerSubmitButton(t('Add to Dashboard')), + $removeButton, + $this->createCancelButton() ]); - $this->getElement('btn_browse')->setWrapper($element->getWrapper()); - } - - /** - * Populate form data from config - * - * @param Dashlet $dashlet - */ - public function load(Dashlet $dashlet) - { - $home = Url::fromRequest()->getParam('home'); - $this->populate(array( - 'org_home' => $home, - 'org_pane' => $dashlet->getPane()->getName(), - 'pane' => $dashlet->getPane()->getTitle(), - 'org_dashlet' => $dashlet->getName(), - 'dashlet' => $dashlet->getTitle(), - 'url' => $dashlet->getUrl()->getRelativeUrl() - )); + $this->addHtml($formControls); } protected function onSuccess() @@ -192,27 +134,38 @@ protected function onSuccess() $conn = Dashboard::getConn(); $dashboard = $this->dashboard; + $selectedHome = $this->getPopulatedValue('home'); + if (! $selectedHome || $selectedHome === self::CREATE_NEW_HOME) { + $selectedHome = $this->getPopulatedValue('new_home'); + } + + $selectedPane = $this->getPopulatedValue('pane'); + // If "pane" element is disabled, there will be no populated value for it + if (! $selectedPane || $selectedPane === self::CREATE_NEW_PANE) { + $selectedPane = $this->getPopulatedValue('new_pane'); + } + if (Url::fromRequest()->getPath() === Dashboard::BASE_ROUTE . '/new-dashlet') { - $home = new DashboardHome($this->getValue('home')); - if ($dashboard->hasHome($home->getName())) { - $home = $dashboard->getHome($home->getName()); - if ($home->getName() !== $dashboard->getActiveHome()->getName()) { - $home->setActive(); - $home->loadPanesFromDB(); + $currentHome = new DashboardHome($selectedHome); + if ($dashboard->hasEntry($currentHome->getName())) { + $currentHome = clone $dashboard->getEntry($currentHome->getName()); + if ($currentHome->getName() !== $dashboard->getActiveHome()->getName()) { + $currentHome->setActive(); + $currentHome->loadDashboardEntries(); } } - $pane = new Pane($this->getValue('pane')); - if ($home->hasPane($pane->getName())) { - $pane = $home->getPane($pane->getName()); + $currentPane = new Pane($selectedPane); + if ($currentHome->hasEntry($currentPane->getName())) { + $currentPane = clone $currentHome->getEntry($currentPane->getName()); } - $dashlet = new Dashlet($this->getValue('dashlet'), $this->getValue('url'), $pane); - if ($pane->hasDashlet($dashlet->getName())) { + $dashlet = new Dashlet($this->getValue('dashlet'), $this->getValue('url'), $currentPane); + if ($currentPane->hasEntry($dashlet->getName())) { Notification::error(sprintf( t('Dashlet "%s" already exists within the "%s" dashboard pane'), $dashlet->getTitle(), - $pane->getTitle() + $currentPane->getTitle() )); return; @@ -221,12 +174,12 @@ protected function onSuccess() $conn->beginTransaction(); try { - $dashboard->manageHome($home); - $home->managePanes($pane); - $pane->manageDashlets($dashlet); + $dashboard->manageEntry($currentHome); + $currentHome->manageEntry($currentPane); + $currentPane->manageEntry($dashlet); $conn->commitTransaction(); - } catch (Exception $err) { // This error handling is just for debugging purpose! Will be removed! + } catch (Exception $err) { Logger::error($err); $conn->rollBackTransaction(); @@ -235,26 +188,35 @@ protected function onSuccess() Notification::success(sprintf(t('Created dashlet "%s" successfully'), $dashlet->getTitle())); } else { - $orgHome = $dashboard->getHome($this->getValue('org_home')); - $orgPane = $orgHome->getPane($this->getValue('org_pane')); - $orgDashlet = $orgPane->getDashlet($this->getValue('org_dashlet')); + $orgHome = $dashboard->getEntry($this->getValue('org_home')); + $orgPane = $orgHome->getEntry($this->getValue('org_pane')); + $orgDashlet = $orgPane->getEntry($this->getValue('org_dashlet')); - $currentHome = new DashboardHome($this->getValue('home')); - if ($dashboard->hasHome($currentHome->getName())) { - $currentHome = $dashboard->getHome($currentHome->getName()); + $currentHome = new DashboardHome($selectedHome); + if ($dashboard->hasEntry($currentHome->getName())) { + $currentHome = clone $dashboard->getEntry($currentHome->getName()); $activeHome = $dashboard->getActiveHome(); if ($currentHome->getName() !== $activeHome->getName()) { $currentHome->setActive(); - $currentHome->loadPanesFromDB(); + $currentHome->loadDashboardEntries(); } } - $currentPane = new Pane($this->getValue('pane')); - if ($currentHome->hasPane($currentPane->getName())) { - $currentPane = $currentHome->getPane($currentPane->getName()); + $currentPane = new Pane($selectedPane); + if ($currentHome->hasEntry($currentPane->getName())) { + $currentPane = clone $currentHome->getEntry($currentPane->getName()); } $currentPane->setHome($currentHome); + // When the user wishes to create a new dashboard pane, we have to explicitly reset the dashboard panes + // of the original home, so that it isn't considered as we want to move the pane even though it isn't + // supposed to when the original home contains a dashboard with the same name + // @see DashboardHome::managePanes() for details + $selectedPane = $this->getPopulatedValue('pane'); + if ((! $selectedPane || $selectedPane === self::CREATE_NEW_PANE) + && ! $currentHome->hasEntry($currentPane->getName())) { + $orgHome->setEntries([]); + } $currentDashlet = clone $orgDashlet; $currentDashlet @@ -263,7 +225,7 @@ protected function onSuccess() ->setTitle($this->getValue('dashlet')); if ($orgPane->getName() !== $currentPane->getName() - && $currentPane->hasDashlet($currentDashlet->getName())) { + && $currentPane->hasEntry($currentDashlet->getName())) { Notification::error(sprintf( t('Failed to move dashlet "%s": Dashlet already exists within the "%s" dashboard pane'), $currentDashlet->getTitle(), @@ -287,12 +249,18 @@ function ($val) { return; } + if (empty($paneDiff)) { + // No dashboard diff means the dashlet is still in the same pane, so just + // reset the dashlets of the original pane + $orgPane->setEntries([]); + } + $conn->beginTransaction(); try { - $dashboard->manageHome($currentHome); - $currentHome->managePanes($currentPane, $orgHome); - $currentPane->manageDashlets($currentDashlet, $orgPane); + $dashboard->manageEntry($currentHome); + $currentHome->manageEntry($currentPane, $orgHome); + $currentPane->manageEntry($currentDashlet, $orgPane); $conn->commitTransaction(); } catch (Exception $err) { @@ -305,4 +273,21 @@ function ($val) { Notification::success(sprintf(t('Updated dashlet "%s" successfully'), $currentDashlet->getTitle())); } } + + public function load(BaseDashboard $dashlet) + { + $home = Url::fromRequest()->getParam('home'); + /** @var Dashlet $dashlet */ + $this->populate(array( + 'org_home' => $home, + 'org_pane' => $dashlet->getPane()->getName(), + 'org_dashlet' => $dashlet->getName(), + 'dashlet' => $dashlet->getTitle(), + 'url' => $dashlet->getUrl()->getRelativeUrl() + )); + + if ($this->getPopulatedValue('pane') !== self::CREATE_NEW_PANE) { + $this->populate(['pane' => $dashlet->getPane()->getTitle()]); + } + } } diff --git a/application/forms/Dashboard/HomePaneForm.php b/application/forms/Dashboard/HomePaneForm.php index 84f1d5db35..088f737fb8 100644 --- a/application/forms/Dashboard/HomePaneForm.php +++ b/application/forms/Dashboard/HomePaneForm.php @@ -5,81 +5,53 @@ namespace Icinga\Forms\Dashboard; use Icinga\Application\Logger; -use Icinga\Web\Navigation\DashboardHome; +use Icinga\Web\Dashboard\Common\BaseDashboard; +use Icinga\Web\Dashboard\DashboardHome; use Icinga\Web\Notification; use Icinga\Web\Dashboard\Dashboard; -use Icinga\Web\Dashboard\Pane; -use ipl\Web\Compat\CompatForm; use ipl\Web\Url; -class HomePaneForm extends CompatForm +class HomePaneForm extends BaseDashboardForm { - /** @var Dashboard */ - protected $dashboard; - - public function __construct(Dashboard $dashboard) + protected function assemble() { - $this->dashboard = $dashboard; - - // We need to set this explicitly needed for modals - $this->setAction((string) Url::fromRequest()); - } + $titleDesc = t('Edit the title of this dashboard home'); + $buttonLabel = t('Update Home'); + $removeButtonLabel = t('Remove Home'); - /** - * Populate form data from config - * - * @param DashboardHome|Pane $widget - */ - public function load($widget) - { - $title = $widget instanceof Pane ? $widget->getTitle() : $widget->getLabel(); - $this->populate([ - 'org_title' => $title, - 'title' => $title, - 'org_name' => $widget->getName() - ]); - } + $requestUrl = Url::fromRequest(); + $removeTargetUrl = (clone $requestUrl)->setPath(Dashboard::BASE_ROUTE . '/remove-home'); - protected function assemble() - { $this->addElement('hidden', 'org_name', ['required' => false]); $this->addElement('hidden', 'org_title', ['required' => false]); - $titleDesc = t('Edit the title of this dashboard home'); - $buttonLabel = t('Update Home'); - if (Url::fromRequest()->getPath() === Dashboard::BASE_ROUTE . '/edit-pane') { + if ($requestUrl->getPath() === Dashboard::BASE_ROUTE . '/edit-pane') { $titleDesc = t('Edit the title of this dashboard pane'); $buttonLabel = t('Update Pane'); + $removeButtonLabel = t('Remove Pane'); - $homes = $this->dashboard->getHomeKeyTitleArr(); - $this->addElement('checkbox', 'create_new_home', [ - 'required' => false, - 'class' => 'autosubmit', - 'disabled' => empty($homes) ?: null, - 'label' => t('New Dashboard Home'), - 'description' => t('Check this box if you want to move the pane to a new dashboard home.'), - ]); + $removeTargetUrl = (clone $requestUrl)->setPath(Dashboard::BASE_ROUTE . '/remove-pane'); + $homes = $this->dashboard->getEntryKeyTitleArr(); $activeHome = $this->dashboard->getActiveHome(); $populatedHome = $this->getPopulatedValue('home', $activeHome->getName()); - if (empty($homes) || $this->getPopulatedValue('create_new_home') === 'y') { - $this->getElement('create_new_home')->addAttributes(['checked' => 'checked']); + $this->addElement('select', 'home', [ + 'class' => 'autosubmit', + 'required' => true, + 'value' => $populatedHome, + 'multiOptions' => array_merge([self::CREATE_NEW_HOME => self::CREATE_NEW_HOME], $homes), + 'label' => t('Assign to Home'), + 'description' => t('Select a dashboard home you want to move the dashboard to.'), + ]); - $this->addElement('text', 'home', [ + if (empty($homes) || $this->getPopulatedValue('home') === self::CREATE_NEW_HOME) { + $this->addElement('text', 'new_home', [ 'required' => true, 'label' => t('Dashboard Home'), + 'placeholder' => t('Enter dashboard home title'), 'description' => t('Enter a title for the new dashboard home.'), ]); - } else { - $this->addElement('select', 'home', [ - 'required' => true, - 'class' => 'autosubmit', - 'value' => $populatedHome, - 'multiOptions' => $homes, - 'label' => t('Move to Home'), - 'description' => t('Select a dashboard home you want to move the dashboard to.'), - ]); } } @@ -89,46 +61,64 @@ protected function assemble() 'description' => $titleDesc ]); - $this->addElement('submit', 'btn_update', ['label' => $buttonLabel]); + $formControls = $this->createFormControls(); + $formControls->add([ + $this->registerSubmitButton($buttonLabel), + $this->createRemoveButton($removeTargetUrl, $removeButtonLabel), + $this->createCancelButton() + ]); + + $this->addHtml($formControls); } protected function onSuccess() { $requestUrl = Url::fromRequest(); if ($requestUrl->getPath() === Dashboard::BASE_ROUTE . '/edit-pane') { - $orgHome = $this->dashboard->getHome($requestUrl->getParam('home')); + $orgHome = $this->dashboard->getEntry($requestUrl->getParam('home')); + + $selectedHome = $this->getPopulatedValue('home'); + if (! $selectedHome || $selectedHome === self::CREATE_NEW_HOME) { + $selectedHome = $this->getPopulatedValue('new_home'); + } - $currentHome = new DashboardHome($this->getValue('home')); - if ($this->dashboard->hasHome($currentHome->getName())) { - $currentHome = $this->dashboard->getHome($currentHome->getName()); + $currentHome = new DashboardHome($selectedHome); + if ($this->dashboard->hasEntry($currentHome->getName())) { + /** @var DashboardHome $currentHome */ + $currentHome = clone $this->dashboard->getEntry($currentHome->getName()); $activeHome = $this->dashboard->getActiveHome(); if ($currentHome->getName() !== $activeHome->getName()) { - $currentHome->setActive(); - $currentHome->loadPanesFromDB(); + $currentHome->setActive()->loadDashboardEntries(); } } - $currentPane = $orgHome->getPane($this->getValue('org_name')); + $currentPane = clone $orgHome->getEntry($this->getValue('org_name')); $currentPane ->setHome($currentHome) ->setTitle($this->getValue('title')); - if ($orgHome->getName() !== $currentHome->getName() && $currentHome->hasPane($currentPane->getName())) { + if ($orgHome->getName() !== $currentHome->getName() && $currentHome->hasEntry($currentPane->getName())) { Notification::error(sprintf( t('Failed to move dashboard "%s": Dashbaord pane already exists within the "%s" dashboard home'), $currentPane->getTitle(), - $currentHome->getLabel() + $currentHome->getTitle() )); return; } + if ($currentHome->getName() === $orgHome->getName()) { + // There is no dashboard home diff so clear all the dashboard pane + // of the org home + $orgHome->setEntries([]); + } + $conn = Dashboard::getConn(); $conn->beginTransaction(); try { - $this->dashboard->manageHome($currentHome); - $currentHome->managePanes($currentPane, $orgHome); + $this->dashboard->manageEntry($currentHome); + $currentHome->manageEntry($currentPane, $orgHome); $conn->commitTransaction(); } catch (\Exception $err) { @@ -139,10 +129,19 @@ protected function onSuccess() Notification::success(sprintf(t('Updated dashboard pane "%s" successfully'), $currentPane->getTitle())); } else { $home = $this->dashboard->getActiveHome(); - $home->setLabel($this->getValue('title')); + $home->setTitle($this->getValue('title')); - $this->dashboard->manageHome($home); - Notification::success(sprintf(t('Updated dashboard home "%s" successfully'), $home->getLabel())); + $this->dashboard->manageEntry($home); + Notification::success(sprintf(t('Updated dashboard home "%s" successfully'), $home->getTitle())); } } + + public function load(BaseDashboard $dashboard) + { + $this->populate([ + 'org_title' => $dashboard->getTitle(), + 'title' => $dashboard->getTitle(), + 'org_name' => $dashboard->getName() + ]); + } } diff --git a/application/forms/Dashboard/NewHomePaneForm.php b/application/forms/Dashboard/NewHomePaneForm.php index 5066fdd9f9..7be3834c6e 100644 --- a/application/forms/Dashboard/NewHomePaneForm.php +++ b/application/forms/Dashboard/NewHomePaneForm.php @@ -3,80 +3,92 @@ namespace Icinga\Forms\Dashboard; use Icinga\Web\Dashboard\Dashboard; +use Icinga\Web\Dashboard\DashboardHome; use Icinga\Web\Dashboard\Pane; use Icinga\Web\Notification; -use ipl\Web\Compat\CompatForm; use ipl\Web\Url; -class NewHomePaneForm extends CompatForm +class NewHomePaneForm extends BaseDashboardForm { - /** @var Dashboard */ - protected $dashboard; - public function __construct(Dashboard $dashboard) { - $this->dashboard = $dashboard; + parent::__construct($dashboard); $requestUrl = Url::fromRequest(); - - // We need to set this explicitly needed for modals - $this->setAction((string) $requestUrl); - if ($requestUrl->hasParam('home')) { $this->populate(['home' => $requestUrl->getParam('home')]); } } - public function hasBeenSubmitted() - { - return $this->hasBeenSent() - && ($this->getPopulatedValue('btn_cancel') - || $this->getPopulatedValue('submit')); - } - protected function assemble() { $populatedHome = Url::fromRequest()->getParam('home'); $this->addElement('text', 'pane', [ 'required' => true, 'label' => t('Title'), + 'placeholder' => t('Create new Dashboard'), 'description' => t('Add new dashboard to this home.') ]); + $homes = array_merge([self::CREATE_NEW_HOME => self::CREATE_NEW_HOME], $this->dashboard->getEntryKeyTitleArr()); $this->addElement('select', 'home', [ 'required' => true, 'class' => 'autosubmit', 'value' => $populatedHome, - 'multiOptions' => $this->dashboard->getHomeKeyTitleArr(), + 'multiOptions' => $homes, 'label' => t('Assign to Home'), 'description' => t('A dashboard home you want to assign the new dashboard to.'), ]); + if ($this->getPopulatedValue('home') === self::CREATE_NEW_HOME) { + $this->addElement('text', 'new_home', [ + 'required' => true, + 'label' => t('Dashboard Home'), + 'placeholder' => t('Enter dashboard home title'), + 'description' => t('Enter a title for the new dashboard home.'), + ]); + } + $submitButton = $this->createElement('submit', 'submit', [ - 'class' => 'autosubmit', - 'label' => t('Add Dashboard'), + 'class' => 'btn-primary', + 'label' => t('Add Dashboard') + ]); + $this->registerElement($submitButton); + + $formControls = $this->createFormControls(); + $formControls->add([ + $this->registerSubmitButton(t('Add Dashboard')), + $this->createCancelButton() ]); - $this->registerElement($submitButton)->decorate($submitButton); - $this->addElement('submit', 'btn_cancel', ['label' => t('Cancel')]); - $this->getElement('btn_cancel')->setWrapper($submitButton->getWrapper()); + $this->addHtml($formControls); } protected function onSuccess() { $requestUrl = Url::fromRequest(); - $dashboard = $this->dashboard; $conn = Dashboard::getConn(); + $selectedHome = $this->getPopulatedValue('home'); + if (! $selectedHome || $selectedHome === self::CREATE_NEW_HOME) { + $selectedHome = $this->getPopulatedValue('new_home'); + } + if ($requestUrl->getPath() === Dashboard::BASE_ROUTE . '/new-pane') { - $home = $this->getPopulatedValue('home'); - $home = $dashboard->getHome($home); + $currentHome = new DashboardHome($selectedHome); + if ($this->dashboard->hasEntry($currentHome->getName())) { + $currentHome = clone $this->dashboard->getEntry($currentHome->getName()); + if ($currentHome->getName() !== $this->dashboard->getActiveHome()->getName()) { + $currentHome->setActive()->loadDashboardEntries(); + } + } + $pane = new Pane($this->getPopulatedValue('pane')); $conn->beginTransaction(); try { - $pane = new Pane($this->getPopulatedValue('pane')); - $home->managePanes($pane); + $this->dashboard->manageEntry($currentHome); + $currentHome->manageEntry($pane); $conn->commitTransaction(); } catch (\Exception $err) { @@ -85,8 +97,6 @@ protected function onSuccess() } Notification::success('Added dashboard successfully'); - } else { - } } } diff --git a/application/forms/Dashboard/RemoveDashletForm.php b/application/forms/Dashboard/RemoveDashletForm.php index a5dbd2b252..3aad587166 100644 --- a/application/forms/Dashboard/RemoveDashletForm.php +++ b/application/forms/Dashboard/RemoveDashletForm.php @@ -5,21 +5,14 @@ namespace Icinga\Forms\Dashboard; use Icinga\Web\Notification; -use Icinga\Web\Dashboard\Dashboard; use ipl\Html\HtmlElement; -use ipl\Web\Compat\CompatForm; use ipl\Web\Url; -class RemoveDashletForm extends CompatForm +class RemoveDashletForm extends BaseDashboardForm { - /** @var Dashboard */ - protected $dashboard; - - public function __construct(Dashboard $dashboard) + public function hasBeenSubmitted() { - $this->dashboard = $dashboard; - - $this->setAction((string) Url::fromRequest()); + return $this->hasBeenSent() && $this->getPopulatedValue('btn_remove'); } protected function assemble() @@ -29,17 +22,20 @@ protected function assemble() Url::fromRequest()->getParam('dashlet') ))); - $this->addElement('submit', 'remove_dashlet', ['label' => t('Remove Dashlet')]); + $submit = $this->registerSubmitButton(t('Remove Dashlet')); + $submit->setName('btn_remove'); + + $this->addHtml($submit); } protected function onSuccess() { $requestUrl = Url::fromRequest(); $home = $this->dashboard->getActiveHome(); - $pane = $home->getPane($requestUrl->getParam('pane')); + $pane = $home->getEntry($requestUrl->getParam('pane')); $dashlet = $requestUrl->getParam('dashlet'); - $pane->removeDashlet($dashlet); + $pane->removeEntry($dashlet); Notification::success(sprintf(t('Removed dashlet "%s" successfully'), $dashlet)); } diff --git a/application/forms/Dashboard/RemoveHomePaneForm.php b/application/forms/Dashboard/RemoveHomePaneForm.php index c29818f1c5..8116da81bc 100644 --- a/application/forms/Dashboard/RemoveHomePaneForm.php +++ b/application/forms/Dashboard/RemoveHomePaneForm.php @@ -6,24 +6,32 @@ use Icinga\Web\Notification; use Icinga\Web\Dashboard\Dashboard; -use ipl\Web\Compat\CompatForm; +use ipl\Html\HtmlElement; use ipl\Web\Url; -class RemoveHomePaneForm extends CompatForm +class RemoveHomePaneForm extends BaseDashboardForm { - /** @var Dashboard */ - protected $dashboard; - - public function __construct(Dashboard $dashboard) + public function hasBeenSubmitted() { - $this->dashboard = $dashboard; - - $this->setAction((string) Url::fromRequest()); + return $this->hasBeenSent() && $this->getPopulatedValue('btn_remove'); } protected function assemble() { - $this->addElement('submit', 'btn_remove', ['label' => t('Remove Home')]); + $requestRoute = Url::fromRequest(); + $label = t('Remove Home'); + $message = sprintf(t('Please confirm removal of dashboard home "%s"'), $requestRoute->getParam('home')); + if ($requestRoute->getPath() === Dashboard::BASE_ROUTE . '/remove-pane') { + $label = t('Remove Pane'); + $message = sprintf(t('Please confirm removal of dashboard pane "%s"'), $requestRoute->getParam('pane')); + } + + $this->addHtml(HtmlElement::create('h1', null, $message)); + + $submit = $this->registerSubmitButton($label); + $submit->setName('btn_remove'); + + $this->addHtml($submit); } protected function onSuccess() @@ -32,12 +40,12 @@ protected function onSuccess() $home = $this->dashboard->getActiveHome(); if ($requestUrl->getPath() === Dashboard::BASE_ROUTE . '/remove-home') { - $this->dashboard->removeHome($home); + $this->dashboard->removeEntry($home); - Notification::success(sprintf(t('Removed dashboard home "%s" successfully'), $home->getLabel())); + Notification::success(sprintf(t('Removed dashboard home "%s" successfully'), $home->getTitle())); } else { - $pane = $home->getPane($requestUrl->getParam('pane')); - $home->removePane($pane); + $pane = $home->getEntry($requestUrl->getParam('pane')); + $home->removeEntry($pane); Notification::success(sprintf(t('Removed dashboard pane "%s" successfully'), $pane->getTitle())); } diff --git a/application/forms/Dashboard/WelcomeForm.php b/application/forms/Dashboard/WelcomeForm.php index 41673d8238..d0278dec65 100644 --- a/application/forms/Dashboard/WelcomeForm.php +++ b/application/forms/Dashboard/WelcomeForm.php @@ -5,7 +5,7 @@ namespace Icinga\Forms\Dashboard; use Icinga\Web\Dashboard\Dashboard; -use Icinga\Web\Navigation\DashboardHome; +use Icinga\Web\Dashboard\DashboardHome; use ipl\Web\Compat\CompatForm; use ipl\Web\Url; @@ -43,12 +43,12 @@ protected function assemble() protected function onSuccess() { if ($this->getPopulatedValue('btn_use_defaults')) { - $home = $this->dashboard->getHome(DashboardHome::DEFAULT_HOME); + $home = $this->dashboard->getEntry(DashboardHome::DEFAULT_HOME); $conn = Dashboard::getConn(); $conn->beginTransaction(); try { - $home->managePanes($this->dashboard->getSystemDefaults(), null, true); + $home->manageEntry($this->dashboard->getSystemDefaults(), null, true); $conn->commitTransaction(); } catch (\Exception $err) { diff --git a/application/views/scripts/layout/menu.phtml b/application/views/scripts/layout/menu.phtml index 6aa04e1b0a..5ebdaf623c 100644 --- a/application/views/scripts/layout/menu.phtml +++ b/application/views/scripts/layout/menu.phtml @@ -5,7 +5,7 @@ use Icinga\Web\Widget\SearchDashboard; $searchDashboard = new SearchDashboard(); $searchDashboard->setUser($this->Auth()->getUser()); -if ($searchDashboard->search('dummy')->getActiveHome()->getPane('search')->hasDashlets()): ?> +if (! $searchDashboard->search('dummy')->getActiveHome()->getEntry('search')->hasEntries()): ?> name = $name; + $this->title = $name; + + if (! empty($properties)) { + $this->fromArray($properties); + } + } + + /** + * Set this widget's unique identifier + * + * @param int|string $uuid + * + * @return $this + */ + public function setUuid($uuid) + { + $this->uuid = $uuid; + + return $this; + } + + /** + * Get this widget's unique identifier + * + * @return string + */ + public function getUuid() + { + return $this->uuid; + } + + /** + * Set the name of this widget + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Returns the name of this widget + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the title of this widget + * + * @param string $title + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * Returns the title of this widget + * + * @return string + */ + public function getTitle() + { + return $this->title !== null ? $this->title : $this->getName(); + } + + /** + * Set the owner of this widget + * + * @param string $owner + * + * @return $this + */ + public function setOwner($owner) + { + $this->owner = $owner; + + return $this; + } + + /** + * Get owner of this widget + * + * @return string + */ + public function getOwner() + { + return $this->owner; + } + + /** + * Get the widget's description + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set the widget's description + * + * @param string $description + * + * @return $this + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Set the priority order of this widget + * + * @param int $order + * + * @return $this + */ + public function setPriority(int $order) + { + $this->order = $order; + + return $this; + } + + /** + * Get the priority order of this widget + * + * @return int + */ + public function getPriority() + { + return $this->order; + } + + public function hasEntries() + { + } + + public function getEntry($name) + { + } + + public function hasEntry($name) + { + } + + public function getEntries() + { + } + + public function setEntries(array $entries) + { + } + + public function addEntry(BaseDashboard $dashboard) + { + } + + public function createEntry($name, $url = null) + { + } + + public function getEntryKeyTitleArr() + { + } + + public function removeEntry($entry) + { + } + + public function removeEntries(array $entries = []) + { + } + + public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) + { + } + + public function loadDashboardEntries($name = '') + { + } + + public function rewindEntries() + { + } +} diff --git a/library/Icinga/Web/Dashboard/Common/DashboardControls.php b/library/Icinga/Web/Dashboard/Common/DashboardControls.php new file mode 100644 index 0000000000..86f0f06206 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Common/DashboardControls.php @@ -0,0 +1,126 @@ +dashboards); + } + + public function getEntry($name) + { + if (! $this->hasEntry($name)) { + throw new ProgrammingError('Trying to retrieve invalid dashboard entry "%s"', $name); + } + + return $this->dashboards[$name]; + } + + public function hasEntry($name) + { + return array_key_exists($name, $this->dashboards); + } + + public function getEntries() + { + // An entry can be added individually afterwards, it might be the case that the priority + // order gets mixed up, so we have to sort things here before being able to render them + uasort($this->dashboards, function (BaseDashboard $x, BaseDashboard $y) { + return $x->getPriority() - $y->getPriority(); + }); + + return $this->dashboards; + } + + public function setEntries(array $entries) + { + $this->dashboards = $entries; + + return $this; + } + + public function addEntry(BaseDashboard $dashboard) + { + if ($this->hasEntry($dashboard->getName())) { + $this->getEntry($dashboard->getName())->fromArray($dashboard->toArray(false)); + } else { + $this->dashboards[$dashboard->getName()] = $dashboard; + } + + return $this; + } + + public function getEntryKeyTitleArr() + { + $dashboards = []; + foreach ($this->getEntries() as $dashboard) { + $dashboards[$dashboard->getName()] = $dashboard->getTitle(); + } + + return $dashboards; + } + + public function removeEntries(array $entries = []) + { + $dashboards = ! empty($entries) ? $entries : $this->getEntries(); + foreach ($dashboards as $dashboard) { + $this->removeEntry($dashboard); + } + + return $this; + } + + public function createEntry($name, $url = null) + { + } + + public function rewindEntries() + { + return reset($this->dashboards); + } + + public function reorderWidget(BaseDashboard $dashboard, $position, Sortable $origin = null) + { + if ($origin && ! $origin instanceof $this) { + throw new \InvalidArgumentException(sprintf( + __METHOD__ . ' expects parameter "$origin" to be an instance of "%s". Got "%s" instead.', + get_php_type($this), + get_php_type($origin) + )); + } + + if (! $this->hasEntry($dashboard->getName())) { + $dashboard->setPriority($position); + $data = [$dashboard]; + } else { + $data = array_values($this->getEntries()); + array_splice($data, array_search($dashboard->getName(), array_keys($this->getEntries())), 1); + array_splice($data, $position, 0, [$dashboard]); + } + + foreach ($data as $index => $item) { + if (count($data) !== 1) { + $item->setPriority($index); + } + + $this->manageEntry($item, $dashboard->getName() === $item->getName() ? $origin : null); + } + + return $this; + } +} diff --git a/library/Icinga/Web/Dashboard/Common/DashboardEntry.php b/library/Icinga/Web/Dashboard/Common/DashboardEntry.php new file mode 100644 index 0000000000..0d5ab139f4 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Common/DashboardEntry.php @@ -0,0 +1,131 @@ +title format + * + * @return string[] + */ + public function getEntryKeyTitleArr(); + + /** + * Remove the given entry from this widget + * + * @param BaseDashboard|string $entry + * + * @return $this + */ + public function removeEntry($entry); + + /** + * Removes the given list of entries from this widget + * + * If there is no entries passed, all the available entries of this widget will be removed + * + * @param BaseDashboard[] $entries + * + * @return $this + */ + public function removeEntries(array $entries = []); + + /** + * Manage the given widget(s) + * + * Performs all kinds of database actions for the given widget(s) except the DELETE action. If you want to + * move pane(s)|dashlet(s) from another to this widget you have to also provide the origin from which the + * given entry(ies) originated + * + * @param BaseDashboard|BaseDashboard[] $entry + * @param ?BaseDashboard $origin + * @param bool $updateChildEntries + * + * @return $this + */ + public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false); + + /** + * Load all the assigned entries to this widget + * + * @param ?string $name Name of the dashboard widget you want to load the dashboard entries for + * + * @return $this + */ + public function loadDashboardEntries($name = ''); + + /** + * Reset the current position of the internal dashboard entries pointer + * + * @return false|BaseDashboard + */ + public function rewindEntries(); +} diff --git a/library/Icinga/Web/Dashboard/Common/DashboardManager.php b/library/Icinga/Web/Dashboard/Common/DashboardManager.php index 77237fcf2a..273aec0be5 100644 --- a/library/Icinga/Web/Dashboard/Common/DashboardManager.php +++ b/library/Icinga/Web/Dashboard/Common/DashboardManager.php @@ -11,10 +11,10 @@ use Icinga\Model; use Icinga\User; use Icinga\Web\Dashboard\Dashboard; +use Icinga\Web\Dashboard\DashboardHome; use Icinga\Web\Dashboard\Dashlet; use Icinga\Web\Dashboard\Pane; use Icinga\Web\HomeMenu; -use Icinga\Web\Navigation\DashboardHome; use ipl\Orm\Query; use ipl\Sql\Connection; use ipl\Sql\Expression; @@ -38,17 +38,10 @@ trait DashboardManager */ private static $defaultPanes = []; - /** - * A list of @see DashboardHome - * - * @var DashboardHome[] - */ - private $homes = []; - public function load() { - $this->loadHomesFromMenu(); - $this->loadDashboards(); + $this->setEntries((new HomeMenu())->loadHomes()); + $this->loadDashboardEntries(); $this->initGetDefaultHome(); self::deployModuleDashlets(); @@ -84,115 +77,33 @@ public static function getSHA1($name) return sha1($name, true); } - /** - * Load dashboard homes from the navigation menu - * - * @return $this - */ - public function loadHomesFromMenu() + public function loadDashboardEntries($name = '') { - $menu = new HomeMenu(); - foreach ($menu->getItem('dashboard')->getChildren() as $home) { - if (! $home instanceof DashboardHome) { - continue; - } - - $this->homes[$home->getName()] = $home; - } - - return $this; - } - - /** - * Load dashboards assigned to the given home or active home being loaded - * - * @param ?string $name Name of the dashboard home you want to load the dashboards for - * - * @return $this - */ - public function loadDashboards($name = null) - { - if ($name && $this->hasHome($name)) { - $home = $this->getHome($name); + if ($name && $this->hasEntry($name)) { + $home = $this->getEntry($name); } else { $requestRoute = Url::fromRequest(); if ($requestRoute->getPath() === Dashboard::BASE_ROUTE) { $home = $this->initGetDefaultHome(); } else { $homeParam = $requestRoute->getParam('home'); - if (empty($homeParam) || ! $this->hasHome($homeParam)) { - if (! ($home = $this->rewindHomes())) { + if (empty($homeParam) || ! $this->hasEntry($homeParam)) { + if (! ($home = $this->rewindEntries())) { // No dashboard homes return $this; } } else { - $home = $this->getHome($homeParam); + $home = $this->getEntry($homeParam); } } } $this->activateHome($home); - $home->loadPanesFromDB(); - - return $this; - } - - /** - * Get a dashboard home by the given name - * - * @param string $name - * - * @return DashboardHome - */ - public function getHome($name) - { - if ($this->hasHome($name)) { - return $this->homes[$name]; - } - - throw new ProgrammingError('Trying to retrieve invalid dashboard home "%s"', $name); - } - - /** - * Get all dashboard homes assigned to the active user - * - * @return DashboardHome[] - */ - public function getHomes() - { - return $this->homes; - } - - /** - * Set this user's dashboard homes - * - * @param DashboardHome|DashboardHome[] $homes - * - * @return $this - */ - public function setHomes($homes) - { - if ($homes instanceof DashboardHome) { - $homes = [$homes->getName() => $homes]; - } - - $this->homes = $homes; + $home->loadDashboardEntries(); return $this; } - /** - * Get whether the given home exist - * - * @param string $name - * - * @return bool - */ - public function hasHome($name) - { - return array_key_exists($name, $this->homes); - } - /** * Activates the given home and deactivates all other active homes * @@ -207,7 +118,7 @@ public function activateHome(DashboardHome $home) $activeHome->setActive(false); } - $home->setActive(true); + $home->setActive(); return $this; } @@ -219,7 +130,8 @@ public function activateHome(DashboardHome $home) */ public function getActiveHome() { - foreach ($this->getHomes() as $home) { + /** @var DashboardHome $home */ + foreach ($this->getEntries() as $home) { if ($home->getActive()) { return $home; } @@ -228,123 +140,59 @@ public function getActiveHome() return null; } - /** - * Reset the current position of the internal dashboard homes pointer - * - * @return false|DashboardHome - */ - public function rewindHomes() - { - return reset($this->homes); - } - - /** - * Remove the given home - * - * @param DashboardHome|string $home - * - * @return $this - */ - public function removeHome($home) + public function removeEntry($home) { $name = $home instanceof DashboardHome ? $home->getName() : $home; - if (! $this->hasHome($name)) { + if (! $this->hasEntry($name)) { throw new ProgrammingError('Trying to remove invalid dashboard home "%s"', $name); } - $home = $home instanceof DashboardHome ? $home : $this->getHome($home); - if (! $home->isDisabled()) { - $home->removePanes(); - + $home = $home instanceof DashboardHome ? $home : $this->getEntry($home); + $home->removeEntries(); + if ($home->getName() !== DashboardHome::DEFAULT_HOME) { self::getConn()->delete(DashboardHome::TABLE, ['id = ?' => $home->getUuid()]); } return $this; } - /** - * Remove all|given list of dashboard homes - * - * @param DashboardHome[] $homes Optional list of dashboard homes - * - * @return $this - */ - public function removeHomes(array $homes = []) - { - $homes = ! empty($homes) ? $homes : $this->getHomes(); - foreach ($homes as $home) { - $this->removeHome($home); - } - - return $this; - } - - /** - * Manage the given home - * - * @param DashboardHome $home - * - * @return $this - */ - public function manageHome(DashboardHome $home) + public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) { $conn = self::getConn(); - if (! $this->hasHome($home->getName())) { + $home = $entry; + if (! $this->hasEntry($home->getName())) { + $priority = $home->getName() === DashboardHome::DEFAULT_HOME ? 0 : count($this->getEntries()); $conn->insert(DashboardHome::TABLE, [ 'name' => $home->getName(), - 'label' => $home->getLabel(), + 'label' => $home->getTitle(), 'username' => self::getUser()->getUsername(), - 'priority' => count($this->getHomes()) + 1, + // highest priority is 0, so count($entries) are always lowest prio + 1 + 'priority' => $priority, 'type' => $home->getType() !== Dashboard::SYSTEM ? $home->getType() : Dashboard::PRIVATE_DS ]); $home->setUuid($conn->lastInsertId()); } elseif ($home->getName() !== DashboardHome::DEFAULT_HOME) { - $conn->update(DashboardHome::TABLE, [ - 'label' => $home->getLabel(), - 'priority' => $home->getPriority() - ], ['id = ?' => $home->getUuid()]); - } else { - $conn->update(DashboardHome::TABLE, ['priority' => $home->getPriority()], ['id = ?' => $home->getUuid()]); + $conn->update(DashboardHome::TABLE, ['label' => $home->getTitle()], ['id = ?' => $home->getUuid()]); } return $this; } - /** - * Get an array with home name=>title format - * - * @return array - */ - public function getHomeKeyTitleArr() - { - $panes = []; - foreach ($this->getHomes() as $home) { - if ($home->isDisabled()) { - continue; - } - - $panes[$home->getName()] = $home->getLabel(); - } - - return $panes; - } - /** * Get and|or init the default dashboard home * - * @return DashboardHome + * @return BaseDashboard */ public function initGetDefaultHome() { - if ($this->hasHome(DashboardHome::DEFAULT_HOME)) { - return $this->getHome(DashboardHome::DEFAULT_HOME); + if ($this->hasEntry(DashboardHome::DEFAULT_HOME)) { + return $this->getEntry(DashboardHome::DEFAULT_HOME); } $default = new DashboardHome(DashboardHome::DEFAULT_HOME); - $this->manageHome($default); - - $this->homes[$default->getName()] = $default; + $this->manageEntry($default); + $this->addEntry($default); return $default; } @@ -400,7 +248,8 @@ public static function deployModuleDashlets() foreach ($moduleManager->getLoadedModules() as $module) { foreach ($module->getDashboard() as $dashboardPane) { $priority = 0; - foreach ($dashboardPane->getDashlets() as $dashlet) { + /** @var Dashlet $dashlet */ + foreach ($dashboardPane->getEntries() as $dashlet) { $uuid = self::getSHA1($module->getName() . $dashboardPane->getName() . $dashlet->getName()); $dashlet ->setUuid($uuid) diff --git a/library/Icinga/Web/Dashboard/Common/DisableWidget.php b/library/Icinga/Web/Dashboard/Common/DisableWidget.php deleted file mode 100644 index 15e7c55324..0000000000 --- a/library/Icinga/Web/Dashboard/Common/DisableWidget.php +++ /dev/null @@ -1,37 +0,0 @@ -disabled = $disable; - - return $this; - } - - /** - * Get whether this widget is disabled - * - * @return bool - */ - public function isDisabled() - { - return $this->disabled; - } -} diff --git a/library/Icinga/Web/Dashboard/Common/ItemListControl.php b/library/Icinga/Web/Dashboard/Common/ItemListControl.php index f211643fcc..9d3f22ff40 100644 --- a/library/Icinga/Web/Dashboard/Common/ItemListControl.php +++ b/library/Icinga/Web/Dashboard/Common/ItemListControl.php @@ -6,35 +6,82 @@ use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; +use ipl\Web\Url; use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; abstract class ItemListControl extends BaseHtmlElement { protected $tag = 'div'; /** - * Set a class name for the collapsible control + * Get this item's unique html identifier * - * @var string + * @return string */ - protected $collapsibleControlClass; + protected abstract function getHtmlId(); - protected function setCollapsibleControlClass($class) + /** + * Get a class name for the collapsible control + * + * @return string + */ + protected abstract function getCollapsibleControlClass(); + + /** + * Create an action link to be added at the end of the list + * + * @return HtmlElement + */ + protected abstract function createActionLink(); + + /** + * Create the appropriate item list of this control + * + * @return HtmlElement + */ + protected abstract function createItemList(); + + /** + * Assemble a header element for this item list + * + * @param Url $url + * @param string $header + * + * @return void + */ + protected function assembleHeader(Url $url, $header, $disable = false) { - $this->collapsibleControlClass = $class; + $header = HtmlElement::create('h1', ['class' => 'collapsible-header'], $header); + $header->addHtml(new Link(t('Edit'), $url, [ + 'class' => $disable ? 'disabled' : null, + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ])); - return $this; + $this->addHtml($header); } protected function assemble() { - $this->addHtml(HtmlElement::create('div', ['class' => $this->collapsibleControlClass], [ + $this->getAttributes()->add([ + 'id' => $this->getHtmlId(), + 'class' => ['collapsible'], + 'data-toggle-element' => '.dashboard-list-info', + ]); + + $this->addHtml(HtmlElement::create('div', ['class' => $this->getCollapsibleControlClass()], [ new Icon('angle-down', ['class' => 'expand-icon', 'title' => t('Expand')]), new Icon('angle-up', ['class' => 'collapse-icon', 'title' => t('Collapse')]) ])); - $this->getAttributes()->registerAttributeCallback('draggable', function () { - return 'true'; - }); + $this->addHtml($this->createItemList()); + $actionLink = $this->createActionLink(); + $actionLink->getAttributes()->add([ + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ]); + + $this->addHtml($actionLink); } } diff --git a/library/Icinga/Web/Dashboard/Common/OrderWidget.php b/library/Icinga/Web/Dashboard/Common/OrderWidget.php deleted file mode 100644 index b726a20d40..0000000000 --- a/library/Icinga/Web/Dashboard/Common/OrderWidget.php +++ /dev/null @@ -1,39 +0,0 @@ -order = $order; - - return $this; - } - - /** - * Get the priority order of this widget - * - * @return int - */ - public function getPriority() - { - return $this->order; - } -} diff --git a/library/Icinga/Web/Dashboard/OverridingWidget.php b/library/Icinga/Web/Dashboard/Common/OverridingWidget.php similarity index 91% rename from library/Icinga/Web/Dashboard/OverridingWidget.php rename to library/Icinga/Web/Dashboard/Common/OverridingWidget.php index b9c0524484..d4c6b5ec4c 100644 --- a/library/Icinga/Web/Dashboard/OverridingWidget.php +++ b/library/Icinga/Web/Dashboard/Common/OverridingWidget.php @@ -2,7 +2,7 @@ /* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */ -namespace Icinga\Web\Dashboard; +namespace Icinga\Web\Dashboard\Common; interface OverridingWidget { diff --git a/library/Icinga/Web/Dashboard/Common/Sortable.php b/library/Icinga/Web/Dashboard/Common/Sortable.php new file mode 100644 index 0000000000..6ea9c693f2 --- /dev/null +++ b/library/Icinga/Web/Dashboard/Common/Sortable.php @@ -0,0 +1,23 @@ +tabs->disableLegacyExtensions(); - if (! $activeHome || $activeHome->isDisabled()) { + if (! $activeHome) { return $this->tabs; } - foreach ($activeHome->getPanes() as $key => $pane) { - if ($pane->isDisabled()) { - continue; - } - + foreach ($activeHome->getEntries() as $key => $pane) { if (! $this->tabs->get($key)) { $this->tabs->add( $key, @@ -173,12 +169,9 @@ private function setDefaultPane() { $active = null; $activeHome = $this->getActiveHome(); - - foreach ($activeHome->getPanes() as $key => $pane) { - if ($pane->isDisabled() === false) { - $active = $key; - break; - } + foreach ($activeHome->getEntries() as $key => $pane) { + $active = $key; + break; } if ($active !== null) { @@ -211,7 +204,7 @@ public function determineActivePane() if (! $active) { if ($active = Url::fromRequest()->getParam($this->tabParam)) { - if ($activeHome->hasPane($active)) { + if ($activeHome->hasEntry($active)) { $this->activate($active); } else { throw new ProgrammingError('Try to get an inexistent pane.'); @@ -223,7 +216,7 @@ public function determineActivePane() $active = $active->getName(); } - $panes = $activeHome->getPanes(); + $panes = $activeHome->getEntries(); if (isset($panes[$active])) { return $panes[$active]; } @@ -241,7 +234,7 @@ public function setWelcomeForm(Form $form) protected function assemble() { $activeHome = $this->getActiveHome(); - if (! $activeHome || ($activeHome->getName() === DashboardHome::DEFAULT_HOME && ! $activeHome->hasPanes())) { + if (! $activeHome || (! $activeHome->hasEntries() && $activeHome->getName() === DashboardHome::DEFAULT_HOME)) { $this->setAttribute('class', 'content welcome-view'); $wrapper = HtmlElement::create('div', ['class' => 'dashboard-introduction']); @@ -266,19 +259,21 @@ protected function assemble() $wrapper->addHtml($this->welcomeForm); $this->addHtml($wrapper); - } elseif (! empty($activeHome->getPanes(true))) { - $dashlets = $this->getActivePane()->getDashlets(); + } elseif (! $activeHome->hasEntries()) { + $this->setAttribute('class', 'content'); + $this->addHtml(HtmlElement::create('h1', null, t('No dashboard added to this dashboard home'))); + } else { + $activePane = $this->getActivePane(); $this->setAttribute('data-icinga-pane', $activeHome->getName() . '|' . $this->getActivePane()->getName()); - if (empty($dashlets)) { + if (! $activePane->hasEntries()) { $this->setAttribute('class', 'content'); - $dashlets = HtmlElement::create('h1', null, t('No dashlet added to this pane.')); + $this->addHtml(HtmlElement::create('h1', null, t('No dashlet added to this pane.'))); + } else { + foreach ($activePane->getEntries() as $dashlet) { + $this->addHtml($dashlet->getHtml()); + } } - - $this->add($dashlets); - } else { - // TODO: What to do with dashboard homes without any dashboards?? - exit(0); } } } diff --git a/library/Icinga/Web/Dashboard/DashboardHome.php b/library/Icinga/Web/Dashboard/DashboardHome.php new file mode 100644 index 0000000000..fc7cf97b7b --- /dev/null +++ b/library/Icinga/Web/Dashboard/DashboardHome.php @@ -0,0 +1,240 @@ +getName(), $homeItem->getAttributes()); + } + + /** + * Set whether this home is active + * + * DB dashboards will load only when this home has been activated + * + * @param bool $active + * + * @return $this + */ + public function setActive($active = true) + { + $this->active = $active; + + return $this; + } + + /** + * Get whether this home has been activated + * + * @return bool + */ + public function getActive() + { + return $this->active; + } + + /** + * Set the type of this dashboard home + * + * @param string $type + * + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get the type of this dashboard home + * + * @return string + */ + public function getType() + { + return $this->type; + } + + public function removeEntry($pane) + { + $name = $pane instanceof Pane ? $pane->getName() : $pane; + if (! $this->hasEntry($name)) { + throw new ProgrammingError('Trying to remove invalid dashboard pane "%s"', $name); + } + + $pane = $pane instanceof Pane ? $pane : $this->getEntry($pane); + if (! $pane->isOverriding()) { + $pane->removeEntries(); + + Dashboard::getConn()->delete(Pane::TABLE, [ + 'id = ?' => $pane->getUuid(), + 'home_id = ?' => $this->getUuid() + ]); + } + + return $this; + } + + public function loadDashboardEntries($name = '') + { + if (! $this->getActive()) { + return $this; + } + + $this->setEntries([]); + $panes = \Icinga\Model\Pane::on(Dashboard::getConn())->utilize('home'); + $panes + ->filter(Filter::equal('home_id', $this->getUuid())) + ->filter(Filter::equal('username', Dashboard::getUser()->getUsername())); + + foreach ($panes as $pane) { + $newPane = new Pane($pane->name); + $newPane->fromArray([ + 'uuid' => $pane->id, + 'title' => $pane->label, + 'priority' => $pane->priority, + 'home' => $this + ]); + + $newPane->loadDashboardEntries($name); + $this->addEntry($newPane); + } + + return $this; + } + + public function createEntry($name, $url = null) + { + $entry = new Pane($name); + $entry->setHome($this); + + $this->addEntry($entry); + + return $this; + } + + public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) + { + $user = Dashboard::getUser(); + $conn = Dashboard::getConn(); + + $panes = is_array($entry) ? $entry : [$entry]; + // highest priority is 0, so count($entries) are all always lowest prio + 1 + $order = count($this->getEntries()); + + if ($origin && ! $origin instanceof DashboardHome) { + throw new \InvalidArgumentException(sprintf( + __METHOD__ . ' expects parameter "$origin" to be an instance of "%s". Got "%s" instead.', + get_php_type($this), + get_php_type($origin) + )); + } + + /** @var Pane $pane */ + foreach ($panes as $pane) { + $uuid = Dashboard::getSHA1($user->getUsername() . $this->getName() . $pane->getName()); + if (! $pane->isOverriding()) { + if (! $this->hasEntry($pane->getName()) && (! $origin || ! $origin->hasEntry($pane->getName()))) { + $conn->insert(Pane::TABLE, [ + 'id' => $uuid, + 'home_id' => $this->getUuid(), + 'name' => $pane->getName(), + 'label' => $pane->getTitle(), + 'username' => $user->getUsername(), + 'priority' => $order++ + ]); + } elseif (! $this->hasEntry($pane->getName()) || ! $origin || ! $origin->hasEntry($pane->getName())) { + $filterCondition = [ + 'id = ?' => $pane->getUuid(), + 'home_id = ?' => $this->getUuid() + ]; + + if ($origin && $origin->hasEntry($pane->getName())) { + $filterCondition = [ + 'id = ?' => $origin->getEntry($pane->getName())->getUuid(), + 'home_id = ?' => $origin->getUuid() + ]; + } + + $conn->update(Pane::TABLE, [ + 'id' => $uuid, + 'home_id' => $this->getUuid(), + 'label' => $pane->getTitle(), + 'priority' => $pane->getPriority() + ], $filterCondition); + } else { + // Failed to move the pane! Should have been handled already by the caller + break; + } + + $pane->setHome($this); + $pane->setUuid($uuid); + } + + if ($updateChildEntries) { + // Those dashboard panes are usually system defaults and go up when + // the user is clicking on the "Use System Defaults" button + $dashlets = $pane->getEntries(); + $pane->setEntries([]); + $pane->manageEntry($dashlets); + } + } + } + + public function toArray($stringify = true) + { + return [ + 'id' => $this->getUuid(), + 'name' => $this->getName(), + 'title' => $this->getTitle(), + 'priority' => $this->getPriority() + ]; + } +} diff --git a/library/Icinga/Web/Dashboard/Dashlet.php b/library/Icinga/Web/Dashboard/Dashlet.php index a0c2d0c97e..78d2af8870 100644 --- a/library/Icinga/Web/Dashboard/Dashlet.php +++ b/library/Icinga/Web/Dashboard/Dashlet.php @@ -5,13 +5,10 @@ namespace Icinga\Web\Dashboard; use Icinga\Application\Icinga; -use Icinga\Common\DataExtractor; -use Icinga\Web\Dashboard\Common\DisableWidget; +use Icinga\Web\Dashboard\Common\BaseDashboard; use Icinga\Web\Dashboard\Common\ModuleDashlet; -use Icinga\Web\Dashboard\Common\OrderWidget; use Icinga\Web\Request; use Icinga\Web\Url; -use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Web\Widget\Link; @@ -20,23 +17,13 @@ * * This is the new element being used for the Dashlets view */ -class Dashlet extends BaseHtmlElement +class Dashlet extends BaseDashboard { - use DisableWidget; - use OrderWidget; use ModuleDashlet; - use DataExtractor; /** @var string Database table name */ const TABLE = 'dashlet'; - protected $tag = 'div'; - - protected $defaultAttributes = [ - 'class' => 'container widget-sortable', - 'draggable' => 'true' - ]; - /** * The url of this Dashlet * @@ -44,19 +31,6 @@ class Dashlet extends BaseHtmlElement */ protected $url; - /** - * Not translatable name of this dashlet - * - * @var string - */ - protected $name; - - /** - * The title being displayed on top of the dashlet - * @var - */ - protected $title; - /** * The pane this dashlet belongs to * @@ -71,107 +45,21 @@ class Dashlet extends BaseHtmlElement */ protected $progressLabel; - /** - * Unique identifier of this dashlet - * - * @var string - */ - protected $uuid; - - /** - * The dashlet's description - * - * @var string - */ - protected $description; - /** * Create a new dashlet displaying the given url in the provided pane * - * @param string $title The title to use for this dashlet + * @param string $name The title to use for this dashlet * @param Url|string $url The url this dashlet uses for displaying information * @param Pane|null $pane The pane this Dashlet will be added to */ - public function __construct($title, $url, Pane $pane = null) + public function __construct($name, $url, Pane $pane = null) { - $this->name = $title; - $this->title = $title; + parent::__construct($name); + $this->pane = $pane; $this->url = $url; } - /** - * Set the identifier of this dashlet - * - * @param string $id - * - * @return $this - */ - public function setUuid($id) - { - $this->uuid = $id; - - return $this; - } - - /** - * Get the unique identifier of this dashlet - * - * @return string - */ - public function getUuid() - { - return $this->uuid; - } - - /** - * Setter for this name - * - * @param $name - * - * @return $this - */ - public function setName($name) - { - $this->name = $name; - - return $this; - } - - /** - * Getter for this name - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Retrieve the dashlets title - * - * @return string - */ - public function getTitle() - { - return $this->title !== null ? $this->title : $this->getName(); - } - - /** - * Set the title of this dashlet - * - * @param string $title - * - * @return $this - */ - public function setTitle($title) - { - $this->title = $title; - - return $this; - } - /** * Retrieve the dashlets url * @@ -232,30 +120,6 @@ public function getProgressLabel() return $this->progressLabel; } - /** - * Get the dashlet's description - * - * @return string - */ - public function getDescription() - { - return $this->description; - } - - /** - * Set the dashlet's description - * - * @param string $description - * - * @return $this - */ - public function setDescription($description) - { - $this->description = $description; - - return $this; - } - /** * Set the Pane of this dashlet * @@ -280,11 +144,17 @@ public function getPane() return $this->pane; } - protected function assemble() + /** + * Generate a html widget for this dashlet + * + * @return HtmlElement + */ + public function getHtml() { + $dashletHtml = HtmlElement::create('div', ['class' => 'container']); if (! $this->getUrl()) { - $this->addHtml(HtmlElement::create('h1', null, t($this->getTitle()))); - $this->addHtml(HtmlElement::create( + $dashletHtml->addHtml(HtmlElement::create('h1', null, t($this->getTitle()))); + $dashletHtml->addHtml(HtmlElement::create( 'p', ['class' => 'error-message'], sprintf(t('Cannot create dashboard dashlet "%s" without valid URL'), t($this->getTitle())) @@ -293,10 +163,10 @@ protected function assemble() $url = $this->getUrl(); $url->setParam('showCompact', true); - $this->setAttribute('data-icinga-url', $url); - $this->setAttribute('data-icinga-dashlet', $this->getName()); + $dashletHtml->setAttribute('data-icinga-url', $url); + $dashletHtml->setAttribute('data-icinga-dashlet', $this->getName()); - $this->addHtml(new HtmlElement('h1', null, new Link( + $dashletHtml->addHtml(new HtmlElement('h1', null, new Link( t($this->getTitle()), $url->getUrlWithout(['showCompact', 'limit'])->getRelativeUrl(), [ @@ -306,7 +176,7 @@ protected function assemble() ] ))); - $this->addHtml(HtmlElement::create( + $dashletHtml->addHtml(HtmlElement::create( 'p', ['class' => 'progress-label'], [ @@ -317,18 +187,20 @@ protected function assemble() ] )); } + + return $dashletHtml; } - public function toArray() + public function toArray($stringify = true) { + $pane = $this->getPane(); return [ - 'id' => $this->getUuid(), - 'pane' => $this->getPane() ? $this->getPane()->getName() : null, - 'name' => $this->getName(), - 'url' => $this->getUrl()->getRelativeUrl(), - 'label' => $this->getTitle(), - 'order' => $this->getPriority(), - 'disabled' => (int) $this->isDisabled(), + 'id' => $this->getUuid(), + 'pane' => ! $stringify ? $pane : ($pane ? $pane->getName() : null), + 'name' => $this->getName(), + 'url' => $this->getUrl()->getRelativeUrl(), + 'label' => $this->getTitle(), + 'order' => $this->getPriority(), ]; } } diff --git a/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php b/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php index 17bd54d295..d1e05a1889 100644 --- a/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php +++ b/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php @@ -6,72 +6,64 @@ use Icinga\Web\Dashboard\Common\ItemListControl; use Icinga\Web\Dashboard\Dashboard; -use Icinga\Web\Navigation\DashboardHome; +use Icinga\Web\Dashboard\DashboardHome; use ipl\Html\HtmlElement; use ipl\Web\Url; use ipl\Web\Widget\ActionLink; -use ipl\Web\Widget\Link; class DashboardHomeList extends ItemListControl { /** @var DashboardHome */ protected $home; + protected $defaultAttributes = ['class' => 'home-list-control']; + public function __construct(DashboardHome $home) { $this->home = $home; $this->home->setActive(); - $this->home->loadPanesFromDB(); + $this->home->loadDashboardEntries(); - $this->setCollapsibleControlClass('dashboard-list-info'); $this->getAttributes() - ->registerAttributeCallback('class', function () { - return 'home-list-control collapsible widget-sortable'; - }) - ->registerAttributeCallback('data-toggle-element', function () { - return '.dashboard-list-info'; - }) ->registerAttributeCallback('data-icinga-home', function () { return $this->home->getName(); - }) - ->registerAttributeCallback('id', function () { - return 'home_' . $this->home->getPriority(); }); } - protected function assemble() + protected function getHtmlId() { - // TODO: How should disabled homes look like? - parent::assemble(); + return $this->home->getUuid(); + } - $header = HtmlElement::create('h1', ['class' => 'collapsible-header home'], $this->home->getLabel()); - $url = Url::fromPath(Dashboard::BASE_ROUTE . '/rename-home')->setParams([ - 'home' => $this->home->getName() - ]); + protected function getCollapsibleControlClass() + { + return 'dashboard-list-info'; + } - $header->addHtml(new Link(t('Edit'), $url, [ - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ])); + protected function createItemList() + { + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-home') + ->setParams(['home' => $this->home->getName()]); - $this->addHtml($header); + $disable = $this->home->getName() === DashboardHome::DEFAULT_HOME; + $this->assembleHeader($url, $this->home->getTitle(), $disable); $list = HtmlElement::create('ul', ['class' => 'dashboard-item-list']); - $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'); - $url->setParams(['home' => $this->home->getName()]); - // List all dashboard panes - foreach ($this->home->getPanes() as $pane) { + foreach ($this->home->getEntries() as $pane) { $pane->setHome($this->home); // In case it's not set $list->addHtml(new DashboardList($pane)); } - $this->addHtml($list); - $this->addHtml(new ActionLink(t('Add Dashboard'), $url, 'plus', [ - 'class' => 'add-dashboard', - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ])); + return $list; + } + + protected function createActionLink() + { + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-pane'); + $url->setParams(['home' => $this->home->getName()]); + + return new ActionLink(t('Add Dashboard'), $url, 'plus', ['class' => 'add-dashboard']); } } diff --git a/library/Icinga/Web/Dashboard/ItemList/DashboardList.php b/library/Icinga/Web/Dashboard/ItemList/DashboardList.php index 17dbba40ea..ce8324a71d 100644 --- a/library/Icinga/Web/Dashboard/ItemList/DashboardList.php +++ b/library/Icinga/Web/Dashboard/ItemList/DashboardList.php @@ -10,67 +10,65 @@ use ipl\Html\HtmlElement; use ipl\Web\Url; use ipl\Web\Widget\ActionLink; -use ipl\Web\Widget\Link; class DashboardList extends ItemListControl { /** @var Pane */ protected $pane; + protected $defaultAttributes = ['class' => 'dashboard-list-control']; + public function __construct(Pane $pane) { $this->pane = $pane; - $this->setCollapsibleControlClass('dashlets-list-info'); $this->getAttributes() - ->registerAttributeCallback('class', function () { - return 'dashboard-list-control collapsible widget-sortable'; - }) - ->registerAttributeCallback('data-toggle-element', function () { - return '.dashlets-list-info'; - }) ->registerAttributeCallback('data-icinga-pane', function () { - return $this->pane->getHome()->getName() . '|' . $this->pane->getName(); - }) - ->registerAttributeCallback('id', function () { - return 'pane_' . $this->pane->getPriority(); + return $this->pane->getName(); }); } - protected function assemble() + protected function getHtmlId() { - // TODO: How should disabled dashboard panes look like? - parent::assemble(); + return bin2hex($this->pane->getUuid()); + } - $header = HtmlElement::create('h1', ['class' => 'collapsible-header'], $this->pane->getTitle()); - $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-pane')->setParams([ - 'home' => $this->pane->getHome()->getName(), - 'pane' => $this->pane->getName() - ]); + protected function getCollapsibleControlClass() + { + return 'dashlets-list-info'; + } - $header->addHtml(new Link(t('Edit'), $url, [ - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ])); + protected function createItemList() + { + $pane = $this->pane; + $this->getAttributes()->set('data-toggle-element', '.dashlets-list-info'); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-pane') + ->setParams(['home' => $pane->getHome()->getName(), 'pane' => $pane->getName()]); - $this->addHtml($header); + $this->assembleHeader($url, $pane->getTitle()); $list = HtmlElement::create('ul', ['class' => 'dashlet-item-list']); $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'); $url->setParams([ - 'home' => $this->pane->getHome(), - 'pane' => $this->pane->getName() + 'home' => $pane->getHome()->getName(), + 'pane' => $pane->getName() ]); - foreach ($this->pane->getDashlets() as $dashlet) { + foreach ($pane->getEntries() as $dashlet) { $list->addHtml(new DashletListItem($dashlet, true)); } - $this->addHtml($list); - $this->addHtml(new ActionLink(t('Add Dashlet'), $url, 'plus', [ - 'class' => 'add-dashlet', - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ])); + return $list; + } + + protected function createActionLink() + { + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'); + $url->setParams([ + 'home' => $this->pane->getHome()->getName(), + 'pane' => $this->pane->getName() + ]); + + return new ActionLink(t('Add Dashlet'), $url, 'plus', ['class' => 'add-dashlet']); } } diff --git a/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php b/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php index fb6f266e27..d83bc3750b 100644 --- a/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php +++ b/library/Icinga/Web/Dashboard/ItemList/DashletListItem.php @@ -28,17 +28,13 @@ public function __construct(Dashlet $dashlet = null, $renderEditButton = false) $this->dashlet = $dashlet; $this->renderEditButton = $renderEditButton; - if ($this->dashlet) { - $this->getAttributes() - ->set('draggable', 'true') - ->add('class', 'widget-sortable'); - + if ($this->dashlet && $renderEditButton) { $this->getAttributes() ->registerAttributeCallback('data-icinga-dashlet', function () { return $this->dashlet->getName(); }) ->registerAttributeCallback('id', function () { - return 'dashlet_' . $this->dashlet->getPriority(); + return bin2hex($this->dashlet->getUuid()); }); } } @@ -63,23 +59,33 @@ protected function assembleTitle() if (! $this->dashlet) { $title->add(t('Custom Url')); + } elseif ($this->renderEditButton) { + $title->addHtml(new Link( + t($this->dashlet->getTitle()), + $this->dashlet->getUrl()->getUrlWithout(['showCompact', 'limit'])->getRelativeUrl(), + [ + 'class' => 'dashlet-title', + 'aria-label' => t($this->dashlet->getTitle()), + 'title' => t($this->dashlet->getTitle()), + 'data-base-target' => '_next' + ] + )); + + $pane = $this->dashlet->getPane(); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-dashlet'); + $url->setParams([ + 'home' => $pane->getHome()->getName(), + 'pane' => $pane->getName(), + 'dashlet' => $this->dashlet->getName() + ]); + + $title->addHtml(new Link(t('Edit'), $url, [ + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ])); } else { $title->add($this->dashlet->getTitle()); - - if ($this->renderEditButton) { - $pane = $this->dashlet->getPane(); - $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-dashlet'); - $url->setParams([ - 'home' => $pane->getHome()->getName(), - 'pane' => $pane->getName(), - 'dashlet' => $this->dashlet->getName() - ]); - - $title->addHtml(new Link(t('Edit'), $url, [ - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ])); - } + $title->getAttributes()->set('title', $this->dashlet->getTitle()); } return $title; @@ -92,7 +98,12 @@ protected function assembleSummary() if (! $this->dashlet) { $section->add(t('Create a dashlet with custom url and filter')); } else { - $section->add($this->dashlet->getDescription() ?: $this->dashlet->getTitle()); + $section->getAttributes()->set( + 'title', + $this->dashlet->getDescription() ?: t('There is no provided description.') + ); + + $section->add($this->dashlet->getDescription() ?: t('There is no provided dashlet description.')); } return $section; diff --git a/library/Icinga/Web/Dashboard/ItemList/DashletListMultiSelect.php b/library/Icinga/Web/Dashboard/ItemList/DashletListMultiSelect.php index 2040d4d71e..af38a3b67c 100644 --- a/library/Icinga/Web/Dashboard/ItemList/DashletListMultiSelect.php +++ b/library/Icinga/Web/Dashboard/ItemList/DashletListMultiSelect.php @@ -1,5 +1,7 @@ name = $name; - $this->title = $name; - - if (! empty($properties)) { - $this->fromArray($properties); - } - } - - /** - * Set the name of this pane - * - * @param string $name - */ - public function setName($name) - { - $this->name = $name; - } - - /** - * Returns the name of this pane - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the title of this pane - * - * @return string - */ - public function getTitle() - { - return $this->title; - } - - /** - * Overwrite the title of this pane - * - * @param string $title The new title to use for this pane - * - * @return $this - */ - public function setTitle($title) - { - $this->title = $title; - - return $this; - } + protected $home; public function override(bool $override) { @@ -153,54 +65,6 @@ public function isOverriding() return $this->override; } - /** - * Set this pane's unique identifier - * - * @param string $uuid - * - * @return $this - */ - public function setUuid($uuid) - { - $this->uuid = $uuid; - - return $this; - } - - /** - * Get this pane's unique identifier - * - * @return string - */ - public function getUuid() - { - return $this->uuid; - } - - /** - * Set the owner of this dashboard - * - * @param string $owner - * - * @return $this - */ - public function setOwner($owner) - { - $this->owner = $owner; - - return $this; - } - - /** - * Get owner of this dashboard - * - * @return string - */ - public function getOwner() - { - return $this->owner; - } - /** * Set the number of users who have subscribed to this pane if (public) * @@ -249,86 +113,12 @@ public function setHome(DashboardHome $home) return $this; } - /** - * Return true if a dashlet with the given name exists in this pane - * - * @param string $name The title of the dashlet to check for existence - * - * @return bool - */ - public function hasDashlet($name) - { - return array_key_exists($name, $this->dashlets); - } - - /** - * Checks if the current pane has any dashlets - * - * @return bool - */ - public function hasDashlets() - { - return ! empty($this->dashlets); - } - - /** - * Get a dashlet with the given name if existing - * - * @param string $name - * - * @return Dashlet - */ - public function getDashlet($name) - { - if ($this->hasDashlet($name)) { - return $this->dashlets[$name]; - } - - throw new ProgrammingError('Trying to access invalid dashlet: %s', $name); - } - - /** - * Get all dashlets belongs to this pane - * - * @return Dashlet[] - */ - public function getDashlets() - { - uasort($this->dashlets, function (Dashlet $x, Dashlet $y) { - return $x->getPriority() - $y->getPriority(); - }); - - return $this->dashlets; - } - - /** - * Set dashlets of this pane - * - * @param Dashlet[] $dashlets - * - * @return $this - */ - public function setDashlets(array $dashlets) - { - $this->dashlets = $dashlets; - - return $this; - } - - /** - * Create, add and return a new dashlet - * - * @param string $name - * @param string $url - * - * @return Dashlet - */ - public function createDashlet($name, $url = null) + public function createEntry($name, $url = null) { $dashlet = new Dashlet($name, $url, $this); $this->addDashlet($dashlet); - return $dashlet; + return $this; } /** @@ -343,9 +133,9 @@ public function createDashlet($name, $url = null) public function addDashlet($dashlet, $url = null) { if ($dashlet instanceof Dashlet) { - $this->dashlets[$dashlet->getName()] = $dashlet; + $this->addEntry($dashlet); } elseif (is_string($dashlet) && $url !== null) { - $this->createDashlet($dashlet, $url); + $this->createEntry($dashlet, $url); } else { throw new ConfigurationError('Invalid dashlet added: %s', $dashlet); } @@ -364,31 +154,24 @@ public function addDashlet($dashlet, $url = null) */ public function add($name, $url, $priority = 0, $description = null) { - $dashlet = $this->createDashlet($name, $url); + $this->createEntry($name, $url); + $dashlet = $this->getEntry($name); $dashlet ->setDescription($description) ->setPriority($priority); - $this->addDashlet($dashlet); return $this; } - /** - * Remove the given dashlet if it exists in this pane - * - * @param Dashlet|string $dashlet - * - * @return $this - */ - public function removeDashlet($dashlet) + public function removeEntry($dashlet) { $name = $dashlet instanceof Dashlet ? $dashlet->getName() : $dashlet; - if (! $this->hasDashlet($name)) { + if (! $this->hasEntry($name)) { throw new ProgrammingError('Trying to remove invalid dashlet: %s', $name); } if (! $dashlet instanceof Dashlet) { - $dashlet = $this->getDashlet($dashlet); + $dashlet = $this->getEntry($dashlet); } Dashboard::getConn()->delete(Dashlet::TABLE, [ @@ -399,38 +182,12 @@ public function removeDashlet($dashlet) return $this; } - /** - * Removes all or a given list of dashlets from this pane - * - * @param array $dashlets Optional list of dashlets - * - * @return $this - */ - public function removeDashlets(array $dashlets = []) - { - $dashlets = ! empty($dashlets) ? $dashlets : $this->getDashlets(); - foreach ($dashlets as $dashlet) { - $this->removeDashlet($dashlet); - } - - return $this; - } - - /** - * Load all dashlets this dashboard is assigned to - * - * @return $this - */ - public function loadDashletsFromDB() + public function loadDashboardEntries($name = '') { - if ($this->isDisabled()) { - return $this; - } - - $this->dashlets = []; $dashlets = Model\Dashlet::on(Dashboard::getConn())->with('module_dashlet'); $dashlets->filter(Filter::equal('dashboard_id', $this->getUuid())); + $this->setEntries([]); foreach ($dashlets as $dashlet) { $newDashlet = new Dashlet($dashlet->name, $dashlet->url, $this); $newDashlet->fromArray([ @@ -447,44 +204,40 @@ public function loadDashletsFromDB() return $this; } - /** - * Manage the given dashlet(s) - * - * If you want to move the dashlet(s) from another to this pane, - * you have to also pass the origin pane - * - * @param Dashlet|Dashlet[] $dashlets - * @param ?Pane $origin - * - * @return $this - */ - public function manageDashlets($dashlets, Pane $origin = null) + public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) { + if ($origin && ! $origin instanceof Pane) { + throw new \InvalidArgumentException(sprintf( + __METHOD__ . ' expects parameter "$origin" to be an instance of "%s". Got "%s" instead.', + get_php_type($this), + get_php_type($origin) + )); + } + if (! $this->getHome()) { throw new \LogicException( 'Dashlets cannot be managed. Please make sure to set the current dashboard home beforehand.' ); } - $user = Dashboard::getUser(); + $home = $this->getHome(); + $user = Dashboard::getUser()->getUsername(); $conn = Dashboard::getConn(); - $dashlets = is_array($dashlets) ? $dashlets : [$dashlets]; - $order = count($this->getDashlets()) + 1; + $dashlets = is_array($entry) ? $entry : [$entry]; + // highest priority is 0, so count($entries) are all always lowest prio + 1 + $order = count($this->getEntries()); foreach ($dashlets as $dashlet) { if (is_array($dashlet)) { - $this->manageDashlets($dashlet, $origin); + $this->manageEntry($dashlet, $origin); } if (! $dashlet instanceof Dashlet) { break; } - $uuid = Dashboard::getSHA1( - $user->getUsername() . $this->getHome()->getName() . $this->getName() . $dashlet->getName() - ); - - if (! $this->hasDashlet($dashlet->getName()) && (! $origin || ! $origin->hasDashlet($dashlet->getName()))) { + $uuid = Dashboard::getSHA1($user . $home->getName() . $this->getName() . $dashlet->getName()); + if (! $this->hasEntry($dashlet->getName()) && (! $origin || ! $origin->hasEntry($dashlet->getName()))) { $conn->insert(Dashlet::TABLE, [ 'id' => $uuid, 'dashboard_id' => $this->getUuid(), @@ -495,22 +248,41 @@ public function manageDashlets($dashlets, Pane $origin = null) ]); if ($dashlet->isModuleDashlet()) { - $systemUuid = Dashboard::getSHA1($dashlet->getModule() . $this->getName() . $dashlet->getName()); - $conn->insert('dashlet_system', [ - 'dashlet_id' => $uuid, - 'module_dashlet_id' => $systemUuid - ]); + $systemUuid = $dashlet->getUuid(); + if (! $systemUuid && $dashlet->getPane()) { + $systemUuid = Dashboard::getSHA1( + $dashlet->getModule() . $dashlet->getPane()->getName() . $dashlet->getName() + ); + } + + if ($systemUuid) { + $conn->insert('dashlet_system', [ + 'dashlet_id' => $uuid, + 'module_dashlet_id' => $systemUuid + ]); + } } - } elseif (! $this->hasDashlet($dashlet->getName()) - || ! $origin - || ! $origin->hasDashlet($dashlet->getName())) { + } elseif (! $this->hasEntry($dashlet->getName()) || ! $origin + || ! $origin->hasEntry($dashlet->getName())) { + $filterCondition = [ + 'id = ?' => $dashlet->getUuid(), + 'dashboard_id = ?' => $this->getUuid() + ]; + + if ($origin && $origin->hasEntry($dashlet->getName())) { + $filterCondition = [ + 'id = ?' => $origin->getEntry($dashlet->getName())->getUuid(), + 'dashboard_id = ?' => $origin->getUuid() + ]; + } + $conn->update(Dashlet::TABLE, [ 'id' => $uuid, 'dashboard_id' => $this->getUuid(), 'label' => $dashlet->getTitle(), 'url' => $dashlet->getUrl()->getRelativeUrl(), 'priority' => $dashlet->getPriority() - ], ['id = ?' => $dashlet->getUuid()]); + ], $filterCondition); } else { // This should have already been handled by the caller break; @@ -522,15 +294,15 @@ public function manageDashlets($dashlets, Pane $origin = null) return $this; } - public function toArray() + public function toArray($stringify = true) { + $home = $this->getHome(); return [ 'id' => $this->getUuid(), 'name' => $this->getName(), 'label' => $this->getTitle(), - 'home' => $this->getHome() ? $this->getHome()->getName() : null, + 'home' => ! $stringify ? $home : ($home ? $home->getName() : null), 'priority' => $this->getPriority(), - 'disabled' => (int) $this->isDisabled() ]; } } diff --git a/library/Icinga/Web/Dashboard/Settings.php b/library/Icinga/Web/Dashboard/Settings.php index 09cf65a529..8b88d4df90 100644 --- a/library/Icinga/Web/Dashboard/Settings.php +++ b/library/Icinga/Web/Dashboard/Settings.php @@ -6,7 +6,6 @@ use Icinga\Web\Dashboard\ItemList\DashboardHomeList; use Icinga\Web\Dashboard\ItemList\DashboardList; -use Icinga\Web\Navigation\DashboardHome; use ipl\Html\BaseHtmlElement; use ipl\Web\Url; use ipl\Web\Widget\ActionLink; @@ -27,31 +26,25 @@ public function __construct(Dashboard $dashboard) protected function assemble() { - // TODO: What we should do with disabled homes?? $activeHome = $this->dashboard->getActiveHome(); - - if (empty($this->dashboard->getHomes())) { - // TODO: No dashboard homes :( what should we render now?? - } elseif (count($this->dashboard->getHomes()) === 1 && $activeHome->getName() === DashboardHome::DEFAULT_HOME) { - foreach ($activeHome->getPanes() as $pane) { + if (count($this->dashboard->getEntries()) === 1 && $activeHome->getName() === DashboardHome::DEFAULT_HOME) { + foreach ($activeHome->getEntries() as $pane) { $pane->setHome($activeHome); $this->addHtml(new DashboardList($pane)); } - $this->addHtml(new ActionLink( - t('Add Dashboard'), - Url::fromPath(Dashboard::BASE_ROUTE . '/new-dashlet'), - 'plus', - [ - 'class' => 'add-dashboard', - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ] - )); + $url = Url::fromPath(Dashboard::BASE_ROUTE . '/new-pane') + ->setParams(['home' => $activeHome->getName()]); + + $this->addHtml(new ActionLink(t('Add Dashboard'), $url, 'plus', [ + 'class' => 'add-dashboard', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ])); } else { // Make a list of dashboard homes - foreach ($this->dashboard->getHomes() as $home) { + foreach ($this->dashboard->getEntries() as $home) { $this->addHtml(new DashboardHomeList($home)); } } diff --git a/library/Icinga/Web/Dashboard/Setup/SetupNewDashboard.php b/library/Icinga/Web/Dashboard/Setup/SetupNewDashboard.php index 5e8deed1f8..c0b3046bf7 100644 --- a/library/Icinga/Web/Dashboard/Setup/SetupNewDashboard.php +++ b/library/Icinga/Web/Dashboard/Setup/SetupNewDashboard.php @@ -4,29 +4,26 @@ namespace Icinga\Web\Dashboard\Setup; +use Icinga\Forms\Dashboard\BaseDashboardForm; use Icinga\Web\Dashboard\Dashboard; +use Icinga\Web\Dashboard\DashboardHome; use Icinga\Web\Dashboard\Dashlet; use Icinga\Web\Dashboard\Pane; -use Icinga\Web\Navigation\DashboardHome; use Icinga\Web\Notification; use Icinga\Web\Dashboard\ItemList\DashletListMultiSelect; use ipl\Html\HtmlElement; use ipl\Html\ValidHtml; -use ipl\Web\Compat\CompatForm; use ipl\Web\Url; use ipl\Web\Widget\Icon; -class SetupNewDashboard extends CompatForm +class SetupNewDashboard extends BaseDashboardForm { - /** @var Dashboard */ - protected $dashboard; - /** @var array Module dashlets from the DB */ private $dashlets = []; public function __construct(Dashboard $dashboard) { - $this->dashboard = $dashboard; + parent::__construct($dashboard); $this->setRedirectUrl((string) Url::fromPath(Dashboard::BASE_ROUTE)); $this->setAction($this->getRedirectUrl() . '/setup-dashboard'); @@ -46,13 +43,6 @@ public function initDashlets(array $dashlets) return $this; } - public function hasBeenSubmitted() - { - return $this->hasBeenSent() - && ($this->getPopulatedValue('btn_cancel') - || $this->getPopulatedValue('submit')); - } - protected function assemble() { $this->getAttributes()->add('class', 'modal-form'); @@ -60,6 +50,7 @@ protected function assemble() if ($this->getPopulatedValue('btn_next')) { // Configure Dashlets $this->dumpArbitaryDashlets(); + $submitButtonLabel = t('Add Dashlets'); $this->addElement('text', 'pane', [ 'required' => true, 'label' => t('Dashboard Title'), @@ -134,10 +125,8 @@ protected function assemble() } } } - - $submitButton = $this->createElement('submit', 'submit', ['label' => t('Add Dashlets')]); - $this->registerElement($submitButton)->decorate($submitButton); } else { // Select Dashlets + $submitButtonLabel = t('Next'); $list = HtmlElement::create('ul', ['class' => 'dashlet-item-list empty-list']); $multi = new DashletListMultiSelect(); $multi->setCheckBox($this->createElement('checkbox', 'custom_url', ['class' => 'sr-only'])); @@ -169,16 +158,19 @@ protected function assemble() $this->addHtml($listControl->addHtml($list)); } + } - $submitButton = $this->createElement('submit', 'btn_next', [ - 'class' => 'autosubmit', - 'label' => t('Next'), - ]); - $this->registerElement($submitButton)->decorate($submitButton); + $submitButton = $this->registerSubmitButton($submitButtonLabel); + if (! $this->getPopulatedValue('btn_next')) { + $submitButton + ->setName('btn_next') + ->getAttributes()->add('class', 'autosubmit'); } - $this->addElement('submit', 'btn_cancel', ['label' => t('Cancel')]); - $this->getElement('btn_cancel')->setWrapper($submitButton->getWrapper()); + $formControls = $this->createFormControls(); + $formControls->add([$submitButton, $this->createCancelButton()]); + + $this->addHtml($formControls); } protected function onSuccess() @@ -190,12 +182,12 @@ protected function onSuccess() $conn->beginTransaction(); try { - $this->dashboard->getHome(DashboardHome::DEFAULT_HOME)->managePanes($pane); + $this->dashboard->getEntry(DashboardHome::DEFAULT_HOME)->manageEntry($pane); // If element name "dashlet" and "url" are set we need to only store one dashlet if (($name = $this->getPopulatedValue('dashlet')) && ($url = $this->getPopulatedValue('url'))) { $dashlet = new Dashlet($name, $url, $pane); - $pane->manageDashlets($dashlet); + $pane->manageEntry($dashlet); } else { foreach ($this->dashlets as $module => $dashlets) { $moduleDashlets = []; @@ -226,7 +218,7 @@ protected function onSuccess() $moduleDashlets[$dashlet->getName()] = $dashlet; } - $pane->manageDashlets($moduleDashlets); + $pane->manageEntry($moduleDashlets); } } diff --git a/library/Icinga/Web/HomeMenu.php b/library/Icinga/Web/HomeMenu.php index a7704ea466..a5a202ca29 100644 --- a/library/Icinga/Web/HomeMenu.php +++ b/library/Icinga/Web/HomeMenu.php @@ -1,10 +1,13 @@ filter(Filter::equal('username', $user->getUsername())); foreach ($homes as $home) { - $dashboardHome = new DashboardHome($home->name, [ + $dashboardHome = new DashboardHomeItem($home->name, [ 'uuid' => $home->id, 'label' => t($home->label), 'priority' => $home->priority, @@ -38,4 +41,26 @@ public function initHome() $dashboardItem->addChild($dashboardHome); } } + + /** + * Load dashboard homes form the navigation menu + * + * @return DashboardHome[] + */ + public function loadHomes() + { + $homes = []; + foreach ($this->getItem('dashboard')->getChildren() as $child) { + if (! $child instanceof DashboardHomeItem) { + continue; + } + + $home = DashboardHome::create($child); + $home->setTitle($child->getLabel()); + + $homes[$child->getName()] = $home; + } + + return $homes; + } } diff --git a/library/Icinga/Web/Navigation/DashboardHome.php b/library/Icinga/Web/Navigation/DashboardHome.php deleted file mode 100644 index 399a7f4d25..0000000000 --- a/library/Icinga/Web/Navigation/DashboardHome.php +++ /dev/null @@ -1,414 +0,0 @@ -getName() !== self::DEFAULT_HOME && ! $this->isDisabled()) { - $this->setUrl(Url::fromPath(Dashboard::BASE_ROUTE . '/home', [ - 'home' => $this->getName() - ])); - } - } - - /** - * Get this dashboard home's url - * - * Parent class would always report a default url if $this->url isn't - * set, which we do it on purpose. - * - * @return \Icinga\Web\Url - */ - public function getUrl() - { - return $this->url; - } - - /** - * Get whether this home has been activated - * - * @return bool - */ - public function getActive() - { - return $this->active; - } - - /** - * Set whether this home is active - * - * DB dashboard will load only when this home has been activated - * - * @param bool $active - * - * @return $this - */ - public function setActive($active = true) - { - $this->active = $active; - - return $this; - } - - /** - * Get the type of this dashboard home - * - * @return string - */ - public function getType() - { - return $this->type; - } - - /** - * Set the type of this dashboard home - * - * @param string $type - * - * @return $this - */ - public function setType($type) - { - $this->type = $type; - - return $this; - } - - /** - * Get the uuid of this dashboard home - * - * @return int - */ - public function getUuid() - { - return $this->uuid; - } - - /** - * Get the uuid of this dashboard home - * - * @param int $uuid - * - * @return $this - */ - public function setUuid(int $uuid) - { - $this->uuid = $uuid; - - return $this; - } - - /** - * Get a pane with the given name if exists - * - * @param string $name - * - * @return Pane - */ - public function getPane($name) - { - if (! $this->hasPane($name)) { - throw new ProgrammingError('Trying to retrieve invalid dashboard pane "%s"', $name); - } - - return $this->panes[$name]; - } - - /** - * Get whether this home has any dashboard panes - * - * @return bool - */ - public function hasPanes() - { - return ! empty($this->panes); - } - - /** - * Get whether the given pane exist - * - * @param string $name - * - * @return bool - */ - public function hasPane($name) - { - return array_key_exists($name, $this->panes); - } - - /** - * Get all dashboards of this home - * - * @param bool $skipDisabled Whether to skip disabled dashboards - * - * @return Pane[] - */ - public function getPanes($skipDisabled = false) - { - // As the panes can also be added individually afterwards, it might be the case that the priority - // order gets mixed up, so we have to sort things here before being able to render them - uasort($this->panes, function (Pane $x, Pane $y) { - return $x->getPriority() - $y->getPriority(); - }); - - return ! $skipDisabled ? $this->panes : array_filter($this->panes, function ($pane) { - return ! $pane->isDisabled(); - }); - } - - /** - * Set dashboards of this home - * - * @param Pane|Pane[] $panes - * - * @return $this - */ - public function setPanes($panes) - { - if ($panes instanceof Pane) { - $panes = [$panes->getName() => $panes]; - } - - $this->panes = $panes; - - return $this; - } - - /** - * Add a new dashboard pane to this home - * - * @param Pane|string $pane - * - * @return $this - */ - public function addPane($pane) - { - if (! $pane instanceof Pane) { - $pane = new Pane($pane); - $pane - ->setHome($this) - ->setTitle($pane->getName()); - } - - $this->panes[$pane->getName()] = $pane; - - return $this; - } - - /** - * Get an array with pane name=>title format - * - * @return string[] - */ - public function getPaneKeyTitleArr() - { - $panes = []; - foreach ($this->getPanes(true) as $pane) { - $panes[$pane->getName()] = $pane->getName(); - } - - return $panes; - } - - /** - * Remove the given pane from this home - * - * @param Pane|string $pane - * - * @return $this - */ - public function removePane($pane) - { - $name = $pane instanceof Pane ? $pane->getName() : $pane; - if (! $this->hasPane($name)) { - throw new ProgrammingError('Trying to remove invalid dashboard pane "%s"', $name); - } - - $pane = $pane instanceof Pane ? $pane : $this->getPane($pane); - if (! $pane->isOverriding()) { - $pane->removeDashlets(); - - Dashboard::getConn()->delete(Pane::TABLE, [ - 'id = ?' => $pane->getUuid(), - 'home_id = ?' => $this->getUuid() - ]); - } - - return $this; - } - - /** - * Remove all/the given dashboard panes from this home - * - * @param Pane[] $panes - * - * @return $this - */ - public function removePanes(array $panes = []) - { - $panes = ! empty($panes) ? $panes : $this->getPanes(); - foreach ($panes as $pane) { - $this->removePane($pane); - } - - return $this; - } - - /** - * Load all dashboards this user is assigned to from the DB - * - * @return $this - */ - public function loadPanesFromDB() - { - // Skip when this home is either disabled or inactive - if (! $this->getActive() || $this->isDisabled()) { - return $this; - } - - $this->panes = []; - $panes = Model\Pane::on(Dashboard::getConn())->utilize('home'); - $panes - ->filter(Filter::equal('home_id', $this->getUuid())) - ->filter(Filter::equal('username', Dashboard::getUser()->getUsername())); - - foreach ($panes as $pane) { - $newPane = new Pane($pane->name); - //$newPane->disable($pane->disable); - $newPane->fromArray([ - 'uuid' => $pane->id, - 'title' => $pane->label, - 'priority' => $pane->priority, - 'home' => $this - ]); - - $newPane->loadDashletsFromDB(); - - $this->panes[$newPane->getName()] = $newPane; - } - - return $this; - } - - /** - * Manage the given pane(s) - * - * If you want to move the pane(s) from another to this home, - * you have to also pass through the origin home with - * - * @param Pane|Pane[] $panes - * @param ?DashboardHome $origin - * @param bool $mngPaneDashlets - * - * @return $this - */ - public function managePanes($panes, DashboardHome $origin = null, $mngPaneDashlets = false) - { - $user = Dashboard::getUser(); - $conn = Dashboard::getConn(); - - $panes = is_array($panes) ? $panes : [$panes]; - $order = count($this->getPanes()) + 1; - - foreach ($panes as $pane) { - $uuid = Dashboard::getSHA1($user->getUsername() . $this->getName() . $pane->getName()); - if (! $pane->isOverriding()) { - if (! $this->hasPane($pane->getName()) && (! $origin || ! $origin->hasPane($pane->getName()))) { - $conn->insert(Pane::TABLE, [ - 'id' => $uuid, - 'home_id' => $this->getUuid(), - 'name' => $pane->getName(), - 'label' => $pane->getTitle(), - 'username' => $user->getUsername(), - 'priority' => $order++ - ]); - } elseif (! $this->hasPane($pane->getName()) || ! $origin || ! $origin->hasPane($pane->getName())) { - $conn->update(Pane::TABLE, [ - 'id' => $uuid, - 'home_id' => $this->getUuid(), - 'label' => $pane->getTitle(), - 'priority' => $pane->getPriority() - ], [ - 'id = ?' => $pane->getUuid(), - 'home_id = ?' => $this->getUuid() - ]); - } else { - // Failed to move the pane! Should have been handled already by the caller - break; - } - - $pane->setUuid($uuid); - } else { - // TODO(TBD) - } - - $pane->setHome($this); - if ($mngPaneDashlets) { - // Those dashboard panes are usually system defaults and go up when - // the user is clicking on the "Use System Defaults" button - $dashlets = $pane->getDashlets(); - $pane->setDashlets([]); - $pane->manageDashlets($dashlets); - } - } - - return $this; - } -} diff --git a/library/Icinga/Web/Navigation/DashboardHomeItem.php b/library/Icinga/Web/Navigation/DashboardHomeItem.php new file mode 100644 index 0000000000..0c3155f47c --- /dev/null +++ b/library/Icinga/Web/Navigation/DashboardHomeItem.php @@ -0,0 +1,42 @@ +getName() !== DashboardHome::DEFAULT_HOME) { + $this->setUrl(Url::fromPath(Dashboard::BASE_ROUTE . '/home', [ + 'home' => $this->getName() + ])); + } + } + + /** + * Get this dashboard home's url + * + * Parent class would always report a default url if $this->url isn't + * set, which we do it on purpose. + * + * @return \Icinga\Web\Url + */ + public function getUrl() + { + return $this->url; + } +} diff --git a/library/Icinga/Web/Widget/SearchDashboard.php b/library/Icinga/Web/Widget/SearchDashboard.php index f6301f0496..1fc097e43d 100644 --- a/library/Icinga/Web/Widget/SearchDashboard.php +++ b/library/Icinga/Web/Widget/SearchDashboard.php @@ -5,7 +5,7 @@ use Icinga\Exception\Http\HttpNotFoundException; use Icinga\Application\Icinga; -use Icinga\Web\Navigation\DashboardHome; +use Icinga\Web\Dashboard\DashboardHome; use Icinga\Web\Url; /** @@ -37,7 +37,6 @@ class SearchDashboard extends \Icinga\Web\Dashboard\Dashboard public function __construct() { $this->searchHome = new DashboardHome(self::SEARCH_HOME); - $this->searchHome->setActive(); } public function getTabs() @@ -72,7 +71,8 @@ public function getActiveHome() */ public function search($searchString = '') { - $pane = $this->searchHome->addPane(self::SEARCH_PANE)->getPane(self::SEARCH_PANE)->setTitle(t('Search')); + $pane = $this->searchHome->createEntry(self::SEARCH_PANE)->getEntry(self::SEARCH_PANE); + $pane->setTitle(t('Search')); $this->activate(self::SEARCH_PANE); $manager = Icinga::app()->getModuleManager(); @@ -83,7 +83,7 @@ public function search($searchString = '') $moduleSearchUrls = $module->getSearchUrls(); if (! empty($moduleSearchUrls)) { if ($searchString === '') { - $pane->addDashlet(t('Ready to search'), 'search/hint'); + $pane->createEntry(t('Ready to search'), 'search/hint'); return $this; } $searchUrls = array_merge($searchUrls, $moduleSearchUrls); @@ -95,8 +95,8 @@ public function search($searchString = '') foreach (array_reverse($searchUrls) as $searchUrl) { $title = $searchUrl->title . ': ' . $searchString; - $pane->addDashlet($title, Url::fromPath($searchUrl->url, array('q' => $searchString))); - $pane->getDashlet($title)->setProgressLabel(t('Searching')); + $pane->createEntry($title, Url::fromPath($searchUrl->url, array('q' => $searchString))); + $pane->getEntry($title)->setProgressLabel(t('Searching')); } return $this; @@ -104,11 +104,11 @@ public function search($searchString = '') protected function assemble() { - if (! $this->searchHome->getPane(self::SEARCH_PANE)->hasDashlets()) { + if ($this->searchHome->getEntry(self::SEARCH_PANE)->hasEntries()) { throw new HttpNotFoundException(t('Page not found')); } - $this->add($this->searchHome->getPane(self::SEARCH_PANE)->getDashlets()); + $this->add($this->searchHome->getEntry(self::SEARCH_PANE)->getEntries()); } /** diff --git a/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php b/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php index ff05fd7391..286d599183 100644 --- a/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php +++ b/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php @@ -35,14 +35,10 @@ public function __construct(array $urlParam = []) public function apply(Tabs $tabs) { $url = Url::fromPath(Dashboard::BASE_ROUTE . '/settings'); - $url = empty($this->urlParam) ? $url : $url->addParams($this->urlParam); - $tabs->add( - 'dashboard_settings', - [ - 'icon' => 'service', - 'url' => (string) $url, - 'priority' => -100 - ] - ); + $url = empty($this->urlParam) ? $url : $url->addParams($this->urlParam); + $tabs->add('dashboard_settings', [ + 'icon' => 'service', + 'url' => (string) $url, + ]); } } diff --git a/public/css/icinga/dashboards.less b/public/css/icinga/dashboards.less index b18c2454bc..0f34137bc3 100644 --- a/public/css/icinga/dashboards.less +++ b/public/css/icinga/dashboards.less @@ -82,7 +82,7 @@ list-style-type: none; label { - width: 49%; + width: 49.5%; margin-bottom: .5em; } @@ -93,24 +93,29 @@ .dashlet-list-item { display: flex; flex-direction: column; - border-radius: .4em; - border: 1px solid @gray-light; background: @gray-lightest; + border: 1px solid @gray-light; + .rounded-corners(); section.caption, h1 { margin-left: .5em; font-weight: 500; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; } h1 { border-bottom: none; + overflow: hidden; + -webkit-line-clamp: 1; } .caption { height: 3em; - overflow-y: auto; font-size: 1.1em; color: @text-color-light; + -webkit-line-clamp: 2; } } @@ -141,16 +146,10 @@ // Add new home button of the dashboard manager .controls .add-home { + .button(); float: right; - outline: none; - cursor: pointer; font-size: 1.2em; font-weight: 500; - padding: .5em 1em; - line-height: normal; - text-decoration: none; - border: 2px solid @icinga-blue; - border-radius: .4em; color: @text-color-on-icinga-blue; background-color: @icinga-blue; } @@ -160,27 +159,27 @@ } .action-link.add-dashboard { - width: 81.5%; - margin-top: .5em; + width: 85%; + margin: .5em 0 0; } .action-link.add-dashlet, .action-link.add-dashboard { + .rounded-corners(); order: 1; font-size: 1.2em; font-weight: 500; padding: .6em 1em; - border-radius: .3em; text-decoration: none; background-color: @low-sat-blue; } .dashboard-list-control .dashlet-item-list { - width: 100%; - display: flex; margin-left: 2.2em; + display: flex; + width: 97.8%; // 100% - 2.2em margin-left .dashlet-list-item { - width: 49.4%; + width: 49.7%; margin-bottom: .5em; background: transparent; } @@ -191,14 +190,14 @@ } .dashboard-list-control { - width: 80%; + width: 85%; h1.collapsible-header { margin-left: 1.5em; } .action-link.add-dashlet { - width: 99.5%; + width: 97.8%; margin-left: 1.8em; } } @@ -213,12 +212,16 @@ border-bottom: none; } - h1.collapsible-header a, h1.dashlet-header a { + h1.collapsible-header a:not(.dashlet-title), h1.dashlet-header a:not(.dashlet-title) { margin-left: .3em; font-weight: normal; color: @icinga-blue; } + h1.dashlet-header a.dashlet-title { + color: @text-color; + } + .dashlets-list-info, .dashboard-list-info { .expand-icon { margin-top: .9em; @@ -230,17 +233,22 @@ } } +// Dashboard home list controls only .home-list-control { width: 100%; - h1.collapsible-header.home { + & > h1.collapsible-header { margin-left: 1.5em; - margin-bottom: 0; + margin-bottom: -1em; + + & > a.disabled { + pointer-events: none; + cursor: default; + } } - .action-link.add-dashboard { - width: 81.6%; - margin-left: 1.3em; + .dashboard-list-control, .action-link.add-dashboard { + margin-left: 1.4em; } .dashboard-list-control { @@ -249,20 +257,13 @@ } .dashlet-item-list { - margin-left: 4em; - } - - .dashlets-list-info { - margin-left: 2.2em; - } - - h1.collapsible-header { - margin-left: 3em; + margin-left: 2em; + width: 98%; // 100% - 2em margin-left } .action-link.add-dashlet { - margin-left: 3.4em; - width: 99.2%; + margin-left: 1.6em; + width: calc((49.74% * 2) - 1.2em); // 2x flex columns - own size } } } @@ -292,17 +293,37 @@ } // Make the submit buttons a bit bigger than the normal size -.modal-form.icinga-controls .control-group.form-controls input[type="submit"] { +.modal-form.icinga-controls .control-group.form-controls input[type="submit"], +.control-group.form-controls .modal-cancel { min-width: 7em; font-size: 1.2em; - margin-right: .4em; + padding: ~"calc(@{vertical-padding} - 2px)" @horizontal-padding; +} + +// Remove and modal cancel buttons +form.icinga-form .control-group.form-controls .remove-button { + .cancel-button(); + margin-right: auto; +} + +.modal-form .form-controls .modal-cancel { + padding: .5em 1em; +} + +.control-group.form-controls .modal-cancel { + background-color: @low-sat-blue; + border: none; } // Drag and drop -.drag-over { +.sortable-chosen { + .box-shadow(); background: fade(@icinga-blue, 20); border: 5px @body-bg-color dashed; - box-shadow: 4px 4px 4px 4px @black; +} + +.home-list-control.sortable-drag { + width: calc(85% + 1.4em); } .drag-active * { diff --git a/public/js/icinga/behavior/dashboards.js b/public/js/icinga/behavior/dashboards.js index 392af2a43a..065c8b06dd 100644 --- a/public/js/icinga/behavior/dashboards.js +++ b/public/js/icinga/behavior/dashboards.js @@ -4,19 +4,26 @@ 'use strict'; + try { + var Sortable = require('icinga/icinga-php-library/vendor/Sortable') + } catch (e) { + console.warn('Unable to provide Sortable. Library not available:', e); + return; + } + /** * Possible type of widgets this behavior is being applied to * * @type {object} */ - const WIDGET_TYPES = { Dashlets : 'Dashlets', Dashboards : 'Dashboards', DashboardHomes : 'Homes' }; + const WIDGET_TYPES = { Dashlet : 'Dashlets', Dashboard : 'Dashboards', DashboardHome : 'Homes' }; Icinga.Behaviors = Icinga.Behaviors || {}; /** * Behavior for the enhanced Icinga Web 2 dashboards * - * @param icinga {Icinga} The current Icinga Object + * @param {Icinga} icinga The current Icinga Object * * @constructor */ @@ -25,281 +32,180 @@ this.icinga = icinga; - /** - * Type of the widget which is currently being sorted - * - * @type {string} - */ - this.sortedWidgetType = WIDGET_TYPES.Dashlets; - - /** - * Widget container id which is currently being dragged - * - * @type {null|string} - */ - this.containerId = null; - - // Register event handlers for drag and drop functionalities - this.on('dragstart', '.widget-sortable', this.onDragStart, this); - this.on('dragover', '.widget-sortable', this.onDragOver, this); - this.on('dragleave', '.widget-sortable', this.onDragLeave, this); - this.on('dragend', '.widget-sortable', this.onDragEnd, this); - this.on('drop', '.widget-sortable', this.onDrop, this); + this.on('rendered', '#main > .container', this.onRendered, this); + this.on('end', '.dashboard-settings, .dashboard-list-control, .dashlet-list-item', this.elementDropped, this); + // This is for the normal dashboard/dashlets view + this.on('end', '.dashboard.content', this.elementDropped, this); }; Dashboard.prototype = new Icinga.EventListener(); /** - * A user tries to drag an element, so make sure it's sortable and setup the procedure + * Get the widget type of the given element * - * @param event {Event} The `dragstart` event triggered when starting to drag the element - * with a mouse click and begin to move it - */ - Dashboard.prototype.onDragStart = function (event) { - let _this = event.data.self; - let $target = $(event.target); - - if (! _this.isDraggable($target)) { - return false; - } - - _this.containerId = $target.attr('id'); - $target.addClass('draggable-element'); - - let $parent = $target.parent()[0]; - // Prevents child elements from being the target of pointer events - $($parent).children('.widget-sortable').addClass('drag-active'); - }; - - /** - * Event handler for drag over - * - * Check that the target el is draggable and isn't the el itself - * which is currently being dragged + * @param {HTMLElement} target * - * @param event {Event} The `drag over` event triggered when dragging over another dashlet + * @returns {null|string} */ - Dashboard.prototype.onDragOver = function (event) { - let $target = $(event.target); - let _this = event.data.self; - - // Moving an element arbitrarily elsewhere isn't allowed - if (! _this.isDraggable($target) || ! _this.isDraggableSiblingOf($target)) { - return false; + Dashboard.prototype.getTypeFor = function (target) { + if (target.matches('.dashboard-settings')) { + return WIDGET_TYPES.DashboardHome; + } else if (target.matches('.dashboard-item-list')) { + return WIDGET_TYPES.Dashboard; + } else if (target.matches('.dashlet-item-list') || target.matches('.dashboard.content')) { + return WIDGET_TYPES.Dashlet; } - // Don't show mouse drop cursor if the target element is the draggable element - if ($target.attr('id') !== _this.containerId) { - event.preventDefault(); - event.stopPropagation(); - - $target.addClass('drag-over'); - } + return null; }; /** - * The element doesn't get dragged over anymore, so just remove the drag-over class + * Set up a request with the reordered widget and post the data to the controller * - * @param event {Event} The `drag leave` event triggered when dragging over a dashlet - * and leaving without dropping the draggable element - */ - Dashboard.prototype.onDragLeave = function (event) { - let $target = $(event.target); - let _this = event.data.self; - - if (! _this.isDraggable($target) || ! _this.isDraggableSiblingOf($target)) { - return false; - } - - $target.removeClass('drag-over'); - }; - - /** - * Remove all class names added dynamically + * @param event * - * @param event {Event} The `drag end` event triggered when the draggable element is released + * @returns {boolean} */ - Dashboard.prototype.onDragEnd = function (event) { - let $target = $(event.target); - let _this = event.data.self; + Dashboard.prototype.elementDropped = function (event) { + let _this = event.data.self, + orgEvt = event.originalEvent, + data = {}; - if (! _this.isDraggable($target) || ! _this.isDraggableSiblingOf($target)) { + if (orgEvt.to === orgEvt.from && orgEvt.newIndex === orgEvt.oldIndex) { return false; } - $target.removeClass('draggable-element'); - $target.removeClass('drag-over'); - - let $parent = $target.parent()[0]; - // The draggable is now released, so we have to remove the class to enable the pointer events again - $($parent).children('.widget-sortable').removeClass('drag-active'); - }; + let item = orgEvt.item; + switch (_this.getTypeFor(orgEvt.to)) { + case WIDGET_TYPES.DashboardHome: { + let home = item.dataset.icingaHome; + data[home] = orgEvt.newIndex; + break; + } + case WIDGET_TYPES.Dashboard: { + let pane = item.dataset.icingaPane, + home = orgEvt.to.closest('.home-list-control').dataset.icingaHome; + if (orgEvt.to !== orgEvt.from) { + data.originals = { + originalHome : orgEvt.from.closest('.home-list-control').dataset.icingaHome, + originalPane : pane + }; + } - /** - * Event handler for on drop action - * - * @param event {Event} The `ondrop` event triggered when the dashlet has been dropped - */ - Dashboard.prototype.onDrop = function (event) { - let $target = $(event.target); - let _this = event.data.self; + data[home] = { [pane] : orgEvt.newIndex }; + break; + } + case WIDGET_TYPES.Dashlet: { + let dashlet = item.dataset.icingaDashlet, + pane, + home; + + if (orgEvt.to.matches('.dashboard.content')) { + let parentData = orgEvt.to.dataset.icingaPane.split('|', 2); + home = parentData.shift(); + pane = parentData.shift(); + + data.redirectPath = 'dashboards'; + } else { // Dashboard manager view + let parent = orgEvt.to.closest('.dashboard-list-control'); + pane = parent.dataset.icingaPane; + home = parent.closest('.home-list-control').dataset.icingaHome; + + if (orgEvt.to !== orgEvt.from) { + let parent = orgEvt.from.closest('.dashboard-list-control'); + data.originals = { + originalHome : parent.closest('.home-list-control').dataset.icingaHome, + originalPane : parent.dataset.icingaPane + } + } + } - // Don't allow to drop an element arbitrarily elsewhere - if (! _this.isDraggable($target) || ! _this.isDraggableSiblingOf($target)) { - return false; + dashlet = { [dashlet] : orgEvt.newIndex }; + data[home] = { [pane] : dashlet }; + } } - // Prevent default behaviors to allow the drop event - event.preventDefault(); - event.stopPropagation(); - - const draggable = $target.parent().children('#' + _this.containerId); - - // If the target element has been located before the draggable element, - // insert the draggable before the target element otherwise after it - if ($target.nextAll().filter(draggable).length) { - draggable.insertBefore($target); - } else { - draggable.insertAfter($target); - } + if (Object.keys(data).length) { + data.Type = _this.getTypeFor(orgEvt.to); + if (! data.hasOwnProperty('originals')) { + data.originals = null; + } - // Draggable element is now dropped, so drag-over class must also be removed - $target.removeClass('drag-over'); + if (! data.hasOwnProperty('redirectPath')) { + data.redirectPath = 'dashboards/settings'; + } - if ($target.data('icinga-pane')) { - _this.sortedWidgetType = WIDGET_TYPES.Dashboards; - } else if ($target.data('icinga-home')) { - _this.sortedWidgetType = WIDGET_TYPES.DashboardHomes; + data = { dashboardData : JSON.stringify(data) }; + let url = _this.icinga.config.baseUrl + '/dashboards/reorder-widgets'; + _this.icinga.loader.loadUrl(url, $('#col1'), data, 'post'); } - - _this.sendReorderedWidgets($target); }; /** - * Get whether the given element is draggable + * Get whether the given element is a valid target of the drag & drop events * - * @param $target {jQuery} + * @param to + * @param from + * @param item + * @param event * * @returns {boolean} */ - Dashboard.prototype.isDraggable = function ($target) { - return $target.attr('draggable'); - }; + Dashboard.prototype.isValid = function (to, from, item, event) { + if (typeof from.options.group === 'undefined' || typeof to.options.group === 'undefined') { + return false; + } - /** - * Get whether the given element is sibling of the element currently being dragged - * - * @param $target {jQuery} - * - * @returns {number} - */ - Dashboard.prototype.isDraggableSiblingOf = function ($target) { - return $target.parent().children('#' + this.containerId).length; + return from.options.group.name === to.options.group.name; }; - /** - * Set up a request with the reordered containers and post the data to the controller - * - * @param $target {jQuery} - */ - Dashboard.prototype.sendReorderedWidgets = function ($target) { - let _this = this, - data = {}; - - switch (_this.sortedWidgetType) { - case WIDGET_TYPES.DashboardHomes: { - let $homes = []; - $target.parent().children('.home-list-control.widget-sortable').each(function () { - let home = $(this); - if (typeof home.data('icinga-home') === 'undefined') { - _this.icinga.logger.error( - '[Dashboards] Dashboard home widget has no "icingaHome" data attribute registered: ', - home[0] - ); - return; - } - - $homes.push(home.data('icinga-home')); - }); - - data = { ...$homes }; - break; - } - case WIDGET_TYPES.Dashboards: { - let $home, $panes = []; - $target.parent().children('.dashboard-list-control.widget-sortable').each(function () { - let pane = $(this); - if (typeof pane.data('icinga-pane') === 'undefined') { - _this.icinga.logger.error( - '[Dashboards] Dashboard widget has no "icingaPane" data attribute registered: ', - pane[0] - ); - return; - } - - pane = pane.data('icinga-pane').split('|', 2); - if (! $home) { - $home = pane.shift(); - } - - $panes.push(pane.pop()); - }); - - data[$home] = $panes; - break; - } - case WIDGET_TYPES.Dashlets: { - let $home, $pane, $dashlets = []; - $target.parent().children('.widget-sortable').each(function () { - let dashlet = $(this); - if (typeof dashlet.data('icinga-dashlet') === 'undefined') { - _this.icinga.logger.error( - '[Dashboards] Dashlet widget has no "icingaDashlet" data attribute registered: ', - dashlet[0] - ); - return; - } - - if (! $home && ! $pane) { - let pane = dashlet.parent(); - if (typeof pane.data('icinga-pane') === 'undefined') { - // Nested parents - pane = pane.parent(); - if (typeof pane.data('icinga-pane') === 'undefined') { - _this.icinga.logger.error( - '[Dashboards] Dashlet parent widget has no "icingaPane" data attribute registered: ', - pane[0] - ); - return; - } - } - - pane = pane.data('icinga-pane').split('|', 2); - $home = pane.shift(); - $pane = pane.shift(); + Dashboard.prototype.onRendered = function (e) { + let _this = e.data.self; + $(e.target).find('.dashboard-settings, .dashboard.content, .dashboard-item-list, .dashlet-item-list').each(function () { + let groupName = _this.getTypeFor(this), + draggable, + handle; + + switch (groupName) { + case WIDGET_TYPES.DashboardHome: { + groupName = WIDGET_TYPES.DashboardHome; + draggable = '.home-list-control'; + handle = '.home-list-control > h1'; + break; + } + case WIDGET_TYPES.Dashboard: { + groupName = WIDGET_TYPES.Dashboard; + draggable = '.dashboard-list-control'; + handle = '.dashboard-list-control > h1' + break; + } + case WIDGET_TYPES.Dashlet: { + groupName = WIDGET_TYPES.Dashlet; + if (this.matches('.dashboard.content')) { + draggable = '> .container'; + } else { + draggable = '.dashlet-list-item'; } - $dashlets.push(dashlet.data('icinga-dashlet')); - }); - - if ($home && $pane) { - data[$home] = { [$pane] : $dashlets }; + handle = draggable; } } - } - if (Object.keys(data).length) { - data.Type = _this.sortedWidgetType; + let options = { + scroll : true, + invertSwap : true, + dataIdAttr : 'id', + direction : 'vertical', + draggable : draggable, + handle : handle, + group : { + name : groupName, + put : _this['isValid'], + } + }; - $.ajax({ - context : _this, - type : 'post', - url : _this.icinga.config.baseUrl + '/dashboards/reorder-widgets', - headers : { 'Accept' : 'application/json' }, - contentType : 'application/json', - data : JSON.stringify(data), - }); - } + Sortable.create(this, options); + }); }; Icinga.Behaviors.Dashboard = Dashboard; From 96c3b316864f8692c50fbb3193a44a79399f4615 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 1 Apr 2022 16:40:53 +0200 Subject: [PATCH 035/106] Delay sorting events for 100ms --- application/forms/Dashboard/HomePaneForm.php | 2 +- .../Web/Dashboard/Common/DashboardManager.php | 35 +++++++++++-------- .../Web/Dashboard/Common/ItemListControl.php | 3 +- .../Dashboard/ItemList/DashboardHomeList.php | 3 +- public/css/icinga/dashboards.less | 5 --- public/js/icinga/behavior/dashboards.js | 1 + 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/application/forms/Dashboard/HomePaneForm.php b/application/forms/Dashboard/HomePaneForm.php index 088f737fb8..ff2cb296ac 100644 --- a/application/forms/Dashboard/HomePaneForm.php +++ b/application/forms/Dashboard/HomePaneForm.php @@ -19,6 +19,7 @@ protected function assemble() $buttonLabel = t('Update Home'); $removeButtonLabel = t('Remove Home'); + $activeHome = $this->dashboard->getActiveHome(); $requestUrl = Url::fromRequest(); $removeTargetUrl = (clone $requestUrl)->setPath(Dashboard::BASE_ROUTE . '/remove-home'); @@ -33,7 +34,6 @@ protected function assemble() $removeTargetUrl = (clone $requestUrl)->setPath(Dashboard::BASE_ROUTE . '/remove-pane'); $homes = $this->dashboard->getEntryKeyTitleArr(); - $activeHome = $this->dashboard->getActiveHome(); $populatedHome = $this->getPopulatedValue('home', $activeHome->getName()); $this->addElement('select', 'home', [ diff --git a/library/Icinga/Web/Dashboard/Common/DashboardManager.php b/library/Icinga/Web/Dashboard/Common/DashboardManager.php index 273aec0be5..d4a2e898c5 100644 --- a/library/Icinga/Web/Dashboard/Common/DashboardManager.php +++ b/library/Icinga/Web/Dashboard/Common/DashboardManager.php @@ -159,21 +159,28 @@ public function removeEntry($home) public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) { $conn = self::getConn(); - $home = $entry; - if (! $this->hasEntry($home->getName())) { - $priority = $home->getName() === DashboardHome::DEFAULT_HOME ? 0 : count($this->getEntries()); - $conn->insert(DashboardHome::TABLE, [ - 'name' => $home->getName(), - 'label' => $home->getTitle(), - 'username' => self::getUser()->getUsername(), - // highest priority is 0, so count($entries) are always lowest prio + 1 - 'priority' => $priority, - 'type' => $home->getType() !== Dashboard::SYSTEM ? $home->getType() : Dashboard::PRIVATE_DS - ]); + $homes = is_array($entry) ? $entry : [$entry]; - $home->setUuid($conn->lastInsertId()); - } elseif ($home->getName() !== DashboardHome::DEFAULT_HOME) { - $conn->update(DashboardHome::TABLE, ['label' => $home->getTitle()], ['id = ?' => $home->getUuid()]); + /** @var DashboardHome $home */ + foreach ($homes as $home) { + if (! $this->hasEntry($home->getName())) { + $priority = $home->getName() === DashboardHome::DEFAULT_HOME ? 0 : count($this->getEntries()); + $conn->insert(DashboardHome::TABLE, [ + 'name' => $home->getName(), + 'label' => $home->getTitle(), + 'username' => self::getUser()->getUsername(), + // highest priority is 0, so count($entries) are always lowest prio + 1 + 'priority' => $priority, + 'type' => $home->getType() !== Dashboard::SYSTEM ? $home->getType() : Dashboard::PRIVATE_DS + ]); + + $home->setUuid($conn->lastInsertId()); + } else { + $conn->update(DashboardHome::TABLE, [ + 'label' => $home->getTitle(), + 'priority' => $home->getPriority() + ], ['id = ?' => $home->getUuid()]); + } } return $this; diff --git a/library/Icinga/Web/Dashboard/Common/ItemListControl.php b/library/Icinga/Web/Dashboard/Common/ItemListControl.php index 9d3f22ff40..afd11e7413 100644 --- a/library/Icinga/Web/Dashboard/Common/ItemListControl.php +++ b/library/Icinga/Web/Dashboard/Common/ItemListControl.php @@ -50,11 +50,10 @@ protected abstract function createItemList(); * * @return void */ - protected function assembleHeader(Url $url, $header, $disable = false) + protected function assembleHeader(Url $url, $header) { $header = HtmlElement::create('h1', ['class' => 'collapsible-header'], $header); $header->addHtml(new Link(t('Edit'), $url, [ - 'class' => $disable ? 'disabled' : null, 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ])); diff --git a/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php b/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php index d1e05a1889..c9c0b35dce 100644 --- a/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php +++ b/library/Icinga/Web/Dashboard/ItemList/DashboardHomeList.php @@ -45,8 +45,7 @@ protected function createItemList() $url = Url::fromPath(Dashboard::BASE_ROUTE . '/edit-home') ->setParams(['home' => $this->home->getName()]); - $disable = $this->home->getName() === DashboardHome::DEFAULT_HOME; - $this->assembleHeader($url, $this->home->getTitle(), $disable); + $this->assembleHeader($url, $this->home->getTitle()); $list = HtmlElement::create('ul', ['class' => 'dashboard-item-list']); // List all dashboard panes diff --git a/public/css/icinga/dashboards.less b/public/css/icinga/dashboards.less index 0f34137bc3..cc7e7adb1a 100644 --- a/public/css/icinga/dashboards.less +++ b/public/css/icinga/dashboards.less @@ -240,11 +240,6 @@ & > h1.collapsible-header { margin-left: 1.5em; margin-bottom: -1em; - - & > a.disabled { - pointer-events: none; - cursor: default; - } } .dashboard-list-control, .action-link.add-dashboard { diff --git a/public/js/icinga/behavior/dashboards.js b/public/js/icinga/behavior/dashboards.js index 065c8b06dd..e745d2d24a 100644 --- a/public/js/icinga/behavior/dashboards.js +++ b/public/js/icinga/behavior/dashboards.js @@ -194,6 +194,7 @@ let options = { scroll : true, invertSwap : true, + delay : 100, dataIdAttr : 'id', direction : 'vertical', draggable : draggable, From d903b902fc99687c23ac42f49bd2ebb4b60aa366 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 1 Apr 2022 16:57:59 +0200 Subject: [PATCH 036/106] JS: Replace jquery.find() with pure js --- public/js/icinga/behavior/dashboards.js | 83 +++++++++++++------------ 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/public/js/icinga/behavior/dashboards.js b/public/js/icinga/behavior/dashboards.js index e745d2d24a..f070d98858 100644 --- a/public/js/icinga/behavior/dashboards.js +++ b/public/js/icinga/behavior/dashboards.js @@ -161,52 +161,53 @@ Dashboard.prototype.onRendered = function (e) { let _this = e.data.self; - $(e.target).find('.dashboard-settings, .dashboard.content, .dashboard-item-list, .dashlet-item-list').each(function () { - let groupName = _this.getTypeFor(this), - draggable, - handle; - - switch (groupName) { - case WIDGET_TYPES.DashboardHome: { - groupName = WIDGET_TYPES.DashboardHome; - draggable = '.home-list-control'; - handle = '.home-list-control > h1'; - break; - } - case WIDGET_TYPES.Dashboard: { - groupName = WIDGET_TYPES.Dashboard; - draggable = '.dashboard-list-control'; - handle = '.dashboard-list-control > h1' - break; - } - case WIDGET_TYPES.Dashlet: { - groupName = WIDGET_TYPES.Dashlet; - if (this.matches('.dashboard.content')) { - draggable = '> .container'; - } else { - draggable = '.dashlet-list-item'; + e.target.querySelectorAll('.dashboard-settings, .dashboard.content, .dashboard-item-list, .dashlet-item-list') + .forEach(sortable => { + let groupName = _this.getTypeFor(sortable), + draggable, + handle; + + switch (groupName) { + case WIDGET_TYPES.DashboardHome: { + groupName = WIDGET_TYPES.DashboardHome; + draggable = '.home-list-control'; + handle = '.home-list-control > h1'; + break; } + case WIDGET_TYPES.Dashboard: { + groupName = WIDGET_TYPES.Dashboard; + draggable = '.dashboard-list-control'; + handle = '.dashboard-list-control > h1' + break; + } + case WIDGET_TYPES.Dashlet: { + groupName = WIDGET_TYPES.Dashlet; + if (sortable.matches('.dashboard.content')) { + draggable = '> .container'; + } else { + draggable = '.dashlet-list-item'; + } - handle = draggable; + handle = draggable; + } } - } - let options = { - scroll : true, - invertSwap : true, - delay : 100, - dataIdAttr : 'id', - direction : 'vertical', - draggable : draggable, - handle : handle, - group : { - name : groupName, - put : _this['isValid'], - } - }; + let options = { + scroll : true, + invertSwap : true, + delay : 100, + dataIdAttr : 'id', + direction : 'vertical', + draggable : draggable, + handle : handle, + group : { + name : groupName, + put : _this['isValid'], + } + }; - Sortable.create(this, options); - }); + Sortable.create(sortable, options); + }); }; Icinga.Behaviors.Dashboard = Dashboard; From ac3f3a66ea16397b50e07b46049f888c33cbfe21 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 4 Apr 2022 09:44:44 +0200 Subject: [PATCH 037/106] Module: Restore removed imports --- library/Icinga/Application/Modules/Module.php | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 67d24ba4f5..b96241be21 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -15,6 +15,7 @@ use Icinga\Util\File; use Icinga\Web\Dashboard; use Icinga\Web\Navigation\Navigation; +use Icinga\Web\Widget; use ipl\I18n\GettextTranslator; use ipl\I18n\StaticTranslator; use ipl\I18n\Translation; From 654d9252d41be9b8f48f2d4a49396adfc0dd8d83 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 4 Apr 2022 09:46:03 +0200 Subject: [PATCH 038/106] Pane: Utilize query with dashboard when selecting dashlets --- library/Icinga/Web/Dashboard/Pane.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/library/Icinga/Web/Dashboard/Pane.php b/library/Icinga/Web/Dashboard/Pane.php index 98e679ba5e..d41a62677a 100644 --- a/library/Icinga/Web/Dashboard/Pane.php +++ b/library/Icinga/Web/Dashboard/Pane.php @@ -25,13 +25,6 @@ class Pane extends BaseDashboard implements Sortable, OverridingWidget const TABLE = 'dashboard'; - /** - * An array of @see Dashlet that are displayed in this pane - * - * @var array - */ - protected $dashlets = []; - /** * Whether this widget overrides another widget * @@ -184,7 +177,9 @@ public function removeEntry($dashlet) public function loadDashboardEntries($name = '') { - $dashlets = Model\Dashlet::on(Dashboard::getConn())->with('module_dashlet'); + $dashlets = Model\Dashlet::on(Dashboard::getConn()) + ->utilize('dashboard') + ->with('module_dashlet'); $dashlets->filter(Filter::equal('dashboard_id', $this->getUuid())); $this->setEntries([]); From 2913a4fd44b65a25f5ea9ee9265b70a3ad30166b Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 4 Apr 2022 09:46:44 +0200 Subject: [PATCH 039/106] Fix some logical inconsistencies --- application/views/scripts/layout/menu.phtml | 2 +- library/Icinga/Web/Widget/SearchDashboard.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/views/scripts/layout/menu.phtml b/application/views/scripts/layout/menu.phtml index 5ebdaf623c..c50a6d2e5a 100644 --- a/application/views/scripts/layout/menu.phtml +++ b/application/views/scripts/layout/menu.phtml @@ -5,7 +5,7 @@ use Icinga\Web\Widget\SearchDashboard; $searchDashboard = new SearchDashboard(); $searchDashboard->setUser($this->Auth()->getUser()); -if (! $searchDashboard->search('dummy')->getActiveHome()->getEntry('search')->hasEntries()): ?> +if ($searchDashboard->search('dummy')->getActiveHome()->getEntry('search')->hasEntries()): ?> searchHome->getEntry(self::SEARCH_PANE)->hasEntries()) { + if (! $this->searchHome->getEntry(self::SEARCH_PANE)->hasEntries()) { throw new HttpNotFoundException(t('Page not found')); } - $this->add($this->searchHome->getEntry(self::SEARCH_PANE)->getEntries()); + /** @var Dashlet $dashlet */ + foreach ($this->searchHome->getEntry(self::SEARCH_PANE)->getEntries() as $dashlet) { + $this->addHtml($dashlet->getHtml()); + } } /** From 0a94adcaca955555044d0876d65d5286e408802f Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 4 Apr 2022 12:20:54 +0200 Subject: [PATCH 040/106] Adjust param description & extract home properties from a homeitem properly --- .../Web/Dashboard/Common/BaseDashboard.php | 2 +- .../Web/Dashboard/Common/DashboardEntry.php | 9 +++++---- .../Web/Dashboard/Common/DashboardManager.php | 2 +- library/Icinga/Web/Dashboard/DashboardHome.php | 18 +++++++++++++++--- library/Icinga/Web/Dashboard/Pane.php | 4 ++-- library/Icinga/Web/HomeMenu.php | 5 +---- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/library/Icinga/Web/Dashboard/Common/BaseDashboard.php b/library/Icinga/Web/Dashboard/Common/BaseDashboard.php index de39d2da7b..abe3b9fe20 100644 --- a/library/Icinga/Web/Dashboard/Common/BaseDashboard.php +++ b/library/Icinga/Web/Dashboard/Common/BaseDashboard.php @@ -253,7 +253,7 @@ public function removeEntries(array $entries = []) { } - public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) + public function manageEntry($entry, BaseDashboard $origin = null, $manageRecursive = false) { } diff --git a/library/Icinga/Web/Dashboard/Common/DashboardEntry.php b/library/Icinga/Web/Dashboard/Common/DashboardEntry.php index 0d5ab139f4..92b891d9f4 100644 --- a/library/Icinga/Web/Dashboard/Common/DashboardEntry.php +++ b/library/Icinga/Web/Dashboard/Common/DashboardEntry.php @@ -105,13 +105,14 @@ public function removeEntries(array $entries = []); * move pane(s)|dashlet(s) from another to this widget you have to also provide the origin from which the * given entry(ies) originated * - * @param BaseDashboard|BaseDashboard[] $entry - * @param ?BaseDashboard $origin - * @param bool $updateChildEntries + * @param BaseDashboard|BaseDashboard[] $entry The actual dashboard entry to be managed + * @param ?BaseDashboard $origin The original widget from which the given entry originates + * @param bool $manageRecursive Whether the given entry should be managed recursively e.g if the given entry + * is a Pane type, all its dashlets can be managed recursively * * @return $this */ - public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false); + public function manageEntry($entry, BaseDashboard $origin = null, $manageRecursive = false); /** * Load all the assigned entries to this widget diff --git a/library/Icinga/Web/Dashboard/Common/DashboardManager.php b/library/Icinga/Web/Dashboard/Common/DashboardManager.php index d4a2e898c5..b2e73170a4 100644 --- a/library/Icinga/Web/Dashboard/Common/DashboardManager.php +++ b/library/Icinga/Web/Dashboard/Common/DashboardManager.php @@ -156,7 +156,7 @@ public function removeEntry($home) return $this; } - public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) + public function manageEntry($entry, BaseDashboard $origin = null, $manageRecursive = false) { $conn = self::getConn(); $homes = is_array($entry) ? $entry : [$entry]; diff --git a/library/Icinga/Web/Dashboard/DashboardHome.php b/library/Icinga/Web/Dashboard/DashboardHome.php index fc7cf97b7b..d3adf3c104 100644 --- a/library/Icinga/Web/Dashboard/DashboardHome.php +++ b/library/Icinga/Web/Dashboard/DashboardHome.php @@ -38,6 +38,11 @@ class DashboardHome extends BaseDashboard implements Sortable */ protected $type = Dashboard::SYSTEM; + /** + * A flag whether this home has been activated + * + * @var bool + */ protected $active; /** @@ -49,7 +54,14 @@ class DashboardHome extends BaseDashboard implements Sortable */ public static function create(DashboardHomeItem $homeItem) { - return new self($homeItem->getName(), $homeItem->getAttributes()); + $self = new self($homeItem->getName()); + $self + ->setTitle($homeItem->getLabel()) + ->setPriority($homeItem->getPriority()) + ->setType($homeItem->getAttribute('type')) + ->setUuid($homeItem->getAttribute('uuid')); + + return $self; } /** @@ -160,7 +172,7 @@ public function createEntry($name, $url = null) return $this; } - public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) + public function manageEntry($entry, BaseDashboard $origin = null, $manageRecursive = false) { $user = Dashboard::getUser(); $conn = Dashboard::getConn(); @@ -218,7 +230,7 @@ public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEn $pane->setUuid($uuid); } - if ($updateChildEntries) { + if ($manageRecursive) { // Those dashboard panes are usually system defaults and go up when // the user is clicking on the "Use System Defaults" button $dashlets = $pane->getEntries(); diff --git a/library/Icinga/Web/Dashboard/Pane.php b/library/Icinga/Web/Dashboard/Pane.php index d41a62677a..1b26266483 100644 --- a/library/Icinga/Web/Dashboard/Pane.php +++ b/library/Icinga/Web/Dashboard/Pane.php @@ -187,7 +187,7 @@ public function loadDashboardEntries($name = '') $newDashlet = new Dashlet($dashlet->name, $dashlet->url, $this); $newDashlet->fromArray([ 'uuid' => $dashlet->id, - 'title' => t($dashlet->label), + 'title' => $dashlet->label, 'priority' => $dashlet->priority, 'pane' => $this, 'description' => $dashlet->module_dashlet->description @@ -199,7 +199,7 @@ public function loadDashboardEntries($name = '') return $this; } - public function manageEntry($entry, BaseDashboard $origin = null, $updateChildEntries = false) + public function manageEntry($entry, BaseDashboard $origin = null, $manageRecursive = false) { if ($origin && ! $origin instanceof Pane) { throw new \InvalidArgumentException(sprintf( diff --git a/library/Icinga/Web/HomeMenu.php b/library/Icinga/Web/HomeMenu.php index a5a202ca29..7bd640645b 100644 --- a/library/Icinga/Web/HomeMenu.php +++ b/library/Icinga/Web/HomeMenu.php @@ -55,10 +55,7 @@ public function loadHomes() continue; } - $home = DashboardHome::create($child); - $home->setTitle($child->getLabel()); - - $homes[$child->getName()] = $home; + $homes[$child->getName()] = DashboardHome::create($child); } return $homes; From 85a53764f69785ade86dec8c3e22eadeafc125be Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Wed, 6 Apr 2022 15:42:06 +0200 Subject: [PATCH 041/106] Remove some unused interfaces/attrs --- .../Web/Dashboard/Common/OverridingWidget.php | 24 ------ .../Icinga/Web/Dashboard/DashboardHome.php | 74 +++++++++---------- library/Icinga/Web/Dashboard/Pane.php | 51 +------------ 3 files changed, 36 insertions(+), 113 deletions(-) delete mode 100644 library/Icinga/Web/Dashboard/Common/OverridingWidget.php diff --git a/library/Icinga/Web/Dashboard/Common/OverridingWidget.php b/library/Icinga/Web/Dashboard/Common/OverridingWidget.php deleted file mode 100644 index d4c6b5ec4c..0000000000 --- a/library/Icinga/Web/Dashboard/Common/OverridingWidget.php +++ /dev/null @@ -1,24 +0,0 @@ -getEntry($pane); - if (! $pane->isOverriding()) { - $pane->removeEntries(); + $pane->removeEntries(); - Dashboard::getConn()->delete(Pane::TABLE, [ - 'id = ?' => $pane->getUuid(), - 'home_id = ?' => $this->getUuid() - ]); - } + Dashboard::getConn()->delete(Pane::TABLE, [ + 'id = ?' => $pane->getUuid(), + 'home_id = ?' => $this->getUuid() + ]); return $this; } @@ -192,44 +190,42 @@ public function manageEntry($entry, BaseDashboard $origin = null, $manageRecursi /** @var Pane $pane */ foreach ($panes as $pane) { $uuid = Dashboard::getSHA1($user->getUsername() . $this->getName() . $pane->getName()); - if (! $pane->isOverriding()) { - if (! $this->hasEntry($pane->getName()) && (! $origin || ! $origin->hasEntry($pane->getName()))) { - $conn->insert(Pane::TABLE, [ - 'id' => $uuid, - 'home_id' => $this->getUuid(), - 'name' => $pane->getName(), - 'label' => $pane->getTitle(), - 'username' => $user->getUsername(), - 'priority' => $order++ - ]); - } elseif (! $this->hasEntry($pane->getName()) || ! $origin || ! $origin->hasEntry($pane->getName())) { + if (! $this->hasEntry($pane->getName()) && (! $origin || ! $origin->hasEntry($pane->getName()))) { + $conn->insert(Pane::TABLE, [ + 'id' => $uuid, + 'home_id' => $this->getUuid(), + 'name' => $pane->getName(), + 'label' => $pane->getTitle(), + 'username' => $user->getUsername(), + 'priority' => $order++ + ]); + } elseif (! $this->hasEntry($pane->getName()) || ! $origin || ! $origin->hasEntry($pane->getName())) { + $filterCondition = [ + 'id = ?' => $pane->getUuid(), + 'home_id = ?' => $this->getUuid() + ]; + + if ($origin && $origin->hasEntry($pane->getName())) { $filterCondition = [ - 'id = ?' => $pane->getUuid(), - 'home_id = ?' => $this->getUuid() + 'id = ?' => $origin->getEntry($pane->getName())->getUuid(), + 'home_id = ?' => $origin->getUuid() ]; - - if ($origin && $origin->hasEntry($pane->getName())) { - $filterCondition = [ - 'id = ?' => $origin->getEntry($pane->getName())->getUuid(), - 'home_id = ?' => $origin->getUuid() - ]; - } - - $conn->update(Pane::TABLE, [ - 'id' => $uuid, - 'home_id' => $this->getUuid(), - 'label' => $pane->getTitle(), - 'priority' => $pane->getPriority() - ], $filterCondition); - } else { - // Failed to move the pane! Should have been handled already by the caller - break; } - $pane->setHome($this); - $pane->setUuid($uuid); + $conn->update(Pane::TABLE, [ + 'id' => $uuid, + 'home_id' => $this->getUuid(), + 'label' => $pane->getTitle(), + 'priority' => $pane->getPriority() + ], $filterCondition); + } else { + // Failed to move the pane! Should have been handled already by the caller + break; } + $pane->setHome($this); + $pane->setUuid($uuid); + if ($manageRecursive) { // Those dashboard panes are usually system defaults and go up when // the user is clicking on the "Use System Defaults" button diff --git a/library/Icinga/Web/Dashboard/Pane.php b/library/Icinga/Web/Dashboard/Pane.php index 1b26266483..cfdde08d89 100644 --- a/library/Icinga/Web/Dashboard/Pane.php +++ b/library/Icinga/Web/Dashboard/Pane.php @@ -9,7 +9,6 @@ use Icinga\Exception\ConfigurationError; use Icinga\Model; use Icinga\Web\Dashboard\Common\DashboardControls; -use Icinga\Web\Dashboard\Common\OverridingWidget; use Icinga\Web\Dashboard\Common\Sortable; use ipl\Stdlib\Filter; use ipl\Web\Url; @@ -19,26 +18,12 @@ /** * A pane, displaying different Dashboard dashlets */ -class Pane extends BaseDashboard implements Sortable, OverridingWidget +class Pane extends BaseDashboard implements Sortable { use DashboardControls; const TABLE = 'dashboard'; - /** - * Whether this widget overrides another widget - * - * @var bool - */ - protected $override; - - /** - * Number of users who have subscribed to this pane if (public) - * - * @var int - */ - protected $acceptance; - /** * A dashboard home this pane is a part of * @@ -46,40 +31,6 @@ class Pane extends BaseDashboard implements Sortable, OverridingWidget */ protected $home; - public function override(bool $override) - { - $this->override = $override; - - return $this; - } - - public function isOverriding() - { - return $this->override; - } - - /** - * Set the number of users who have subscribed to this pane if (public) - * - * @param int $acceptance - */ - public function setAcceptance($acceptance) - { - $this->acceptance = $acceptance; - - return $this; - } - - /** - * Get the number of users who have subscribed to this pane if (public) - * - * @return int - */ - public function getAcceptance() - { - return $this->acceptance; - } - /** * Get the dashboard home this pane is a part of * From e5f501dae34367a03c5ee37ca4f60c7ffc4a0930 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Wed, 6 Apr 2022 16:46:22 +0200 Subject: [PATCH 042/106] Remove some unnecessary classes --- library/Icinga/Model/DashboardOverride.php | 53 ------------------ library/Icinga/Web/HomeMenu.php | 63 ---------------------- 2 files changed, 116 deletions(-) delete mode 100644 library/Icinga/Model/DashboardOverride.php delete mode 100644 library/Icinga/Web/HomeMenu.php diff --git a/library/Icinga/Model/DashboardOverride.php b/library/Icinga/Model/DashboardOverride.php deleted file mode 100644 index d498539ddf..0000000000 --- a/library/Icinga/Model/DashboardOverride.php +++ /dev/null @@ -1,53 +0,0 @@ - new Expression('0') - ]; - } - - public function getMetaData() - { - return ['priority' => t('Dashboard Priority Order')]; - } - - public function getSearchColumns() - { - return ['name']; - } - - public function getDefaultSort() - { - return 'dashboard.name'; - } - - public function createRelations(Relations $relations) - { - $relations->belongsTo('dashboard', Pane::class); - } -} diff --git a/library/Icinga/Web/HomeMenu.php b/library/Icinga/Web/HomeMenu.php deleted file mode 100644 index 7bd640645b..0000000000 --- a/library/Icinga/Web/HomeMenu.php +++ /dev/null @@ -1,63 +0,0 @@ -initHome(); - } - - public function initHome() - { - $user = Dashboard::getUser(); - $dashboardItem = $this->getItem('dashboard'); - - $homes = Home::on(Dashboard::getConn()); - $homes->filter(Filter::equal('username', $user->getUsername())); - - foreach ($homes as $home) { - $dashboardHome = new DashboardHomeItem($home->name, [ - 'uuid' => $home->id, - 'label' => t($home->label), - 'priority' => $home->priority, - 'type' => $home->type, - ]); - - $dashboardItem->addChild($dashboardHome); - } - } - - /** - * Load dashboard homes form the navigation menu - * - * @return DashboardHome[] - */ - public function loadHomes() - { - $homes = []; - foreach ($this->getItem('dashboard')->getChildren() as $child) { - if (! $child instanceof DashboardHomeItem) { - continue; - } - - $homes[$child->getName()] = DashboardHome::create($child); - } - - return $homes; - } -} From 52809312d2cba6f042ca036668e117bda8b31adc Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Wed, 6 Apr 2022 19:44:33 +0200 Subject: [PATCH 043/106] dashboard.js: Use js class syntax --- public/js/icinga/behavior/dashboards.js | 329 +++++++++++------------- 1 file changed, 152 insertions(+), 177 deletions(-) diff --git a/public/js/icinga/behavior/dashboards.js b/public/js/icinga/behavior/dashboards.js index f070d98858..7d87b9f733 100644 --- a/public/js/icinga/behavior/dashboards.js +++ b/public/js/icinga/behavior/dashboards.js @@ -4,211 +4,186 @@ 'use strict'; - try { - var Sortable = require('icinga/icinga-php-library/vendor/Sortable') - } catch (e) { - console.warn('Unable to provide Sortable. Library not available:', e); - return; - } - - /** - * Possible type of widgets this behavior is being applied to - * - * @type {object} - */ - const WIDGET_TYPES = { Dashlet : 'Dashlets', Dashboard : 'Dashboards', DashboardHome : 'Homes' }; - - Icinga.Behaviors = Icinga.Behaviors || {}; - /** * Behavior for the enhanced Icinga Web 2 dashboards * * @param {Icinga} icinga The current Icinga Object - * - * @constructor */ - var Dashboard = function (icinga) { - Icinga.EventListener.call(this, icinga); - - this.icinga = icinga; - - this.on('rendered', '#main > .container', this.onRendered, this); - this.on('end', '.dashboard-settings, .dashboard-list-control, .dashlet-list-item', this.elementDropped, this); - // This is for the normal dashboard/dashlets view - this.on('end', '.dashboard.content', this.elementDropped, this); - }; - - Dashboard.prototype = new Icinga.EventListener(); + class Dashboard extends Icinga.EventListener { + constructor(icinga) { + super(icinga); + + try { + this.Sortable = require('icinga/icinga-php-library/vendor/Sortable'); + } catch (e) { + console.warn('Unable to provide Sortable. Library not available:', e); + return; + } - /** - * Get the widget type of the given element - * - * @param {HTMLElement} target - * - * @returns {null|string} - */ - Dashboard.prototype.getTypeFor = function (target) { - if (target.matches('.dashboard-settings')) { - return WIDGET_TYPES.DashboardHome; - } else if (target.matches('.dashboard-item-list')) { - return WIDGET_TYPES.Dashboard; - } else if (target.matches('.dashlet-item-list') || target.matches('.dashboard.content')) { - return WIDGET_TYPES.Dashlet; + /** + * Possible type of widgets this behavior is being applied to + * + * @type {object} + */ + this.widgetTypes = { Dashlet : 'Dashlets', Dashboard : 'Dashboards', DashboardHome : 'Homes' }; + + this.on('rendered', '#main > .container', this.onRendered, this); + this.on('end', '.dashboard-settings, .dashboard-list-control, .dashlet-list-item', this.elementDropped, this); + // This is for the normal dashboard/dashlets view + this.on('end', '.dashboard.content', this.elementDropped, this); } - return null; - }; - - /** - * Set up a request with the reordered widget and post the data to the controller - * - * @param event - * - * @returns {boolean} - */ - Dashboard.prototype.elementDropped = function (event) { - let _this = event.data.self, - orgEvt = event.originalEvent, - data = {}; + /** + * Get the widget type of the given element + * + * @param {HTMLElement} target + * + * @returns {null|string} + */ + getTypeFor(target) { + if (target.matches('.dashboard-settings')) { + return this.widgetTypes.DashboardHome; + } else if (target.matches('.dashboard-item-list')) { + return this.widgetTypes.Dashboard; + } else if (target.matches('.dashlet-item-list') || target.matches('.dashboard.content')) { + return this.widgetTypes.Dashlet; + } - if (orgEvt.to === orgEvt.from && orgEvt.newIndex === orgEvt.oldIndex) { - return false; + return null; } - let item = orgEvt.item; - switch (_this.getTypeFor(orgEvt.to)) { - case WIDGET_TYPES.DashboardHome: { - let home = item.dataset.icingaHome; - data[home] = orgEvt.newIndex; - break; - } - case WIDGET_TYPES.Dashboard: { - let pane = item.dataset.icingaPane, - home = orgEvt.to.closest('.home-list-control').dataset.icingaHome; - if (orgEvt.to !== orgEvt.from) { - data.originals = { - originalHome : orgEvt.from.closest('.home-list-control').dataset.icingaHome, - originalPane : pane - }; - } - - data[home] = { [pane] : orgEvt.newIndex }; - break; + /** + * Set up a request with the reordered widget and post the data to the controller + * + * @param event + * + * @returns {boolean} + */ + elementDropped(event) { + let _this = event.data.self, + orgEvt = event.originalEvent, + data = {}; + + if (orgEvt.to === orgEvt.from && orgEvt.newIndex === orgEvt.oldIndex) { + return false; } - case WIDGET_TYPES.Dashlet: { - let dashlet = item.dataset.icingaDashlet, - pane, - home; - - if (orgEvt.to.matches('.dashboard.content')) { - let parentData = orgEvt.to.dataset.icingaPane.split('|', 2); - home = parentData.shift(); - pane = parentData.shift(); - - data.redirectPath = 'dashboards'; - } else { // Dashboard manager view - let parent = orgEvt.to.closest('.dashboard-list-control'); - pane = parent.dataset.icingaPane; - home = parent.closest('.home-list-control').dataset.icingaHome; + let item = orgEvt.item; + switch (_this.getTypeFor(orgEvt.to)) { + case _this.widgetTypes.DashboardHome: { + let home = item.dataset.icingaHome; + data[home] = orgEvt.newIndex; + break; + } + case _this.widgetTypes.Dashboard: { + let pane = item.dataset.icingaPane, + home = orgEvt.to.closest('.home-list-control').dataset.icingaHome; if (orgEvt.to !== orgEvt.from) { - let parent = orgEvt.from.closest('.dashboard-list-control'); data.originals = { - originalHome : parent.closest('.home-list-control').dataset.icingaHome, - originalPane : parent.dataset.icingaPane - } + originalHome : orgEvt.from.closest('.home-list-control').dataset.icingaHome, + originalPane : pane + }; } - } - dashlet = { [dashlet] : orgEvt.newIndex }; - data[home] = { [pane] : dashlet }; - } - } + data[home] = { [pane] : orgEvt.newIndex }; + break; + } + case _this.widgetTypes.Dashlet: { + let dashlet = item.dataset.icingaDashlet, + pane, + home; + + if (orgEvt.to.matches('.dashboard.content')) { + let parentData = orgEvt.to.dataset.icingaPane.split('|', 2); + home = parentData.shift(); + pane = parentData.shift(); + + data.redirectPath = 'dashboards'; + } else { // Dashboard manager view + let parent = orgEvt.to.closest('.dashboard-list-control'); + pane = parent.dataset.icingaPane; + // If there is only default home in the dashboard manager view, there won't be rendered + // ".home-list-control", so we need to look for an alternative + home = parent.closest('.home-list-control, .dashboard-settings').dataset.icingaHome; + + if (orgEvt.to !== orgEvt.from) { + let parent = orgEvt.from.closest('.dashboard-list-control'); + data.originals = { + originalHome : parent.closest('.home-list-control').dataset.icingaHome, + originalPane : parent.dataset.icingaPane + } + } + } - if (Object.keys(data).length) { - data.Type = _this.getTypeFor(orgEvt.to); - if (! data.hasOwnProperty('originals')) { - data.originals = null; + dashlet = { [dashlet] : orgEvt.newIndex }; + data[home] = { [pane] : dashlet }; + } } - if (! data.hasOwnProperty('redirectPath')) { - data.redirectPath = 'dashboards/settings'; - } + if (Object.keys(data).length) { + data.Type = _this.getTypeFor(orgEvt.to); + if (! data.originals) { + data.originals = null; + } - data = { dashboardData : JSON.stringify(data) }; - let url = _this.icinga.config.baseUrl + '/dashboards/reorder-widgets'; - _this.icinga.loader.loadUrl(url, $('#col1'), data, 'post'); - } - }; + if (! data.redirectPath) { + data.redirectPath = 'dashboards/settings'; + } - /** - * Get whether the given element is a valid target of the drag & drop events - * - * @param to - * @param from - * @param item - * @param event - * - * @returns {boolean} - */ - Dashboard.prototype.isValid = function (to, from, item, event) { - if (typeof from.options.group === 'undefined' || typeof to.options.group === 'undefined') { - return false; + data = { dashboardData : JSON.stringify(data) }; + let url = _this.icinga.config.baseUrl + '/dashboards/reorder-widgets'; + _this.icinga.loader.loadUrl(url, $('#col1'), data, 'post'); + } } - return from.options.group.name === to.options.group.name; - }; - - Dashboard.prototype.onRendered = function (e) { - let _this = e.data.self; - e.target.querySelectorAll('.dashboard-settings, .dashboard.content, .dashboard-item-list, .dashlet-item-list') - .forEach(sortable => { - let groupName = _this.getTypeFor(sortable), - draggable, - handle; - - switch (groupName) { - case WIDGET_TYPES.DashboardHome: { - groupName = WIDGET_TYPES.DashboardHome; - draggable = '.home-list-control'; - handle = '.home-list-control > h1'; - break; + onRendered(e) { + let _this = e.data.self; + e.target.querySelectorAll('.dashboard-settings, .dashboard.content,' + + ' .dashboard-item-list, .dashlet-item-list') + .forEach(sortable => { + let groupName = _this.getTypeFor(sortable), + draggable, + handle; + + switch (groupName) { + case _this.widgetTypes.DashboardHome: + groupName = _this.widgetTypes.DashboardHome; + draggable = '.home-list-control'; + handle = '.home-list-control > h1'; + break; + case _this.widgetTypes.Dashboard: + groupName = _this.widgetTypes.Dashboard; + draggable = '.dashboard-list-control'; + handle = '.dashboard-list-control > h1' + break; + case _this.widgetTypes.Dashlet: + groupName = _this.widgetTypes.Dashlet; + if (sortable.matches('.dashboard.content')) { + draggable = '> .container'; + } else { + draggable = '.dashlet-list-item'; + } + + handle = draggable; } - case WIDGET_TYPES.Dashboard: { - groupName = WIDGET_TYPES.Dashboard; - draggable = '.dashboard-list-control'; - handle = '.dashboard-list-control > h1' - break; - } - case WIDGET_TYPES.Dashlet: { - groupName = WIDGET_TYPES.Dashlet; - if (sortable.matches('.dashboard.content')) { - draggable = '> .container'; - } else { - draggable = '.dashlet-list-item'; - } - handle = draggable; - } - } + let options = { + scroll : true, + invertSwap : true, + delay : 100, + dataIdAttr : 'id', + direction : 'vertical', + draggable : draggable, + handle : handle, + group : { name : groupName } + }; - let options = { - scroll : true, - invertSwap : true, - delay : 100, - dataIdAttr : 'id', - direction : 'vertical', - draggable : draggable, - handle : handle, - group : { - name : groupName, - put : _this['isValid'], - } - }; + _this.Sortable.create(sortable, options); + }); + } + } - Sortable.create(sortable, options); - }); - }; + Icinga.Behaviors = Icinga.Behaviors || {}; Icinga.Behaviors.Dashboard = Dashboard; From 100e852e26bc9d1dfa9d0b09a0d3999eae031b1e Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Wed, 6 Apr 2022 19:45:24 +0200 Subject: [PATCH 044/106] modal.js: Change modal cancel selector --- public/js/icinga/behavior/modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/icinga/behavior/modal.js b/public/js/icinga/behavior/modal.js index d58d903a3d..1d59fbb64a 100644 --- a/public/js/icinga/behavior/modal.js +++ b/public/js/icinga/behavior/modal.js @@ -23,7 +23,7 @@ this.on('change', '#modal form input.autosubmit', this.onFormAutoSubmit, this); this.on('click', '[data-icinga-modal]', this.onModalToggleClick, this); this.on('mousedown', '#layout > #modal', this.onModalLeave, this); - this.on('click', '.modal-header > button, .modal-cancel', this.onModalClose, this); + this.on('click', '#modal [data-icinga-modal-cancel]', this.onModalClose, this); this.on('keydown', this.onKeyDown, this); }; From 41f1ac4a907d05f18b29fc1e383e13dc4fd91a17 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Wed, 6 Apr 2022 19:47:22 +0200 Subject: [PATCH 045/106] Use Menu class instead of HomeMenu --- application/controllers/LayoutController.php | 4 +- .../forms/Dashboard/BaseDashboardForm.php | 6 ++- application/layouts/scripts/layout.phtml | 2 +- .../layouts/scripts/parts/navigation.phtml | 4 +- application/views/scripts/layout/menu.phtml | 3 +- library/Icinga/Web/Menu.php | 46 +++++++++++++++++++ 6 files changed, 58 insertions(+), 7 deletions(-) diff --git a/application/controllers/LayoutController.php b/application/controllers/LayoutController.php index 217c275d76..237681cfa5 100644 --- a/application/controllers/LayoutController.php +++ b/application/controllers/LayoutController.php @@ -4,7 +4,7 @@ namespace Icinga\Controllers; use Icinga\Web\Controller\ActionController; -use Icinga\Web\HomeMenu; +use Icinga\Web\Menu; /** * Create complex layout parts @@ -18,7 +18,7 @@ public function menuAction() { $this->setAutorefreshInterval(15); $this->_helper->layout()->disableLayout(); - $this->view->menuRenderer = (new HomeMenu())->getRenderer(); + $this->view->menuRenderer = (new Menu())->getRenderer(); } public function announcementsAction() diff --git a/application/forms/Dashboard/BaseDashboardForm.php b/application/forms/Dashboard/BaseDashboardForm.php index 5490caade9..2fd48ccbc2 100644 --- a/application/forms/Dashboard/BaseDashboardForm.php +++ b/application/forms/Dashboard/BaseDashboardForm.php @@ -76,7 +76,11 @@ protected function createFormControls() */ protected function createCancelButton() { - return $this->createElement('submitButton', 'btn_cancel', ['class' => 'modal-cancel', 'label' => t('Cancel')]); + return $this->createElement('submitButton', 'btn_cancel', [ + 'class' => 'modal-cancel', + 'label' => t('Cancel'), + 'data-icinga-modal-cancel' => true + ]); } /** diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml index 1800f2c157..c4417c6c95 100644 --- a/application/layouts/scripts/layout.phtml +++ b/application/layouts/scripts/layout.phtml @@ -87,7 +87,7 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml';