summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:16:36 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:16:36 +0000
commitd61b7618d9c04ff90fdf8d3b584ad5976faedad9 (patch)
tree6de6eaca7793f0f1f756c9a5a0fa9e07957c8569
parentInitial commit. (diff)
downloadicingaweb2-module-cube-upstream.tar.xz
icingaweb2-module-cube-upstream.zip
Adding upstream version 1.3.2.upstream/1.3.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.mailmap9
-rw-r--r--AUTHORS14
-rw-r--r--LICENSE339
-rw-r--r--README.md62
-rw-r--r--application/controllers/HostsController.php45
-rw-r--r--application/controllers/IdoHostsController.php24
-rw-r--r--application/controllers/IdoServicesController.php24
-rw-r--r--application/controllers/IndexController.php15
-rw-r--r--application/controllers/ServicesController.php45
-rw-r--r--application/forms/DimensionsForm.php211
-rw-r--r--application/views/scripts/cube-details.phtml12
-rw-r--r--application/views/scripts/cube-index.phtml22
-rw-r--r--configuration.php5
-rw-r--r--doc/01-About.md56
-rw-r--r--doc/02-Installation.md23
-rw-r--r--doc/02-Installation.md.d/From-Source.md15
-rw-r--r--doc/img/cube_action-links.pngbin0 -> 36553 bytes
-rw-r--r--doc/img/cube_director.pngbin0 -> 97465 bytes
-rw-r--r--doc/img/cube_move-up.pngbin0 -> 189410 bytes
-rw-r--r--doc/img/cube_simple.pngbin0 -> 88338 bytes
-rw-r--r--doc/img/cube_slice.pngbin0 -> 177946 bytes
-rw-r--r--library/Cube/Cube.php341
-rw-r--r--library/Cube/CubeRenderer.php512
-rw-r--r--library/Cube/CubeRenderer/HostStatusCubeRenderer.php143
-rw-r--r--library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php149
-rw-r--r--library/Cube/Dimension.php70
-rw-r--r--library/Cube/DimensionParams.php85
-rw-r--r--library/Cube/Hook/ActionsHook.php99
-rw-r--r--library/Cube/Hook/IcingaDbActionsHook.php125
-rw-r--r--library/Cube/IcingaDb/CustomVariableDimension.php166
-rw-r--r--library/Cube/IcingaDb/IcingaDbCube.php338
-rw-r--r--library/Cube/IcingaDb/IcingaDbHostStatusCube.php80
-rw-r--r--library/Cube/IcingaDb/IcingaDbServiceStatusCube.php94
-rw-r--r--library/Cube/Ido/CustomVarDimension.php146
-rw-r--r--library/Cube/Ido/DataView/Hoststatus.php17
-rw-r--r--library/Cube/Ido/DbCube.php298
-rw-r--r--library/Cube/Ido/IdoCube.php219
-rw-r--r--library/Cube/Ido/IdoHostStatusCube.php97
-rw-r--r--library/Cube/Ido/IdoServiceStatusCube.php97
-rw-r--r--library/Cube/Ido/Query/HoststatusQuery.php47
-rw-r--r--library/Cube/Ido/ZfSelectWrapper.php77
-rw-r--r--library/Cube/ProvidedHook/Cube/IcingaDbActions.php48
-rw-r--r--library/Cube/ProvidedHook/Cube/MonitoringActions.php53
-rw-r--r--library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php11
-rw-r--r--library/Cube/Web/ActionLink.php103
-rw-r--r--library/Cube/Web/ActionLinks.php115
-rw-r--r--library/Cube/Web/Controller.php297
-rw-r--r--library/Cube/Web/IdoController.php198
-rw-r--r--module.info10
-rw-r--r--phpstan-baseline.neon1031
-rw-r--r--phpstan.neon27
-rw-r--r--public/css/module.less354
-rw-r--r--run.php48
53 files changed, 6416 insertions, 0 deletions
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..d00642d
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,9 @@
+Alexander A. Klimov <alexander.klimov@icinga.com> <alexander.klimov@netways.de>
+Feu Mourek <feu.mourek@icinga.com> <feu.mourek@netways.de>
+Feu Mourek <feu.mourek@icinga.com> <jennifer.mourek@icinga.com>
+Florian Strohmaier <florian.strohmaier@icinga.com> <florian.strohmaier@me.com>
+Michael Friedrich <michael.friedrich@icinga.com> <michael.friedrich@netways.de>
+Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com> <sukhwinder33445@gmail.com>
+Thomas Gelf <thomas.gelf@icinga.com> <thomas@gelf.net>
+Yonas Habteab <yonas.habteab@icinga.com> <57616252+Yonas-net@users.noreply.github.com>
+raviks789 <ravi.srinivasa@icinga.com> <33730024+raviks789@users.noreply.github.com>
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..4b5abaf
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,14 @@
+Alexander A. Klimov <alexander.klimov@icinga.com>
+Eric Lippmann <eric.lippmann@icinga.com>
+Feu Mourek <feu.mourek@icinga.com>
+Florian Strohmaier <florian.strohmaier@icinga.com>
+Johannes Meyer <johannes.meyer@icinga.com>
+Ken Jungclaus <lum33n@web.de>
+Michael Friedrich <michael.friedrich@icinga.com>
+Nicolai Buchwitz <nicolai.buchwitz@enda.eu>
+Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com>
+Thomas Gelf <thomas.gelf@icinga.com>
+Timm Ortloff <timm.ortloff@icinga.com>
+Yonas Habteab <yonas.habteab@icinga.com>
+raviks789 <ravi.srinivasa@icinga.com>
+sant-swedge <simon.wedge@sant.ox.ac.uk>
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..45fce7a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,62 @@
+# Icinga Cube
+
+[![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/)
+![Build Status](https://github.com/icinga/icingaweb2-module-cube/workflows/PHP%20Tests/badge.svg?branch=main)
+[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-cube.svg)](https://github.com/Icinga/icingaweb2-module-cube)
+
+![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png)
+
+The Icinga Cube is a tiny but useful [Icinga Web](https://github.com/Icinga/icingaweb2)
+module. It currently shows host and service statistics (total count, health) grouped by
+various custom variables in multiple dimensions.
+
+![Cube - Overview](doc/img/cube_simple.png)
+
+It will be your new best friend in case you are running a large environment and
+want to get a quick answers to questions like:
+
+* Which project uses how many servers per environment at which location/site?
+ * Who occupies most servers?
+ * How many of those are used in production?
+ * Which project has only development and test boxes?
+* Which operating system is used for which project and in which environment?
+ * Do we still have Debian Lenny?
+ * Which projects are to blame for this?
+ * Do we have applications where the operating systems used differ in staging
+ and production?
+* Which project uses which operating system version for which application?
+ * Which projects have homogeneous environments?
+ * Which projects are at a consistent patch level?
+ * How many RHEL 6 variants (6.1, 6.2, 6.3...) do we use?
+ * Who is running the oldest ones? In production?
+* Which projects are still using physical servers in which environment?
+
+For Businessmen - Drill and Slice
+---------------------------------
+
+Get answers to your questions. Quick and fully autonomous, using the cube
+requires no technical skills. Choose amongst all available dimensions and rotate
+the Cube to fit your needs.
+
+![Cube - Configure Dimensions](doc/img/cube_move-up.png)
+
+Want to drill down? Choose a slice and get your answers:
+
+![Cube - Configure Dimensions](doc/img/cube_slice.png)
+
+All facts configured for systems monitored by [Icinga](https://www.icinga.com/)
+can be used for your research.
+
+For Icinga Director users
+-------------------------
+
+In case you are using the [Icinga Director](https://github.com/Icinga/icingaweb2-module-director),
+in addition to the multi-selection/edit feature the cube provides a nice way to
+modify multiple hosts at once.
+
+![Cube - Director multi-edit](doc/img/cube_director.png)
+
+Installation
+------------
+
+To install Icinga Cube see [Installation](https://icinga.com/docs/icinga-cube/latest/doc/02-Installation/).
diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php
new file mode 100644
index 0000000..c41d846
--- /dev/null
+++ b/application/controllers/HostsController.php
@@ -0,0 +1,45 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Controllers;
+
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbHostStatusCube;
+use Icinga\Module\Cube\Web\Controller;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+
+class HostsController extends Controller
+{
+ public function indexAction(): void
+ {
+ $this->createTabs()->activate('cube/hosts');
+
+ $this->renderCube();
+ }
+
+ protected function getCube(): IcingaDbCube
+ {
+ return new IcingaDbHostStatusCube();
+ }
+
+ public function completeAction(): void
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Host::class);
+ $suggestions->forRequest($this->getServerRequest());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction(): void
+ {
+ $editor = $this->createSearchEditor(
+ Host::on($this->getDb()),
+ $this->preserveParams
+ );
+
+ $this->getDocument()->add($editor);
+ $this->setTitle($this->translate('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/IdoHostsController.php b/application/controllers/IdoHostsController.php
new file mode 100644
index 0000000..8648823
--- /dev/null
+++ b/application/controllers/IdoHostsController.php
@@ -0,0 +1,24 @@
+<?php
+
+// Icinga Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Controllers;
+
+use Icinga\Module\Cube\Ido\IdoCube;
+use Icinga\Module\Cube\Ido\IdoHostStatusCube;
+use Icinga\Module\Cube\Web\IdoController;
+
+class IdoHostsController extends IdoController
+{
+ public function indexAction(): void
+ {
+ $this->createTabs()->activate('cube/hosts');
+
+ $this->renderCube();
+ }
+
+ protected function getCube(): IdoCube
+ {
+ return new IdoHostStatusCube();
+ }
+}
diff --git a/application/controllers/IdoServicesController.php b/application/controllers/IdoServicesController.php
new file mode 100644
index 0000000..f55e1d7
--- /dev/null
+++ b/application/controllers/IdoServicesController.php
@@ -0,0 +1,24 @@
+<?php
+
+// Icinga Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Controllers;
+
+use Icinga\Module\Cube\Ido\IdoCube;
+use Icinga\Module\Cube\Ido\IdoServiceStatusCube;
+use Icinga\Module\Cube\Web\IdoController;
+
+class IdoServicesController extends IdoController
+{
+ public function indexAction(): void
+ {
+ $this->createTabs()->activate('cube/services');
+
+ $this->renderCube();
+ }
+
+ protected function getCube(): IdoCube
+ {
+ return new IdoServiceStatusCube();
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..082fda3
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,15 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Controllers;
+
+use Icinga\Web\Controller;
+
+class IndexController extends Controller
+{
+ public function indexAction()
+ {
+ $this->redirectNow('cube/hosts' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString()));
+ }
+}
diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php
new file mode 100644
index 0000000..0914aa2
--- /dev/null
+++ b/application/controllers/ServicesController.php
@@ -0,0 +1,45 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Controllers;
+
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbServiceStatusCube;
+use Icinga\Module\Cube\Web\Controller;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+
+class ServicesController extends Controller
+{
+ public function indexAction(): void
+ {
+ $this->createTabs()->activate('cube/services');
+
+ $this->renderCube();
+ }
+
+ protected function getCube(): IcingaDbCube
+ {
+ return new IcingaDbServiceStatusCube();
+ }
+
+ public function completeAction(): void
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Service::class);
+ $suggestions->forRequest($this->getServerRequest());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction(): void
+ {
+ $editor = $this->createSearchEditor(
+ Service::on($this->getDb()),
+ $this->preserveParams
+ );
+
+ $this->getDocument()->add($editor);
+ $this->setTitle($this->translate('Adjust Filter'));
+ }
+}
diff --git a/application/forms/DimensionsForm.php b/application/forms/DimensionsForm.php
new file mode 100644
index 0000000..fbfbbc7
--- /dev/null
+++ b/application/forms/DimensionsForm.php
@@ -0,0 +1,211 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Forms;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+use Icinga\Module\Cube\DimensionParams;
+use Icinga\Web\Notification;
+use ipl\Html\Form;
+use ipl\Html\Html;
+use ipl\I18n\Translation;
+use ipl\Web\Common\FormUid;
+use ipl\Web\Url;
+use ipl\Web\Widget\Icon;
+
+class DimensionsForm extends Form
+{
+ use FormUid;
+ use Translation;
+
+ protected $defaultAttributes = [
+ 'class' => 'icinga-controls',
+ 'name' => 'dimensions-form'
+ ];
+
+ /**
+ * @var Cube
+ */
+ private $cube;
+
+ /**
+ * @var Url
+ */
+ private $url;
+
+ /**
+ * Get the url
+ *
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Set the url
+ *
+ * @param mixed $url
+ */
+ public function setUrl($url): self
+ {
+ $this->url = $url;
+
+ return $this;
+ }
+
+ public function setCube(Cube $cube)
+ {
+ $this->cube = $cube;
+ return $this;
+ }
+
+ public function hasBeenSubmitted(): bool
+ {
+ // required to submit dimension controls and the selected dropdown option
+ return $this->hasBeenSent() &&
+ ($this->getPressedSubmitElement() !== null || $this->getPopulatedValue('addDimension'));
+ }
+
+ public function assemble()
+ {
+ $dimensions = $this->cube->listDimensions();
+ $cnt = count($dimensions);
+
+ if ($cnt < 3) {
+ $allDimensions = $this->cube->listAdditionalDimensions();
+
+ $this->addElement('select', 'addDimension', [
+ 'options' => [null => $this->translate('+ Add a dimension')] + $allDimensions,
+ 'class' => 'autosubmit'
+ ]);
+ }
+
+ $pos = 0;
+ foreach ($dimensions as $dimension) {
+ $this->addDimensionButtons($dimension, $pos++, $cnt);
+ }
+
+ foreach ($this->cube->getSlices() as $key => $value) {
+ $this->addSlice($this->cube->getDimension($key), $value);
+ }
+
+ $this->addElement($this->createUidElement());
+ }
+
+ protected function addSlice(Dimension $dimension, $value)
+ {
+ $sliceId = sha1($this->cube::SLICE_PREFIX . $dimension->getName());
+
+ $sliceFieldset = Html::tag('fieldset', ['class' => 'dimensions']);
+
+ $btn = $this->createElement('submitButton', 'removeSlice_' . $sliceId, [
+ 'label' => new Icon('trash'),
+ 'class' => 'dimension-control'
+ ]);
+
+ $this->registerElement($btn);
+ $sliceFieldset->addHtml($btn);
+
+ $sliceFieldset->addHtml(Html::tag(
+ 'span',
+ ['class' => 'dimension-name'],
+ sprintf('%s: %s = %s', $this->translate('Slice/Filter'), $dimension->getLabel(), $value)
+ ));
+
+ $this->addHtml($sliceFieldset);
+ }
+
+ protected function addDimensionButtons(Dimension $dimension, $pos, $total)
+ {
+ $dimensionId = sha1($dimension->getName());
+
+ $dimensionFieldset = Html::tag('fieldset', ['class' => 'dimensions']);
+
+ $btn = $this->createElement('submitButton', 'removeDimension_' . $dimensionId, [
+ 'label' => new Icon('trash'),
+ 'title' => sprintf($this->translate('Remove dimension "%s"'), $dimension->getLabel()),
+ 'class' => 'dimension-control'
+ ]);
+
+ $this->registerElement($btn);
+ $dimensionFieldset->addHtml($btn);
+
+ if ($pos > 0) {
+ $btn = $this->createElement('submitButton', 'moveDimensionUp_' . $dimensionId, [
+ 'label' => new Icon('angle-double-up'),
+ 'title' => sprintf($this->translate('Move dimension "%s" up'), $dimension->getLabel()),
+ 'class' => 'dimension-control',
+ ]);
+
+ $this->registerElement($btn);
+ $dimensionFieldset->addHtml($btn);
+ }
+
+ if ($pos + 1 !== $total) {
+ $btn = $this->createElement('submitButton', 'moveDimensionDown_' . $dimensionId, [
+ 'label' => new Icon('angle-double-down'),
+ 'title' => sprintf($this->translate('Move dimension "%s" down'), $dimension->getLabel()),
+ 'class' => 'dimension-control'
+ ]);
+
+ $this->registerElement($btn);
+ $dimensionFieldset->addHtml($btn);
+ }
+
+ $dimensionFieldset->addHtml(Html::tag('span', ['class' => 'dimension-name'], $dimension->getLabel()));
+
+ $this->addHtml($dimensionFieldset);
+ }
+
+ public function onSuccess()
+ {
+ $url = $this->getUrl();
+
+ if ($dimension = $this->getValue('addDimension')) {
+ $url->setParam('dimensions', DimensionParams::fromUrl($url)->add($dimension)->getParams());
+ Notification::success($this->translate('New dimension has been added'));
+ } else {
+ $updateDimensions = false;
+ $pressedButtonName = $this->getPressedSubmitElement()->getName();
+
+ foreach ($this->cube->listDimensions() as $name => $_) {
+ $dimensionId = sha1($name);
+
+ switch (true) {
+ case ($pressedButtonName === 'removeDimension_' . $dimensionId):
+ $this->cube->removeDimension($name);
+ $updateDimensions = true;
+ break 2;
+ case ($pressedButtonName === 'moveDimensionUp_' . $dimensionId):
+ $this->cube->moveDimensionUp($name);
+ $updateDimensions = true;
+ break 2;
+ case ($pressedButtonName === 'moveDimensionDown_' . $dimensionId):
+ $this->cube->moveDimensionDown($name);
+ $updateDimensions = true;
+ break 2;
+ }
+ }
+
+ if ($updateDimensions) {
+ $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices());
+ $url->setParam('dimensions', DimensionParams::update($dimensions)->getParams());
+ } else {
+ foreach ($this->cube->listSlices() as $slice) {
+ $slice = $this->cube::SLICE_PREFIX . $slice;
+ $sliceId = sha1($slice);
+
+ if ($pressedButtonName === 'removeSlice_' . $sliceId) {
+ $url->getParams()->remove(rawurlencode($slice));
+ }
+ }
+ }
+ }
+
+ $this->setRedirectUrl($url);
+ }
+}
diff --git a/application/views/scripts/cube-details.phtml b/application/views/scripts/cube-details.phtml
new file mode 100644
index 0000000..8e0611f
--- /dev/null
+++ b/application/views/scripts/cube-details.phtml
@@ -0,0 +1,12 @@
+<div class="controls">
+<?php if (! \Icinga\Module\Cube\Cube::isUsingIcingaDb()): ?>
+ <?= $this->tabs ?>
+<?php endif ?>
+<h1><?= $this->escape($this->title) ?></h1>
+</div>
+
+<div class="content">
+ <ul class="action-links">
+ <?= $this->links ?>
+ </ul>
+</div>
diff --git a/application/views/scripts/cube-index.phtml b/application/views/scripts/cube-index.phtml
new file mode 100644
index 0000000..f46d74a
--- /dev/null
+++ b/application/views/scripts/cube-index.phtml
@@ -0,0 +1,22 @@
+<div class="controls">
+ <?php if (! $this->compact): ?>
+ <?php if (! \Icinga\Module\Cube\Cube::isUsingIcingaDb()): ?>
+ <?= $this->tabs ?>
+ <?php endif ?>
+ <h1><?= $this->escape($this->title) ?></h1>
+ <?php if ($this->form && ! $this->compact): ?>
+ <?= $this->qlink('Hide settings', $this->url->without('showSettings'), null, ['icon' => 'wrench']) ?>
+ <?php else: ?>
+ <?= $this->qlink('Show settings', $this->url->with('showSettings', true), null, ['icon' => 'wrench']) ?>
+ <?php endif ?>
+ <?php endif ?>
+</div>
+
+<div class="content">
+ <?php if ($this->form && ! $this->compact): ?>
+ <?= $this->form ?>
+ <?php endif ?>
+ <?php if ($this->cube): ?>
+ <?= $this->cube->render($this) ?>
+ <?php endif ?>
+</div>
diff --git a/configuration.php b/configuration.php
new file mode 100644
index 0000000..eaf1130
--- /dev/null
+++ b/configuration.php
@@ -0,0 +1,5 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+$this->menuSection(N_('Reporting'))->add($this->translate('Cube'))->setUrl('cube/hosts')->setPriority(10);
diff --git a/doc/01-About.md b/doc/01-About.md
new file mode 100644
index 0000000..bd9dd58
--- /dev/null
+++ b/doc/01-About.md
@@ -0,0 +1,56 @@
+# Icinga Cube
+
+The Icinga Cube is a tiny but useful [Icinga Web](https://github.com/Icinga/icingaweb2)
+module. It currently shows host and service statistics (total count, health) grouped by
+various custom variables in multiple dimensions.
+
+![Cube - Overview](img/cube_simple.png)
+
+It will be your new best friend in case you are running a large environment and
+want to get a quick answers to questions like:
+
+* Which project uses how many servers per environment at which location/site?
+ * Who occupies most servers?
+ * How many of those are used in production?
+ * Which project has only development and test boxes?
+* Which operating system is used for which project and in which environment?
+ * Do we still have Debian Lenny?
+ * Which projects are to blame for this?
+ * Do we have applications where the operating systems used differ in staging
+ and production?
+* Which project uses which operating system version for which application?
+ * Which projects have homogeneous environments?
+ * Which projects are at a consistent patch level?
+ * How many RHEL 6 variants (6.1, 6.2, 6.3...) do we use?
+ * Who is running the oldest ones? In production?
+* Which projects are still using physical servers in which environment?
+
+For Businessmen - Drill and Slice
+---------------------------------
+
+Get answers to your questions. Quick and fully autonomous, using the cube
+requires no technical skills. Choose amongst all available dimensions and rotate
+the Cube to fit your needs.
+
+![Cube - Configure Dimensions](img/cube_move-up.png)
+
+Want to drill down? Choose a slice and get your answers:
+
+![Cube - Configure Dimensions](img/cube_slice.png)
+
+All facts configured for systems monitored by [Icinga](https://www.icinga.com/)
+can be used for your research.
+
+For Icinga Director users
+-------------------------
+
+In case you are using the [Icinga Director](https://github.com/Icinga/icingaweb2-module-director),
+in addition to the multi-selection/edit feature the cube provides a nice way to
+modify multiple hosts at once.
+
+![Cube - Director multi-edit](img/cube_director.png)
+
+Installation
+------------
+
+To install Icinga Cube see [Installation](02-Installation.md).
diff --git a/doc/02-Installation.md b/doc/02-Installation.md
new file mode 100644
index 0000000..d6f3271
--- /dev/null
+++ b/doc/02-Installation.md
@@ -0,0 +1,23 @@
+<!-- {% if index %} -->
+# Installing Icinga Cube
+
+The recommended way to install Icinga Cube is to use prebuilt packages for
+all supported platforms from our official release repository.
+Please note that [Icinga Web](https://icinga.com/docs/icinga-web) is required to run Icinga Cube
+and if it is not already set up, it is best to do this first.
+
+The following steps will guide you through installing and setting up Icinga Cube.
+<!-- {% else %} -->
+<!-- {% if not icingaDocs %} -->
+
+## Installing the Package
+
+If the [repository](https://packages.icinga.com) is not configured yet, please add it first.
+Then use your distribution's package manager to install the `icinga-cube` package
+or install [from source](02-Installation.md.d/From-Source.md).
+<!-- {% endif %} --><!-- {# end if not icingaDocs #} -->
+
+## Configuring Icinga Cube
+
+No additional steps are required to set up Icinga Cube and it is ready to use right after installation.
+<!-- {% endif %} --><!-- {# end else if index #} -->
diff --git a/doc/02-Installation.md.d/From-Source.md b/doc/02-Installation.md.d/From-Source.md
new file mode 100644
index 0000000..92ac15e
--- /dev/null
+++ b/doc/02-Installation.md.d/From-Source.md
@@ -0,0 +1,15 @@
+# Installing Icinga Cube from Source
+
+Please see the Icinga Web documentation on
+[how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source.
+Make sure you use `cube` as the module name. The following requirements must also be met.
+
+## Requirements
+
+* PHP (≥7.2)
+* [Icinga Web](https://github.com/Icinga/icingaweb2) (≥2.9)
+* [Icinga DB Web](https://github.com/Icinga/icingadb-web) (≥1.0)
+* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥0.13.0)
+
+If you are using PostgreSQL, you need at least version 9.5 which provides the `ROLLUP` feature.
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/img/cube_action-links.png b/doc/img/cube_action-links.png
new file mode 100644
index 0000000..2f3abe3
--- /dev/null
+++ b/doc/img/cube_action-links.png
Binary files differ
diff --git a/doc/img/cube_director.png b/doc/img/cube_director.png
new file mode 100644
index 0000000..b081c1d
--- /dev/null
+++ b/doc/img/cube_director.png
Binary files differ
diff --git a/doc/img/cube_move-up.png b/doc/img/cube_move-up.png
new file mode 100644
index 0000000..1fb3c09
--- /dev/null
+++ b/doc/img/cube_move-up.png
Binary files differ
diff --git a/doc/img/cube_simple.png b/doc/img/cube_simple.png
new file mode 100644
index 0000000..91bb8f2
--- /dev/null
+++ b/doc/img/cube_simple.png
Binary files differ
diff --git a/doc/img/cube_slice.png b/doc/img/cube_slice.png
new file mode 100644
index 0000000..4880cc3
--- /dev/null
+++ b/doc/img/cube_slice.png
Binary files differ
diff --git a/library/Cube/Cube.php b/library/Cube/Cube.php
new file mode 100644
index 0000000..1e688f0
--- /dev/null
+++ b/library/Cube/Cube.php
@@ -0,0 +1,341 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Web\View;
+
+abstract class Cube
+{
+ /** @var ?string Prefix for slice params */
+ public const SLICE_PREFIX = null;
+
+ /** @var ?bool Whether the icingadb backend is in use */
+ public const IS_USING_ICINGADB = null;
+
+ /** @var array<string, Dimension> Available dimensions */
+ protected $availableDimensions;
+
+ /** @var array Fact names */
+ protected $chosenFacts;
+
+ /** @var Dimension[] */
+ protected $dimensions = array();
+
+ protected $slices = array();
+
+ protected $renderer;
+
+ abstract public function fetchAll();
+
+ /**
+ * Get whether the icingadb backend is in use
+ *
+ * @return bool
+ */
+ public static function isUsingIcingaDb(): bool
+ {
+ return static::IS_USING_ICINGADB
+ ?? (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend());
+ }
+
+ public function removeDimension($name)
+ {
+ unset($this->dimensions[$name]);
+ unset($this->slices[$name]);
+ return $this;
+ }
+
+ /**
+ * @return CubeRenderer
+ * @throws IcingaException
+ */
+ public function getRenderer()
+ {
+ throw new IcingaException('Got no cube renderer');
+ }
+
+ public function getPathLabel()
+ {
+ $dimensions = $this->getDimensionsLabel();
+ $slices = $this->getSlicesLabel();
+ $parts = array();
+ if ($dimensions !== null) {
+ $parts[] = $dimensions;
+ }
+
+ if ($slices !== null) {
+ $parts[] = $slices;
+ }
+
+ return implode(', ', $parts);
+ }
+
+ public function getDimensionsLabel()
+ {
+ $dimensions = $this->listDimensions();
+ if (empty($dimensions)) {
+ return null;
+ }
+
+ return implode(' -> ', array_map(function ($d) {
+ return $d->getLabel();
+ }, $dimensions));
+ }
+
+ public function getSlicesLabel()
+ {
+ $parts = array();
+
+ $slices = $this->getSlices();
+ if (empty($slices)) {
+ return null;
+ }
+ foreach ($slices as $key => $value) {
+ $parts[] = sprintf('%s = %s', $this->getDimension($key)->getLabel(), $value);
+ }
+
+ return implode(', ', $parts);
+ }
+
+ /**
+ * Create a new dimension
+ *
+ * @param string $name
+ * @return Dimension
+ */
+ abstract public function createDimension($name);
+
+ protected function registerAvailableDimensions()
+ {
+ if ($this->availableDimensions !== null) {
+ return;
+ }
+
+ $this->availableDimensions = [];
+ foreach ($this->listAvailableDimensions() as $name => $label) {
+ if (! isset($this->availableDimensions[$name])) {
+ $this->availableDimensions[$name] = $this->createDimension($name)->setLabel($label);
+ } else {
+ $this->availableDimensions[$name]->addLabel($label);
+ }
+ }
+ }
+
+ public function listAdditionalDimensions()
+ {
+ $this->registerAvailableDimensions();
+
+ $list = [];
+ foreach ($this->availableDimensions as $name => $dimension) {
+ if (! $this->hasDimension($name)) {
+ $list[$name] = $dimension->getLabel();
+ }
+ }
+
+ return $list;
+ }
+
+ abstract public function listAvailableDimensions();
+
+ public function getDimensionAfter($name)
+ {
+ $found = false;
+ $after = null;
+
+ foreach ($this->listDimensions() as $k => $d) {
+ if ($found) {
+ $after = $d;
+ break;
+ }
+
+ if ($k === $name) {
+ $found = true;
+ }
+ }
+
+ return $after;
+ }
+
+ public function listDimensionsUpTo($name)
+ {
+ $res = array();
+ foreach ($this->listDimensions() as $d => $_) {
+ $res[] = $d;
+ if ($d === $name) {
+ break;
+ }
+ }
+
+ return $res;
+ }
+
+ public function moveDimensionUp($name)
+ {
+ $last = $found = null;
+ $positions = array_keys($this->dimensions);
+
+ foreach ($positions as $k => $v) {
+ if ($v === $name) {
+ $found = $k;
+ break;
+ }
+
+ $last = $k;
+ }
+
+ if ($found !== null) {
+ $this->flipPositions($positions, $last, $found);
+ }
+
+ $this->reOrderDimensions($positions);
+ return $this;
+ }
+
+ public function moveDimensionDown($name)
+ {
+ $next = $found = null;
+ $positions = array_keys($this->dimensions);
+
+ foreach ($positions as $k => $v) {
+ if ($found !== null) {
+ $next = $k;
+ break;
+ }
+
+ if ($v === $name) {
+ $found = $k;
+ }
+ }
+
+ if ($next !== null) {
+ $this->flipPositions($positions, $next, $found);
+ }
+
+ $this->reOrderDimensions($positions);
+ return $this;
+ }
+
+ protected function flipPositions(&$array, $pos1, $pos2)
+ {
+ list(
+ $array[$pos1],
+ $array[$pos2]
+ ) = array(
+ $array[$pos2],
+ $array[$pos1]
+ );
+ }
+
+ protected function reOrderDimensions($positions)
+ {
+ $dimensions = array();
+ foreach ($positions as $pos => $key) {
+ $dimensions[$key] = $this->dimensions[$key];
+ }
+
+ $this->dimensions = $dimensions;
+ }
+
+ public function addDimension(Dimension $dimension)
+ {
+ $name = $dimension->getName();
+ if ($this->hasDimension($name)) {
+ throw new IcingaException('Cannot add dimension "%s" twice', $name);
+ }
+
+ $this->dimensions[$name] = $dimension;
+ return $this;
+ }
+
+ public function slice($key, $value)
+ {
+ if ($this->hasDimension($key)) {
+ $this->slices[$key] = $value;
+ } else {
+ throw new IcingaException('Got no such dimension: "%s"', $key);
+ }
+
+ return $this;
+ }
+
+ public function hasDimension($name)
+ {
+ return array_key_exists($name, $this->dimensions);
+ }
+
+ public function hasSlice($name)
+ {
+ return array_key_exists($name, $this->slices);
+ }
+
+ public function listSlices()
+ {
+ return array_keys($this->slices);
+ }
+
+ public function getSlices()
+ {
+ return $this->slices;
+ }
+
+ public function hasFact($name)
+ {
+ return array_key_exists($name, $this->chosenFacts);
+ }
+
+ public function getDimension($name)
+ {
+ return $this->dimensions[$name];
+ }
+
+ /**
+ * Return a list of chosen facts
+ *
+ * @return array
+ */
+ public function listFacts()
+ {
+ return $this->chosenFacts;
+ }
+
+ /**
+ * Choose a list of facts
+ *
+ * @param array $facts
+ * @return $this
+ */
+ public function chooseFacts(array $facts)
+ {
+ $this->chosenFacts = $facts;
+ return $this;
+ }
+
+ public function listDimensions()
+ {
+ return array_diff_key($this->dimensions, $this->slices);
+ }
+
+ public function listColumns()
+ {
+ return array_merge(array_keys($this->listDimensions()), $this->listFacts());
+ }
+
+ /**
+ * @param View $view
+ * @param CubeRenderer $renderer
+ * @return string
+ */
+ public function render(View $view, CubeRenderer $renderer = null)
+ {
+ if ($renderer === null) {
+ $renderer = $this->getRenderer();
+ }
+
+ return $renderer->render($view);
+ }
+}
diff --git a/library/Cube/CubeRenderer.php b/library/Cube/CubeRenderer.php
new file mode 100644
index 0000000..e88a938
--- /dev/null
+++ b/library/Cube/CubeRenderer.php
@@ -0,0 +1,512 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Web\View;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+use Generator;
+use Icinga\Data\Tree\TreeNode;
+
+/**
+ * CubeRenderer base class
+ *
+ * Every Cube Renderer must extend this class.
+ *
+ * TODO: Should we introduce DimensionRenderer, FactRenderer and SummaryHelper
+ * instead?
+ *
+ * @package Icinga\Module\Cube
+ */
+abstract class CubeRenderer
+{
+ /** @var View */
+ protected $view;
+
+ /** @var Cube */
+ protected $cube;
+
+ /** @var array Our dimensions */
+ protected $dimensions;
+
+ /** @var array Our dimensions in regular order */
+ protected $dimensionOrder;
+
+ /** @var array Our dimensions in reversed order as a quick lookup source */
+ protected $reversedDimensions;
+
+ /** @var array Level (deepness) for each dimension (0, 1, 2...) */
+ protected $dimensionLevels;
+
+ protected $facts;
+
+ /** @var object The row before the current one */
+ protected $lastRow;
+
+ /**
+ * Current summaries
+ *
+ * This is an object of objects, with dimension names being the keys and
+ * a facts row containing current (rollup) summaries for that dimension
+ * being it's value
+ *
+ * @var object
+ */
+ protected $summaries;
+
+ protected $started;
+
+ /**
+ * CubeRenderer constructor.
+ *
+ * @param Cube $cube
+ */
+ public function __construct(Cube $cube)
+ {
+ $this->cube = $cube;
+ }
+
+ /**
+ * Render the given facts
+ *
+ * @param $facts
+ * @return string
+ */
+ abstract public function renderFacts($facts);
+
+ /**
+ * Returns the base url for the details action
+ *
+ * @return string
+ */
+ abstract protected function getDetailsBaseUrl();
+
+ /**
+ * Get the severity sort columns
+ *
+ * @return Generator
+ */
+ abstract protected function getSeveritySortColumns(): Generator;
+
+ /**
+ * Initialize all we need
+ */
+ protected function initialize()
+ {
+ $this->started = false;
+ $this->initializeDimensions()
+ ->initializeFacts()
+ ->initializeLastRow()
+ ->initializeSummaries();
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeLastRow()
+ {
+ $object = (object) array();
+ foreach ($this->dimensions as $dimension) {
+ $object->{$dimension->getName()} = null;
+ }
+
+ $this->lastRow = $object;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeDimensions()
+ {
+ $this->dimensions = $this->cube->listDimensions();
+
+ $min = 3;
+ $cnt = count($this->dimensions);
+ if ($cnt < $min) {
+ $pos = 0;
+ $diff = $min - $cnt;
+ $this->dimensionOrder = [];
+ foreach ($this->dimensions as $name => $_) {
+ $this->dimensionOrder[$pos++ + $diff] = $name;
+ }
+ } else {
+ $this->dimensionOrder = array_keys($this->dimensions);
+ }
+
+ $this->reversedDimensions = array_reverse($this->dimensionOrder);
+ $this->dimensionLevels = array_flip($this->dimensionOrder);
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeFacts()
+ {
+ $this->facts = $this->cube->listFacts();
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeSummaries()
+ {
+ $this->summaries = (object) array();
+ return $this;
+ }
+
+ /**
+ * @param object $row
+ * @return bool
+ */
+ protected function startsDimension($row)
+ {
+ foreach ($this->dimensionOrder as $name) {
+ if ($row->$name === null) {
+ $this->summaries->$name = $this->extractFacts($row);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $row
+ * @return object
+ */
+ protected function extractFacts($row)
+ {
+ $res = (object) array();
+
+ foreach ($this->facts as $fact) {
+ $res->$fact = $row->$fact;
+ }
+
+ return $res;
+ }
+
+ public function render(View $view)
+ {
+ $this->view = $view;
+ $this->initialize();
+ $htm = $this->beginContainer();
+
+ $results = $this->cube->fetchAll();
+
+ if (! empty($results) && $this->cube::isUsingIcingaDb()) {
+ $sortBy = $this->cube->getSortBy();
+ if ($sortBy && $sortBy[0] === $this->cube::DIMENSION_SEVERITY_SORT_PARAM) {
+ $isSortDirDesc = isset($sortBy[1]) && $sortBy[1] !== 'asc';
+ $results = $this->sortBySeverity($results, $isSortDirDesc);
+ }
+ }
+
+ foreach ($results as $row) {
+ $htm .= $this->renderRow($row);
+ }
+
+ return $htm . $this->closeDimensions() . $this->endContainer();
+ }
+
+
+ /**
+ * Sort the results by severity
+ *
+ * @param $results array The fetched results
+ * @param $isSortDirDesc bool Whether the sort direction is descending
+ *
+ * @return Generator
+ */
+ private function sortBySeverity(array $results, bool $isSortDirDesc): Generator
+ {
+ $perspective = end($this->dimensionOrder);
+ $resultsCount = count($results);
+ $tree = [new TreeNode()];
+
+ $prepareHeaders = function (array $tree, object $row): TreeNode {
+ $node = (new TreeNode())
+ ->setValue($row);
+ $parent = end($tree);
+ $parent->appendChild($node);
+
+ return $node;
+ };
+
+ $i = 0;
+ do {
+ $row = $results[$i];
+ while ($row->$perspective === null) {
+ $tree[] = $prepareHeaders($tree, $row);
+
+ if (! isset($results[++$i])) {
+ break;
+ }
+
+ $row = $results[$i];
+ }
+
+ for (; $i < $resultsCount; $i++) {
+ $row = $results[$i];
+
+ $anyNull = false;
+ foreach ($this->dimensionOrder as $dimension) {
+ if ($row->$dimension === null) {
+ $anyNull = true;
+ array_pop($tree);
+ }
+ }
+
+ if ($anyNull) {
+ break;
+ }
+
+ $prepareHeaders($tree, $row);
+ }
+ } while ($i < $resultsCount);
+
+ $nodes = function (TreeNode $node) use (&$nodes, $isSortDirDesc): Generator {
+ yield $node->getValue();
+ $children = $node->getChildren();
+
+ uasort($children, function (TreeNode $a, TreeNode $b) use ($isSortDirDesc): int {
+ foreach ($this->getSeveritySortColumns() as $column) {
+ $comparison = $a->getValue()->$column <=> $b->getValue()->$column;
+ if ($comparison !== 0) {
+ return $comparison * ($isSortDirDesc ? -1 : 1);
+ }
+ }
+
+ // $a and $b are equal in terms of $priorities.
+ return 0;
+ });
+
+ foreach ($children as $node) {
+ yield from $nodes($node);
+ }
+ };
+
+ return $nodes($tree[1]);
+ }
+
+ protected function renderRow($row)
+ {
+ $htm = '';
+ if ($dimension = $this->startsDimension($row)) {
+ return $htm;
+ }
+
+ $htm .= $this->closeDimensionsForRow($row);
+ $htm .= $this->beginDimensionsForRow($row);
+ $htm .= $this->renderFacts($row);
+ $this->lastRow = $row;
+ return $htm;
+ }
+
+ protected function beginDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->beginDimensionsUpFrom($name, $row);
+ }
+ }
+
+ return '';
+ }
+
+ protected function beginDimensionsUpFrom($dimension, $row)
+ {
+ $htm = '';
+ $found = false;
+
+ foreach ($this->dimensionOrder as $name) {
+ if ($name === $dimension) {
+ $found = true;
+ }
+
+ if ($found) {
+ $htm .= $this->beginDimension($name, $row);
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->closeDimensionsDownTo($name);
+ }
+ }
+
+ return '';
+ }
+
+ protected function closeDimensionsDownTo($name)
+ {
+ $htm = '';
+
+ foreach ($this->reversedDimensions as $dimension) {
+ $htm .= $this->closeDimension($dimension);
+
+ if ($name === $dimension) {
+ break;
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensions()
+ {
+ $htm = '';
+ foreach ($this->reversedDimensions as $name) {
+ $htm .= $this->closeDimension($name);
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimension($name)
+ {
+ if (! $this->started) {
+ return '';
+ }
+
+ $indent = $this->getIndent($name);
+ return $indent . ' </div>' . "\n" . $indent . "</div><!-- $name -->\n";
+ }
+
+ protected function getIndent($name)
+ {
+ return str_repeat(' ', $this->getLevel($name));
+ }
+
+ protected function beginDimension($name, $row)
+ {
+ $indent = $this->getIndent($name);
+ if (! $this->started) {
+ $this->started = true;
+ }
+ $view = $this->view;
+ $dimension = $this->cube->getDimension($name);
+
+ return
+ $indent . '<div class="'
+ . $this->getDimensionClassString($name, $row)
+ . '">' . "\n"
+ . $indent . ' <div class="header"><a href="'
+ . $this->getDetailsUrl($name, $row)
+ . '" title="' . $view->escape(sprintf('Show details for %s: %s', $dimension->getLabel(), $row->$name)) . '"'
+ . ' data-base-target="_next">'
+ . $this->renderDimensionLabel($name, $row)
+ . '</a><a class="icon-filter" href="'
+ . $this->getSliceUrl($name, $row)
+ . '" title="' . $view->escape('Slice this cube') . '"></a></div>' . "\n"
+ . $indent . ' <div class="body">' . "\n";
+ }
+
+ /**
+ * Render the label for a given dimension name
+ *
+ * To have some context available, also
+ *
+ * @param $name
+ * @param $row
+ * @return string
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $caption = $row->$name;
+ if (empty($caption)) {
+ $caption = '_';
+ }
+
+ return $this->view->escape($caption);
+ }
+
+ protected function getDetailsUrl($name, $row)
+ {
+ $url = Url::fromPath($this->getDetailsBaseUrl());
+
+ if ($this->cube instanceof IcingaDbCube && $this->cube->hasBaseFilter()) {
+ /** @var Filter\Rule $baseFilter */
+ $baseFilter = $this->cube->getBaseFilter();
+ $url->setFilter($baseFilter);
+ }
+
+ $urlParams = $url->getParams();
+
+ $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices());
+ $urlParams->add('dimensions', DimensionParams::update($dimensions)->getParams());
+
+ foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) {
+ $urlParams->add($this->cube::SLICE_PREFIX . $dimensionName, $row->$dimensionName);
+ }
+
+ foreach ($this->cube->getSlices() as $key => $val) {
+ $urlParams->add($this->cube::SLICE_PREFIX . $key, $val);
+ }
+
+ return $url;
+ }
+
+ protected function getSliceUrl($name, $row)
+ {
+ return $this->view->url()
+ ->setParam($this->cube::SLICE_PREFIX . $name, $row->$name);
+ }
+
+ protected function isOuterDimension($name)
+ {
+ return $this->reversedDimensions[0] !== $name;
+ }
+
+ protected function getDimensionClassString($name, $row)
+ {
+ return implode(' ', $this->getDimensionClasses($name, $row));
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ return array('cube-dimension' . $this->getLevel($name));
+ }
+
+ protected function getLevel($name)
+ {
+ return $this->dimensionLevels[$name];
+ }
+
+ /**
+ * @return string
+ */
+ protected function beginContainer()
+ {
+ return '<div class="cube">' . "\n";
+ }
+
+ /**
+ * @return string
+ */
+ protected function endContainer()
+ {
+ return '</div>' . "\n";
+ }
+
+ /**
+ * Well... just to be on the safe side
+ */
+ public function __destruct()
+ {
+ unset($this->cube);
+ }
+}
diff --git a/library/Cube/CubeRenderer/HostStatusCubeRenderer.php b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
new file mode 100644
index 0000000..777f41b
--- /dev/null
+++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
@@ -0,0 +1,143 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Generator;
+use Icinga\Module\Cube\CubeRenderer;
+
+class HostStatusCubeRenderer extends CubeRenderer
+{
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->hosts_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+ $sums = $row;
+
+ $next = $this->cube->getDimensionAfter($name);
+ if ($next && isset($this->summaries->{$next->getName()})) {
+ $sums = $this->summaries->{$next->getName()};
+ }
+
+ $severityClass = [];
+ if ($sums->hosts_unhandled_down > 0) {
+ $severityClass[] = 'critical';
+ } elseif (isset($sums->hosts_unhandled_unreachable) && $sums->hosts_unhandled_unreachable > 0) {
+ $severityClass[] = 'unreachable';
+ }
+
+ if (empty($severityClass)) {
+ if ($sums->hosts_down > 0) {
+ $severityClass = ['critical', 'handled'];
+ } elseif (isset($sums->hosts_unreachable) && $sums->hosts_unreachable > 0) {
+ $severityClass = ['unreachable', 'handled'];
+ } else {
+ $severityClass[] = 'ok';
+ }
+ }
+
+ return array_merge($classes, $severityClass);
+ }
+
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = array();
+
+ if ($facts->hosts_unhandled_down > 0) {
+ $parts['critical'] = $facts->hosts_unhandled_down;
+ }
+
+ if (isset($facts->hosts_unhandled_unreachable) && $facts->hosts_unhandled_unreachable > 0) {
+ $parts['unreachable'] = $facts->hosts_unhandled_unreachable;
+ }
+
+ if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) {
+ $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down;
+ }
+
+ if (
+ isset($facts->hosts_unreachable, $facts->hosts_unhandled_unreachable)
+ && $facts->hosts_unreachable > 0
+ && $facts->hosts_unreachable >
+ $facts->hosts_unhandled_unreachable
+ ) {
+ $parts['unreachable handled'] = $facts->hosts_unreachable - $facts->hosts_unhandled_unreachable;
+ }
+
+ if (
+ $facts->hosts_cnt > $facts->hosts_down
+ && (! isset($facts->hosts_unreachable) || $facts->hosts_cnt > $facts->hosts_unreachable)
+ ) {
+ $ok = $facts->hosts_cnt - $facts->hosts_down;
+ if (isset($facts->hosts_unreachable)) {
+ $ok -= $facts->hosts_unreachable;
+ }
+
+ $parts['ok'] = $ok;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($count === 0) {
+ continue;
+ }
+
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/hosts/details';
+ }
+
+ protected function getSeveritySortColumns(): Generator
+ {
+ $columns = ['down', 'unreachable'];
+ foreach ($columns as $column) {
+ yield "hosts_unhandled_$column";
+ }
+
+ foreach ($columns as $column) {
+ yield "hosts_$column";
+ }
+ }
+}
diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
new file mode 100644
index 0000000..f115742
--- /dev/null
+++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
@@ -0,0 +1,149 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Generator;
+use Icinga\Module\Cube\CubeRenderer;
+
+class ServiceStatusCubeRenderer extends CubeRenderer
+{
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = [];
+
+ if ($facts->services_unhandled_critical > 0) {
+ $parts['critical'] = $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_unhandled_unknown > 0) {
+ $parts['unknown'] = $facts->services_unhandled_unknown;
+ }
+
+ if ($facts->services_unhandled_warning > 0) {
+ $parts['warning'] = $facts->services_unhandled_warning;
+ }
+
+ if ($facts->services_critical > 0 && $facts->services_critical > $facts->services_unhandled_critical) {
+ $parts['critical handled'] = $facts->services_critical - $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_unknown > 0 && $facts->services_unknown > $facts->services_unhandled_unknown) {
+ $parts['unknown handled'] = $facts->services_unknown - $facts->services_unhandled_unknown;
+ }
+
+ if ($facts->services_warning > 0 && $facts->services_warning > $facts->services_unhandled_warning) {
+ $parts['warning handled'] = $facts->services_warning - $facts->services_unhandled_warning;
+ }
+
+ if (
+ $facts->services_cnt > $facts->services_critical && $facts->services_cnt > $facts->services_warning
+ && $facts->services_cnt > $facts->services_unknown
+ ) {
+ $parts['ok'] = $facts->services_cnt - $facts->services_critical - $facts->services_warning -
+ $facts->services_unknown;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($count === 0) {
+ continue;
+ }
+
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->services_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+ $sums = $row;
+
+ $next = $this->cube->getDimensionAfter($name);
+ if ($next && isset($this->summaries->{$next->getName()})) {
+ $sums = $this->summaries->{$next->getName()};
+ }
+
+ if ($sums->services_unhandled_critical > 0) {
+ $severityClass[] = 'critical';
+ } elseif ($sums->services_unhandled_unknown > 0) {
+ $severityClass[] = 'unknown';
+ } elseif ($sums->services_unhandled_warning > 0) {
+ $severityClass[] = 'warning';
+ }
+
+ if (empty($severityClass)) {
+ if ($sums->services_critical > 0) {
+ $severityClass = ['critical', 'handled'];
+ } elseif ($sums->services_unknown > 0) {
+ $severityClass = ['unknown', 'handled'];
+ } elseif ($sums->services_warning > 0) {
+ $severityClass = ['warning', 'handled'];
+ } else {
+ $severityClass[] = 'ok';
+ }
+ }
+
+ return array_merge($classes, $severityClass);
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/services/details';
+ }
+
+ protected function getSeveritySortColumns(): Generator
+ {
+ $columns = ['critical', 'unknown', 'warning'];
+ foreach ($columns as $column) {
+ yield "services_unhandled_$column";
+ }
+
+ foreach ($columns as $column) {
+ yield "services_$column";
+ }
+ }
+}
diff --git a/library/Cube/Dimension.php b/library/Cube/Dimension.php
new file mode 100644
index 0000000..071e934
--- /dev/null
+++ b/library/Cube/Dimension.php
@@ -0,0 +1,70 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+/**
+ * Dimension interface
+ *
+ * All available dimensions must implement this interface
+ *
+ * @package Icinga\Module\Cube
+ */
+interface Dimension
+{
+ /**
+ * The name of this dimension
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Fetch label of this dimension
+ *
+ * @return string
+ */
+ public function getLabel();
+
+ /**
+ * Add a label
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function addLabel(string $label);
+
+ /**
+ * Set the label for the dimension
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setLabel(string $label);
+
+ /**
+ * Column expression
+ *
+ * This is the expression used to fetch the related column. Usually an SQL
+ * snippet when a relational database is involved
+ *
+ * @param Cube $cube
+ *
+ * @return string
+ */
+ public function getColumnExpression(Cube $cube);
+
+ /**
+ * Add this dimension to a cube
+ *
+ * This allows your dimension to apply itself to the Cube. That way your
+ * dimension is able to join optional tables and more
+ *
+ * @param Cube $cube
+ * @return void
+ */
+ public function addToCube(Cube $cube);
+}
diff --git a/library/Cube/DimensionParams.php b/library/Cube/DimensionParams.php
new file mode 100644
index 0000000..c0205bb
--- /dev/null
+++ b/library/Cube/DimensionParams.php
@@ -0,0 +1,85 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2020 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Web\Url;
+use ipl\Stdlib\Str;
+
+class DimensionParams
+{
+ /**
+ * @var array Raw dimensions
+ */
+ protected $dimensions = [];
+
+ /**
+ * @var string encoded dimensions separated by coma
+ */
+ protected $params;
+
+ public static function fromUrl(Url $url)
+ {
+ return static::fromString($url->getParam('dimensions'));
+ }
+
+ public static function fromArray(array $dimensions = [])
+ {
+ $self = new static();
+
+ $self->dimensions = array_filter($dimensions);
+
+ return $self;
+ }
+
+ public static function fromString($dimensions)
+ {
+ return static::fromArray(Str::trimSplit($dimensions));
+ }
+
+ /**
+ * @param $dimension
+ *
+ * @return $this
+ */
+ public function add($dimension)
+ {
+ if (! empty($dimension)) {
+ $this->dimensions[] = $dimension;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Overwrite dimensions
+ *
+ * @param $dimensions
+ *
+ * @return DimensionParams
+ */
+ public static function update($dimensions)
+ {
+ $self = new static();
+ $self->dimensions = $dimensions;
+
+ return $self;
+ }
+
+ /**
+ * @return string encoded dimensions separated by coma
+ */
+ public function getParams()
+ {
+ return implode(',', $this->dimensions);
+ }
+
+ /**
+ * @return array
+ */
+ public function getDimensions()
+ {
+ return $this->dimensions;
+ }
+}
diff --git a/library/Cube/Hook/ActionsHook.php b/library/Cube/Hook/ActionsHook.php
new file mode 100644
index 0000000..8ba8a7c
--- /dev/null
+++ b/library/Cube/Hook/ActionsHook.php
@@ -0,0 +1,99 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Hook;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Web\ActionLink;
+use Icinga\Module\Cube\Web\ActionLinks;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * ActionsHook
+ *
+ * Implement this hook in case your module wants to add links to the detail
+ * page shown for a slice.
+ *
+ * @package Icinga\Module\Cube\Hook
+ */
+abstract class ActionsHook
+{
+ /** @var ActionLinks */
+ private $actionLinks;
+
+ /**
+ * Your implementation should extend this method
+ *
+ * Then use the addActionLink() method, eventually combined with the
+ * createUrl() helper like this:
+ *
+ * <code>
+ * $this->addActionLink(
+ * $this->makeUrl('mymodule/controller/action', array('some' => 'param')),
+ * 'A shown title',
+ * 'A longer description text, should fit into the available square field',
+ * 'icon-name'
+ * );
+ * </code>
+ *
+ * For a list of available icon names please enable the Icinga Web 2 'doc'
+ * module and go to "Documentation" -> "Developer - Style" -> "Icons"
+ *
+ * @param Cube $cube
+ * @param View $view
+ *
+ * @return void
+ */
+ abstract public function prepareActionLinks(Cube $cube, View $view);
+
+ /**
+ * Lazy access to an ActionLinks object
+ *
+ * @return ActionLinks
+ */
+ public function getActionLinks()
+ {
+ if ($this->actionLinks === null) {
+ $this->actionLinks = new ActionLinks();
+ }
+ return $this->actionLinks;
+ }
+
+ /**
+ * Helper method instantiating an ActionLink object
+ *
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ *
+ * @return $this
+ */
+ public function addActionLink(Url $url, $title, $description, $icon)
+ {
+ $this->getActionLinks()->add(
+ new ActionLink($url, $title, $description, $icon)
+ );
+
+ return $this;
+ }
+
+ /**
+ * Helper method instantiating an Url object
+ *
+ * @param string $path
+ * @param array $params
+ * @return Url
+ */
+ public function makeUrl($path, $params = null)
+ {
+ $url = Url::fromPath($path);
+ if ($params !== null) {
+ $url->getParams()->mergeValues($params);
+ }
+
+ return $url;
+ }
+}
diff --git a/library/Cube/Hook/IcingaDbActionsHook.php b/library/Cube/Hook/IcingaDbActionsHook.php
new file mode 100644
index 0000000..63c24fe
--- /dev/null
+++ b/library/Cube/Hook/IcingaDbActionsHook.php
@@ -0,0 +1,125 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Hook;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use ipl\Web\Url;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+
+/**
+ * ActionsHook
+ *
+ * Implement this hook in case your module wants to add links to the detail
+ * page shown for a slice.
+ *
+ * @package Icinga\Module\Cube\Hook
+ */
+abstract class IcingaDbActionsHook
+{
+ /** @var Link[] */
+ private $actionLinks = [];
+
+ /**
+ * Create additional action links for the given cube
+ *
+ * @param IcingaDbCube $cube
+ * @return void
+ */
+ abstract public function createActionLinks(IcingaDbCube $cube);
+
+ /**
+ * Return the action links for the cube
+ *
+ * @return Link[]
+ */
+ final protected function getActionLinks(): array
+ {
+ return $this->actionLinks;
+ }
+
+ /**
+ * Helper method to populate action links array
+ *
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ *
+ * @return $this
+ */
+ final protected function addActionLink(Url $url, string $title, string $description, string $icon): self
+ {
+ $linkContent = (new HtmlDocument());
+ $linkContent->addHtml(new Icon($icon));
+ $linkContent->addHtml(HtmlElement::create('span', ['class' => 'title'], $title));
+ $linkContent->addHtml(HtmlElement::create('p', null, $description));
+
+ $this->actionLinks[] = new Link($linkContent, $url);
+
+ return $this;
+ }
+
+ /**
+ * Helper method instantiating an Url object
+ *
+ * @param string $path
+ * @param array $params
+ * @return Url
+ */
+ final protected function makeUrl(string $path, array $params = null): Url
+ {
+ $url = Url::fromPath($path);
+ if ($params !== null) {
+ $url->getParams()->mergeValues($params);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Render all links for all Hook implementations
+ *
+ * This is what the Cube calls when rendering details
+ *
+ * @param IcingaDbCube $cube
+ *
+ * @return string
+ */
+ public static function renderAll(Cube $cube)
+ {
+ $html = new HtmlDocument();
+
+ /** @var IcingaDbActionsHook $hook */
+ foreach (Hook::all('Cube/IcingaDbActions') as $hook) {
+ try {
+ $hook->createActionLinks($cube);
+ } catch (Exception $e) {
+ $html->addHtml(HtmlElement::create('li', ['class' => 'error'], $e->getMessage()));
+ }
+
+ foreach ($hook->getActionLinks() as $link) {
+ $html->addHtml(HtmlElement::create('li', null, $link));
+ }
+ }
+
+ if ($html->isEmpty()) {
+ $html->addHtml(
+ HtmlElement::create(
+ 'li',
+ ['class' => 'error'],
+ t('No action links have been provided for this cube')
+ )
+ );
+ }
+
+ return $html->render();
+ }
+}
diff --git a/library/Cube/IcingaDb/CustomVariableDimension.php b/library/Cube/IcingaDb/CustomVariableDimension.php
new file mode 100644
index 0000000..34a395c
--- /dev/null
+++ b/library/Cube/IcingaDb/CustomVariableDimension.php
@@ -0,0 +1,166 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Sql\Expression;
+use ipl\Stdlib\Filter;
+
+class CustomVariableDimension implements Dimension
+{
+ use Auth;
+
+ /** @var string Prefix for host custom variable */
+ public const HOST_PREFIX = 'host.vars.';
+
+ /** @var string Prefix for service custom variable */
+ public const SERVICE_PREFIX = 'service.vars.';
+
+ /** @var ?string variable source name */
+ protected $sourceName;
+
+ /** @var ?string Variable name without prefix */
+ protected $varName;
+
+ /** @var string Variable name with prefix */
+ protected $name;
+
+ protected $label;
+
+ protected $wantNull = false;
+
+ public function __construct($name)
+ {
+ if (preg_match('/^(host|service)\.vars\.(.*)/', $name, $matches)) {
+ $this->sourceName = $matches[1];
+ $this->varName = $matches[2];
+ }
+
+ $this->name = $name;
+ }
+
+ /**
+ * Get the variable name without prefix
+ *
+ * @return string
+ */
+ public function getVarName(): string
+ {
+ return $this->varName ?? $this->getName();
+ }
+
+ /**
+ * Get the variable source name
+ *
+ * @return ?string
+ */
+ public function getSourceName(): ?string
+ {
+ return $this->sourceName;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getLabel()
+ {
+ return $this->label ?: $this->getName();
+ }
+
+ public function setLabel($label)
+ {
+ $this->label = $label;
+
+ return $this;
+ }
+
+ public function addLabel($label)
+ {
+ if ($this->label === null) {
+ $this->setLabel($label);
+ } else {
+ $this->label .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaDbCube $cube
+ * @return Expression|string
+ */
+ public function getColumnExpression(Cube $cube)
+ {
+ $expression = $cube->getDb()->quoteIdentifier([$this->createCustomVarAlias(), 'flatvalue']);
+
+ if ($this->wantNull) {
+ return new Expression("COALESCE($expression, '-')");
+ }
+
+ return $expression;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ /** @var IcingaDbCube $cube */
+ $innerQuery = $cube->innerQuery();
+ $sourceTable = $this->getSourceName() ?? $innerQuery->getModel()->getTableName();
+
+ $subQuery = $innerQuery->createSubQuery(new CustomvarFlat(), $sourceTable . '.vars');
+ $subQuery->getSelectBase()->resetWhere(); // The link to the outer query is the ON condition
+ $subQuery->columns(['flatvalue', 'object_id' => $sourceTable . '.id']);
+ $subQuery->filter(Filter::like('flatname', $this->getVarName()));
+
+ // Values might not be unique (wildcard dimensions)
+ $subQueryModelAlias = $subQuery->getResolver()->getAlias($subQuery->getModel());
+ $subQuery->getSelectBase()->groupBy([
+ $subQueryModelAlias . '.flatname', // Required by postgres, if there are any custom variable protections
+ $subQueryModelAlias . '.flatvalue',
+ 'object_id'
+ ]);
+
+ $this->applyRestrictions($subQuery);
+
+ $subQueryAlias = $cube->getDb()->quoteIdentifier([$this->createCustomVarAlias()]);
+ $innerQuery->getSelectBase()->groupBy($subQueryAlias . '.flatvalue');
+
+ $sourceIdPath = '.id';
+ if ($innerQuery->getModel() instanceof Service && $sourceTable === 'host') {
+ $sourceIdPath = '.host_id';
+ }
+
+ $innerQuery->getSelectBase()->join(
+ [$subQueryAlias => $subQuery->assembleSelect()],
+ [
+ $subQueryAlias . '.object_id = '
+ . $innerQuery->getResolver()->getAlias($innerQuery->getModel()) . $sourceIdPath
+ ]
+ );
+ }
+
+ protected function createCustomVarAlias(): string
+ {
+ return implode('_', ['c', $this->getSourceName(), $this->getVarName()]);
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbCube.php b/library/Cube/IcingaDb/IcingaDbCube.php
new file mode 100644
index 0000000..44c7619
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbCube.php
@@ -0,0 +1,338 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Orm\Common\SortUtil;
+use ipl\Orm\Query;
+use ipl\Sql\Adapter\Pgsql;
+use ipl\Sql\Expression;
+use ipl\Sql\Select;
+use ipl\Stdlib\BaseFilter;
+
+abstract class IcingaDbCube extends Cube
+{
+ use Auth;
+ use BaseFilter;
+ use Database;
+
+ public const SLICE_PREFIX = 'slice.';
+ public const IS_USING_ICINGADB = true;
+
+ /** @var bool Whether to show problems only */
+ protected $problemsOnly = false;
+
+ /** @var string Sort param used to sort dimensions by value */
+ public const DIMENSION_VALUE_SORT_PARAM = 'value';
+
+ /** @var string Sort param used to sort dimensions by severity */
+ public const DIMENSION_SEVERITY_SORT_PARAM = 'severity';
+
+ /** @var Query The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ protected $objectsFilter;
+
+ /** @var array The sort order of dimensions, column as key and direction as value */
+ protected $sortBy;
+
+ abstract public function getObjectsFilter();
+ /**
+ * An IcingaDbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return Query
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return Query
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = $this->prepareInnerQuery();
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * Get our rollup query
+ *
+ * @return Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ $this->addDimension($this->createDimension($name));
+
+ return $this;
+ }
+
+ /**
+ * Set whether to show problems only
+ *
+ * @param bool $problemOnly
+ *
+ * @return $this
+ */
+ public function problemsOnly(bool $problemOnly = true): self
+ {
+ $this->problemsOnly = $problemOnly;
+
+ return $this;
+ }
+
+
+ /**
+ * Get whether to show problems only
+ *
+ * @return bool
+ */
+ public function isProblemsOnly(): bool
+ {
+ return $this->problemsOnly;
+ }
+
+ /**
+ * Fetch the host variable dimensions
+ *
+ * @return array
+ */
+ public function fetchHostVariableDimensions(): array
+ {
+ $query = Host::on($this->getDb())
+ ->with('customvar_flat')
+ ->columns('customvar_flat.flatname')
+ ->orderBy('customvar_flat.flatname');
+
+ $this->applyRestrictions($query);
+
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->customvar_flat->flatname);
+ $name = strtolower($name);
+ $dimensions[CustomVariableDimension::HOST_PREFIX . $name] = 'Host ' . $name;
+ }
+
+ return $dimensions;
+ }
+
+ /**
+ * Fetch the service variable dimensions
+ *
+ * @return array
+ */
+ public function fetchServiceVariableDimensions(): array
+ {
+ $query = Service::on($this->getDb())
+ ->with('customvar_flat')
+ ->columns('customvar_flat.flatname')
+ ->orderBy('customvar_flat.flatname');
+
+ $this->applyRestrictions($query);
+
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->customvar_flat->flatname);
+ $name = strtolower($name);
+ $dimensions[CustomVariableDimension::SERVICE_PREFIX . $name] = 'Service ' . $name;
+ }
+
+ return $dimensions;
+ }
+
+ /**
+ * Set sort by columns
+ *
+ * @param ?string $sortBy
+ *
+ * @return $this
+ */
+ public function sortBy(?string $sortBy): self
+ {
+ if (empty($sortBy)) {
+ return $this;
+ }
+
+ $this->sortBy = SortUtil::createOrderBy($sortBy)[0];
+
+ return $this;
+ }
+
+ /**
+ * Get sort by columns
+ *
+ * @return ?array Column as key and direction as value
+ */
+ public function getSortBy(): ?array
+ {
+ return $this->sortBy;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ protected function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery();
+ $select = $query->getSelectBase();
+
+ $columns = [];
+ foreach ($this->dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+ $dimension->addToCube($this);
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+
+ if ($this->hasSlice($name)) {
+ $select->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $select->columns($columns);
+
+ $this->applyRestrictions($query);
+ if ($this->hasBaseFilter()) {
+ $query->filter($this->getBaseFilter());
+ }
+ }
+
+ protected function prepareRollupQuery()
+ {
+ $dimensions = $this->listDimensions();
+ $this->finalizeInnerQuery();
+
+ $columns = [];
+ $groupBy = [];
+ foreach ($dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+
+ $columns[$quotedDimension] = 'f.' . $quotedDimension;
+ $groupBy[] = $quotedDimension;
+ }
+
+ $availableFacts = $this->getAvailableFactColumns();
+
+ foreach ($this->chosenFacts as $alias) {
+ $columns[$alias] = new Expression('SUM(f.' . $availableFacts[$alias] . ')');
+ }
+
+ if (! empty($groupBy)) {
+ if ($this->getDb()->getAdapter() instanceof Pgsql) {
+ $groupBy = 'ROLLUP(' . implode(', ', $groupBy) . ')';
+ } else {
+ $groupBy[count($groupBy) - 1] .= ' WITH ROLLUP';
+ }
+ }
+
+ $rollupQuery = new Select();
+ $rollupQuery->from(['f' => $this->innerQuery()->assembleSelect()])
+ ->columns($columns)
+ ->groupBy($groupBy);
+
+ return $rollupQuery;
+ }
+
+ protected function prepareFullQuery()
+ {
+ $rollupQuery = $this->rollupQuery();
+ $columns = [];
+ $orderBy = [];
+ $sortBy = $this->getSortBy();
+ foreach ($this->listColumns() as $column) {
+ $quotedColumn = $this->getDb()->quoteIdentifier([$column]);
+ $columns[$quotedColumn] = 'rollup.' . $quotedColumn;
+
+ if ($this->hasDimension($column)) {
+ $orderBy["($quotedColumn IS NOT NULL)"] = null;
+
+ $sortDir = 'ASC';
+ if ($sortBy && self::DIMENSION_VALUE_SORT_PARAM === $sortBy[0]) {
+ $sortDir = $sortBy[1] ?? 'ASC';
+ }
+
+ $orderBy[$quotedColumn] = $sortDir;
+ }
+ }
+
+ return (new Select())
+ ->from(['rollup' => $rollupQuery])
+ ->columns($columns)
+ ->orderBy($orderBy);
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->getDb()->fetchAll($query);
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbHostStatusCube.php b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
new file mode 100644
index 0000000..14e083f
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
@@ -0,0 +1,80 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\HoststateSummary;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Str;
+
+class IcingaDbHostStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'hosts_cnt' => 'hosts_total',
+ 'hosts_down' => 'hosts_down_handled + f.hosts_down_unhandled',
+ 'hosts_unhandled_down' => 'hosts_down_unhandled',
+ ];
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function listAvailableDimensions()
+ {
+ return $this->fetchHostVariableDimensions();
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = HoststateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Host())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ $this->innerQuery = $query;
+ return $this->innerQuery;
+ }
+
+ /**
+ * Return Filter for Hosts cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $hosts = $this->innerQuery()->columns(['host' => 'host.name']);
+ $hosts->getSelectBase()->resetGroupBy();
+
+ $filter = Filter::any();
+
+ foreach ($hosts as $object) {
+ $filter->add(Filter::equal('host.name', $object->host));
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
new file mode 100644
index 0000000..ac59de2
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
@@ -0,0 +1,94 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Str;
+
+class IcingaDbServiceStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'services_total',
+ 'services_critical' => 'services_critical_handled + f.services_critical_unhandled',
+ 'services_unhandled_critical' => 'services_critical_unhandled',
+ 'services_warning' => 'services_warning_handled + f.services_warning_unhandled',
+ 'services_unhandled_warning' => 'services_warning_unhandled',
+ 'services_unknown' => 'services_unknown_handled + f.services_unknown_unhandled',
+ 'services_unhandled_unknown' => 'services_unknown_unhandled',
+ ];
+ }
+
+ public function listAvailableDimensions()
+ {
+ return array_merge(
+ $this->fetchServiceVariableDimensions(),
+ $this->fetchHostVariableDimensions()
+ );
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = ServicestateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Service())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ return $query;
+ }
+
+ /**
+ * Return Filter for Services cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $services = $this->innerQuery()->columns([
+ 'host_name' => 'host.name',
+ 'service_name' => 'service.name'
+ ]);
+
+ $services->getSelectBase()->resetGroupBy();
+ $filter = Filter::any();
+
+ foreach ($services as $service) {
+ $filter->add(
+ Filter::all(
+ Filter::equal('service.name', $service->service_name),
+ Filter::equal('host.name', $service->host_name)
+ )
+ );
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/Ido/CustomVarDimension.php b/library/Cube/Ido/CustomVarDimension.php
new file mode 100644
index 0000000..df45497
--- /dev/null
+++ b/library/Cube/Ido/CustomVarDimension.php
@@ -0,0 +1,146 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+
+/**
+ * CustomVarDimension
+ *
+ * This provides dimenstions for custom variables available in the IDO
+ *
+ * TODO: create safe aliases for special characters
+ *
+ * @package Icinga\Module\Cube\Ido
+ */
+class CustomVarDimension implements Dimension
+{
+ public const TYPE_HOST = 'host';
+
+ public const TYPE_SERVICE = 'service';
+
+ /**
+ * @var string custom variable name
+ */
+ protected $varName;
+
+ /**
+ * @var string custom variable label
+ */
+ protected $varLabel;
+
+ /**
+ * @var bool Whether null values should be shown
+ */
+ protected $wantNull = false;
+
+ /** @var string Type of the custom var */
+ protected $type;
+
+ /**
+ * CustomVarDimension constructor.
+ *
+ * @param $varName
+ * @param string $type Type of the custom var
+ */
+ public function __construct($varName, $type = null)
+ {
+ $this->varName = $varName;
+ $this->type = $type;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->varName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLabel()
+ {
+ return $this->varLabel ?: $this->getName();
+ }
+
+ public function setLabel($label)
+ {
+ $this->varLabel = $label;
+
+ return $this;
+ }
+
+ public function addLabel($label)
+ {
+ if ($this->varLabel === null) {
+ $this->setLabel($label);
+ } else {
+ $this->varLabel .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ public function getColumnExpression(Cube $cube)
+ {
+ /** @var IdoCube $cube */
+ if ($this->wantNull) {
+ return 'COALESCE(' . $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']) . ", '-')";
+ } else {
+ return $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']);
+ }
+ }
+
+ protected function safeVarname($name)
+ {
+ return $name;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ switch ($this->type) {
+ case self::TYPE_HOST:
+ $objectId = 'ho.object_id';
+ break;
+ case self::TYPE_SERVICE:
+ $objectId = 'so.object_id';
+ break;
+ default:
+ $objectId = 'o.object_id';
+ }
+ $name = $this->safeVarname($this->varName);
+ /** @var IdoCube $cube */
+ $alias = $cube->db()->quoteIdentifier(['c_' . $name]);
+
+ if ($cube->isPgsql()) {
+ $on = "LOWER($alias.varname) = ?";
+ $name = strtolower($name);
+ } else {
+ $on = $alias . '.varname = ? COLLATE latin1_general_ci';
+ }
+
+ $cube->innerQuery()->joinLeft(
+ array($alias => $cube->tableName('icinga_customvariablestatus')),
+ $cube->db()->quoteInto($on, $name)
+ . ' AND ' . $alias . '.object_id = ' . $objectId,
+ array()
+ )->group($alias . '.varvalue');
+ }
+}
diff --git a/library/Cube/Ido/DataView/Hoststatus.php b/library/Cube/Ido/DataView/Hoststatus.php
new file mode 100644
index 0000000..32ee44b
--- /dev/null
+++ b/library/Cube/Ido/DataView/Hoststatus.php
@@ -0,0 +1,17 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido\DataView;
+
+use Icinga\Data\ConnectionInterface;
+use Icinga\Module\Cube\Ido\Query\HoststatusQuery;
+
+class Hoststatus extends \Icinga\Module\Monitoring\DataView\Hoststatus
+{
+ public function __construct(ConnectionInterface $connection, array $columns = null)
+ {
+ $this->connection = $connection;
+ $this->query = new HoststatusQuery($connection->getResource(), $columns);
+ }
+}
diff --git a/library/Cube/Ido/DbCube.php b/library/Cube/Ido/DbCube.php
new file mode 100644
index 0000000..5fc5b47
--- /dev/null
+++ b/library/Cube/Ido/DbCube.php
@@ -0,0 +1,298 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Data\Db\DbConnection;
+use Icinga\Module\Cube\Cube;
+
+abstract class DbCube extends Cube
+{
+ /** @var DbConnection */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var ZfSelectWrapper The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var \Zend_Db_Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var \Zend_Db_Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ /** @var string Database name. Allows to eventually join over multiple dbs */
+ protected $dbName;
+
+ /** @var array Key/value array containing our chosen facts and the corresponding SQL expression */
+ protected $factColumns = array();
+
+ /**
+ * A DbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return \Zend_Db_Select
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Set a database connection
+ *
+ * @param DbConnection $connection
+ * @return $this
+ */
+ public function setConnection(DbConnection $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ return $this;
+ }
+
+ /**
+ * Prepare the query and fetch all data
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->db()->fetchAll($query);
+ }
+
+ /**
+ * Choose a one or more facts
+ *
+ * This also initializes a fact column lookup array
+ *
+ * @param array $facts
+ * @return $this
+ */
+ public function chooseFacts(array $facts)
+ {
+ parent::chooseFacts($facts);
+
+ $this->factColumns = array();
+ $columns = $this->getAvailableFactColumns();
+ foreach ($this->chosenFacts as $name) {
+ $this->factColumns[$name] = $columns[$name];
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ * @return $this
+ */
+ public function setDbName($name)
+ {
+ $this->dbName = $name;
+ return $this;
+ }
+
+ /**
+ * Gives back the table name, eventually prefixed with a defined DB name
+ *
+ * @param string $name
+ * @return string
+ */
+ public function tableName($name)
+ {
+ if ($this->dbName === null) {
+ return $name;
+ } else {
+ return $this->dbName . '.' . $name;
+ }
+ }
+
+ /**
+ * Returns an eventually defined DB name
+ *
+ * @return string|null
+ */
+ public function getDbName()
+ {
+ return $this->dbName;
+ }
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return ZfSelectWrapper
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = new ZfSelectWrapper($this->prepareInnerQuery());
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ public function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery()->unwrap();
+ $columns = array();
+ foreach ($this->dimensions as $name => $dimension) {
+ $dimension->addToCube($this);
+ if ($this->hasSlice($name)) {
+ $query->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$name] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $c = [];
+
+ foreach ($columns + $this->factColumns as $k => $v) {
+ $c[$this->db()->quoteIdentifier([$k])] = $v;
+ }
+
+ $query->columns($c);
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * The full query wraps the rollup query in a sub-query to work around
+ * MySQL limitations. This is required to not get into trouble when ordering,
+ * especially combined with the need to keep control over (eventually desired)
+ * NULL value fact columns
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function prepareFullQuery()
+ {
+ $alias = 'rollup';
+ $cols = $this->listColumns();
+
+ $columns = array();
+
+ foreach ($cols as $col) {
+ $columns[$this->db()->quoteIdentifier([$col])] = $alias . '.' . $this->db()->quoteIdentifier([$col]);
+ }
+
+ $select = $this->db()->select()->from(
+ array($alias => $this->rollupQuery()),
+ $columns
+ );
+
+ foreach ($columns as $col) {
+ $select->order('(' . $col . ' IS NOT NULL)');
+ $select->order($col);
+ }
+
+ return $select;
+ }
+
+ /**
+ * Provide access to our DB
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function db()
+ {
+ return $this->db;
+ }
+
+ /**
+ * Whether our connection is PostgreSQL
+ *
+ * @return bool
+ */
+ public function isPgsql()
+ {
+ return $this->connection->getDbType() === 'pgsql';
+ }
+
+
+ /**
+ * This prepares the rollup query
+ *
+ * Inner query is wrapped in a subquery, summaries for all facts are
+ * fetched. Rollup considers all defined dimensions and expects them
+ * to exist as columns in the innerQuery
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function prepareRollupQuery()
+ {
+ $alias = 'sub';
+
+ $dimensions = array_map(function ($val) {
+ return $this->db()->quoteIdentifier([$val]);
+ }, array_keys($this->listDimensions()));
+ $this->finalizeInnerQuery();
+ $columns = array();
+ foreach ($dimensions as $dimension) {
+ $columns[$dimension] = $alias . '.' . $dimension;
+ }
+
+ foreach ($this->listFacts() as $fact) {
+ $columns[$fact] = 'SUM(' . $fact . ')';
+ }
+
+ $select = $this->db()->select()->from(
+ array($alias => $this->innerQuery()->unwrap()),
+ $columns
+ );
+
+ if ($this->isPgsql()) {
+ $select->group('ROLLUP (' . implode(', ', $dimensions) . ')');
+ } else {
+ $select->group('(' . implode('), (', $dimensions) . ') WITH ROLLUP');
+ }
+
+ return $select;
+ }
+}
diff --git a/library/Cube/Ido/IdoCube.php b/library/Cube/Ido/IdoCube.php
new file mode 100644
index 0000000..7ad609d
--- /dev/null
+++ b/library/Cube/Ido/IdoCube.php
@@ -0,0 +1,219 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Util\GlobFilter;
+
+/**
+ * IdoCube
+ *
+ * Base class for IDO-related cubes
+ *
+ * @package Icinga\Module\Cube\Ido
+ */
+abstract class IdoCube extends DbCube
+{
+ /** @var array */
+ protected $availableFacts = array();
+
+ /** @var string We ask for the IDO version for compatibility reasons */
+ protected $idoVersion;
+
+ /** @var MonitoringBackend */
+ protected $backend;
+
+ /**
+ * Cache for {@link filterProtectedCustomvars()}
+ *
+ * @var string|null
+ */
+ protected $protectedCustomvars;
+
+ /** @var GlobFilter The properties to hide from the user */
+ protected $blacklistedProperties;
+
+ public const IS_USING_ICINGADB = false;
+
+ /**
+ * Add a specific named dimension
+ *
+ * Right now these are just custom vars, we might support group memberships
+ * or other properties in future
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function addDimensionByName($name): self
+ {
+ if (count($this->filterProtectedCustomvars([$name])) === 1) {
+ $this->addDimension($this->createDimension($name));
+ }
+
+ return $this;
+ }
+
+ /**
+ * We can steal the DB connection directly from a Monitoring backend
+ *
+ * @param MonitoringBackend $backend
+ * @return $this
+ */
+ public function setBackend(MonitoringBackend $backend)
+ {
+ $this->backend = $backend;
+
+ $resource = $backend->getResource();
+ $resource->getDbAdapter()
+ ->getConnection()
+ ->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_NATURAL);
+
+ $this->setConnection($resource);
+
+ return $this;
+ }
+
+ /**
+ * Provice access to our DB resource
+ *
+ * This lazy-loads the default monitoring backend in case no DB has been
+ * given
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function db()
+ {
+ $this->requireBackend();
+ return parent::db();
+ }
+
+ /**
+ * Returns the Icinga IDO version
+ *
+ * @return string
+ */
+ protected function getIdoVersion()
+ {
+ if ($this->idoVersion === null) {
+ $db = $this->db();
+ $this->idoVersion = $db->fetchOne(
+ $db->select()->from('icinga_dbversion', 'version')
+ );
+ }
+
+ return $this->idoVersion;
+ }
+
+ /**
+ * Steal the default monitoring DB resource...
+ *
+ * ...in case none has been defined otherwise
+ *
+ * @return void
+ */
+ protected function requireBackend()
+ {
+ if ($this->db === null) {
+ $this->setBackend(MonitoringBackend::instance());
+ }
+ }
+
+ protected function getMonitoringRestriction()
+ {
+ $restriction = Filter::matchAny();
+ $restriction->setAllowedFilterColumns(array(
+ 'host_name',
+ 'hostgroup_name',
+ 'instance_name',
+ 'service_description',
+ 'servicegroup_name',
+ function ($c) {
+ return preg_match('/^_(?:host|service)_/i', $c);
+ }
+ ));
+
+ $filters = Auth::getInstance()->getUser()->getRestrictions('monitoring/filter/objects');
+
+ foreach ($filters as $filter) {
+ if ($filter === '*') {
+ return Filter::matchAny();
+ }
+ try {
+ $restriction->addFilter(Filter::fromQueryString($filter));
+ } catch (QueryException $e) {
+ throw new ConfigurationError(
+ 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s',
+ 'monitoring/filter/objects',
+ $filter,
+ implode(', ', array(
+ 'instance_name',
+ 'host_name',
+ 'hostgroup_name',
+ 'service_description',
+ 'servicegroup_name',
+ '_(host|service)_<customvar-name>'
+ )),
+ $e
+ );
+ }
+ }
+
+ return $restriction;
+ }
+
+ /**
+ * Return the given array without values matching the custom variables protected by the monitoring module
+ *
+ * @param string[] $customvars
+ *
+ * @return string[]
+ */
+ protected function filterProtectedCustomvars(array $customvars)
+ {
+ if ($this->blacklistedProperties === null) {
+ $this->blacklistedProperties = new GlobFilter(
+ Auth::getInstance()->getRestrictions('monitoring/blacklist/properties')
+ );
+ }
+
+ if ($this instanceof IdoServiceStatusCube) {
+ $type = 'service';
+ } else {
+ $type = 'host';
+ }
+
+ $customvars = $this->blacklistedProperties->removeMatching(
+ [$type => ['vars' => array_flip($customvars)]]
+ );
+
+ $customvars = isset($customvars[$type]['vars']) ? array_flip($customvars[$type]['vars']) : [];
+
+ if ($this->protectedCustomvars === null) {
+ $config = Config::module('monitoring')->get('security', 'protected_customvars');
+ $protectedCustomvars = array();
+
+ foreach (preg_split('~,~', $config, -1, PREG_SPLIT_NO_EMPTY) as $pattern) {
+ $regex = array();
+ foreach (explode('*', $pattern) as $literal) {
+ $regex[] = preg_quote($literal, '/');
+ }
+
+ $protectedCustomvars[] = implode('.*', $regex);
+ }
+
+ $this->protectedCustomvars = empty($protectedCustomvars)
+ ? '/^$/'
+ : '/^(?:' . implode('|', $protectedCustomvars) . ')$/';
+ }
+
+ return preg_grep($this->protectedCustomvars, $customvars, PREG_GREP_INVERT);
+ }
+}
diff --git a/library/Cube/Ido/IdoHostStatusCube.php b/library/Cube/Ido/IdoHostStatusCube.php
new file mode 100644
index 0000000..3f79dc8
--- /dev/null
+++ b/library/Cube/Ido/IdoHostStatusCube.php
@@ -0,0 +1,97 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Cube\Ido\DataView\Hoststatus;
+
+class IdoHostStatusCube extends IdoCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAvailableFactColumns()
+ {
+ return array(
+ 'hosts_cnt' => 'SUM(CASE WHEN hs.has_been_checked = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1'
+ . ' THEN 1 ELSE 0 END)',
+ 'hosts_unhandled_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1'
+ . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'hosts_unreachable' => 'SUM(CASE WHEN hs.current_state = 2 THEN 1 ELSE 0 END)',
+ 'hosts_unhandled_unreachable' => 'SUM(CASE WHEN hs.current_state = 2'
+ . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ );
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVarDimension($name, CustomVarDimension::TYPE_HOST);
+ }
+
+ /**
+ * This returns a list of all available Dimensions
+ *
+ * @return array
+ */
+ public function listAvailableDimensions()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('hoststatus');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ $select
+ ->columns('cv.varname')
+ ->join(
+ ['cv' => $this->tableName('icinga_customvariablestatus')],
+ 'cv.object_id = ho.object_id',
+ []
+ )
+ ->group('cv.varname');
+
+ if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) {
+ $select->where('cv.is_json = 0');
+ }
+
+ $select->order('cv.varname');
+
+ $dimensions = $this->filterProtectedCustomvars($this->db()->fetchCol($select));
+ $keys = array_map('strtolower', $dimensions);
+
+ return array_combine($keys, $dimensions);
+ }
+
+ public function prepareInnerQuery()
+ {
+ $this->requireBackend();
+
+ $view = new Hoststatus($this->backend);
+
+ $view->getQuery()->requireColumn('host_state');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ return $select;
+ }
+}
diff --git a/library/Cube/Ido/IdoServiceStatusCube.php b/library/Cube/Ido/IdoServiceStatusCube.php
new file mode 100644
index 0000000..a403645
--- /dev/null
+++ b/library/Cube/Ido/IdoServiceStatusCube.php
@@ -0,0 +1,97 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+
+class IdoServiceStatusCube extends IdoCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'SUM(CASE WHEN ss.has_been_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_unhandled_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)',
+ 'services_unhandled_warning' => 'SUM(CASE WHEN ss.current_state = 1'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)',
+ 'services_unhandled_unknown' => 'SUM(CASE WHEN ss.current_state = 3'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ ];
+ }
+
+ /**
+ * This returns a list of all available Dimensions
+ *
+ * @return array
+ */
+ public function listAvailableDimensions()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('servicestatus');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ $select
+ ->columns('cv.varname')
+ ->join(
+ ['cv' => $this->tableName('icinga_customvariablestatus')],
+ 'cv.object_id = so.object_id',
+ []
+ )
+ ->group('cv.varname');
+
+ if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) {
+ $select->where('cv.is_json = 0');
+ }
+
+ $select->order('cv.varname');
+
+ $dimensions = $this->filterProtectedCustomvars($this->db()->fetchCol($select));
+ $keys = array_map('strtolower', $dimensions);
+
+ return array_combine($keys, $dimensions);
+ }
+
+ public function prepareInnerQuery()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('servicestatus');
+
+ $view->getQuery()->requireColumn('service_state');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ return $select;
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVarDimension($name, CustomVarDimension::TYPE_SERVICE);
+ }
+}
diff --git a/library/Cube/Ido/Query/HoststatusQuery.php b/library/Cube/Ido/Query/HoststatusQuery.php
new file mode 100644
index 0000000..6a9aa96
--- /dev/null
+++ b/library/Cube/Ido/Query/HoststatusQuery.php
@@ -0,0 +1,47 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido\Query;
+
+use Exception;
+use Icinga\Application\Version;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
+
+class HoststatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\HoststatusQuery
+{
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup',
+ 'services' => 'servicestatus'
+ );
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'servicestatus') {
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+
+ protected function createSubQueryFilter(FilterExpression $filter, $queryName)
+ {
+ try {
+ return parent::createSubQueryFilter($filter, $queryName);
+ } catch (Exception $e) {
+ if (version_compare(Version::VERSION, '2.10.0', '>=')) {
+ throw $e;
+ }
+
+ if ($e->getMessage() === 'Undefined array key 0' && basename($e->getFile()) === 'IdoQuery.php') {
+ // Ensures compatibility with earlier Icinga Web 2 versions
+ throw new NotImplementedError('');
+ } else {
+ throw $e;
+ }
+ }
+ }
+}
diff --git a/library/Cube/Ido/ZfSelectWrapper.php b/library/Cube/Ido/ZfSelectWrapper.php
new file mode 100644
index 0000000..745d7a5
--- /dev/null
+++ b/library/Cube/Ido/ZfSelectWrapper.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Icinga\Module\Cube\Ido;
+
+/**
+ * Since version 1.1.0 we're using the monitoring module's queries as the cubes' base queries.
+ * Before, the host object table was available using the alias 'o'. Now it's 'ho'.
+ * Without this wrapper, the action link hook provided by the director would fail because it relies on the alias 'o'.
+ */
+class ZfSelectWrapper
+{
+ /** @var \Zend_Db_Select */
+ protected $select;
+
+ public function __construct(\Zend_Db_Select $select)
+ {
+ $this->select = $select;
+ }
+
+ /**
+ * Get the underlying Zend_Db_Select query
+ *
+ * @return \Zend_Db_Select
+ */
+ public function unwrap()
+ {
+ return $this->select;
+ }
+
+ /**
+ * {@see \Zend_Db_Select::reset()}
+ */
+ public function reset($part = null)
+ {
+ $this->select->reset($part);
+
+ return $this;
+ }
+
+ /**
+ * {@see \Zend_Db_Select::columns()}
+ */
+ public function columns($cols = '*', $correlationName = null)
+ {
+ if (is_array($cols)) {
+ foreach ($cols as $alias => &$col) {
+ if (substr($col, 0, 2) === 'o.') {
+ $col = 'ho.' . substr($col, 2);
+ }
+ }
+ }
+
+ return $this->select->columns($cols, $correlationName);
+ }
+
+ /**
+ * Proxy Zend_Db_Select method calls
+ *
+ * @param string $name The name of the method to call
+ * @param array $arguments Arguments for the method to call
+ *
+ * @return mixed
+ *
+ * @throws \BadMethodCallException If the called method does not exist
+ */
+ public function __call($name, array $arguments)
+ {
+ if (! method_exists($this->select, $name)) {
+ $class = get_class($this);
+ $message = "Call to undefined method $class::$name";
+
+ throw new \BadMethodCallException($message);
+ }
+
+ return call_user_func_array([$this->select, $name], $arguments);
+ }
+}
diff --git a/library/Cube/ProvidedHook/Cube/IcingaDbActions.php b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php
new file mode 100644
index 0000000..1fbd05d
--- /dev/null
+++ b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Cube\ProvidedHook\Cube;
+
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbServiceStatusCube;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+
+class IcingaDbActions extends IcingaDbActionsHook
+{
+ public function createActionLinks(IcingaDbCube $cube)
+ {
+ $type = 'host';
+ if ($cube instanceof IcingaDbServiceStatusCube) {
+ $type = 'service';
+ }
+
+ $filter = Filter::all();
+ if ($cube->hasBaseFilter()) {
+ $filter->add($cube->getBaseFilter());
+ }
+
+ foreach ($cube->getSlices() as $dimension => $slice) {
+ $filter->add(Filter::equal($dimension, $slice));
+ }
+
+ $url = Url::fromPath('icingadb/' . $type . 's');
+ $url->setFilter($filter);
+
+ if ($type === 'host') {
+ $this->addActionLink(
+ $url,
+ t('Show hosts status'),
+ t('This shows all matching hosts and their current state in Icinga DB Web'),
+ 'server'
+ );
+ } else {
+ $this->addActionLink(
+ $url,
+ t('Show services status'),
+ t('This shows all matching hosts and their current state in Icinga DB Web'),
+ 'cog'
+ );
+ }
+ }
+}
diff --git a/library/Cube/ProvidedHook/Cube/MonitoringActions.php b/library/Cube/ProvidedHook/Cube/MonitoringActions.php
new file mode 100644
index 0000000..ae65c67
--- /dev/null
+++ b/library/Cube/ProvidedHook/Cube/MonitoringActions.php
@@ -0,0 +1,53 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\ProvidedHook\Cube;
+
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Ido\IdoHostStatusCube;
+use Icinga\Module\Cube\Ido\IdoServiceStatusCube;
+use Icinga\Web\View;
+
+/**
+ * MonitoringActionLinks
+ *
+ * An action link hook implementation linking to matching hosts/services in the
+ * monitoring module
+ */
+class MonitoringActions extends ActionsHook
+{
+ public function prepareActionLinks(Cube $cube, View $view)
+ {
+ if ($cube instanceof IdoHostStatusCube) {
+ $vars = [];
+ foreach ($cube->getSlices() as $key => $val) {
+ $vars['_host_' . $key] = $val;
+ }
+
+ $url = 'monitoring/list/hosts';
+
+ $this->addActionLink(
+ $this->makeUrl($url, $vars),
+ $view->translate('Show hosts status'),
+ $view->translate('This shows all matching hosts and their current state in the monitoring module'),
+ 'host'
+ );
+ } elseif ($cube instanceof IdoServiceStatusCube) {
+ $vars = [];
+ foreach ($cube->getSlices() as $key => $val) {
+ $vars['_service_' . $key] = $val;
+ }
+
+ $url = 'monitoring/list/services';
+
+ $this->addActionLink(
+ $this->makeUrl($url, $vars),
+ $view->translate('Show services status'),
+ $view->translate('This shows all matching services and their current state in the monitoring module'),
+ 'host'
+ );
+ }
+ }
+}
diff --git a/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php
new file mode 100644
index 0000000..a32310a
--- /dev/null
+++ b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php
@@ -0,0 +1,11 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+
+class IcingadbSupport extends IcingadbSupportHook
+{
+}
diff --git a/library/Cube/Web/ActionLink.php b/library/Cube/Web/ActionLink.php
new file mode 100644
index 0000000..c9ad87b
--- /dev/null
+++ b/library/Cube/Web/ActionLink.php
@@ -0,0 +1,103 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * ActionLink
+ *
+ * ActionLinksHook implementations return instances of this class
+ *
+ * @package Icinga\Module\Cube\Web
+ */
+class ActionLink
+{
+ /** @var Url */
+ protected $url;
+
+ /** @var string */
+ protected $title;
+
+ /** @var string */
+ protected $description;
+
+ /** @var string */
+ protected $icon;
+
+ /**
+ * ActionLink constructor.
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ */
+ public function __construct(Url $url, $title, $description, $icon)
+ {
+ $this->url = $url;
+ $this->title = $title;
+ $this->description = $description;
+ $this->icon = $icon;
+ }
+
+ /**
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ /**
+ * Render our icon
+ *
+ * @param View $view
+ * @return string
+ */
+ protected function renderIcon(View $view)
+ {
+ return $view->icon($this->getIcon());
+ }
+
+ /**
+ * @param View $view
+ * @return string
+ */
+ public function render(View $view)
+ {
+ return sprintf(
+ '<a href="%s">%s<span class="title">%s</span><p>%s</p></a>',
+ $this->getUrl(),
+ $this->renderIcon($view),
+ $view->escape($this->getTitle()),
+ $view->escape($this->getDescription())
+ );
+ }
+}
diff --git a/library/Cube/Web/ActionLinks.php b/library/Cube/Web/ActionLinks.php
new file mode 100644
index 0000000..4b84fac
--- /dev/null
+++ b/library/Cube/Web/ActionLinks.php
@@ -0,0 +1,115 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Web\View;
+
+/**
+ * ActionLink
+ *
+ * ActionsHook implementations return instances of this class
+ *
+ * @package Icinga\Module\Cube\Web
+ */
+class ActionLinks
+{
+ /** @var ActionLink[] */
+ protected $links = array();
+
+ /**
+ * Get all links for all Hook implementations
+ *
+ * This is what the Cube calls when rendering details
+ *
+ * @param Cube $cube
+ * @param View $view
+ *
+ * @return string
+ */
+ public static function renderAll(Cube $cube, View $view)
+ {
+ $html = array();
+
+ /** @var ActionsHook $hook */
+ foreach (Hook::all('Cube/Actions') as $hook) {
+ try {
+ $hook->prepareActionLinks($cube, $view);
+ } catch (Exception $e) {
+ $html[] = self::renderErrorItem($e, $view);
+ }
+
+ foreach ($hook->getActionLinks()->getLinks() as $link) {
+ $html[] = '<li>' . $link->render($view) . '</li>';
+ }
+ }
+
+ if (empty($html)) {
+ $html[] = self::renderErrorItem(
+ $view->translate('No action links have been provided for this cube'),
+ $view
+ );
+ }
+
+ return implode("\n", $html) . "\n";
+ }
+
+ /**
+ * @param Exception|string $error
+ * @param View $view
+ * @return string
+ */
+ private static function renderErrorItem($error, View $view)
+ {
+ if ($error instanceof Exception) {
+ $error = $error->getMessage();
+ }
+ return '<li class="error">' . $view->escape($error) . '</li>';
+ }
+
+ /**
+ * Add an ActionLink to this set of actions
+ *
+ * @param ActionLink $link
+ * @return $this
+ */
+ public function add(ActionLink $link)
+ {
+ $this->links[] = $link;
+ return $this;
+ }
+
+ /**
+ * @return ActionLink[]
+ */
+ public function getLinks()
+ {
+ return $this->links;
+ }
+
+ /**
+ * @param View $view
+ *
+ * @return string
+ */
+ public function render(View $view)
+ {
+ $links = $this->getLinks();
+ if (empty($links)) {
+ return '';
+ }
+
+ $html = '<ul class="action-links">';
+ foreach ($links as $link) {
+ $html .= $link->render($view);
+ }
+ $html .= '</ul>';
+
+ return $html;
+ }
+}
diff --git a/library/Cube/Web/Controller.php b/library/Cube/Web/Controller.php
new file mode 100644
index 0000000..028a744
--- /dev/null
+++ b/library/Cube/Web/Controller.php
@@ -0,0 +1,297 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Module\Cube\DimensionParams;
+use Icinga\Module\Cube\Forms\DimensionsForm;
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\IcingaDb\CustomVariableDimension;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Web\Control\ProblemToggle;
+use ipl\Html\FormElement\CheckboxElement;
+use ipl\Html\HtmlString;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Str;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Compat\SearchControls;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Tabs;
+
+abstract class Controller extends CompatController
+{
+ use SearchControls;
+ use Database;
+ use Auth;
+
+ /** @var string[] Preserved params for searchbar and search editor controls */
+ protected $preserveParams = [
+ 'dimensions',
+ 'showSettings',
+ 'wantNull',
+ 'problems',
+ 'sort'
+ ];
+
+ /** @var Filter\Rule Filter from query string parameters */
+ private $filter;
+
+ /**
+ * Return this controllers' cube
+ *
+ * @return IcingaDbCube
+ */
+ abstract protected function getCube(): IcingaDbCube;
+
+ /**
+ * Get the filter created from query string parameters
+ *
+ * @return Filter\Rule
+ */
+ public function getFilter(): Filter\Rule
+ {
+ if ($this->filter === null) {
+ $this->filter = QueryString::parse((string) $this->params);
+ }
+
+ return $this->filter;
+ }
+
+ public function detailsAction(): void
+ {
+ $cube = $this->prepareCube();
+ $this->getTabs()->add('details', [
+ 'label' => $this->translate('Cube details'),
+ 'url' => $this->getRequest()->getUrl()
+ ])->activate('details');
+
+ $cube->setBaseFilter($this->getFilter());
+
+ $this->setTitle($cube->getSlicesLabel());
+ $this->view->links = IcingaDbActionsHook::renderAll($cube);
+
+ $this->addContent(
+ HtmlString::create($this->view->render('/cube-details.phtml'))
+ );
+ }
+
+ protected function renderCube(): void
+ {
+ $cube = $this->prepareCube();
+ $this->setTitle(sprintf(
+ $this->translate('Cube: %s'),
+ $cube->getPathLabel()
+ ));
+
+ $this->params->shift('format');
+ $showSettings = $this->params->shift('showSettings');
+
+ $query = $cube->innerQuery();
+ $problemsOnly = (bool) $this->params->shift('problems', false);
+ $problemToggle = (new ProblemToggle($problemsOnly ?: null))
+ ->setIdProtector([$this->getRequest(), 'protectId'])
+ ->on(ProblemToggle::ON_SUCCESS, function (ProblemToggle $form) {
+ /** @var CheckboxElement $problems */
+ $problems = $form->getElement('problems');
+ if (! $problems->isChecked()) {
+ $this->redirectNow(Url::fromRequest()->remove('problems'));
+ } else {
+ $this->redirectNow(Url::fromRequest()->setParam('problems'));
+ }
+ })->handleRequest($this->getServerRequest());
+
+ $this->addControl($problemToggle);
+
+ $sortControl = SortControl::create([
+ IcingaDbCube::DIMENSION_VALUE_SORT_PARAM => t('Value'),
+ IcingaDbCube::DIMENSION_SEVERITY_SORT_PARAM . ' desc' => t('Severity'),
+ ]);
+
+ $this->params->shift($sortControl->getSortParam());
+ $cube->sortBy($sortControl->getSort());
+ $this->addControl($sortControl);
+
+ $searchBar = $this->createSearchBar(
+ $query,
+ $this->preserveParams
+ );
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ if ($problemsOnly) {
+ $filter = Filter::all($filter, Filter::equal('state.is_problem', true));
+ }
+
+ $cube->setBaseFilter($filter);
+ $cube->problemsOnly($problemsOnly);
+
+ $this->addControl($searchBar);
+
+ if (count($cube->listDimensions()) > 0) {
+ $this->view->cube = $cube;
+ } else {
+ $showSettings = true;
+ }
+
+ $this->view->url = Url::fromRequest()
+ ->onlyWith($this->preserveParams)
+ ->setFilter($searchBar->getFilter());
+
+ if ($showSettings) {
+ $form = (new DimensionsForm())
+ ->setUrl($this->view->url)
+ ->setCube($cube)
+ ->on(DimensionsForm::ON_SUCCESS, function ($form) {
+ $this->redirectNow($form->getRedirectUrl());
+ })
+ ->handleRequest($this->getServerRequest());
+
+ $this->view->form = $form;
+ } else {
+ $this->setAutorefreshInterval(15);
+ }
+
+ $this->addContent(
+ HtmlString::create($this->view->render('/cube-index.phtml'))
+ );
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+ }
+
+ private function prepareCube(): IcingaDbCube
+ {
+ $cube = $this->getCube();
+ $cube->chooseFacts(array_keys($cube->getAvailableFactColumns()));
+
+ $dimensions = DimensionParams::fromString(
+ $this->params->shift('dimensions', '')
+ )->getDimensions();
+
+ if ($this->hasLegacyDimensionParams($dimensions)) {
+ $this->transformLegacyDimensionParamsAndRedirect($dimensions);
+ }
+
+ $wantNull = $this->params->shift('wantNull');
+ foreach ($dimensions as $dimension) {
+ $cube->addDimensionByName($dimension);
+ if ($wantNull) {
+ $cube->getDimension($dimension)->wantNull();
+ }
+
+ $sliceParamWithPrefix = rawurlencode($cube::SLICE_PREFIX . $dimension);
+
+ if ($this->params->has($sliceParamWithPrefix)) {
+ $this->preserveParams[] = $sliceParamWithPrefix;
+ $cube->slice($dimension, $this->params->shift($sliceParamWithPrefix));
+ }
+ }
+
+ return $cube;
+ }
+
+ /**
+ * Get whether the given dimension param is legacy dimension param
+ *
+ * @param string $dimensionParam
+ *
+ * @return bool
+ */
+ private function isLegacyDimensionParam(string $dimensionParam): bool
+ {
+ return ! Str::startsWith($dimensionParam, CustomVariableDimension::HOST_PREFIX)
+ && ! Str::startsWith($dimensionParam, CustomVariableDimension::SERVICE_PREFIX);
+ }
+
+ /**
+ * Get whether the dimensions contain legacy dimension
+ *
+ * @param array $dimensions
+ *
+ * @return bool
+ */
+ private function hasLegacyDimensionParams(array $dimensions): bool
+ {
+ foreach ($dimensions as $dimension) {
+ if ($this->isLegacyDimensionParam($dimension)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Transform legacy dimension and slice params and redirect
+ *
+ * This adds the new prefix to params and then redirects so that the new URL contains the prefixed params
+ * Slices are prefixed to differ filter and slice params
+ *
+ * @param array $legacyDimensions
+ */
+ private function transformLegacyDimensionParamsAndRedirect(array $legacyDimensions): void
+ {
+ $dimensions = [];
+ $slices = [];
+
+ $dimensionPrefix = CustomVariableDimension::HOST_PREFIX;
+ if ($this->getRequest()->getControllerName() === 'services') {
+ $dimensionPrefix = CustomVariableDimension::SERVICE_PREFIX;
+ }
+
+ foreach ($legacyDimensions as $param) {
+ $newParam = $param;
+ if ($this->isLegacyDimensionParam($param)) {
+ $newParam = $dimensionPrefix . $param;
+ }
+
+ $slice = $this->params->shift($param);
+ if ($slice) {
+ $slices[IcingaDbCube::SLICE_PREFIX . $newParam] = $slice;
+ }
+
+ $dimensions[] = $newParam;
+ }
+
+ $this->redirectNow(
+ Url::fromRequest()
+ ->setParam('dimensions', DimensionParams::fromArray($dimensions)->getParams())
+ ->addParams($slices)
+ ->without($legacyDimensions)
+ );
+ }
+
+ public function createTabs(): Tabs
+ {
+ $params = Url::fromRequest()
+ ->onlyWith($this->preserveParams)
+ ->getParams()
+ ->toString();
+
+ return $this->getTabs()
+ ->add('cube/hosts', [
+ 'label' => $this->translate('Hosts'),
+ 'url' => 'cube/hosts' . ($params === '' ? '' : '?' . $params)
+ ])
+ ->add('cube/services', [
+ 'label' => $this->translate('Services'),
+ 'url' => 'cube/services' . ($params === '' ? '' : '?' . $params)
+ ]);
+ }
+}
diff --git a/library/Cube/Web/IdoController.php b/library/Cube/Web/IdoController.php
new file mode 100644
index 0000000..a9feec9
--- /dev/null
+++ b/library/Cube/Web/IdoController.php
@@ -0,0 +1,198 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Cube\DimensionParams;
+use Icinga\Module\Cube\Forms\DimensionsForm;
+use Icinga\Module\Cube\IcingaDb\CustomVariableDimension;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\Ido\IdoCube;
+use ipl\Stdlib\Str;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Url;
+use ipl\Web\Widget\Tabs;
+
+abstract class IdoController extends CompatController
+{
+ /**
+ * Return this controllers' cube
+ *
+ * @return IdoCube
+ */
+ abstract protected function getCube(): IdoCube;
+
+ public function detailsAction(): void
+ {
+ $cube = $this->prepareCube();
+
+ $this->getTabs()->add('details', [
+ 'label' => $this->translate('Cube details'),
+ 'url' => $this->getRequest()->getUrl()
+ ])->activate('details');
+
+ $this->view->title = $cube->getSlicesLabel();
+
+ $this->view->links = ActionLinks::renderAll($cube, $this->view);
+
+ $this->render('cube-details', null, true);
+ }
+
+ protected function renderCube(): void
+ {
+ $this->params->shift('format');
+ $showSettings = $this->params->shift('showSettings');
+
+ $cube = $this->prepareCube();
+
+ $this->view->title = sprintf(
+ $this->translate('Cube: %s'),
+ $cube->getPathLabel()
+ );
+
+ if (count($cube->listDimensions()) > 0) {
+ $this->view->cube = $cube;
+ } else {
+ $showSettings = true;
+ }
+
+ $this->view->url = Url::fromRequest();
+ if ($showSettings) {
+ $form = (new DimensionsForm())
+ ->setUrl($this->view->url)
+ ->setCube($cube)
+ ->setUrl(Url::fromRequest())
+ ->on(DimensionsForm::ON_SUCCESS, function ($form) {
+ $this->redirectNow($form->getRedirectUrl());
+ })
+ ->handleRequest($this->getServerRequest());
+
+ $this->view->form = $form;
+ } else {
+ $this->setAutorefreshInterval(15);
+ }
+
+ $this->render('cube-index', null, true);
+ }
+
+ private function prepareCube(): IdoCube
+ {
+ $cube = $this->getCube();
+ $cube->chooseFacts(array_keys($cube->getAvailableFactColumns()));
+
+ $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions();
+
+ $resolved = $this->params->shift('resolved', false);
+
+ if (
+ ! $resolved
+ && Module::exists('icingadb')
+ && $this->hasIcingadbDimensionParams($vars)
+ ) {
+ $this->transformIcingadbDimensionParamsAndRedirect($vars);
+ } elseif ($resolved) {
+ $this->redirectNow(Url::fromRequest()->without('resolved'));
+ }
+
+ $wantNull = $this->params->shift('wantNull');
+
+ foreach ($vars as $var) {
+ $cube->addDimensionByName($var);
+ if ($wantNull) {
+ $cube->getDimension($var)->wantNull();
+ }
+ }
+
+ foreach ($this->params->toArray() as $param) {
+ $cube->slice(rawurldecode($param[0]), rawurldecode($param[1]));
+ }
+
+ return $cube;
+ }
+
+ /**
+ * Get whether the dimensions contain icingadb dimension
+ *
+ * @param array $dimensions
+ *
+ * @return bool
+ */
+ private function hasIcingadbDimensionParams(array $dimensions): bool
+ {
+ foreach ($dimensions as $dimension) {
+ if (
+ Str::startsWith($dimension, CustomVariableDimension::HOST_PREFIX)
+ || Str::startsWith($dimension, CustomVariableDimension::SERVICE_PREFIX)
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Transform icingadb dimension and slice params and redirect
+ *
+ * This remove the new icingadb prefix from params and remove sort, problems-only, filter params
+ *
+ * @param array $icingadbDimensions
+ */
+ private function transformIcingadbDimensionParamsAndRedirect(array $icingadbDimensions): void
+ {
+ $dimensions = [];
+ $slices = [];
+ $toRemoveSlices = [];
+
+ $prefix = CustomVariableDimension::HOST_PREFIX;
+ if ($this->getRequest()->getControllerName() === 'ido-services') {
+ $prefix = CustomVariableDimension::SERVICE_PREFIX;
+ }
+
+ foreach ($icingadbDimensions as $param) {
+ $newParam = $param;
+ if (strpos($param, $prefix) !== false) {
+ $newParam = substr($param, strlen($prefix));
+ }
+
+ $slice = $this->params->shift(IcingaDbCube::SLICE_PREFIX . $param);
+ if ($slice) {
+ $slices[$newParam] = $slice;
+ $toRemoveSlices[] = IcingaDbCube::SLICE_PREFIX . $param;
+ }
+
+ $dimensions[] = $newParam;
+ }
+
+ $icingadbParams = array_merge(
+ $icingadbDimensions,
+ $toRemoveSlices,
+ array_keys($this->params->toArray(false))
+ );
+
+ $this->redirectNow(
+ Url::fromRequest()
+ ->setParam('dimensions', DimensionParams::fromArray($dimensions)->getParams())
+ ->addParams($slices)
+ ->addParams(['resolved' => true])
+ ->without($icingadbParams)
+ );
+ }
+
+ public function createTabs(): Tabs
+ {
+ $params = Url::fromRequest()->getParams()->toString();
+
+ return $this->getTabs()
+ ->add('cube/hosts', [
+ 'label' => $this->translate('Hosts'),
+ 'url' => 'cube/hosts' . ($params === '' ? '' : '?' . $params)
+ ])
+ ->add('cube/services', [
+ 'label' => $this->translate('Services'),
+ 'url' => 'cube/services' . ($params === '' ? '' : '?' . $params)
+ ]);
+ }
+}
diff --git a/module.info b/module.info
new file mode 100644
index 0000000..64dfc12
--- /dev/null
+++ b/module.info
@@ -0,0 +1,10 @@
+Name: Cube
+Version: 1.3.2
+Requires:
+ Libraries: icinga-php-library (>=0.13.0)
+ Modules: monitoring (>= 2.9.0), icingadb (>= 1.0.0)
+Description: Cube for Icinga Web
+ The Cube allows you to analyze data across multiple dimensions in your
+ Icinga Web frontend. Currently it shows host statistics (total count,
+ health) grouped by various custom variables in multiple dimensions. It
+ integrates well with other modules and provides extensible hooks
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..0c37e72
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,1031 @@
+parameters:
+ ignoreErrors:
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Controllers\\\\IndexController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/IndexController.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on ipl\\\\Html\\\\Contract\\\\FormSubmitElement\\|null\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addDimensionButtons\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addDimensionButtons\\(\\) has parameter \\$pos with no type specified\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addDimensionButtons\\(\\) has parameter \\$total with no type specified\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addSlice\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addSlice\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:setCube\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:\\$url \\(ipl\\\\Web\\\\Url\\) does not accept mixed\\.$#"
+ count: 1
+ path: application/forms/DimensionsForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:addDimension\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:chooseFacts\\(\\) has parameter \\$facts with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:fetchAll\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:flipPositions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:flipPositions\\(\\) has parameter \\$array with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:flipPositions\\(\\) has parameter \\$pos1 with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:flipPositions\\(\\) has parameter \\$pos2 with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimension\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimension\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimensionAfter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimensionAfter\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimensionsLabel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getPathLabel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getSlices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getSlicesLabel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasDimension\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasDimension\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasFact\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasFact\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasSlice\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasSlice\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listAdditionalDimensions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listAvailableDimensions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listDimensions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listDimensionsUpTo\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listDimensionsUpTo\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listFacts\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listSlices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:moveDimensionDown\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:moveDimensionDown\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:moveDimensionUp\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:moveDimensionUp\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:reOrderDimensions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:reOrderDimensions\\(\\) has parameter \\$positions with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:registerAvailableDimensions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:removeDimension\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:removeDimension\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:slice\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:slice\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:slice\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:\\$availableDimensions \\(array\\<string, Icinga\\\\Module\\\\Cube\\\\Dimension\\>\\) does not accept array\\<int\\|string, Icinga\\\\Module\\\\Cube\\\\Dimension\\>\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:\\$chosenFacts type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:\\$renderer has no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:\\$slices has no type specified\\.$#"
+ count: 1
+ path: library/Cube/Cube.php
+
+ -
+ message: "#^Access to undefined constant Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:DIMENSION_SEVERITY_SORT_PARAM\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getSortBy\\(\\)\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimension\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimension\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimension\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsForRow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsForRow\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsUpFrom\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsUpFrom\\(\\) has parameter \\$dimension with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsUpFrom\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimension\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimension\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensionsDownTo\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensionsDownTo\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensionsForRow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensionsForRow\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:extractFacts\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDetailsUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDetailsUrl\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDetailsUrl\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClassString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClassString\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClassString\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClasses\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getIndent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getIndent\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getLevel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getLevel\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getSliceUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getSliceUrl\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getSliceUrl\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:initialize\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:isOuterDimension\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:isOuterDimension\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderFacts\\(\\) has parameter \\$facts with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderRow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderRow\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:sortBySeverity\\(\\) has parameter \\$results with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$dimensionLevels type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$dimensionOrder type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$dimensions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$facts has no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$reversedDimensions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$started has no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has parameter \\$count with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:renderFacts\\(\\) has parameter \\$facts with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has parameter \\$count with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:renderFacts\\(\\) has parameter \\$facts with no type specified\\.$#"
+ count: 1
+ path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:add\\(\\) has parameter \\$dimension with no type specified\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromArray\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromArray\\(\\) has parameter \\$dimensions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromString\\(\\) has parameter \\$dimensions with no type specified\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:getDimensions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:update\\(\\) has parameter \\$dimensions with no type specified\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:\\$dimensions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/DimensionParams.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Hook\\\\ActionsHook\\:\\:makeUrl\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Hook/ActionsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Hook\\\\IcingaDbActionsHook\\:\\:makeUrl\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Hook/IcingaDbActionsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\CustomVariableDimension\\:\\:__construct\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/CustomVariableDimension.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\CustomVariableDimension\\:\\:\\$label has no type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/CustomVariableDimension.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\CustomVariableDimension\\:\\:\\$wantNull has no type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/CustomVariableDimension.php
+
+ -
+ message: "#^Cannot access property \\$customvar_flat on mixed\\.$#"
+ count: 2
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:fetchAll\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:fetchHostVariableDimensions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:fetchServiceVariableDimensions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:getObjectsFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:getSortBy\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:prepareFullQuery\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:prepareRollupQuery\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Parameter \\#1 \\$filter of method ipl\\\\Orm\\\\Query\\:\\:filter\\(\\) expects ipl\\\\Stdlib\\\\Filter\\\\Rule, ipl\\\\Stdlib\\\\Filter\\\\Rule\\|null given\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:\\$objectsFilter has no type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:\\$sortBy \\(array\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:\\$sortBy type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbCube.php
+
+ -
+ message: "#^Cannot access property \\$host on mixed\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbHostStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbHostStatusCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbHostStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbHostStatusCube\\:\\:listAvailableDimensions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbHostStatusCube.php
+
+ -
+ message: "#^Cannot access property \\$host_name on mixed\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
+
+ -
+ message: "#^Cannot access property \\$service_name on mixed\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbServiceStatusCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbServiceStatusCube\\:\\:listAvailableDimensions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:joinLeft\\(\\)\\.$#"
+ count: 1
+ path: library/Cube/Ido/CustomVarDimension.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\CustomVarDimension\\:\\:__construct\\(\\) has parameter \\$varName with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/CustomVarDimension.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\CustomVarDimension\\:\\:safeVarname\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/CustomVarDimension.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\CustomVarDimension\\:\\:safeVarname\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/CustomVarDimension.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\CustomVarDimension\\:\\:\\$type \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: library/Cube/Ido/CustomVarDimension.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\ConnectionInterface\\:\\:getResource\\(\\)\\.$#"
+ count: 1
+ path: library/Cube/Ido/DataView/Hoststatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DataView\\\\Hoststatus\\:\\:__construct\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/DataView/Hoststatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:chooseFacts\\(\\) has parameter \\$facts with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/DbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/DbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:fetchAll\\(\\) should return array but returns array\\|null\\.$#"
+ count: 1
+ path: library/Cube/Ido/DbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/DbCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:setDbName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/DbCube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:\\$factColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/DbCube.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Cannot access offset 'host'\\|'service' on array\\|stdClass\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Cannot call method getDbAdapter\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Cannot call method getRestrictions\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:filterProtectedCustomvars\\(\\) should return array\\<string\\> but returns array\\|false\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:getIdoVersion\\(\\) should return string but returns string\\|false\\|null\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:getMonitoringRestriction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Parameter \\#1 \\$connection of method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:setConnection\\(\\) expects Icinga\\\\Data\\\\Db\\\\DbConnection, mixed given\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_split expects string, mixed given\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:\\$availableFacts type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:\\$idoVersion \\(string\\) does not accept string\\|false\\|null\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoCube.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:getSelectQuery\\(\\)\\.$#"
+ count: 2
+ path: library/Cube/Ido/IdoHostStatusCube.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:requireColumn\\(\\)\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoHostStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoHostStatusCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoHostStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoHostStatusCube\\:\\:listAvailableDimensions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoHostStatusCube.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:getSelectQuery\\(\\)\\.$#"
+ count: 2
+ path: library/Cube/Ido/IdoServiceStatusCube.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:requireColumn\\(\\)\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoServiceStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoServiceStatusCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoServiceStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoServiceStatusCube\\:\\:listAvailableDimensions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/IdoServiceStatusCube.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:__call\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Ido/ZfSelectWrapper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:columns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/ZfSelectWrapper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:columns\\(\\) has parameter \\$cols with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/ZfSelectWrapper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:columns\\(\\) has parameter \\$correlationName with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/ZfSelectWrapper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:reset\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/ZfSelectWrapper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:reset\\(\\) has parameter \\$part with no type specified\\.$#"
+ count: 1
+ path: library/Cube/Ido/ZfSelectWrapper.php
+
+ -
+ message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\{Zend_Db_Select, string\\} given\\.$#"
+ count: 1
+ path: library/Cube/Ido/ZfSelectWrapper.php
+
+ -
+ message: "#^Parameter \\#1 \\$rule of method ipl\\\\Stdlib\\\\Filter\\\\Chain\\:\\:add\\(\\) expects ipl\\\\Stdlib\\\\Filter\\\\Rule, ipl\\\\Stdlib\\\\Filter\\\\Rule\\|null given\\.$#"
+ count: 1
+ path: library/Cube/ProvidedHook/Cube/IcingaDbActions.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Web\\\\Controller\\:\\:hasLegacyDimensionParams\\(\\) has parameter \\$dimensions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Web\\\\Controller\\:\\:transformLegacyDimensionParamsAndRedirect\\(\\) has parameter \\$legacyDimensions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Web/Controller.php
+
+ -
+ message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:shift\\(\\) expects string\\|null, false given\\.$#"
+ count: 1
+ path: library/Cube/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Web\\\\IdoController\\:\\:hasIcingadbDimensionParams\\(\\) has parameter \\$dimensions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Web/IdoController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Cube\\\\Web\\\\IdoController\\:\\:transformIcingadbDimensionParamsAndRedirect\\(\\) has parameter \\$icingadbDimensions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Cube/Web/IdoController.php
+
+ -
+ message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:shift\\(\\) expects string\\|null, false given\\.$#"
+ count: 1
+ path: library/Cube/Web/IdoController.php
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..82abfa0
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,27 @@
+includes:
+ - phpstan-baseline.neon
+
+parameters:
+ level: max
+
+ checkFunctionNameCase: true
+ checkInternalClassCaseSensitivity: true
+ treatPhpDocTypesAsCertain: false
+
+ paths:
+ - application
+ - library
+
+ scanDirectories:
+ - vendor
+
+ ignoreErrors:
+ -
+ messages:
+ - '#Unsafe usage of new static\(\)#'
+ - '#. but return statement is missing#'
+ reportUnmatched: false
+
+ universalObjectCratesClasses:
+ - Icinga\Web\View
+ - ipl\Orm\Model
diff --git a/public/css/module.less b/public/css/module.less
new file mode 100644
index 0000000..88ba80e
--- /dev/null
+++ b/public/css/module.less
@@ -0,0 +1,354 @@
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+.controls > a {
+ color: @icinga-blue;
+
+ &:hover::before {
+ text-decoration: none;
+ }
+}
+
+div.cube {
+ div.cube-dimension0 > .header {
+ font-size: 1.2em;
+ font-weight: bold;
+ border-bottom: 1px solid @text-color;
+ }
+
+ div.cube-dimension1 {
+ clear: both;
+ border-left: 0.5em solid transparent;
+ }
+
+ div.cube-dimension1 > .header,
+ div.cube-dimension2 {
+ display: inline-block;
+ width: 10em;
+ height: 10em;
+ margin: 0;
+ padding: 0.2em;
+ margin-top: 0.3em;
+ overflow: hidden;
+ }
+
+ div.cube-dimension1 > .header {
+ display: flex;
+ float: left;
+ font-weight: bold;
+ text-align: center;
+ padding-left: 0;
+
+ > a:first-child {
+ flex: 1;
+ max-width: 8em;
+ word-wrap: break-word;
+ }
+ }
+
+ div.cube-dimension2 {
+ > .header {
+ display: flex;
+ text-align: center;
+ color: @text-color-on-icinga-blue;
+ margin-bottom: 1em;
+
+ > a:first-child {
+ flex: 1;
+ max-width: 8em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ background: @color-ok;
+
+ &.critical {
+ background: @color-critical;
+ }
+
+ &.warning {
+ background: @color-warning;
+ }
+
+ &.unknown {
+ background: @color-unknown;
+ }
+
+ &.unreachable {
+ background: @color-unreachable;
+ }
+
+ &.ok {
+ background: @color-ok;
+ }
+
+ > .body {
+ text-align: center;
+ }
+ }
+
+ span.critical, span.warning, span.unknown, span.unreachable, span.ok {
+ display: inline;
+ border-radius: 0.1em;
+ padding: 0.05em 0.2em;
+ color: @text-color-on-icinga-blue;
+ font-size: 3em;
+ }
+
+ span.ok {
+ background: @color-ok;
+ }
+
+ .others {
+ display: flex;
+ justify-content: center;
+ span {
+ margin-left: 0.2em;
+ margin-top: 0.8em;
+ font-size: 1em;
+ }
+ }
+
+ span.critical {
+ background: @color-critical;
+ }
+
+ span.critical.handled {
+ background: @color-critical-handled;
+ }
+
+ div.cube-dimension0.critical > .header {
+ color: @color-critical;
+ border-color: @color-critical;
+ }
+
+ div.cube-dimension1.critical {
+ border-left-color: @color-critical;
+
+ &.handled {
+ border-left-color: @color-critical-handled;
+ }
+ }
+
+ div.cube-dimension2.critical {
+ span.sum {
+ font-size: 1em;
+ }
+ }
+
+ span.warning {
+ background: @color-warning;
+ }
+
+ span.warning.handled {
+ background: @color-warning-handled;
+ }
+
+ div.cube-dimension0.warning > .header {
+ color: @color-warning;
+ border-color: @color-warning;
+ }
+
+ div.cube-dimension1.warning {
+ border-left-color: @color-warning;
+
+ &.handled {
+ border-left-color: @color-warning-handled;
+ }
+ }
+
+ div.cube-dimension2.warning {
+ span.sum {
+ font-size: 1em;
+ }
+ }
+
+ span.unknown{
+ background: @color-unknown;
+ }
+
+ span.unknown.handled {
+ background: @color-unknown-handled;
+ }
+
+ div.cube-dimension0.unknown > .header {
+ color: @color-unknown;
+ border-color: @color-unknown;
+ }
+
+ div.cube-dimension1.unknown {
+ border-left-color: @color-unknown;
+
+ &.handled {
+ border-left-color: @color-unknown-handled;
+ }
+ }
+
+ div.cube-dimension2.unknown {
+ span.sum {
+ font-size: 1em;
+ }
+ }
+}
+
+span.unreachable{
+ background: @color-unreachable;
+}
+
+span.unreachable.handled {
+ background: @color-unreachable-handled;
+}
+
+div.cube-dimension0.unreachable > .header {
+ color: @color-unreachable;
+ border-color: @color-unreachable;
+}
+
+div.cube-dimension1.unreachable {
+ border-left-color: @color-unreachable;
+
+ &.handled {
+ border-left-color: @color-unreachable-handled;
+ }
+}
+
+div.cube-dimension2.unreachable {
+ span.sum {
+ font-size: 1em;
+ }
+}
+
+fieldset.dimensions {
+ margin: 0;
+ padding: 0;
+
+ span {
+ min-width: 18em;
+ display: inline-block;
+ }
+
+ button[type=submit] {
+ border: none;
+ color: @text-color;
+ background: none;
+
+ padding-left: .75em;
+ padding-right: .75em;
+
+ i:before {
+ margin-right: 0;
+ }
+
+ &:not([disabled]):hover, &:focus {
+ .rounded-corners();
+ background-color: @icinga-blue;
+ color: @text-color-on-icinga-blue;
+ }
+
+ &:focus {
+ outline: 3px solid fade(@icinga-blue, 50%);
+ outline-offset: 1px;
+ }
+ }
+
+ button[type=submit][disabled] {
+ color: @disabled-gray;
+ background-color: @body-bg-color;
+ cursor: auto;
+ }
+
+ &:first-of-type button[type=submit]:nth-of-type(2) {
+ margin-left: 2.50em;
+ }
+
+ &:not(:first-of-type) {
+ button[type=submit]:nth-of-type(2):last-of-type {
+ margin-right: 2.50em;
+ }
+ }
+
+ &:last-of-type {
+ margin-bottom: 0.5em;
+ }
+}
+
+.dimension-name {
+ font-weight: bold;
+ margin-left: 1em;
+}
+
+ul.action-links {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+ul.action-links {
+ li {
+ margin: 0;
+ padding: 0;
+ float: left;
+ display: inline-block;
+ width: 18em;
+ min-height: 10em;
+ overflow: hidden;
+ }
+
+ li.error {
+ background-color: @color-critical;
+ color: @text-color-on-icinga-blue;
+ padding: 1em;
+ font-weight: bold;
+ text-align: center;
+ }
+
+ a {
+ width: 100%;
+ height: 100%;
+ padding: 0.5em 1em;
+ display: block;
+ text-decoration: none;
+ color: @gray;
+
+ .title {
+ display: inline-block;
+ margin-left: 5em;
+ font-weight: bold;
+ color: @link-color;
+ }
+
+ p {
+ margin-left: 5em;
+ }
+
+ i {
+ position: absolute;
+ font-size: 3em;
+ display: inline-block;
+ }
+ &:hover {
+ background: @tr-hover-color;
+ text-decoration: none;
+ }
+ }
+}
+
+/** Form **/
+
+.content form {
+ margin-bottom: 2em;
+
+ button[type=submit] {
+ margin-top: 0.5em;
+ }
+
+ select {
+ width: 100%;
+ }
+}
+
+.controls .icinga-form .toggle-switch {
+ margin-top: .25em;
+ margin-bottom: .25em;
+}
diff --git a/run.php b/run.php
new file mode 100644
index 0000000..454db18
--- /dev/null
+++ b/run.php
@@ -0,0 +1,48 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+use Icinga\Module\Cube\Cube;
+
+$this->provideHook('cube/Actions', 'Cube/MonitoringActions');
+$this->provideHook('cube/IcingaDbActions', 'Cube/IcingaDbActions');
+
+$this->provideHook('icingadb/icingadbSupport');
+
+if (! Cube::isUsingIcingaDb()) {
+ $this->addRoute('cube/hosts', new Zend_Controller_Router_Route_Static(
+ 'cube/hosts',
+ [
+ 'controller' => 'ido-hosts',
+ 'action' => 'index',
+ 'module' => 'cube'
+ ]
+ ));
+
+ $this->addRoute('cube/hosts/details', new Zend_Controller_Router_Route_Static(
+ 'cube/hosts/details',
+ [
+ 'controller' => 'ido-hosts',
+ 'action' => 'details',
+ 'module' => 'cube'
+ ]
+ ));
+
+ $this->addRoute('cube/services', new Zend_Controller_Router_Route_Static(
+ 'cube/services',
+ [
+ 'controller' => 'ido-services',
+ 'action' => 'index',
+ 'module' => 'cube'
+ ]
+ ));
+
+ $this->addRoute('cube/services/details', new Zend_Controller_Router_Route_Static(
+ 'cube/services/details',
+ [
+ 'controller' => 'ido-services',
+ 'action' => 'details',
+ 'module' => 'cube'
+ ]
+ ));
+}