summaryrefslogtreecommitdiffstats
path: root/public
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:39:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:39:39 +0000
commit8ca6cc32b2c789a3149861159ad258f2cb9491e3 (patch)
tree2492de6f1528dd44eaa169a5c1555026d9cb75ec /public
parentInitial commit. (diff)
downloadicingaweb2-upstream/2.11.4.tar.xz
icingaweb2-upstream/2.11.4.zip
Adding upstream version 2.11.4.upstream/2.11.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'public')
-rw-r--r--public/css/icinga/about.less99
-rw-r--r--public/css/icinga/animation.less366
-rw-r--r--public/css/icinga/audit.less363
-rw-r--r--public/css/icinga/badges.less22
-rw-r--r--public/css/icinga/base.less344
-rw-r--r--public/css/icinga/compat.less35
-rw-r--r--public/css/icinga/configmenu.less303
-rw-r--r--public/css/icinga/controls.less281
-rw-r--r--public/css/icinga/dev.less10
-rw-r--r--public/css/icinga/forms.less596
-rw-r--r--public/css/icinga/grid.less47
-rw-r--r--public/css/icinga/health.less69
-rw-r--r--public/css/icinga/layout-structure.less167
-rw-r--r--public/css/icinga/layout.less379
-rw-r--r--public/css/icinga/login-orbs.less104
-rw-r--r--public/css/icinga/login.less183
-rw-r--r--public/css/icinga/main.less452
-rw-r--r--public/css/icinga/menu.less554
-rw-r--r--public/css/icinga/mixins.less201
-rw-r--r--public/css/icinga/modal.less113
-rw-r--r--public/css/icinga/nav.less50
-rw-r--r--public/css/icinga/php-diff.less17
-rw-r--r--public/css/icinga/print.less39
-rw-r--r--public/css/icinga/responsive.less167
-rw-r--r--public/css/icinga/setup.less479
-rw-r--r--public/css/icinga/spinner.less42
-rw-r--r--public/css/icinga/tabs.less105
-rw-r--r--public/css/icinga/widgets.less643
-rw-r--r--public/css/modes/light.less4
-rw-r--r--public/css/modes/none.less4
-rw-r--r--public/css/modes/system.less4
-rw-r--r--public/css/pdf/pdfprint.less103
-rw-r--r--public/css/themes/Winter.less32
-rw-r--r--public/css/themes/colorblind.less29
-rw-r--r--public/css/themes/high-contrast.less250
-rw-r--r--public/css/vendor/normalize.css427
-rw-r--r--public/error_norewrite.html1
-rw-r--r--public/error_unavailable.html7
-rw-r--r--public/font/ifont.eotbin0 -> 45480 bytes
-rw-r--r--public/font/ifont.svg286
-rw-r--r--public/font/ifont.ttfbin0 -> 45324 bytes
-rw-r--r--public/font/ifont.woffbin0 -> 27320 bytes
-rw-r--r--public/font/ifont.woff2bin0 -> 22700 bytes
-rw-r--r--public/img/favicon.pngbin0 -> 806 bytes
-rw-r--r--public/img/icinga-loader-light.gifbin0 -> 94929 bytes
-rw-r--r--public/img/icinga-loader.gifbin0 -> 32942 bytes
-rw-r--r--public/img/icinga-logo-big-dark.pngbin0 -> 3491 bytes
-rw-r--r--public/img/icinga-logo-big-dark.svg14
-rw-r--r--public/img/icinga-logo-big.pngbin0 -> 3586 bytes
-rw-r--r--public/img/icinga-logo-big.svg1
-rw-r--r--public/img/icinga-logo-compact-inverted.svg9
-rw-r--r--public/img/icinga-logo-compact.svg30
-rw-r--r--public/img/icinga-logo-dark.svg30
-rw-r--r--public/img/icinga-logo-inverted.svg1
-rw-r--r--public/img/icinga-logo.pngbin0 -> 1132 bytes
-rw-r--r--public/img/icinga-logo.svg32
-rw-r--r--public/img/icingaweb2-background-orbs.jpgbin0 -> 1467100 bytes
-rw-r--r--public/img/icingaweb2-background.jpgbin0 -> 305235 bytes
-rw-r--r--public/img/icons/acknowledgement.pngbin0 -> 501 bytes
-rw-r--r--public/img/icons/acknowledgement_petrol.pngbin0 -> 492 bytes
-rw-r--r--public/img/icons/active_checks_disabled.pngbin0 -> 511 bytes
-rw-r--r--public/img/icons/active_checks_disabled_petrol.pngbin0 -> 513 bytes
-rw-r--r--public/img/icons/active_passive_checks_disabled.pngbin0 -> 537 bytes
-rw-r--r--public/img/icons/active_passive_checks_disabled_petrol.pngbin0 -> 546 bytes
-rw-r--r--public/img/icons/comment.pngbin0 -> 491 bytes
-rw-r--r--public/img/icons/comment_petrol.pngbin0 -> 502 bytes
-rw-r--r--public/img/icons/configuration.pngbin0 -> 647 bytes
-rw-r--r--public/img/icons/configuration_petrol.pngbin0 -> 645 bytes
-rw-r--r--public/img/icons/create.pngbin0 -> 475 bytes
-rw-r--r--public/img/icons/create_petrol.pngbin0 -> 482 bytes
-rw-r--r--public/img/icons/csv.pngbin0 -> 509 bytes
-rw-r--r--public/img/icons/csv_petrol.pngbin0 -> 511 bytes
-rw-r--r--public/img/icons/dashboard.pngbin0 -> 415 bytes
-rw-r--r--public/img/icons/dashboard_petrol.pngbin0 -> 420 bytes
-rw-r--r--public/img/icons/disabled.pngbin0 -> 535 bytes
-rw-r--r--public/img/icons/disabled_petrol.pngbin0 -> 525 bytes
-rw-r--r--public/img/icons/down.pngbin0 -> 492 bytes
-rw-r--r--public/img/icons/down_petrol.pngbin0 -> 505 bytes
-rw-r--r--public/img/icons/downtime_end.pngbin0 -> 550 bytes
-rw-r--r--public/img/icons/downtime_end_petrol.pngbin0 -> 561 bytes
-rw-r--r--public/img/icons/downtime_start.pngbin0 -> 524 bytes
-rw-r--r--public/img/icons/downtime_start__petrol.pngbin0 -> 529 bytes
-rw-r--r--public/img/icons/edit.pngbin0 -> 486 bytes
-rw-r--r--public/img/icons/edit_petrol.pngbin0 -> 493 bytes
-rw-r--r--public/img/icons/error.pngbin0 -> 532 bytes
-rw-r--r--public/img/icons/error_petrol.pngbin0 -> 535 bytes
-rw-r--r--public/img/icons/error_white.pngbin0 -> 429 bytes
-rw-r--r--public/img/icons/expand.pngbin0 -> 2993 bytes
-rw-r--r--public/img/icons/expand_petrol.pngbin0 -> 2992 bytes
-rw-r--r--public/img/icons/flapping.pngbin0 -> 621 bytes
-rw-r--r--public/img/icons/flapping_petrol.pngbin0 -> 636 bytes
-rw-r--r--public/img/icons/history.pngbin0 -> 516 bytes
-rw-r--r--public/img/icons/history_petrol.pngbin0 -> 520 bytes
-rw-r--r--public/img/icons/host.pngbin0 -> 500 bytes
-rw-r--r--public/img/icons/host_petrol.pngbin0 -> 489 bytes
-rw-r--r--public/img/icons/hostgroup.pngbin0 -> 541 bytes
-rw-r--r--public/img/icons/hostgroup_petrol.pngbin0 -> 554 bytes
-rw-r--r--public/img/icons/in_downtime.pngbin0 -> 490 bytes
-rw-r--r--public/img/icons/in_downtime_petrol.pngbin0 -> 497 bytes
-rw-r--r--public/img/icons/json.pngbin0 -> 517 bytes
-rw-r--r--public/img/icons/json_petrol.pngbin0 -> 525 bytes
-rw-r--r--public/img/icons/logout.pngbin0 -> 462 bytes
-rw-r--r--public/img/icons/logout_petrol.pngbin0 -> 470 bytes
-rw-r--r--public/img/icons/next.pngbin0 -> 509 bytes
-rw-r--r--public/img/icons/next_petrol.pngbin0 -> 513 bytes
-rw-r--r--public/img/icons/notification.pngbin0 -> 544 bytes
-rw-r--r--public/img/icons/notification_disabled.pngbin0 -> 487 bytes
-rw-r--r--public/img/icons/notification_disabled_petrol.pngbin0 -> 494 bytes
-rw-r--r--public/img/icons/notification_petrol.pngbin0 -> 548 bytes
-rw-r--r--public/img/icons/pdf.pngbin0 -> 507 bytes
-rw-r--r--public/img/icons/pdf_petrol.pngbin0 -> 511 bytes
-rw-r--r--public/img/icons/prev.pngbin0 -> 514 bytes
-rw-r--r--public/img/icons/prev_petrol.pngbin0 -> 519 bytes
-rw-r--r--public/img/icons/refresh.pngbin0 -> 523 bytes
-rw-r--r--public/img/icons/refresh_petrol.pngbin0 -> 517 bytes
-rw-r--r--public/img/icons/remove.pngbin0 -> 661 bytes
-rw-r--r--public/img/icons/remove_petrol.pngbin0 -> 661 bytes
-rw-r--r--public/img/icons/reschedule.pngbin0 -> 400 bytes
-rw-r--r--public/img/icons/reschedule_petrol.pngbin0 -> 403 bytes
-rw-r--r--public/img/icons/save.pngbin0 -> 506 bytes
-rw-r--r--public/img/icons/save_petrol.pngbin0 -> 517 bytes
-rw-r--r--public/img/icons/search.pngbin0 -> 491 bytes
-rw-r--r--public/img/icons/search_icinga_blue.pngbin0 -> 432 bytes
-rw-r--r--public/img/icons/search_petrol.pngbin0 -> 493 bytes
-rw-r--r--public/img/icons/search_white.pngbin0 -> 1352 bytes
-rw-r--r--public/img/icons/service.pngbin0 -> 496 bytes
-rw-r--r--public/img/icons/service_petrol.pngbin0 -> 505 bytes
-rw-r--r--public/img/icons/servicegroup.pngbin0 -> 598 bytes
-rw-r--r--public/img/icons/servicegroup_petrol.pngbin0 -> 605 bytes
-rw-r--r--public/img/icons/softstate.pngbin0 -> 511 bytes
-rw-r--r--public/img/icons/submit.pngbin0 -> 418 bytes
-rw-r--r--public/img/icons/submit_petrol.pngbin0 -> 426 bytes
-rw-r--r--public/img/icons/success.pngbin0 -> 509 bytes
-rw-r--r--public/img/icons/success_petrol.pngbin0 -> 505 bytes
-rw-r--r--public/img/icons/tux.pngbin0 -> 495 bytes
-rw-r--r--public/img/icons/uebersicht.pngbin0 -> 20920 bytes
-rw-r--r--public/img/icons/unhandled.pngbin0 -> 553 bytes
-rw-r--r--public/img/icons/unhandled_petrol.pngbin0 -> 555 bytes
-rw-r--r--public/img/icons/up.pngbin0 -> 497 bytes
-rw-r--r--public/img/icons/up_petrol.pngbin0 -> 499 bytes
-rw-r--r--public/img/icons/user.pngbin0 -> 487 bytes
-rw-r--r--public/img/icons/user_petrol.pngbin0 -> 508 bytes
-rw-r--r--public/img/icons/win.pngbin0 -> 738 bytes
-rw-r--r--public/img/orb-analytics.pngbin0 -> 232972 bytes
-rw-r--r--public/img/orb-automation.pngbin0 -> 320334 bytes
-rw-r--r--public/img/orb-cloud.pngbin0 -> 294936 bytes
-rw-r--r--public/img/orb-icinga.pngbin0 -> 259264 bytes
-rw-r--r--public/img/orb-infrastructure.pngbin0 -> 293092 bytes
-rw-r--r--public/img/orb-metrics.pngbin0 -> 289015 bytes
-rw-r--r--public/img/orb-notifications.pngbin0 -> 308893 bytes
-rw-r--r--public/img/select-icon-2x.pngbin0 -> 305 bytes
-rw-r--r--public/img/select-icon.pngbin0 -> 176 bytes
-rw-r--r--public/img/select-icon.svg1
-rw-r--r--public/img/textarea-corner-2x.pngbin0 -> 238 bytes
-rw-r--r--public/img/textarea-corner.pngbin0 -> 181 bytes
-rw-r--r--public/img/theme-mode-thumbnail-dark.svg59
-rw-r--r--public/img/theme-mode-thumbnail-light.svg54
-rw-r--r--public/img/theme-mode-thumbnail-system.svg91
-rw-r--r--public/img/touch-icon.pngbin0 -> 4777 bytes
-rw-r--r--public/img/tree/tree-minus.gifbin0 -> 149 bytes
-rw-r--r--public/img/tree/tree-plus.gifbin0 -> 151 bytes
-rw-r--r--public/img/website-icon.svg4
-rw-r--r--public/img/winter/logo_icinga_big_winter.pngbin0 -> 9474 bytes
-rw-r--r--public/img/winter/snow1.pngbin0 -> 2765 bytes
-rw-r--r--public/img/winter/snow2.pngbin0 -> 4867 bytes
-rw-r--r--public/img/winter/snow3.pngbin0 -> 3117 bytes
-rw-r--r--public/index.php4
-rw-r--r--public/js/define.js118
-rw-r--r--public/js/helpers.js91
-rw-r--r--public/js/icinga.js279
-rw-r--r--public/js/icinga/behavior/actiontable.js498
-rw-r--r--public/js/icinga/behavior/application-state.js40
-rw-r--r--public/js/icinga/behavior/autofocus.js28
-rw-r--r--public/js/icinga/behavior/collapsible.js463
-rw-r--r--public/js/icinga/behavior/datetime-picker.js222
-rw-r--r--public/js/icinga/behavior/detach.js73
-rw-r--r--public/js/icinga/behavior/dropdown.js66
-rw-r--r--public/js/icinga/behavior/filtereditor.js77
-rw-r--r--public/js/icinga/behavior/flyover.js85
-rw-r--r--public/js/icinga/behavior/form.js96
-rw-r--r--public/js/icinga/behavior/input-enrichment.js148
-rw-r--r--public/js/icinga/behavior/modal.js231
-rw-r--r--public/js/icinga/behavior/navigation.js464
-rw-r--r--public/js/icinga/behavior/selectable.js49
-rw-r--r--public/js/icinga/eventlistener.js78
-rw-r--r--public/js/icinga/events.js415
-rw-r--r--public/js/icinga/history.js338
-rw-r--r--public/js/icinga/loader.js1323
-rw-r--r--public/js/icinga/logger.js129
-rw-r--r--public/js/icinga/module.js134
-rw-r--r--public/js/icinga/storage.js549
-rw-r--r--public/js/icinga/timer.js176
-rw-r--r--public/js/icinga/timezone.js105
-rw-r--r--public/js/icinga/ui.js634
-rw-r--r--public/js/icinga/utils.js582
195 files changed, 15198 insertions, 0 deletions
diff --git a/public/css/icinga/about.less b/public/css/icinga/about.less
new file mode 100644
index 0000000..4ccaaf8
--- /dev/null
+++ b/public/css/icinga/about.less
@@ -0,0 +1,99 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+#about {
+ &.content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ section {
+ width: auto;
+
+ > * {
+ margin-bottom: 2em;
+ }
+ }
+
+ h2 {
+ margin: 0;
+ }
+
+ .name-value-table {
+ th {
+ width: 100%;
+ }
+
+ th,
+ td {
+ white-space: nowrap;
+
+ a {
+ color: @icinga-blue
+ }
+ }
+ }
+
+ section:not(:last-child),
+ .icinga-logo {
+ margin-bottom: 2em;
+ }
+
+ .external-links {
+ .rounded-corners();
+ border: 1px solid @gray-light;
+ display: flex;
+ padding: .5em 0;
+ overflow: hidden;
+
+ .col {
+ flex: 1 1 auto;
+ text-align: center;
+ font-size: 12/14em;
+ }
+
+ .col:not(:last-child) {
+ border-right: 1px solid @gray-light;
+ }
+
+ a {
+ display: block;
+ padding: .75em 1em;
+ margin: -7/12em 0;
+ }
+
+ a:hover {
+ text-decoration: none;
+ background: @gray-light;
+ }
+
+ i {
+ font-size: 2*14/12em;
+ opacity: .8;
+ margin-bottom: .25em;
+ display: block;
+
+ &:before {
+ margin-right: 0;
+ }
+ }
+ }
+
+ footer {
+ margin-top: auto;
+ align-self: stretch;
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+
+ a {
+ i {
+ font-size: 2em;
+ }
+
+ &:hover {
+ opacity: .6;
+ }
+ }
+ }
+}
diff --git a/public/css/icinga/animation.less b/public/css/icinga/animation.less
new file mode 100644
index 0000000..aad3ffb
--- /dev/null
+++ b/public/css/icinga/animation.less
@@ -0,0 +1,366 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+.animate(@animate) {
+ -moz-animation: @animate;
+ -o-animation: @animate;
+ -webkit-animation: @animate;
+ animation: @animate;
+}
+
+@-moz-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-webkit-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-o-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-ms-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+@-moz-keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+@-webkit-keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+@-o-keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+@-ms-keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+@keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 0.2;
+ }
+
+ 20% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0.2;
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: .5;
+ transform: scale(1);
+ }
+
+ 50% {
+ opacity: 1;
+ transform: scale(1.2);
+ }
+
+ 100% {
+ opacity: .5;
+ transform: scale(1);
+ }
+}
diff --git a/public/css/icinga/audit.less b/public/css/icinga/audit.less
new file mode 100644
index 0000000..23ab5b9
--- /dev/null
+++ b/public/css/icinga/audit.less
@@ -0,0 +1,363 @@
+// Style
+
+.privilege-audit-role-control {
+ list-style-type: none;
+
+ li {
+ .rounded-corners(3px);
+ border: 1px solid;
+ border-color: @low-sat-blue;
+
+ &.active {
+ border-color: @icinga-blue;
+ }
+ }
+}
+
+.privilege-audit {
+ &, ul, ol {
+ list-style-type: none;
+ }
+
+ .privilege-section > summary {
+ font-weight: @font-weight-bold;
+ border-bottom: 1px solid @gray-light;
+ }
+
+ .privilege-section > summary em,
+ .previews em,
+ .privilege-label em {
+ color: @text-color-light;
+ }
+ .privilege-section > summary em {
+ font-weight: normal;
+ }
+ .privilege-label em {
+ font-style: normal;
+ }
+
+ .icon {
+ color: @gray-light;
+
+ &.granted {
+ color: @color-granted;
+ }
+
+ &.refused {
+ color: @color-refused;
+ }
+
+ &.restricted {
+ color: @color-restricted;
+ }
+ }
+
+ .privilege-list > li {
+ .spacer {
+ opacity: 0;
+ .transition(opacity .5s ease-out);
+ }
+
+ &:hover .spacer {
+ .transition(opacity .25s .25s ease-in);
+ border: 0 dashed;
+ border-color: @gray-light;
+ border-top-width: .2em;
+ opacity: 1;
+ }
+ }
+
+ .vertical-line {
+ border: 0 solid;
+ border-left-width: 2px;
+
+ &.granted {
+ border-color: @color-granted;
+ }
+
+ &.refused {
+ border-color: @color-refused;
+ }
+ }
+
+ .connector {
+ border: 0 solid;
+ border-color: @gray-lighter;
+ border-bottom-width: 2px;
+
+ &.granted {
+ border-color: @color-granted;
+ }
+
+ &.refused {
+ border-color: @color-refused;
+ }
+
+ &:first-child {
+ border-width: 0 0 2px 2px;
+ border-bottom-left-radius: .5em;
+ }
+ }
+
+ .role {
+ .rounded-corners(1em);
+ border: 2px solid;
+ border-color: @gray-lighter;
+
+ &.granted {
+ border: 2px solid;
+ border-color: @color-granted;
+ }
+
+ &.refused {
+ border: 2px solid;
+ border-color: @color-refused;
+ }
+ }
+
+ .restriction {
+ font-family: @font-family-fixed;
+ background-color: @gray-lighter;
+ }
+}
+
+// Layout
+
+.privilege-audit-role-control {
+ display: inline-flex;
+ flex-wrap: wrap;
+
+ margin: 0 0 0 1em;
+ padding: 0;
+
+ li {
+ margin-top: @vertical-padding;
+
+ &:not(:first-child) {
+ margin-left: .5em;
+ }
+ }
+}
+
+.privilege-audit {
+ &, ul, ol {
+ margin: 0;
+ padding: 0;
+ }
+
+ .flex-overflow,
+ .privilege-list > li,
+ .inheritance-paths > ol {
+ display: flex;
+ }
+
+ .privilege-list > li {
+ margin-top: 1em;
+
+ > :last-child {
+ // This aids the usage of text-overflow:ellipsis in any of the children.
+ // It seems that to get this working while none of the children has a
+ // defined width, any flex item on the way up to the clipped container
+ // also must have a overflow value of "hidden".
+ // https://codepen.io/unthinkingly/pen/XMwJLG
+ overflow: hidden;
+ }
+
+ > details:last-child {
+ // The overflow above cuts off the outline of the summary otherwise
+ margin: -4px;
+ padding: 4px;
+ }
+ }
+
+ .privilege-section {
+ &:not(.collapsed) {
+ margin-bottom: 2em;
+ }
+ }
+
+ .privilege-section > summary {
+ display: flex;
+ align-items: baseline;
+ font-size: 1.167em;
+ margin: 0.556em 0 0.333em;
+
+ > :first-child {
+ flex: 3 1 auto;
+ min-width: 20em;
+ max-width: 40em / 1.167em; // privilege label width + spacer width / summary font-size
+ }
+
+ .audit-preview {
+ flex: 1 1 auto;
+
+ .icon:before {
+ width: 1.25em;
+ font-size: 1.25em / 1.167em; // privilege state icon font-size / summary font-size
+ }
+ }
+
+ em {
+ font-size: .857em;
+ }
+ }
+
+ h4,
+ .privilege-label {
+ flex-shrink: 0;
+ width: 20em;
+ margin: 0;
+ text-align: right;
+ }
+
+ ol + h4 {
+ margin-top: 1em;
+ }
+
+ .spacer {
+ flex: 20 1 auto;
+ min-width: 10em; // TODO: Mobile?
+ max-width: 18.8em; // 20em - (margin-left + margin-right)
+ margin: .6em;
+ }
+
+ .inheritance-paths,
+ .restrictions {
+ flex: 1 1 auto;
+
+ > summary {
+ line-height: 1;
+
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ > .icon:before {
+ width: 1.25em;
+ font-size: 1.25em;
+ }
+ }
+ }
+
+ .vertical-line {
+ margin-left: ~"calc(.75em - 1px)";
+ }
+
+ .connector {
+ flex: 1 1 auto;
+ width: 2em;
+ max-width: 2em;
+ min-width: 1em;
+ margin-bottom: ~"calc(1em - 1px)";
+
+ &:first-child {
+ margin-left: ~"calc(.75em - 1px)";
+ }
+
+ &.initiator {
+ z-index: 1;
+ margin-right: ~"calc(-.25em - 2px)";
+ }
+ }
+
+ .vertical-line + .connector {
+ min-width: ~"calc(.75em - 2px)";
+ width: ~"calc(.75em - 2px)";
+ flex-grow: 0;
+
+ &.initiator {
+ width: ~"calc(1em - 1px)";
+ }
+ }
+ .connector:first-child {
+ min-width: .75em;
+ width: .75em;
+ flex-grow: 0;
+
+ &.initiator {
+ width: 1em;
+ }
+ }
+
+ .role {
+ padding: .25em .5em .25em .5em;
+ line-height: 1;
+
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ .icon:before {
+ font-size: 1.25em;
+ }
+ }
+ .inheritance-paths .role {
+ min-width: 4em;
+ margin-top: .5em;
+ padding-left: .25em;
+ }
+ .restrictions .role {
+ display: inline-block;
+ }
+
+ .previews {
+ display: flex;
+ margin-top: .25em;
+
+ em {
+ // explicit margin + ((header icon width + its margin right) * 125% font-size)
+ margin: 0 1em 0 1em + ((1.25em + .2em) * 1.25em);
+ }
+ }
+
+ .links li:not(:last-child):after {
+ content: ",";
+ }
+
+ .restrictions > ul > li {
+ margin-top: .5em;
+
+ .role {
+ margin-left: 1.25em + .2em * 1.25em; // (header icon width + its margin right) * 125% font-size
+ margin-right: 1em;
+ }
+ }
+
+ .restriction {
+ font-size: .8em;
+ padding: .335em / .8em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ .user-select(all);
+ }
+}
+
+#layout.minimal-layout,
+#layout.poor-layout {
+ .privilege-audit {
+ .privilege-section > summary > :first-child {
+ flex-grow: 99;
+ }
+
+ h4,
+ .privilege-label {
+ width: 12em;
+ }
+
+ .spacer {
+ flex: 0;
+ min-width: 0;
+ }
+ }
+}
+
+// Integrations
+
+.privilege-audit .collapsible {
+ .collapsible-control {
+ cursor: pointer;
+ .user-select(none);
+ }
+}
diff --git a/public/css/icinga/badges.less b/public/css/icinga/badges.less
new file mode 100644
index 0000000..eae95c2
--- /dev/null
+++ b/public/css/icinga/badges.less
@@ -0,0 +1,22 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+@badge-color: @body-bg-color;
+@badge-line-height: 1.2;
+@badge-padding: 0.25em;
+
+.badge {
+ .bg-stateful();
+ .rounded-corners();
+
+ background-color: @gray;
+ color: @badge-color;
+ display: inline-block;
+ font-family: @font-family-wide;
+ font-size: @font-size-small;
+ line-height: @badge-line-height;
+ min-width: 2em;
+ padding: @badge-padding;
+ text-align: center;
+ vertical-align: middle;
+ white-space: nowrap;
+}
diff --git a/public/css/icinga/base.less b/public/css/icinga/base.less
new file mode 100644
index 0000000..9ce5d7e
--- /dev/null
+++ b/public/css/icinga/base.less
@@ -0,0 +1,344 @@
+/*! Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+// Black colors
+@black: #535353;
+@white: #fff;
+
+// Gray colors
+@gray: #c4c4c4;
+@gray-semilight: #888;
+@gray-light: #5c5c5c;
+@gray-lighter: #4b4b4b;
+@gray-lightest: #3a3a3a;
+
+@disabled-gray: #9a9a9a;
+
+// State colors
+@color-ok: #44bb77;
+@color-up: @color-ok;
+@color-warning: #ffaa44;
+@color-warning-handled: #ffcc66;
+@color-critical: #ff5566;
+@color-critical-handled: #ff99aa;
+@color-critical-accentuated: darken(@color-critical, 10%);
+@color-down: @color-critical;
+@color-down-handled: @color-critical-handled;
+@color-unknown: #aa44ff;
+@color-unknown-handled: #cc77ff;
+@color-unreachable: @color-unknown;
+@color-unreachable-handled: @color-unknown-handled;
+@color-pending: #77aaff;
+
+// Icinga colors
+@icinga-blue: #00C3ED;
+@icinga-secondary: #EF4F98;
+@icinga-secondary-dark: darken(@icinga-secondary, 10%);
+@low-sat-blue: #404d72;
+@low-sat-blue-dark: #434374;
+@icinga-blue-light: fade(@icinga-blue, 50%);
+@icinga-blue-dark: #0081a6;
+
+// Notification colors
+@color-notification-error: @color-critical;
+@color-notification-info: @color-pending;
+@color-notification-success: @color-ok;
+@color-notification-warning: @color-warning;
+
+// Background color for <body>
+@body-bg-color: #282E39;
+@body-bg-color-transparent: fade(@body-bg-color, 0);
+
+// Text colors
+@text-color: @white;
+@text-color-inverted: @body-bg-color;
+@text-color-light: fade(@text-color, 75%);
+@text-color-on-icinga-blue: @body-bg-color;
+@light-text-bg-color: fade(@gray, 5%);
+
+// Text color on <a>
+@link-color: @text-color;
+
+@tr-active-color: fade(@icinga-blue, 25);
+@tr-hover-color: fade(@icinga-blue, 5);
+
+// Menu colors
+@menu-bg-color: #06062B;
+@menu-hover-bg-color: lighten(@menu-bg-color, 5%);
+@menu-search-hover-bg-color: @menu-hover-bg-color;
+@menu-active-bg-color: #181742;
+@menu-active-hover-bg-color: lighten(@menu-active-bg-color, 5%);
+@menu-color: #DBDBDB;
+@menu-active-color: @text-color;
+@menu-highlight-color: @icinga-blue;
+@menu-highlight-hover-bg-color: @icinga-blue-dark;
+@menu-2ndlvl-color: #c4c4c4;
+@menu-2ndlvl-highlight-bg-color: @tr-hover-color;
+@menu-2ndlvl-active-bg-color: @menu-highlight-color;
+@menu-2ndlvl-active-color: @text-color-inverted;
+@menu-2ndlvl-active-hover-bg-color: darken(@menu-2ndlvl-active-bg-color, 5%);
+@menu-2ndlvl-active-hover-color: @menu-2ndlvl-active-color;
+@menu-flyout-bg-color: @body-bg-color;
+@menu-flyout-color: @text-color;
+@tab-hover-bg-color: fade(@body-bg-color, 50%);
+
+// Form colors
+@form-info-bg-color: fade(@color-ok, 20%);
+@form-error-bg-color: fade(@color-critical, 30%);
+@form-warning-bg-color: fade(@color-warning, 40%);
+@login-box-background: fade(#0B0B2F, 30%);
+
+// Other colors
+@color-granted: #59cd59;
+@color-refused: #ee7373;
+@color-restricted: #dede7d;
+
+// Font families
+@font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+@font-family-fixed: "Liberation Mono", "Lucida Console", Courier, monospace;
+@font-family-wide: Tahoma, Verdana, sans-serif;
+
+// Font sizes
+@font-size: 0.750em; // 12px
+@font-size-small: 11/12em; // 11px
+@font-size-dashboard: 3.5em; // 56px
+@font-size-dashboard-small: 1.1em; // 17px
+@font-weight-bold: 600;
+
+// Set line-height w/o unit so that the line-height is dynamically calculated as font-size * line-height
+@line-height: 1.5;
+
+@table-column-padding: 0.333em; // 4px
+
+@vertical-padding: 0.5em; // 6px
+@horizontal-padding: 1em; // 12px
+
+@light-mode: {
+ @light-body-bg-color: #F5F9FA;
+
+ @iplWebLightRules();
+
+ :root {
+ --body-bg-color: @light-body-bg-color;
+ --body-bg-color-transparent: fade(@light-body-bg-color, 0);
+ --badge-color: #F5F9FA;
+ --text-color-inverted: #F5F9FA;
+ --text-color-on-icinga-blue: #F5F9FA;
+ --menu-flyout-bg-color: #F5F9FA;
+ --tab-hover-bg-color: fade(#F5F9FA, 50%);
+
+ --menu-color: #535353;
+ --menu-bg-color: #DEECF1;
+ --menu-hover-bg-color: darken(#DEECF1, 10%);
+ --menu-search-hover-bg-color: darken(#DEECF1, 10%);
+ --menu-active-bg-color: #EDF7FC;
+ --menu-active-hover-bg-color: darken(#EDF7FC, 20%);
+ --menu-highlight-hover-bg-color: darken(#EDF7FC, 20%);
+ --menu-2ndlvl-color: #676767;
+
+ --text-color: #535353;
+ --text-color-light: fade(#535353, 75%);
+ --light-text-bg-color: fade(#7F7F7F, 5%);
+ --link-color: #535353;
+ --menu-active-color: #535353;
+ --menu-flyout-color: #535353;
+
+ --low-sat-blue: #DEECF1;
+ --low-sat-blue-dark: #c0cccd;
+
+ --gray: #819398;
+ --gray-semilight: #94a5a6;
+ --gray-light: #d0d3da;
+ --gray-lighter: #e8ecef;
+ --gray-lightest: #F7F7F7;
+
+ // ipl-web overrides
+ --base-gray: var(--gray);
+ --base-gray-light: var(--gray-light);
+ --base-gray-lighter: var(--gray-lighter);
+
+ --default-text-color: var(--text-color);
+ --default-text-color-light: var(--text-color-light);
+ --default-text-color-inverted: var(--text-color-inverted);
+
+ --searchbar-bg: var(--low-sat-blue);
+
+ --search-logical-operator-bg: fade(#819398, 50%); // --gray
+ }
+};
+
+// ipl-web overrides
+@default-bg: @body-bg-color;
+
+@base-gray: @gray;
+@base-gray-light: @gray-light;
+@base-gray-lighter: @gray-lighter;
+@base-disabled: @disabled-gray;
+
+@base-primary-color: @icinga-blue;
+@base-primary-bg: @icinga-blue;
+
+@default-text-color: @text-color;
+@default-text-color-light: @text-color-light;
+
+@state-ok: @color-ok;
+@state-warning: @color-warning;
+@state-critical: @color-critical;
+@state-pending: @color-pending;
+@state-unknown: @color-unknown;
+
+@primary-button-hover-bg: @icinga-blue-dark;
+
+@searchbar-bg: @low-sat-blue;
+
+// Make padding not affect the final computed width of an element
+html {
+ box-sizing: border-box;
+}
+details > * {
+ // children somehow default to content-box no matter the inheritance
+ box-sizing: border-box;
+}
+*,
+*:before,
+*:after {
+ -webkit-box-sizing: inherit;
+ -moz-box-sizing: inherit;
+ box-sizing: inherit;
+}
+
+a {
+ // Reset defaults
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+:focus {
+ outline: 3px solid fade(@icinga-blue, 50%);
+ outline-offset: 1px;
+}
+
+// Default margin for block text
+blockquote, p, pre {
+ margin: 0 0 1em 0;
+}
+
+blockquote {
+ border-left: 5px solid @gray-lighter;
+ padding: 0.667em 0.333em;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: @font-weight-bold;
+ margin: 0.556em 0 0.333em;
+}
+
+h1 {
+ border-bottom: 1px solid @gray-lighter;
+ font-size: 1.333em;
+}
+
+h2 {
+ font-size: 1.333em;
+}
+
+h3 {
+ font-size: 1.167em;
+}
+
+h4 {
+ font-size: 1em;
+}
+
+h5 {
+ font-size: @font-size-small;
+}
+
+h6 {
+ font-size: @font-size-small;
+ font-weight: normal;
+}
+
+pre {
+ .rounded-corners(.25em);
+ background-color: @gray-lighter;
+ font-family: @font-family-fixed;
+ font-size: @font-size-small;
+ padding: @vertical-padding @horizontal-padding;
+ white-space: pre-wrap;
+}
+
+td, th {
+ padding: @table-column-padding;
+}
+
+[class^="icon-"], [class*=" icon-"] {
+ // Smooth icons; ifont claims to have it, but it does not work in :before
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ &:before {
+ margin-left: 0;
+ }
+}
+
+// Styles for when the page is loading. JS will remove this class once the document is ready
+.loading * {
+ // Disable all transition on page load
+ -webkit-transition: none !important;
+ -moz-transition: none !important;
+ -o-transition: none !important;
+ transition: none !important;
+}
+
+.container {
+ &:before,
+ > .content:before {
+ content: "";
+ display: block;
+
+ background: url(../img/icinga-loader.gif) no-repeat center center;
+ background-color: @body-bg-color;
+ background-size: 4em 4em;
+
+ opacity: 0;
+ z-index: -1;
+ pointer-events: none;
+ .transition(none);
+ }
+
+ &.impact,
+ > .content.impact {
+ overflow: hidden;
+ position: relative;
+
+ &:before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+
+ opacity: .7;
+ z-index: 1000;
+ pointer-events: all;
+ .transition(opacity 1s 2s linear);
+ }
+ }
+
+ &.impact:before {
+ top: 2.5em;
+ }
+}
+
+@light-mode: {
+ .container {
+ &:before,
+ > .content:before {
+ background-image: url(../img/icinga-loader-light.gif)
+ }
+ }
+};
diff --git a/public/css/icinga/compat.less b/public/css/icinga/compat.less
new file mode 100644
index 0000000..d57a189
--- /dev/null
+++ b/public/css/icinga/compat.less
@@ -0,0 +1,35 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+@colorMainLayout: @icinga-blue;
+@colorMainBackground: @body-bg-color;
+@colorMainForeground: @text-color;
+@colorMainLink: @text-color;
+@colorSecondary: @gray-lightest;
+@colorGray: @gray-lightest;
+@colorLinkDefault: @text-color;
+@colorTextDefault: @text-color;
+@colorTextDarkDefault: @text-color;
+@colorOk: @color-ok;
+@colorWarning: #ffaa44;
+@colorWarningHandled: #ffcc66;
+@colorCritical: #ff5566;
+@colorCriticalHandled: #ff99aa;
+@colorUnknown: #aa44ff;
+@colorUnknownHandled: #cc77ff;
+@colorUnreachable: #aa44ff;
+@colorUnreachableHandled: #cc77ff;
+@colorPending: #77aaff;
+@colorInvalid: #999;
+@colorFormNotificationInfo: #77aaff;
+@colorFormNotificationWarning: #ffaa44;
+@colorFormNotificationError: #ff5566;
+@colorPetrol: @icinga-blue;
+@menu-2ndlvl-highlight-color: @menu-2ndlvl-active-color;
+
+table.action {
+ .common-table();
+}
+
+table.avp {
+ .name-value-table();
+}
diff --git a/public/css/icinga/configmenu.less b/public/css/icinga/configmenu.less
new file mode 100644
index 0000000..05e50e8
--- /dev/null
+++ b/public/css/icinga/configmenu.less
@@ -0,0 +1,303 @@
+#menu {
+ margin-bottom: 3em;
+}
+
+.sidebar-collapsed #menu {
+ margin-bottom: 8em;
+}
+
+#menu .config-menu {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: @menu-bg-color;
+ margin-top: auto;
+
+ > ul {
+ display: flex;
+ flex-wrap: nowrap;
+ padding: 0;
+
+ > li {
+ > a {
+ padding: 0.5em 0.5em 0.5em 0.75em;
+ line-height: 2.167em;
+ white-space: nowrap;
+ text-decoration: none;
+
+ }
+
+ &:hover .nav-level-1 {
+ display: block;
+ }
+ }
+
+ li.active a:after {
+ display: none;
+ }
+
+ .user-nav-item {
+ width: 100%;
+ overflow: hidden; // necessary for .text-ellipsis of <a>
+
+ > a {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &:not(.active):hover a,
+ &:not(.active) a:focus {
+ background: @menu-hover-bg-color;
+ }
+ }
+
+ .config-nav-item {
+ line-height: 2;
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ > button {
+ background: none;
+ border: none;
+ display: block;
+ .rounded-corners();
+
+ > .state-badge {
+ position: absolute;
+ pointer-events: none;
+ }
+
+ .icon {
+ opacity: .8;
+ font-size: 1.25em;
+
+ &:before {
+ margin-right: 0;
+ }
+ }
+ }
+
+ &:hover > button {
+ background: fade(@menu-hover-bg-color, 25);
+
+ > .state-badge {
+ display: none;
+ }
+ }
+
+ button:focus {
+ background: fade(@menu-hover-bg-color, 25);
+ }
+
+ &.active > button {
+ color: @text-color-inverted;
+ background: @icinga-blue;
+ }
+ }
+
+ .state-badge {
+ line-height: 1.2;
+ padding: .25em;
+ font-family: @font-family-wide;
+ }
+ }
+
+ .nav-level-1 li {
+ &.badge-nav-item > a {
+ display: flex;
+ align-items: baseline;
+ width: 100%;
+
+ .state-badge {
+ margin-left: auto;
+ }
+ }
+ }
+
+ .nav-item-logout {
+ color: @color-critical;
+ border-top: 1px solid @gray-lighter;
+ }
+
+ .user-ball {
+ .ball();
+ .ball-size-l();
+ .ball-solid(@icinga-blue);
+
+ // icingadb-web/public/css/common.less: .user-ball
+ font-weight: bold;
+ text-transform: uppercase;
+
+ // compensate border vertically and add space to the right;
+ margin: -1px .2em -2px 0;
+ border: 1px solid @text-color-inverted;
+ font-style: normal;
+ line-height: 1.2;
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .config-menu {
+ .user-nav-item {
+ > a {
+ padding-right: 4.75em;
+ }
+
+ &.active.selected + .config-nav-item {
+ > button {
+ color: @text-color-inverted;
+ }
+ }
+ }
+
+ .config-nav-item {
+ position: absolute;
+ right: 2.5em;
+ bottom: 0;
+ top: 0;
+
+ .state-badge {
+ left: -1em;
+ top: 0;
+ }
+ }
+
+ .flyout {
+ bottom: 100%;
+ right: -2em;
+ width: 15em;
+ }
+}
+
+.sidebar-collapsed #menu .config-menu {
+ ul {
+ flex-direction: column;
+
+ .user-ball {
+ margin-left: .25em * 1.5/2;
+ margin-right: .5em + .25em * 1.5/2;
+ width: 2em * 1.5/2 ;
+ height: 2em * 1.5/2;
+ font-size: 2/1.5em;
+ line-height: 1;
+ }
+
+ .config-nav-item {
+ padding-right: 0;
+ margin-bottom: 3em;
+
+ .icon {
+ font-size: 1.5em;
+ }
+
+ button {
+ position: relative;
+ width: 3em;
+ margin: .125em .5em;
+ padding: .5em .75em;
+
+ .state-badge {
+ right: -.25em;
+ bottom: -.25em;
+ font-size: .75em;
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 4em;
+ }
+ }
+ }
+ }
+
+ .flyout {
+ bottom: 0;
+ left: 100%;
+ width: 14em;
+
+ &:before {
+ left: -.6em;
+ bottom: 1em;
+ transform: rotate(135deg);
+ }
+ }
+}
+
+.flyout {
+ display: none;
+ position: absolute;
+ border: 1px solid @gray-lighter;
+ background: @body-bg-color;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,.25);
+ z-index: 1;
+ .rounded-corners();
+
+ a {
+ font-size: 11/12em;
+ padding: 0.364em 0.545em 0.364em 2em;
+ line-height: 2;
+
+ &:hover {
+ text-decoration: none;
+ background: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+
+ h3 {
+ font-size: 10/12em;
+ color: @text-color-light;
+ letter-spacing: .1px;
+ padding: 0.364em 0.545em 0.364em 0.545em;
+ margin: 0;
+ }
+
+ .flyout-content {
+ overflow: auto;
+ // Partially escape to have ems calculated
+ max-height: calc(~"100vh - " 50/12em);
+ padding: .5em 0;
+ position: relative;
+ }
+
+ // Caret
+ &:before {
+ content: "";
+ display: block;
+ position: absolute;
+ transform: rotate(45deg);
+ background: @body-bg-color;
+ border-bottom: 1px solid @gray-lighter;
+ border-right: 1px solid @gray-lighter;
+ height: 1.1em;
+ width: 1.1em;
+ bottom: -.6em;
+ right: 2.5em;
+ }
+}
+
+// Prevent flyout to vanish on autorefresh
+#layout.config-flyout-open .config-nav-item {
+ .flyout {
+ display: block;
+ }
+
+ > button > .state-badge {
+ display: none;
+ }
+}
+
+#layout.minimal-layout .config-menu {
+ display: none;
+}
+
+#layout.minimal-layout #menu {
+ margin-bottom: 0;
+}
+
+#layout:not(.minimal-layout) #menu .primary-nav {
+ .user-nav-item,
+ .configuration-nav-item,
+ .system-nav-item {
+ display: none;
+ }
+}
diff --git a/public/css/icinga/controls.less b/public/css/icinga/controls.less
new file mode 100644
index 0000000..01cf152
--- /dev/null
+++ b/public/css/icinga/controls.less
@@ -0,0 +1,281 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+// TODO(el): Rename .filter to .filter-control
+
+// Hide auto sumbit info in controls
+.controls .autosubmit-info {
+ display: none;
+}
+
+
+// Backend selection control in user and group list views
+.backend-selection {
+ float: left;
+
+ .control-label-group, select {
+ display: inline-block;
+ }
+
+ select {
+ margin-left: .5em;
+ }
+}
+
+.controls input.search,
+input.search {
+ .transition(border 0.3s ease);
+ .transition(background-image 0.2s ease);
+
+ background-image: url(../img/icons/search_white.png);
+ background-repeat: no-repeat;
+ background-size: 1em 1em;
+ background-position: .25em center;
+ outline: none;
+ padding-left: 1.5em;
+ width: 20em;
+
+ &:focus {
+ background-image: url(../img/icons/search_icinga_blue.png);
+ }
+
+ &:focus:not([readonly]) {
+ border-color: @icinga-blue;
+ }
+}
+
+@light-mode: {
+ #menu input.search,
+ .controls input.search,
+ input.search {
+ background-image: url(../img/icons/search.png);
+ }
+};
+
+.backend-selection,
+.pagination-control,
+.selection-info,
+.sort-controls-container {
+ margin-bottom: 0.5em;
+}
+
+.filter {
+ // Display filter control on a new line
+ clear: both;
+ margin: .5em 0;
+
+ > a {
+ color: @icinga-blue;
+ padding: .5em;
+ line-height: 1;
+ }
+
+ > a > i {
+ text-align: center;
+ &:before {
+ margin-right: 0;
+ }
+ }
+
+ .form input {
+ padding: @vertical-padding @vertical-padding;
+ }
+}
+
+.controls .filter {
+ form .search {
+ height: 2em;
+ }
+}
+
+.controls .button-link {
+ height: 2em;
+}
+
+.limiter-control > select {
+ margin-left: .5em;
+}
+
+.pagination-control {
+ // Display the pagination-control on a new line
+ clear: both;
+ float: left;
+
+ li {
+ line-height: 1;
+
+ &.active {
+ border-bottom: 2px solid @icinga-blue;
+
+ > a,
+ > a:hover {
+ color: @icinga-blue;
+ /* Compensate border-bottom: 2px */
+ margin-bottom: -2px;
+ }
+
+ > a:hover {
+ background: none;
+ cursor: default;
+ text-decoration: none;
+ }
+ }
+
+ &.disabled {
+ color: @disabled-gray;
+ cursor: no-drop;
+ }
+
+ > a,
+ > span {
+ padding: 0.5em;
+ }
+ > a:hover {
+ background-color: @gray-lighter;
+ text-decoration: none;
+ }
+ }
+
+ .previous-page {
+ padding-left: 0;
+ }
+
+ .next-page {
+ padding-right: 0;
+ }
+}
+
+// Multi-selection info
+.selection-info {
+ float: right;
+ font-size: @font-size-small;
+
+ &:hover {
+ cursor: help;
+ }
+}
+
+.sort-control {
+ label {
+ width: auto;
+ margin-right: 0.5em;
+ }
+
+ select[name=sort] {
+ width: 12em;
+ margin-left: 0;
+ }
+
+ select[name=dir] {
+ width: 8em;
+ margin-left: 0;
+ }
+}
+
+.sort-controls-container {
+ clear: right;
+ float: right;
+ display: flex;
+
+ > *:not(:last-child) {
+ margin-right: .5em;
+ }
+}
+
+.sort-direction-control {
+ margin-left: 0.25em;
+ width: 1em;
+
+ .spinner {
+ line-height: 1;
+ }
+}
+
+.controls .icinga-controls {
+ .control-label-group {
+ margin-top: 0;
+ margin-bottom: 0;
+ line-height: 1.5em;
+ padding-top: 0.25em;
+ padding-bottom: 0.25em;
+ }
+
+ input,
+ select {
+ max-width: 16em;
+ }
+
+ select {
+ padding-right: 1.526em;
+ margin-top: 0;
+ margin-bottom: 0;
+ /* compensate inconsistent select height calculations */
+ line-height: 1;
+ max-height: 2em;
+ }
+}
+
+// Datetime picker colors
+
+// The less variables are essentially the official dark theme for the flatpickr
+@fp-calendarBackground: #3f4458;
+@fp-calendarBorderColor: darken(#3f4458, 50%);
+
+@fp-monthForeground: #fff;
+@fp-monthBackground: #3f4458;
+
+@fp-weekdaysBackground: transparent;
+@fp-weekdaysForeground: #fff;
+
+@fp-dayForeground: fadeout(white, 5%);
+@fp-dayHoverBackground: lighten(@fp-calendarBackground, 25%);
+
+@fp-todayColor: #eee;
+@fp-today_fg_color: #3f4458;
+
+@fp-selectedDayBackground: #80CBC4;
+
+.icinga-datetime-picker .flatpickr-day.today {
+ &:hover,
+ &:focus {
+ color: @fp-today_fg_color;
+ }
+}
+
+@light-mode: {
+ :root {
+ // These are actually the default colors for the flatpickr
+
+ @fp-dayForeground: #393939;
+ @fp-dayHoverBackground: #e6e6e6;
+
+ --fp-calendarBackground: #ffffff;
+ --fp-calendarBorderColor: @fp-dayHoverBackground;
+
+ --fp-arrowColor: fadeout(@fp-dayForeground, 40%);
+ --fp-arrow_hover_color: #f64747;
+
+ --fp-monthForeground: fadeout(black, 10%);
+ --fp-monthBackground: transparent;
+
+ --fp-weekdaysBackground: transparent;
+ --fp-weekdaysForeground: fadeout(black, 46%);
+ --fp-weekNumberForeground: fadeout(@fp-dayForeground, 70%);
+
+ --fp-dayForeground: @fp-dayForeground;
+ --fp-dayHoverBackground: @fp-dayHoverBackground;
+ --fp-disabledDayForeground: fadeout(@fp-dayForeground, 90%);
+ --fp-outsideRangeDayForeground: fadeout(@fp-dayForeground, 70%);
+ --fp-selectedDayBackground: #569FF7;
+ --fp-todayColor: #959ea9;
+
+ --fp-timeHoverBg: lighten(@fp-dayHoverBackground, 3);
+
+ --fp-hoverInvertedBg: fadeout(black, 95%);
+
+ --fp-numChooserSvgFillColor: fadeout(fadeout(black, 10%), 50%);
+ --fp-hoverNumChooserBg: fadeout(black, 90%);
+ --fp-numChooserBorderColor: fadeout(@fp-dayForeground, 85%);
+ }
+};
+
+// Datetime picker colors (end)
diff --git a/public/css/icinga/dev.less b/public/css/icinga/dev.less
new file mode 100644
index 0000000..a1e34be
--- /dev/null
+++ b/public/css/icinga/dev.less
@@ -0,0 +1,10 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+#fontsize-calc {
+ display: none;
+ width: 1000em;
+ height: 1em;
+ font-size: 1em;
+ position: absolute;
+ top: -2em;
+}
diff --git a/public/css/icinga/forms.less b/public/css/icinga/forms.less
new file mode 100644
index 0000000..00389ea
--- /dev/null
+++ b/public/css/icinga/forms.less
@@ -0,0 +1,596 @@
+/*! Icinga Web 2 | (c) 2019 Icinga Development Team | GPLv2+ */
+
+/**
+ Rules found in here are structured with two layers:
+
+ 1) form.icinga-form, that's what defines the general structure of our single/individual forms. It's not
+ supposed to be used for any other forms that are not the only content on the page (e.g. inline-forms)
+ 2) .icinga-controls, this defines the design of our controls. Any input that's part of a container with
+ this class gets our design applied
+ */
+
+// General form layout
+
+form.icinga-form {
+ max-width: 70em;
+ width: 80%;
+
+ .control-group {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+
+ // Negative margin-right because every child gets 1em right but we can't exclude
+ // the last element as it's impossible to identify the last *visible* element
+ margin: 1em -1em 1em 0;
+
+ > fieldset {
+ > .control-group:first-of-type {
+ margin-top: 0;
+ }
+
+ > .control-group:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .control-group > :not(.control-label-group) {
+ margin-right: 1em;
+ }
+
+ .form-controls {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ &.inline {
+ width: auto;
+
+ .control-group {
+ margin: 0;
+ align-items: center;
+
+ > :not(.control-label-group) {
+ margin-right: .5em;
+ }
+
+ &:last-child {
+ margin-right: -.5em;
+ }
+ }
+ }
+}
+
+form.inline {
+ display: inline-block;
+
+ fieldset {
+ display: inline-block;
+ vertical-align: top;
+ border: none;
+ }
+}
+
+// Minimal form layout
+
+#layout.minimal-layout,
+#layout.twocols:not(.wide-layout) {
+ form.icinga-form {
+ &:not(.inline) {
+ width: 100%;
+ }
+
+ .control-label-group {
+ text-align: left;
+ padding-bottom: 0;
+ padding-left: 0;
+ margin-bottom: 0;
+ }
+
+ .toggle-switch ~ .control-info:before {
+ margin-left: 0;
+ }
+
+ .errors {
+ margin: 0;
+ }
+ }
+}
+
+#layout.minimal-layout .icinga-form {
+ .form-controls {
+ input[type="submit"] {
+ width: 100%;
+
+ &:not(:last-child) {
+ margin-bottom: 1em;
+ }
+ }
+ }
+}
+
+// Label styles
+
+form.icinga-form .control-label-group {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ line-height: 1.1em;
+ padding: .5625em .5625em .5625em 0;
+ max-height: 2.5em;
+ text-align: right;
+ width: 14em;
+}
+
+form.icinga-form.inline .control-label-group {
+ width: auto;
+ line-height: 0.857em;
+}
+
+.icinga-controls fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+
+ legend {
+ font-weight: bold;
+ font-size: 4/3em;
+ margin: 0.556em 0 0.333em;
+ }
+}
+
+.icinga-controls .control-info {
+ line-height: 2.25em;
+ opacity: .6;
+
+ &:before {
+ margin-right: 0;
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+}
+
+form.icinga-form .control-group .control-info {
+ margin-left: -.5em;
+}
+form.icinga-form .control-group .toggle-switch ~ .control-info {
+ margin-left: 0;
+}
+
+// General input styles
+
+.icinga-controls {
+ input[type="text"],
+ input[type="password"],
+ input[type="number"],
+ input[type="datetime-local"],
+ input[type="date"],
+ input[type="time"],
+ input[type="file"],
+ textarea,
+ select {
+ background-color: @low-sat-blue;
+ }
+}
+
+form.icinga-form {
+ input[type="text"],
+ input[type="password"],
+ input[type="number"],
+ input[type="datetime-local"],
+ input[type="date"],
+ input[type="time"],
+ input[type="file"],
+ .control-group > fieldset,
+ textarea,
+ select {
+ flex: 1 1 auto;
+ width: 0;
+ }
+}
+
+.icinga-controls {
+ input:not([type="radio"]),
+ .toggle-switch,
+ button,
+ select,
+ textarea {
+ border: none;
+ .rounded-corners(.25em);
+ .appearance(none);
+ }
+}
+
+.icinga-controls {
+ input:not([type="checkbox"]),
+ .toggle-switch,
+ select,
+ textarea,
+ button,
+ .toggle-switch {
+ font-size: inherit;
+ padding: @vertical-padding;
+ }
+
+ input[type="radio"] {
+ margin-right: .25em;
+ }
+}
+
+form.icinga-form {
+ .control-group .toggle-switch,
+ .form-controls .toggle-switch {
+ margin-top: 0.5em*0.666666667;
+ margin-bottom: 0.5em*0.666666667;
+ }
+}
+
+form.icinga-form select:not([multiple]) {
+ // Compensate inconsistent select height calculations
+ line-height: 1em;
+ height: 2.25em;
+}
+
+// Remove native dropdown arrow in IE10+
+.icinga-controls select::-ms-expand {
+ display: none;
+ opacity: 0;
+}
+
+.icinga-controls select:not([multiple]) {
+ padding-right: 1.5625em;
+ background-image: url(../img/select-icon.svg);
+ background-repeat: no-repeat;
+ background-position: right center;
+ background-size: contain;
+}
+
+form.icinga-form select {
+ width: 0; // Prevent selects with long option values from exceeding the container
+}
+
+form.inline select {
+ width: auto;
+}
+
+
+// Specific input styles
+
+.link-button {
+ .action-link();
+ // Reset defaults
+ background: none;
+ border: none;
+ display: inline-block;
+ padding: 0;
+
+ text-align: left;
+}
+
+.icinga-controls {
+ input ~ .spinner,
+ button ~ .spinner,
+ select ~ .spinner,
+ textarea ~ .spinner {
+ line-height: normal;
+ padding: .5em 0;
+
+ &:before {
+ vertical-align: middle;
+ margin-left: .5em;
+ opacity: 0.4;
+ }
+ }
+}
+
+/* selects get their spinner specifically placed */
+.icinga-controls select:not([multiple]) + .spinner {
+ height: 2.25em;
+ margin: 0;
+
+ &:before {
+ margin-left: -3.75em;
+ }
+}
+
+form.icinga-form .form-controls {
+ .spinner {
+ order: -1;
+ }
+
+ .btn-primary {
+ order: 99;
+ }
+}
+
+// Button styles
+
+.icinga-controls {
+ button:not([type]),
+ button[type="submit"],
+ input[type="submit"],
+ input[type="submit"].btn-confirm {
+ .button();
+ }
+
+ input[type="submit"].btn-remove {
+ .button(@body-bg-color, @color-critical, @color-critical-accentuated);
+ }
+
+ input[type="submit"].btn-cancel {
+ .button(@body-bg-color, @gray, @black);
+ }
+
+ button.noscript-apply {
+ color: @gray;
+ background-color: @gray-lightest;
+ border-color: @gray;
+ border-width: 1px;
+ }
+
+ button[type="button"] {
+ background-color: @low-sat-blue;
+ }
+}
+
+form.icinga-form {
+ button[type="button"] {
+ line-height: normal;
+ }
+}
+
+form.inline {
+ :not([type="hidden"]) {
+ & ~ button:not([type]),
+ & ~ button[type="submit"],
+ & ~ input[type="submit"],
+ & ~ input[type="submit"].btn-confirm {
+ margin-left: @horizontal-padding;
+ }
+ }
+
+ button.noscript-apply {
+ margin-left: .5em;
+ padding: .1em;
+ }
+}
+
+// Toggle styles
+
+.icinga-controls .toggle-switch {
+ cursor: pointer;
+ position: relative;
+ display: inline-block;
+ height: 1.5em;
+ width: 2.625em;
+}
+
+.icinga-controls .toggle-switch .toggle-slider {
+ position: absolute;
+ left: 0;
+ top: 0;
+
+ display: inline-block;
+ background: @low-sat-blue;
+ border: 1px solid;
+ border-color: @low-sat-blue;
+ box-sizing: content-box;
+ border-radius: 1em;
+ height: 4/3em;
+ width: 8/3em;
+ vertical-align: middle;
+}
+
+.icinga-controls .toggle-switch .toggle-slider:before {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ background: @text-color-inverted;
+ border-radius: 1em;
+ border: 1px solid;
+ border-color: @low-sat-blue;
+ box-sizing: border-box;
+ content: "";
+ display: block;
+ height: 4/3em;
+ margin-left: 0;
+ width: 4/3em;
+
+ @transition: left .2s ease, margin .2s ease;
+ -webkit-transition: @transition;
+ -moz-transition: @transition;
+ -o-transition: @transition;
+ transition: @transition;
+}
+
+.icinga-controls input[type="checkbox"]:checked + .toggle-switch .toggle-slider {
+ background-color: @icinga-blue;
+ border: 1px solid;
+ border-color: @icinga-blue;
+}
+
+.icinga-controls input[type="checkbox"]:focus + .toggle-switch .toggle-slider {
+ box-shadow: 0 0 0 2px @body-bg-color, 0 0 0 4px @icinga-blue-light;
+}
+
+.icinga-controls input[type="checkbox"]:checked + .toggle-switch .toggle-slider:before {
+ border: 1px solid;
+ border-color: @icinga-blue;
+ left: 100%;
+ margin-left: -4/3em;
+}
+
+// Disabled inputs
+
+.icinga-controls .toggle-switch.disabled {
+ cursor: default;
+
+ & > .toggle-slider {
+ background-color: @gray-light;
+ border-color: @gray-light;
+
+ &:before {
+ background-color: @gray-lighter;
+ border-color: @gray-light;
+ }
+ }
+}
+
+form.icinga-form .control-group.disabled .control-label-group {
+ color: @disabled-gray;
+}
+
+.icinga-controls {
+ input[disabled],
+ select[disabled] {
+ background-color: @gray-lighter;
+ border-color: transparent;
+ }
+}
+
+// Errors and additional information
+
+form.icinga-form {
+ .form-notifications,
+ .form-description {
+ border-radius: .25em;
+ display: flex;
+ list-style: none;
+ align-items: center;
+ margin: 0 0 1em 0;
+ padding: .25em .5em;
+
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0 .5em;
+ }
+
+ li {
+ list-style: none;
+ }
+
+ & .form-notification-icon,
+ & .form-description-icon {
+ font-size: 2em;
+ margin-left: .25em;
+ opacity: .4;
+ align-self: flex-start;
+ }
+ }
+
+ .form-notifications {
+ &.info {
+ background-color: @form-info-bg-color;
+ }
+
+ &.error {
+ background-color: @form-error-bg-color;
+ }
+
+ &.warning {
+ background-color: @form-warning-bg-color;
+ }
+ }
+
+ .form-description {
+ background-color: @light-text-bg-color;
+ }
+
+ .errors {
+ list-style: none;
+ display: block;
+ width: 100%;
+ margin: 0 0 0 15em;
+ padding: 0;
+
+ & > li {
+ margin: 0.5em;
+ color: #f56;
+ }
+ }
+}
+
+form.icinga-form .form-info {
+ color: @text-color-light;
+ font-size: @font-size-small;
+ list-style: none;
+ padding-left: 0;
+}
+
+// Placeholder styles
+
+.icinga-controls {
+ input::placeholder {
+ color: @disabled-gray;
+ font-style: italic;
+ opacity: 1;
+ }
+
+ input:-ms-input-placeholder {
+ color: @disabled-gray;
+ font-style: italic;
+ opacity: 1;
+ }
+}
+
+// Specific form styles
+
+.search.inline {
+ display: inline-block;
+}
+
+/* Flyover form styles */
+
+.flyover-content form:not(.inline):not([role="search"]) {
+ width: auto;
+}
+
+.flyover-content .control-label-group {
+ text-align: left;
+}
+
+.theme-mode-input {
+ display: none;
+
+ &:checked + img {
+ border-color: @icinga-blue;
+ border-radius: .25em;
+ }
+
+ & + img {
+ margin: 0 auto;
+ border: .25em solid transparent;
+ display: block;
+ width: 6em;
+ overflow: hidden;
+ box-shadow: 0 0 .25em 0 rgba(0,0,0,.4);
+ }
+
+ &[disabled] ~ img,
+ &[disabled] ~ span {
+ opacity: .25;
+ }
+
+ & ~ span {
+ display: block;
+ text-align: center;
+ }
+}
+
+#layout.minimal-layout .icinga-form {
+ .theme-mode {
+ .control-label-group {
+ width: 100%;
+ margin-bottom: .5em;
+ }
+
+ label:first-of-type {
+ margin-left: auto;
+ }
+ }
+}
diff --git a/public/css/icinga/grid.less b/public/css/icinga/grid.less
new file mode 100644
index 0000000..e061bc8
--- /dev/null
+++ b/public/css/icinga/grid.less
@@ -0,0 +1,47 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.grid {
+ .clearfix();
+}
+
+[class^="col-"],
+[class*=" col-"] {
+ float: left;
+ // Fix that empty columns don't consume their width
+ min-height: 1px;
+}
+
+.controls {
+ [class^="col-"],
+ [class*=" col-"] {
+ padding: @vertical-padding / 2 0;
+ }
+}
+
+.col-1-2 {
+ width: 50%;
+}
+
+.col-1-3 {
+ width: 33.33%;
+}
+
+.col-2-3 {
+ width: 66.66%;
+}
+
+.col-3-3 {
+ width: 100%;
+}
+
+// TODO(el): Set proper breakpoints
+#layout.twocols,
+#layout.compact-layout,
+#layout.minimal-layout,
+#layout.poor-layout {
+ [class^="col-"],
+ [class*=" col-"] {
+ float: none;
+ width: 100%;
+ }
+}
diff --git a/public/css/icinga/health.less b/public/css/icinga/health.less
new file mode 100644
index 0000000..50cc11a
--- /dev/null
+++ b/public/css/icinga/health.less
@@ -0,0 +1,69 @@
+// Style
+
+.app-health {
+ header {
+ color: @text-color-light;
+
+ span {
+ color: @text-color;
+ }
+ }
+
+ span {
+ &.state-ok {
+ background-color: @color-ok;
+ }
+
+ &.state-warning {
+ background-color: @color-warning;
+ }
+
+ &.state-critical {
+ background-color: @color-critical;
+ }
+
+ &.state-unknown {
+ background-color: @color-unknown;
+ }
+ }
+
+ a {
+ font-weight: bold;
+ }
+
+ tbody tr, tr.active {
+ border: none;
+ }
+
+ tr:not(:last-child) td {
+ border: 0 solid;
+ border-color: @gray-light;
+ border-bottom-width: 1px;
+ }
+
+ section {
+ color: @text-color-light;
+ font-family: @font-family-fixed;
+ }
+}
+
+// Layout
+
+.app-health {
+ th {
+ width: 2.5em;
+ padding: .5em 1em 0 .5em;
+ vertical-align: top;
+ }
+
+ td {
+ padding: .5em 0;
+ }
+
+ section {
+ margin-top: .25em;
+ height: 3em;
+ overflow: hidden;
+ word-break: break-word;
+ }
+}
diff --git a/public/css/icinga/layout-structure.less b/public/css/icinga/layout-structure.less
new file mode 100644
index 0000000..b1ca8e6
--- /dev/null
+++ b/public/css/icinga/layout-structure.less
@@ -0,0 +1,167 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+html {
+ height: 100%;
+ font-family: 'default-layout';
+}
+
+body {
+ height: 100%;
+ overflow: hidden;
+}
+
+#layout {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+#content-wrapper {
+ flex: 1 1 auto;
+ display: flex;
+ height: 0;
+
+}
+
+#sidebar {
+ width: 16em;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ z-index: 2;
+}
+
+#layout:not(.minimal-layout) #sidebar:after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 1em;
+ background: linear-gradient(to left, rgba(0,0,0,.1), rgba(0,0,0,0));
+ z-index: 0;
+ pointer-events: none;
+}
+
+#main {
+ flex: 1;
+ display: flex;
+ z-index: 1;
+}
+
+.iframe {
+ #header, #sidebar {
+ display: none;
+ }
+}
+
+#responsive-debug {
+ font-size: 0.9em;
+ font-family: Courier new, monospace;
+ padding: 0.5em;
+ width: 25em;
+ color: white;
+ height: 10em;
+ display: none;
+ position: fixed;
+ bottom: 0.5em;
+ right: 2em;
+ overflow: hidden;
+ z-index: 1000;
+ background: #333;
+ border-radius: 0.5em;
+ opacity: 0.9;
+}
+
+#layout.minimal-layout #responsive-debug {
+ font-size: 0.6em;
+}
+
+#layout.poor-layout #responsive-debug {
+ font-size: 0.7em;
+}
+
+#layout.compact-layout #responsive-debug {
+ font-size: 0.8em;
+}
+
+#layout.wide-layout #responsive-debug {
+ font-size: 1em;
+}
+
+/** Fullscreen layout **/
+#layout.fullscreen-layout {
+ #sidebar {
+ display: none;
+ }
+
+ .container .controls {
+ padding: 0;
+ }
+
+ .controls > ul.tabs {
+ margin-top: 0;
+ height: 1.5em;
+ font-size: 0.75em;
+ padding: 0.2em 0 0;
+ }
+
+ .controls > ul.tabs > li > a {
+ line-height: 1.5em;
+ }
+}
+
+.controls-separated,
+.container .controls.separated {
+ box-shadow: 0 3px 4px -4px rgba(0, 0, 0, 0.2);
+// border-bottom: 1px solid @gray-lightest;
+ padding-bottom: @horizontal-padding / 2
+}
+
+.hbox {
+ display: inline-block;
+}
+
+.hbox-item {
+ display: inline-block;
+ vertical-align: top;
+ margin-top: 0.5em;
+ margin-bottom: 0.25em;
+ margin-left: 1em;
+ margin-right: 1em;
+}
+
+.hbox-spacer {
+ display: inline-block;
+ vertical-align: top;
+ width: 2em;
+}
+
+/*
+ * Class to hide content from users but available for screen reader
+ * Based on: https://cloudfour.com/thinks/see-no-evil-hidden-content-and-accessibility/
+ */
+.sr-only {
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: polygon(0px 0px, 0px 0px, 0px 0px);
+ -webkit-clip-path: polygon(0px 0px, 0px 0px, 0px 0px);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: fixed; // absolute causes view port glitches in chrome (#4310)
+ width: 1px;
+ white-space: nowrap;
+}
+
+// Hide non-javascript elements if javascript is enabled
+html.js *.no-js {
+ .sr-only;
+}
+
+// Hide javascript elements if javascript is disabled
+html.no-js *.js {
+ .sr-only;
+}
diff --git a/public/css/icinga/layout.less b/public/css/icinga/layout.less
new file mode 100644
index 0000000..c37da79
--- /dev/null
+++ b/public/css/icinga/layout.less
@@ -0,0 +1,379 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+#footer {
+ bottom: 0;
+ right: 0;
+ left: 12em;
+ position: fixed;
+ z-index: 999;
+}
+
+#layout.minimal-layout #footer {
+ left: 0;
+}
+
+.sidebar-collapsed #footer {
+ left: 3em;
+}
+
+#guest-error {
+ background-color: @icinga-blue;
+ height: 100%;
+ overflow: auto;
+}
+
+#guest-error #icinga-logo {
+ .fadein();
+}
+
+#guest-error-message {
+ .fadein();
+ color: @body-bg-color;
+ font-size: 2em;
+}
+
+#header,
+#login,
+#content-wrapper {
+ font-size: @font-size;
+ line-height: @line-height;
+}
+
+#header-logo-container {
+ background: @menu-bg-color;
+ height: 6em;
+ padding: 1.25em;
+ width: 16em;
+}
+
+#header-logo,
+#mobile-menu-logo {
+ background-image: url('../img/icinga-logo.svg');
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ display: block;
+ height: 100%;
+ width: 100%;
+
+ &:focus {
+ opacity: .6;
+ outline: none;
+ }
+}
+
+#mobile-menu-logo {
+ width: 50%;
+ float: left;
+ height: 2em;
+ margin-top: .25em;
+ background-position: .75em center;
+}
+
+#mobile-menu-toggle .icon-cancel {
+ display: none;
+}
+
+#icinga-logo {
+ background-image: url('../img/icinga-logo-big.svg');
+ background-position: center bottom;
+ background-repeat: no-repeat;
+ background-size: contain; // Does not work in IE < 10
+ height: 177px;
+ margin-bottom: 2em;
+ width: 100%;
+
+ &.invert {
+ background-image: url('../img/icinga-logo-big-dark.svg');
+ }
+}
+
+#layout {
+ background-color: @body-bg-color;
+ color: @text-color;
+ font-family: @font-family;
+}
+
+#login {
+ overflow: auto;
+}
+
+@gutter: 1em;
+
+// x-column-layout
+#main {
+ .clearfix();
+
+ & > .container {
+ width: 0;
+ overflow: auto;
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+
+ &:empty {
+ display: none;
+ }
+
+ & > .content {
+ flex: 1 1 auto;
+ overflow: auto;
+ }
+
+ & > .controls {
+ > .tabs {
+ // Remove gutter for tabs
+ margin-left: -1 * @gutter;
+ margin-right: -1 * @gutter;
+ height: 2.5em;
+ }
+
+ .tabs:first-child:not(:last-child) {
+ margin-bottom: .5em;
+ }
+ }
+ }
+}
+
+// Not part of the above to relax specificity and to allow modules adjust this
+:not(.dashboard) > .container {
+ & > .controls {
+ padding-left: @gutter;
+ padding-right: @gutter;
+ }
+
+ & > .content {
+ padding: @gutter;
+ }
+}
+
+// Mobile menu
+#layout.minimal-layout #sidebar {
+ background-color: @menu-bg-color;
+}
+
+#mobile-menu-toggle {
+ color: @menu-color;
+ text-align: right;
+
+ > button {
+ background: none;
+ border: none;
+ font-size: 2em;
+ padding: 0 .5em;
+ line-height: 2;
+
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ }
+
+ i:before {
+ margin-right: 0;
+ }
+}
+
+.container,
+.error-message,
+.modal-window {
+ // Don't outline containers and error messages when focused because they receive focus for accessibility only
+ // programmatically
+ outline: none;
+}
+
+.controls {
+ > .tabs {
+ overflow: hidden;
+ }
+}
+
+// Dashboard grid
+
+.dashboard {
+ letter-spacing: -0.417em;
+
+ > .container {
+ display: inline-block;
+ letter-spacing: normal;
+ vertical-align: top;
+ // Column width controlled by #layout
+ width: 100%;
+
+ &:last-of-type {
+ // See reponsive.less for gutters
+ padding-right: 0;
+ }
+ }
+}
+
+// Notification styles
+
+#notifications {
+ // Reset defaults
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+#notifications > li {
+ color: @text-color;
+ display: block;
+ line-height: 2.5em;
+ border-left: .5em solid @gray-light;
+ background: @body-bg-color;
+ margin-bottom: 1px;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,0.25);
+
+ .icon {
+ padding: .5em;
+ width: 3em;
+ text-align: center;
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ &.error {
+ border-color: @color-notification-error;
+ background: @color-notification-error;
+ color: @text-color-on-icinga-blue;
+
+ .icon {
+ color: @text-color-on-icinga-blue;
+ }
+ }
+
+ &.info {
+ border-color: @color-notification-info;
+
+ .icon {
+ color: @color-notification-info;
+ }
+ }
+
+ &.success {
+ border-color: @color-notification-success;
+
+ .icon {
+ color: @color-notification-success;
+ }
+ }
+
+ &.warning {
+ border-color: @color-notification-warning;
+ background: @color-notification-warning;
+ color: @text-color-inverted;
+
+ .icon {
+ color: @text-color-inverted;
+ }
+ }
+}
+
+// Collapsed sidebar
+#layout:not(.minimal-layout).sidebar-collapsed {
+ #header-logo-container {
+ height: 3em;
+ padding: 0.25em 0.125em;
+ width: 4em;
+ }
+
+ #header-logo {
+ background-image: url('../img/icinga-logo-compact.svg');
+ }
+
+ #sidebar {
+ width: 4em;
+ }
+
+ #open-sidebar {
+ display: inline;
+ }
+
+ #close-sidebar {
+ display: none;
+ }
+
+ #menu {
+ .nav-level-1 {
+ > .badge-nav-item > a {
+ position: relative;
+
+ > .badge {
+ position: absolute;
+ right: .5em;
+ bottom: .25em;
+ font-size: 75%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 4em;
+ }
+ }
+
+ > .nav-item.active > a > .badge {
+ display: unset;
+ }
+ }
+
+ img.icon {
+ margin: 0 1.25em -.25em 0.25em;
+ font-size: 1.5em;
+ }
+
+ .nav-item {
+ white-space: nowrap;
+ }
+
+ .nav-item.no-icon > a {
+ padding-left: .75em;
+ }
+
+ .nav-level-1 > .nav-item i {
+ font-size: 1.5em;
+ margin-right: .5em;
+ }
+
+ > .search-control {
+ height: 3.333em;
+ }
+ }
+
+ #search {
+ padding-left: 3.75em;
+ }
+
+ #search:focus {
+ background-color: @menu-bg-color;
+ border-radius: 0 .25em .25em 0;
+ box-shadow: 0 0 .25em 0 rgba(0, 0, 0, .2);
+ color: @menu-color;
+ width: 20em;
+ position: fixed;
+ z-index: 1;
+ }
+
+ .search-input {
+ font-size: 1.25em;
+ padding-right: .625em;
+ }
+
+ .search-reset {
+ display: none;
+ }
+
+ .skip-links {
+ a, button {
+ width: 20em;
+ }
+ }
+}
+
+@light-mode: {
+ #header-logo,
+ #mobile-menu-logo,
+ #about .icinga-logo {
+ filter: brightness(0.415) sepia(1) ~"saturate(0.1)" hue-rotate(144deg);
+ }
+};
diff --git a/public/css/icinga/login-orbs.less b/public/css/icinga/login-orbs.less
new file mode 100644
index 0000000..b0426dd
--- /dev/null
+++ b/public/css/icinga/login-orbs.less
@@ -0,0 +1,104 @@
+/*! Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#login {
+ background-image: none;
+
+ .login-form {
+ background: none;
+ box-shadow: none;
+ }
+}
+
+.orb {
+ display: block;
+ position: absolute;
+ pointer-events: none;
+ transform-origin: center center;
+}
+
+.orb img {
+ height: auto;
+ width: 100%;
+}
+
+#orb-analytics {
+ top: -19%;
+ width: 25%;
+ left: 22.5%;
+ z-index: 0;
+}
+
+#orb-analytics img {
+ opacity: .2;
+}
+
+#orb-automation {
+ bottom: -6%;
+ width: 60%;
+ left: 7%;
+ z-index: 0;
+ margin-left: -30%;
+ margin-bottom: -30%;
+}
+
+#orb-automation img {
+ opacity: .75;
+}
+
+#orb-cloud {
+ top: -6%;
+ width: 25%;
+ right: 4%;
+ z-index: 0;
+ margin-right: -12.5%;
+ margin-top: -12.5%;
+}
+
+#orb-cloud img {
+ opacity: .4;
+}
+
+#orb-notifactions {
+ top: 7%;
+ right: 46%;
+ width: 10%;
+ margin: -5%;
+}
+
+#orb-notifactions img {
+ opacity: .5;
+}
+
+#orb-metrics {
+ left: 5%;
+ top: 20%;
+ width: 35%;
+ margin: -17.5%;
+}
+
+#orb-metrics img {
+ opacity: .5;
+}
+
+#orb-icinga {
+ left: 50%;
+ top: 50%;
+ margin-top: -38.5em;
+ margin-left: -38em;
+ width: 75em;
+ z-index: 0;
+}
+
+#orb-icinga img {
+ opacity: .8;
+}
+
+#orb-infrastructure {
+ top: -36%;
+ left: -15%;
+ width: 30%;
+}
+
+#orb-infrastructure img {
+ opacity: .6;
+}
diff --git a/public/css/icinga/login.less b/public/css/icinga/login.less
new file mode 100644
index 0000000..b37dbd8
--- /dev/null
+++ b/public/css/icinga/login.less
@@ -0,0 +1,183 @@
+/*! Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+// Login page styles
+
+#login {
+ height: 100%;
+ background-color: @menu-bg-color;
+ background-image: url(../img/icingaweb2-background-orbs.jpg);
+ background-repeat: no-repeat;
+ background-size: cover;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .login-form {
+ width: 36em;
+ position: relative;
+ z-index: 10;
+ padding: 2em 6em;
+ background-color: @login-box-background;
+ .box-shadow(0, 0, 1em, 1em, @login-box-background);
+ }
+
+ #icinga-logo {
+ width: 100%;
+ max-width: 18em;
+ height: auto;
+ margin: 0 auto 2em auto;
+
+ &:after {
+ content: "";
+ display: block;
+ width: 100%;
+ padding-bottom: 35%;
+ }
+ }
+
+ .errors,
+ .form-errors {
+ list-style-type: none;
+ padding: 0.5em;
+ }
+
+ .errors {
+ background-color: @color-critical;
+ color: white;
+ }
+
+ .form-errors {
+ margin-top: 0;
+ padding: 0;
+ }
+
+ .form-errors,
+ .control-group {
+ &:not(:last-child) {
+ margin-bottom: 1em;
+ }
+ }
+
+ input[type=password],
+ input[type=text] {
+ display: block;
+ height: 2.5em;
+ margin: 0;
+ transition: none;
+ width: 100%;
+
+ &:focus {
+ .rounded-corners(3px);
+ border-radius: 0;
+ padding-bottom: 3px;
+ }
+ }
+
+ input[type="submit"]:focus {
+ outline: 3px solid;
+ outline-color: @icinga-blue-light;
+ }
+
+ input[type=submit] {
+ border-radius: .25em;
+ background: @icinga-secondary;
+ color: white;
+ border: none;
+ height: 2.5em;
+ margin: 0;
+ width: 100%;
+
+ &:hover {
+ background-color: @icinga-secondary-dark;
+ }
+ }
+
+ .config-note {
+ background-color: @color-critical;
+ margin: 0 auto 2em auto; // Center horizontally w/ bottom margin
+ max-width: 50%;
+ min-width: 24em;
+ padding: 1em;
+
+ a {
+ color: @text-color-inverted;
+ font-weight: bold;
+ }
+ }
+
+ .remember-me-box {
+ display: flex;
+ align-items: flex-start;
+
+ .toggle-switch {
+ margin-right: 1em;
+ }
+
+ .control-info {
+ line-height: 1.5;
+ margin-left: .5em;
+ }
+ }
+}
+
+#social {
+ position: fixed;
+ right: 1em; bottom: 1em;
+ letter-spacing: -.417em;
+ margin: 0;
+
+ > * {
+ letter-spacing: normal;
+ }
+
+ > li {
+ display: inline-block;
+
+ a {
+ display: block;
+ text-decoration: none;
+ -webkit-transform: scale(1, 1);
+ -moz-transform: scale(1, 1);
+ -ms-transform: scale(1, 1);
+ transform: scale(1, 1);
+ }
+
+ i {
+ font-size: 3em;
+ color: white;
+ text-shadow: 0 0 .5em #01507B;
+ }
+ }
+
+ > li a:hover {
+ -webkit-transform: scale(1.2, 1.2);
+ -moz-transform: scale(1.2, 1.2);
+ -ms-transform: scale(1.2, 1.2);
+ transform: scale(1.2, 1.2);
+ }
+
+ li:not(:last-child) {
+ margin-right: 2em;
+ }
+}
+
+#login-footer {
+ padding: .5em 0;
+
+ p {
+ margin-bottom: 0;
+ }
+
+ a {
+ text-decoration: underline;
+
+ &:hover {
+ opacity: .8;
+ }
+ }
+}
+
+.orb {
+ display: none;
+}
diff --git a/public/css/icinga/main.less b/public/css/icinga/main.less
new file mode 100644
index 0000000..bfe97d5
--- /dev/null
+++ b/public/css/icinga/main.less
@@ -0,0 +1,452 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+// Url for static ipl assets
+@iplWebAssets: "../lib/icinga/icinga-php-library";
+
+// Width for the name column--th--of name-value-table
+@name-value-table-name-width: 38/3em;
+
+.action-link {
+ color: @icinga-blue;
+}
+
+.error-message {
+ font-weight: @font-weight-bold;
+}
+
+.error-reason {
+ margin-top: 4em;
+}
+
+.large-icon {
+ font-size: 200%;
+}
+
+.content-centered {
+ margin: 0 auto;
+ text-align: center;
+}
+
+.icon-col {
+ text-align: center;
+ width: 1em;
+}
+
+.preformatted {
+ font-family: @font-family-fixed;
+ white-space: pre-wrap;
+}
+
+.markdown {
+ > * {
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ a {
+ border-bottom: 1px @text-color-light dotted;
+
+ &:hover, &:focus {
+ border-bottom: 1px @text-color solid;
+ text-decoration: none;
+ }
+
+ img {
+ max-width: 32em;
+ }
+
+ &.with-thumbnail {
+ img {
+ padding: 1px;
+ }
+
+ &:hover, &:focus {
+ img {
+ padding: 0;
+ }
+ }
+ }
+ }
+
+ table {
+ border-collapse: collapse;
+
+ th {
+ text-align: left;
+ background-color: @gray-lighter;
+ }
+
+ &, th, td {
+ border: 1px solid @gray-light;
+ }
+ }
+}
+
+.no-wrap {
+ white-space: nowrap;
+}
+
+.pull-right {
+ float: right;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.user-avatar {
+ height: 16px;
+ width: 16px;
+}
+
+.v-center {
+ > * {
+ vertical-align: middle;
+ }
+}
+
+.section {
+ margin-bottom: 2em;
+}
+
+a:hover > .icon-cancel {
+ color: @color-critical;
+}
+
+.icon-stateful {
+ .fg-stateful();
+}
+
+// Link styles
+
+.button-link {
+ .action-link();
+ .rounded-corners(3px);
+
+ background: @low-sat-blue;
+ display: inline-block;
+ padding: 0.25em 0.5em;
+
+ &:hover {
+ background: @low-sat-blue-dark;
+ text-decoration: none;
+ }
+}
+
+// List styles
+
+.comment-list {
+ margin: 0;
+
+ > dt {
+ border-bottom: 1px solid @gray-lighter;
+ margin-bottom: 0.25em;
+
+ &:hover {
+ background-color: @gray-lightest;
+
+ > .remove-action button:not(.spinner.active) {
+ visibility: visible;
+ }
+ }
+
+ > .remove-action button:not(.spinner.active) {
+ visibility: hidden;
+ }
+ }
+
+ > dd {
+ margin: 0 0 1em 0;
+ }
+}
+
+.comment-time {
+ color: @text-color-light;
+ font-size: @font-size-small;
+}
+
+.name-value-list {
+ > dd {
+ // Reset default margin
+ margin: 0;
+ }
+
+ > dt {
+ color: @text-color-light;
+ font-size: @font-size-small;
+ }
+}
+
+// Table styles
+
+.common-table {
+ width: 100%;
+
+ td, th {
+ padding-top: 1em;
+ }
+
+ td {
+ padding-bottom: 1em;
+ }
+
+ th {
+ text-align: left;
+ padding-bottom: 0.5em;
+ }
+
+ thead {
+ border-bottom: 1px solid @gray-light;
+ }
+
+ tbody tr {
+ border-bottom: 1px solid @gray-lightest;
+ border-left: 5px solid transparent;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ tr[href].active {
+ background-color: @tr-active-color;
+ border-left-color: @icinga-blue;
+ }
+
+ tr[href]:hover {
+ background-color: @tr-hover-color;
+ cursor: pointer;
+ }
+}
+
+.name-value-table {
+ width: 100%;
+}
+
+.name-value-table > caption {
+ margin-top: .5em;
+ text-align: left;
+ font-weight: bold;
+}
+
+.name-value-table > tbody > tr > th {
+ color: @text-color-light;
+ // Reset default font-weight
+ font-weight: normal;
+ padding-left: 0;
+ text-align: left;
+ vertical-align: top;
+ width: @name-value-table-name-width;
+}
+
+/* Styles for centering content of unknown width and height both horizontally and vertically
+ *
+ * Example markup:
+ * <div class="centered-ghost">
+ * <div class="centered-content">
+ * <p>I'm centered.</p>
+ * </div>
+ * </div>
+ */
+
+.centered-content {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.centered-ghost {
+ height: 100%;
+ text-align: center;
+ letter-spacing: -0.417em; // Remove gap between content and ghost
+}
+
+.centered-ghost > * {
+ letter-spacing: normal;
+}
+
+.centered-ghost:after {
+ content: '';
+ display: inline-block;
+ height: 100%;
+ vertical-align: middle;
+}
+
+// Responsive iFrames
+
+.iframe-container {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ padding-bottom: 75%;
+ width: 100%;
+
+ & > iframe {
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 100%;
+ }
+}
+
+// Collapsible Control
+#collapsible-control-ghost {
+ display: none;
+}
+
+.collapsible + .collapsible-control {
+ position: relative;
+ z-index: 1;
+
+ button {
+ .rounded-corners(50%);
+
+ float: right;
+ width: 2em;
+ height: 2em;
+ padding: 0;
+ margin-top: -1em;
+ margin-right: .25em;
+
+ background: @gray-lighter;
+ color: @gray;
+ border: none;
+ -webkit-box-shadow: 0 0 1/3em rgba(0,0,0,.3);
+ -moz-box-shadow: 0 0 1/3em rgba(0,0,0,.3);
+ box-shadow: 0 0 1/3em rgba(0,0,0,.3);
+
+ &:hover {
+ background: @gray-light;
+ }
+ }
+
+ button i:before {
+ margin-right: 0;
+ }
+}
+
+.collapsible[data-can-collapse]:not(.collapsed) + .collapsible-control button,
+.collapsible[data-can-collapse]:not(.collapsed) > .collapsible-control,
+details.collapsible[open] + .collapsible-control button,
+details.collapsible[open] > .collapsible-control {
+ i.expand-icon {
+ display: none;
+ }
+
+ i.collapse-icon {
+ display: inline;
+ }
+}
+
+.collapsible.collapsed + .collapsible-control button,
+.collapsible.collapsed > .collapsible-control,
+details.collapsible:not([open]) + .collapsible-control button,
+details.collapsible:not([open]) > .collapsible-control {
+ i.expand-icon {
+ display: inline;
+ }
+
+ i.collapse-icon {
+ display: none;
+ }
+}
+
+// Collapsibles
+
+.collapsible.collapsed:not(details) {
+ overflow: hidden;
+}
+
+.collapsible.collapsed:not([data-toggle-element], details) {
+ position: relative;
+
+ &:after {
+ content: "";
+ display: block;
+ height: 2em;
+ background: linear-gradient(@body-bg-color-transparent, @body-bg-color);
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1;
+
+ opacity: 1;
+ transition: opacity 2s 1s linear;
+ }
+}
+
+.role-memberships {
+ letter-spacing: -0.417em;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ > li {
+ display: inline-block;
+ letter-spacing: normal;
+ margin: 0;
+ padding: 0 0.25em 0 0;
+
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+}
+
+.module-dependencies {
+ .unmet-dependencies {
+ background-color: @color-warning;
+ color: @text-color-on-icinga-blue;
+ padding: .25em .5em;
+ margin-left: -.5em;
+ }
+
+ .name-value-table {
+ > caption {
+ font-weight: normal;
+ color: @text-color-light;
+ }
+
+ > tbody > tr > th {
+ font-weight: bold;
+ color: @text-color;
+ }
+
+ .missing {
+ color: @color-critical;
+ font-weight: bold;
+ }
+
+ td {
+ white-space: nowrap;
+
+ &.or-separator {
+ width: 100%;
+ transform: translate(0, 50%);
+ padding-left: 3em;
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: 1.5em;
+ width: 1.5em;
+ left: 0.5em;
+ border-top: 3px solid @gray;
+ border-right: 3px solid @gray;
+ border-top-right-radius: .50em;
+ transform: rotate(45deg);
+ }
+ }
+ }
+ }
+}
diff --git a/public/css/icinga/menu.less b/public/css/icinga/menu.less
new file mode 100644
index 0000000..98650a2
--- /dev/null
+++ b/public/css/icinga/menu.less
@@ -0,0 +1,554 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+#menu [class^="icon-"],
+#menu [class*=" icon-"] {
+ &:before {
+ width: 1.5em;
+ }
+}
+
+@icon-width: 1.7em; // 1.5em width + 0.2em right margin
+
+#menu {
+ background-color: @menu-bg-color;
+ width: 100%;
+ flex: 1;
+ overflow: auto;
+ overflow-x: hidden;
+}
+
+#menu .nav-item {
+ vertical-align: middle;
+
+ > a {
+ position: relative;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .nav-item > a:first-of-type {
+ // Respect overflowing content
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item {
+ overflow: hidden;
+}
+
+#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item > a {
+ // Clip overflowing content
+ overflow: hidden;
+ width: 4em;
+}
+
+#menu .nav-level-1 > .nav-item {
+ line-height: 2.167em; // 26 px
+ color: @menu-color;
+
+ &.active {
+ color: @menu-active-color;
+
+ > a > .badge {
+ display: none;
+ }
+
+ background-color: @menu-active-bg-color;
+ }
+
+ &.no-icon > a {
+ padding-left: @icon-width + .75em;
+ }
+
+ > a {
+ padding: 0.5em 0.5em 0.5em .75em;
+ }
+
+ &.active:not(.selected) > a:focus,
+ &.active:not(.selected) > a:hover {
+ background-color: @menu-active-hover-bg-color;
+ }
+
+ &:not(.selected) > a:hover,
+ &:not(.selected) > a:focus {
+ background-color: @menu-hover-bg-color;
+ }
+
+ // Balance icon weight for non active menu items
+ &:not(.active) > a > i {
+ opacity: .8;
+ }
+
+ & > a > .icon-letter:before {
+ content: attr(data-letter);
+ font-family: @font-family;
+ font-weight: 800;
+ text-transform: uppercase;
+ }
+}
+
+#menu ul:not(.nav-level-2) > .selected > a {
+ background-color: @menu-highlight-color;
+ color: @text-color-inverted;
+
+ &:focus {
+ background-color: @menu-highlight-hover-bg-color;
+ }
+
+ &:after {
+ .transform(rotate(45deg));
+
+ position: absolute;
+ right: -.75em;
+
+ background-color: @body-bg-color;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,0.6);
+ content: "";
+ display: block;
+ height: 1.25em;
+ margin-top: -1.75em;
+ width: 1.25em;
+ }
+}
+
+#menu .nav-level-2 > .nav-item {
+ // Collapse menu by default
+ display: none;
+ line-height: 1.833em; // 22px
+
+ > a {
+ color: @menu-2ndlvl-color;
+ font-size: @font-size-small;
+ padding: 0.364em 0.545em 0.364em 0.545em;
+
+ &:first-of-type {
+ padding-left: (@icon-width + .75em)/@font-size-small;
+ }
+ }
+
+ &.active {
+ overflow: hidden;
+ position: relative;
+ }
+
+ // Little caret on active level-2 item
+ &.active:after {
+ .transform(rotate(45deg));
+
+ background-color: @body-bg-color;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,.6);
+ content: "";
+ display: block;
+ height: 1.25em;
+ width: 1.25em;
+ position: absolute;
+ top: .5em;
+ right: -.75em;
+ z-index: 3;
+ }
+
+ &.active > a {
+ color: @menu-2ndlvl-active-color;
+ background-color: @menu-2ndlvl-active-bg-color;
+
+ &:focus {
+ &:first-of-type,
+ &:first-of-type ~ a {
+ color: @menu-2ndlvl-active-hover-color;
+ background-color: @menu-2ndlvl-active-hover-bg-color;
+ }
+ }
+ }
+}
+
+.no-js #menu .nav-level-2 > .nav-item {
+ // Expand menu if JavaScript is disabled
+ display: block;
+}
+
+#layout:not(.sidebar-collapsed) {
+ #menu .nav-level-1 > .nav-item {
+ &.active {
+ .nav-level-2 > li {
+ // Expand menu if active
+ display: block;
+ }
+ }
+ }
+}
+
+#menu img.icon {
+ line-height: 1;
+ margin: 0 0.5em -.05em 0.25em;
+ width: 1em;
+}
+
+#menu img[src*="/img/icons/"] {
+ &:not([src$="tux.png"]):not([src$="win.png"]):not([src$="_white.png"]) {
+ -webkit-filter: invert(100%);
+ -moz-filter: invert(100%);
+ -ms-filter: invert(100%);
+ filter: invert(100%);
+ }
+}
+
+.nav-item:hover img.icon {
+ opacity: .6;
+}
+
+#menu input.search {
+ background: transparent url('../img/icons/search_white.png') no-repeat 1em center;
+ background-size: 1em auto;
+ border: none;
+ color: @menu-color;
+ line-height: 2.167em;
+ padding: .25em;
+ padding-left: @icon-width + .75em;
+ width: 100%;
+
+ &:focus::placeholder {
+ color: @menu-color;
+ }
+ &:focus::-ms-input-placeholder {
+ color: @menu-color;
+ }
+
+ &.active {
+ background-color: @menu-active-bg-color;
+ }
+
+ &:hover,
+ &:focus {
+ background-color: @menu-search-hover-bg-color;
+ }
+}
+
+// Badge offset correction
+#menu > nav > .nav-level-1 > .badge-nav-item > a > .badge {
+ margin-top: 0.2em;
+}
+
+#menu .nav-level-2 > .badge-nav-item > a > .badge {
+ margin-top: 0.2em;
+ margin-right: .5em
+}
+
+// Hovered menu
+#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover,
+#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
+ > .nav-level-2 {
+ background-color: @menu-flyout-bg-color;
+ border: 1px solid;
+ border-color: @gray-light;
+ border-radius: .25em;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,.3);
+ padding: @vertical-padding 0;
+ width: 14em;
+ position: fixed;
+ z-index: 1;
+
+ &:after {
+ .transform(rotate(45deg));
+
+ background-color: @body-bg-color;
+ border-bottom: 1px solid @gray-light;
+ border-left: 1px solid @gray-light;
+ content: "";
+ display: block;
+ height: 1.1em;
+ width: 1.1em;
+ position: absolute;
+ top: 1em;
+ left: -.6em;
+ z-index: -1;
+ }
+ > .nav-item {
+ display: block;
+ padding-left: 0;
+ position: relative;
+
+ > a {
+ color: @menu-flyout-color;
+
+ &:first-of-type {
+ padding-left: 1.5em;
+ }
+ }
+
+ &:not(.active) {
+ a:hover, a:focus {
+ &:first-of-type,
+ &:first-of-type ~ a {
+ background-color: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+ }
+
+ &.active > a {
+ color: @menu-color;
+ }
+
+ // Hide activity caret when displayed as flyout
+ &:after {
+ display: none;
+ }
+ }
+ }
+
+ > a > .badge {
+ display: none;
+ }
+
+ img.icon {
+ opacity: .6;
+ }
+}
+
+#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
+ > .nav-level-2 {
+ // Position relative to parent
+ margin-left: 16em;
+ margin-top: -3.167em;
+ }
+}
+
+#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover {
+ > .nav-level-2 {
+ // Position relative to parent
+ margin-left: 4em;
+ margin-top: -3.333em;
+
+ > .badge-nav-item {
+ display: flex;
+
+ a:first-of-type {
+ flex: 1 1 auto;
+ width: 0;
+ }
+
+ a:first-of-type ~ a {
+ flex: 0;
+ width: auto;
+
+ &:hover,
+ &:focus {
+ .badge {
+ opacity: .6;
+ }
+ }
+ }
+ }
+ }
+}
+
+// Accessibility skip links
+.skip-links {
+ position: relative;
+ font-size: 1/.75em;
+
+ ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ li {
+ display: block;
+ a, button[type="submit"] {
+ background-color: @body-bg-color;
+ border: none;
+ left: -999px;
+ padding: @vertical-padding @horizontal-padding;
+ position: absolute;
+ width: 100%;
+ z-index: 1;
+ &:focus {
+ left: 0;
+ outline-offset: -3px;
+ }
+ }
+ button[type="submit"] {
+ text-align: left;
+ }
+ }
+ }
+}
+
+#sidebar.expanded {
+ #mobile-menu-toggle .icon-menu {
+ display: none;
+ }
+
+ #mobile-menu-toggle .icon-cancel {
+ display: inline-block;
+ }
+}
+
+.search-control {
+ position: relative;
+}
+
+.search-input:focus ~ .search-reset:hover {
+ background-color: @menu-active-hover-bg-color;
+}
+
+.search-reset {
+ background: none;
+ border: 0;
+ color: @menu-color;
+ cursor: pointer;
+ display: none;
+ height: 100%;
+ padding: 0;
+ user-select: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ &:focus,
+ &:hover {
+ background-color: @menu-search-hover-bg-color;
+ outline: none;
+ }
+}
+
+// Override forms.less
+input[type=text].search-input {
+ padding-right: 1.4em;
+ text-overflow: ellipsis;
+ transition: none;
+}
+
+.search-input:focus:-moz-placeholder { // FF 18-
+ color: @gray-light;
+}
+
+.search-input:focus::-moz-placeholder { // FF 19+
+ color: @gray-light;
+}
+
+.search-input:focus:-ms-input-placeholder {
+ color: @gray-light;
+}
+
+.search-input:focus::-webkit-input-placeholder {
+ color: @gray-light;
+}
+
+.search-input ~ .search-reset {
+ opacity: 0;
+}
+
+.search-input:valid ~ .search-reset {
+ display: block;
+ opacity: 1;
+}
+
+.search-input:invalid,
+.search-input:-moz-submit-invalid,
+.search-input:-moz-ui-invalid {
+ // Disable glow
+ box-shadow: none;
+}
+
+// Toggle sidebar button
+#toggle-sidebar {
+ font-size: 1/.75em;
+
+ // Reset button styles
+ background: none;
+ border: none;
+ padding: 0;
+ color: @text-color-light;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ line-height: 2;
+
+ i {
+ background-color: @body-bg-color;
+ border-radius: .25em 0 0 .25em;
+ font-size: 1.125em;
+ width: 2em;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:hover, &:focus {
+ i {
+ color: @menu-highlight-color;
+ }
+ }
+}
+
+html.no-js #toggle-sidebar {
+ display: none;
+}
+
+#layout.minimal-layout #toggle-sidebar {
+ display: none;
+}
+
+#open-sidebar {
+ display: none;
+}
+
+#open-sidebar:before,
+#close-sidebar:before {
+ width: 1.4em;
+ margin-right: 0;
+}
+
+#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li {
+ &.nav-item:not(.badge-nav-item) {
+ &:not(.selected):not(.active) a:hover,
+ &:not(.selected):not(.active) a:focus {
+ background-color: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li,
+#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item:not(.active).hover .nav-level-2 > li {
+ &.badge-nav-item {
+ display: flex;
+ }
+
+ &.badge-nav-item a:first-of-type {
+ flex: 1 1 auto;
+ width: 0;
+ }
+
+ &.badge-nav-item a:first-of-type ~ a {
+ flex: 0;
+ width: auto;
+
+ &:hover,
+ &:focus {
+ .badge {
+ opacity: .6;
+ }
+ }
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li {
+ &.badge-nav-item:not(.selected) {
+ a:hover,
+ a:focus {
+ &:first-of-type,
+ &:first-of-type ~ a {
+ background-color: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+ }
+}
diff --git a/public/css/icinga/mixins.less b/public/css/icinga/mixins.less
new file mode 100644
index 0000000..6c55512
--- /dev/null
+++ b/public/css/icinga/mixins.less
@@ -0,0 +1,201 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.button(
+ @background-color: @body-bg-color,
+ @border-font-color: @icinga-blue,
+ @color-dark: @icinga-blue-dark
+) {
+ .rounded-corners(3px);
+
+ display: inline-flex;
+ align-items: baseline;
+ background-color: @background-color;
+ border: 2px solid @border-font-color;
+ color: @border-font-color;
+ cursor: pointer;
+ line-height: normal;
+ outline: none;
+ padding: ~"calc(@{vertical-padding} - 2px)" @horizontal-padding;
+
+ @duration: 0.2s;
+ // The trailing semicolon is needed to be able to pass this as a css list
+ .transition(background @duration, border @duration ease, color @duration ease;);
+
+ &:focus,
+ &:hover,
+ &.btn-primary {
+ background-color: @border-font-color;
+ color: @background-color;
+ }
+
+ &.btn-primary:focus,
+ &.btn-primary:hover {
+ background-color: @color-dark;
+ border-color: @color-dark;
+ color: @background-color;
+ }
+
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.clearfix {
+ &:after {
+ content: "";
+ clear: both;
+ display: table;
+ }
+}
+
+.opacity(@opacity: 0.6) {
+ opacity: @opacity;
+}
+
+.transform(@transform) {
+ -webkit-transform: @transform;
+ -moz-transform: @transform;
+ -ms-transform: @transform;
+ -o-transform: @transform;
+ transform: @transform;
+}
+
+.user-select(@user-select) {
+ -webkit-user-select: @user-select;
+ -moz-user-select: @user-select;
+ -ms-user-select: @user-select;
+ user-select: @user-select;
+}
+
+.transition (@transition) {
+ -webkit-transition: @transition;
+ -moz-transition: @transition;
+ -o-transition: @transition;
+ transition: @transition;
+}
+
+// Fadein animation
+
+/* Chrome, WebKit */
+@-webkit-keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+/* FF < 16 */
+@-moz-keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+/* Opera < 12.1 */
+@-o-keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.fadein() {
+ opacity: 0;
+
+ -webkit-animation: fadein 2s ease-in; /* Chrome, WebKit */
+ -moz-animation: fadein 2s ease-in; /* FF < 16 */
+ -o-animation: fadein 2s ease-in; /* Opera < 12.1 */
+ animation: fadein 2s ease-in;
+
+ // Make sure that after animation is done we remain at the last keyframe value (opacity: 1)
+ -webkit-animation-fill-mode: forwards;
+ -moz-animation-fill-mode: forwards;
+ -o-animation-fill-mode: forwards;
+ animation-fill-mode: forwards;
+}
+
+// Mixin for stateful foreground colors, e.g. text or icons
+.fg-stateful {
+ &.state-ok {
+ color: @color-ok;
+ }
+ &.state-up {
+ color: @color-up;
+ }
+ &.state-warning {
+ color: @color-warning;
+ &.handled {
+ color: @color-warning-handled;
+ }
+ }
+ &.state-critical {
+ color: @color-critical;
+ &.handled {
+ color: @color-critical-handled;
+ }
+ }
+ &.state-down {
+ color: @color-down;
+ &.handled {
+ color: @color-down-handled;
+ }
+ }
+ &.state-unreachable {
+ color: @color-unreachable;
+ &.handled {
+ color: @color-unreachable-handled;
+ }
+ }
+ &.state-unknown {
+ color: @color-unknown;
+ &.handled {
+ color: @color-unknown-handled;
+ }
+ }
+ &.state-pending {
+ color: @color-pending;
+ }
+}
+
+// Mixin for stateful background colors
+.bg-stateful {
+ &.state-ok {
+ background-color: @color-ok;
+ }
+ &.state-up {
+ background-color: @color-up;
+ }
+ &.state-warning {
+ background-color: @color-warning;
+ &.handled {
+ background-color: @color-warning-handled;
+ }
+ }
+ &.state-critical {
+ background-color: @color-critical;
+ &.handled {
+ background-color: @color-critical-handled;
+ }
+ }
+ &.state-down {
+ background-color: @color-down;
+ &.handled {
+ background-color: @color-down-handled;
+ }
+ }
+ &.state-unreachable {
+ background-color: @color-unreachable;
+ &.handled {
+ background-color: @color-unreachable-handled;
+ }
+ }
+ &.state-unknown {
+ background-color: @color-unknown;
+ &.handled {
+ background-color: @color-unknown-handled;
+ }
+ }
+ &.state-pending {
+ background-color: @color-pending;
+ }
+}
diff --git a/public/css/icinga/modal.less b/public/css/icinga/modal.less
new file mode 100644
index 0000000..3d497d6
--- /dev/null
+++ b/public/css/icinga/modal.less
@@ -0,0 +1,113 @@
+#layout > #modal {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+
+ background-color: rgba(0, 0, 0, .6);
+ opacity: 0;
+ font-size: @font-size;
+ line-height: @line-height;
+ pointer-events: none;
+ transition: opacity .2s ease-in; // This is coupled with a `setTimout` in modal.js
+ z-index: 1000;
+
+ &.active {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ > div {
+ height: 100%;
+ pointer-events: none;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
+
+#modal-content {
+ display: flex;
+ flex: 10;
+ flex-direction: column;
+ justify-content: stretch;
+
+ > .content {
+ padding: 1em;
+
+ > .icinga-form {
+ width: 100%;
+ }
+ }
+}
+
+#modal-ghost {
+ display: none;
+}
+
+.modal-area {
+ display: flex;
+ flex-direction: row;
+ flex-grow: 1;
+ justify-content: stretch;
+}
+
+.modal-header {
+ padding: .25em 0;
+ position: relative;
+ text-align: center;
+
+ > button {
+ position: absolute;
+ top: .75em;
+ right: 1em;
+
+ background-color: @gray;
+ border: none;
+ border-radius: 50%;
+ color: @text-color-inverted;
+ height: 1.5em;
+ line-height: 1em;
+ padding: 0;
+ text-align: center;
+ width: 1.5em;
+
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ }
+
+ > button:hover {
+ opacity: .8;
+ }
+
+ > button > .icon-cancel:before {
+ margin-right: 0;
+ }
+}
+
+.modal-header h1 {
+ padding: .25em;
+ margin: 0;
+}
+
+.modal-window {
+ overflow: auto;
+ pointer-events: auto;
+
+ display: flex;
+ align-items: stretch;
+ flex-direction: column;
+
+ background-color: @body-bg-color;
+ border-radius: .5em;
+ box-shadow: 0 0 2em 0 rgba(0, 0, 0, .6);
+ flex: 1;
+ margin: 0 auto;
+ max-height: 80%;
+ min-height: 40vh;
+ max-width: 60em;
+}
diff --git a/public/css/icinga/nav.less b/public/css/icinga/nav.less
new file mode 100644
index 0000000..f7950f1
--- /dev/null
+++ b/public/css/icinga/nav.less
@@ -0,0 +1,50 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.badge-nav-item {
+ > a {
+ .clearfix();
+
+ > .badge {
+ float: right;
+ }
+ }
+}
+
+.dropdown-nav-item > ul {
+ display: none;
+ position: absolute;
+}
+
+.dropdown-nav-item.active > ul,
+.dropdown-nav-item:hover > ul {
+ display: block;
+}
+
+.nav {
+ // Reset defaults
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ li > a,
+ li > span {
+ // Rollover
+ display: block;
+ }
+}
+
+.nav .nav-item .icon:before {
+ width: 1.5em;
+}
+
+.tab-nav {
+ .clearfix();
+
+ > li {
+ float: left;
+ }
+}
+
+.primary-nav a {
+ font-weight: 500;
+}
diff --git a/public/css/icinga/php-diff.less b/public/css/icinga/php-diff.less
new file mode 100644
index 0000000..d8f6a80
--- /dev/null
+++ b/public/css/icinga/php-diff.less
@@ -0,0 +1,17 @@
+@diff-bg-color: transparent;
+@diff-text-color: @text-color;
+
+@diff-bg-color-ins-base: @color-up;
+@diff-bg-color-del-base: @color-down;
+@diff-bg-color-rep-base: @color-warning;
+
+@diff-border-color: @gray-light;
+
+@light-mode: {
+ :root {
+ --diff-bg-color: var(--body-bg-color);
+ --diff-text-color: var(--text-color);
+
+ --diff-border-color: var(--gray-light);
+ }
+};
diff --git a/public/css/icinga/print.less b/public/css/icinga/print.less
new file mode 100644
index 0000000..75d4728
--- /dev/null
+++ b/public/css/icinga/print.less
@@ -0,0 +1,39 @@
+/*! Icinga Web 2 | (c) 2015 Icinga GmbH | GPLv2+ */
+
+@media print {
+ #sidebar,
+ #migrate-popup, // Icinga DB Web
+ .controls,
+ .footer, // ipl
+ .dontprint, // Compat only, use dont-print instead
+ .dont-print {
+ display: none !important;
+ }
+
+ #main > .container {
+ overflow: visible !important;
+
+ > .content {
+ overflow: visible !important;
+ }
+ }
+
+ :root {
+ --body-bg-color: #fff !important;
+ --text-color: #535353 !important;
+ --text-color-light: #7F7F7F !important;
+ --tr-active-color: #fff !important;
+ --tr-hover-color: #fff !important;
+
+ // ipl-web overrides
+ --default-bg: #fff !important;
+ --default-text-color: #535353 !important;
+ --default-text-color-inverted: #fff !important;
+ }
+}
+
+@media not print {
+ .print-only {
+ display: none !important;
+ }
+}
diff --git a/public/css/icinga/responsive.less b/public/css/icinga/responsive.less
new file mode 100644
index 0000000..6d1cae8
--- /dev/null
+++ b/public/css/icinga/responsive.less
@@ -0,0 +1,167 @@
+/*! Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+// Not growing larger than 3840px at 1em=16px right now
+@media screen and (min-width: 240em) {
+ #header {
+ min-width: 240em;
+ }
+
+ #main {
+ width: 227em;
+ }
+}
+
+// More than 100em, usually 1600px at 1em=16px
+@media screen and (min-width: 100em) {
+ html {
+ font-family: 'wide-layout';
+ }
+}
+
+// Up to 1152px at 1em=16px
+@media screen and (max-width:72em) {
+ html {
+ font-family: 'compact-layout';
+ }
+}
+
+// Up to 752px at 1em=16px
+@media screen and (max-width: 47em) {
+ html {
+ font-family: 'poor-layout';
+ }
+}
+
+// Up to 576px at 1em=16px, should fit 320px devices
+@media screen and (max-width: 36em) {
+ html {
+ font-family: 'minimal-layout';
+ }
+}
+
+#layout.compact-layout {
+ font-size: 0.875em;
+}
+
+#layout.poor-layout {
+ font-size: 0.875em;
+
+ #layout.twocols {
+ #col1 {
+ display: none;
+ }
+
+ #main > .container {
+ width: 100%;
+ }
+ }
+
+ .dashboard > div.container {
+ width: 100%;
+ }
+}
+
+#layout:not(.minimal-layout) {
+ #mobile-menu-toggle {
+ display: none;
+ }
+}
+
+#layout.minimal-layout {
+ #sidebar {
+ width: 100%;
+ overflow: auto;
+ }
+
+ #header-logo-container {
+ width: auto;
+ height: 4em;
+ padding: 0;
+ background: inherit;
+ }
+
+ #header-logo {
+ float: left;
+ width: 9em;
+ height: 3em;
+ margin: .5em 1em;
+ background-position: left center;
+ }
+
+ #mobile-menu-toggle {
+ float: right;
+ }
+
+ #sidebar:not(.expanded) #menu {
+ display: none;
+ }
+
+ #menu {
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ }
+
+ #content-wrapper {
+ flex-direction: column;
+ }
+
+ #main {
+ flex: 1 1 auto;
+ height: 0;
+ }
+
+ ul > .selected > a:after,
+ ul > .nav-item.active:after {
+ display: none;
+ }
+
+ .dashboard > div.container {
+ width: 100%;
+ }
+}
+
+// Dashboard
+
+.dashboard > .container {
+ padding-right: 0;
+ width: 100%;
+}
+
+#layout:not(.twocols).default-layout .dashboard > .container:not(:only-child) {
+ padding-right: @gutter;
+ width: 50%;
+}
+
+#layout:not(.twocols).wide-layout .dashboard > .container:not(:only-child) {
+ padding-right: @gutter;
+ width: 33.33%;
+}
+
+// Columns
+
+#layout.twocols #col2 {
+ border-left: 1px solid @gray-lighter;
+}
+
+#layout.twocols.wide-layout #col2 {
+ flex-grow: 2;
+}
+
+// Safe areas for iPhone X
+
+#header, #sidebar, #footer {
+ padding-left: constant(safe-area-inset-left);
+}
+
+#main, #footer {
+ padding-right: constant(safe-area-inset-right);
+}
+
+#layout.twocols #col2 {
+ border-left: 1px solid @gray-lighter;
+
+ &:empty {
+ display: flex;
+ }
+}
diff --git a/public/css/icinga/setup.less b/public/css/icinga/setup.less
new file mode 100644
index 0000000..c848bc7
--- /dev/null
+++ b/public/css/icinga/setup.less
@@ -0,0 +1,479 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+#setup-content-wrapper {
+ height: 0;
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+
+ > .setup-content {
+ height: 0;
+ overflow: auto;
+ flex: 1 1 auto;
+ }
+}
+
+.setup-header {
+ width: 100%;
+ height: 5.5em;
+ background-color: @icinga-blue;
+ text-align: center;
+
+ img {
+ width: 7.5em;
+ margin: 1.5em;
+ float: left;
+ }
+
+ form[name='setup_restart_form'] button {
+ background: none;
+ border: none;
+ color: #ffffff;
+ cursor: pointer;
+ outline: none;
+ font-size: 1.4em;
+ margin-right: 0.6em;
+ -moz-transform: scale(1, -1);
+ -webkit-transform: scale(1, -1);
+ -o-transform: scale(1, -1);
+ -ms-transform: scale(1, -1);
+ transform: scale(1, -1);
+ }
+
+ .progress-bar {
+ overflow: hidden;
+ padding-top: 1em;
+
+ .step {
+ float: left;
+
+ h1 {
+ margin: 0;
+ color: white;
+ font-size: 0.9em;
+ text-align: center;
+ border-bottom: none;
+ }
+
+ table {
+ margin-top: 0.3em;
+
+ td {
+ padding: 0;
+
+ &.left, &.right {
+ width: 50%;
+ }
+ }
+ }
+
+ div {
+ background-color: lightgrey;
+
+ &.line {
+ height: 0.4em;
+
+ &.left {
+ margin-left: 0.1em;
+ margin-right: -0.1em;
+ border-top-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
+ }
+
+ &.right {
+ margin-left: -0.1em;
+ margin-right: 0.1em;
+ border-top-right-radius: 0.5em;
+ border-bottom-right-radius: 0.5em;
+ }
+ }
+
+ &.bubble {
+ width: 1.2em;
+ height: 1.2em;
+ border-radius: 1.2em;
+
+ // Make sure that such a bubble overlays lines
+ position: relative;
+ z-index: 1337;
+ }
+
+ &.active {
+ background-color: white;
+ }
+
+ &.complete {
+ background-color: @color-ok;
+ }
+
+ &.visited {
+ background-color: #eee;
+ }
+ }
+ }
+ }
+}
+
+.setup-content {
+ padding: 1.5em 10em 0 10em;
+
+ h1 {
+ font-weight: bold;
+ }
+
+ form {
+ h2 {
+ font-size: 2.0em;
+ }
+ }
+}
+
+.setup-content .control-group > :not([hidden]) {
+ display: inline-block;
+ margin-right: 1em;
+}
+
+.setup-content div.buttons {
+ margin-top: 1.5em; // Yes, -top and -bottom, keep it like that...
+ margin-bottom: 1.5em;
+
+ .double {
+ position: absolute;
+ left: -1337px;
+ }
+
+ .control-button,
+ input[type="submit"] {
+ .button();
+ }
+
+ .control-button[disabled] {
+ background: none;
+ cursor: default;
+ color: @control-disabled-color;
+ border: 1px solid @control-disabled-color;
+
+ &:hover {
+ color: @control-disabled-color;
+ background: none;
+ border: 1px solid @control-disabled-color;
+ }
+ }
+
+ button.finish, a.button-like.login {
+ min-width: 25em;
+ }
+
+ .spinner {
+ margin-left: 1em;
+ }
+}
+
+.setup-content div.buttons + ul.hints {
+ margin-top: -1.5em;
+ margin-bottom: 1.5em;
+}
+
+form#setup_requirements {
+ margin-top: 2em;
+ padding-top: 0.5em;
+ border-top: 1px solid #888;
+
+ div.buttons div.requirements-refresh {
+ width: 25%;
+ float: right;
+ text-align: center;
+
+ a.button-like {
+ padding: 0.1em 0.4em;
+ }
+ }
+}
+
+.setup-content ul.requirements {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+
+ li {
+ margin-bottom: 1em;
+
+ & > ul {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ }
+
+ div {
+ float: left;
+ padding-top: 0.4em;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ }
+
+ div.title {
+ width: 25%;
+
+ h2 {
+ padding: 0;
+ margin: 0 1em 0 0;
+ border-bottom: 0;
+ }
+ }
+
+ div.description {
+ width: 50%;
+ border-left: 0.4em solid transparent;
+ border-right: 0.4em solid transparent;
+
+ ul {
+ margin: 0;
+ padding-left: 1em;
+ list-style-type: square;
+ }
+ }
+
+ div.state {
+ width: 25%;
+ color: white;
+ padding: 0.4em;
+
+ &.fulfilled {
+ background-color: @color-ok;
+ }
+
+ &.not-available {
+ color: black;
+ background-color: #e8ec70;
+ }
+
+ &.missing {
+ background-color: @color-critical;
+ }
+ }
+ }
+}
+
+#setup_ldap_discovery_confirm table {
+ margin: 1em 0;
+ border-collapse: separate;
+ border-spacing: 1em 0.2em;
+}
+
+#setup_admin_account {
+ div.instructions {
+ width: 30.2em;
+ display: inline-block;
+ }
+
+ div.radiobox {
+ vertical-align: top;
+ display: inline-block;
+ padding: 0.9em 0.2em 0;
+ }
+}
+
+.setup-content {
+ div.summary {
+ font-size: 90%;
+
+ div.page {
+ float: left;
+ width: 25em;
+ min-height: 25em;
+ padding: 0 1em 1em;
+ margin: 1em 1.5em 1.5em;
+ border: 1px dashed lightgrey;
+
+ h2 {
+ font-size: 1.2em;
+ font-weight: bold;
+ }
+
+ div.topic {
+ margin-left: 2em;
+
+ h3 {
+ font-size: 1em;
+ }
+
+ ul {
+ list-style-type: circle;
+ }
+
+ table {
+ border-spacing: 0.5em;
+ border-collapse: separate;
+ font-size: 0.9em;
+ margin-left: 2em;
+ }
+ }
+ }
+ }
+
+ form.summary {
+ clear: left;
+ }
+}
+
+#setup-finish {
+ h2 {
+ padding: 0.5em;
+ border-bottom: 0;
+ font-variant: normal;
+ font-weight: bold;
+ color: white;
+
+ &.success {
+ background-color: @color-ok;
+ }
+
+ &.failure {
+ background-color: @color-critical;
+ }
+ }
+
+ pre.log-output {
+ width: 66%;
+ height: 25em;
+ max-height: none;
+ }
+
+ div.buttons {
+ margin-top: 0;
+ text-align: center;
+
+ a {
+ padding: 0.5em;
+ }
+ }
+}
+
+.welcome-page {
+ margin-top: 3em;
+ text-align: center;
+
+ h2 {
+ font-size: 2.0em;
+ margin-bottom: 2em;
+ }
+
+ div.info {
+ padding: 0 1em;
+ background-color: #eee;
+ border: 1px solid lightgrey;
+ }
+
+ p.restart-warning {
+ color: coral;
+ font-weight: bold;
+ }
+
+ form ul.errors {
+ display: block;
+
+ list-style-type: none;
+ color: red;
+ }
+
+ div.note {
+ padding: 1em 1em 0;
+ margin: 3em auto 0;
+ text-align: left;
+ font-size: 0.9em;
+ border: 1px solid;
+ border-color: @gray-light;
+
+ h3 {
+ padding: 0.2em;
+ margin: -1em -1em 1em;
+ text-align: center;
+ color: @text-color;
+ background-color: @gray-lightest;
+ border: 1px solid;
+ border-color: @gray-light;
+ }
+
+ img {
+ float: right;
+ }
+
+ p {
+ margin: 2em 0 1em 0;
+
+ &:first-child {
+ margin-top: 1em;
+ }
+ }
+
+ div.code {
+ margin: 0 2em;
+
+ span {
+ display: block;
+ font-family: monospace;
+ }
+ }
+ }
+}
+
+#setup_monitoring_welcome {
+ .welcome-page;
+ margin-top: 0;
+ padding: 1em;
+
+ h2 {
+ margin-top: 0;
+ }
+}
+
+#setup_modules {
+ div.module {
+ float: left;
+ width: 15em;
+ height: 15em;
+ margin: 1em;
+ padding: 0.3em;
+ border: 1px solid;
+ border-color: @gray-semilight;
+ background-color: @gray-lightest;
+
+ .header {
+ height: 2.5em;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ h3 {
+ margin: 0;
+ border: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ label {
+ cursor: pointer;
+ }
+ }
+
+ label.description {
+ display: inline-block;
+ width: 14.4em;
+ height: 12em;
+ overflow: auto;
+ cursor: pointer;
+ font-weight: normal;
+ }
+
+ input[type=checkbox] {
+ height: 10em;
+ float: right;
+ margin: 0;
+ }
+ }
+
+ div.buttons {
+ padding-top: 1em;
+ clear: both;
+ }
+}
diff --git a/public/css/icinga/spinner.less b/public/css/icinga/spinner.less
new file mode 100644
index 0000000..c1cf93e
--- /dev/null
+++ b/public/css/icinga/spinner.less
@@ -0,0 +1,42 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.refresh-container-control > i:before {
+ margin: 0;
+}
+
+a.spinner.active > i,
+button.spinner.active > i,
+i.spinner.active {
+ &:before {
+ .animate(spin 2s infinite linear);
+
+ // icon-spin6
+ content: '\e874';
+ }
+
+ &.fa:before {
+ // fa spinner
+ content: '\f110';
+ }
+}
+
+div.spinner {
+ display: inline-block;
+ vertical-align: middle;
+
+ i {
+ visibility: hidden;
+
+ &.active {
+ visibility: visible;
+
+ &:before {
+ .animate(spin 2s infinite linear);
+ }
+ }
+
+ &:before {
+ margin: 0; // Disables wobbling
+ }
+ }
+}
diff --git a/public/css/icinga/tabs.less b/public/css/icinga/tabs.less
new file mode 100644
index 0000000..8103735
--- /dev/null
+++ b/public/css/icinga/tabs.less
@@ -0,0 +1,105 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+// Styles for tab navigation of containers
+
+.tabs {
+ background-color: @menu-bg-color;
+ letter-spacing: -0.417em;
+}
+
+.tabs > li {
+ display: inline-block;
+ letter-spacing: normal;
+}
+
+.tabs a {
+ padding: 0 1em;
+ line-height: 2.5em;
+
+ &:focus {
+ outline-offset: -0.5em;
+ }
+}
+
+.tabs > li {
+ &:not(:last-child) {
+ margin-right: 0.5em;
+ }
+
+ > a {
+ color: @menu-color;
+
+ &:hover {
+ text-decoration: none;
+ background: @tab-hover-bg-color;
+ }
+ }
+
+ &.active > a,
+ > a:focus {
+ background-color: @body-bg-color;
+ color: @text-color;
+ }
+}
+
+.tabs > .dropdown-nav-item > a,
+.tabs > li > .close-container-control,
+.tabs > li > .refresh-container-control {
+ text-align: center;
+ width: 3em;
+}
+
+.tabs > .dropdown-nav-item:hover > a,
+.tabs > .dropdown-nav-item > a:focus,
+.tabs > li > .close-container-control:focus,
+.tabs > li > .close-container-control:hover,
+.tabs > li > .refresh-container-control:focus,
+.tabs > li > .refresh-container-control:hover {
+ background-color: @body-bg-color;
+ color: @text-color;
+ text-decoration: none;
+}
+
+.tabs > .dropdown-nav-item > ul {
+ .box-shadow();
+ .rounded-corners(0 0 0.3em 0.3em);
+
+ background-color: @body-bg-color;
+ border: 1px solid;
+ border-color: @gray-light;
+ border-top: none;
+ margin-left: -1px;
+ min-width: 14em;
+ z-index: 10;
+}
+
+.tabs > .dropdown-nav-item > ul > li:hover > a {
+ background-color: @gray-lighter;
+ text-decoration: none;
+}
+
+// Dropdown tabs after the fourth title should be right-aligned
+.tabs > li:nth-child(n+5).dropdown-nav-item > ul {
+ transform: translate(~"calc(-100% + 3em)"); // -full width + tab width
+ margin-left: 1px;
+}
+
+// TODO(el): Rename display-on-hover and move it to main.less
+.display-on-hover {
+ font-size: @font-size-small;
+ left: -999em;
+ position: relative;
+}
+
+.dropdown-nav-item > ul > li > a:focus > .display-on-hover,
+.dropdown-nav-item > ul > li:hover > a > .display-on-hover {
+ position: static;
+}
+
+.tabs > li > .close-container-control {
+ display: none;
+}
+
+#layout.twocols .tabs > li > .close-container-control {
+ display: block;
+}
diff --git a/public/css/icinga/widgets.less b/public/css/icinga/widgets.less
new file mode 100644
index 0000000..654e7d5
--- /dev/null
+++ b/public/css/icinga/widgets.less
@@ -0,0 +1,643 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+#announcements > ul {
+ background-color: @body-bg-color;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ > li {
+ border-bottom: 1px solid @gray-lighter;
+ line-height: 1.5em;
+ padding: 0.5em 1em 0.5em 3em;
+
+ position: relative;
+
+ &:before {
+ color: @icinga-blue;
+ content: "\e811";
+ font-family: 'ifont';
+
+ position: absolute;
+ left: 1.25em;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ a {
+ color: @icinga-blue;
+ }
+
+ .message {
+ display: inline-block;
+ vertical-align: middle;
+ padding-right: 1.5em;
+ font-size: 7/6em;
+ }
+
+ p {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.acknowledge-announcement-control,
+.application-state-acknowledge-message-control {
+ background: none;
+ border: none;
+ display: block;
+ margin-top: -0.75em;
+
+ position: absolute;
+ right: .75em;
+ top: 50%;
+}
+
+.application-state-acknowledge-message-control .link-button {
+ color: #fff;
+
+ &:hover .icon-cancel {
+ color: @icinga-blue;
+ }
+}
+
+#application-state-summary > div {
+ background-color: @color-critical;
+ color: @text-color-on-icinga-blue;
+ line-height: 1.5em;
+ padding: 0.5em 1em 0.5em 3em;
+
+ position: relative;
+
+ &:before {
+ content: "\e84d";
+ font-family: 'ifont';
+
+ position: absolute;
+ text-align: center;
+ left: .4em;
+ padding: .5em;
+ width: 3em;
+ top: 0;
+ }
+
+ > section {
+ margin-left: .5em;
+ }
+
+ > form .icon-cancel:before {
+ color: @text-color-on-icinga-blue;
+ }
+}
+
+.dashboard-link {
+ .clearfix();
+ display: block;
+ max-width: 100%;
+ vertical-align: middle;
+ padding: 1em;
+ width: 36em;
+
+ &:hover {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding;
+ background-clip: padding-box;
+
+ -webkit-box-shadow: 0 0 0.5em 0 rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0 0 0.5em 0 rgba(0, 0, 0, 0.2);
+ box-shadow: 0 0 0.5em 0 rgba(0, 0, 0, 0.2);
+
+ background-color: @tr-hover-color;
+ text-decoration: none;
+ }
+}
+
+.dashboard.content > .container {
+ overflow-x: auto;
+}
+
+.link-meta {
+ display: table-cell;
+ vertical-align: middle;
+}
+
+.link-label {
+ font-weight: @font-weight-bold;
+}
+
+.link-description {
+ color: @text-color-light;
+}
+
+.link-icon {
+ display: table-cell;
+ padding-right: .5em;
+ vertical-align: middle;
+ text-align: center;
+
+ > i {
+ font-size: 3em;
+ opacity: 0.7;
+ line-height: 1.5;
+
+ &:before {
+ min-width: 1.25em;
+ }
+ }
+
+ > img {
+ width: 3em;
+ height: 3em;
+ margin-right: .6em;
+ }
+}
+
+table.historycolorgrid {
+ font-size: 1.5em;
+}
+
+table.historycolorgrid th {
+ width: 1em;
+ height: 1em;
+ margin: 0.5em;
+ font-size: 0.55em;
+ font-weight: bold;
+}
+
+table.historycolorgrid td {
+ width: 1em;
+ height: 1em;
+ margin: 1em;
+}
+
+table.historycolorgrid td:hover {
+ opacity: 0.5;
+}
+
+table.historycolorgrid td.weekday {
+ font-size: 0.55em;
+ font-weight: bold;
+ width: 2.5em;
+ opacity: 1.0;
+}
+
+table.historycolorgrid a, table.historycolorgrid span {
+ .rounded-corners(0.2em);
+ margin: 0;
+ text-decoration: none;
+ display: block;
+ width: 1.1em;
+ height: 1.1em;
+}
+
+table.historycolorgrid a:hover {
+ text-decoration: none;
+}
+
+table.multiselect tr[href] td {
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+}
+
+#main div.filter {
+ form.editor {
+ input[type=text], select {
+ width: 12em;
+ height: 2em;
+ line-height: 1;
+ }
+
+ ul.tree li.active {
+ background-color: @gray-lightest;
+ }
+
+ button {
+ padding: .5em;
+ border: none;
+ background: none;
+ color: @text-color;
+
+ &:hover, &:focus {
+ color: @icinga-blue;
+ }
+ }
+
+ .buttons {
+ margin-left: 25em;
+ padding: .25em 0;
+ }
+
+ .buttons input:not(:last-child) {
+ margin-right:.5em;
+ }
+ }
+}
+
+form.role-form {
+ &.icinga-form .control-label-group {
+ width: 20em;
+ }
+
+ .control-label-group em {
+ color: @text-color-light;
+ font-style: normal;
+ }
+
+ .control-label > * {
+ display: inline-block;
+ }
+
+ summary {
+ border-bottom: 1px solid @gray-light;
+ .user-select(none);
+ cursor: pointer;
+
+ font-weight: @font-weight-bold;
+ margin: 0.556em 0 0.333em;
+ font-size: 1.167em;
+
+ display: flex;
+ align-items: baseline;
+ .privilege-preview {
+ flex: 1 1 auto;
+ }
+
+ > :first-child {
+ display: inline-block;
+ width: 20em / 1.167em; // element label width / summary font-size
+ }
+
+ > :nth-last-child(1),
+ > :nth-last-child(2) {
+ font-size: .75em;
+ opacity: .6;
+ }
+
+ .privilege-preview .icon {
+ &.granted {
+ color: @color-granted;
+ }
+
+ &.refused {
+ color: @color-refused;
+ }
+
+ &.restricted {
+ color: @color-restricted;
+ }
+ }
+ }
+
+ .collapsible {
+ summary em {
+ font-size: .857em;
+ font-weight: normal;
+ color: @text-color-light;
+ }
+
+ h4 {
+ display: inline-block;
+ width: 20em;
+ margin-top: 1.5em;
+ padding-right: .5625em;
+ text-align: right;
+
+ & ~ i {
+ display: inline-block;
+ width: 2.625em;
+ margin-right: 1em;
+ text-align: center;
+
+ &.icon-ok {
+ color: @color-granted;
+ }
+
+ &.icon-cancel {
+ color: @color-refused;
+ }
+ }
+ }
+ }
+}
+
+ul.tree select:first-of-type { /* ?? */
+ margin-bottom: 0.3em;
+ margin-left: 2em;
+}
+
+ul.tree {
+ padding: 0;
+ margin: 0;
+ padding-top: .5em;
+}
+
+ul.tree ul {
+ padding-left: 1em;
+}
+
+ul.tree li {
+ margin: 0;
+ list-style-type: none;
+ position: relative;
+ padding: 0;
+}
+
+ul.tree li .handle {
+ background-image: url('../img/tree/tree-minus.gif');
+ background-repeat: no-repeat;
+ display: inline-block;
+ position: absolute;
+ width: 1.5em;
+ height: 2em;
+ left: 0em;
+ background-position: center center;
+ z-index: 1;
+ cursor: pointer;
+}
+
+ul.tree li.collapsed > .handle {
+ background-image: url('../img/tree/tree-plus.gif');
+}
+
+ul.tree li.collapsed > ul {
+ display: none;
+}
+
+ul.tree li::before, ul.tree li::after {
+ content: '';
+ position: absolute;
+ right: auto;
+ left: -0.2em;
+ border-color: @gray-light;
+ border-style: dotted;
+ border-width: 0;
+}
+
+/* This is the left vertical line */
+ul.tree li::before {
+ border-left-width: 1px;
+ top: -.5em;
+ width: 1em;
+ height: 2.5em;
+ bottom: 1em;
+}
+
+/* This is the horizontal dash in front of each item */
+ul.tree li::after {
+ border-top-width: 1px;
+ top: 1em;
+ width: 2em;
+ height: 1em;
+}
+
+/* Stop left vertical line at "mid-height" after last nodes (at each level) */
+ul.tree li:last-child::before {
+ height: 1.5em;
+}
+
+/* No border for the root element - there must be only ONE root */
+ul.tree > li::before, ul.tree > li::after {
+ display: none;
+}
+
+/* No connector before (each) root element */
+ul.tree > ul > li::before, ul.tree > ul > li::after {
+ border: 0;
+}
+
+ul.tree li a {
+ display: inline-block;
+ line-height: 2em;
+ padding: 0 .5em;
+ text-decoration: none;
+ color: @gray;
+ background-repeat: no-repeat;
+ background-position: 0.8em 0.4em;
+}
+
+ul.tree li a.error {
+ color: @color-critical-handled;
+}
+
+ul.tree li a:hover {
+ color: @text-color;
+ text-decoration: underline;
+}
+
+ul.tree li a.error:hover {
+ color: @color-critical;
+}
+
+/* charts should grow as much as possible but never beyond the current viewport's size */
+.svg-container-responsive {
+ padding: 1.5em;
+ height: 80vh;
+}
+
+.tipsy .tipsy-inner {
+ // overwrite tooltip max width, we need them to grow bigger
+ font-family: @font-family;
+ font-size: @font-size-small;
+ max-width: 300px;
+ text-align: left;
+ background-color: rgba(0,0,0,0.8);
+}
+
+.progress-label span {
+ font-size: 1.5em;
+ .animate(blink 1.4s infinite both);
+
+ &:nth-child(2) {
+ animation-delay: .2s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: .4s;
+ }
+}
+
+.flyover:not(.flyover-expanded) .flyover-content {
+ display: none;
+}
+
+.flyover {
+ position: relative;
+
+ .flyover-content {
+ background-color: @body-bg-color;
+ border: 1px solid;
+ border-color: @gray-lighter;
+ box-shadow: 0 0 .5em 0 rgba(0, 0, 0, 0.2);
+ position: absolute;
+ padding: @vertical-padding @horizontal-padding;
+ .rounded-corners();
+ }
+
+ &.flyover-arrow-top .flyover-content:before {
+ background: @body-bg-color;
+ border-left: 1px solid @gray-lighter;
+ border-top: 1px solid @gray-lighter;
+ content: "";
+ height: 1em;
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+ width: 1em;
+
+ position: absolute;
+ left: 6px;
+ top: -7px;
+ }
+
+ &.flyover-right .flyover-content {
+ left: auto;
+ right: 0;
+ }
+
+ &.flyover-arrow-top.flyover-right .flyover-content:before {
+ left: auto;
+ right: 6px;
+ }
+}
+
+.slice-state-ok {
+ stroke: @color-ok;
+ background: @color-ok;
+}
+
+.slice-state-warning-handled {
+ stroke: @color-warning-handled;
+ background: @color-warning-handled;
+}
+
+.slice-state-warning {
+ stroke: @color-warning;
+ background: @color-unreachable-handled;
+}
+
+.slice-state-critical-handled {
+ stroke: @color-critical-handled;
+ background: @color-critical-handled;
+}
+
+.slice-state-critical {
+ stroke: @color-critical;
+ background: @color-critical;
+}
+
+.slice-state-unknown-handled {
+ stroke: @color-unknown-handled;
+ background: @color-unknown-handled;
+}
+
+.slice-state-unknown {
+ stroke: @color-unknown;
+ background: @color-unknown;
+}
+
+.slice-state-unreachable-handled {
+ stroke: @color-unreachable-handled;
+ background: @color-unreachable-handled;
+}
+
+.slice-state-unreachable {
+ stroke: @color-unreachable;
+ background: @color-unreachable;
+}
+
+.slice-state-pending {
+ stroke: @color-pending;
+ background: @color-pending;
+}
+
+.slice-state-not-checked {
+ stroke: @gray-light;
+ background: @gray-light;
+}
+
+.donut {
+ width: 22em;
+ height: 22em;
+ min-width: 11.5em;
+ display: table;
+}
+
+.donut-graph {
+ width: 22em;
+ height: 22em;
+}
+
+.donut-label {
+ font-weight: bold;
+ fill: @text-color;
+}
+
+.donut-label {
+ margin-top: -12.5em;
+ text-align: center;
+}
+
+.donut-label-big {
+ color: @gray-light;
+ .fg-stateful();
+ font-size: 6em;
+ line-height: 0;
+ text-anchor: middle;
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.donut-label-small {
+ fill: @text-color;
+ font-size: 1.2em;
+ text-anchor: middle;
+ -moz-transform: translateY(0.35em);
+ -ms-transform: translateY(0.35em);
+ -webkit-transform: translateY(0.35em);
+ transform: translateY(0.35em);
+}
+
+.donut-container {
+ float: left;
+
+ &:not(:last-of-type) {
+ margin-right: 10em;
+ }
+}
+
+.dashboard .donut-container .donut-legend {
+ margin-left: auto;
+}
+
+.donut-legend {
+ width: 50%;
+ padding: 0;
+ margin-left: 18em;
+ list-style-type: none;
+
+ li {
+ vertical-align: middle;
+
+ &:not(:last-child) {
+ margin-bottom: .5em;
+ }
+
+ .badge {
+ font-weight: bold;
+ margin-right: .5em;
+ vertical-align: initial;
+ }
+ }
+}
+
+html.no-js .progress-label {
+ display: none;
+}
diff --git a/public/css/modes/light.less b/public/css/modes/light.less
new file mode 100644
index 0000000..7044007
--- /dev/null
+++ b/public/css/modes/light.less
@@ -0,0 +1,4 @@
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+@enable-color-preference: 999999px;
+@prefer-light-color-scheme: 0px;
diff --git a/public/css/modes/none.less b/public/css/modes/none.less
new file mode 100644
index 0000000..05c618c
--- /dev/null
+++ b/public/css/modes/none.less
@@ -0,0 +1,4 @@
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+@enable-color-preference: 999999px;
+@prefer-light-color-scheme: 999999px;
diff --git a/public/css/modes/system.less b/public/css/modes/system.less
new file mode 100644
index 0000000..07bd06a
--- /dev/null
+++ b/public/css/modes/system.less
@@ -0,0 +1,4 @@
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+@enable-color-preference: 0px;
+@prefer-light-color-scheme: 999999px;
diff --git a/public/css/pdf/pdfprint.less b/public/css/pdf/pdfprint.less
new file mode 100644
index 0000000..2c68d37
--- /dev/null
+++ b/public/css/pdf/pdfprint.less
@@ -0,0 +1,103 @@
+/*! Icinga Web 2 | (c) 2014 Icinga GmbH | GPLv2+ */
+
+// Ensure styling is light, exports use a white background
+
+@gray: #7F7F7F;
+@gray-semilight: #A9A9A9;
+@gray-light: #C9C9C9;
+@gray-lighter: #EEEEEE;
+@gray-lightest: #F7F7F7;
+@icinga-blue: #0095BF;
+@low-sat-blue: #dae3e6;
+@low-sat-blue-dark: #becbcf;
+@body-bg-color: #fff;
+@text-color: @black;
+@text-color-light: @gray;
+@tr-active-color: @body-bg-color;
+@tr-hover-color: @body-bg-color;
+
+// Page layout
+
+@page {
+ margin: 1cm;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ padding-top: 37px; // ~ logo height in the header
+}
+
+.content {
+ font-size: 9pt;
+}
+
+#header,
+#footer {
+ position: fixed;
+ left: 0;
+ right: 0;
+ color: #aaa;
+ font-size: 0.9em;
+}
+
+#header {
+ top: 0;
+ border-bottom: 0.1pt solid #aaa;
+
+ .title {
+ text-align: left;
+ }
+
+ img {
+ margin-bottom: 3px;
+ }
+}
+
+#footer {
+ bottom: 0;
+ padding-top: 2em;
+}
+
+.content table {
+ margin-bottom: 3em;
+}
+
+#header table,
+#footer table {
+ width: 100%;
+ border-collapse: collapse;
+ border: none;
+}
+
+#header td,
+#header th,
+#footer td,
+#footer th {
+ padding: 0;
+ width: 50%;
+}
+
+.page-number {
+ padding-top: 0.5em;
+ border-top: 0.1pt solid #aaa;
+ text-align: center;
+}
+
+.page-number:before {
+ content: "Page " counter(page);
+}
+
+hr {
+ page-break-after: always;
+ border: 0;
+}
+
+// General style
+.state-icons,
+.overview-performance-data,
+.controls,
+.dontprint, // Compat only, use dont-print instead
+.dont-print {
+ display: none !important;
+}
diff --git a/public/css/themes/Winter.less b/public/css/themes/Winter.less
new file mode 100644
index 0000000..547d109
--- /dev/null
+++ b/public/css/themes/Winter.less
@@ -0,0 +1,32 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+@icinga-blue: #2b95ff;
+@control-color: @icinga-blue;
+
+@menu-2ndlvl-highlight-bg-color: transparent;
+
+.letitsnow {
+ background-image: url('../img/winter/snow1.png'), url('../img/winter/snow2.png'), url('../img/winter/snow3.png');
+ animation: ~"snow" 10s linear infinite;
+}
+
+#header-logo {
+ background-image: url('../img/winter/logo_icinga_big_winter.png');
+}
+
+/* Snow, from http://codepen.io/NickyCDK/pen/AIonk */
+#login, #header-logo-container, #main > .container > .controls > .tabs {
+ .letitsnow()
+}
+
+@keyframes ~"snow" {
+ 0% {background-position: 0px 0px, 0px 0px, 0px 0px;}
+ 50% {background-position: 500px 500px, 100px 200px, -100px 150px;}
+ 100% {background-position: 500px 1000px, 200px 400px, -100px 300px;}
+}
+
+#menu ul.nav-level-1 > .nav-item {
+ &:focus, &:hover {
+ .letitsnow()
+ }
+}
diff --git a/public/css/themes/colorblind.less b/public/css/themes/colorblind.less
new file mode 100644
index 0000000..c6df585
--- /dev/null
+++ b/public/css/themes/colorblind.less
@@ -0,0 +1,29 @@
+/*! Icinga Web 2 | (c) 2019 Icinga Development Team | GPLv2+ */
+
+@color-ok: fade(#77E08E, 25%);
+@color-critical: #FE5566;
+@color-critical-handled: fade(@color-critical, 33%);
+@color-warning: #B0A029;
+@color-warning-handled: fade(@color-warning, 33%);
+@color-unknown: #7791E0;
+@color-unknown-handled: fade(@color-unknown, 50%);
+@color-unreachable: @color-unknown;
+@color-unreachable-handled: @color-unknown-handled;
+@color-pending: fade(#FFFFFF, 75%);
+
+/* Adapt font color to match handled / unhandled states */
+.badge,
+.state-badge {
+ font-weight: bold;
+ color: @text-color !important;
+
+ &.handled,
+ &.state-up,
+ &.state-ok {
+ color: fade(@text-color, 75%) !important;
+ }
+}
+
+.processinfo .process > div.backend-running {
+ color: @text-color;
+}
diff --git a/public/css/themes/high-contrast.less b/public/css/themes/high-contrast.less
new file mode 100644
index 0000000..b22ffd5
--- /dev/null
+++ b/public/css/themes/high-contrast.less
@@ -0,0 +1,250 @@
+/*! Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+@icinga-blue: #006D8C;
+
+// Gray colors
+@gray: #7F7F7F;
+@gray-semilight: #A9A9A9;
+@gray-light: #C9C9C9;
+@gray-lighter: #EEEEEE;
+@gray-lightest: #F7F7F7;
+@disabled-gray: #9a9a9a;
+
+// State colors
+@color-ok: #006400;
+@color-critical: #EE0000;
+@color-critical-handled: #EE0000;
+@color-warning: #8B5A00;
+@color-warning-handled: #8B5A00;
+@color-unknown: #800080;
+@color-unknown-handled: #800080;
+@color-unreachable: #800080;
+@color-unreachable-handled: #800080;
+@color-pending: #0000EE;
+
+// Icinga colors
+@low-sat-blue: #dae3e6;
+@low-sat-blue-dark: #c0cccd;
+
+// Background color for <body>
+@body-bg-color: @white;
+
+@text-color: #191919;
+@text-color-light: #555555;
+
+@menu-highlight-color: white;
+@menu-2ndlvl-color: white;
+@menu-2ndlvl-highlight-color: white;
+@menu-2ndlvl-active-hover-color: @text-color;
+
+#menu ul.nav-level-1 > .nav-item > a {
+ &:focus, &:hover {
+ text-decoration: underline;
+ }
+}
+
+#menu ul.nav-level-1 > .nav-item.active > a,
+#menu .nav-level-1 > .nav-item.active:not(.selected) > a:hover {
+ color: @text-color;
+ background-color: @white;
+}
+
+#menu .nav-level-2 > .nav-item.active {
+ background-color: @white;
+
+ a {
+ color: @text-color;
+ }
+
+ > a:focus, > a:hover {
+ opacity: 1;
+ }
+}
+
+#menu .nav-level-2 > .nav-item > a {
+ &:hover, &:focus {
+ text-decoration: underline;
+ }
+}
+
+#menu ul:not(.nav-level-2) > .selected > a {
+ color: @text-color;
+}
+
+#menu .active > a {
+ text-decoration: underline;
+}
+
+.badge:not(.handled),
+.state-badge:not(.handled) {
+ &.state-warning {
+ border: 1px solid @color-warning;
+ }
+
+ &.state-critical,
+ &.state-down {
+ border: 1px solid @color-critical;
+ }
+
+ &.state-unreachable {
+ border: 1px solid @color-unreachable;
+ }
+
+ &.state-unknown {
+ border: 1px solid @color-unknown;
+ }
+
+ &.state-ok,
+ &.state-up {
+ border: 1px solid @color-ok;
+ }
+}
+
+.badge.handled,
+.badge.state-ok,
+.state-badge.handled,
+.state-badge.state-ok {
+ background-color: @body-bg-color !important;
+ color: @text-color !important;
+
+ &.state-warning {
+ border: 1px solid @color-warning-handled;
+ }
+
+ &.state-critical,
+ &.state-down {
+ border: 1px solid @color-critical-handled;
+ }
+
+ &.state-unreachable {
+ border: 1px solid @color-unreachable-handled;
+ }
+
+ &.state-unknown {
+ border: 1px solid @color-unknown-handled;
+ }
+}
+
+.boxview a:focus {
+ color: @text-color;
+ text-decoration: underline;
+}
+
+.icinga-module.module-monitoring {
+ @timeline-notification-color: #1650CF;
+ @timeline-hard-state-color: #A24600;
+ @timeline-comment-color: #346964;
+ @timeline-ack-color: #855D18;
+ @timeline-downtime-start-color: #515151;
+ @timeline-downtime-end-color: #5e5e2f;
+
+ // Unfortunately it does not suffice to only override the timeline colors here, because our less compiler seems to
+ // have the related style block in module.less already evaluated
+
+ .timeline-notification {
+ background-color: @timeline-notification-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-notification-color, 20%);
+ }
+ }
+
+ .timeline-hard-state {
+ background-color: @timeline-hard-state-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-hard-state-color, 20%);
+ }
+ }
+
+ .timeline-comment {
+ background-color: @timeline-comment-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-comment-color, 20%);
+ }
+ }
+
+ .timeline-ack {
+ background-color: @timeline-ack-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-ack-color, 20%);
+ }
+ }
+
+ .timeline-downtime-start {
+ background-color: @timeline-downtime-start-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-downtime-start-color, 20%);
+ }
+ }
+
+ .timeline-downtime-end {
+ background-color: @timeline-downtime-end-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-downtime-end-color, 20%);
+ }
+ }
+}
+
+.icinga-controls {
+ input:not([type="checkbox"]):not([type="radio"]),
+ .toggle-switch .toggle-slider:before,
+ .toggle-switch > .toggle-slider,
+ select,
+ textarea {
+ border: 1px solid @icinga-blue;
+ }
+
+ input[type="checkbox"]:not(:checked) + .toggle-switch .toggle-slider:before {
+ height: 1.166666667em;
+ width: 1.166666667em;
+ margin: 1px;
+ }
+}
+
+.search-suggestions {
+ input:not([type="checkbox"]):not([type="radio"]),
+ .toggle-switch .toggle-slider:before,
+ .toggle-switch > .toggle-slider,
+ select,
+ textarea {
+ border: none;
+ }
+}
+
+.icinga-module.module-icingadb .list-item.overdue {
+ background: none;
+
+ header > *:not(time),
+ .caption {
+ opacity: 1;
+ }
+}
+
+.controls input.search,
+input.search {
+ background-image: url(../img/icons/search.png);
+}
+
+.search-bar,
+.button-link,
+.view-mode-switcher > label {
+ border: 1px solid @icinga-blue;
+}
+
+// compensate for 1px border
+.filter-input-area {
+ padding: 1/12em !important;
+}
+
+.view-mode-switcher > label {
+ padding: (10/16)*.25em (10/16)*.5em !important;
+
+ &:not(:first-of-type) {
+ border-left: none;
+ }
+}
diff --git a/public/css/vendor/normalize.css b/public/css/vendor/normalize.css
new file mode 100644
index 0000000..458eea1
--- /dev/null
+++ b/public/css/vendor/normalize.css
@@ -0,0 +1,427 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; /* 2 */
+ box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
diff --git a/public/error_norewrite.html b/public/error_norewrite.html
new file mode 100644
index 0000000..d85a002
--- /dev/null
+++ b/public/error_norewrite.html
@@ -0,0 +1 @@
+<h1>The rewrite module is not enabled</h1>
diff --git a/public/error_unavailable.html b/public/error_unavailable.html
new file mode 100644
index 0000000..1c2df0e
--- /dev/null
+++ b/public/error_unavailable.html
@@ -0,0 +1,7 @@
+<h1>Backend unavailable</h1>
+<p>
+ It seems that the PHP FPM service is not running. Make sure to start PHP FPM service in order to access Icinga Web 2.
+ If you upgraded Icinga Web 2 recently, make sure to read the
+ <a href="https://icinga.com/docs/icingaweb2/latest/doc/02-Installation/">docs regarding PHP FPM</a>,
+ also locally available under <code>/usr/share/icingaweb2/doc/02-Installation.md</code>.
+</p>
diff --git a/public/font/ifont.eot b/public/font/ifont.eot
new file mode 100644
index 0000000..3a9092f
--- /dev/null
+++ b/public/font/ifont.eot
Binary files differ
diff --git a/public/font/ifont.svg b/public/font/ifont.svg
new file mode 100644
index 0000000..2747b47
--- /dev/null
+++ b/public/font/ifont.svg
@@ -0,0 +1,286 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata>
+<defs>
+<font id="ifont" horiz-adv-x="1000" >
+<font-face font-family="ifont" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
+<missing-glyph horiz-adv-x="1000" />
+<glyph glyph-name="dashboard" unicode="&#xe800;" d="M286 154v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m0 285v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m357-285v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m-357 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m357-286v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m357-285v-108q0-22-16-37t-38-16h-178q-22 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m-357 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m357-286v-107q0-22-16-38t-38-15h-178q-22 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m0 286v-107q0-22-16-38t-38-16h-178q-22 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="user" unicode="&#xe801;" d="M714 69q0-60-35-104t-84-44h-476q-49 0-84 44t-35 104q0 48 5 90t17 85 33 73 52 50 76 19q73-72 174-72t175 72q42 0 75-19t52-50 33-73 18-85 4-90z m-143 495q0-88-62-151t-152-63-151 63-63 151 63 152 151 63 152-63 62-152z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="users" unicode="&#xe802;" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 24 12q34 0 62-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="ok" unicode="&#xe803;" d="M352-10l-334 333 158 160 176-174 400 401 159-160z" horiz-adv-x="928" />
+
+<glyph glyph-name="cancel" unicode="&#xe804;" d="M799 116l-156-157-234 235-235-235-156 157 234 234-234 234 156 157 235-235 234 235 156-157-234-234z" horiz-adv-x="817" />
+
+<glyph glyph-name="plus" unicode="&#xe805;" d="M911 462l0-223-335 0 0-336-223 0 0 336-335 0 0 223 335 0 0 335 223 0 0-335 335 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="minus" unicode="&#xe806;" d="M18 239l0 223 893 0 0-223-893 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="folder-empty" unicode="&#xe807;" d="M464 685l447 0 0-669q0-47-33-80t-79-33l-669 0q-46 0-79 33t-33 80l0 781 446 0 0-112z m-334 0l0-223 669 0 0 112-446 0 0 111-223 0z m669-669l0 335-669 0 0-335 669 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="download" unicode="&#xe808;" d="M714 100q0 15-10 25t-25 11-25-11-11-25 11-25 25-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-39l-250-250q-10-11-25-11t-25 11l-250 250q-17 16-8 39 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="upload" unicode="&#xe809;" d="M714 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="git" unicode="&#xe80a;" d="M332 5q0 56-92 56-88 0-88-58 0-57 96-57 84 0 84 59z m-33 422q0 34-17 56t-49 23q-69 0-69-81 0-75 69-75 66 0 66 77z m150 180v-112q-20-7-44-13 9-24 9-47 0-70-41-120t-110-63q-22-5-33-15t-11-33q0-17 13-28t32-18 44-12 48-15 44-21 32-35 13-55q0-170-203-170-38 0-72 7t-65 23-49 46-18 71q0 92 102 125v3q-38 22-38 70 0 61 35 76v3q-40 13-66 60t-27 93q0 77 53 129t131 51q54 0 100-26 54 0 121 26z m178-491h-124q2 25 2 74v340q0 53-2 72h124q-3-19-3-69v-343q0-49 3-74z m335 124v-110q-40-22-97-22-35 0-60 12t-39 27-22 44-10 51-2 58v196h1v2q-4 0-11 0t-10 1q-12 0-33-3v106h54v42q0 30-4 50h127q-3-23-3-92h95v-106q-8 0-24 1t-24 1h-47v-204q0-73 48-73 34 0 61 19z m-321 528q0-32-22-57t-54-24q-32 0-54 24t-23 57q0 33 22 57t55 25q33 0 54-25t22-57z" horiz-adv-x="1000" />
+
+<glyph glyph-name="cubes" unicode="&#xe80b;" d="M357-61l214 107v176l-214-92v-191z m-36 254l226 96-226 97-225-97z m608-254l214 107v176l-214-92v-191z m-36 254l225 96-225 97-226-97z m-250 163l214 92v149l-214-92v-149z m-36 212l246 105-246 106-246-106z m607-289v-233q0-20-10-37t-29-26l-250-125q-14-8-32-8t-32 8l-250 125q-2 1-4 2-1-1-4-2l-250-125q-14-8-32-8t-31 8l-250 125q-19 9-29 26t-11 37v233q0 21 12 39t32 26l242 104v223q0 22 12 40t31 26l250 107q13 6 28 6t28-6l250-107q20-9 32-26t12-40v-223l242-104q20-8 32-26t11-39z" horiz-adv-x="1285.7" />
+
+<glyph glyph-name="database" unicode="&#xe80c;" d="M429 421q132 0 247 24t181 71v-95q0-38-57-71t-157-52-214-19-215 19-156 52-58 71v95q66-47 181-71t248-24z m0-428q132 0 247 24t181 71v-95q0-39-57-72t-157-52-214-19-215 19-156 52-58 72v95q66-47 181-71t248-24z m0 214q132 0 247 24t181 71v-95q0-38-57-71t-157-52-214-20-215 20-156 52-58 71v95q66-47 181-71t248-24z m0 643q116 0 214-19t157-52 57-72v-71q0-39-57-72t-157-52-214-19-215 19-156 52-58 72v71q0 39 58 72t156 52 215 19z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="gauge" unicode="&#xe80d;" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
+
+<glyph glyph-name="sitemap" unicode="&#xe80e;" d="M1000 154v-179q0-22-16-38t-38-16h-178q-22 0-38 16t-16 38v179q0 22 16 38t38 15h53v107h-285v-107h53q23 0 38-15t16-38v-179q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v179q0 22 16 38t38 15h53v107h-285v-107h53q23 0 38-15t16-38v-179q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v179q0 22 16 38t38 15h53v107q0 29 21 51t51 21h285v107h-53q-23 0-38 16t-16 37v179q0 22 16 38t38 16h178q23 0 38-16t16-38v-179q0-22-16-37t-38-16h-53v-107h285q29 0 51-21t21-51v-107h53q23 0 38-15t16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="sort-name-up" unicode="&#xe80f;" d="M665 622h98l-40 122-6 26q-2 9-2 11h-2l-1-11q0 0-2-10t-5-16z m-254-576q0-6-6-13l-178-178q-5-5-13-5-6 0-12 5l-179 179q-8 9-4 19 4 11 17 11h107v768q0 8 5 13t13 5h107q8 0 13-5t5-13v-768h107q8 0 13-5t5-13z m466-66v-130h-326v50l206 295q7 11 12 16l6 5v1q-1 0-3 0t-5 0q-6-2-16-2h-130v-64h-67v128h317v-50l-206-296q-4-4-12-14l-6-7v-1l8 1q5 2 16 2h139v66h67z m50 501v-60h-161v60h42l-26 80h-136l-26-80h42v-60h-160v60h39l128 369h91l128-369h39z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="sort-name-down" unicode="&#xe810;" d="M665 51h98l-40 122-6 26q-2 9-2 11h-2l-1-11q0-1-2-10t-5-16z m-254-5q0-6-6-13l-178-178q-5-5-13-5-6 0-12 5l-179 179q-8 9-4 19 4 11 17 11h107v768q0 8 5 13t13 5h107q8 0 13-5t5-13v-768h107q8 0 13-5t5-13z m516-137v-59h-161v59h42l-26 80h-136l-26-80h42v-59h-160v59h39l128 370h91l128-370h39z m-50 643v-131h-326v51l206 295q7 10 12 15l6 5v2q-1 0-3-1t-5 0q-6-2-16-2h-130v-64h-67v128h317v-50l-206-295q-4-5-12-15l-6-5v-2l8 2q5 0 16 0h139v67h67z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="megaphone" unicode="&#xe811;" d="M929 493q29 0 50-21t21-51-21-50-50-21v-214q0-29-22-50t-50-22q-233 194-453 212-32-10-51-36t-17-57 22-51q-11-19-13-37t4-32 19-31 26-28 35-28q-17-32-63-46t-94-7-73 31q-4 13-17 49t-18 53-12 50-9 56 2 55 12 62h-68q-36 0-63 26t-26 63v107q0 37 26 63t63 26h268q243 0 500 215 29 0 50-22t22-50v-214z m-72-337v532q-220-168-428-191v-151q210-23 428-190z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bug" unicode="&#xe812;" d="M911 314q0-14-11-25t-25-10h-125q0-96-37-162l116-117q10-11 10-25t-10-25q-10-11-25-11t-25 11l-111 110q-3-3-8-7t-24-16-36-21-46-16-54-7v500h-71v-500q-29 0-57 7t-49 19-36 22-25 18l-8 8-102-116q-11-12-27-12-13 0-24 9-11 10-11 25t8 26l113 127q-32 63-32 153h-125q-15 0-25 10t-11 25 11 25 25 11h125v164l-97 97q-11 10-11 25t11 25 25 10 25-10l97-97h471l96 97q11 10 25 10t26-10 10-25-10-25l-97-97v-164h125q15 0 25-11t11-25z m-268 322h-357q0 74 52 126t126 52 127-52 52-126z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="tasks" unicode="&#xe813;" d="M571 64h358v72h-358v-72z m-214 286h572v71h-572v-71z m357 286h215v71h-215v-71z m286-465v-142q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v142q0 15 11 26t25 10h928q15 0 25-10t11-26z m0 286v-143q0-14-11-25t-25-10h-928q-15 0-25 10t-11 25v143q0 15 11 25t25 11h928q15 0 25-11t11-25z m0 286v-143q0-14-11-25t-25-11h-928q-15 0-25 11t-11 25v143q0 14 11 25t25 11h928q15 0 25-11t11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="filter" unicode="&#xe814;" d="M783 685q9-22-8-39l-275-275v-414q0-23-22-33-7-3-14-3-15 0-25 11l-143 143q-10 11-10 25v271l-275 275q-18 17-8 39 9 22 33 22h714q23 0 33-22z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="off" unicode="&#xe815;" d="M857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 23-111t61-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-23 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="book" unicode="&#xe816;" d="M915 583q22-31 10-72l-154-505q-10-36-42-60t-69-25h-515q-43 0-83 30t-55 74q-14 37-1 71 0 2 1 15t3 20q0 5-2 12t-2 11q1 6 5 12t9 13 9 13q13 21 25 51t17 51q2 6 0 17t0 16q2 6 9 15t10 13q12 20 23 51t14 51q1 5-1 17t0 16q2 7 12 17t13 13q10 14 23 47t16 54q0 4-2 14t-1 15q1 4 5 10t10 13 10 11q4 7 9 17t8 20 9 20 11 18 15 13 20 6 26-3l0-1q21 5 28 5h425q41 0 64-32t10-72l-153-506q-20-66-40-85t-72-20h-485q-15 0-21-8-6-9-1-24 14-39 81-39h515q16 0 31 9t20 23l167 550q4 13 3 32 21-8 33-24z m-594-1q-2-7 1-12t11-6h339q8 0 15 6t9 12l12 36q2 7-1 12t-12 6h-339q-7 0-14-6t-9-12z m-46-143q-3-7 1-12t11-6h339q7 0 14 6t10 12l11 36q3 7-1 13t-11 5h-339q-7 0-14-5t-10-13z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="paste" unicode="&#xe817;" d="M429-79h500v358h-233q-22 0-37 15t-16 38v232h-214v-643z m142 804v36q0 7-5 12t-12 6h-393q-7 0-13-6t-5-12v-36q0-7 5-13t13-5h393q7 0 12 5t5 13z m143-375h167l-167 167v-167z m286-71v-375q0-23-16-38t-38-16h-535q-23 0-38 16t-16 38v89h-303q-23 0-38 16t-16 37v750q0 23 16 38t38 16h607q22 0 38-16t15-38v-183q12-7 20-15l228-228q16-15 27-42t11-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="scissors" unicode="&#xe818;" d="M536 350q14 0 25-11t10-25-10-25-25-10-25 10-11 25 11 25 25 11z m167-36l283-222q16-11 14-31-3-20-19-28l-72-36q-7-4-16-4-10 0-17 4l-385 216-62-36q-4-3-7-3 8-28 6-54-4-43-31-83t-74-69q-74-47-154-47-76 0-124 44-51 47-44 116 4 42 31 82t73 69q74 47 155 47 46 0 84-18 5 8 13 13l68 40-68 41q-8 5-13 12-38-17-84-17-81 0-155 47-46 30-73 69t-31 82q-3 33 8 63t36 52q47 44 124 44 80 0 154-47 46-29 74-68t31-83q2-27-6-54 3-1 7-3l62-37 385 216q7 5 17 5 9 0 16-4l72-36q16-9 19-28 2-20-14-32z m-380 145q26 24 12 61t-59 65q-52 33-107 33-42 0-63-20-26-24-12-60t59-66q51-33 107-33 41 0 63 20z m-47-415q45 28 59 65t-12 60q-22 20-63 20-56 0-107-33-45-28-59-65t12-60q21-20 63-20 55 0 107 33z m99 342l54-33v7q0 20 18 31l8 4-44 26-15-14q-1-2-5-6t-7-7q-1-1-2-2t-2-1z m125-125l54-18 410 321-71 36-429-240v-64l-89-53 5-5q1-1 4-3 2-2 6-7t6-6l15-15z m393-232l71 35-290 228-99-77q-1-2-7-4z" horiz-adv-x="1000" />
+
+<glyph glyph-name="globe" unicode="&#xe819;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m153-291q-2-1-6-5t-7-6q1 0 2 3t3 6 2 4q3 4 12 8 8 4 29 7 19 5 29-6-1 1 5 7t8 7q2 1 8 3t9 4l1 12q-7-1-10 4t-3 12q0-2-4-5 0 4-2 5t-7-1-5-1q-5 2-8 5t-5 9-2 8q-1 3-5 6t-5 6q-1 1-2 3t-1 4-3 3-3 1-4-3-4-5-2-3q-2 1-4 1t-2-1-3-1-3-2q-1-2-4-2t-5-1q8 3-1 6-5 2-9 2 6 2 5 6t-5 8h3q-1 2-5 5t-10 5-7 3q-5 3-19 5t-18 1q-3-4-3-6t2-8 2-7q1-3-3-7t-3-7q0-4 7-9t6-12q-2-4-9-9t-9-6q-3-5-1-11t6-9q1-1 1-2t-2-3-3-2-4-2l-1-1q-7-3-12 3t-7 15q-4 14-9 17-13 4-16-1-3 7-23 15-14 5-33 2 4 0 0 8-4 9-10 7 1 3 2 10t0 7q2 8 7 13 1 1 4 5t5 7 1 4q19-3 28 6 2 3 6 9t6 10q5 3 8 3t8-3 8-3q8-1 8 6t-4 11q7 0 2 10-2 4-5 5-6 2-15-3-4-2 2-4-1 0-6-6t-9-10-9 3q0 0-3 7t-5 8q-5 0-9-9 1 5-6 9t-14 4q11 7-4 15-4 3-12 3t-11-2q-2-4-3-7t3-4 6-3 6-2 5-2q8-6 5-8-1 0-5-2t-6-2-4-2q-1-3 0-8t-1-8q-3 3-5 10t-4 9q4-5-14-3l-5 0q-3 0-9-1t-12-1-7 5q-3 4 0 11 0 2 2 1-2 2-6 5t-6 5q-25-8-52-23 3 0 6 1 3 1 8 4t5 3q19 7 24 4l3 2q7-9 11-14-4 3-17 1-11-3-12-7 4-6 2-10-2 2-6 6t-8 6-8 3q-9 0-13-1-81-45-131-124 4-4 7-4 2-1 3-5t1-6 6 1q5-4 2-10 1 0 25-15 10-10 11-12 2-6-5-10-1 1-5 5t-5 2q-2-3 0-10t6-7q-4 0-5-9t-2-20 0-13l1-1q-2-6 3-19t12-11q-7-1 11-24 3-4 4-5 2-1 7-4t9-6 5-5q2-3 6-13t8-13q-2-3 5-11t6-13q-1 0-2-1t-1 0q2-4 9-8t8-7q1-2 1-6t2-6 4-1q2 11-13 35-8 13-9 16-2 2-4 8t-2 8q1 0 3 0t5-2 4-3 1-1q-1-4 1-10t7-10 10-11 6-7q4-4 8-11t0-8q5 0 11-5t10-11q3-5 4-15t3-13q1-4 5-8t7-5l9-5t7-3q3-2 10-6t12-7q6-2 9-2t8 1 8 2q8 1 16-8t12-12q20-10 30-6-1 0 1-4t4-9 5-8 3-5q3-3 10-8t10-8q4 2 4 5-1-5 4-11t10-6q8 2 8 18-17-8-27 10 0 0-2 3t-2 5-1 4 0 5 2 1q5 0 6 2t-1 7-2 8q-1 4-6 11t-7 8q-3-5-9-4t-9 5q0-1-1-3t-1-4q-7 0-8 0 1 2 1 10t2 13q1 2 3 6t5 9 2 7-3 5-9 1q-11 0-15-11-1-2-2-6t-2-6-5-4q-4-2-14-1t-13 3q-8 4-13 16t-5 20q0 6 1 15t2 14-3 14q2 1 5 5t5 6q2 1 3 1t3 0 2 1 1 3q0 1-2 2-1 1-2 1 4-1 16 1t15-1q9-6 12 1 0 1-1 6t0 7q3-15 16-5 2-1 9-3t9-2q2-1 4-3t3-3 3 0 5 4q5-8 7-13 6-23 10-25 4-2 6-1t3 5 0 8-1 7l-1 5v10l0 4q-8 2-10 7t0 10 9 10q0 1 4 2t9 4 7 4q12 11 8 20 4 0 6 5 0 0-2 2t-5 2-2 2q5 2 1 8 3 2 4 7t4 5q5-6 12-1 5 5 1 9 2 4 11 6t10 5q4-1 5 1t0 7 2 7q2 2 9 5t7 2l9 7q2 2 0 2 10-1 18 6 5 6-4 11 2 4-1 5t-9 4q2 0 7 0t5 1q9 5-3 9-10 2-24-7z m-91-490q115 21 195 106-1 2-7 2t-7 2q-10 4-13 5 1 4-1 7t-5 5-7 5-6 4q-1 1-4 3t-4 3-4 2-5 2-5-1l-2-1q-2 0-3-1t-3-2-2-1 0-2q-12 10-20 13-3 0-6 3t-6 4-6 0-6-3q-3-3-4-9t-1-7q-4 3 0 10t1 10q-1 3-6 2t-6-2-7-5-5-3-4-3-5-5q-2-2-4-6t-2-6q-1 2-7 3t-5 3q1-5 2-19t3-22q4-17-7-26-15-14-16-23-2-12 7-14 0-4-5-12t-4-12q0-3 2-9z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="cloud" unicode="&#xe81a;" d="M1071 207q0-89-62-151t-152-63h-607q-103 0-177 73t-73 177q0 74 40 135t104 91q-1 16-1 24 0 118 84 202t202 84q88 0 159-49t105-129q39 35 93 35 59 0 101-42t42-101q0-42-23-77 72-17 119-75t46-134z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="flash" unicode="&#xe81b;" d="M494 534q10-11 4-24l-302-646q-7-14-23-14-2 0-8 1-9 3-14 11t-3 16l110 451-226-56q-2-1-7-1-10 0-17 7-10 8-7 21l112 461q2 8 9 13t15 5h183q11 0 18-7t7-17q0-4-2-10l-96-258 221 54q5 2 7 2 11 0 19-9z" horiz-adv-x="500" />
+
+<glyph glyph-name="barchart" unicode="&#xe81c;" d="M143 46v-107q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v107q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 72v-179q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v179q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 143v-322q0-8-5-13t-12-5h-108q-7 0-12 5t-5 13v322q0 8 5 13t12 5h108q7 0 12-5t5-13z m215 214v-536q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v536q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 286v-822q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v822q0 8 5 13t13 5h107q8 0 13-5t5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="down-dir" unicode="&#xe81d;" d="M571 457q0-14-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 11-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="up-dir" unicode="&#xe81e;" d="M571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="left-dir" unicode="&#xe81f;" d="M357 600v-500q0-14-10-25t-26-11-25 11l-250 250q-10 11-10 25t10 25l250 250q11 11 25 11t26-11 10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="right-dir" unicode="&#xe820;" d="M321 350q0-14-10-25l-250-250q-11-11-25-11t-25 11-11 25v500q0 15 11 25t25 11 25-11l250-250q10-10 10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="down-open" unicode="&#xe821;" d="M939 399l-414-413q-10-11-25-11t-25 11l-414 413q-11 11-11 26t11 25l93 92q10 11 25 11t25-11l296-296 296 296q11 11 25 11t26-11l92-92q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+
+<glyph glyph-name="right-open" unicode="&#xe822;" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="up-open" unicode="&#xe823;" d="M939 107l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+
+<glyph glyph-name="left-open" unicode="&#xe824;" d="M654 682l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="up-big" unicode="&#xe825;" d="M899 308q0-28-21-50l-41-42q-22-21-51-21-30 0-50 21l-165 164v-393q0-29-20-47t-51-19h-71q-30 0-51 19t-21 47v393l-164-164q-20-21-50-21t-50 21l-42 42q-21 21-21 50 0 30 21 51l363 363q20 21 50 21 30 0 51-21l363-363q21-22 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="right-big" unicode="&#xe826;" d="M821 314q0-30-20-50l-363-364q-22-20-51-20-29 0-50 20l-42 42q-22 21-22 51t22 51l163 163h-393q-29 0-47 21t-18 51v71q0 30 18 51t47 20h393l-163 165q-22 20-22 50t22 50l42 42q21 21 50 21 29 0 51-21l363-363q20-20 20-51z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="left-big" unicode="&#xe827;" d="M857 350v-71q0-30-18-51t-47-21h-393l164-164q21-20 21-50t-21-50l-42-43q-21-20-51-20-29 0-50 20l-364 364q-20 21-20 50 0 29 20 51l364 363q21 21 50 21 29 0 51-21l42-41q21-22 21-51t-21-51l-164-164h393q29 0 47-20t18-51z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="down-big" unicode="&#xe828;" d="M899 386q0-30-21-50l-363-364q-22-21-51-21-29 0-50 21l-363 364q-21 20-21 50 0 29 21 51l41 41q22 21 51 21 29 0 50-21l164-164v393q0 29 21 50t51 22h71q29 0 50-22t21-50v-393l165 164q20 21 50 21 29 0 51-21l41-41q21-22 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="resize-full-alt" unicode="&#xe829;" d="M716 548l-198-198 198-198 80 80q17 18 39 8 22-9 22-33v-250q0-14-10-25t-26-11h-250q-23 0-32 23-10 21 7 38l81 81-198 198-198-198 80-81q17-17 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l80-80 198 198-198 198-80-80q-11-11-25-11-7 0-14 3-22 9-22 33v250q0 14 11 25t25 11h250q23 0 33-23 9-21-8-38l-80-81 198-198 198 198-81 81q-17 17-7 38 9 23 32 23h250q15 0 26-11t10-25v-250q0-24-22-33-7-3-14-3-14 0-25 11z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="resize-full" unicode="&#xe82a;" d="M421 261q0-7-5-13l-185-185 80-81q10-10 10-25t-10-25-25-11h-250q-15 0-25 11t-11 25v250q0 15 11 25t25 11 25-11l80-80 186 185q5 6 12 6t13-6l64-63q5-6 5-13z m436 482v-250q0-15-10-25t-26-11-25 11l-80 80-185-185q-6-6-13-6t-13 6l-64 64q-5 5-5 12t5 13l186 185-81 81q-10 10-10 25t10 25 25 11h250q15 0 26-11t10-25z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="resize-small" unicode="&#xe82b;" d="M429 314v-250q0-14-11-25t-25-10-25 10l-81 81-185-186q-5-5-13-5t-12 5l-64 64q-6 6-6 13t6 13l185 185-80 80q-11 11-11 25t11 25 25 11h250q14 0 25-11t11-25z m421 375q0-7-6-12l-185-186 80-80q11-11 11-25t-11-25-25-11h-250q-14 0-25 11t-10 25v250q0 14 10 25t25 10 25-10l81-80 185 185q6 5 13 5t13-5l63-64q6-5 6-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="move" unicode="&#xe82c;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-215v-215h72q14 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-11 10-11 25t11 25 25 10h72v215h-215v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h215v215h-72q-14 0-25 10t-11 25 11 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26t-11-25-25-10h-72v-215h215v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="resize-horizontal" unicode="&#xe82d;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-572v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h572v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="resize-vertical" unicode="&#xe82e;" d="M393 671q0-14-11-25t-25-10h-71v-572h71q15 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-10 10-10 25t10 25 25 10h72v572h-72q-14 0-25 10t-10 25 10 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26z" horiz-adv-x="428.6" />
+
+<glyph glyph-name="zoom-in" unicode="&#xe82f;" d="M571 404v-36q0-7-5-13t-12-5h-125v-125q0-7-6-13t-12-5h-36q-7 0-13 5t-5 13v125h-125q-7 0-12 5t-6 13v36q0 7 6 12t12 5h125v125q0 8 5 13t13 5h36q7 0 12-5t6-13v-125h125q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="block" unicode="&#xe830;" d="M732 352q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="zoom-out" unicode="&#xe831;" d="M571 404v-36q0-7-5-13t-12-5h-322q-7 0-12 5t-6 13v36q0 7 6 12t12 5h322q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="lightbulb" unicode="&#xe832;" d="M411 529q0-8-6-13t-12-5-13 5-5 13q0 25-30 39t-59 14q-7 0-13 5t-5 13 5 13 13 5q28 0 55-9t49-30 21-50z m89 0q0 40-19 74t-50 57-69 35-76 12-76-12-69-35-50-57-20-74q0-57 38-101 6-6 17-18t17-19q72-85 79-166h127q8 81 79 166 6 6 17 19t17 18q38 44 38 101z m71 0q0-87-57-150-25-27-42-48t-33-54-19-60q26-15 26-46 0-20-13-35 13-15 13-36 0-29-25-45 8-13 8-26 0-26-18-40t-43-14q-11-25-34-39t-48-15-49 15-33 39q-26 0-44 14t-17 40q0 13 7 26-25 16-25 45 0 21 14 36-14 15-14 35 0 31 26 46-2 28-19 60t-33 54-41 48q-58 63-58 150 0 55 25 103t65 79 92 49 104 19 104-19 91-49 66-79 24-103z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="clock" unicode="&#xe833;" d="M500 546v-250q0-7-5-12t-13-5h-178q-8 0-13 5t-5 12v36q0 8 5 13t13 5h125v196q0 8 5 13t12 5h36q8 0 13-5t5-13z m232-196q0 83-41 152t-110 111-152 41-153-41-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152z m125 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="volume-up" unicode="&#xe834;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z m214-304q0-42-24-79t-63-52q-5-3-14-3-14 0-25 10t-10 26q0 12 6 20t17 14 19 12 16 21 6 31-6 32-16 20-19 13-17 13-6 20q0 15 10 26t25 10q9 0 14-3 39-15 63-52t24-79z m143 0q0-85-48-158t-125-105q-7-3-14-3-15 0-26 11t-10 25q0 22 21 33 32 16 43 25 41 30 64 75t23 97-23 97-64 75q-11 9-43 25-21 11-21 33 0 14 10 25t25 11q8 0 15-3 78-33 125-105t48-158z m143 0q0-128-71-236t-189-158q-7-3-14-3-15 0-25 11t-11 25q0 20 22 33 4 2 12 6t13 6q25 14 46 28 68 51 107 127t38 161-38 161-107 127q-21 15-46 28-4 3-13 6t-12 6q-22 13-22 33 0 15 11 25t25 11q7 0 14-3 118-51 189-158t71-236z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="volume-down" unicode="&#xe835;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z m214-304q0-42-24-79t-63-52q-5-3-14-3-14 0-25 10t-10 26q0 12 6 20t17 14 19 12 16 21 6 31-6 32-16 20-19 13-17 13-6 20q0 15 10 26t25 10q9 0 14-3 39-15 63-52t24-79z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="volume-off" unicode="&#xe836;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z" horiz-adv-x="428.6" />
+
+<glyph glyph-name="mute" unicode="&#xe837;" d="M151 323l-56-57q-24 58-24 120v71q0 15 11 25t25 11 25-11 11-25v-71q0-30 8-63z m622 336l-202-202v-71q0-74-52-126t-126-53q-31 0-61 11l-53-54q54-28 114-28 103 0 177 73t73 177v71q0 15 11 25t25 11 25-11 10-25v-71q0-124-82-215t-203-104v-74h142q15 0 26-11t10-25-10-25-26-11h-357q-14 0-25 11t-10 25 10 25 25 11h143v74q-70 7-131 45l-142-142q-5-6-13-6t-12 6l-46 46q-6 5-6 13t6 12l689 689q5 6 12 6t13-6l46-46q6-5 6-13t-6-12z m-212 73l-347-346v285q0 74 53 127t126 52q57 0 103-33t65-85z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="mic" unicode="&#xe838;" d="M643 457v-71q0-124-82-215t-204-104v-74h143q15 0 25-11t11-25-11-25-25-11h-357q-15 0-25 11t-11 25 11 25 25 11h143v74q-121 13-204 104t-82 215v71q0 15 11 25t25 11 25-11 10-25v-71q0-103 74-177t176-73 177 73 73 177v71q0 15 11 25t25 11 25-11 11-25z m-143 214v-285q0-74-52-126t-127-53-126 53-52 126v285q0 74 52 127t126 52 127-52 52-127z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="endtime" unicode="&#xe839;" d="M661 350q0-14-11-25l-303-304q-11-10-26-10t-25 10-10 25v161h-250q-15 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 10 25t25 10 26-10l303-304q11-10 11-25z m196 196v-392q0-67-47-114t-114-47h-178q-7 0-13 5t-5 13q0 2-1 11t0 15 2 13 5 11 12 3h178q37 0 64 27t26 63v392q0 37-26 64t-64 26h-174t-6 0-6 2-5 3-4 5-1 8q0 2-1 11t0 15 2 13 5 11 12 3h178q67 0 114-47t47-114z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="starttime" unicode="&#xe83a;" d="M357 46q0-2 1-11t0-14-2-14-5-11-12-3h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="calendar-empty" unicode="&#xe83b;" d="M71-79h786v572h-786v-572z m215 679v161q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h36q8 0 13 5t5 13z m428 0v161q0 8-5 13t-13 5h-35q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="calendar" unicode="&#xe83c;" d="M71-79h161v161h-161v-161z m197 0h178v161h-178v-161z m-197 197h161v178h-161v-178z m197 0h178v178h-178v-178z m-197 214h161v161h-161v-161z m411-411h179v161h-179v-161z m-214 411h178v161h-178v-161z m428-411h161v161h-161v-161z m-214 197h179v178h-179v-178z m-196 482v161q0 7-6 12t-12 6h-36q-7 0-12-6t-6-12v-161q0-7 6-13t12-5h36q7 0 12 5t6 13z m410-482h161v178h-161v-178z m-214 214h179v161h-179v-161z m214 0h161v161h-161v-161z m18 268v161q0 7-5 12t-13 6h-35q-7 0-13-6t-5-12v-161q0-7 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="wrench" unicode="&#xe83d;" d="M214 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="sliders" unicode="&#xe83e;" d="M196 64v-71h-196v71h196z m197 72q14 0 25-11t11-25v-143q0-14-11-25t-25-11h-143q-14 0-25 11t-11 25v143q0 15 11 25t25 11h143z m89 214v-71h-482v71h482z m-357 286v-72h-125v72h125z m732-572v-71h-411v71h411z m-536 643q15 0 26-10t10-26v-142q0-15-10-25t-26-11h-142q-15 0-25 11t-11 25v142q0 15 11 26t25 10h142z m358-286q14 0 25-10t10-25v-143q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v143q0 14 11 25t25 10h143z m178-71v-71h-125v71h125z m0 286v-72h-482v72h482z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="services" unicode="&#xe83f;" d="M500 350q0 59-42 101t-101 42-101-42-42-101 42-101 101-42 101 42 42 101z m429-286q0 29-22 51t-50 21-50-21-21-51q0-29 21-50t50-21 51 21 21 50z m0 572q0 29-22 50t-50 21-50-21-21-50q0-30 21-51t50-21 51 21 21 51z m-215-235v-103q0-6-4-11t-8-6l-87-14q-6-19-18-42 19-27 50-64 4-6 4-11 0-7-4-11-12-17-46-50t-43-33q-7 0-12 4l-64 50q-21-11-43-17-6-60-13-87-4-13-17-13h-104q-6 0-11 4t-5 10l-13 85q-19 6-42 18l-66-50q-4-4-11-4-6 0-12 4-80 75-80 90 0 5 4 10 5 8 23 30t26 34q-13 24-20 46l-85 13q-5 1-9 5t-4 11v104q0 5 4 10t9 6l86 14q7 19 18 42-19 27-50 64-4 6-4 11 0 7 4 12 12 16 46 49t44 33q6 0 12-4l64-50q19 10 43 18 6 60 13 86 3 13 16 13h104q6 0 11-4t6-10l13-85q19-6 42-17l65 49q5 4 12 4 6 0 11-4 81-75 81-90 0-4-4-10-7-9-24-30t-25-34q13-27 19-46l85-12q6-2 9-6t4-11z m357-298v-78q0-9-83-17-6-15-16-29 28-63 28-77 0-2-2-4-68-40-69-40-5 0-26 27t-29 37q-11-1-17-1t-17 1q-7-11-29-37t-25-27q-1 0-69 40-3 2-3 4 0 14 29 77-10 14-17 29-83 8-83 17v78q0 9 83 18 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-38q12 1 17 1t17-1q28 40 51 63l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-9 83-18z m0 572v-78q0-9-83-18-6-15-16-29 28-63 28-77 0-2-2-4-68-39-69-39-5 0-26 26t-29 38q-11-1-17-1t-17 1q-7-12-29-38t-25-26q-1 0-69 39-3 2-3 4 0 14 29 77-10 14-17 29-83 9-83 18v78q0 9 83 17 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-37q12 1 17 1t17-1q28 39 51 62l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-8 83-17z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="service" unicode="&#xe840;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="phone" unicode="&#xe841;" d="M786 158q0-15-6-39t-12-38q-11-28-68-60-52-28-103-28-15 0-30 2t-32 7-26 8-31 11-28 10q-54 20-97 47-71 44-148 120t-120 148q-27 43-46 97-2 5-10 28t-12 31-8 26-7 32-2 29q0 52 29 104 31 57 59 68 14 6 38 12t39 6q8 0 12-2 10-3 30-42 6-11 16-31t20-35 17-30q2-2 10-14t12-20 4-16q0-11-16-27t-35-31-34-30-16-25q0-5 3-13t4-11 8-14 7-10q42-77 97-132t131-97q1 0 10-6t14-8 11-5 13-2q10 0 25 16t30 34 31 35 28 16q7 0 15-4t20-12 14-10q14-8 30-17t36-20 30-17q39-19 42-29 2-4 2-12z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="file-pdf" unicode="&#xe842;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-287 331q18-14 47-31 33 4 65 4 82 0 99-27 9-13 1-29 0-1-1-1l-1-2v0q-3-21-39-21-27 0-64 11t-73 29q-123-13-219-46-85-146-135-146-8 0-15 4l-14 7q0 0-3 2-6 6-4 20 5 23 32 51t73 54q8 5 13-3 1-1 1-2 29 47 60 110 38 76 58 146-13 46-17 89t4 71q6 22 23 22h12q13 0 20-8 10-12 5-38-1-3-2-4 0-2 0-5v-17q-1-68-8-107 31-91 82-133z m-321-229q29 13 76 88-29-22-49-47t-27-41z m222 513q-9-23-2-73 1 4 4 24 0 2 4 24 1 3 3 5-1 0-1 1-1 1-1 2 0 12-7 20 0-1 0-1v-2z m-70-368q76 30 159 45-1 0-7 5t-9 8q-43 37-71 98-15-48-46-110-17-31-26-46z m361 9q-13 13-78 13 42-16 69-16 8 0 10 1 0 0-1 2z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="file-word" unicode="&#xe843;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-656 500v-59h39l92-369h88l72 271q4 11 5 25 2 9 2 14h2l1-14q1-1 2-11t3-14l72-271h89l91 369h39v59h-167v-59h50l-55-245q-3-11-4-25l-1-12h-3q0 2 0 4t-1 4 0 4q-1 2-2 11t-3 14l-81 304h-63l-81-304q-1-5-2-13t-2-12l-2-12h-2l-2 12q-1 14-3 25l-56 245h50v59h-167z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="file-excel" unicode="&#xe844;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-547 131v-59h157v59h-42l58 90q3 4 5 9t5 8 2 2h1q0-2 3-6 1-2 2-4t3-4 4-5l60-90h-43v-59h163v59h-38l-107 152 108 158h38v59h-156v-59h41l-57-89q-2-4-6-9t-5-8l-1-1h-1q0 2-3 5-3 6-9 13l-59 89h42v59h-162v-59h38l106-152-109-158h-38z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="doc-text" unicode="&#xe845;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-572 483q0 7 5 12t13 5h393q8 0 13-5t5-12v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36z m411-125q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z m0-143q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="trash" unicode="&#xe846;" d="M286 439v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m143 0v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m142 0v-321q0-8-5-13t-12-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q7 0 12-5t5-13z m72-404v529h-500v-529q0-12 4-22t8-15 6-5h464q2 0 6 5t8 15 4 22z m-375 601h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="comment-empty" unicode="&#xe847;" d="M500 636q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" />
+
+<glyph glyph-name="comment" unicode="&#xe848;" d="M1000 350q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12-10-1-17 5t-10 16v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 73 40 139t106 114 160 76 194 28q136 0 251-48t182-130 67-179z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chat" unicode="&#xe849;" d="M786 421q0-77-53-143t-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38 197-38 143-104 53-144z m214-142q0-67-40-126t-108-98q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chat-empty" unicode="&#xe84a;" d="M393 636q-85 0-160-29t-118-79-44-107q0-45 30-88t83-73l54-32-19-46q19 11 34 21l25 18 30-6q43-8 85-8 85 0 160 29t118 79 43 106-43 107-118 79-160 29z m0 71q106 0 197-38t143-104 53-144-53-143-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38z m459-652q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128q0-67-40-126t-108-98z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell" unicode="&#xe84b;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-372 160h726q-149 168-149 465 0 28-13 58t-39 58-67 45-95 17-95-17-67-45-39-58-13-58q0-297-149-465z m827 0q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell-alt" unicode="&#xe84c;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m455 160q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="attention-alt" unicode="&#xe84d;" d="M286 154v-125q0-15-11-25t-25-11h-143q-14 0-25 11t-11 25v125q0 14 11 25t25 10h143q15 0 25-10t11-25z m17 589l-16-429q-1-14-12-25t-25-10h-143q-14 0-25 10t-12 25l-15 429q-1 14 10 25t24 11h179q14 0 25-11t10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="print" unicode="&#xe84e;" d="M214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="edit" unicode="&#xe84f;" d="M496 189l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="forward" unicode="&#xe850;" d="M1000 493q0-15-11-25l-285-286q-11-11-25-11t-25 11-11 25v143h-125q-55 0-98-3t-86-12-74-24-59-39-45-56-27-77-10-101q0-31 3-69 0-4 2-13t1-15q0-8-5-14t-13-6q-9 0-15 10-4 5-8 12t-7 17-6 13q-71 159-71 252 0 111 30 186 90 225 488 225h125v143q0 14 11 25t25 10 25-10l285-286q11-11 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply" unicode="&#xe851;" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply-all" unicode="&#xe852;" d="M357 246v-39q0-23-22-33-7-3-14-3-15 0-25 11l-285 286q-11 10-11 25t11 25l285 286q17 17 39 8 22-10 22-33v-39l-221-222q-11-11-11-25t11-25z m643-21q0-32-9-74t-22-77-27-70-22-51l-11-22q-5-10-16-10-3 0-5 1-14 4-13 19 24 223-59 315-36 40-95 62t-150 29v-140q0-23-21-33-8-3-14-3-15 0-25 11l-286 286q-11 10-11 25t11 25l286 286q16 17 39 8 21-10 21-33v-147q230-15 335-123 94-96 94-284z" horiz-adv-x="1000" />
+
+<glyph glyph-name="eye" unicode="&#xe853;" d="M929 314q-85 132-213 197 34-58 34-125 0-103-73-177t-177-73-177 73-73 177q0 67 34 125-128-65-213-197 75-114 187-182t242-68 243 68 186 182z m-402 215q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m473-215q0-19-11-38-78-129-210-206t-279-77-279 77-210 206q-11 19-11 38t11 39q78 128 210 205t279 78 279-78 210-205q11-20 11-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="tag" unicode="&#xe854;" d="M250 600q0 30-21 51t-50 20-51-20-21-51 21-50 51-21 50 21 21 50z m595-321q0-30-20-51l-274-274q-22-21-51-21-30 0-50 21l-399 399q-21 21-36 57t-15 65v232q0 29 21 50t50 22h233q29 0 65-15t57-36l399-399q20-21 20-50z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="tags" unicode="&#xe855;" d="M250 600q0 30-21 51t-50 20-51-20-21-51 21-50 51-21 50 21 21 50z m595-321q0-30-20-51l-274-274q-22-21-51-21-30 0-50 21l-399 399q-21 21-36 57t-15 65v232q0 29 21 50t50 22h233q29 0 65-15t57-36l399-399q20-21 20-50z m215 0q0-30-21-51l-274-274q-22-21-51-21-20 0-33 8t-29 25l262 262q21 21 21 51 0 29-21 50l-399 399q-21 21-57 36t-65 15h125q29 0 65-15t57-36l399-399q21-21 21-50z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="lock-open-alt" unicode="&#xe856;" d="M589 421q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="lock-open" unicode="&#xe857;" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="lock" unicode="&#xe858;" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="home" unicode="&#xe859;" d="M786 296v-267q0-15-11-25t-25-11h-214v214h-143v-214h-214q-15 0-25 11t-11 25v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-3-7 1-12 6l-35 41q-4 6-3 13t6 12l401 334q18 15 42 15t43-15l136-113v108q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q6-4 6-12t-4-13z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="info" unicode="&#xe85a;" d="M357 100v-71q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v71q0 15 11 25t25 11h35v214h-35q-15 0-25 11t-11 25v71q0 15 11 25t25 11h214q15 0 25-11t11-25v-321h35q15 0 26-11t10-25z m-71 643v-107q0-15-11-25t-25-11h-143q-14 0-25 11t-11 25v107q0 14 11 25t25 11h143q15 0 25-11t11-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="help" unicode="&#xe85b;" d="M393 149v-134q0-9-7-15t-15-7h-134q-9 0-16 7t-7 15v134q0 9 7 16t16 6h134q9 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 11t-6 20v26q0 46 37 87t79 60q33 16 47 32t14 42q0 24-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="search" unicode="&#xe85c;" d="M643 386q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="flapping" unicode="&#xe85d;" d="M372 582q-34-52-77-153-12 25-20 41t-23 35-28 32-36 19-45 8h-125q-8 0-13 5t-5 13v107q0 8 5 13t13 5h125q139 0 229-125z m628-446q0-8-5-13l-179-179q-5-5-12-5-8 0-13 6t-5 12v107q-18 0-48 0t-45-1-41 1-39 3-36 6-35 10-32 16-33 22-31 30-31 39q33 52 76 152 12-25 20-40t23-36 28-31 35-20 46-8h143v107q0 8 5 13t13 5q6 0 13-5l178-178q5-5 5-13z m0 500q0-8-5-13l-179-179q-5-5-12-5-8 0-13 6t-5 12v107h-143q-27 0-49-8t-38-25-29-34-25-44q-18-34-43-95-16-37-28-62t-30-59-36-55-41-47-50-38-60-23-71-10h-125q-8 0-13 5t-5 13v107q0 8 5 13t13 5h125q27 0 48 9t39 25 28 34 26 43q17 35 43 96 16 36 28 62t30 58 36 56 41 46 50 39 59 23 72 9h143v107q0 8 5 13t13 5q6 0 13-5l178-178q5-5 5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="rewind" unicode="&#xe85e;" d="M532 736q170 0 289-120t119-290-119-290-289-120q-142 0-252 88l70 74q84-60 182-60 126 0 216 90t90 218-90 218-216 90q-124 0-214-87t-92-211l142 0-184-204-184 204 124 0q2 166 122 283t286 117z" horiz-adv-x="940" />
+
+<glyph glyph-name="chart-line" unicode="&#xe85f;" d="M1143-7v-72h-1143v858h71v-786h1072z m-72 696v-242q0-12-10-17t-20 4l-68 68-353-353q-6-6-13-6t-13 6l-130 130-232-233-107 108 327 326q5 6 12 6t13-6l130-130 259 259-67 68q-9 8-5 19t17 11h243q7 0 12-5t5-13z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="bell-off" unicode="&#xe860;" d="M869 375q35-199 167-311 0-29-21-50t-51-21h-250q0-59-42-101t-101-42-100 42-42 100z m-298-480q9 0 9 9t-9 8q-32 0-56 24t-24 57q0 9-9 9t-9-9q0-41 29-70t69-28z m560 893q4-6 4-14t-6-12l-1045-905q-5-5-13-4t-12 6l-47 53q-4 6-4 14t6 12l104 90q-11 17-11 36 28 24 51 49t47 67 42 89 28 115 11 145q0 84 65 157t171 89q-4 10-4 21 0 23 16 38t37 16 38-16 16-38q0-11-4-21 69-10 122-46t82-88l234 202q5 5 13 4t12-6z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="bell-off-empty" unicode="&#xe861;" d="M580-96q0 8-9 8-32 0-56 24t-24 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-299 265l489 424q-23 49-74 82t-125 32q-51 0-94-17t-68-45-38-58-14-58q0-215-76-360z m755-105q0-29-21-50t-51-21h-250q0-59-42-101t-101-42-100 42-42 100l83 72h422q-92 105-126 256l61 55q35-199 167-311z m48 777l47-53q4-6 4-14t-6-12l-1045-905q-5-5-13-4t-12 6l-47 53q-4 6-4 14t6 12l104 90q-11 17-11 36 28 24 51 49t47 67 42 89 28 115 11 145q0 84 65 157t171 89q-4 10-4 21 0 23 16 38t37 16 38-16 16-38q0-11-4-21 69-10 122-46t82-88l234 202q5 5 13 4t12-6z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="plug" unicode="&#xe862;" d="M979 597q21-21 21-50t-21-51l-223-223 83-84-89-89q-91-91-217-104t-230 56l-202-202h-101v101l202 202q-69 103-56 230t104 217l89 89 84-83 223 223q21 21 51 21t50-21 21-50-21-51l-223-223 131-131 223 223q22 21 51 21t50-21z" horiz-adv-x="1000" />
+
+<glyph glyph-name="eye-off" unicode="&#xe863;" d="M310 105l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="arrows-cw" unicode="&#xe864;" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="cw" unicode="&#xe865;" d="M408 760q168 0 287-116t123-282l122 0-184-206-184 206 144 0q-4 124-94 210t-214 86q-126 0-216-90t-90-218q0-126 90-216t216-90q104 0 182 60l70-76q-110-88-252-88-168 0-288 120t-120 290 120 290 288 120z" horiz-adv-x="940" />
+
+<glyph glyph-name="host" unicode="&#xe866;" d="M232 136q-37 0-63 26t-26 63v393q0 37 26 63t63 26h607q37 0 63-26t27-63v-393q0-37-27-63t-63-26h-607z m-18 482v-393q0-7 6-13t12-5h607q8 0 13 5t5 13v393q0 7-5 12t-13 6h-607q-7 0-12-6t-6-12z m768-518h89v-54q0-22-26-37t-63-16h-893q-36 0-63 16t-26 37v54h982z m-402-54q9 0 9 9t-9 9h-89q-9 0-9-9t9-9h89z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="thumbs-up" unicode="&#xe867;" d="M143 100q0 15-11 25t-25 11-25-11-11-25 11-25 25-11 25 11 11 25z m643 321q0 29-22 50t-50 22h-196q0 32 27 89t26 89q0 55-17 81t-72 27q-14-15-21-48t-17-70-33-61q-13-13-43-51-2-3-13-16t-18-23-19-24-22-25-22-19-22-15-20-6h-18v-357h18q7 0 18-1t18-4 21-6 20-7 20-6 16-6q118-41 191-41h67q107 0 107 93 0 15-2 31 16 9 26 30t10 41-10 38q29 28 29 67 0 14-5 31t-14 26q18 1 30 26t12 45z m71 1q0-50-27-91 5-18 5-38 0-43-21-81 1-12 1-24 0-56-33-99 0-78-48-123t-126-45h-72q-54 0-106 13t-121 36q-65 23-77 23h-161q-29 0-50 21t-21 50v357q0 30 21 51t50 21h153q20 13 77 86 32 42 60 72 13 14 19 48t17 70 35 60q22 21 50 21 47 0 84-18t57-57 20-104q0-51-27-107h98q58 0 101-42t42-100z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="thumbs-down" unicode="&#xe868;" d="M143 600q0 15-11 25t-25 11-25-11-11-25 11-25 25-11 25 11 11 25z m643-321q0 19-12 45t-30 26q8 10 14 27t5 31q0 38-29 66 10 17 10 38 0 21-10 41t-26 30q2 16 2 31 0 47-27 70t-76 23h-71q-73 0-191-41-3-1-16-5t-20-7-20-7-21-6-18-4-18-1h-18v-357h18q9 0 20-5t22-15 22-20 22-25 19-24 18-22 13-17q30-38 43-51 23-24 33-61t17-70 21-48q54 0 72 27t17 81q0 33-26 89t-27 89h196q28 0 50 22t22 50z m71-1q0-57-42-100t-101-42h-98q27-55 27-107 0-66-20-104-19-39-57-57t-84-18q-28 0-50 21-19 18-30 45t-14 51-10 47-17 36q-27 28-60 71-57 73-77 86h-153q-29 0-50 21t-21 51v357q0 29 21 50t50 21h161q12 0 77 23 72 24 125 36t111 13h63q78 0 126-44t48-121v-3q33-43 33-99 0-12-1-24 21-38 21-80 0-21-5-39 27-41 27-91z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="spinner" unicode="&#xe869;" d="M294 72q0-29-21-50t-51-21q-29 0-50 21t-21 50q0 30 21 51t50 21 51-21 21-51z m277-115q0-29-20-50t-51-21-50 21-21 50 21 51 50 21 51-21 20-51z m-392 393q0-30-21-50t-51-21-50 21-21 50 21 51 50 20 51-20 21-51z m670-278q0-29-21-50t-50-21q-30 0-51 21t-20 50 20 51 51 21 50-21 21-51z m-538 556q0-37-26-63t-63-26-63 26-26 63 26 63 63 26 63-26 26-63z m653-278q0-30-21-50t-50-21-51 21-21 50 21 51 51 20 50-20 21-51z m-357 393q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m296-115q0-52-37-88t-88-37q-52 0-88 37t-37 88q0 51 37 88t88 37q51 0 88-37t37-88z" horiz-adv-x="1000" />
+
+<glyph glyph-name="attach" unicode="&#xe86a;" d="M784 77q0-65-45-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 89 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-12 5l-339 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-36 81-36 36 0 59 24t24 59q0 46-35 81l-325 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-6 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 84-35l324-325q56-54 56-131z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="keyboard" unicode="&#xe86b;" d="M214 198v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m72 143v-53q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h125q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m572-286v-53q0-9-9-9h-482q-9 0-9 9v53q0 9 9 9h482q9 0 9-9z m-357 143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-8-9h-54q-9 0-9 9v53q0 9 9 9h54q8 0 8-9z m-71 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m215-143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-286 286v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-196q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h62v134q0 9 9 9h54q9 0 9-9z m71-420v500h-929v-500h929z m71 500v-500q0-29-20-50t-51-21h-929q-29 0-50 21t-21 50v500q0 30 21 51t50 21h929q30 0 51-21t20-51z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="menu" unicode="&#xe86c;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="wifi" unicode="&#xe86d;" d="M571 0q-11 0-51 41t-41 52q0 18 35 30t57 13 58-13 35-30q0-11-41-52t-52-41z m151 151q-1 0-22 14t-57 28-72 14-71-14-57-28-22-14q-10 0-52 42t-42 52q0 7 5 13 44 43 109 67t130 25 131-25 109-67q5-6 5-13 0-10-42-52t-52-42z m152 152q-6 0-12 5-76 58-141 86t-150 27q-47 0-95-12t-83-29-63-35-44-30-18-12q-9 0-51 42t-42 52q0 7 6 12 74 74 178 115t212 40 213-40 178-115q6-5 6-12 0-10-42-52t-52-42z m152 151q-6 0-13 5-99 88-207 132t-235 45-234-45-207-132q-7-5-13-5-9 0-51 42t-43 52q0 7 6 13 104 104 248 161t294 57 295-57 248-161q5-6 5-13 0-10-42-52t-51-42z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="moon" unicode="&#xe86e;" d="M704 123q-30-5-61-5-102 0-188 50t-137 137-50 188q0 107 58 199-112-33-183-128t-72-214q0-72 29-139t76-113 114-77 139-28q80 0 152 34t123 96z m114 47q-53-113-159-181t-230-68q-87 0-167 34t-136 92-92 137-34 166q0 85 32 163t87 135 132 92 161 38q25 1 34-22 11-23-8-40-48-43-73-101t-26-122q0-83 41-152t111-111 152-41q66 0 127 29 23 10 40-7 8-8 10-19t-2-22z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="chart-pie" unicode="&#xe86f;" d="M429 353l304-304q-59-61-138-94t-166-34q-117 0-216 58t-155 156-58 215 58 215 155 156 216 58v-426z m104-3h431q0-88-33-167t-94-138z m396 71h-429v429q117 0 215-57t156-156 58-216z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chart-area" unicode="&#xe870;" d="M1143-7v-72h-1143v858h71v-786h1072z m-214 571l142-500h-928v322l250 321 321-321z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="chart-bar" unicode="&#xe871;" d="M357 350v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="beaker" unicode="&#xe872;" d="M852 42q31-50 12-85t-78-36h-643q-59 0-78 36t12 85l280 443v222h-36q-14 0-25 11t-10 25 10 25 25 11h286q15 0 25-11t11-25-11-25-25-11h-36v-222z m-435 405l-151-240h397l-152 240-11 17v243h-71v-243z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="magic" unicode="&#xe873;" d="M664 526l164 163-60 60-164-163z m250 163q0-15-10-25l-718-718q-10-10-25-10t-25 10l-111 111q-10 10-10 25t10 25l718 718q10 10 25 10t25-10l111-111q10-10 10-25z m-754 106l54-16-54-17-17-55-17 55-55 17 55 16 17 55z m195-90l109-34-109-33-34-109-33 109-109 33 109 34 33 109z m519-267l55-17-55-16-17-55-17 55-54 16 54 17 17 55z m-357 357l54-16-54-17-17-55-17 55-54 17 54 16 17 55z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="spin6" unicode="&#xe874;" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" />
+
+<glyph glyph-name="down-small" unicode="&#xe875;" d="M505 346q15-15 15-37t-15-37l-245-245-245 245q-15 15-15 37t15 37 37 15 37-15l120-119 0 395q0 21 15 36t36 15 37-15 16-36l0-395 120 119q15 15 36 15t36-15z" horiz-adv-x="520" />
+
+<glyph glyph-name="left-small" unicode="&#xe876;" d="M595 403q21 0 36-16t15-37-15-37-36-15l-395 0 119-119q15-15 15-37t-15-37-36-15q-23 0-38 15l-245 245 245 245q15 15 37 15t37-15 15-37-15-37l-119-118 395 0z" horiz-adv-x="646" />
+
+<glyph glyph-name="right-small" unicode="&#xe877;" d="M328 595q15 15 36 15t37-15l245-245-245-245q-15-15-36-15-22 0-37 15t-15 37 15 37l120 119-395 0q-22 0-37 15t-16 37 16 37 37 16l395 0-120 118q-15 15-15 37t15 37z" horiz-adv-x="646" />
+
+<glyph glyph-name="up-small" unicode="&#xe878;" d="M260 673l245-245q15-15 15-37t-15-37-36-15-36 15l-120 120 0-395q0-21-16-37t-37-15-36 15-15 37l0 395-120-120q-15-15-37-15t-37 15-15 37 15 37z" horiz-adv-x="520" />
+
+<glyph glyph-name="pin" unicode="&#xe879;" d="M573 37q0-23-15-38t-37-15q-21 0-37 16l-169 169-315-236 236 315-168 169q-24 23-12 56 14 32 48 32 157 0 270 57 90 45 151 171 9 24 36 32t50-13l208-209q21-23 14-50t-32-36q-127-63-172-152-56-110-56-268z" horiz-adv-x="834" />
+
+<glyph glyph-name="angle-double-left" unicode="&#xe87a;" d="M350 82q0-7-6-13l-28-28q-5-5-12-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q5 6 13 6t12-6l28-28q6-5 6-13t-6-12l-219-220 219-219q6-6 6-13z m214 0q0-7-5-13l-28-28q-6-5-13-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q6 6 13 6t13-6l28-28q5-5 5-13t-5-12l-220-220 220-219q5-6 5-13z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="angle-double-right" unicode="&#xe87b;" d="M332 314q0-7-5-12l-261-261q-5-5-12-5t-13 5l-28 28q-6 6-6 13t6 13l219 219-219 220q-6 5-6 12t6 13l28 28q5 6 13 6t12-6l261-260q5-5 5-13z m214 0q0-7-5-12l-260-261q-6-5-13-5t-13 5l-28 28q-5 6-5 13t5 13l219 219-219 220q-5 5-5 12t5 13l28 28q6 6 13 6t13-6l260-260q5-5 5-13z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="circle" unicode="&#xe87c;" d="M857 350q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="info-circled" unicode="&#xe87d;" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m52-152q-42 0-65-24t-23-50q-2-28 15-44t49-16q38 0 61 22t23 54q0 58-60 58z m-120-594q30 0 84 26t106 78l-18 24q-48-36-72-36-14 0-4 38l42 160q26 96-22 96-30 0-89-29t-115-75l16-26q52 34 74 34 12 0 0-34l-36-152q-26-104 34-104z" horiz-adv-x="920" />
+
+<glyph glyph-name="twitter" unicode="&#xe87e;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="facebook-squared" unicode="&#xe87f;" d="M696 779q67 0 114-48t47-113v-536q0-66-47-113t-114-48h-104v333h111l16 129h-127v83q0 31 13 46t51 16l68 1v115q-35 5-100 5-75 0-121-44t-45-127v-95h-112v-129h112v-333h-297q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="gplus-squared" unicode="&#xe880;" d="M512 345q0 15-4 36h-202v-74h122q-2-13-10-28t-21-29-37-25-54-10q-55 0-94 40t-39 95 39 95 94 40q52 0 86-33l58 57q-60 55-144 55-89 0-151-62t-63-152 63-151 151-63q92 0 149 58t57 151z m192-26h61v62h-61v61h-61v-61h-61v-62h61v-61h61v61z m153 299v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="attention-circled" unicode="&#xe881;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m71-696v106q0 8-5 13t-12 5h-107q-8 0-13-5t-6-13v-106q0-8 6-13t13-6h107q7 0 12 6t5 13z m-1 192l10 346q0 7-6 10-5 5-13 5h-123q-8 0-13-5-6-3-6-10l10-346q0-6 5-10t14-4h103q8 0 13 4t6 10z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="check" unicode="&#xe883;" d="M786 331v-177q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-6-5-13-5-1 0-5 1-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v141q0 8 5 13l36 35q6 6 13 6 3 0 7-2 11-4 11-16z m129 273l-455-454q-13-14-31-14t-32 14l-240 240q-14 13-14 31t14 32l61 62q14 13 32 13t32-13l147-147 361 361q13 13 31 13t32-13l62-61q13-14 13-32t-13-32z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="reschedule" unicode="&#xe884;" d="M186 140l116 116 0-292-276 16 88 86q-116 122-114 290t120 288q100 100 240 116l4-102q-100-16-172-88-88-88-90-213t84-217z m332 598l276-16-88-86q116-122 114-290t-120-288q-96-98-240-118l-2 104q98 16 170 88 88 88 90 213t-84 217l-114-116z" horiz-adv-x="820" />
+
+<glyph glyph-name="warning-empty" unicode="&#xe885;" d="M514 701q-49 0-81-55l-308-513q-32-55-11-95t87-40l625 0q65 0 87 40t-12 95l-307 513q-33 55-80 55z m0 105q106 0 169-107l308-513q63-105 12-199-52-93-177-93l-625 0q-123 0-177 93-53 92 11 199l309 513q62 107 170 107z m-69-652q0 69 69 69 67 0 67-69 0-67-67-67-69 0-69 67z m146 313q0-14-6-29l-71-179q-44 108-73 179-6 15-6 29 0 32 23 55t56 24 55-24 22-55z" horiz-adv-x="1026" />
+
+<glyph glyph-name="th-list" unicode="&#xf009;" d="M0 62q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m234-576q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z" horiz-adv-x="937.5" />
+
+<glyph glyph-name="th-thumb-empty" unicode="&#xf00b;" d="M0-66v286q0 22 15 37t37 16h286q21 0 37-16t15-37v-286q0-21-15-36t-37-15h-286q-22 0-37 15t-15 36z m0 546v286q0 21 15 36t37 15h286q21 0 37-15t15-36v-286q0-22-15-37t-37-16h-286q-21 0-37 16t-15 37z m88-510h214v214h-214v-214z m0 546h214v213h-214v-213z m459-582v286q0 22 15 37t37 16h286q21 0 37-16t15-37v-286q0-21-15-36t-37-15h-286q-21 0-37 15t-15 36z m0 546v286q0 21 15 36t37 15h286q22 0 37-15t15-36v-286q0-22-15-37t-37-16h-286q-21 0-37 16t-15 37z m88-510h215v214h-215v-214z m0 546h215v213h-215v-213z" horiz-adv-x="937.5" />
+
+<glyph glyph-name="github-circled" unicode="&#xf09b;" d="M429 779q116 0 215-58t156-156 57-215q0-140-82-252t-211-155q-15-3-22 4t-7 17q0 1 0 43t0 75q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 67-44 115 21 51-4 114-16 5-46-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-47 7q-25-63-5-114-44-48-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-21-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l6-13q7-21 24-34t37-17 39-3 31 1l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58z m-267-616q2 4-3 7-6 1-8-1-1-4 4-7 5-3 7 1z m18-19q4 3-1 9-6 5-9 2-4-3 1-9 5-6 9-2z m16-25q6 4 0 11-4 7-9 3-5-3 0-10t9-4z m24-23q4 4-2 10-7 7-11 2-5-5 2-11 6-6 11-1z m32-14q1 6-8 9-8 2-10-4t7-9q8-3 11 4z m35-3q0 7-10 6-9 0-9-6 0-7 10-6 9 0 9 6z m32 5q-1 7-10 5-9-1-8-8t10-4 8 7z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="history" unicode="&#xf1da;" d="M857 350q0-87-34-166t-91-137-137-92-166-34q-96 0-183 41t-147 114q-4 6-4 13t5 11l76 77q6 5 14 5 9-1 13-7 41-53 100-82t126-29q58 0 110 23t92 61 61 91 22 111-22 111-61 91-92 61-110 23q-55 0-105-20t-90-57l77-77q17-16 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l72-72q60 57 137 88t159 31q87 0 166-34t137-92 91-137 34-166z m-357 161v-250q0-8-5-13t-13-5h-178q-8 0-13 5t-5 13v35q0 8 5 13t13 5h125v197q0 8 5 13t12 5h36q8 0 13-5t5-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
+</font>
+</defs>
+</svg> \ No newline at end of file
diff --git a/public/font/ifont.ttf b/public/font/ifont.ttf
new file mode 100644
index 0000000..8dd6019
--- /dev/null
+++ b/public/font/ifont.ttf
Binary files differ
diff --git a/public/font/ifont.woff b/public/font/ifont.woff
new file mode 100644
index 0000000..2efa0ae
--- /dev/null
+++ b/public/font/ifont.woff
Binary files differ
diff --git a/public/font/ifont.woff2 b/public/font/ifont.woff2
new file mode 100644
index 0000000..25ca736
--- /dev/null
+++ b/public/font/ifont.woff2
Binary files differ
diff --git a/public/img/favicon.png b/public/img/favicon.png
new file mode 100644
index 0000000..d5c0044
--- /dev/null
+++ b/public/img/favicon.png
Binary files differ
diff --git a/public/img/icinga-loader-light.gif b/public/img/icinga-loader-light.gif
new file mode 100644
index 0000000..04ac07f
--- /dev/null
+++ b/public/img/icinga-loader-light.gif
Binary files differ
diff --git a/public/img/icinga-loader.gif b/public/img/icinga-loader.gif
new file mode 100644
index 0000000..727f4b4
--- /dev/null
+++ b/public/img/icinga-loader.gif
Binary files differ
diff --git a/public/img/icinga-logo-big-dark.png b/public/img/icinga-logo-big-dark.png
new file mode 100644
index 0000000..c75d194
--- /dev/null
+++ b/public/img/icinga-logo-big-dark.png
Binary files differ
diff --git a/public/img/icinga-logo-big-dark.svg b/public/img/icinga-logo-big-dark.svg
new file mode 100644
index 0000000..ed45b8b
--- /dev/null
+++ b/public/img/icinga-logo-big-dark.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 286 99" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <rect id="icinga-logo-big-dark" x="0" y="0" width="286" height="99" style="fill:none;"/>
+ <clipPath id="_clip1">
+ <rect id="icinga-logo-big-dark1" serif:id="icinga-logo-big-dark" x="0" y="0" width="286" height="99"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g>
+ <path id="Mask" d="M78.018,47.989C77.199,48.807 76.791,49.804 76.791,50.979L76.791,93.158C76.791,94.336 77.213,95.332 78.057,96.15C78.901,96.968 79.884,97.377 81.01,97.377C82.186,97.377 83.182,96.968 84,96.15C84.818,95.332 85.227,94.336 85.227,93.158L85.227,50.979C85.227,49.804 84.806,48.807 83.962,47.989C83.118,47.171 82.133,46.762 81.01,46.762C79.833,46.762 78.835,47.171 78.018,47.989ZM102.215,59.76C102.751,58.508 103.491,57.421 104.439,56.501C105.384,55.58 106.484,54.853 107.736,54.316C108.987,53.779 110.33,53.51 111.762,53.51C113.96,53.51 115.916,54.123 117.629,55.351C119.341,56.577 120.582,58.138 121.348,60.03C122.165,61.716 123.444,62.56 125.183,62.56C126.512,62.56 127.534,62.125 128.25,61.257C128.966,60.387 129.323,59.442 129.323,58.418C129.323,58.112 129.286,57.755 129.209,57.345C129.132,56.937 128.991,56.553 128.787,56.195C128.071,54.559 127.139,53.063 125.987,51.708C124.838,50.355 123.521,49.178 122.039,48.18C120.555,47.183 118.944,46.417 117.207,45.88C115.469,45.343 113.653,45.074 111.762,45.074C109.155,45.074 106.713,45.561 104.439,46.531C102.162,47.503 100.183,48.832 98.494,50.52C96.808,52.206 95.478,54.189 94.507,56.462C93.535,58.739 93.05,61.155 93.05,63.709L93.05,80.352C93.05,82.909 93.548,85.312 94.545,87.56C95.543,89.811 96.885,91.779 98.571,93.465C100.259,95.153 102.238,96.495 104.515,97.492C106.789,98.489 109.205,98.987 111.762,98.987C113.756,98.987 115.647,98.706 117.438,98.143C119.226,97.58 120.861,96.777 122.345,95.728C123.828,94.68 125.144,93.442 126.295,92.009C127.445,90.578 128.353,88.991 129.018,87.253C129.221,86.794 129.323,86.257 129.323,85.643C129.323,84.621 128.953,83.676 128.211,82.806C127.47,81.937 126.46,81.502 125.183,81.502C124.517,81.502 123.814,81.732 123.074,82.192C122.332,82.653 121.782,83.24 121.426,83.955C120.607,85.849 119.341,87.42 117.629,88.672C115.916,89.926 113.96,90.551 111.762,90.551C110.382,90.551 109.066,90.282 107.814,89.746C106.559,89.209 105.461,88.481 104.515,87.56C103.568,86.641 102.815,85.555 102.253,84.301C101.69,83.049 101.41,81.732 101.41,80.352L101.41,63.709C101.41,62.329 101.678,61.014 102.215,59.76M141.672,46.762C140.494,46.762 139.496,47.171 138.68,47.989C137.861,48.807 137.453,49.804 137.453,50.979L137.453,93.158C137.453,94.336 137.874,95.332 138.718,96.15C139.562,96.968 140.546,97.377 141.672,97.377C142.846,97.377 143.843,96.968 144.661,96.15C145.479,95.332 145.888,94.336 145.888,93.158L145.888,50.979C145.888,49.804 145.467,48.807 144.623,47.989C143.779,47.171 142.795,46.762 141.672,46.762M185.536,50.673C183.849,48.986 181.867,47.643 179.593,46.646C177.317,45.65 174.901,45.151 172.346,45.151C169.788,45.151 167.373,45.65 165.099,46.646C162.822,47.643 160.842,48.986 159.154,50.673C157.468,52.36 156.138,54.328 155.166,56.577C154.195,58.827 153.71,61.231 153.71,63.787L153.71,93.158C153.71,94.336 154.119,95.332 154.937,96.15C155.754,96.968 156.752,97.377 157.927,97.377C159.103,97.377 160.087,96.968 160.881,96.15C161.672,95.332 162.068,94.336 162.068,93.158L162.068,63.787C162.068,62.405 162.338,61.09 162.875,59.837C163.412,58.585 164.14,57.497 165.06,56.577C165.98,55.657 167.066,54.928 168.319,54.392C169.571,53.855 170.913,53.587 172.346,53.587C173.776,53.587 175.118,53.855 176.372,54.392C177.624,54.928 178.723,55.657 179.67,56.577C180.615,57.497 181.356,58.585 181.893,59.837C182.431,61.09 182.698,62.405 182.698,63.787L182.698,93.158C182.698,94.336 183.121,95.332 183.965,96.15C184.807,96.968 185.792,97.377 186.917,97.377C188.092,97.377 189.076,96.968 189.869,96.15C190.661,95.332 191.057,94.336 191.057,93.158L191.057,63.787C191.057,61.231 190.559,58.827 189.562,56.577C188.565,54.328 187.223,52.36 185.536,50.673M232.623,69.922L224.264,69.922C223.089,69.922 222.103,70.332 221.311,71.149C220.519,71.968 220.123,72.939 220.123,74.064C220.123,75.239 220.519,76.237 221.311,77.054C222.103,77.872 223.089,78.281 224.264,78.281L228.405,78.281L228.405,80.352C228.405,81.732 228.137,83.049 227.601,84.301C227.063,85.555 226.335,86.641 225.415,87.56C224.494,88.481 223.408,89.209 222.156,89.746C220.901,90.282 219.56,90.551 218.129,90.551C216.749,90.551 215.432,90.282 214.18,89.746C212.926,89.209 211.827,88.481 210.882,87.56C209.936,86.641 209.195,85.555 208.658,84.301C208.121,83.049 207.852,81.732 207.852,80.352L207.852,63.787C207.852,62.405 208.121,61.09 208.658,59.837C209.195,58.585 209.924,57.497 210.844,56.577C211.765,55.657 212.85,54.928 214.103,54.392C215.355,53.855 216.697,53.587 218.129,53.587C220.326,53.587 222.282,54.201 223.996,55.428C225.709,56.654 226.974,58.24 227.792,60.182C228.148,61.001 228.685,61.614 229.403,62.023C230.118,62.433 230.834,62.637 231.55,62.637C232.879,62.637 233.901,62.202 234.617,61.332C235.333,60.464 235.69,59.519 235.69,58.496C235.69,58.189 235.678,57.894 235.652,57.613C235.627,57.332 235.563,57.065 235.462,56.808C234.745,55.07 233.812,53.485 232.662,52.054C231.511,50.623 230.181,49.396 228.674,48.372C227.165,47.351 225.517,46.557 223.727,45.994C221.937,45.433 220.071,45.151 218.129,45.151C215.572,45.151 213.156,45.637 210.882,46.608C208.606,47.579 206.625,48.908 204.937,50.596C203.252,52.283 201.909,54.265 200.913,56.54C199.915,58.815 199.416,61.231 199.416,63.787L199.416,80.352C199.416,82.909 199.915,85.312 200.913,87.56C201.909,89.811 203.252,91.778 204.937,93.465C206.625,95.152 208.606,96.495 210.882,97.492C213.156,98.488 215.572,98.986 218.129,98.986C220.685,98.986 223.102,98.488 225.376,97.492C227.65,96.495 229.633,95.152 231.32,93.465C233.008,91.778 234.349,89.811 235.346,87.56C236.343,85.312 236.842,82.909 236.842,80.352L236.842,74.064C236.842,72.888 236.432,71.903 235.614,71.11C234.796,70.318 233.8,69.922 232.623,69.922" style="fill:rgb(51,41,49);fill-rule:nonzero;"/>
+ <path d="M257.318,75.366C257.879,74.037 258.416,72.786 258.928,71.608C259.438,70.433 259.95,69.283 260.463,68.158C260.972,67.034 261.47,65.895 261.957,64.744C262.443,63.595 262.966,62.405 263.53,61.178L269.666,75.366L257.318,75.366ZM285.616,91.701L267.364,48.986C266.954,48.065 266.214,47.35 265.14,46.838C264.884,46.684 264.615,46.583 264.335,46.531C264.053,46.48 263.784,46.454 263.53,46.454C261.791,46.454 260.512,47.298 259.694,48.986L241.443,91.701C241.339,91.907 241.263,92.162 241.213,92.469C241.161,92.775 241.136,93.056 241.136,93.312C241.136,94.284 241.505,95.218 242.248,96.111C242.988,97.007 243.999,97.453 245.278,97.453C246.094,97.453 246.836,97.249 247.502,96.84C248.165,96.43 248.702,95.792 249.111,94.923C249.879,93.031 250.658,91.179 251.451,89.362C252.243,87.548 253.047,85.694 253.866,83.803L273.27,83.803L278.024,94.923C278.688,96.61 279.966,97.453 281.859,97.453C283.135,97.453 284.145,97.007 284.887,96.111C285.628,95.218 286,94.284 286,93.312C286,92.851 285.871,92.315 285.616,91.701ZM80.591,27.652C77.112,27.915 74.507,30.948 74.767,34.423C74.807,34.951 74.917,35.455 75.076,35.935L49.432,42.259C48.984,39.778 47.922,37.37 46.218,35.278C44.84,33.588 43.176,32.272 41.357,31.329L48.881,15.691C51.373,16.477 54.201,16.036 56.379,14.265C59.818,11.459 60.332,6.397 57.531,2.959C54.726,-0.48 49.662,-0.996 46.224,1.805C42.785,4.608 42.266,9.671 45.073,13.111C45.834,14.047 46.767,14.756 47.785,15.252L40.289,30.83C35.188,28.666 29.089,29.321 24.498,33.062C23.996,33.471 23.539,33.913 23.103,34.369L14.839,27.604C15.509,26.342 15.596,24.794 14.923,23.414C13.805,21.128 11.043,20.18 8.758,21.3C6.472,22.422 5.524,25.181 6.642,27.465C7.762,29.751 10.521,30.701 12.809,29.582C13.334,29.324 13.78,28.971 14.154,28.565L22.312,35.243C17.747,40.802 17.563,48.986 22.29,54.774C23.395,56.131 24.687,57.235 26.091,58.111L16.657,71.637C14.711,70.169 12.246,69.366 9.623,69.565C3.893,69.995 -0.402,74.987 0.03,80.712C0.459,86.443 5.452,90.736 11.181,90.305C16.909,89.875 21.201,84.881 20.771,79.154C20.57,76.476 19.364,74.121 17.56,72.405L27.113,58.707C31.657,61.075 37.195,61.055 41.792,58.478L47.17,67.462C46.447,67.974 45.932,68.769 45.823,69.717C45.616,71.493 46.888,73.099 48.665,73.305C50.439,73.507 52.043,72.237 52.251,70.46C52.455,68.687 51.185,67.081 49.408,66.876C49.005,66.829 48.614,66.868 48.244,66.962L42.804,57.874C43.213,57.601 43.615,57.31 44.005,56.993C48.186,53.582 50.109,48.429 49.595,43.434L75.559,37.031C76.722,39.104 79.009,40.435 81.536,40.245C85.014,39.987 87.621,36.954 87.359,33.478C87.097,30 84.066,27.393 80.591,27.652" style="fill:rgb(51,41,49);fill-rule:nonzero;"/>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/icinga-logo-big.png b/public/img/icinga-logo-big.png
new file mode 100644
index 0000000..90f64e4
--- /dev/null
+++ b/public/img/icinga-logo-big.png
Binary files differ
diff --git a/public/img/icinga-logo-big.svg b/public/img/icinga-logo-big.svg
new file mode 100644
index 0000000..740cbc8
--- /dev/null
+++ b/public/img/icinga-logo-big.svg
@@ -0,0 +1 @@
+<svg width="286" height="99" viewBox="0 0 286 99" xmlns="http://www.w3.org/2000/svg"><title>icinga-logo-big</title><g fill="#FEFEFE"><path d="M78.018 47.989c-.819.818-1.227 1.815-1.227 2.99v42.179c0 1.178.422 2.174 1.266 2.992.844.818 1.827 1.227 2.953 1.227 1.176 0 2.172-.409 2.99-1.227.818-.818 1.227-1.814 1.227-2.992v-42.179c0-1.175-.421-2.172-1.265-2.99-.844-.818-1.829-1.227-2.952-1.227-1.177 0-2.175.409-2.992 1.227zM102.215 59.76c.536-1.252 1.276-2.339 2.224-3.259.945-.921 2.045-1.648 3.297-2.185 1.251-.537 2.594-.806 4.026-.806 2.198 0 4.154.613 5.867 1.841 1.712 1.226 2.953 2.787 3.719 4.679.817 1.686 2.096 2.53 3.835 2.53 1.329 0 2.351-.435 3.067-1.303.716-.87 1.073-1.815 1.073-2.839 0-.306-.037-.663-.114-1.073-.077-.408-.218-.792-.422-1.15-.716-1.636-1.648-3.132-2.8-4.487-1.149-1.353-2.466-2.53-3.948-3.528-1.484-.997-3.095-1.763-4.832-2.3-1.738-.537-3.554-.806-5.445-.806-2.607 0-5.049.487-7.323 1.457-2.277.972-4.256 2.301-5.945 3.989-1.686 1.686-3.016 3.669-3.987 5.942-.972 2.277-1.457 4.693-1.457 7.247v16.643c0 2.557.498 4.96 1.495 7.208.998 2.251 2.34 4.219 4.026 5.905 1.688 1.688 3.667 3.03 5.944 4.027 2.274.997 4.69 1.495 7.247 1.495 1.994 0 3.885-.281 5.676-.844 1.788-.563 3.423-1.366 4.907-2.415 1.483-1.048 2.799-2.286 3.95-3.719 1.15-1.431 2.058-3.018 2.723-4.756.203-.459.305-.996.305-1.61 0-1.022-.37-1.967-1.112-2.837-.741-.869-1.751-1.304-3.028-1.304-.666 0-1.369.23-2.109.69-.742.461-1.292 1.048-1.648 1.763-.819 1.894-2.085 3.465-3.797 4.717-1.713 1.254-3.669 1.879-5.867 1.879-1.38 0-2.696-.269-3.948-.805-1.255-.537-2.353-1.265-3.299-2.186-.947-.919-1.7-2.005-2.262-3.259-.563-1.252-.843-2.569-.843-3.949v-16.643c0-1.38.268-2.695.805-3.949M141.672 46.762c-1.178 0-2.176.409-2.992 1.227-.819.818-1.227 1.815-1.227 2.99v42.179c0 1.178.421 2.174 1.265 2.992.844.818 1.828 1.227 2.954 1.227 1.174 0 2.171-.409 2.989-1.227.818-.818 1.227-1.814 1.227-2.992v-42.179c0-1.175-.421-2.172-1.265-2.99-.844-.818-1.828-1.227-2.951-1.227M185.536 50.673c-1.687-1.687-3.669-3.03-5.943-4.027-2.276-.996-4.692-1.495-7.247-1.495-2.558 0-4.973.499-7.247 1.495-2.277.997-4.257 2.34-5.945 4.027-1.686 1.687-3.016 3.655-3.988 5.904-.971 2.25-1.456 4.654-1.456 7.21v29.371c0 1.178.409 2.174 1.227 2.992.817.818 1.815 1.227 2.99 1.227 1.176 0 2.16-.409 2.954-1.227.791-.818 1.187-1.814 1.187-2.992v-29.371c0-1.382.27-2.697.807-3.95.537-1.252 1.265-2.34 2.185-3.26.92-.92 2.006-1.649 3.259-2.185 1.252-.537 2.594-.805 4.027-.805 1.43 0 2.772.268 4.026.805 1.252.536 2.351 1.265 3.298 2.185.945.92 1.686 2.008 2.223 3.26.538 1.253.805 2.568.805 3.95v29.371c0 1.178.423 2.174 1.267 2.992.842.818 1.827 1.227 2.952 1.227 1.175 0 2.159-.409 2.952-1.227.792-.818 1.188-1.814 1.188-2.992v-29.371c0-2.556-.498-4.96-1.495-7.21-.997-2.249-2.339-4.217-4.026-5.904M232.623 69.922h-8.359c-1.175 0-2.161.41-2.953 1.227-.792.819-1.188 1.79-1.188 2.915 0 1.175.396 2.173 1.188 2.99.792.818 1.778 1.227 2.953 1.227h4.141v2.071c0 1.38-.268 2.697-.804 3.949-.538 1.254-1.266 2.34-2.186 3.259-.921.921-2.007 1.649-3.259 2.186-1.255.536-2.596.805-4.027.805-1.38 0-2.697-.269-3.949-.805-1.254-.537-2.353-1.265-3.298-2.186-.946-.919-1.687-2.005-2.224-3.259-.537-1.252-.806-2.569-.806-3.949v-16.565c0-1.382.269-2.697.806-3.95.537-1.252 1.266-2.34 2.186-3.26.921-.92 2.006-1.649 3.259-2.185 1.252-.537 2.594-.805 4.026-.805 2.197 0 4.153.614 5.867 1.841 1.713 1.226 2.978 2.812 3.796 4.754.356.819.893 1.432 1.611 1.841.715.41 1.431.614 2.147.614 1.329 0 2.351-.435 3.067-1.305.716-.868 1.073-1.813 1.073-2.836 0-.307-.012-.602-.038-.883-.025-.281-.089-.548-.19-.805-.717-1.738-1.65-3.323-2.8-4.754-1.151-1.431-2.481-2.658-3.988-3.682-1.509-1.021-3.157-1.815-4.947-2.378-1.79-.561-3.656-.843-5.598-.843-2.557 0-4.973.486-7.247 1.457-2.276.971-4.257 2.3-5.945 3.988-1.685 1.687-3.028 3.669-4.024 5.944-.998 2.275-1.497 4.691-1.497 7.247v16.565c0 2.557.499 4.96 1.497 7.208.996 2.251 2.339 4.218 4.024 5.905 1.688 1.687 3.669 3.03 5.945 4.027 2.274.996 4.69 1.494 7.247 1.494 2.556 0 4.973-.498 7.247-1.494 2.274-.997 4.257-2.34 5.944-4.027 1.688-1.687 3.029-3.654 4.026-5.905.997-2.248 1.496-4.651 1.496-7.208v-6.288c0-1.176-.41-2.161-1.228-2.954-.818-.792-1.814-1.188-2.991-1.188" id="Mask"/><path d="M257.318 75.366c.561-1.329 1.098-2.58 1.61-3.758.51-1.175 1.022-2.325 1.535-3.45.509-1.124 1.007-2.263 1.494-3.414.486-1.149 1.009-2.339 1.573-3.566l6.136 14.188h-12.348zm28.298 16.335l-18.252-42.715c-.41-.921-1.15-1.636-2.224-2.148-.256-.154-.525-.255-.805-.307-.282-.051-.551-.077-.805-.077-1.739 0-3.018.844-3.836 2.532l-18.251 42.715c-.104.206-.18.461-.23.768-.052.306-.077.587-.077.843 0 .972.369 1.906 1.112 2.799.74.896 1.751 1.342 3.03 1.342.816 0 1.558-.204 2.224-.613.663-.41 1.2-1.048 1.609-1.917.768-1.892 1.547-3.744 2.34-5.561.792-1.814 1.596-3.668 2.415-5.559h19.404l4.754 11.12c.664 1.687 1.942 2.53 3.835 2.53 1.276 0 2.286-.446 3.028-1.342.741-.893 1.113-1.827 1.113-2.799 0-.461-.129-.997-.384-1.611zM80.591 27.652c-3.479.263-6.084 3.296-5.824 6.771.04.528.15 1.032.309 1.512l-25.644 6.324c-.448-2.481-1.51-4.889-3.214-6.981-1.378-1.69-3.042-3.006-4.861-3.949l7.524-15.638c2.492.786 5.32.345 7.498-1.426 3.439-2.806 3.953-7.868 1.152-11.306-2.805-3.439-7.869-3.955-11.307-1.154-3.439 2.803-3.958 7.866-1.151 11.306.761.936 1.694 1.645 2.712 2.141l-7.496 15.578c-5.101-2.164-11.2-1.509-15.791 2.232-.502.409-.959.851-1.395 1.307l-8.264-6.765c.67-1.262.757-2.81.084-4.19-1.118-2.286-3.88-3.234-6.165-2.114-2.286 1.122-3.234 3.881-2.116 6.165 1.12 2.286 3.879 3.236 6.167 2.117.525-.258.971-.611 1.345-1.017l8.158 6.678c-4.565 5.559-4.749 13.743-.022 19.531 1.105 1.357 2.397 2.461 3.801 3.337l-9.434 13.526c-1.946-1.468-4.411-2.271-7.034-2.072-5.73.43-10.025 5.422-9.593 11.147.429 5.731 5.422 10.024 11.151 9.593 5.728-.43 10.02-5.424 9.59-11.151-.201-2.678-1.407-5.033-3.211-6.749l9.553-13.698c4.544 2.368 10.082 2.348 14.679-.229l5.378 8.984c-.723.512-1.238 1.307-1.347 2.255-.207 1.776 1.065 3.382 2.842 3.588 1.774.202 3.378-1.068 3.586-2.845.204-1.773-1.066-3.379-2.843-3.584-.403-.047-.794-.008-1.164.086l-5.44-9.088c.409-.273.811-.564 1.201-.881 4.181-3.411 6.104-8.564 5.59-13.559l25.964-6.403c1.163 2.073 3.45 3.404 5.977 3.214 3.478-.258 6.085-3.291 5.823-6.767-.262-3.478-3.293-6.085-6.768-5.826"/></g></svg>
diff --git a/public/img/icinga-logo-compact-inverted.svg b/public/img/icinga-logo-compact-inverted.svg
new file mode 100644
index 0000000..e71c632
--- /dev/null
+++ b/public/img/icinga-logo-compact-inverted.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 41 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
+ <g>
+ <path d="M17.444,16.237L9.477,27.661M17.444,16.237L33.957,12.164M17.757,16.572L22.381,24.296M15.795,14.324L9.01,8.77M18.425,13.096L22.419,4.799" style="fill:none;fill-rule:nonzero;stroke:rgb(126,129,130);stroke-width:0.75px;"/>
+ <path d="M5.234,28.221C5.083,26.219 6.585,24.474 8.588,24.324C10.591,24.171 12.337,25.675 12.486,27.677C12.636,29.679 11.136,31.425 9.133,31.575C7.13,31.726 5.384,30.224 5.234,28.221" style="fill:rgb(126,129,130);fill-rule:nonzero;"/>
+ <path d="M21.246,24.377C21.317,23.756 21.878,23.312 22.5,23.383C23.121,23.455 23.565,24.017 23.494,24.637C23.42,25.258 22.86,25.702 22.24,25.631C21.619,25.56 21.174,24.998 21.246,24.377M7.546,9.604C7.155,8.805 7.486,7.839 8.286,7.449C9.085,7.057 10.05,7.389 10.442,8.188C10.832,8.987 10.502,9.953 9.702,10.343C8.902,10.734 7.938,10.402 7.546,9.604M31.367,12.036C31.275,10.821 32.186,9.762 33.403,9.669C34.618,9.578 35.678,10.489 35.77,11.705C35.861,12.921 34.95,13.98 33.733,14.072C32.518,14.163 31.458,13.252 31.367,12.036M21.386,0.631C22.588,-0.348 24.359,-0.168 25.34,1.034C26.32,2.236 26.139,4.008 24.937,4.988C23.734,5.967 21.963,5.786 20.984,4.584C20.002,3.381 20.184,1.611 21.386,0.631M13.79,11.561C16.099,9.679 19.499,10.025 21.384,12.334C23.264,14.644 22.918,18.043 20.61,19.928C18.298,21.808 14.899,21.463 13.018,19.152C11.132,16.843 11.481,13.442 13.79,11.561" style="fill:rgb(126,129,130);fill-rule:nonzero;"/>
+ </g>
+</svg>
diff --git a/public/img/icinga-logo-compact.svg b/public/img/icinga-logo-compact.svg
new file mode 100644
index 0000000..8b6a531
--- /dev/null
+++ b/public/img/icinga-logo-compact.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="41px" height="35px" viewBox="0 82.75 41 35" enable-background="new 0 82.75 41 35" xml:space="preserve">
+<g>
+ <path fill="none" stroke="#FFFFFF" stroke-width="0.75" d="M17.444,98.987l-7.967,11.424 M17.444,98.987l16.513-4.073
+ M17.757,99.322l4.624,7.724 M15.795,97.074L9.01,91.52 M18.425,95.846l3.994-8.297"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="5.224" y="107.062" width="7.272" height="7.272">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="5.224" y="107.062" width="7.272" height="7.272" id="f_1_">
+ <g filter="url(#Adobe_OpacityMaskFilter)">
+ <polygon id="e_1_" fill="#FFFFFF" points="5.224,107.062 5.224,114.335 12.497,114.335 12.497,107.062 "/>
+ </g>
+ </mask>
+ <path mask="url(#f_1_)" fill="#FFFFFF" d="M5.234,110.971c-0.151-2.002,1.351-3.747,3.354-3.897
+ c2.003-0.153,3.749,1.351,3.898,3.353c0.15,2.002-1.35,3.748-3.353,3.898C7.13,114.476,5.384,112.974,5.234,110.971"/>
+ <path fill="#FFFFFF" d="M21.246,107.127c0.071-0.621,0.632-1.065,1.254-0.994c0.621,0.072,1.065,0.634,0.994,1.254
+ c-0.074,0.621-0.634,1.065-1.254,0.994C21.619,108.31,21.174,107.748,21.246,107.127 M7.546,92.354
+ c-0.391-0.799-0.06-1.765,0.74-2.155c0.799-0.392,1.764-0.06,2.156,0.739c0.39,0.799,0.06,1.765-0.74,2.155
+ C8.902,93.484,7.938,93.152,7.546,92.354 M31.367,94.786c-0.092-1.215,0.819-2.274,2.036-2.367c1.215-0.091,2.275,0.82,2.367,2.036
+ c0.091,1.216-0.82,2.275-2.037,2.367C32.518,96.913,31.458,96.002,31.367,94.786 M21.386,83.381
+ c1.202-0.979,2.973-0.799,3.954,0.403c0.98,1.202,0.799,2.974-0.403,3.954c-1.203,0.979-2.974,0.798-3.953-0.404
+ C20.002,86.131,20.184,84.361,21.386,83.381 M13.79,94.311c2.309-1.882,5.709-1.536,7.594,0.773c1.88,2.31,1.534,5.709-0.774,7.594
+ c-2.312,1.88-5.711,1.535-7.592-0.776C11.132,99.593,11.481,96.192,13.79,94.311"/>
+</g>
+</svg>
diff --git a/public/img/icinga-logo-dark.svg b/public/img/icinga-logo-dark.svg
new file mode 100644
index 0000000..44be390
--- /dev/null
+++ b/public/img/icinga-logo-dark.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 101 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
+ <g transform="matrix(1,0,0,1,-327,0)">
+ <g id="icinga-logo-dark" transform="matrix(1,0,0,1,327.107,0)">
+ <rect x="0" y="0" width="100" height="35" style="fill:none;"/>
+ <clipPath id="_clip1">
+ <rect x="0" y="0" width="100" height="35"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g>
+ <path d="M26.85,32.573C26.85,32.975 27.011,33.324 27.306,33.619C27.6,33.914 27.922,34.048 28.325,34.048C28.727,34.048 29.075,33.914 29.371,33.619C29.665,33.324 29.8,32.975 29.8,32.573L29.8,17.825C29.8,17.423 29.665,17.074 29.371,16.779C29.075,16.484 28.727,16.35 28.325,16.35C27.922,16.35 27.574,16.484 27.279,16.779C26.984,17.074 26.85,17.423 26.85,17.825L26.85,32.573Z" style="fill:rgb(51,41,49);"/>
+ <g transform="matrix(1,0,0,1,32,15.61)">
+ <path d="M0.535,12.484C0.535,13.369 0.723,14.227 1.072,15.004C1.42,15.782 1.876,16.479 2.466,17.069C3.056,17.659 3.753,18.115 4.557,18.463C5.335,18.812 6.193,19 7.077,19C7.775,19 8.445,18.892 9.062,18.705C9.679,18.517 10.268,18.222 10.778,17.847C11.288,17.498 11.77,17.069 12.173,16.56C12.575,16.05 12.87,15.514 13.111,14.897C13.191,14.736 13.218,14.548 13.218,14.334C13.218,13.985 13.084,13.637 12.843,13.342C12.575,13.047 12.226,12.886 11.77,12.886C11.529,12.886 11.288,12.966 11.046,13.128C10.778,13.288 10.59,13.503 10.456,13.744C10.161,14.414 9.733,14.951 9.143,15.38C8.526,15.836 7.855,16.05 7.077,16.05C6.595,16.05 6.139,15.942 5.71,15.755C5.254,15.567 4.879,15.326 4.557,15.004C4.209,14.683 3.941,14.307 3.753,13.851C3.565,13.422 3.458,12.966 3.458,12.484L3.458,6.665C3.458,6.182 3.565,5.727 3.753,5.271C3.941,4.842 4.182,4.466 4.531,4.144C4.852,3.822 5.227,3.554 5.683,3.367C6.112,3.179 6.568,3.099 7.077,3.099C7.855,3.099 8.526,3.314 9.143,3.742C9.733,4.171 10.161,4.707 10.43,5.378C10.724,5.968 11.154,6.263 11.77,6.263C12.226,6.263 12.601,6.102 12.843,5.807C13.084,5.512 13.218,5.163 13.218,4.815C13.218,4.707 13.218,4.573 13.191,4.439C13.165,4.305 13.111,4.171 13.031,4.037C12.789,3.474 12.468,2.938 12.065,2.455C11.663,1.999 11.181,1.57 10.671,1.221C10.161,0.873 9.598,0.605 8.982,0.417C8.365,0.23 7.748,0.149 7.077,0.149C6.166,0.149 5.308,0.31 4.531,0.659C3.726,1.007 3.029,1.463 2.439,2.053C1.849,2.643 1.393,3.34 1.045,4.117C0.696,4.922 0.535,5.78 0.535,6.665L0.535,12.484Z" style="fill:rgb(51,41,49);"/>
+ </g>
+ <path d="M48.06,32.573C48.06,32.975 48.221,33.324 48.516,33.619C48.811,33.914 49.133,34.048 49.535,34.048C49.937,34.048 50.286,33.914 50.581,33.619C50.875,33.324 51.01,32.975 51.01,32.573L51.01,17.825C51.01,17.423 50.875,17.074 50.581,16.779C50.286,16.484 49.937,16.35 49.535,16.35C49.133,16.35 48.784,16.484 48.489,16.779C48.194,17.074 48.06,17.423 48.06,17.825L48.06,32.573ZM53.745,32.573C53.745,32.975 53.879,33.324 54.174,33.619C54.469,33.914 54.818,34.048 55.22,34.048C55.622,34.048 55.971,33.914 56.266,33.619C56.534,33.324 56.668,32.975 56.668,32.573L56.668,22.303C56.668,21.82 56.775,21.365 56.963,20.909C57.151,20.48 57.392,20.105 57.714,19.782C58.036,19.461 58.411,19.193 58.867,19.005C59.296,18.817 59.751,18.737 60.261,18.737C60.77,18.737 61.226,18.817 61.682,19.005C62.111,19.193 62.487,19.461 62.835,19.782C63.157,20.105 63.425,20.48 63.613,20.909C63.8,21.365 63.881,21.82 63.881,22.303L63.881,32.573C63.881,32.975 64.042,33.324 64.337,33.619C64.632,33.914 64.953,34.048 65.356,34.048C65.758,34.048 66.106,33.914 66.401,33.619C66.669,33.324 66.804,32.975 66.804,32.573L66.804,22.303C66.804,21.418 66.643,20.56 66.294,19.782C65.946,19.005 65.463,18.308 64.873,17.718C64.283,17.128 63.586,16.645 62.808,16.297C62.004,15.948 61.146,15.787 60.261,15.787C59.376,15.787 58.518,15.948 57.741,16.297C56.936,16.645 56.239,17.128 55.649,17.718C55.059,18.308 54.603,19.005 54.255,19.782C53.906,20.56 53.745,21.418 53.745,22.303L53.745,32.573Z" style="fill:rgb(51,41,49);"/>
+ <g transform="matrix(1,0,0,1,69,15.61)">
+ <path d="M0.726,12.484C0.726,13.369 0.913,14.227 1.262,15.004C1.611,15.782 2.067,16.479 2.657,17.069C3.247,17.659 3.944,18.115 4.748,18.463C5.525,18.812 6.384,19 7.268,19C8.154,19 9.012,18.812 9.816,18.463C10.594,18.115 11.291,17.659 11.881,17.069C12.471,16.479 12.954,15.782 13.302,15.004C13.65,14.227 13.811,13.369 13.811,12.484L13.811,10.285C13.811,9.883 13.677,9.534 13.382,9.239C13.087,8.971 12.739,8.837 12.337,8.837L9.414,8.837C9.012,8.837 8.663,8.971 8.395,9.266C8.1,9.561 7.966,9.883 7.966,10.285C7.966,10.687 8.1,11.036 8.395,11.331C8.663,11.626 9.012,11.76 9.414,11.76L10.862,11.76L10.862,12.484C10.862,12.966 10.781,13.422 10.594,13.851C10.406,14.307 10.138,14.683 9.816,15.004C9.495,15.326 9.119,15.567 8.69,15.755C8.234,15.942 7.778,16.05 7.268,16.05C6.786,16.05 6.33,15.942 5.901,15.755C5.445,15.567 5.07,15.326 4.748,15.004C4.4,14.683 4.158,14.307 3.971,13.851C3.783,13.422 3.676,12.966 3.676,12.484L3.676,6.692C3.676,6.209 3.783,5.754 3.971,5.297C4.158,4.868 4.4,4.493 4.722,4.171C5.043,3.849 5.418,3.581 5.874,3.394C6.303,3.206 6.759,3.126 7.268,3.126C8.046,3.126 8.717,3.34 9.333,3.769C9.923,4.198 10.352,4.761 10.647,5.431C10.781,5.727 10.969,5.941 11.21,6.075C11.452,6.209 11.72,6.29 11.961,6.29C12.417,6.29 12.792,6.129 13.034,5.834C13.275,5.539 13.409,5.19 13.409,4.842L13.409,4.52C13.382,4.439 13.355,4.332 13.329,4.252C13.087,3.635 12.766,3.099 12.364,2.589C11.961,2.079 11.479,1.651 10.969,1.302C10.433,0.954 9.843,0.659 9.226,0.471C8.609,0.283 7.939,0.176 7.268,0.176C6.384,0.176 5.525,0.337 4.748,0.686C3.944,1.034 3.247,1.489 2.657,2.079C2.067,2.67 1.611,3.367 1.262,4.144C0.913,4.949 0.726,5.807 0.726,6.692L0.726,12.484Z" style="fill:rgb(51,41,49);"/>
+ </g>
+ <path d="M94.289,26.352L89.972,26.352C90.159,25.896 90.347,25.44 90.535,25.038C90.723,24.636 90.883,24.234 91.071,23.831C91.259,23.429 91.419,23.027 91.607,22.624C91.768,22.223 91.956,21.82 92.143,21.391L94.289,26.352ZM84.421,32.063C84.394,32.144 84.367,32.225 84.34,32.331C84.313,32.438 84.313,32.546 84.313,32.626C84.313,32.975 84.448,33.297 84.716,33.592C84.957,33.914 85.306,34.075 85.762,34.075C86.057,34.075 86.298,33.994 86.539,33.86C86.781,33.726 86.968,33.485 87.102,33.19C87.371,32.519 87.639,31.876 87.934,31.232C88.202,30.615 88.47,29.972 88.765,29.302L95.549,29.302L97.212,33.19C97.453,33.78 97.882,34.075 98.552,34.075C99.008,34.075 99.357,33.914 99.625,33.592C99.866,33.297 100,32.975 100,32.626C100,32.466 99.947,32.278 99.866,32.063L93.484,17.127C93.351,16.806 93.082,16.565 92.706,16.377C92.626,16.323 92.519,16.297 92.439,16.27C92.331,16.243 92.224,16.243 92.143,16.243C91.527,16.243 91.098,16.538 90.803,17.127L84.421,32.063Z" style="fill:rgb(51,41,49);"/>
+ <path d="M12.22,16.237L4.253,27.661M12.22,16.237L28.733,12.164M12.533,16.572L17.157,24.296M10.571,14.325L3.786,8.77M13.201,13.096L17.195,4.798" style="fill:none;stroke:rgb(51,41,49);stroke-width:0.75px;"/>
+ <g transform="matrix(1,0,0,1,0,23.61)">
+ <path d="M0.01,4.61C-0.141,2.608 1.361,0.863 3.364,0.713C5.367,0.56 7.113,2.063 7.262,4.065C7.412,6.068 5.912,7.814 3.909,7.964C1.906,8.115 0.16,6.613 0.01,4.61" style="fill:rgb(51,41,49);"/>
+ </g>
+ <path d="M16.022,24.376C16.093,23.756 16.654,23.311 17.276,23.382C17.897,23.454 18.341,24.016 18.27,24.636C18.196,25.257 17.636,25.702 17.016,25.631C16.395,25.559 15.95,24.998 16.022,24.376M2.322,9.603C1.931,8.805 2.262,7.839 3.062,7.448C3.861,7.057 4.826,7.388 5.218,8.187C5.608,8.986 5.278,9.952 4.478,10.343C3.678,10.734 2.713,10.403 2.322,9.603M26.143,12.036C26.051,10.821 26.962,9.761 28.179,9.669C29.394,9.578 30.454,10.489 30.546,11.705C30.637,12.921 29.726,13.981 28.509,14.072C27.294,14.163 26.234,13.252 26.143,12.036M16.162,0.631C17.364,-0.349 19.135,-0.168 20.116,1.034C21.096,2.236 20.915,4.007 19.713,4.988C18.51,5.966 16.739,5.786 15.76,4.584C14.778,3.381 14.96,1.611 16.162,0.631M8.566,11.561C10.875,9.679 14.275,10.025 16.16,12.335C18.04,14.644 17.694,18.044 15.386,19.927C13.074,21.808 9.675,21.462 7.794,19.152C5.908,16.843 6.257,13.443 8.566,11.561" style="fill:rgb(51,41,49);"/>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/icinga-logo-inverted.svg b/public/img/icinga-logo-inverted.svg
new file mode 100644
index 0000000..ec918e3
--- /dev/null
+++ b/public/img/icinga-logo-inverted.svg
@@ -0,0 +1 @@
+<svg height="35" viewBox="0 0 100 35" width="100" xmlns="http://www.w3.org/2000/svg"><path d="m85.0767 33.7594c.671 0 1.288.081 1.905.268.616.188 1.179.456 1.689.804.51.349.992.778 1.394 1.234.403.483.724 1.019.966 1.582.08.134.134.268.16.402.027.134.027.268.027.376 0 .348-.134.697-.375.992-.242.295-.617.456-1.073.456-.616 0-1.046-.295-1.34-.885-.269-.671-.697-1.207-1.287-1.636-.617-.428-1.288-.643-2.066-.643-.509 0-.965.08-1.394.268-.456.187-.831.455-1.152.777-.349.322-.59.698-.778 1.127-.1566667.38-.2570833.7593056-.2862037 1.1546991l-.0087963.2393009v5.819c0 .482.107.938.295 1.367.188.456.456.832.804 1.153.322.322.697.563 1.153.751.429.187.885.295 1.367.295.778 0 1.449-.214 2.066-.67.59-.429 1.018-.966 1.313-1.636.134-.241.322-.456.59-.616.242-.162.483-.242.724-.242.456 0 .805.161 1.073.456.241.295.375.643.375.992 0 .1605-.0151875.306375-.0565312.4372031l-.0504688.1257969c-.241.617-.536 1.153-.938 1.663-.403.509-.885.938-1.395 1.287-.51.375-1.099.67-1.716.858-.617.187-1.287.295-1.985.295-.884 0-1.742-.188-2.52-.537-.804-.348-1.501-.804-2.091-1.394s-1.046-1.287-1.394-2.065c-.305375-.679875-.4874844-1.4217656-.5282402-2.1894961l-.0087598-.3305039v-5.819c0-.885.161-1.743.51-2.548.348-.777.804-1.474 1.394-2.064s1.287-1.046 2.092-1.394c.777-.349 1.635-.51 2.546-.51zm37.1914.027c.671 0 1.341.107 1.958.295s1.207.483 1.743.831c.51.349.992.777 1.395 1.287.402.51.723 1.046.965 1.663l.08.268v.322c0 .348-.134.697-.375.992-.242.295-.617.456-1.073.456-.241 0-.509-.081-.751-.215-.241-.134-.429-.348-.563-.644-.295-.67-.724-1.233-1.314-1.662-.616-.429-1.287-.643-2.065-.643-.509 0-.965.08-1.394.268-.456.187-.831.455-1.152.777-.322.322-.564.697-.751 1.126-.156667.3808333-.257083.7602778-.286204 1.1556944l-.008796.2393056v5.792c0 .482.107.938.295 1.367.187.456.429.832.777 1.153.322.322.697.563 1.153.751.429.187.885.295 1.367.295.51 0 .966-.108 1.422-.295.429-.188.805-.429 1.126-.751.322-.321.59-.697.778-1.153.155833-.3575.238056-.73375.261134-1.1281713l.006866-.2388287v-.724h-1.448c-.402 0-.751-.134-1.019-.429-.295-.295-.429-.644-.429-1.046s.134-.724.429-1.019c.2345-.258125.531016-.3929844.870789-.422666l.148211-.006334h2.923c.402 0 .75.134 1.045.402.258125.258125.392984.5575938.422666.8977363l.006334.1482637v2.199c0 .885-.161 1.743-.509 2.52-.348.778-.831 1.475-1.421 2.065s-1.287 1.046-2.065 1.394c-.804.349-1.662.537-2.548.537-.884 0-1.743-.188-2.52-.537-.804-.348-1.501-.804-2.091-1.394s-1.046-1.287-1.395-2.065c-.305375-.679875-.486719-1.4217656-.527283-2.1894961l-.008717-.3305039v-5.792c0-.885.187-1.743.536-2.548.349-.777.805-1.474 1.395-2.065.59-.59 1.287-1.045 2.091-1.393.777-.349 1.636-.51 2.52-.51zm15.8745.4566c.081 0 .188 0 .296.027.053333.018.118667.0355556.18.0612593l.087.0457407c.322286.1611429.565959.3612245.712758.6172478l.065242.1327522 6.382 14.936c.081.215.134.403.134.563 0 .349-.134.671-.375.966-.268.322-.617.483-1.073.483-.614167 0-1.025826-.2478819-1.275803-.7436458l-.064197-.1413542-1.663-3.888h-6.784c-.295.67-.563 1.313-.831 1.93-.295.644-.563 1.287-.832 1.958-.134.295-.321.536-.563.67-.241.134-.482.215-.777.215-.399 0-.716078-.1232656-.950564-.3697969l-.095436-.1132031c-.268-.295-.403-.617-.403-.966 0-.08 0-.188.027-.295l.027-.0985926.054-.1694074 6.382-14.936c.295-.589.724-.884 1.34-.884zm-63.8175.1067c.402 0 .75.134 1.046.429.25725.258125.3927656.5575938.422625.8977363l.006375.1482637v14.748c0 .402-.135.751-.429 1.046-.296.295-.644.429-1.046.429-.403 0-.725-.134-1.019-.429-.258125-.258125-.4136562-.5575937-.4485059-.8977363l-.0074941-.1482637v-14.748c0-.402.134-.751.429-1.046s.643-.429 1.046-.429zm21.21 0c.402 0 .751.134 1.046.429.25725.258125.3927656.5575938.422625.8977363l.006375.1482637v14.748c0 .402-.135.751-.429 1.046-.295.295-.644.429-1.046.429s-.724-.134-1.019-.429c-.258125-.258125-.4136562-.5575937-.4485059-.8977363l-.0074941-.1482637v-14.748c0-.402.134-.751.429-1.046s.644-.429 1.046-.429zm10.7255-.563c.885 0 1.743.161 2.547.51.778.348 1.475.831 2.065 1.421s1.073 1.287 1.421 2.064c.305375.68075.466813 1.42275.5024 2.1904941l.0076.3305059v10.27c0 .402-.135.751-.403 1.046-.295.295-.643.429-1.045.429-.403 0-.724-.134-1.019-.429-.258125-.258125-.413656-.5575937-.448506-.8977363l-.007494-.1482637v-10.27c0-.483-.081-.938-.268-1.394-.188-.429-.456-.804-.778-1.127-.348-.321-.724-.589-1.153-.777-.456-.188-.912-.268-1.421-.268-.51 0-.965.08-1.394.268-.456.188-.831.456-1.153.777-.322.323-.563.698-.751 1.127-.156667.38-.257083.7593056-.286204 1.1546991l-.008796.2393009v10.27c0 .402-.134.751-.402 1.046-.295.295-.644.429-1.046.429s-.751-.134-1.046-.429c-.258125-.258125-.3929844-.5575937-.422666-.8977363l-.006334-.1482637v-10.27c0-.885.161-1.743.51-2.521.348-.777.804-1.474 1.394-2.064s1.287-1.073 2.092-1.421c.777-.349 1.635-.51 2.52-.51zm-40.145-14.7524c.98 1.202.799 2.973-.403 3.954-.7135427.5800871-1.6269138.7527777-2.4563199.5464512l-2.6461727 5.4979481c.5779292.3227096 1.1058725.7577926 1.5497926 1.3018007.5578806.6851842.919745 1.4664392 1.0924916 2.2756424l8.9486237-2.2073659c-.0292334-.1184503-.0490052-.2409327-.0584153-.3666765-.092-1.215.819-2.275 2.036-2.367 1.215-.091 2.275.82 2.367 2.036.091 1.216-.82 2.276-2.037 2.367-.8211665.061503-1.5715316-.3346905-1.9997973-.9734027l-9.1509619 2.2572841c.1199705 1.6931832-.5575939 3.4169998-1.9719408 4.5715186-.0916538.0745282-.1850159.1455597-.279924.2131107l1.9393556 3.2387843c.0753599-.0066904.1525329-.005914.2306684.003005.621.072 1.065.634.994 1.254-.074.621-.634 1.066-1.254.995-.621-.072-1.066-.633-.994-1.255.0309663-.2704096.1551414-.5075304.3369336-.6830715l-1.8928981-3.1609715c-1.5260558.7939481-3.3251285.7992664-4.8352112.0718318l-3.3672948 4.8286847c.5634239.5877707.9336449 1.3674834.9987705 2.2425265.15 2.003-1.35 3.749-3.353 3.899-2.003.151-3.749-1.351-3.899-3.354-.151-2.002 1.351-3.747 3.354-3.897.8569941-.0654619 1.6669418.1722239 2.3248898.6224372l3.2886724-4.7148027c-.4356465-.2913976-.8358066-.6517899-1.1839622-1.0795345-1.6157836-1.9781784-1.5911265-4.7571267-.0989533-6.6962604l-2.8636784-2.3463302c-.1044197.0911892-.2224378.1702938-.3529683.2340906-.8.391-1.765.06-2.156-.74-.391-.798-.06-1.764.74-2.155.799-.391 1.764-.06 2.156.739.2078646.4258559.2111957.8991522.0478454 1.3075444l2.9271152 2.3991038c.1179203-.1155836.2421499-.22679.3726394-.3331482 1.5583374-1.2701564 3.6136103-1.5254897 5.3647177-.8471552l2.6265341-5.4562041c-.2974624-.1674645-.5690752-.392321-.7975518-.6728407-.982-1.203-.8-2.973.402-3.953s2.973-.799 3.954.403zm74.173 25.3177-2.146-4.961-.367037.8304444c-.05863.135-.115296.2688889-.168963.4025556l-1.072 2.414c-.094.201-.188.4155-.281875.63675l-.281125.67725z" fill="#7e8182" transform="translate(-46 -18)"/></svg> \ No newline at end of file
diff --git a/public/img/icinga-logo.png b/public/img/icinga-logo.png
new file mode 100644
index 0000000..670f45c
--- /dev/null
+++ b/public/img/icinga-logo.png
Binary files differ
diff --git a/public/img/icinga-logo.svg b/public/img/icinga-logo.svg
new file mode 100644
index 0000000..49a2ef5
--- /dev/null
+++ b/public/img/icinga-logo.svg
@@ -0,0 +1,32 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="35" viewBox="0 0 100 35" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <polygon id="a" points=".535 19 13.218 19 13.218 .149 .535 .149"/>
+ <polygon id="c" points=".726 19 13.811 19 13.811 .176 .726 .176 .726 19"/>
+ <polygon id="e" points="0 .703 0 7.975 7.273 7.975 7.273 .703 0 .703"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#FFFFFF" d="M26.8501,32.5727 C26.8501,32.9747 27.0111,33.3237 27.3061,33.6187 C27.6001,33.9137 27.9221,34.0477 28.3251,34.0477 C28.7271,34.0477 29.0751,33.9137 29.3711,33.6187 C29.6651,33.3237 29.8001,32.9747 29.8001,32.5727 L29.8001,17.8247 C29.8001,17.4227 29.6651,17.0737 29.3711,16.7787 C29.0751,16.4837 28.7271,16.3497 28.3251,16.3497 C27.9221,16.3497 27.5741,16.4837 27.2791,16.7787 C26.9841,17.0737 26.8501,17.4227 26.8501,17.8247 L26.8501,32.5727 Z"/>
+ <g transform="translate(32 15.61)">
+ <mask id="b" fill="white">
+ <use xlink:href="#a"/>
+ </mask>
+ <path fill="#FFFFFF" d="M0.5347,12.4844 C0.5347,13.3694 0.7227,14.2274 1.0717,15.0044 C1.4197,15.7824 1.8757,16.4794 2.4657,17.0694 C3.0557,17.6594 3.7527,18.1154 4.5567,18.4634 C5.3347,18.8124 6.1927,19.0004 7.0767,19.0004 C7.7747,19.0004 8.4447,18.8924 9.0617,18.7054 C9.6787,18.5174 10.2677,18.2224 10.7777,17.8474 C11.2877,17.4984 11.7697,17.0694 12.1727,16.5604 C12.5747,16.0504 12.8697,15.5144 13.1107,14.8974 C13.1907,14.7364 13.2177,14.5484 13.2177,14.3344 C13.2177,13.9854 13.0837,13.6374 12.8427,13.3424 C12.5747,13.0474 12.2257,12.8864 11.7697,12.8864 C11.5287,12.8864 11.2877,12.9664 11.0457,13.1284 C10.7777,13.2884 10.5897,13.5034 10.4557,13.7444 C10.1607,14.4144 9.7327,14.9514 9.1427,15.3804 C8.5257,15.8364 7.8547,16.0504 7.0767,16.0504 C6.5947,16.0504 6.1387,15.9424 5.7097,15.7554 C5.2537,15.5674 4.8787,15.3264 4.5567,15.0044 C4.2087,14.6834 3.9407,14.3074 3.7527,13.8514 C3.5647,13.4224 3.4577,12.9664 3.4577,12.4844 L3.4577,6.6654 C3.4577,6.1824 3.5647,5.7274 3.7527,5.2714 C3.9407,4.8424 4.1817,4.4664 4.5307,4.1444 C4.8517,3.8224 5.2267,3.5544 5.6827,3.3674 C6.1117,3.1794 6.5677,3.0994 7.0767,3.0994 C7.8547,3.0994 8.5257,3.3144 9.1427,3.7424 C9.7327,4.1714 10.1607,4.7074 10.4297,5.3784 C10.7237,5.9684 11.1537,6.2634 11.7697,6.2634 C12.2257,6.2634 12.6007,6.1024 12.8427,5.8074 C13.0837,5.5124 13.2177,5.1634 13.2177,4.8154 C13.2177,4.7074 13.2177,4.5734 13.1907,4.4394 C13.1647,4.3054 13.1107,4.1714 13.0307,4.0374 C12.7887,3.4744 12.4677,2.9384 12.0647,2.4554 C11.6627,1.9994 11.1807,1.5704 10.6707,1.2214 C10.1607,0.8734 9.5977,0.6054 8.9817,0.4174 C8.3647,0.2304 7.7477,0.1494 7.0767,0.1494 C6.1657,0.1494 5.3077,0.3104 4.5307,0.6594 C3.7257,1.0074 3.0287,1.4634 2.4387,2.0534 C1.8487,2.6434 1.3927,3.3404 1.0447,4.1174 C0.6957,4.9224 0.5347,5.7804 0.5347,6.6654 L0.5347,12.4844 Z" mask="url(#b)"/>
+ </g>
+ <path fill="#FFFFFF" d="M48.0601 32.5727C48.0601 32.9747 48.2211 33.3237 48.5161 33.6187 48.8111 33.9137 49.1331 34.0477 49.5351 34.0477 49.9371 34.0477 50.2861 33.9137 50.5811 33.6187 50.8751 33.3237 51.0101 32.9747 51.0101 32.5727L51.0101 17.8247C51.0101 17.4227 50.8751 17.0737 50.5811 16.7787 50.2861 16.4837 49.9371 16.3497 49.5351 16.3497 49.1331 16.3497 48.7841 16.4837 48.4891 16.7787 48.1941 17.0737 48.0601 17.4227 48.0601 17.8247L48.0601 32.5727zM53.7446 32.5727C53.7446 32.9747 53.8786 33.3237 54.1736 33.6187 54.4686 33.9137 54.8176 34.0477 55.2196 34.0477 55.6216 34.0477 55.9706 33.9137 56.2656 33.6187 56.5336 33.3237 56.6676 32.9747 56.6676 32.5727L56.6676 22.3027C56.6676 21.8197 56.7746 21.3647 56.9626 20.9087 57.1506 20.4797 57.3916 20.1047 57.7136 19.7817 58.0356 19.4607 58.4106 19.1927 58.8666 19.0047 59.2956 18.8167 59.7506 18.7367 60.2606 18.7367 60.7696 18.7367 61.2256 18.8167 61.6816 19.0047 62.1106 19.1927 62.4866 19.4607 62.8346 19.7817 63.1566 20.1047 63.4246 20.4797 63.6126 20.9087 63.7996 21.3647 63.8806 21.8197 63.8806 22.3027L63.8806 32.5727C63.8806 32.9747 64.0416 33.3237 64.3366 33.6187 64.6316 33.9137 64.9526 34.0477 65.3556 34.0477 65.7576 34.0477 66.1056 33.9137 66.4006 33.6187 66.6686 33.3237 66.8036 32.9747 66.8036 32.5727L66.8036 22.3027C66.8036 21.4177 66.6426 20.5597 66.2936 19.7817 65.9456 19.0047 65.4626 18.3077 64.8726 17.7177 64.2826 17.1277 63.5856 16.6447 62.8076 16.2967 62.0036 15.9477 61.1456 15.7867 60.2606 15.7867 59.3756 15.7867 58.5176 15.9477 57.7406 16.2967 56.9356 16.6447 56.2386 17.1277 55.6486 17.7177 55.0586 18.3077 54.6026 19.0047 54.2546 19.7817 53.9056 20.5597 53.7446 21.4177 53.7446 22.3027L53.7446 32.5727z"/>
+ <g transform="translate(69 15.61)">
+ <mask id="d" fill="white">
+ <use xlink:href="#c"/>
+ </mask>
+ <path fill="#FFFFFF" d="M0.7261,12.4844 C0.7261,13.3694 0.9131,14.2274 1.2621,15.0044 C1.6111,15.7824 2.0671,16.4794 2.6571,17.0694 C3.2471,17.6594 3.9441,18.1154 4.7481,18.4634 C5.5251,18.8124 6.3841,19.0004 7.2681,19.0004 C8.1541,19.0004 9.0121,18.8124 9.8161,18.4634 C10.5941,18.1154 11.2911,17.6594 11.8811,17.0694 C12.4711,16.4794 12.9541,15.7824 13.3021,15.0044 C13.6501,14.2274 13.8111,13.3694 13.8111,12.4844 L13.8111,10.2854 C13.8111,9.8834 13.6771,9.5344 13.3821,9.2394 C13.0871,8.9714 12.7391,8.8374 12.3371,8.8374 L9.4141,8.8374 C9.0121,8.8374 8.6631,8.9714 8.3951,9.2664 C8.1001,9.5614 7.9661,9.8834 7.9661,10.2854 C7.9661,10.6874 8.1001,11.0364 8.3951,11.3314 C8.6631,11.6264 9.0121,11.7604 9.4141,11.7604 L10.8621,11.7604 L10.8621,12.4844 C10.8621,12.9664 10.7811,13.4224 10.5941,13.8514 C10.4061,14.3074 10.1381,14.6834 9.8161,15.0044 C9.4951,15.3264 9.1191,15.5674 8.6901,15.7554 C8.2341,15.9424 7.7781,16.0504 7.2681,16.0504 C6.7861,16.0504 6.3301,15.9424 5.9011,15.7554 C5.4451,15.5674 5.0701,15.3264 4.7481,15.0044 C4.4001,14.6834 4.1581,14.3074 3.9711,13.8514 C3.7831,13.4224 3.6761,12.9664 3.6761,12.4844 L3.6761,6.6924 C3.6761,6.2094 3.7831,5.7544 3.9711,5.2974 C4.1581,4.8684 4.4001,4.4934 4.7221,4.1714 C5.0431,3.8494 5.4181,3.5814 5.8741,3.3944 C6.3031,3.2064 6.7591,3.1264 7.2681,3.1264 C8.0461,3.1264 8.7171,3.3404 9.3331,3.7694 C9.9231,4.1984 10.3521,4.7614 10.6471,5.4314 C10.7811,5.7274 10.9691,5.9414 11.2101,6.0754 C11.4521,6.2094 11.7201,6.2904 11.9611,6.2904 C12.4171,6.2904 12.7921,6.1294 13.0341,5.8344 C13.2751,5.5394 13.4091,5.1904 13.4091,4.8424 L13.4091,4.5204 C13.3821,4.4394 13.3551,4.3324 13.3291,4.2524 C13.0871,3.6354 12.7661,3.0994 12.3641,2.5894 C11.9611,2.0794 11.4791,1.6514 10.9691,1.3024 C10.4331,0.9544 9.8431,0.6594 9.2261,0.4714 C8.6091,0.2834 7.9391,0.1764 7.2681,0.1764 C6.3841,0.1764 5.5251,0.3374 4.7481,0.6864 C3.9441,1.0344 3.2471,1.4894 2.6571,2.0794 C2.0671,2.6704 1.6111,3.3674 1.2621,4.1444 C0.9131,4.9494 0.7261,5.8074 0.7261,6.6924 L0.7261,12.4844 Z" mask="url(#d)"/>
+ </g>
+ <path fill="#FFFFFF" d="M94.2886,26.352 L89.9716,26.352 C90.1586,25.896 90.3466,25.44 90.5346,25.038 C90.7226,24.636 90.8826,24.234 91.0706,23.831 C91.2586,23.429 91.4186,23.027 91.6066,22.624 C91.7676,22.223 91.9556,21.82 92.1426,21.391 L94.2886,26.352 Z M84.4206,32.063 C84.3936,32.144 84.3666,32.225 84.3396,32.331 C84.3126,32.438 84.3126,32.546 84.3126,32.626 C84.3126,32.975 84.4476,33.297 84.7156,33.592 C84.9566,33.914 85.3056,34.075 85.7616,34.075 C86.0566,34.075 86.2976,33.994 86.5386,33.86 C86.7806,33.726 86.9676,33.485 87.1016,33.19 C87.3706,32.519 87.6386,31.876 87.9336,31.232 C88.2016,30.615 88.4696,29.972 88.7646,29.302 L95.5486,29.302 L97.2116,33.19 C97.4526,33.78 97.8816,34.075 98.5516,34.075 C99.0076,34.075 99.3566,33.914 99.6246,33.592 C99.8656,33.297 99.9996,32.975 99.9996,32.626 C99.9996,32.466 99.9466,32.278 99.8656,32.063 L93.4836,17.127 C93.3506,16.806 93.0816,16.565 92.7056,16.377 C92.6256,16.323 92.5186,16.297 92.4386,16.27 C92.3306,16.243 92.2236,16.243 92.1426,16.243 C91.5266,16.243 91.0976,16.538 90.8026,17.127 L84.4206,32.063 Z"/>
+ <path stroke="#FFFFFF" stroke-width=".75" d="M12.2197 16.2373L4.2527 27.6613M12.2197 16.2373L28.7327 12.1643M12.5327 16.5717L17.1567 24.2957M10.5713 14.3247L3.7863 8.7697M13.2007 13.0962L17.1947 4.7982"/>
+ <g transform="translate(0 23.61)">
+ <mask id="f" fill="white">
+ <use xlink:href="#e"/>
+ </mask>
+ <path fill="#FFFFFF" d="M0.0103,4.6104 C-0.1407,2.6084 1.3613,0.8634 3.3643,0.7134 C5.3673,0.5604 7.1133,2.0634 7.2623,4.0654 C7.4123,6.0684 5.9123,7.8144 3.9093,7.9644 C1.9063,8.1154 0.1603,6.6134 0.0103,4.6104" mask="url(#f)"/>
+ </g>
+ <path fill="#FFFFFF" d="M16.022 24.3764C16.093 23.7564 16.654 23.3114 17.276 23.3824 17.897 23.4544 18.341 24.0164 18.27 24.6364 18.196 25.2574 17.636 25.7024 17.016 25.6314 16.395 25.5594 15.95 24.9984 16.022 24.3764M2.3223 9.603C1.9313 8.805 2.2623 7.839 3.0623 7.448 3.8613 7.057 4.8263 7.388 5.2183 8.187 5.6083 8.986 5.2783 9.952 4.4783 10.343 3.6783 10.734 2.7133 10.403 2.3223 9.603M26.1426 12.0361C26.0506 10.8211 26.9616 9.7611 28.1786 9.6691 29.3936 9.5781 30.4536 10.4891 30.5456 11.7051 30.6366 12.9211 29.7256 13.9811 28.5086 14.0721 27.2936 14.1631 26.2336 13.2521 26.1426 12.0361M16.1616.6313C17.3636-.3487 19.1346-.1677 20.1156 1.0343 21.0956 2.2363 20.9146 4.0073 19.7126 4.9883 18.5096 5.9663 16.7386 5.7863 15.7596 4.5843 14.7776 3.3813 14.9596 1.6113 16.1616.6313M8.5659 11.5605C10.8749 9.6785 14.2749 10.0245 16.1599 12.3345 18.0399 14.6435 17.6939 18.0435 15.3859 19.9275 13.0739 21.8075 9.6749 21.4625 7.7939 19.1515 5.9079 16.8425 6.2569 13.4425 8.5659 11.5605"/>
+ </g>
+</svg>
diff --git a/public/img/icingaweb2-background-orbs.jpg b/public/img/icingaweb2-background-orbs.jpg
new file mode 100644
index 0000000..bb6d40e
--- /dev/null
+++ b/public/img/icingaweb2-background-orbs.jpg
Binary files differ
diff --git a/public/img/icingaweb2-background.jpg b/public/img/icingaweb2-background.jpg
new file mode 100644
index 0000000..6a36024
--- /dev/null
+++ b/public/img/icingaweb2-background.jpg
Binary files differ
diff --git a/public/img/icons/acknowledgement.png b/public/img/icons/acknowledgement.png
new file mode 100644
index 0000000..eb03d2d
--- /dev/null
+++ b/public/img/icons/acknowledgement.png
Binary files differ
diff --git a/public/img/icons/acknowledgement_petrol.png b/public/img/icons/acknowledgement_petrol.png
new file mode 100644
index 0000000..e8345ea
--- /dev/null
+++ b/public/img/icons/acknowledgement_petrol.png
Binary files differ
diff --git a/public/img/icons/active_checks_disabled.png b/public/img/icons/active_checks_disabled.png
new file mode 100644
index 0000000..c21e445
--- /dev/null
+++ b/public/img/icons/active_checks_disabled.png
Binary files differ
diff --git a/public/img/icons/active_checks_disabled_petrol.png b/public/img/icons/active_checks_disabled_petrol.png
new file mode 100644
index 0000000..5614589
--- /dev/null
+++ b/public/img/icons/active_checks_disabled_petrol.png
Binary files differ
diff --git a/public/img/icons/active_passive_checks_disabled.png b/public/img/icons/active_passive_checks_disabled.png
new file mode 100644
index 0000000..664ecec
--- /dev/null
+++ b/public/img/icons/active_passive_checks_disabled.png
Binary files differ
diff --git a/public/img/icons/active_passive_checks_disabled_petrol.png b/public/img/icons/active_passive_checks_disabled_petrol.png
new file mode 100644
index 0000000..c82bfd9
--- /dev/null
+++ b/public/img/icons/active_passive_checks_disabled_petrol.png
Binary files differ
diff --git a/public/img/icons/comment.png b/public/img/icons/comment.png
new file mode 100644
index 0000000..b7d88a0
--- /dev/null
+++ b/public/img/icons/comment.png
Binary files differ
diff --git a/public/img/icons/comment_petrol.png b/public/img/icons/comment_petrol.png
new file mode 100644
index 0000000..53c1223
--- /dev/null
+++ b/public/img/icons/comment_petrol.png
Binary files differ
diff --git a/public/img/icons/configuration.png b/public/img/icons/configuration.png
new file mode 100644
index 0000000..fb52cd6
--- /dev/null
+++ b/public/img/icons/configuration.png
Binary files differ
diff --git a/public/img/icons/configuration_petrol.png b/public/img/icons/configuration_petrol.png
new file mode 100644
index 0000000..8168133
--- /dev/null
+++ b/public/img/icons/configuration_petrol.png
Binary files differ
diff --git a/public/img/icons/create.png b/public/img/icons/create.png
new file mode 100644
index 0000000..b9bedf8
--- /dev/null
+++ b/public/img/icons/create.png
Binary files differ
diff --git a/public/img/icons/create_petrol.png b/public/img/icons/create_petrol.png
new file mode 100644
index 0000000..4e104af
--- /dev/null
+++ b/public/img/icons/create_petrol.png
Binary files differ
diff --git a/public/img/icons/csv.png b/public/img/icons/csv.png
new file mode 100644
index 0000000..5b41917
--- /dev/null
+++ b/public/img/icons/csv.png
Binary files differ
diff --git a/public/img/icons/csv_petrol.png b/public/img/icons/csv_petrol.png
new file mode 100644
index 0000000..05d53f0
--- /dev/null
+++ b/public/img/icons/csv_petrol.png
Binary files differ
diff --git a/public/img/icons/dashboard.png b/public/img/icons/dashboard.png
new file mode 100644
index 0000000..e5f9c09
--- /dev/null
+++ b/public/img/icons/dashboard.png
Binary files differ
diff --git a/public/img/icons/dashboard_petrol.png b/public/img/icons/dashboard_petrol.png
new file mode 100644
index 0000000..04936ba
--- /dev/null
+++ b/public/img/icons/dashboard_petrol.png
Binary files differ
diff --git a/public/img/icons/disabled.png b/public/img/icons/disabled.png
new file mode 100644
index 0000000..54d0364
--- /dev/null
+++ b/public/img/icons/disabled.png
Binary files differ
diff --git a/public/img/icons/disabled_petrol.png b/public/img/icons/disabled_petrol.png
new file mode 100644
index 0000000..7319fb7
--- /dev/null
+++ b/public/img/icons/disabled_petrol.png
Binary files differ
diff --git a/public/img/icons/down.png b/public/img/icons/down.png
new file mode 100644
index 0000000..a844413
--- /dev/null
+++ b/public/img/icons/down.png
Binary files differ
diff --git a/public/img/icons/down_petrol.png b/public/img/icons/down_petrol.png
new file mode 100644
index 0000000..dda1283
--- /dev/null
+++ b/public/img/icons/down_petrol.png
Binary files differ
diff --git a/public/img/icons/downtime_end.png b/public/img/icons/downtime_end.png
new file mode 100644
index 0000000..e1d0d97
--- /dev/null
+++ b/public/img/icons/downtime_end.png
Binary files differ
diff --git a/public/img/icons/downtime_end_petrol.png b/public/img/icons/downtime_end_petrol.png
new file mode 100644
index 0000000..5d0666b
--- /dev/null
+++ b/public/img/icons/downtime_end_petrol.png
Binary files differ
diff --git a/public/img/icons/downtime_start.png b/public/img/icons/downtime_start.png
new file mode 100644
index 0000000..8e33280
--- /dev/null
+++ b/public/img/icons/downtime_start.png
Binary files differ
diff --git a/public/img/icons/downtime_start__petrol.png b/public/img/icons/downtime_start__petrol.png
new file mode 100644
index 0000000..9d56acd
--- /dev/null
+++ b/public/img/icons/downtime_start__petrol.png
Binary files differ
diff --git a/public/img/icons/edit.png b/public/img/icons/edit.png
new file mode 100644
index 0000000..00d8e29
--- /dev/null
+++ b/public/img/icons/edit.png
Binary files differ
diff --git a/public/img/icons/edit_petrol.png b/public/img/icons/edit_petrol.png
new file mode 100644
index 0000000..8199b01
--- /dev/null
+++ b/public/img/icons/edit_petrol.png
Binary files differ
diff --git a/public/img/icons/error.png b/public/img/icons/error.png
new file mode 100644
index 0000000..6245899
--- /dev/null
+++ b/public/img/icons/error.png
Binary files differ
diff --git a/public/img/icons/error_petrol.png b/public/img/icons/error_petrol.png
new file mode 100644
index 0000000..817ec76
--- /dev/null
+++ b/public/img/icons/error_petrol.png
Binary files differ
diff --git a/public/img/icons/error_white.png b/public/img/icons/error_white.png
new file mode 100644
index 0000000..0f50874
--- /dev/null
+++ b/public/img/icons/error_white.png
Binary files differ
diff --git a/public/img/icons/expand.png b/public/img/icons/expand.png
new file mode 100644
index 0000000..8ee1725
--- /dev/null
+++ b/public/img/icons/expand.png
Binary files differ
diff --git a/public/img/icons/expand_petrol.png b/public/img/icons/expand_petrol.png
new file mode 100644
index 0000000..a794ef5
--- /dev/null
+++ b/public/img/icons/expand_petrol.png
Binary files differ
diff --git a/public/img/icons/flapping.png b/public/img/icons/flapping.png
new file mode 100644
index 0000000..4b253e0
--- /dev/null
+++ b/public/img/icons/flapping.png
Binary files differ
diff --git a/public/img/icons/flapping_petrol.png b/public/img/icons/flapping_petrol.png
new file mode 100644
index 0000000..c6d045a
--- /dev/null
+++ b/public/img/icons/flapping_petrol.png
Binary files differ
diff --git a/public/img/icons/history.png b/public/img/icons/history.png
new file mode 100644
index 0000000..db4520d
--- /dev/null
+++ b/public/img/icons/history.png
Binary files differ
diff --git a/public/img/icons/history_petrol.png b/public/img/icons/history_petrol.png
new file mode 100644
index 0000000..1aba36b
--- /dev/null
+++ b/public/img/icons/history_petrol.png
Binary files differ
diff --git a/public/img/icons/host.png b/public/img/icons/host.png
new file mode 100644
index 0000000..33e2f5d
--- /dev/null
+++ b/public/img/icons/host.png
Binary files differ
diff --git a/public/img/icons/host_petrol.png b/public/img/icons/host_petrol.png
new file mode 100644
index 0000000..6568fe6
--- /dev/null
+++ b/public/img/icons/host_petrol.png
Binary files differ
diff --git a/public/img/icons/hostgroup.png b/public/img/icons/hostgroup.png
new file mode 100644
index 0000000..e71e3c9
--- /dev/null
+++ b/public/img/icons/hostgroup.png
Binary files differ
diff --git a/public/img/icons/hostgroup_petrol.png b/public/img/icons/hostgroup_petrol.png
new file mode 100644
index 0000000..68dbd19
--- /dev/null
+++ b/public/img/icons/hostgroup_petrol.png
Binary files differ
diff --git a/public/img/icons/in_downtime.png b/public/img/icons/in_downtime.png
new file mode 100644
index 0000000..d609df6
--- /dev/null
+++ b/public/img/icons/in_downtime.png
Binary files differ
diff --git a/public/img/icons/in_downtime_petrol.png b/public/img/icons/in_downtime_petrol.png
new file mode 100644
index 0000000..f6ee49a
--- /dev/null
+++ b/public/img/icons/in_downtime_petrol.png
Binary files differ
diff --git a/public/img/icons/json.png b/public/img/icons/json.png
new file mode 100644
index 0000000..d8e74c3
--- /dev/null
+++ b/public/img/icons/json.png
Binary files differ
diff --git a/public/img/icons/json_petrol.png b/public/img/icons/json_petrol.png
new file mode 100644
index 0000000..0197455
--- /dev/null
+++ b/public/img/icons/json_petrol.png
Binary files differ
diff --git a/public/img/icons/logout.png b/public/img/icons/logout.png
new file mode 100644
index 0000000..b93487a
--- /dev/null
+++ b/public/img/icons/logout.png
Binary files differ
diff --git a/public/img/icons/logout_petrol.png b/public/img/icons/logout_petrol.png
new file mode 100644
index 0000000..82ece09
--- /dev/null
+++ b/public/img/icons/logout_petrol.png
Binary files differ
diff --git a/public/img/icons/next.png b/public/img/icons/next.png
new file mode 100644
index 0000000..905e598
--- /dev/null
+++ b/public/img/icons/next.png
Binary files differ
diff --git a/public/img/icons/next_petrol.png b/public/img/icons/next_petrol.png
new file mode 100644
index 0000000..488df30
--- /dev/null
+++ b/public/img/icons/next_petrol.png
Binary files differ
diff --git a/public/img/icons/notification.png b/public/img/icons/notification.png
new file mode 100644
index 0000000..ea805d5
--- /dev/null
+++ b/public/img/icons/notification.png
Binary files differ
diff --git a/public/img/icons/notification_disabled.png b/public/img/icons/notification_disabled.png
new file mode 100644
index 0000000..ffcf1e6
--- /dev/null
+++ b/public/img/icons/notification_disabled.png
Binary files differ
diff --git a/public/img/icons/notification_disabled_petrol.png b/public/img/icons/notification_disabled_petrol.png
new file mode 100644
index 0000000..96dde74
--- /dev/null
+++ b/public/img/icons/notification_disabled_petrol.png
Binary files differ
diff --git a/public/img/icons/notification_petrol.png b/public/img/icons/notification_petrol.png
new file mode 100644
index 0000000..fc2fde8
--- /dev/null
+++ b/public/img/icons/notification_petrol.png
Binary files differ
diff --git a/public/img/icons/pdf.png b/public/img/icons/pdf.png
new file mode 100644
index 0000000..eefcef6
--- /dev/null
+++ b/public/img/icons/pdf.png
Binary files differ
diff --git a/public/img/icons/pdf_petrol.png b/public/img/icons/pdf_petrol.png
new file mode 100644
index 0000000..ad044e6
--- /dev/null
+++ b/public/img/icons/pdf_petrol.png
Binary files differ
diff --git a/public/img/icons/prev.png b/public/img/icons/prev.png
new file mode 100644
index 0000000..edfabd3
--- /dev/null
+++ b/public/img/icons/prev.png
Binary files differ
diff --git a/public/img/icons/prev_petrol.png b/public/img/icons/prev_petrol.png
new file mode 100644
index 0000000..9152912
--- /dev/null
+++ b/public/img/icons/prev_petrol.png
Binary files differ
diff --git a/public/img/icons/refresh.png b/public/img/icons/refresh.png
new file mode 100644
index 0000000..b0222a5
--- /dev/null
+++ b/public/img/icons/refresh.png
Binary files differ
diff --git a/public/img/icons/refresh_petrol.png b/public/img/icons/refresh_petrol.png
new file mode 100644
index 0000000..50e8155
--- /dev/null
+++ b/public/img/icons/refresh_petrol.png
Binary files differ
diff --git a/public/img/icons/remove.png b/public/img/icons/remove.png
new file mode 100644
index 0000000..98ada74
--- /dev/null
+++ b/public/img/icons/remove.png
Binary files differ
diff --git a/public/img/icons/remove_petrol.png b/public/img/icons/remove_petrol.png
new file mode 100644
index 0000000..8ec49a2
--- /dev/null
+++ b/public/img/icons/remove_petrol.png
Binary files differ
diff --git a/public/img/icons/reschedule.png b/public/img/icons/reschedule.png
new file mode 100644
index 0000000..5efc20d
--- /dev/null
+++ b/public/img/icons/reschedule.png
Binary files differ
diff --git a/public/img/icons/reschedule_petrol.png b/public/img/icons/reschedule_petrol.png
new file mode 100644
index 0000000..3a24c90
--- /dev/null
+++ b/public/img/icons/reschedule_petrol.png
Binary files differ
diff --git a/public/img/icons/save.png b/public/img/icons/save.png
new file mode 100644
index 0000000..2db2719
--- /dev/null
+++ b/public/img/icons/save.png
Binary files differ
diff --git a/public/img/icons/save_petrol.png b/public/img/icons/save_petrol.png
new file mode 100644
index 0000000..3e42cd7
--- /dev/null
+++ b/public/img/icons/save_petrol.png
Binary files differ
diff --git a/public/img/icons/search.png b/public/img/icons/search.png
new file mode 100644
index 0000000..a66c3ca
--- /dev/null
+++ b/public/img/icons/search.png
Binary files differ
diff --git a/public/img/icons/search_icinga_blue.png b/public/img/icons/search_icinga_blue.png
new file mode 100644
index 0000000..0a284ca
--- /dev/null
+++ b/public/img/icons/search_icinga_blue.png
Binary files differ
diff --git a/public/img/icons/search_petrol.png b/public/img/icons/search_petrol.png
new file mode 100644
index 0000000..c02b702
--- /dev/null
+++ b/public/img/icons/search_petrol.png
Binary files differ
diff --git a/public/img/icons/search_white.png b/public/img/icons/search_white.png
new file mode 100644
index 0000000..638be2e
--- /dev/null
+++ b/public/img/icons/search_white.png
Binary files differ
diff --git a/public/img/icons/service.png b/public/img/icons/service.png
new file mode 100644
index 0000000..9df3b48
--- /dev/null
+++ b/public/img/icons/service.png
Binary files differ
diff --git a/public/img/icons/service_petrol.png b/public/img/icons/service_petrol.png
new file mode 100644
index 0000000..ea165a2
--- /dev/null
+++ b/public/img/icons/service_petrol.png
Binary files differ
diff --git a/public/img/icons/servicegroup.png b/public/img/icons/servicegroup.png
new file mode 100644
index 0000000..d4b87df
--- /dev/null
+++ b/public/img/icons/servicegroup.png
Binary files differ
diff --git a/public/img/icons/servicegroup_petrol.png b/public/img/icons/servicegroup_petrol.png
new file mode 100644
index 0000000..8b963c0
--- /dev/null
+++ b/public/img/icons/servicegroup_petrol.png
Binary files differ
diff --git a/public/img/icons/softstate.png b/public/img/icons/softstate.png
new file mode 100644
index 0000000..8efa442
--- /dev/null
+++ b/public/img/icons/softstate.png
Binary files differ
diff --git a/public/img/icons/submit.png b/public/img/icons/submit.png
new file mode 100644
index 0000000..40e4826
--- /dev/null
+++ b/public/img/icons/submit.png
Binary files differ
diff --git a/public/img/icons/submit_petrol.png b/public/img/icons/submit_petrol.png
new file mode 100644
index 0000000..b0b3156
--- /dev/null
+++ b/public/img/icons/submit_petrol.png
Binary files differ
diff --git a/public/img/icons/success.png b/public/img/icons/success.png
new file mode 100644
index 0000000..2f95639
--- /dev/null
+++ b/public/img/icons/success.png
Binary files differ
diff --git a/public/img/icons/success_petrol.png b/public/img/icons/success_petrol.png
new file mode 100644
index 0000000..302461f
--- /dev/null
+++ b/public/img/icons/success_petrol.png
Binary files differ
diff --git a/public/img/icons/tux.png b/public/img/icons/tux.png
new file mode 100644
index 0000000..c51a0b4
--- /dev/null
+++ b/public/img/icons/tux.png
Binary files differ
diff --git a/public/img/icons/uebersicht.png b/public/img/icons/uebersicht.png
new file mode 100644
index 0000000..501edc4
--- /dev/null
+++ b/public/img/icons/uebersicht.png
Binary files differ
diff --git a/public/img/icons/unhandled.png b/public/img/icons/unhandled.png
new file mode 100644
index 0000000..59dab84
--- /dev/null
+++ b/public/img/icons/unhandled.png
Binary files differ
diff --git a/public/img/icons/unhandled_petrol.png b/public/img/icons/unhandled_petrol.png
new file mode 100644
index 0000000..3972337
--- /dev/null
+++ b/public/img/icons/unhandled_petrol.png
Binary files differ
diff --git a/public/img/icons/up.png b/public/img/icons/up.png
new file mode 100644
index 0000000..beef484
--- /dev/null
+++ b/public/img/icons/up.png
Binary files differ
diff --git a/public/img/icons/up_petrol.png b/public/img/icons/up_petrol.png
new file mode 100644
index 0000000..946bef1
--- /dev/null
+++ b/public/img/icons/up_petrol.png
Binary files differ
diff --git a/public/img/icons/user.png b/public/img/icons/user.png
new file mode 100644
index 0000000..694ddfc
--- /dev/null
+++ b/public/img/icons/user.png
Binary files differ
diff --git a/public/img/icons/user_petrol.png b/public/img/icons/user_petrol.png
new file mode 100644
index 0000000..3fae64d
--- /dev/null
+++ b/public/img/icons/user_petrol.png
Binary files differ
diff --git a/public/img/icons/win.png b/public/img/icons/win.png
new file mode 100644
index 0000000..7ffcea7
--- /dev/null
+++ b/public/img/icons/win.png
Binary files differ
diff --git a/public/img/orb-analytics.png b/public/img/orb-analytics.png
new file mode 100644
index 0000000..7adae6f
--- /dev/null
+++ b/public/img/orb-analytics.png
Binary files differ
diff --git a/public/img/orb-automation.png b/public/img/orb-automation.png
new file mode 100644
index 0000000..eb292b5
--- /dev/null
+++ b/public/img/orb-automation.png
Binary files differ
diff --git a/public/img/orb-cloud.png b/public/img/orb-cloud.png
new file mode 100644
index 0000000..f5e4a64
--- /dev/null
+++ b/public/img/orb-cloud.png
Binary files differ
diff --git a/public/img/orb-icinga.png b/public/img/orb-icinga.png
new file mode 100644
index 0000000..9f7f2a6
--- /dev/null
+++ b/public/img/orb-icinga.png
Binary files differ
diff --git a/public/img/orb-infrastructure.png b/public/img/orb-infrastructure.png
new file mode 100644
index 0000000..8713965
--- /dev/null
+++ b/public/img/orb-infrastructure.png
Binary files differ
diff --git a/public/img/orb-metrics.png b/public/img/orb-metrics.png
new file mode 100644
index 0000000..199f21c
--- /dev/null
+++ b/public/img/orb-metrics.png
Binary files differ
diff --git a/public/img/orb-notifications.png b/public/img/orb-notifications.png
new file mode 100644
index 0000000..297ba58
--- /dev/null
+++ b/public/img/orb-notifications.png
Binary files differ
diff --git a/public/img/select-icon-2x.png b/public/img/select-icon-2x.png
new file mode 100644
index 0000000..57845df
--- /dev/null
+++ b/public/img/select-icon-2x.png
Binary files differ
diff --git a/public/img/select-icon.png b/public/img/select-icon.png
new file mode 100644
index 0000000..e0d9a5a
--- /dev/null
+++ b/public/img/select-icon.png
Binary files differ
diff --git a/public/img/select-icon.svg b/public/img/select-icon.svg
new file mode 100644
index 0000000..cc8a011
--- /dev/null
+++ b/public/img/select-icon.svg
@@ -0,0 +1 @@
+<svg height="32" viewBox="0 0 24 32" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m5.20126707.78766623 4.45386238 4.20402191c.16721345.15783356.16291017.40979541-.00961164.56277256-.081158.0719638-.18974398.11220597-.3027668.11220597h-8.90772462c-.24025844 0-.43502639-.17818569-.43502639-.39798892 0-.10340014.04398717-.20274128.12264801-.27698961l4.45386234-4.20402191c.16721345-.15783357.44262326-.16177048.61514507-.00879333.00325382.00288518.00645805.00581661.00961165.00879333z" fill="#00c3ed" transform="matrix(1 0 0 -1 7 20.666667)"/></svg> \ No newline at end of file
diff --git a/public/img/textarea-corner-2x.png b/public/img/textarea-corner-2x.png
new file mode 100644
index 0000000..ee9cb50
--- /dev/null
+++ b/public/img/textarea-corner-2x.png
Binary files differ
diff --git a/public/img/textarea-corner.png b/public/img/textarea-corner.png
new file mode 100644
index 0000000..3a2242c
--- /dev/null
+++ b/public/img/textarea-corner.png
Binary files differ
diff --git a/public/img/theme-mode-thumbnail-dark.svg b/public/img/theme-mode-thumbnail-dark.svg
new file mode 100644
index 0000000..40f93e8
--- /dev/null
+++ b/public/img/theme-mode-thumbnail-dark.svg
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="800px" height="480px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <g transform="matrix(1,0,0,1,-984,0)">
+ <g id="dark" transform="matrix(1,0,0,0.8,984.021,0)">
+ <rect x="0" y="0" width="800" height="600" style="fill:none;"/>
+ <clipPath id="_clip1">
+ <rect x="0" y="0" width="800" height="600"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g transform="matrix(1,0,0,1.25,0,9.09495e-13)">
+ <clipPath id="_clip2">
+ <rect x="0" y="-0" width="800" height="480"/>
+ </clipPath>
+ <g clip-path="url(#_clip2)">
+ <g transform="matrix(1.81105,0,0,1,0,0)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(7,5,44);"/>
+ </g>
+ <g transform="matrix(1.81105,0,0,0.30498,-1.13687e-13,190.035)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(25,21,68);"/>
+ </g>
+ <g transform="matrix(0.77088,0,0,0.290377,-644.149,483.662)">
+ <rect x="835.602" y="-1337.3" width="326.111" height="326.111" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g>
+ <path d="M251.392,110.852L220.365,141.879L251.392,172.906L251.392,353.271L188.167,353.271C182.556,353.271 178.008,357.819 178.008,363.43L178.008,416.496C178.008,422.107 182.556,426.655 188.167,426.655L251.392,426.655L251.392,600L800,600L800,-0L251.392,-0L251.392,110.852Z" style="fill:rgb(39,46,58);"/>
+ <path d="M251.392,110.852L220.365,141.879L251.392,172.906L251.392,353.271L188.167,353.271C182.556,353.271 178.008,357.819 178.008,363.43L178.008,416.496C178.008,422.107 182.556,426.655 188.167,426.655L251.392,426.655L251.392,600L800,600L800,-0L251.392,-0L251.392,110.852Z" style="fill:rgb(39,46,58);"/>
+ </g>
+ <g transform="matrix(4.8965,0,0,4.8965,-290.421,-2191.75)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,751.454,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(39,46,58);"/>
+ </g>
+ </g>
+ <g transform="matrix(4.8965,0,0,4.8965,-96.0097,-2195.22)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(62,77,116);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,751.454,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(39,46,58);"/>
+ </g>
+ </g>
+ <g transform="matrix(1.39597,0,0,1,-120.257,-227.628)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1,-120.257,-71.831)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054C403.105,523.054 617.133,523.054 675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434ZM687.21,421.434L687.21,502.154C687.21,510.935 682.111,518.054 675.821,518.054C617.133,518.054 403.105,518.054 344.417,518.054C338.127,518.054 333.028,510.935 333.028,502.154L333.028,421.434C333.028,412.653 338.127,405.534 344.417,405.534C344.417,405.534 675.768,405.534 675.821,405.534C682.111,405.534 687.21,412.653 687.21,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(4.71406,0,0,4.71406,191.129,368.75)">
+ <path d="M8.977,8.086L9.507,7.554C9.727,7.336 9.727,6.98 9.507,6.759L7.248,4.5L9.505,2.238C9.725,2.02 9.725,1.664 9.505,1.444L8.975,0.914C8.757,0.694 8.401,0.694 8.18,0.914L4.993,4.102C4.773,4.322 4.773,4.678 4.995,4.898L8.183,8.086C8.401,8.306 8.757,8.306 8.977,8.086ZM4.475,8.086L5.005,7.556C5.225,7.336 5.225,6.98 5.005,6.762L2.748,4.5L5.007,2.241C5.227,2.02 5.227,1.664 5.007,1.446L4.477,0.914C4.257,0.694 3.901,0.694 3.683,0.914L0.495,4.102C0.273,4.322 0.273,4.678 0.493,4.898L3.68,8.086C3.901,8.306 4.257,8.306 4.475,8.086Z" style="fill:rgb(0,196,240);fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/theme-mode-thumbnail-light.svg b/public/img/theme-mode-thumbnail-light.svg
new file mode 100644
index 0000000..a04f441
--- /dev/null
+++ b/public/img/theme-mode-thumbnail-light.svg
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="800px" height="480px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+ <g transform="matrix(1,0,0,1,-984,-674)">
+ <g id="light" transform="matrix(1,0,0,0.8,984.021,674)">
+ <rect x="0" y="0" width="800" height="600" style="fill:none;"/>
+ <g>
+ <clipPath id="_clip1">
+ <rect x="0" y="0" width="800" height="600"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g transform="matrix(1.81105,0,0,1.25,0,-1.13687e-13)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(219,236,242);"/>
+ </g>
+ <g transform="matrix(1.81105,0,0,0.391381,-1.13687e-13,237.544)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(235,247,253);"/>
+ </g>
+ <g transform="matrix(0.77088,0,0,0.362971,-644.149,604.578)">
+ <rect x="835.602" y="-1337.3" width="326.111" height="326.111" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g>
+ <path d="M251.392,139.576L220.365,178.36L251.392,217.144L251.392,750L800,750L800,-0L251.392,-0L251.392,139.576ZM251.392,434.008L188.167,434.008C182.556,434.008 178.008,439.693 178.008,446.707L178.008,513.039C178.008,520.053 182.556,525.739 188.167,525.739L251.392,525.739L251.392,434.008Z" style="fill:rgb(244,249,250);"/>
+ <path d="M251.392,139.576L220.365,178.36L251.392,217.144L251.392,750L800,750L800,-0L251.392,-0L251.392,139.576ZM251.392,434.008L188.167,434.008C182.556,434.008 178.008,439.693 178.008,446.707L178.008,513.039C178.008,520.053 182.556,525.739 188.167,525.739L251.392,525.739L251.392,434.008Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ <g transform="matrix(4.8965,0,0,6.12063,-290.421,-2739.68)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,751.454,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ </g>
+ <g transform="matrix(4.8965,0,0,6.12063,-96.0097,-2741.58)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(219,236,242);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,736.813,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ </g>
+ <g transform="matrix(1.39597,0,0,1.25,-120.257,-292.116)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1.25,-120.257,-97.3693)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:none;stroke:rgb(0,196,240);stroke-width:4.12px;"/>
+ </g>
+ <g transform="matrix(4.71406,0,0,5.89258,191.129,453.357)">
+ <path d="M8.977,8.086L9.507,7.554C9.727,7.336 9.727,6.98 9.507,6.759L7.248,4.5L9.505,2.238C9.725,2.02 9.725,1.664 9.505,1.444L8.975,0.914C8.757,0.694 8.401,0.694 8.18,0.914L4.993,4.102C4.773,4.322 4.773,4.678 4.995,4.898L8.183,8.086C8.401,8.306 8.757,8.306 8.977,8.086ZM4.475,8.086L5.005,7.556C5.225,7.336 5.225,6.98 5.005,6.762L2.748,4.5L5.007,2.241C5.227,2.02 5.227,1.664 5.007,1.446L4.477,0.914C4.257,0.694 3.901,0.694 3.683,0.914L0.495,4.102C0.273,4.322 0.273,4.678 0.493,4.898L3.68,8.086C3.901,8.306 4.257,8.306 4.475,8.086Z" style="fill:rgb(0,196,240);fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/theme-mode-thumbnail-system.svg b/public/img/theme-mode-thumbnail-system.svg
new file mode 100644
index 0000000..55e2168
--- /dev/null
+++ b/public/img/theme-mode-thumbnail-system.svg
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="800px" height="480px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+ <g transform="matrix(1,0,0,1,-984,-1360)">
+ <g id="auto" transform="matrix(1,0,0,0.8,984.021,1360)">
+ <rect x="0" y="0" width="800" height="600" style="fill:none;"/>
+ <clipPath id="_clip1">
+ <rect x="0" y="0" width="800" height="600"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g id="light">
+ <clipPath id="_clip2">
+ <rect x="0" y="0" width="800" height="600"/>
+ </clipPath>
+ <g clip-path="url(#_clip2)">
+ <g transform="matrix(1.81105,0,0,1.25,0,-1.13687e-13)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(219,236,242);"/>
+ </g>
+ <g transform="matrix(1.81105,0,0,0.391381,-1.13687e-13,237.544)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(235,247,253);"/>
+ </g>
+ <g transform="matrix(0.77088,0,0,0.362971,-644.149,604.578)">
+ <rect x="835.602" y="-1337.3" width="326.111" height="326.111" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g>
+ <path d="M251.392,139.576L220.365,178.36L251.392,217.144L251.392,750L800,750L800,-0L251.392,-0L251.392,139.576ZM251.392,434.008L188.167,434.008C182.556,434.008 178.008,439.693 178.008,446.707L178.008,513.039C178.008,520.053 182.556,525.739 188.167,525.739L251.392,525.739L251.392,434.008Z" style="fill:rgb(244,249,250);"/>
+ <path d="M251.392,139.576L220.365,178.36L251.392,217.144L251.392,750L800,750L800,-0L251.392,-0L251.392,139.576ZM251.392,434.008L188.167,434.008C182.556,434.008 178.008,439.693 178.008,446.707L178.008,513.039C178.008,520.053 182.556,525.739 188.167,525.739L251.392,525.739L251.392,434.008Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ <g transform="matrix(4.8965,0,0,6.12063,-290.421,-2739.68)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,751.454,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ </g>
+ <g transform="matrix(4.8965,0,0,6.12063,-96.0097,-2741.58)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(219,236,242);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,736.813,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ </g>
+ <g transform="matrix(1.39597,0,0,1.25,-120.257,-292.116)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1.25,-120.257,-97.3693)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:none;stroke:rgb(0,196,240);stroke-width:4.12px;"/>
+ </g>
+ <g transform="matrix(4.71406,0,0,5.89258,191.129,453.357)">
+ <path d="M8.977,8.086L9.507,7.554C9.727,7.336 9.727,6.98 9.507,6.759L7.248,4.5L9.505,2.238C9.725,2.02 9.725,1.664 9.505,1.444L8.975,0.914C8.757,0.694 8.401,0.694 8.18,0.914L4.993,4.102C4.773,4.322 4.773,4.678 4.995,4.898L8.183,8.086C8.401,8.306 8.757,8.306 8.977,8.086ZM4.475,8.086L5.005,7.556C5.225,7.336 5.225,6.98 5.005,6.762L2.748,4.5L5.007,2.241C5.227,2.02 5.227,1.664 5.007,1.446L4.477,0.914C4.257,0.694 3.901,0.694 3.683,0.914L0.495,4.102C0.273,4.322 0.273,4.678 0.493,4.898L3.68,8.086C3.901,8.306 4.257,8.306 4.475,8.086Z" style="fill:rgb(0,196,240);fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+ <g id="dark" transform="matrix(1,0,0,1.25,400,1.13864e-12)">
+ <clipPath id="_clip3">
+ <rect x="0" y="-0" width="800" height="480"/>
+ </clipPath>
+ <g clip-path="url(#_clip3)">
+ <g transform="matrix(1.81105,0,0,1,0,0)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(7,5,44);"/>
+ </g>
+ <g transform="matrix(1.81105,0,0,0.30498,-1.13687e-13,190.035)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(25,21,68);"/>
+ </g>
+ <g transform="matrix(0.77088,0,0,0.290377,-644.149,483.662)">
+ <rect x="835.602" y="-1337.3" width="326.111" height="326.111" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g>
+ <path d="M251.392,110.852L220.365,141.879L251.392,172.906L251.392,353.271L188.167,353.271C182.556,353.271 178.008,357.819 178.008,363.43L178.008,416.496C178.008,422.107 182.556,426.655 188.167,426.655L251.392,426.655L251.392,600L800,600L800,-0L251.392,-0L251.392,110.852Z" style="fill:rgb(39,46,58);"/>
+ <path d="M251.392,110.852L220.365,141.879L251.392,172.906L251.392,353.271L188.167,353.271C182.556,353.271 178.008,357.819 178.008,363.43L178.008,416.496C178.008,422.107 182.556,426.655 188.167,426.655L251.392,426.655L251.392,600L800,600L800,-0L251.392,-0L251.392,110.852Z" style="fill:rgb(39,46,58);"/>
+ </g>
+ <g transform="matrix(4.8965,0,0,4.8965,6277.58,-3930.98)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1,-120.257,-227.628)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534L344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1,-120.257,-71.831)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434ZM687.21,421.434L687.21,502.154C687.21,510.935 682.111,518.054 675.821,518.054C675.821,518.054 344.417,518.054 344.417,518.054C338.127,518.054 333.028,510.935 333.028,502.154L333.028,421.434C333.028,412.653 338.127,405.534 344.417,405.534C403.105,405.534 617.133,405.534 675.821,405.534C682.111,405.534 687.21,412.653 687.21,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(4.71406,0,0,4.71406,191.129,368.75)">
+ <path d="M8.977,8.086L9.507,7.554C9.727,7.336 9.727,6.98 9.507,6.759L7.248,4.5L9.505,2.238C9.725,2.02 9.725,1.664 9.505,1.444L8.975,0.914C8.757,0.694 8.401,0.694 8.18,0.914L4.993,4.102C4.773,4.322 4.773,4.678 4.995,4.898L8.183,8.086C8.401,8.306 8.757,8.306 8.977,8.086ZM4.475,8.086L5.005,7.556C5.225,7.336 5.225,6.98 5.005,6.762L2.748,4.5L5.007,2.241C5.227,2.02 5.227,1.664 5.007,1.446L4.477,0.914C4.257,0.694 3.901,0.694 3.683,0.914L0.495,4.102C0.273,4.322 0.273,4.678 0.493,4.898L3.68,8.086C3.901,8.306 4.257,8.306 4.475,8.086Z" style="fill:rgb(0,196,240);fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/touch-icon.png b/public/img/touch-icon.png
new file mode 100644
index 0000000..c5d5fdd
--- /dev/null
+++ b/public/img/touch-icon.png
Binary files differ
diff --git a/public/img/tree/tree-minus.gif b/public/img/tree/tree-minus.gif
new file mode 100644
index 0000000..551817e
--- /dev/null
+++ b/public/img/tree/tree-minus.gif
Binary files differ
diff --git a/public/img/tree/tree-plus.gif b/public/img/tree/tree-plus.gif
new file mode 100644
index 0000000..72446cc
--- /dev/null
+++ b/public/img/tree/tree-plus.gif
Binary files differ
diff --git a/public/img/website-icon.svg b/public/img/website-icon.svg
new file mode 100644
index 0000000..6ca6ee5
--- /dev/null
+++ b/public/img/website-icon.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <path d="M8.37,2.58c-0.126,-0.074 -0.241,-0.171 -0.339,-0.29c-0.436,-0.535 -0.356,-1.323 0.179,-1.759c0.535,-0.436 1.323,-0.355 1.759,0.179c0.436,0.535 0.356,1.323 -0.179,1.759c-0.276,0.225 -0.619,0.312 -0.947,0.271l-1.112,2.75c0.369,0.169 0.705,0.423 0.979,0.758c0.236,0.29 0.403,0.614 0.502,0.952l3.299,-1.008c-0.003,-0.022 -0.005,-0.044 -0.007,-0.067c-0.062,-0.825 0.557,-1.545 1.384,-1.608c0.825,-0.062 1.545,0.557 1.608,1.384c0.062,0.826 -0.558,1.546 -1.384,1.608c-0.63,0.047 -1.199,-0.302 -1.46,-0.838l-3.343,1.022c0.091,0.885 -0.251,1.797 -0.991,2.402c-0.031,0.024 -0.062,0.049 -0.093,0.072l0.629,1.051c0.292,-0.211 0.662,-0.317 1.048,-0.273c0.823,0.095 1.412,0.841 1.317,1.662c-0.097,0.823 -0.839,1.412 -1.662,1.319c-0.823,-0.096 -1.413,-0.84 -1.317,-1.663c0.029,-0.251 0.118,-0.481 0.252,-0.676l-0.694,-1.159c-0.806,0.397 -1.753,0.367 -2.526,-0.058l-1.839,2.234c0.316,0.324 0.525,0.758 0.561,1.246c0.083,1.101 -0.742,2.062 -1.844,2.144c-1.102,0.083 -2.062,-0.743 -2.144,-1.845c-0.083,-1.101 0.742,-2.061 1.845,-2.143c0.434,-0.033 0.847,0.075 1.192,0.287l1.814,-2.204c-0.138,-0.114 -0.268,-0.243 -0.385,-0.388c-0.805,-0.986 -0.806,-2.364 -0.086,-3.344l-1.849,-1.514c-0.031,0.02 -0.064,0.038 -0.098,0.055c-0.496,0.243 -1.094,0.037 -1.337,-0.459c-0.243,-0.495 -0.037,-1.094 0.459,-1.337c0.496,-0.243 1.094,-0.037 1.337,0.458c0.147,0.302 0.129,0.642 -0.019,0.917l1.839,1.506c0.046,-0.044 0.095,-0.086 0.145,-0.127c0.696,-0.567 1.587,-0.735 2.397,-0.531l1.11,-2.745Z" />
+</svg>
diff --git a/public/img/winter/logo_icinga_big_winter.png b/public/img/winter/logo_icinga_big_winter.png
new file mode 100644
index 0000000..9fbabd3
--- /dev/null
+++ b/public/img/winter/logo_icinga_big_winter.png
Binary files differ
diff --git a/public/img/winter/snow1.png b/public/img/winter/snow1.png
new file mode 100644
index 0000000..034f5b5
--- /dev/null
+++ b/public/img/winter/snow1.png
Binary files differ
diff --git a/public/img/winter/snow2.png b/public/img/winter/snow2.png
new file mode 100644
index 0000000..325487b
--- /dev/null
+++ b/public/img/winter/snow2.png
Binary files differ
diff --git a/public/img/winter/snow3.png b/public/img/winter/snow3.png
new file mode 100644
index 0000000..5f40f16
--- /dev/null
+++ b/public/img/winter/snow3.png
Binary files differ
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..39a7891
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,4 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+require_once dirname(__DIR__) . '/library/Icinga/Application/webrouter.php';
diff --git a/public/js/define.js b/public/js/define.js
new file mode 100644
index 0000000..a3ce8c6
--- /dev/null
+++ b/public/js/define.js
@@ -0,0 +1,118 @@
+/*! Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+(function(window) {
+
+ 'use strict';
+
+ /**
+ * Provide a reference to be later required by foreign code
+ *
+ * @param {string} name Optional, defaults to the name (and path) of the file
+ * @param {string[]} requirements Optional, list of required references, may be relative if from the same package
+ * @param {function} factory Required, function that accepts as many params as there are requirements and that
+ * produces a value to be referenced
+ */
+ var define = function (name, requirements, factory) {
+ define.defines[name] = {
+ requirements: requirements,
+ factory: factory,
+ ref: null
+ }
+
+ define.resolve(name);
+ }
+
+ /**
+ * Return whether the given name references a value
+ *
+ * @param {string} name The absolute name of the reference
+ * @return {boolean}
+ */
+ define.has = function (name) {
+ return name in define.defines && define.defines[name]['ref'] !== null;
+ }
+
+ /**
+ * Get the value of a reference
+ *
+ * @param {string} name The absolute name of the reference
+ * @return {*}
+ */
+ define.get = function (name) {
+ return define.defines[name]['ref'];
+ }
+
+ /**
+ * Set the value of a reference
+ *
+ * @param {string} name The absolute name of the reference
+ * @param {*} ref The value to reference
+ */
+ define.set = function (name, ref) {
+ define.defines[name]['ref'] = ref;
+ }
+
+ /**
+ * Resolve a reference and, if successful, dependent references
+ *
+ * @param {string} name The absolute name of the reference
+ * @return {boolean}
+ */
+ define.resolve = function (name) {
+ var requirements = define.defines[name]['requirements'];
+
+ var exports, ref;
+ var requiredRefs = [];
+ for (var i = 0; i < requirements.length; i++) {
+ if (define.has(requirements[i])) {
+ ref = define.get(requirements[i]);
+ } else if (requirements[i] === 'exports') {
+ exports = ref = {};
+ } else {
+ return false;
+ }
+
+ requiredRefs.push(ref);
+ }
+
+ var factory = define.defines[name]['factory'];
+ var resolved = factory.apply(null, requiredRefs);
+
+ if (typeof exports === 'object') {
+ if (typeof resolved !== 'undefined') {
+ throw new Error('Factory for ' + name + ' returned, although exports were populated');
+ }
+
+ resolved = exports;
+ }
+
+ define.set(name, resolved);
+
+ for (var definedName in define.defines) {
+ if (define.defines[definedName]['requirements'].indexOf(name) >= 0) {
+ define.resolve(definedName);
+ }
+ }
+ }
+
+ /**
+ * Require a reference
+ *
+ * @param {string} name The absolute name of the reference
+ * @return {*}
+ */
+ var require = function(name) {
+ if (define.has(name)) {
+ return define.get(name);
+ }
+
+ throw new ReferenceError(name + ' is not defined');
+ }
+
+ define.icinga = true;
+ define.defines = {};
+
+ window.define = define;
+ window.require = require;
+
+})(window);
diff --git a/public/js/helpers.js b/public/js/helpers.js
new file mode 100644
index 0000000..bcb2736
--- /dev/null
+++ b/public/js/helpers.js
@@ -0,0 +1,91 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/* jQuery Plugins */
+(function ($) {
+
+ 'use strict';
+
+ /* Get data value or default */
+ $.fn.getData = function (name, fallback) {
+ var value = this.data(name);
+ if (typeof value !== 'undefined') {
+ return value;
+ }
+
+ return fallback;
+ };
+
+ /* Whether a HTML tag has a specific attribute */
+ $.fn.hasAttr = function(name) {
+ // We have inconsistent behaviour across browsers (false VS undef)
+ var val = this.attr(name);
+ return typeof val !== 'undefined' && val !== false;
+ };
+
+ /* Get class list */
+ $.fn.classes = function (callback) {
+
+ var classes = [];
+
+ $.each(this, function (i, el) {
+ var c = $(el).attr('class');
+ if (typeof c === 'string') {
+ $.each(c.split(/\s+/), function(i, p) {
+ if (classes.indexOf(p) === -1) {
+ classes.push(p);
+ }
+ });
+ }
+ });
+
+ if (typeof callback === 'function') {
+ for (var i in classes) {
+ if (classes.hasOwnProperty(i)) {
+ callback(classes[i]);
+ }
+ }
+ }
+
+ return classes;
+ };
+
+ /* Serialize form elements to an object */
+ $.fn.serializeObject = function()
+ {
+ var o = {};
+ var a = this.serializeArray();
+ $.each(a, function() {
+ if (o[this.name] !== undefined) {
+ if (!o[this.name].push) {
+ o[this.name] = [o[this.name]];
+ }
+ o[this.name].push(this.value || '');
+ } else {
+ o[this.name] = this.value || '';
+ }
+ });
+ return o;
+ };
+
+ $.fn.offsetTopRelativeTo = function($ancestor) {
+ if (typeof $ancestor === 'undefined') {
+ return false;
+ }
+
+ var el = this[0];
+ var offset = el.offsetTop;
+ var $parent = $(el.offsetParent);
+
+ if ($parent.is('body') || $parent.is($ancestor)) {
+ return offset;
+ }
+
+ if (el.tagName === 'TR') {
+ // TODO: Didn't found a better way, this will probably break sooner or later
+ return $parent.offsetTopRelativeTo($ancestor);
+ }
+
+ return offset + $parent.offsetTopRelativeTo($ancestor);
+ };
+
+})(jQuery);
diff --git a/public/js/icinga.js b/public/js/icinga.js
new file mode 100644
index 0000000..e8b8bcc
--- /dev/null
+++ b/public/js/icinga.js
@@ -0,0 +1,279 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga starts here.
+ *
+ * Usage example:
+ *
+ * <code>
+ * var icinga = new Icinga({
+ * baseUrl: '/icinga',
+ * });
+ * </code>
+ */
+(function(window, $) {
+
+ 'use strict';
+
+ var Icinga = function (config) {
+
+ this.initialized = false;
+
+ /**
+ * Our config object
+ */
+ this.config = config;
+
+ /**
+ * Icinga.Logger
+ */
+ this.logger = null;
+
+ /**
+ * Icinga.UI
+ */
+ this.ui = null;
+
+ /**
+ * Icinga.Loader
+ */
+ this.loader = null;
+
+ /**
+ * Icinga.Events
+ */
+ this.events = null;
+
+ /**
+ * Icinga.Timer
+ */
+ this.timer = null;
+
+ /**
+ * Icinga.History
+ */
+ this.history = null;
+
+ /**
+ * Icinga.Utils
+ */
+ this.utils = null;
+
+ /**
+ * Additional site behavior
+ */
+ this.behaviors = {};
+
+ /**
+ * Loaded modules
+ */
+ this.modules = {};
+
+ var _this = this;
+ $(document).ready(function () {
+ _this.initialize();
+ _this = null;
+ });
+ };
+
+ Icinga.prototype = {
+
+ /**
+ * Icinga startup, will be triggerd once the document is ready
+ */
+ initialize: function () {
+ if (this.initialized) {
+ return false;
+ }
+
+ this.timezone = new Icinga.Timezone();
+ this.utils = new Icinga.Utils(this);
+ this.logger = new Icinga.Logger(this);
+ this.timer = new Icinga.Timer(this);
+ this.ui = new Icinga.UI(this);
+ this.loader = new Icinga.Loader(this);
+ this.events = new Icinga.Events(this);
+ this.history = new Icinga.History(this);
+ var _this = this;
+ $.each(Icinga.Behaviors, function(name, Behavior) {
+ _this.behaviors[name.toLowerCase()] = new Behavior(_this);
+ });
+
+ this.timezone.initialize();
+ this.timer.initialize();
+ this.events.initialize();
+ this.history.initialize();
+ this.ui.initialize();
+ this.loader.initialize();
+
+ this.logger.info('Icinga is ready, running on jQuery ', $().jquery);
+ this.initialized = true;
+
+ // Trigger our own post-init event, `onLoad` is not reliable enough
+ $(document).trigger('icinga-init');
+ },
+
+ /**
+ * Load a given module by name
+ *
+ * @param {string} name
+ *
+ * @return {boolean}
+ */
+ loadModule: function (name) {
+
+ if (this.isLoadedModule(name)) {
+ this.logger.error('Cannot load module ' + name + ' twice');
+ return false;
+ }
+
+ if (! this.hasModule(name)) {
+ this.logger.error('Cannot find module ' + name);
+ return false;
+ }
+
+ this.modules[name] = new Icinga.Module(
+ this,
+ name,
+ Icinga.availableModules[name]
+ );
+ return true;
+ },
+
+ /**
+ * Whether a module matching the given name exists or is loaded
+ *
+ * @param {string} name
+ *
+ * @return {boolean}
+ */
+ hasModule: function (name) {
+ return this.isLoadedModule(name) ||
+ 'undefined' !== typeof Icinga.availableModules[name];
+ },
+
+ /**
+ * Return whether the given module is loaded
+ *
+ * @param {string} name The name of the module
+ *
+ * @returns {Boolean}
+ */
+ isLoadedModule: function (name) {
+ return 'undefined' !== typeof this.modules[name];
+ },
+
+ /**
+ * Ensure we have loaded the javascript code for a module
+ *
+ * @param {string} moduleName
+ */
+ ensureModule: function(moduleName) {
+ if (this.hasModule(moduleName) && ! this.isLoadedModule(moduleName)) {
+ this.loadModule(moduleName);
+ }
+ },
+
+ /**
+ * If a container contains sub-containers for other modules,
+ * make sure the javascript code for each module is loaded.
+ *
+ * Containers are identified by "data-icinga-module" which
+ * holds the module name.
+ *
+ * @param container
+ */
+ ensureSubModules: function (container) {
+ var icinga = this;
+
+ $(container).find('[data-icinga-module]').each(function () {
+ var moduleName = $(this).data('icingaModule');
+ if (moduleName) {
+ icinga.ensureModule(moduleName);
+ }
+ });
+ },
+
+ /**
+ * Get a module by name
+ *
+ * @param {string} name
+ *
+ * @return {object}
+ */
+ module: function (name) {
+
+ if (this.hasModule(name) && !this.isLoadedModule(name)) {
+ this.modules[name] = new Icinga.Module(
+ this,
+ name,
+ Icinga.availableModules[name]
+ );
+ }
+
+ return this.modules[name];
+ },
+
+ /**
+ * Clean up and unload all Icinga components
+ */
+ destroy: function () {
+
+ $.each(this.modules, function (name, module) {
+ module.destroy();
+ });
+
+ this.timezone.destroy();
+ this.timer.destroy();
+ this.events.destroy();
+ this.loader.destroy();
+ this.ui.destroy();
+ this.logger.debug('Icinga has been destroyed');
+ this.logger.destroy();
+ this.utils.destroy();
+
+ this.modules = [];
+ this.timer = this.events = this.loader = this.ui = this.logger =
+ this.utils = null;
+ this.initialized = false;
+ },
+
+ reload: function () {
+ setTimeout(function () {
+ var oldjQuery = window.jQuery;
+ var oldConfig = window.icinga.config;
+ var oldIcinga = window.Icinga;
+ window.icinga.destroy();
+ window.Icinga = undefined;
+ window.$ = undefined;
+ window.jQuery = undefined;
+ jQuery = undefined;
+ $ = undefined;
+
+ oldjQuery.getScript(
+ oldConfig.baseUrl.replace(/\/$/, '') + '/js/icinga.min.js'
+ ).done(function () {
+ var jQuery = window.jQuery;
+ window.icinga = new window.Icinga(oldConfig);
+ window.icinga.initialize();
+ window.icinga.ui.reloadCss();
+ oldjQuery = undefined;
+ oldConfig = undefined;
+ oldIcinga = undefined;
+ }).fail(function () {
+ window.jQuery = oldjQuery;
+ window.$ = window.jQuery;
+ window.Icinga = oldIcinga;
+ window.icinga = new Icinga(oldConfig);
+ window.icinga.ui.reloadCss();
+ });
+ }, 0);
+ }
+
+ };
+
+ window.Icinga = Icinga;
+
+ Icinga.availableModules = {};
+
+})(window, jQuery);
diff --git a/public/js/icinga/behavior/actiontable.js b/public/js/icinga/behavior/actiontable.js
new file mode 100644
index 0000000..0f914f7
--- /dev/null
+++ b/public/js/icinga/behavior/actiontable.js
@@ -0,0 +1,498 @@
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Behavior.ActionTable
+ *
+ * A multi selection that distincts between the table rows using the row action URL filter
+ */
+(function(Icinga, $) {
+
+ "use strict";
+
+ /**
+ * Remove one leading and trailing bracket and all text outside those brackets
+ *
+ * @param str {String}
+ * @returns {string}
+ */
+ var stripBrackets = function (str) {
+ return str.replace(/^[^\(]*\(/, '').replace(/\)[^\)]*$/, '');
+ };
+
+ /**
+ * Parse the filter query contained in the given url filter string
+ *
+ * @param filterString {String}
+ *
+ * @returns {Array} An object containing each row filter
+ */
+ var parseSelectionQuery = function(filterString) {
+ var selections = [];
+ $.each(stripBrackets(filterString).split('|'), function(i, row) {
+ var tuple = {};
+ $.each(stripBrackets(row).split('&'), function(i, keyValue) {
+ var s = keyValue.split('=');
+ tuple[s[0]] = decodeURIComponent(s[1]);
+ });
+ selections.push(tuple);
+ });
+ return selections;
+ };
+
+ /**
+ * Handle the selection of an action table
+ *
+ * @param table {HTMLElement} The table
+ * @param icinga {Icinga}
+ *
+ * @constructor
+ */
+ var Selection = function(table, icinga) {
+ this.$el = $(table);
+ this.icinga = icinga;
+ this.col = this.$el.closest('div.container').attr('id');
+
+ if (this.hasMultiselection()) {
+ if (! this.getMultiselectionKeys().length) {
+ icinga.logger.error('multiselect table has no data-icinga-multiselect-data');
+ }
+ if (! this.getMultiselectionUrl()) {
+ icinga.logger.error('multiselect table has no data-icinga-multiselect-url');
+ }
+ }
+ };
+
+ Selection.prototype = {
+
+ /**
+ * The container id in which this selection happens
+ */
+ col: null,
+
+ /**
+ * Return all rows as jQuery selector
+ *
+ * @returns {jQuery}
+ */
+ rows: function() {
+ return this.$el.find('tr');
+ },
+
+ /**
+ * Return all row action links as jQuery selector
+ *
+ * @returns {jQuery}
+ */
+ rowActions: function() {
+ return this.$el.find('tr[href]');
+ },
+
+ /**
+ * Return all selected rows as jQuery selector
+ *
+ * @returns {jQuery}
+ */
+ selections: function() {
+ return this.$el.find('tr.active');
+ },
+
+ /**
+ * If this selection allows selecting multiple rows
+ *
+ * @returns {Boolean}
+ */
+ hasMultiselection: function() {
+ return this.$el.hasClass('multiselect');
+ },
+
+ /**
+ * Return all filter keys that are significant when applying the selection
+ *
+ * @returns {Array}
+ */
+ getMultiselectionKeys: function() {
+ var data = this.$el.data('icinga-multiselect-data');
+ return (data && data.split(',')) || [];
+ },
+
+ /**
+ * Return the main target URL that is used when multi selecting rows
+ *
+ * This URL may differ from the url that is used when applying single rows
+ *
+ * @returns {String}
+ */
+ getMultiselectionUrl: function() {
+ return this.$el.data('icinga-multiselect-url');
+ },
+
+ /**
+ * Check whether the given url is
+ *
+ * @param {String} url
+ */
+ hasMultiselectionUrl: function(url) {
+ var urls = this.$el.data('icinga-multiselect-url').split(' ');
+
+ var related = this.$el.data('icinga-multiselect-controllers');
+ if (related && related.length) {
+ urls = urls.concat(this.$el.data('icinga-multiselect-controllers').split(' '));
+ }
+
+ var hasSelection = false;
+ $.each(urls, function (i, object) {
+ if (url.indexOf(object) === 0) {
+ hasSelection = true;
+ }
+ });
+ return hasSelection;
+ },
+
+ /**
+ * Read all filter data from the given row
+ *
+ * @param row {jQuery} The row element
+ *
+ * @returns {Object} An object containing all filter data in this row as key-value pairs
+ */
+ getRowData: function(row) {
+ var params = this.icinga.utils.parseUrl(row.attr('href')).params;
+ var tuple = {};
+ var keys = this.getMultiselectionKeys();
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ for (var j = 0; j < params.length; j++) {
+ if (params[j].key === key && (params[j].value || params[j].value === null)) {
+ tuple[key] = params[j].value ? decodeURIComponent(params[j].value): params[j].value;
+ break;
+ }
+ }
+ }
+ return tuple;
+ },
+
+ /**
+ * Deselect all selected rows
+ */
+ clear: function() {
+ this.selections().removeClass('active');
+ },
+
+ /**
+ * Add all rows that match the given filter to the selection
+ *
+ * @param filter {jQuery|Object} Either an object containing filter variables or the actual row to select
+ */
+ select: function(filter) {
+ if (filter instanceof jQuery) {
+ filter.addClass('active');
+ return;
+ }
+ var _this = this;
+ this.rowActions()
+ .filter(
+ function (i, el) {
+ return _this.icinga.utils.objectsEqual(_this.getRowData($(el)), filter);
+ }
+ )
+ .closest('tr')
+ .addClass('active');
+ },
+
+ /**
+ * Toggle the selection of the row between on and off
+ *
+ * @param row {jQuery} The row to toggle
+ */
+ toggle: function(row) {
+ row.toggleClass('active');
+ },
+
+ /**
+ * Add a new selection range to the closest table, using the selected row as
+ * range target.
+ *
+ * @param row {jQuery} The target of the selected range.
+ *
+ * @returns {boolean} If the selection was changed.
+ */
+ range: function(row) {
+ var from, to;
+ var selected = row.first().get(0);
+ this.rows().each(function(i, el) {
+ if ($(el).hasClass('active') || el === selected) {
+ if (!from) {
+ from = el;
+ }
+ to = el;
+ }
+ });
+ var inRange = false;
+ this.rows().each(function(i, el) {
+ if (el === from) {
+ inRange = true;
+ }
+ if (inRange) {
+ $(el).addClass('active');
+ }
+ if (el === to) {
+ inRange = false;
+ }
+ });
+ return false;
+ },
+
+ /**
+ * Select rows that target the given url
+ *
+ * @param url {String} The target url
+ */
+ selectUrl: function(url) {
+ var formerHref = this.$el.closest('.container').data('icinga-actiontable-former-href')
+
+ var $row = this.rows().filter('[href="' + url + '"]');
+
+ if ($row.length) {
+ this.clear();
+ $row.addClass('active');
+ } else {
+ if (this.col !== 'col2') {
+ // rows sometimes need to be displayed as active when related actions
+ // like command actions are being opened. Do not do this for col2, as it
+ // would always select the opened URL itself.
+ var $row = this.rows().filter('[href$="' + icinga.utils.parseUrl(url).query + '"]');
+ if ($row.length) {
+ this.clear();
+ $row.addClass('active');
+ } else {
+ var $row = this.rows().filter('[href$="' + formerHref + '"]');
+ if ($row.length) {
+ this.clear();
+ $row.addClass('active');
+ } else {
+ var tbl = this.$el;
+ if (ActionTable.prototype.tables(
+ tbl.closest('.dashboard').find('.container')).not(tbl).find('tr.active').length
+ ) {
+ this.clear();
+ }
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Convert all currently selected rows into an url query string
+ *
+ * @returns {String} The filter string
+ */
+ toQuery: function() {
+ var _this = this;
+ var selections = this.selections();
+ var queries = [];
+ var utils = this.icinga.utils;
+ if (selections.length === 1) {
+ return $(selections[0]).attr('href');
+ } else if (selections.length > 1 && _this.hasMultiselection()) {
+ selections.each(function (i, el) {
+ var parts = [];
+ $.each(_this.getRowData($(el)), function(key, value) {
+ var condition = utils.fixedEncodeURIComponent(key);
+ if (value !== null) {
+ condition += '=' + utils.fixedEncodeURIComponent(value);
+ }
+
+ parts.push(condition);
+ });
+ queries.push('(' + parts.join('&') + ')');
+ });
+ return _this.getMultiselectionUrl() + '?(' + queries.join('|') + ')';
+ } else {
+ return '';
+ }
+ },
+
+ /**
+ * Refresh the displayed active columns using the current page location
+ */
+ refresh: function() {
+ var hash = icinga.history.getCol2State().replace(/^#!/, '');
+ if (this.hasMultiselection()) {
+ var query = parseSelectionQuery(hash);
+ if (query.length > 1 && this.hasMultiselectionUrl(this.icinga.utils.parseUrl(hash).path)) {
+ this.clear();
+ // select all rows with matching filters
+ var _this = this;
+ $.each(query, function(i, selection) {
+ _this.select(selection);
+ });
+ }
+ if (query.length > 1) {
+ return;
+ }
+ }
+ this.selectUrl(hash);
+ }
+ };
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var ActionTable = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ /**
+ * If currently loading
+ *
+ * @var Boolean
+ */
+ this.loading = false;
+
+ this.on('rendered', '#main .container', this.onRendered, this);
+ this.on('beforerender', '#main .container', this.beforeRender, this);
+ this.on('click', 'table.action tr[href], table.table-row-selectable tr[href]', this.onRowClicked, this);
+ };
+ ActionTable.prototype = new Icinga.EventListener();
+
+ /**
+ * Return all active tables in this table, or in the context as jQuery selector
+ *
+ * @param context {HTMLElement}
+ * @returns {jQuery}
+ */
+ ActionTable.prototype.tables = function(context) {
+ if (context) {
+ return $(context).find('table.action, table.table-row-selectable');
+ }
+ return $('table.action, table.table-row-selectable');
+ };
+
+ /**
+ * Handle clicks on table rows and update selection and history
+ */
+ ActionTable.prototype.onRowClicked = function (event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+ var $tr = $(event.currentTarget);
+ var table = new Selection($tr.closest('table.action, table.table-row-selectable')[0], _this.icinga);
+
+ if ($tr.closest('[data-no-icinga-ajax]').length > 0) {
+ return true;
+ }
+
+ // some rows may contain form actions that trigger a different action, pass those through
+ if (!$target.hasClass('rowaction') && $target.closest('form').length &&
+ ($target.closest('a').length || // allow regular link clinks
+ $target.closest('button').length || // allow submitting forms
+ $target.closest('input').length || $target.closest('label').length)) { // allow selecting form elements
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ // update selection
+ if (table.hasMultiselection()) {
+ if (event.ctrlKey || event.metaKey) {
+ // add to selection
+ table.toggle($tr);
+ } else if (event.shiftKey) {
+ // range selection
+ table.range($tr);
+ } else {
+ // single selection
+ table.clear();
+ table.select($tr);
+ }
+ } else {
+ table.clear();
+ table.select($tr);
+ }
+
+ var count = table.selections().length;
+ if (count > 0) {
+ var query = table.toQuery();
+ _this.icinga.loader.loadUrl(query, _this.icinga.loader.getLinkTargetFor($tr));
+ } else {
+ if (_this.icinga.loader.getLinkTargetFor($tr).attr('id') === 'col2') {
+ _this.icinga.ui.layout1col();
+ }
+ }
+
+ // redraw all table selections
+ _this.tables().each(function () {
+ new Selection(this, _this.icinga).refresh();
+ });
+
+ // update selection info
+ $('.selection-info-count').text(count);
+ return false;
+ };
+
+ /**
+ * Render the selection and prepare selection rows
+ */
+ ActionTable.prototype.onRendered = function(evt) {
+ var container = evt.target;
+ var _this = evt.data.self;
+
+ if (evt.currentTarget !== container) {
+ // Nested containers are not processed multiple times
+ return;
+ }
+
+ // initialize all rows with the correct row action
+ $('table.action tr, table.table-row-selectable tr', container).each(function(idx, el) {
+
+ // decide which row action to use: links declared with the class rowaction take
+ // the highest precedence before hrefs defined in the tr itself and regular links
+ var $a = $('a[href].rowaction', el).first();
+ if ($a.length) {
+ $(el).attr('href', $a.attr('href'));
+ return;
+ }
+ if ($(el).attr('href') && $(el).attr('href').length) {
+ return;
+ }
+ $a = $('a[href]', el).first();
+ if ($a.length) {
+ $(el).attr('href', $a.attr('href'));
+ }
+ });
+
+ // draw all active selections that have disappeared on reload
+ _this.tables().each(function(i, el) {
+ new Selection(el, _this.icinga).refresh();
+ });
+
+ // update displayed selection counter
+ var table = new Selection(_this.tables(container).first());
+ $(container).find('.selection-info-count').text(table.selections().length);
+ };
+
+ ActionTable.prototype.beforeRender = function(evt) {
+ var container = evt.target;
+ var _this = evt.data.self;
+
+ if (evt.currentTarget !== container) {
+ // Nested containers are not processed multiple times
+ return;
+ }
+
+ var active = _this.tables().find('tr.active');
+ if (active.length) {
+ $(container).data('icinga-actiontable-former-href', active.attr('href'));
+ }
+ };
+
+ ActionTable.prototype.clearAll = function () {
+ var _this = this;
+ this.tables().each(function () {
+ new Selection(this, _this.icinga).clear();
+ });
+ $('.selection-info-count').text('0');
+ };
+
+ Icinga.Behaviors.ActionTable = ActionTable;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/behavior/application-state.js b/public/js/icinga/behavior/application-state.js
new file mode 100644
index 0000000..213c96c
--- /dev/null
+++ b/public/js/icinga/behavior/application-state.js
@@ -0,0 +1,40 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var ApplicationState = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', '#layout', this.onRendered, this);
+ this.icinga = icinga;
+ };
+
+ ApplicationState.prototype = new Icinga.EventListener();
+
+ ApplicationState.prototype.onRendered = function(e) {
+ if (e.currentTarget !== e.target) {
+ // Nested containers are ignored
+ return;
+ }
+
+ if (! $('#application-state').length
+ && ! $('#login').length
+ && ! $('#guest-error').length
+ && ! $('#setup').length
+ ) {
+ var _this = e.data.self;
+
+ $('#layout').append(
+ '<div id="application-state" class="container" style="display: none" data-icinga-url="'
+ + _this.icinga.loader.baseUrl
+ + '/application-state" data-icinga-refresh="60"></div>'
+ );
+ }
+ };
+
+ Icinga.Behaviors.ApplicationState = ApplicationState;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/autofocus.js b/public/js/icinga/behavior/autofocus.js
new file mode 100644
index 0000000..e131d9e
--- /dev/null
+++ b/public/js/icinga/behavior/autofocus.js
@@ -0,0 +1,28 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var Autofocus = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', this.onRendered, this);
+ };
+
+ Autofocus.prototype = new Icinga.EventListener();
+
+ Autofocus.prototype.onRendered = function(e) {
+ setTimeout(function() {
+ if (document.activeElement === e.target
+ || document.activeElement === document.body
+ ) {
+ e.data.self.icinga.ui.focusElement($(e.target).find('.autofocus'));
+ }
+ }, 0);
+ };
+
+ Icinga.Behaviors.Autofocus = Autofocus;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/collapsible.js b/public/js/icinga/behavior/collapsible.js
new file mode 100644
index 0000000..c0b5349
--- /dev/null
+++ b/public/js/icinga/behavior/collapsible.js
@@ -0,0 +1,463 @@
+/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+;(function(Icinga) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ let $ = window.$;
+
+ try {
+ $ = require('icinga/icinga-php-library/notjQuery');
+ } catch (e) {
+ console.warn('[Collapsible] notjQuery unavailable. Using jQuery for now');
+ }
+
+ /**
+ * Behavior for collapsible containers.
+ *
+ * @param icinga Icinga The current Icinga Object
+ */
+ class Collapsible extends Icinga.EventListener {
+ constructor(icinga) {
+ super(icinga);
+
+ this.on('layout-change', this.onLayoutChange, this);
+ this.on('rendered', '#main > .container, #modal-content', this.onRendered, this);
+ this.on('click', '.collapsible + .collapsible-control, .collapsible .collapsible-control',
+ this.onControlClicked, this);
+
+ this.icinga = icinga;
+ this.defaultVisibleRows = 2;
+ this.defaultVisibleHeight = 36;
+
+ this.state = new Icinga.Storage.StorageAwareMap.withStorage(
+ Icinga.Storage.BehaviorStorage('collapsible'),
+ 'expanded'
+ )
+ .on('add', this.onExpand, this)
+ .on('delete', this.onCollapse, this);
+ }
+
+ /**
+ * Initializes all collapsibles. Triggered on rendering of a container.
+ *
+ * @param event Event The `onRender` event triggered by the rendered container
+ */
+ onRendered(event) {
+ let _this = event.data.self,
+ toCollapse = [],
+ toExpand = [];
+
+ event.target.querySelectorAll('.collapsible').forEach(collapsible => {
+ // Assumes that any newly rendered elements are expanded
+ if (! ('canCollapse' in collapsible.dataset) && _this.canCollapse(collapsible)) {
+ if (_this.setupCollapsible(collapsible)) {
+ toCollapse.push([collapsible, _this.calculateCollapsedHeight(collapsible)]);
+ } else if (_this.isDetails(collapsible)) {
+ // Except if it's a <details> element, which may not be expanded by default
+ toExpand.push(collapsible);
+ }
+ }
+ });
+
+ // Elements are all collapsed in a row now, after height calculations are done.
+ // This avoids reflows since instantly collapsing an element will cause one if
+ // the height of the next element is being calculated.
+ for (const collapseInfo of toCollapse) {
+ _this.collapse(collapseInfo[0], collapseInfo[1]);
+ }
+
+ for (const collapsible of toExpand) {
+ _this.expand(collapsible);
+ }
+ }
+
+ /**
+ * Updates all collapsibles.
+ *
+ * @param event Event The `layout-change` event triggered by window resizing or column changes
+ */
+ onLayoutChange(event) {
+ let _this = event.data.self;
+ let toCollapse = [];
+
+ document.querySelectorAll('.collapsible').forEach(collapsible => {
+ if ('canCollapse' in collapsible.dataset) {
+ if (! _this.canCollapse(collapsible)) {
+ let toggleSelector = collapsible.dataset.toggleElement;
+ if (! this.isDetails(collapsible)) {
+ if (! toggleSelector) {
+ collapsible.nextElementSibling.remove();
+ } else {
+ let toggle = document.getElementById(toggleSelector);
+ if (toggle) {
+ toggle.classList.remove('collapsed');
+ delete toggle.dataset.canCollapse;
+ }
+ }
+ }
+
+ delete collapsible.dataset.canCollapse;
+ _this.expand(collapsible);
+ }
+ } else if (_this.canCollapse(collapsible) && _this.setupCollapsible(collapsible)) {
+ // It's expanded but shouldn't
+ toCollapse.push([collapsible, _this.calculateCollapsedHeight(collapsible)]);
+ }
+ });
+
+ setTimeout(function () {
+ for (const collapseInfo of toCollapse) {
+ _this.collapse(collapseInfo[0], collapseInfo[1]);
+ }
+ }, 0);
+ }
+
+ /**
+ * A collapsible got expanded in another window, try to apply this here as well
+ *
+ * @param {string} collapsiblePath
+ */
+ onExpand(collapsiblePath) {
+ let collapsible = document.querySelector(collapsiblePath);
+
+ if (collapsible && 'canCollapse' in collapsible.dataset) {
+ if ('stateCollapses' in collapsible.dataset) {
+ this.collapse(collapsible, this.calculateCollapsedHeight(collapsible));
+ } else {
+ this.expand(collapsible);
+ }
+ }
+ }
+
+ /**
+ * A collapsible got collapsed in another window, try to apply this here as well
+ *
+ * @param {string} collapsiblePath
+ */
+ onCollapse(collapsiblePath) {
+ let collapsible = document.querySelector(collapsiblePath);
+
+ if (collapsible && this.canCollapse(collapsible)) {
+ if ('stateCollapses' in collapsible.dataset) {
+ this.expand(collapsible);
+ } else {
+ this.collapse(collapsible, this.calculateCollapsedHeight(collapsible));
+ }
+ }
+ }
+
+ /**
+ * Event handler for toggling collapsibles. Switches the collapsed state of the respective container.
+ *
+ * @param event Event The `onClick` event triggered by the clicked collapsible-control element
+ */
+ onControlClicked(event) {
+ let _this = event.data.self,
+ target = event.currentTarget;
+
+ let collapsible = target.previousElementSibling;
+ if ('collapsibleAt' in target.dataset) {
+ collapsible = document.querySelector(target.dataset.collapsibleAt);
+ } else if (! collapsible) {
+ collapsible = target.closest('.collapsible');
+ }
+
+ if (! collapsible) {
+ _this.icinga.logger.error(
+ '[Collapsible] Collapsible control has no associated .collapsible: ', target);
+
+ return;
+ } else if ('noPersistence' in collapsible.dataset) {
+ if (collapsible.classList.contains('collapsed')) {
+ _this.expand(collapsible);
+ } else {
+ _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible));
+ }
+ } else {
+ let collapsiblePath = _this.icinga.utils.getCSSPath(collapsible),
+ stateCollapses = 'stateCollapses' in collapsible.dataset;
+
+ if (_this.state.has(collapsiblePath)) {
+ _this.state.delete(collapsiblePath);
+
+ if (stateCollapses) {
+ _this.expand(collapsible);
+ } else {
+ _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible));
+ }
+ } else {
+ _this.state.set(collapsiblePath);
+
+ if (stateCollapses) {
+ _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible));
+ } else {
+ _this.expand(collapsible);
+ }
+ }
+ }
+
+ if (_this.isDetails(collapsible)) {
+ // The browser handles these clicks as well, and would toggle the state again
+ event.preventDefault();
+ }
+ }
+
+ /**
+ * Setup the given collapsible
+ *
+ * @param collapsible The given collapsible container element
+ *
+ * @returns {boolean} Whether it needs to collapse or not
+ */
+ setupCollapsible(collapsible) {
+ if (this.isDetails(collapsible)) {
+ let summary = collapsible.querySelector(':scope > summary');
+ if (! summary.classList.contains('collapsible-control')) {
+ summary.classList.add('collapsible-control');
+ }
+
+ if (collapsible.open) {
+ collapsible.dataset.stateCollapses = '';
+ }
+ } else if (!! collapsible.dataset.toggleElement) {
+ let toggleSelector = collapsible.dataset.toggleElement,
+ toggle = collapsible.querySelector(toggleSelector),
+ externalToggle = false;
+ if (! toggle) {
+ if (collapsible.nextElementSibling && collapsible.nextElementSibling.matches(toggleSelector)) {
+ toggle = collapsible.nextElementSibling;
+ } else {
+ externalToggle = true;
+ toggle = document.getElementById(toggleSelector);
+ }
+ }
+
+ if (! toggle) {
+ if (externalToggle) {
+ this.icinga.logger.error(
+ '[Collapsible] External control with id `'
+ + toggleSelector
+ + '` not found for .collapsible',
+ collapsible
+ );
+ } else {
+ this.icinga.logger.error(
+ '[Collapsible] Control `' + toggleSelector + '` not found in .collapsible', collapsible);
+ }
+
+ return false;
+ } else if (externalToggle) {
+ collapsible.dataset.hasExternalToggle = '';
+
+ toggle.dataset.canCollapse = '';
+ toggle.dataset.collapsibleAt = this.icinga.utils.getCSSPath(collapsible);
+ $(toggle).on('click', e => {
+ // Only required as onControlClicked() is compatible with Icinga.EventListener
+ e.data = { self: this };
+ this.onControlClicked(e);
+ });
+ } else if (! toggle.classList.contains('collapsible-control')) {
+ toggle.classList.add('collapsible-control');
+ }
+ } else {
+ setTimeout(function () {
+ let collapsibleControl = document
+ .getElementById('collapsible-control-ghost')
+ .cloneNode(true);
+ collapsibleControl.removeAttribute('id');
+ collapsible.parentNode.insertBefore(collapsibleControl, collapsible.nextElementSibling);
+ }, 0);
+ }
+
+ collapsible.dataset.canCollapse = '';
+
+ if ('noPersistence' in collapsible.dataset) {
+ return ! ('stateCollapses' in collapsible.dataset);
+ }
+
+ if ('stateCollapses' in collapsible.dataset) {
+ return this.state.has(this.icinga.utils.getCSSPath(collapsible));
+ } else {
+ return ! this.state.has(this.icinga.utils.getCSSPath(collapsible));
+ }
+ }
+
+ /**
+ * Return an appropriate row element selector
+ *
+ * @param collapsible The given collapsible container element
+ *
+ * @returns {string}
+ */
+ getRowSelector(collapsible) {
+ if (!! collapsible.dataset.visibleHeight) {
+ return '';
+ }
+
+ if (collapsible.tagName === 'TABLE') {
+ return ':scope > tbody > tr';
+ } else if (collapsible.tagName === 'UL' || collapsible.tagName === 'OL') {
+ return ':scope > li:not(.collapsible-control)';
+ }
+
+ return '';
+ }
+
+ /**
+ * Check whether the given collapsible needs to collapse
+ *
+ * @param collapsible The given collapsible container element
+ *
+ * @returns {boolean}
+ */
+ canCollapse(collapsible) {
+ if (this.isDetails(collapsible)) {
+ return collapsible.querySelector(':scope > summary') !== null;
+ }
+
+ let rowSelector = this.getRowSelector(collapsible);
+ if (!! rowSelector) {
+ let visibleRows = Number(collapsible.dataset.visibleRows);
+ if (isNaN(visibleRows)) {
+ visibleRows = this.defaultVisibleRows;
+ } else if (visibleRows === 0) {
+ return true;
+ }
+
+ return collapsible.querySelectorAll(rowSelector).length > visibleRows * 2;
+ } else {
+ let maxHeight = Number(collapsible.dataset.visibleHeight);
+ if (isNaN(maxHeight)) {
+ maxHeight = this.defaultVisibleHeight;
+ } else if (maxHeight === 0) {
+ return true;
+ }
+
+ let actualHeight = collapsible.scrollHeight - parseFloat(
+ window.getComputedStyle(collapsible).getPropertyValue('padding-top')
+ );
+
+ return actualHeight >= maxHeight * 2;
+ }
+ }
+
+ /**
+ * Calculate the height the given collapsible should have when collapsed
+ *
+ * @param collapsible
+ */
+ calculateCollapsedHeight(collapsible) {
+ let height;
+
+ if (this.isDetails(collapsible)) {
+ return -1;
+ }
+
+ let rowSelector = this.getRowSelector(collapsible);
+ if (!! rowSelector) {
+ height = collapsible.scrollHeight;
+ height -= parseFloat(window.getComputedStyle(collapsible).getPropertyValue('padding-bottom'));
+
+ let visibleRows = Number(collapsible.dataset.visibleRows);
+ if (isNaN(visibleRows)) {
+ visibleRows = this.defaultVisibleRows;
+ }
+
+ let rows = Array.from(collapsible.querySelectorAll(rowSelector)).slice(visibleRows);
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+
+ if (row.previousElementSibling === null) { // very first element
+ height -= row.offsetHeight;
+ height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-top'));
+ } else if (i < rows.length - 1) { // every element but the last one
+ let prevBottomBorderAt = row.previousElementSibling.offsetTop;
+ prevBottomBorderAt += row.previousElementSibling.offsetHeight;
+ height -= row.offsetTop - prevBottomBorderAt + row.offsetHeight;
+ } else { // the last element
+ height -= row.offsetHeight;
+ height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-top'));
+ height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-bottom'));
+ }
+ }
+ } else {
+ height = Number(collapsible.dataset.visibleHeight);
+ if (isNaN(height)) {
+ height = this.defaultVisibleHeight;
+ }
+
+ height += parseFloat(window.getComputedStyle(collapsible).getPropertyValue('padding-top'));
+
+ if (
+ !! collapsible.dataset.toggleElement
+ && ! ('hasExternalToggle' in collapsible.dataset)
+ && (! collapsible.nextElementSibling
+ || ! collapsible.nextElementSibling.matches(collapsible.dataset.toggleElement))
+ ) {
+ let toggle = collapsible.querySelector(collapsible.dataset.toggleElement);
+ height += toggle.offsetHeight; // TODO: Very expensive at times. (50ms+) Check why!
+ height += parseFloat(window.getComputedStyle(toggle).getPropertyValue('margin-top'));
+ height += parseFloat(window.getComputedStyle(toggle).getPropertyValue('margin-bottom'));
+ }
+ }
+
+ return height;
+ }
+
+ /**
+ * Collapse the given collapsible
+ *
+ * @param collapsible The given collapsible container element
+ * @param toHeight {int} The height in pixels to collapse to
+ */
+ collapse(collapsible, toHeight) {
+ if (this.isDetails(collapsible)) {
+ collapsible.open = false;
+ } else {
+ collapsible.style.cssText = 'display: block; height: ' + toHeight + 'px; padding-bottom: 0';
+
+ if ('hasExternalToggle' in collapsible.dataset) {
+ document.getElementById(collapsible.dataset.toggleElement).classList.add('collapsed');
+ }
+ }
+
+ collapsible.classList.add('collapsed');
+ }
+
+ /**
+ * Expand the given collapsible
+ *
+ * @param collapsible The given collapsible container element
+ */
+ expand(collapsible) {
+ collapsible.classList.remove('collapsed');
+
+ if (this.isDetails(collapsible)) {
+ collapsible.open = true;
+ } else {
+ collapsible.style.cssText = '';
+
+ if ('hasExternalToggle' in collapsible.dataset) {
+ document.getElementById(collapsible.dataset.toggleElement).classList.remove('collapsed');
+ }
+ }
+ }
+
+ /**
+ * Get whether the given collapsible is a <details> element
+ *
+ * @param collapsible
+ *
+ * @return {Boolean}
+ */
+ isDetails(collapsible) {
+ return collapsible.tagName === 'DETAILS';
+ }
+ }
+
+ Icinga.Behaviors.Collapsible = Collapsible;
+
+})(Icinga);
diff --git a/public/js/icinga/behavior/datetime-picker.js b/public/js/icinga/behavior/datetime-picker.js
new file mode 100644
index 0000000..fb0ddff
--- /dev/null
+++ b/public/js/icinga/behavior/datetime-picker.js
@@ -0,0 +1,222 @@
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+/**
+ * DatetimePicker - Behavior for inputs that should show a date and time picker
+ */
+;(function(Icinga, $) {
+
+ 'use strict';
+
+ try {
+ var Flatpickr = require('icinga/icinga-php-library/vendor/flatpickr');
+ var notjQuery = require('icinga/icinga-php-library/notjQuery');
+ } catch (e) {
+ console.warn('Unable to provide datetime picker. Libraries not available:', e);
+ return;
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Behavior for datetime pickers.
+ *
+ * @param icinga {Icinga} The current Icinga Object
+ */
+ var DatetimePicker = function(icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.icinga = icinga;
+
+ /**
+ * The formats the server expects
+ *
+ * In a syntax flatpickr understands. Based on https://flatpickr.js.org/formatting/
+ *
+ * @type {string}
+ */
+ this.server_full_format = 'Y-m-d\\TH:i:S';
+ this.server_date_format = 'Y-m-d';
+ this.server_time_format = 'H:i:S';
+
+ /**
+ * The flatpickr instances created
+ *
+ * @type {Map<Flatpickr, string>}
+ * @private
+ */
+ this._pickers = new Map();
+
+ this.on('rendered', '#main > .container, #modal-content', this.onRendered, this);
+ this.on('close-column', this.onCloseContainer, this);
+ this.on('close-modal', this.onCloseContainer, this);
+ };
+
+ DatetimePicker.prototype = new Icinga.EventListener();
+
+ /**
+ * Add flatpickr widget on selected inputs
+ *
+ * @param event {Event}
+ */
+ DatetimePicker.prototype.onRendered = function(event) {
+ var _this = event.data.self;
+ var containerId = event.target.dataset.icingaContainerId;
+ var inputs = event.target.querySelectorAll('input[data-use-datetime-picker]');
+
+ // Cleanup left-over pickers from the previous content
+ _this.cleanupPickers(containerId);
+
+ $.each(inputs, function () {
+ if (this.type !== 'text') {
+ // Ignore native inputs. Browser widgets are (mostly) superior.
+ // TODO: This makes the type distinction below useless.
+ // Refactor this once we decided how we continue here in the future.
+ return;
+ }
+
+ var server_format = _this.server_full_format;
+ if (this.type === 'date') {
+ server_format = _this.server_date_format;
+ } else if (this.type === 'time') {
+ server_format = _this.server_time_format;
+ }
+
+ // Inject calendar container into a new empty div, inside the column/modal but outside the form.
+ // See https://github.com/flatpickr/flatpickr/issues/2054 for details.
+ var appendTo = document.createElement('div');
+ this.form.parentNode.insertBefore(appendTo, this.form.nextSibling);
+
+ var enableTime = server_format !== _this.server_date_format;
+ var disableDate = server_format === _this.server_time_format;
+ var dateTimeFormatter = _this.createFormatter(! disableDate, enableTime);
+ var options = {
+ locale: _this.loadFlatpickrLocale(),
+ appendTo: appendTo,
+ altInput: true,
+ enableTime: enableTime,
+ noCalendar: disableDate,
+ dateFormat: server_format,
+ formatDate: function (date, format, locale) {
+ return format === this.dateFormat
+ ? Flatpickr.formatDate(date, format, locale)
+ : dateTimeFormatter.format(date);
+ }
+ };
+
+ for (name in this.dataset) {
+ if (name.length > 9 && name.substr(0, 9) === 'flatpickr') {
+ var value = this.dataset[name];
+ if (value === '') {
+ value = true;
+ }
+
+ options[name.charAt(9).toLowerCase() + name.substr(10)] = value;
+ }
+ }
+
+ var element = this;
+ if (!! options.wrap) {
+ element = this.parentNode;
+ }
+
+ var fp = Flatpickr(element, options);
+ fp.calendarContainer.classList.add('icinga-datetime-picker');
+
+ if (! !!options.wrap) {
+ this.parentNode.insertBefore(_this.renderIcon(), fp.altInput.nextSibling);
+ }
+
+ _this._pickers.set(fp, containerId);
+ });
+ };
+
+ /**
+ * Cleanup all flatpickr instances in the closed container
+ *
+ * @param event {Event}
+ */
+ DatetimePicker.prototype.onCloseContainer = function (event) {
+ var _this = event.data.self;
+ var containerId = event.target.dataset.icingaContainerId;
+
+ _this.cleanupPickers(containerId);
+ };
+
+ /**
+ * Destroy all flatpickr instances in the container with the given id
+ *
+ * @param containerId {String}
+ */
+ DatetimePicker.prototype.cleanupPickers = function (containerId) {
+ this._pickers.forEach(function (cId, fp) {
+ if (cId === containerId) {
+ this._pickers.delete(fp);
+ fp.destroy();
+ }
+ }, this);
+ };
+
+ /**
+ * Close all other flatpickr instances and keep the given one
+ *
+ * @param fp {Flatpickr}
+ */
+ DatetimePicker.prototype.closePickers = function (fp) {
+ var containerId = this._pickers.get(fp);
+ this._pickers.forEach(function (cId, fp2) {
+ if (cId === containerId && fp2 !== fp) {
+ fp2.close();
+ }
+ }, this);
+ };
+
+ DatetimePicker.prototype.createFormatter = function (withDate, withTime) {
+ var options = {};
+ if (withDate) {
+ options.year = 'numeric';
+ options.month = 'numeric';
+ options.day = 'numeric';
+ }
+ if (withTime) {
+ options.hour = 'numeric';
+ options.minute = 'numeric';
+ options.timeZoneName = 'short';
+ options.timeZone = this.icinga.config.timezone;
+ }
+
+ return new Intl.DateTimeFormat([this.icinga.config.locale, 'en'], options);
+ };
+
+ DatetimePicker.prototype.loadFlatpickrLocale = function () {
+ switch (this.icinga.config.locale) {
+ case 'ar':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/ar').Arabic;
+ case 'de':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/de').German;
+ case 'es':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/es').Spanish;
+ case 'fi':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/fi').Finnish;
+ case 'fr':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/fr').French;
+ case 'it':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/it').Italian;
+ case 'ja':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/ja').Japanese;
+ case 'pt':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/pt').Portuguese;
+ case 'ru':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/ru').Russian;
+ case 'uk':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/uk').Ukrainian;
+ default:
+ return 'default';
+ }
+ };
+
+ DatetimePicker.prototype.renderIcon = function () {
+ return notjQuery.render('<i class="icon fa fa-calendar" role="image"></i>');
+ };
+
+ Icinga.Behaviors.DatetimePicker = DatetimePicker;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/detach.js b/public/js/icinga/behavior/detach.js
new file mode 100644
index 0000000..16fe157
--- /dev/null
+++ b/public/js/icinga/behavior/detach.js
@@ -0,0 +1,73 @@
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Behavior.Detach
+ *
+ * Detaches DOM elements before an auto-refresh and attaches them back afterwards
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ function Detach(icinga) {
+ Icinga.EventListener.call(this, icinga);
+ }
+
+ Detach.prototype = new Icinga.EventListener();
+
+ /**
+ * Mutates the HTML before it is placed in the DOM after a reload
+ *
+ * @param content {string} The content to be rendered
+ * @param $container {jQuery} The target container
+ * @param action {string} The URL that caused the reload
+ * @param autorefresh {bool} Whether the rendering is due to an auto-refresh
+ *
+ * @return {string|null} The content to be rendered or null, when nothing should be changed
+ */
+ Detach.prototype.renderHook = function(content, $container, action, autorefresh) {
+ // Exit early
+ if (! autorefresh) {
+ return content;
+ } else {
+ var containerId = $container.attr('id');
+
+ if (containerId === 'menu' || containerId === 'application-state') {
+ return content;
+ }
+ }
+
+ if (! $container.find('.detach:first').length) {
+ return content;
+ }
+
+ var $content = $('<div></div>').append(content);
+ var icinga = this.icinga;
+
+ $content.find('.detach').each(function() {
+ // Selector only works w/ IDs because it was initially built to work w/ absolute paths only
+ var $detachTarget = $(this);
+ var detachTargetId = $detachTarget.attr('id');
+ if (detachTargetId === undefined) {
+ return;
+ }
+
+ var selector = '#' + detachTargetId + ':first';
+ var $detachSource = $container.find(selector);
+
+ if ($detachSource.length) {
+ icinga.logger.debug('Detaching ' + selector);
+ $detachSource.detach();
+ $detachTarget.replaceWith($detachSource);
+ $detachTarget.remove();
+ }
+ });
+
+ return $content.html();
+ };
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ Icinga.Behaviors.Detach = Detach;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/behavior/dropdown.js b/public/js/icinga/behavior/dropdown.js
new file mode 100644
index 0000000..691e634
--- /dev/null
+++ b/public/js/icinga/behavior/dropdown.js
@@ -0,0 +1,66 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+;(function(Icinga, $) {
+
+ "use strict";
+
+ /**
+ * Toggle the CSS class active of the dropdown navigation item
+ *
+ * Called when the dropdown toggle has been activated via mouse or keyobard. This will expand/collpase the dropdown
+ * menu according to CSS.
+ *
+ * @param {object} e Event
+ */
+ function setActive(e) {
+ $(this).parent().toggleClass('active');
+ }
+
+ /**
+ * Clear active state of the dropdown navigation item when the mouse leaves the navigation item
+ *
+ * @param {object} e Event
+ */
+ function clearActive(e) {
+ $(this).removeClass('active');
+ }
+
+ /**
+ * Clear active state of the dropdown navigation item when the navigation items loses focus
+ *
+ * @param {object} e Event
+ */
+ function clearFocus(e) {
+ var $dropdown = $(this);
+ // Timeout is required to wait for the next element in the DOM to receive focus
+ setTimeout(function() {
+ if (! $.contains($dropdown[0], document.activeElement)) {
+ $dropdown.removeClass('active');
+ }
+ }, 10);
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Behavior for dropdown navigation items
+ *
+ * The dropdown behavior listens for activity on dropdown navigation items for toggling the CSS class
+ * active on them. CSS is responsible for the expanded and collapsed state.
+ *
+ * @param {Icinga} icinga
+ *
+ * @constructor
+ */
+ var Dropdown = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('click', '.dropdown-nav-item > a', setActive, this);
+ this.on('mouseleave', '.dropdown-nav-item', clearActive, this);
+ this.on('focusout', '.dropdown-nav-item', clearFocus, this);
+ };
+
+ Dropdown.prototype = new Icinga.EventListener();
+
+ Icinga.Behaviors.Dropdown = Dropdown;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/filtereditor.js b/public/js/icinga/behavior/filtereditor.js
new file mode 100644
index 0000000..ffcad01
--- /dev/null
+++ b/public/js/icinga/behavior/filtereditor.js
@@ -0,0 +1,77 @@
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Behavior.FilterEditor
+ *
+ * Initially expanded, but collapsable subtrees
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ var containerId = /^col(\d+)$/;
+ var filterEditors = {};
+
+ function FilterEditor(icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ this.on('beforerender', '#main > .container', this.beforeRender, this);
+ this.on('rendered', '#main > .container', this.onRendered, this);
+ }
+
+ FilterEditor.prototype = new Icinga.EventListener();
+
+ FilterEditor.prototype.beforeRender = function(event) {
+ if (event.currentTarget !== event.target) {
+ // Nested containers are ignored
+ return;
+ }
+
+ var $container = $(event.target);
+ var match = containerId.exec($container.attr('id'));
+
+ if (match !== null) {
+ var id = match[1];
+ var subTrees = {};
+ filterEditors[id] = subTrees;
+
+ $container.find('.tree .handle').each(function () {
+ var $li = $(this).closest('li');
+
+ subTrees[$li.find('select').first().attr('name')] = $li.hasClass('collapsed');
+ });
+ }
+ };
+
+ FilterEditor.prototype.onRendered = function(event) {
+ if (event.currentTarget !== event.target) {
+ // Nested containers are ignored
+ return;
+ }
+
+ var $container = $(event.target);
+ var match = containerId.exec($container.attr('id'));
+
+ if (match !== null) {
+ var id = match[1];
+
+ if (typeof filterEditors[id] !== "undefined") {
+ var subTrees = filterEditors[id];
+ delete filterEditors[id];
+
+ $container.find('.tree .handle').each(function () {
+ var $li = $(this).closest('li');
+ var name = $li.find('select').first().attr('name');
+ if (typeof subTrees[name] !== "undefined" && subTrees[name] !== $li.hasClass('collapsed')) {
+ $li.toggleClass('collapsed');
+ }
+ });
+ }
+ }
+ };
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ Icinga.Behaviors.FilterEditor = FilterEditor;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/behavior/flyover.js b/public/js/icinga/behavior/flyover.js
new file mode 100644
index 0000000..207d577
--- /dev/null
+++ b/public/js/icinga/behavior/flyover.js
@@ -0,0 +1,85 @@
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Behavior.Flyover
+ *
+ * A toggleable flyover
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ var expandedFlyovers = {};
+
+ function Flyover(icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ this.on('rendered', '#main > .container', this.onRendered, this);
+ this.on('click', this.onClick, this);
+ this.on('click', '.flyover-toggle', this.onClickFlyoverToggle, this);
+ }
+
+ Flyover.prototype = new Icinga.EventListener();
+
+ Flyover.prototype.onRendered = function(event) {
+ // Re-expand expanded containers after an auto-refresh
+
+ $(event.target).find('.flyover').each(function() {
+ var $this = $(this);
+
+ if (typeof expandedFlyovers['#' + $this.attr('id')] !== 'undefined') {
+ var $container = $this.closest('.container');
+
+ if ($this.offset().left - $container.offset().left > $container.innerWidth() / 2) {
+ $this.addClass('flyover-right');
+ }
+
+ $this.toggleClass('flyover-expanded');
+ }
+ });
+ };
+
+ Flyover.prototype.onClick = function(event) {
+ // Close flyover on click outside the flyover
+ var $target = $(event.target);
+
+ if (! $target.closest('.flyover').length) {
+ var _this = event.data.self;
+ $.each(expandedFlyovers, function (id) {
+ _this.onClickFlyoverToggle({target: $('.flyover-toggle', id)[0]});
+ });
+ }
+ };
+
+ Flyover.prototype.onClickFlyoverToggle = function(event) {
+ var $flyover = $(event.target).closest('.flyover');
+
+ $flyover.toggleClass('flyover-expanded');
+
+ var $container = $flyover.closest('.container');
+ if ($flyover.hasClass('flyover-expanded')) {
+ if ($flyover.offset().left - $container.offset().left > $container.innerWidth() / 2) {
+ $flyover.addClass('flyover-right');
+ }
+
+ if ($flyover.is('[data-flyover-suspends-auto-refresh]')) {
+ $container[0].dataset.suspendAutorefresh = '';
+ }
+
+ expandedFlyovers['#' + $flyover.attr('id')] = null;
+ } else {
+ $flyover.removeClass('flyover-right');
+
+ if ($flyover.is('[data-flyover-suspends-auto-refresh]')) {
+ delete $container[0].dataset.suspendAutorefresh;
+ }
+
+ delete expandedFlyovers['#' + $flyover.attr('id')];
+ }
+ };
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ Icinga.Behaviors.Flyover = Flyover;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/form.js b/public/js/icinga/behavior/form.js
new file mode 100644
index 0000000..ca9db3b
--- /dev/null
+++ b/public/js/icinga/behavior/form.js
@@ -0,0 +1,96 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Controls behavior of form elements, depending reload and
+ */
+(function(Icinga, $) {
+
+ "use strict";
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var Form = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', '.container', this.onRendered, this);
+
+ // store the modification state of all input fields
+ this.inputs = new WeakMap();
+ };
+ Form.prototype = new Icinga.EventListener();
+
+ /**
+ * @param event
+ */
+ Form.prototype.onRendered = function (event) {
+ var _this = event.data.self;
+ var container = event.target;
+
+ container.querySelectorAll('form input').forEach(function (input) {
+ if (! _this.inputs.has(input) && input.type !== 'hidden') {
+ _this.inputs.set(input, input.value);
+ _this.icinga.logger.debug('registering "' + input.value + '" as original input value');
+ }
+ });
+ };
+
+ /**
+ * Mutates the HTML before it is placed in the DOM after a reload
+ *
+ * @param content {String} The content to be rendered
+ * @param $container {jQuery} The target container where the html will be rendered in
+ * @param action {String} The action-url that caused the reload
+ * @param autorefresh {Boolean} Whether the rendering is due to an autoRefresh
+ * @param autoSubmit {Boolean} Whether the rendering is due to an autoSubmit
+ *
+ * @returns {string|NULL} The content to be rendered, or NULL, when nothing should be changed
+ */
+ Form.prototype.renderHook = function(content, $container, action, autorefresh, autoSubmit) {
+ if ($container.attr('id') === 'menu') {
+ var $search = $container.find('#search');
+ if ($search[0] === document.activeElement) {
+ return null;
+ }
+ if ($search.length) {
+ var $content = $('<div></div>').append(content);
+ $content.find('#search').attr('value', $search.val()).addClass('active');
+ return $content.html();
+ }
+ return content;
+ }
+
+ if (! autorefresh || autoSubmit) {
+ return content;
+ }
+
+ var _this = this;
+ var changed = false;
+ $container[0].querySelectorAll('form input').forEach(function (input) {
+ if (_this.inputs.has(input) && _this.inputs.get(input) !== input.value) {
+ changed = true;
+ _this.icinga.logger.debug(
+ '"' + _this.inputs.get(input) + '" was changed ("' + input.value + '") and aborts reload...'
+ );
+ }
+ });
+ if (changed) {
+ return null;
+ }
+
+ var origFocus = document.activeElement;
+ var containerId = $container.attr('id');
+ if ($container.has(origFocus).length
+ && $(origFocus).length
+ && ! $(origFocus).hasClass('autofocus')
+ && $(origFocus).closest('form').length
+ && $(origFocus).not(':input[type=button], :input[type=submit], :input[type=reset]').length
+ ) {
+ this.icinga.logger.debug('Not changing content for ' + containerId + ' form has focus');
+ return null;
+ }
+
+ return content;
+ };
+
+ Icinga.Behaviors.Form = Form;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/behavior/input-enrichment.js b/public/js/icinga/behavior/input-enrichment.js
new file mode 100644
index 0000000..1540941
--- /dev/null
+++ b/public/js/icinga/behavior/input-enrichment.js
@@ -0,0 +1,148 @@
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+/**
+ * InputEnrichment - Behavior for forms with enriched inputs
+ */
+(function(Icinga) {
+
+ "use strict";
+
+ try {
+ var SearchBar = require('icinga/icinga-php-library/widget/SearchBar');
+ var SearchEditor = require('icinga/icinga-php-library/widget/SearchEditor');
+ var FilterInput = require('icinga/icinga-php-library/widget/FilterInput');
+ var TermInput = require('icinga/icinga-php-library/widget/TermInput');
+ var Completer = require('icinga/icinga-php-library/widget/Completer');
+ } catch (e) {
+ console.warn('Unable to provide input enrichments. Libraries not available:', e);
+ return;
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * @param icinga
+ * @constructor
+ */
+ let InputEnrichment = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ this.on('beforerender', '#main > .container, #modal-content', this.onBeforeRender, this);
+ this.on('rendered', '#main > .container, #modal-content', this.onRendered, this);
+
+ /**
+ * Enriched inputs
+ *
+ * @type {WeakMap<object, SearchEditor|SearchBar|FilterInput|TermInput|Completer>}
+ * @private
+ */
+ this._enrichments = new WeakMap();
+
+ /**
+ * Cached enrichments
+ *
+ * Holds values only during the time between `beforerender` and `rendered`
+ *
+ * @type {{}}
+ * @private
+ */
+ this._cachedEnrichments = {};
+ };
+ InputEnrichment.prototype = new Icinga.EventListener();
+
+ /**
+ * @param data
+ */
+ InputEnrichment.prototype.update = function (data) {
+ var input = document.querySelector(data[0]);
+ if (input !== null && this._enrichments.has(input)) {
+ this._enrichments.get(input).updateTerms(data[1]);
+ }
+ };
+
+ /**
+ * @param event
+ * @param content
+ * @param action
+ * @param autorefresh
+ * @param scripted
+ */
+ InputEnrichment.prototype.onBeforeRender = function (event, content, action, autorefresh, scripted) {
+ if (! autorefresh) {
+ return;
+ }
+
+ let _this = event.data.self;
+ let inputs = event.target.querySelectorAll('[data-enrichment-type]');
+
+ // Remember current instances
+ inputs.forEach((input) => {
+ let enrichment = _this._enrichments.get(input);
+ if (enrichment) {
+ _this._cachedEnrichments[_this.icinga.utils.getDomPath(input).join(' > ')] = enrichment;
+ }
+ });
+ };
+
+ /**
+ * @param event
+ * @param autorefresh
+ * @param scripted
+ */
+ InputEnrichment.prototype.onRendered = function (event, autorefresh, scripted) {
+ let _this = event.data.self;
+ let container = event.target;
+
+ if (autorefresh) {
+ // Apply remembered instances
+ for (let inputPath in _this._cachedEnrichments) {
+ let enrichment = _this._cachedEnrichments[inputPath];
+ let input = container.querySelector(inputPath);
+ if (input !== null) {
+ enrichment.refresh(input);
+ _this._enrichments.set(input, enrichment);
+ } else {
+ enrichment.destroy();
+ }
+
+ delete _this._cachedEnrichments[inputPath];
+ }
+ }
+
+ // Create new instances
+ let inputs = container.querySelectorAll('[data-enrichment-type]');
+ inputs.forEach((input) => {
+ let enrichment = _this._enrichments.get(input);
+ if (! enrichment) {
+ switch (input.dataset.enrichmentType) {
+ case 'search-bar':
+ enrichment = (new SearchBar(input)).bind();
+ break;
+ case 'search-editor':
+ enrichment = (new SearchEditor(input)).bind();
+ break;
+ case 'filter':
+ enrichment = (new FilterInput(input)).bind();
+ enrichment.restoreTerms();
+
+ if (_this._enrichments.has(input.form)) {
+ _this._enrichments.get(input.form).setFilterInput(enrichment);
+ }
+
+ break;
+ case 'terms':
+ enrichment = (new TermInput(input)).bind();
+ enrichment.restoreTerms();
+ break;
+ case 'completion':
+ enrichment = (new Completer(input)).bind();
+ }
+
+ _this._enrichments.set(input, enrichment);
+ }
+ });
+ };
+
+ Icinga.Behaviors.InputEnrichment = InputEnrichment;
+
+})(Icinga);
diff --git a/public/js/icinga/behavior/modal.js b/public/js/icinga/behavior/modal.js
new file mode 100644
index 0000000..0083f76
--- /dev/null
+++ b/public/js/icinga/behavior/modal.js
@@ -0,0 +1,231 @@
+/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+;(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Behavior for modal dialogs.
+ *
+ * @param icinga {Icinga} The current Icinga Object
+ */
+ var Modal = function(icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ this.icinga = icinga;
+ this.$layout = $('#layout');
+ this.$ghost = $('#modal-ghost');
+
+ this.on('submit', '#modal form', this.onFormSubmit, this);
+ this.on('change', '#modal form select.autosubmit', this.onFormAutoSubmit, this);
+ this.on('change', '#modal form input.autosubmit', this.onFormAutoSubmit, this);
+ this.on('click', '[data-icinga-modal]', this.onModalToggleClick, this);
+ this.on('mousedown', '#layout > #modal', this.onModalLeave, this);
+ this.on('click', '.modal-header > button', this.onModalClose, this);
+ this.on('keydown', this.onKeyDown, this);
+ };
+
+ Modal.prototype = new Icinga.EventListener();
+
+ /**
+ * Event handler for toggling modals. Shows the link target in a modal dialog.
+ *
+ * @param event {Event} The `onClick` event triggered by the clicked modal-toggle element
+ * @returns {boolean}
+ */
+ Modal.prototype.onModalToggleClick = function(event) {
+ var _this = event.data.self;
+ var $a = $(event.currentTarget);
+ var url = $a.attr('href');
+ var $modal = _this.$ghost.clone();
+ var $urlTarget = _this.icinga.loader.getLinkTargetFor($a, false);
+
+ _this.modalOpener = event.currentTarget;
+
+ // Disable pointer events to block further function calls
+ _this.modalOpener.style.pointerEvents = 'none';
+
+ // Add showCompact, we don't want controls in a modal
+ url = _this.icinga.utils.addUrlFlag(url, 'showCompact');
+
+ // Set the toggle's base target on the modal to use it as redirect target
+ $modal.data('redirectTarget', $urlTarget);
+
+ // Final preparations, the id is required so that it's not `display:none` anymore
+ $modal.attr('id', 'modal');
+ _this.$layout.append($modal);
+
+ var req = _this.icinga.loader.loadUrl(url, $modal.find('#modal-content'));
+ req.addToHistory = false;
+ req.done(function () {
+ _this.setTitle($modal, req.$target.data('icingaTitle').replace(/\s::\s.*/, ''));
+ _this.show($modal);
+ _this.focus($modal);
+ });
+ req.fail(function (req, _, errorThrown) {
+ if (req.status >= 500) {
+ // Yes, that's done twice (by us and by the base fail handler),
+ // but `renderContentToContainer` does too many useful things..
+ _this.icinga.loader.renderContentToContainer(req.responseText, $urlTarget, req.action);
+ } else if (req.status > 0) {
+ var msg = $(req.responseText).find('.error-message').text();
+ if (msg && msg !== errorThrown) {
+ errorThrown += ': ' + msg;
+ }
+
+ _this.icinga.loader.createNotice('error', errorThrown);
+ }
+
+ _this.hide($modal);
+ });
+
+ return false;
+ };
+
+ /**
+ * Event handler for form submits within a modal.
+ *
+ * @param event {Event} The `submit` event triggered by a form within the modal
+ * @param $autoSubmittedBy {jQuery} The element triggering the auto submit, if any
+ * @returns {boolean}
+ */
+ Modal.prototype.onFormSubmit = function(event, $autoSubmittedBy) {
+ var _this = event.data.self;
+ var $form = $(event.currentTarget).closest('form');
+ var $modal = $form.closest('#modal');
+
+ var $button;
+ var $rememberedSubmittButton = $form.data('submitButton');
+ if (typeof $rememberedSubmittButton != 'undefined') {
+ if ($form.has($rememberedSubmittButton)) {
+ $button = $rememberedSubmittButton;
+ }
+ $form.removeData('submitButton');
+ }
+
+ var req = _this.icinga.loader.submitForm($form, $autoSubmittedBy, $button);
+ req.addToHistory = false;
+ req.$redirectTarget = $modal.data('redirectTarget');
+ req.done(function (data, textStatus, req) {
+ var title = req.getResponseHeader('X-Icinga-Title');
+ if (!! title) {
+ _this.setTitle($modal, decodeURIComponent(title).replace(/\s::\s.*/, ''));
+ }
+
+ if (req.getResponseHeader('X-Icinga-Redirect')) {
+ _this.hide($modal);
+ }
+ });
+
+ if (typeof $autoSubmittedBy === 'undefined') {
+ // otherwise the form is submitted several times by clicking the "Submit" button several times
+ $form.find('input[type=submit],button[type=submit],button:not([type])').prop('disabled', true);
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ };
+
+ /**
+ * Event handler for form auto submits within a modal.
+ *
+ * @param event {Event} The `change` event triggered by a form input within the modal
+ * @returns {boolean}
+ */
+ Modal.prototype.onFormAutoSubmit = function(event) {
+ return event.data.self.onFormSubmit(event, $(event.currentTarget));
+ };
+
+ /**
+ * Event handler for closing the modal. Closes it when the user clicks on the overlay.
+ *
+ * @param event {Event} The `click` event triggered by clicking on the overlay
+ */
+ Modal.prototype.onModalLeave = function(event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+
+ if ($target.is('#modal')) {
+ _this.hide($target);
+ }
+ };
+
+ /**
+ * Event handler for closing the modal. Closes it when the user clicks on the close button.
+ *
+ * @param event {Event} The `click` event triggered by clicking on the close button
+ */
+ Modal.prototype.onModalClose = function(event) {
+ var _this = event.data.self;
+
+ _this.hide($(event.currentTarget).closest('#modal'));
+ };
+
+ /**
+ * Event handler for closing the modal. Closes it when the user pushed ESC.
+ *
+ * @param event {Event} The `keydown` event triggered by pushing a key
+ */
+ Modal.prototype.onKeyDown = function(event) {
+ var _this = event.data.self;
+
+ if (! event.isDefaultPrevented() && event.key === 'Escape') {
+ let $modal = _this.$layout.children('#modal');
+ if ($modal.length) {
+ _this.hide($modal);
+ }
+ }
+ };
+
+ /**
+ * Make final preparations and add the modal to the DOM
+ *
+ * @param $modal {jQuery} The modal element
+ */
+ Modal.prototype.show = function($modal) {
+ $modal.addClass('active');
+ };
+
+ /**
+ * Set a title for the modal
+ *
+ * @param $modal {jQuery} The modal element
+ * @param title {string} The title
+ */
+ Modal.prototype.setTitle = function($modal, title) {
+ $modal.find('.modal-header > h1').html(title);
+ };
+
+ /**
+ * Focus the modal
+ *
+ * @param $modal {jQuery} The modal element
+ */
+ Modal.prototype.focus = function($modal) {
+ this.icinga.ui.focusElement($modal.find('.modal-window'));
+ };
+
+ /**
+ * Hide the modal and remove it from the DOM
+ *
+ * @param $modal {jQuery} The modal element
+ */
+ Modal.prototype.hide = function($modal) {
+ // Remove pointerEvent none style to make the button clickable again
+ this.modalOpener.style.pointerEvents = '';
+ this.modalOpener = null;
+
+ $modal.removeClass('active');
+ // Using `setTimeout` here to let the transition finish
+ setTimeout(function () {
+ $modal.find('#modal-content').trigger('close-modal');
+ $modal.remove();
+ }, 200);
+ };
+
+ Icinga.Behaviors.Modal = Modal;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js
new file mode 100644
index 0000000..df0e1e6
--- /dev/null
+++ b/public/js/icinga/behavior/navigation.js
@@ -0,0 +1,464 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ "use strict";
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var Navigation = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('click', '#menu a', this.linkClicked, this);
+ this.on('click', '#menu tr[href]', this.linkClicked, this);
+ this.on('rendered', '#menu', this.onRendered, this);
+ this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item', this.showFlyoutMenu, this);
+ this.on('mouseleave', '#menu .primary-nav', this.hideFlyoutMenu, this);
+ this.on('click', '#toggle-sidebar', this.toggleSidebar, this);
+
+ this.on('click', '#menu .config-nav-item button', this.toggleConfigFlyout, this);
+ this.on('mouseenter', '#menu .config-menu .config-nav-item', this.showConfigFlyout, this);
+ this.on('mouseleave', '#menu .config-menu .config-nav-item', this.hideConfigFlyout, this);
+
+ this.on('keydown', '#menu .config-menu .config-nav-item', this.onKeyDown, this);
+
+ /**
+ * The DOM-Path of the active item
+ *
+ * @see getDomPath
+ *
+ * @type {null|Array}
+ */
+ this.active = null;
+
+ /**
+ * The menu
+ *
+ * @type {jQuery}
+ */
+ this.$menu = null;
+
+ /**
+ * Local storage
+ *
+ * @type {Icinga.Storage}
+ */
+ this.storage = Icinga.Storage.BehaviorStorage('navigation');
+
+ this.storage.setBackend(window.sessionStorage);
+
+ // Restore collapsed sidebar if necessary
+ if (this.storage.get('sidebar-collapsed')) {
+ $('#layout').addClass('sidebar-collapsed');
+ }
+ };
+
+ Navigation.prototype = new Icinga.EventListener();
+
+ /**
+ * Activate menu items if their class is set to active or if the current URL matches their link
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.onRendered = function(e) {
+ var _this = e.data.self;
+
+ _this.$menu = $(e.target);
+
+ if (! _this.active) {
+ // There is no stored menu item, therefore it is assumed that this is the first rendering
+ // of the navigation after the page has been opened.
+
+ // initialise the menu selected by the backend as active.
+ var $active = _this.$menu.find('li.active');
+ if ($active.length) {
+ $active.each(function() {
+ _this.setActiveAndSelected($(this));
+ });
+ } else {
+ // if no item is marked as active, try to select the menu from the current URL
+ _this.setActiveAndSelectedByUrl($('#col1').data('icingaUrl'));
+ }
+ }
+
+ _this.refresh();
+ };
+
+ /**
+ * Re-render the menu selection according to the current state
+ */
+ Navigation.prototype.refresh = function() {
+ // restore selection to current active element
+ if (this.active) {
+ var $el = $(this.icinga.utils.getElementByDomPath(this.active));
+ this.setActiveAndSelected($el);
+
+ /*
+ * Recreate the html content of the menu item to force the browser to update the layout, or else
+ * the link would only be visible as active after another click or page reload in Gecko and WebKit.
+ *
+ * fixes #7897
+ */
+ if ($el.is('li')) {
+ $el.html($el.html());
+ }
+ }
+ };
+
+ /**
+ * Handle a link click in the menu
+ *
+ * @param event
+ */
+ Navigation.prototype.linkClicked = function(event) {
+ var $a = $(this);
+ var href = $a.attr('href');
+ var _this = event.data.self;
+ var icinga = _this.icinga;
+
+ // Check for ctrl or cmd click to open new tab and don't unfold other menus
+ if (event.ctrlKey || event.metaKey) {
+ return false;
+ }
+
+ if (href.match(/#/)) {
+ // ...it may be a menu section without a dedicated link.
+ // Switch the active menu item:
+ _this.setActiveAndSelected($a);
+ } else {
+ _this.setActiveAndSelected($(event.target));
+ }
+
+ // update target url of the menu container to the clicked link
+ var $menu = $('#menu');
+ var menuDataUrl = icinga.utils.parseUrl($menu.data('icinga-url'));
+ menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href });
+ $menu.data('icinga-url', menuDataUrl);
+ };
+
+ /**
+ * Activate a menu item based on the current URL
+ *
+ * Activate a menu item that is an exact match or fall back to items that match the base URL
+ *
+ * @param url {String} The url to match
+ */
+ Navigation.prototype.setActiveAndSelectedByUrl = function(url) {
+ var $menu = $('#menu');
+
+ if (! $menu.length) {
+ return;
+ }
+
+ // try to active the first item that has an exact URL match
+ this.setActiveAndSelected($menu.find('[href="' + url + '"]'));
+
+ // the url may point to the search field, which must be activated too
+ if (! this.active) {
+ this.setActiveAndSelected($menu.find('form[action="' + this.icinga.utils.parseUrl(url).path + '"]'));
+ }
+
+ // some urls may have custom filters which won't match any menu item, in that case search
+ // for a menu item that points to the base action without any filters
+ if (! this.active) {
+ this.setActiveAndSelected($menu.find('[href="' + this.icinga.utils.parseUrl(url).path + '"]').first());
+ }
+ };
+
+ /**
+ * Try to select a new URL by
+ *
+ * @param url
+ */
+ Navigation.prototype.trySetActiveAndSelectedByUrl = function(url) {
+ var active = this.active;
+ this.setActiveAndSelectedByUrl(url);
+
+ if (! this.active && active) {
+ this.setActiveAndSelected($(this.icinga.utils.getElementByDomPath(active)));
+ }
+ };
+
+ /**
+ * Remove all active elements
+ */
+ Navigation.prototype.clear = function() {
+ if (this.$menu) {
+ this.$menu.find('.active').removeClass('active');
+ }
+ };
+
+ /**
+ * Remove all selected elements
+ */
+ Navigation.prototype.clearSelected = function() {
+ if (this.$menu) {
+ this.$menu.find('.selected').removeClass('selected');
+ }
+ };
+
+ /**
+ * Select all menu items in the selector as active and unfold surrounding menus when necessary
+ *
+ * @param $item {jQuery} The jQuery selector
+ */
+ Navigation.prototype.select = function($item) {
+ // support selecting the url of the menu entry
+ var $input = $item.find('input');
+ $item = $item.closest('li');
+
+ if ($item.length) {
+ // select the current item
+ var $selectedMenu = $item.addClass('active');
+
+ // unfold the containing menu
+ var $outerMenu = $selectedMenu.parent().closest('li');
+ if ($outerMenu.length) {
+ $outerMenu.addClass('active');
+ }
+ } else if ($input.length) {
+ $input.addClass('active');
+ }
+ };
+
+ Navigation.prototype.setActiveAndSelected = function ($el) {
+ if ($el.length > 1) {
+ $el.each((key, el) => {
+ if (! this.active) {
+ this.setActiveAndSelected($(el));
+ }
+ });
+ } else if ($el.length) {
+ let parent = $el[0].closest('.nav-level-1 > .nav-item, .config-menu');
+
+ if ($el[0].offsetHeight || $el[0].offsetWidth || parent.offsetHeight || parent.offsetWidth) {
+ // It's either a visible menu item or a config menu item
+ this.setActive($el);
+ this.setSelected($el);
+ }
+ }
+ };
+
+ /**
+ * Change the active menu element
+ *
+ * @param $el {jQuery} A selector pointing to the active element
+ */
+ Navigation.prototype.setActive = function($el) {
+ this.clear();
+ this.select($el);
+ if ($el.closest('li')[0]) {
+ this.active = this.icinga.utils.getDomPath($el.closest('li')[0]);
+ } else if ($el.find('input')[0]) {
+ this.active = this.icinga.utils.getDomPath($el[0]);
+ } else {
+ this.active = null;
+ }
+ // TODO: push to history
+ };
+
+ Navigation.prototype.setSelected = function($el) {
+ this.clearSelected();
+ $el = $el.closest('li');
+
+ if ($el.length) {
+ $el.addClass('selected');
+ }
+ };
+
+ /**
+ * Reset the active element to nothing
+ */
+ Navigation.prototype.resetActive = function() {
+ this.clear();
+ this.active = null;
+ };
+
+ /**
+ * Reset the selected element to nothing
+ */
+ Navigation.prototype.resetSelected = function() {
+ this.clearSelected();
+ this.selected = null;
+ };
+
+ /**
+ * Show the fly-out menu
+ *
+ * @param e
+ */
+ Navigation.prototype.showFlyoutMenu = function(e) {
+ var $layout = $('#layout');
+
+ if ($layout.hasClass('minimal-layout')) {
+ return;
+ }
+
+ var $target = $(this);
+ var $flyout = $target.find('.nav-level-2');
+
+ if (! $flyout.length) {
+ $layout.removeClass('menu-hovered');
+ $target.siblings().not($target).removeClass('hover');
+ return;
+ }
+
+ var delay = 300;
+
+ if ($layout.hasClass('menu-hovered')) {
+ delay = 0;
+ }
+
+ setTimeout(function() {
+ try {
+ if (! $target.is(':hover')) {
+ return;
+ }
+ } catch(e) { /* Bypass because if IE8 */ }
+
+ $layout.addClass('menu-hovered');
+ $target.siblings().not($target).removeClass('hover');
+ $target.addClass('hover');
+
+ $flyout.css({
+ bottom: 'auto',
+ top: $target.offset().top + $target.outerHeight()
+ });
+
+ var rect = $flyout[0].getBoundingClientRect();
+
+ if (rect.y + rect.height > window.innerHeight) {
+ $flyout.css({
+ bottom: 0,
+ top: 'auto'
+ });
+ }
+ }, delay);
+ };
+
+ /**
+ * Hide the fly-out menu
+ *
+ * @param e
+ */
+ Navigation.prototype.hideFlyoutMenu = function(e) {
+ var $layout = $('#layout');
+ var $nav = $(e.currentTarget);
+ var $hovered = $nav.find('.nav-level-1 > .nav-item.hover');
+
+ if (! $hovered.length) {
+ $layout.removeClass('menu-hovered');
+
+ return;
+ }
+
+ setTimeout(function() {
+ try {
+ if ($hovered.is(':hover') || $nav.is(':hover')) {
+ return;
+ }
+ } catch(e) { /* Bypass because if IE8 */ };
+ $hovered.removeClass('hover');
+ $layout.removeClass('menu-hovered');
+ }, 600);
+ };
+
+ /**
+ * Collapse or expand sidebar
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.toggleSidebar = function(e) {
+ var _this = e.data.self;
+ var $layout = $('#layout');
+ $layout.toggleClass('sidebar-collapsed');
+ _this.storage.set('sidebar-collapsed', $layout.is('.sidebar-collapsed'));
+ $(window).trigger('resize');
+ };
+
+ /**
+ * Toggle config flyout visibility
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.toggleConfigFlyout = function(e) {
+ var _this = e.data.self;
+ if ($('#layout').is('.config-flyout-open')) {
+ _this.hideConfigFlyout(e);
+ } else {
+ _this.showConfigFlyout(e);
+ }
+ }
+
+ /**
+ * Hide config flyout
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.hideConfigFlyout = function(e) {
+ $('#layout').removeClass('config-flyout-open');
+ if (e.target) {
+ delete $(e.target).closest('.container')[0].dataset.suspendAutorefresh;
+ }
+ }
+
+ /**
+ * Show config flyout
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.showConfigFlyout = function(e) {
+ $('#layout').addClass('config-flyout-open');
+ $(e.target).closest('.container')[0].dataset.suspendAutorefresh = '';
+ }
+
+ /**
+ * Hide, config flyout when "Enter" key is pressed to follow `.flyout` nav item link
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.onKeyDown = function(e) {
+ var _this = e.data.self;
+
+ if (e.key == 'Enter' && $(document.activeElement).is('.flyout a')) {
+ _this.hideConfigFlyout(e);
+ }
+ }
+
+ /**
+ * Called when the history changes
+ *
+ * @param url The url of the new state
+ * @param data The active menu item of the new state
+ */
+ Navigation.prototype.onPopState = function (url, data) {
+ // 1. get selection data and set active menu
+ if (data) {
+ var active = this.icinga.utils.getElementByDomPath(data);
+ if (!active) {
+ this.logger.fail(
+ 'Could not restore active menu from history, path in DOM not found.',
+ data,
+ url
+ );
+ return;
+ }
+ this.setActiveAndSelected($(active))
+ } else {
+ this.resetActive();
+ this.resetSelected();
+ }
+ };
+
+ /**
+ * Called when the current state gets pushed onto the history, can return a value
+ * to be preserved as the current state
+ *
+ * @returns {null|Array} The currently active menu item
+ */
+ Navigation.prototype.onPushState = function () {
+ return this.active;
+ };
+
+ Icinga.Behaviors.Navigation = Navigation;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/selectable.js b/public/js/icinga/behavior/selectable.js
new file mode 100644
index 0000000..3f32840
--- /dev/null
+++ b/public/js/icinga/behavior/selectable.js
@@ -0,0 +1,49 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+;(function(Icinga, $) {
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Select all contents from the target of the given event
+ *
+ * @param {object} e Event
+ */
+ function onSelect(e) {
+ var b = document.body,
+ r;
+ if (b.createTextRange) {
+ r = b.createTextRange();
+ r.moveToElementText(e.target);
+ r.select();
+ } else if (window.getSelection) {
+ var s = window.getSelection();
+ r = document.createRange();
+ r.selectNodeContents(e.target);
+ s.removeAllRanges();
+ s.addRange(r);
+ }
+ }
+
+ /**
+ * Behavior for text that is selectable via double click
+ *
+ * @param {Icinga} icinga
+ *
+ * @constructor
+ */
+ var Selectable = function(icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', this.onRendered, this);
+ };
+
+ $.extend(Selectable.prototype, new Icinga.EventListener(), {
+ onRendered: function(e) {
+ $(e.target).find('.selectable').on('dblclick', onSelect);
+ }
+ });
+
+ Icinga.Behaviors.Selectable = Selectable;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/eventlistener.js b/public/js/icinga/eventlistener.js
new file mode 100644
index 0000000..678e775
--- /dev/null
+++ b/public/js/icinga/eventlistener.js
@@ -0,0 +1,78 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * EventListener contains event handlers and can bind / and unbind them from
+ * event emitting objects
+ */
+(function(Icinga, $) {
+
+ "use strict";
+
+ var EventListener = function (icinga) {
+ this.icinga = icinga;
+ this.handlers = [];
+ };
+
+ /**
+ * Add an handler to this EventLister
+ *
+ * @param evt {String} The name of the triggering event
+ * @param cond {String} The filter condition
+ * @param fn {Function} The event handler to execute
+ * @param scope {Object} The optional 'this' of the called function
+ */
+ EventListener.prototype.on = function(evt, cond, fn, scope) {
+ if (typeof cond === 'function') {
+ scope = fn;
+ fn = cond;
+ cond = 'body';
+ }
+ this.icinga.logger.debug('on: ' + evt + '(' + cond + ')');
+ this.handlers.push({ evt: evt, cond: cond, fn: fn, scope: scope });
+ };
+
+ /**
+ * Bind all listeners to the given event emitter
+ *
+ * All event handlers will be executed when the associated event is
+ * triggered on the given Emitter.
+ *
+ * @param emitter {String} An event emitter that supports the function
+ * 'on' to register listeners
+ */
+ EventListener.prototype.bind = function (emitter) {
+ var _this = this;
+
+ if (typeof emitter.jquery === 'undefined') {
+ emitter = $(emitter);
+ }
+
+ $.each(this.handlers, function(i, handler) {
+ _this.icinga.logger.debug('bind: ' + handler.evt + '(' + handler.cond + ')');
+ emitter.on(
+ handler.evt, handler.cond,
+ {
+ self: handler.scope || emitter,
+ icinga: _this.icinga
+ }, handler.fn
+ );
+ });
+ };
+
+ /**
+ * Unbind all listeners from the given event emitter
+ *
+ * @param emitter {String} An event emitter that supports the function
+ * 'off' to un-register listeners.
+ */
+ EventListener.prototype.unbind = function (emitter) {
+ var _this = this;
+ $.each(this.handlers, function(i, handler) {
+ _this.icinga.logger.debug('unbind: ' + handler.evt + '(' + handler.cond + ')');
+ emitter.off(handler.evt, handler.cond, handler.fn);
+ });
+ };
+
+ Icinga.EventListener = EventListener;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js
new file mode 100644
index 0000000..186a41c
--- /dev/null
+++ b/public/js/icinga/events.js
@@ -0,0 +1,415 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Events
+ *
+ * Event handlers
+ */
+(function (Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Events = function (icinga) {
+ this.icinga = icinga;
+
+ this.searchValue = '';
+ this.searchTimer = null;
+ };
+
+ Icinga.Events.prototype = {
+
+ /**
+ * Icinga will call our initialize() function once it's ready
+ */
+ initialize: function () {
+ this.applyGlobalDefaults();
+ },
+
+ /**
+ * Global default event handlers
+ */
+ applyGlobalDefaults: function () {
+ $(document).on('visibilitychange', { self: this }, this.onVisibilityChange);
+
+ $.each(this.icinga.behaviors, function (name, behavior) {
+ behavior.bind($(document));
+ });
+
+ // Initialize module javascript (Applies only to module.js code)
+ this.icinga.ensureSubModules(document);
+
+ // We catch resize events
+ $(window).on('resize', { self: this.icinga.ui }, this.icinga.ui.onWindowResize);
+
+ // Trigger 'rendered' event also on page loads
+ $(document).on('icinga-init', { self: this }, this.onInit);
+
+ // Destroy Icinga, clean up and interrupt pending requests on unload
+ $( window ).on('unload', { self: this }, this.onUnload);
+ $( window ).on('beforeunload', { self: this }, this.onUnload);
+
+ // Remove notifications on click
+ $(document).on('click', '#notifications li', function () { $(this).remove(); });
+
+ // We want to catch each link click
+ $(document).on('click', 'a', { self: this }, this.linkClicked);
+ $(document).on('click', 'tr[href]', { self: this }, this.linkClicked);
+
+ $(document).on('click', 'input[type="submit"], button[type="submit"]', this.rememberSubmitButton);
+ // We catch all form submit events
+ $(document).on('submit', 'form', { self: this }, this.submitForm);
+
+ // We support an 'autosubmit' class on dropdown form elements
+ $(document).on('change', 'form select.autosubmit', { self: this }, this.autoSubmitForm);
+ $(document).on('change', 'form input.autosubmit', { self: this }, this.autoSubmitForm);
+
+ // Automatically check a radio button once a specific input is focused
+ $(document).on('focus', 'form select[data-related-radiobtn]', { self: this }, this.autoCheckRadioButton);
+ $(document).on('focus', 'form input[data-related-radiobtn]', { self: this }, this.autoCheckRadioButton);
+
+ $(document).on('rendered', '#menu', { self: this }, this.onRenderedMenu);
+ $(document).on('keyup', '#search', { self: this }, this.autoSubmitSearch);
+
+ $(document).on('click', '.tree .handle', { self: this }, this.treeNodeToggle);
+
+ $(document).on('click', '#search + .search-reset', this.clearSearch);
+
+ $(document).on('rendered', '.container', { self: this }, this.loadDashlets);
+
+ // TBD: a global autocompletion handler
+ // $(document).on('keyup', 'form.auto input', this.formChangeDelayed);
+ // $(document).on('change', 'form.auto input', this.formChanged);
+ // $(document).on('change', 'form.auto select', this.submitForm);
+ },
+
+ treeNodeToggle: function () {
+ var $parent = $(this).closest('li');
+ if ($parent.hasClass('collapsed')) {
+ $('li', $parent).addClass('collapsed');
+ $parent.removeClass('collapsed');
+ } else {
+ $parent.addClass('collapsed');
+ }
+ },
+
+ onInit: function (event) {
+ $('body').removeClass('loading');
+
+ // Trigger the initial `rendered` events
+ $('.container').trigger('rendered');
+
+ // Additionally trigger a `rendered` event on the layout, some behaviors may
+ // want to differentiate whether a container or the entire layout is rendered
+ $('#layout').trigger('rendered');
+ },
+
+ onUnload: function (event) {
+ var icinga = event.data.self.icinga;
+ icinga.logger.info('Unloading Icinga');
+ icinga.destroy();
+ },
+
+ onVisibilityChange: function (event) {
+ var icinga = event.data.self.icinga;
+
+ if (document.visibilityState === undefined || document.visibilityState === 'visible') {
+ icinga.loader.autorefreshSuspended = false;
+ icinga.logger.debug('Page visible, enabling auto-refresh');
+ } else {
+ icinga.loader.autorefreshSuspended = true;
+ icinga.logger.debug('Page invisible, disabling auto-refresh');
+ }
+ },
+
+ autoCheckRadioButton: function (event) {
+ var $input = $(event.currentTarget);
+ var $radio = $('#' + $input.attr('data-related-radiobtn'));
+ if ($radio.length) {
+ $radio.prop('checked', true);
+ }
+ return true;
+ },
+
+ onRenderedMenu: function(event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+
+ var $searchField = $target.find('input.search');
+ // Remember initial search field value if any
+ if ($searchField.length && $searchField.val().length) {
+ _this.searchValue = $searchField.val();
+ }
+ },
+
+ autoSubmitSearch: function(event) {
+ var _this = event.data.self;
+ var $searchField = $(event.target);
+
+ if ($searchField.val() === _this.searchValue) {
+ return;
+ }
+ _this.searchValue = $searchField.val();
+
+ if (_this.searchTimer !== null) {
+ clearTimeout(_this.searchTimer);
+ _this.searchTimer = null;
+ }
+ var _event = $.extend({}, event); // event seems gc'd once the timeout is over
+ _this.searchTimer = setTimeout(function () {
+ _this.submitForm(_event, $searchField);
+ _this.searchTimer = null;
+ }, 500);
+ },
+
+ loadDashlets: function(event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+
+ if ($target.children('.dashboard').length) {
+ $target.find('.dashboard > .container').each(function () {
+ var $dashlet = $(this);
+ var url = $dashlet.data('icingaUrl');
+ if (typeof url !== 'undefined') {
+ _this.icinga.loader.loadUrl(url, $dashlet).autorefresh = true;
+ }
+ });
+ }
+ },
+
+ rememberSubmitButton: function(e) {
+ var $button = $(this);
+ var $form = $button.closest('form');
+ $form.data('submitButton', $button);
+ },
+
+ autoSubmitForm: function (event) {
+ return event.data.self.submitForm(event, $(event.currentTarget));
+ },
+
+ /**
+ *
+ */
+ submitForm: function (event, $autoSubmittedBy) {
+ var _this = event.data.self;
+
+ // .closest is not required unless subelements to trigger this
+ var $form = $(event.currentTarget).closest('form');
+
+ if ($form.closest('[data-no-icinga-ajax]').length > 0) {
+ return true;
+ }
+
+ var $button;
+ var $rememberedSubmittButton = $form.data('submitButton');
+ if (typeof $rememberedSubmittButton != 'undefined') {
+ if ($form.has($rememberedSubmittButton)) {
+ $button = $rememberedSubmittButton;
+ }
+ $form.removeData('submitButton');
+ }
+
+ if (typeof $button === 'undefined') {
+ var $el;
+
+ if (typeof event.originalEvent !== 'undefined'
+ && typeof event.originalEvent.explicitOriginalTarget === 'object') { // Firefox
+ $el = $(event.originalEvent.explicitOriginalTarget);
+ _this.icinga.logger.debug('events/submitForm: Button is event.originalEvent.explicitOriginalTarget');
+ } else {
+ $el = $(event.currentTarget);
+ _this.icinga.logger.debug('events/submitForm: Button is event.currentTarget');
+ }
+
+ if ($el && ($el.is('input[type=submit]') || $el.is('button[type=submit]'))) {
+ $button = $el;
+ } else {
+ _this.icinga.logger.debug(
+ 'events/submitForm: Can not determine submit button, using the first one in form'
+ );
+ }
+ }
+
+ if (! $autoSubmittedBy && event.detail && event.detail.submittedBy) {
+ $autoSubmittedBy = $(event.detail.submittedBy);
+ }
+
+ _this.icinga.loader.submitForm($form, $autoSubmittedBy, $button);
+
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ },
+
+ handleExternalTarget: function($node) {
+ var linkTarget = $node.attr('target');
+
+ // TODO: Let remote links pass through. Right now they only work
+ // combined with target="_blank" or target="_self"
+ // window.open is used as return true; didn't work reliable
+ if (linkTarget === '_blank' || linkTarget === '_self') {
+ window.open($node.attr('href'), linkTarget);
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Someone clicked a link or tr[href]
+ */
+ linkClicked: function (event) {
+ var _this = event.data.self;
+ var icinga = _this.icinga;
+ var $a = $(this);
+ var $eventTarget = $(event.target);
+ var href = $a.attr('href');
+ var linkTarget = $a.attr('target');
+ var $target;
+ var formerUrl;
+
+ if (! href) {
+ return;
+ }
+
+ if (href.match(/^(?:(?:mailto|javascript|data):|[a-z]+:\/\/)/)) {
+ event.stopPropagation();
+ return true;
+ }
+
+ if ($a.closest('[data-no-icinga-ajax]').length > 0) {
+ return true;
+ }
+
+ // Check for ctrl or cmd click to open new tab unless clicking on a multiselect row
+ if ((event.ctrlKey || event.metaKey) && href !== '#' && $a.is('a')) {
+ window.open(href, linkTarget);
+ return false;
+ }
+
+ // Special checks for link clicks in action tables
+ if (! $a.is('tr[href]') && $a.closest('table.action').length > 0) {
+
+ // ignore clicks to ANY link with special key pressed
+ if ($a.closest('table.multiselect').length > 0 && (event.ctrlKey || event.metaKey || event.shiftKey)) {
+ return true;
+ }
+
+ // ignore inner links matching the row URL
+ if ($a.attr('href') === $a.closest('tr[href]').attr('href')) {
+ return true;
+ }
+ }
+
+ // window.open is used as return true; didn't work reliable
+ if (linkTarget === '_blank' || linkTarget === '_self') {
+ window.open(href, linkTarget);
+ return false;
+ }
+
+ if (! $eventTarget.is($a)) {
+ if ($eventTarget.is('input') || $eventTarget.is('button')) {
+ // Ignore form elements in action rows
+ return;
+ } else {
+ var $button = $('input[type=submit]:focus').add('button[type=submit]:focus');
+ if ($button.length > 0 && $.contains($button[0], $eventTarget[0])) {
+ // Ignore any descendant of form elements
+ return;
+ }
+ }
+ }
+
+ // ignore multiselect table row clicks
+ if ($a.is('tr') && $a.closest('table.multiselect').length > 0) {
+ return;
+ }
+
+ // Handle all other links as XHR requests
+ event.stopPropagation();
+ event.preventDefault();
+
+ // This is an anchor only
+ if (href.substr(0, 1) === '#' && href.length > 1
+ && href.substr(1, 1) !== '!'
+ ) {
+ icinga.ui.focusElement(href.substr(1), $a.closest('.container'));
+ return;
+ }
+
+ // activate spinner indicator
+ if ($a.hasClass('spinner')) {
+ $a.addClass('active');
+ }
+
+ // If link has hash tag...
+ if (href.match(/#/)) {
+ if (href === '#') {
+ if ($a.hasClass('close-container-control')) {
+ if (! icinga.ui.isOneColLayout()) {
+ var $cont = $a.closest('.container').first();
+ if ($cont.attr('id') === 'col1') {
+ icinga.ui.moveToLeft();
+ icinga.ui.layout1col();
+ } else {
+ icinga.ui.layout1col();
+ }
+ icinga.history.pushCurrentState();
+ }
+ }
+ return false;
+ }
+ $target = icinga.loader.getLinkTargetFor($a);
+
+ formerUrl = $target.data('icingaUrl');
+ if (typeof formerUrl !== 'undefined' && formerUrl.split(/#/)[0] === href.split(/#/)[0]) {
+ icinga.ui.focusElement(href.split(/#/)[1], $target);
+ $target.data('icingaUrl', href);
+ if (formerUrl !== href) {
+ icinga.history.pushCurrentState();
+ }
+ return false;
+ }
+ } else {
+ $target = icinga.loader.getLinkTargetFor($a);
+ }
+
+ // Load link URL
+ icinga.loader.loadUrl(href, $target);
+
+ if ($a.closest('#menu').length > 0) {
+ // Menu links should remove all but the first layout column
+ icinga.ui.layout1col();
+ }
+
+ return false;
+ },
+
+ clearSearch: function (event) {
+ $(event.target).parent().find('#search').attr('value', '');
+ },
+
+ unbindGlobalHandlers: function () {
+ $.each(this.icinga.behaviors, function (name, behavior) {
+ behavior.unbind($(document));
+ });
+ $(window).off('resize', this.onWindowResize);
+ $(window).off('load', this.onLoad);
+ $(window).off('unload', this.onUnload);
+ $(window).off('beforeunload', this.onUnload);
+ $(document).off('scroll', '.container', this.onContainerScroll);
+ $(document).off('click', 'a', this.linkClicked);
+ $(document).off('submit', 'form', this.submitForm);
+ $(document).off('change', 'form select.autosubmit', this.submitForm);
+ $(document).off('change', 'form input.autosubmit', this.submitForm);
+ $(document).off('focus', 'form select[data-related-radiobtn]', this.autoCheckRadioButton);
+ $(document).off('focus', 'form input[data-related-radiobtn]', this.autoCheckRadioButton);
+ $(document).off('visibilitychange', this.onVisibilityChange);
+ },
+
+ destroy: function() {
+ // This is gonna be hard, clean up the mess
+ this.unbindGlobalHandlers();
+ this.icinga = null;
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/history.js b/public/js/icinga/history.js
new file mode 100644
index 0000000..150be7c
--- /dev/null
+++ b/public/js/icinga/history.js
@@ -0,0 +1,338 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.History
+ *
+ * This is where we care about the browser History API
+ */
+(function (Icinga, $) {
+
+ 'use strict';
+
+ Icinga.History = function (icinga) {
+
+ /**
+ * YES, we need Icinga
+ */
+ this.icinga = icinga;
+
+ /**
+ * Our base url
+ */
+ this.baseUrl = icinga.config.baseUrl;
+
+ /**
+ * Initial URL at load time
+ */
+ this.initialUrl = location.href;
+
+ /**
+ * Whether the History API is enabled
+ */
+ this.enabled = false;
+ };
+
+ Icinga.History.prototype = {
+
+ /**
+ * Icinga will call our initialize() function once it's ready
+ */
+ initialize: function () {
+
+ // History API will not be enabled without browser support, no fallback
+ if ('undefined' !== typeof window.history &&
+ typeof window.history.pushState === 'function'
+ ) {
+ this.enabled = true;
+ this.icinga.logger.debug('History API enabled');
+ this.applyLocationBar(true);
+ $(window).on('popstate', { self: this }, this.onHistoryChange);
+ }
+
+ },
+
+ /**
+ * Get the current state (url and title) as object
+ *
+ * @returns {object}
+ */
+ getCurrentState: function () {
+ if (! this.enabled) {
+ return null;
+ }
+
+ var title = null;
+ var url = null;
+
+ // We only store URLs of containers sitting directly under #main:
+ $('#main > .container').each(function (idx, container) {
+ var $container = $(container),
+ cUrl = $container.data('icingaUrl'),
+ cTitle = $container.data('icingaTitle');
+
+ // TODO: I'd prefer to have the rightmost URL first
+ if ('undefined' !== typeof cUrl) {
+ // TODO: solve this on server side cUrl = icinga.utils.removeUrlParams(cUrl, blacklist);
+ if (! url) {
+ url = cUrl;
+ } else {
+ url = url + '#!' + cUrl;
+ }
+ }
+
+ if (typeof cTitle !== 'undefined') {
+ title = cTitle; // Only uses the rightmost title
+ }
+ });
+
+ return {
+ title: title,
+ url: url,
+ };
+ },
+
+ /**
+ * Detect active URLs and push combined URL to history
+ *
+ * TODO: How should we handle POST requests? e.g. search VS login
+ */
+ pushCurrentState: function () {
+ // No history API, no action
+ if (! this.enabled) {
+ return;
+ }
+
+ var state = this.getCurrentState();
+
+ // Did we find any URL? Then push it!
+ if (state.url) {
+ this.icinga.logger.debug('Pushing current state to history');
+ this.push(state.url);
+ }
+ if (state.title) {
+ this.icinga.ui.setTitle(state.title);
+ }
+ },
+
+ /**
+ * Replace the current history entry with the current state
+ */
+ replaceCurrentState: function () {
+ if (! this.enabled) {
+ return;
+ }
+
+ var state = this.getCurrentState();
+
+ if (state.url) {
+ this.icinga.logger.debug('Replacing current history state');
+ this.lastPushUrl = state.url;
+ window.history.replaceState(
+ this.getBehaviorState(),
+ null,
+ state.url
+ );
+ }
+ },
+
+ /**
+ * Push the given url as the new history state, unless the history is disabled
+ *
+ * @param {string} url The full url path, including anchor
+ */
+ pushUrl: function (url) {
+ // No history API, no action
+ if (!this.enabled) {
+ return;
+ }
+ this.push(url);
+ },
+
+ /**
+ * Execute the history state, preserving the current state of behaviors
+ *
+ * Used internally by the history and should not be called externally, instead use {@link pushUrl}.
+ *
+ * @param {string} url
+ */
+ push: function (url) {
+ url = url.replace(/[\?&]?_(render|reload)=[a-z0-9]+/g, '');
+ if (this.lastPushUrl === url) {
+ this.icinga.logger.debug(
+ 'Ignoring history state push for url ' + url + ' as it\' currently on top of the stack'
+ );
+ return;
+ }
+ this.lastPushUrl = url;
+ window.history.pushState(
+ this.getBehaviorState(),
+ null,
+ url
+ );
+ },
+
+ /**
+ * Fetch the current state of all JS behaviors that need history support
+ *
+ * @return {Object} A key-value map, mapping behavior names to state
+ */
+ getBehaviorState: function () {
+ var data = {};
+ $.each(this.icinga.behaviors, function (i, behavior) {
+ if (behavior.onPushState instanceof Function) {
+ data[i] = behavior.onPushState();
+ }
+ });
+ return data;
+ },
+
+ /**
+ * Event handler for pop events
+ *
+ * TODO: Fix active selection, multiple cols
+ */
+ onHistoryChange: function (event) {
+
+ var _this = event.data.self,
+ icinga = _this.icinga;
+
+ icinga.logger.debug('Got a history change');
+
+ // We might find browsers showing strange behaviour, this log could help
+ if (event.originalEvent.state === null) {
+ icinga.logger.debug('No more history steps available');
+ } else {
+ icinga.logger.debug('History state', event.originalEvent.state);
+ }
+
+ // keep the last pushed url in sync with history changes
+ _this.lastPushUrl = location.href;
+
+ _this.applyLocationBar();
+
+ // notify behaviors of the state change
+ $.each(this.icinga.behaviors, function (i, behavior) {
+ if (behavior.onPopState instanceof Function && history.state) {
+ behavior.onPopState(location.href, history.state[i]);
+ }
+ });
+ },
+
+ /**
+ * Update the application containers to match the current url
+ *
+ * Read the pane url from the current URL and load the corresponding panes into containers to
+ * match the current history state.
+ *
+ * @param {Boolean} onload Set to true when the main pane should not be updated, defaults to false
+ */
+ applyLocationBar: function (onload = false) {
+ let col2State = this.getCol2State();
+
+ if (onload && document.querySelector('#layout > #login')) {
+ // The user landed on the login
+ let redirectInput = document.querySelector('#login form input[name=redirect]');
+ redirectInput.value = redirectInput.value + col2State;
+ return;
+ }
+
+ let col1 = document.getElementById('col1'),
+ col2 = document.getElementById('col2'),
+ col1Url = document.location.pathname + document.location.search;
+
+ let col2Url;
+ if (col2State && col2State.match(/^#!/)) {
+ col2Url = col2State.split(/#!/)[1];
+ }
+
+ // This uses jQuery only because of its internal data attribute cache -.-
+ let currentCol1Url = $(col1).data('icingaUrl'),
+ currentCol2Url = $(col2).data('icingaUrl');
+
+ let loadCol1 = ! onload,
+ loadCol2 = !! col2Url;
+ if (currentCol2Url === col1Url) {
+ // User navigated forward
+ this.icinga.ui.moveToLeft();
+ loadCol1 = false;
+ } else if (currentCol1Url === col2Url) {
+ // User navigated back
+ this.icinga.ui.moveToRight();
+ loadCol2 = false;
+ }
+
+ if (loadCol1 && currentCol1Url !== col1Url) {
+ let anchor = this.getPaneAnchor(0);
+ if (anchor) {
+ col1Url += '#' + anchor;
+ }
+
+ this.icinga.loader.loadUrl(col1Url, $(col1)).addToHistory = false;
+ }
+
+ if (loadCol2 && currentCol2Url !== col2Url) {
+ let col2Req = this.icinga.loader.loadUrl(col2Url, $(col2));
+ col2Req.addToHistory = false;
+ col2Req.scripted = onload;
+
+ this.icinga.ui.layout2col();
+ } else if (! loadCol2 && ! col2Url) {
+ this.icinga.ui.layout1col();
+ }
+ },
+
+ /**
+ * Get the state of the selected pane
+ *
+ * @param col {int} The column index 0 or 1
+ *
+ * @returns {String} The string representing the state
+ */
+ getPaneAnchor: function (col) {
+ if (col !== 1 && col !== 0) {
+ throw 'Trying to get anchor for non-existing column: ' + col;
+ }
+ var panes = document.location.toString().split('#!')[col];
+ return panes && panes.split('#')[1] || '';
+ },
+
+ /**
+ * Get the side pane state after (and including) the #!
+ *
+ * @returns {string} The pane url
+ */
+ getCol2State: function () {
+ var hash = document.location.hash;
+ if (hash) {
+ if (hash.match(/^#[^!]/)) {
+ var hashs = hash.split('#');
+ hashs.shift();
+ hashs.shift();
+ hash = '#' + hashs.join('#');
+ }
+ }
+ return hash || '';
+ },
+
+ /**
+ * Return the main pane state fragment
+ *
+ * @returns {string} The main url including anchors, without #!
+ */
+ getCol1State: function () {
+ var anchor = this.getPaneAnchor(0);
+ var hash = window.location.pathname + window.location.search +
+ (anchor.length ? ('#' + anchor) : '');
+ return hash || '';
+ },
+
+ /**
+ * Cleanup
+ */
+ destroy: function () {
+ $(window).off('popstate', this.onHistoryChange);
+ this.icinga = null;
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js
new file mode 100644
index 0000000..2ad05d2
--- /dev/null
+++ b/public/js/icinga/loader.js
@@ -0,0 +1,1323 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Loader
+ *
+ * This is where we take care of XHR requests, responses and failures.
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Loader = function (icinga) {
+
+ /**
+ * YES, we need Icinga
+ */
+ this.icinga = icinga;
+
+ /**
+ * Our base url
+ */
+ this.baseUrl = icinga.config.baseUrl;
+
+ this.failureNotice = null;
+
+ /**
+ * Pending requests
+ */
+ this.requests = {};
+
+ this.iconCache = {};
+
+ /**
+ * Whether auto-refresh is enabled
+ */
+ this.autorefreshEnabled = true;
+
+ /**
+ * Whether auto-refresh is suspended due to visibility of page
+ */
+ this.autorefreshSuspended = false;
+ };
+
+ Icinga.Loader.prototype = {
+
+ initialize: function () {
+ this.icinga.timer.register(this.autorefresh, this, 500);
+ },
+
+ submitForm: function ($form, $autoSubmittedBy, $button) {
+ var icinga = this.icinga;
+ var url = $form.attr('action');
+ var method = $form.attr('method');
+ var encoding = $form.attr('enctype');
+ var progressTimer;
+ var $target;
+ var data;
+
+ if (typeof method === 'undefined') {
+ method = 'POST';
+ } else {
+ method = method.toUpperCase();
+ }
+
+ if (typeof encoding === 'undefined') {
+ encoding = 'application/x-www-form-urlencoded';
+ }
+
+ if (typeof $autoSubmittedBy === 'undefined') {
+ $autoSubmittedBy = false;
+ }
+
+ if (typeof $button === 'undefined') {
+ $button = $('input[type=submit]:focus', $form).add('button[type=submit]:focus', $form);
+ }
+
+ if ($button.length === 0) {
+ $button = $('input[type=submit]', $form).add('button[type=submit]', $form).first();
+ }
+
+ if ($button.length) {
+ // Activate spinner
+ if ($button.hasClass('spinner')) {
+ $button.addClass('active');
+ }
+
+ $target = this.getLinkTargetFor($button);
+ } else {
+ $target = this.getLinkTargetFor($form);
+ }
+
+ if (! url) {
+ // Use the URL of the target container if the form's action is not set
+ url = $target.closest('.container').data('icinga-url');
+ }
+
+ icinga.logger.debug('Submitting form: ' + method + ' ' + url, method);
+
+ if (method === 'GET') {
+ var dataObj = $form.serializeObject();
+
+ if (! $autoSubmittedBy) {
+ if ($button.length && $button.attr('name') !== 'undefined') {
+ dataObj[$button.attr('name')] = $button.attr('value');
+ }
+ }
+
+ url = icinga.utils.addUrlParams(url, dataObj);
+ } else {
+ if (encoding === 'multipart/form-data') {
+ data = new window.FormData($form[0]);
+ } else {
+ data = $form.serializeArray();
+ }
+
+ if (! $autoSubmittedBy) {
+ if ($button.length && $button.attr('name') !== 'undefined') {
+ if (encoding === 'multipart/form-data') {
+ data.append($button.attr('name'), $button.attr('value'));
+ } else {
+ data.push({
+ name: $button.attr('name'),
+ value: $button.attr('value')
+ });
+ }
+ }
+ }
+ }
+
+ // Disable all form controls to prevent resubmission except for our search input
+ // Note that disabled form inputs will not be enabled via JavaScript again
+ if (! $autoSubmittedBy
+ && ! $form.is('[role="search"]')
+ && $target.attr('id') === $form.closest('.container').attr('id')
+ ) {
+ $form.find('input[type=submit],button[type=submit],button:not([type])').prop('disabled', true);
+ }
+
+ // Show a spinner depending on how the form is being submitted
+ if ($autoSubmittedBy && $autoSubmittedBy.siblings('.spinner').length) {
+ $autoSubmittedBy.siblings('.spinner').first().addClass('active');
+ } else if ($button.length && $button.is('button') && $button.hasClass('animated')) {
+ $button.addClass('active');
+ } else if ($button.length && $button.attr('data-progress-label')) {
+ var isInput = $button.is('input');
+ if (isInput) {
+ $button.prop('value', $button.attr('data-progress-label') + '...');
+ } else {
+ $button.html($button.attr('data-progress-label') + '...');
+ }
+
+ // Use a fixed width to prevent the button from wobbling
+ $button.css('width', $button.css('width'));
+
+ progressTimer = icinga.timer.register(function () {
+ var label = isInput ? $button.prop('value') : $button.html();
+ var dots = label.substr(-3);
+
+ // Using empty spaces here to prevent centered labels from wobbling
+ if (dots === '...') {
+ label = label.slice(0, -2) + ' ';
+ } else if (dots === '.. ') {
+ label = label.slice(0, -1) + '.';
+ } else if (dots === '. ') {
+ label = label.slice(0, -2) + '. ';
+ }
+
+ if (isInput) {
+ $button.prop('value', label);
+ } else {
+ $button.html(label);
+ }
+ }, null, 100);
+ } else if ($button.length && $button.next().hasClass('spinner')) {
+ $('i', $button.next()).addClass('active');
+ } else if ($form.attr('data-progress-element')) {
+ var $progressElement = $('#' + $form.attr('data-progress-element'));
+ if ($progressElement.length) {
+ if ($progressElement.hasClass('spinner')) {
+ $('i', $progressElement).addClass('active');
+ } else {
+ $('i.spinner', $progressElement).addClass('active');
+ }
+ }
+ }
+
+ var extraHeaders = {};
+ if ($autoSubmittedBy && ($autoSubmittedBy.attr('name') || $autoSubmittedBy.attr('id'))) {
+ extraHeaders['X-Icinga-AutoSubmittedBy'] = $autoSubmittedBy.attr('name') || $autoSubmittedBy.attr('id');
+ }
+
+ var req = this.loadUrl(url, $target, data, method, undefined, undefined, undefined, extraHeaders);
+ req.forceFocus = $autoSubmittedBy ? $autoSubmittedBy : $button.length ? $button : null;
+ req.autosubmit = !! $autoSubmittedBy;
+ req.addToHistory = method === 'GET';
+ req.progressTimer = progressTimer;
+
+ if ($autoSubmittedBy) {
+ if ($autoSubmittedBy.closest('.controls').length) {
+ $('.content', req.$target).addClass('impact');
+ } else {
+ req.$target.addClass('impact');
+ }
+ }
+
+ return req;
+ },
+
+ /**
+ * Load the given URL to the given target
+ *
+ * @param {string} url URL to be loaded
+ * @param {object} target Target jQuery element
+ * @param {object} data Optional parameters, usually for POST requests
+ * @param {string} method HTTP method, default is 'GET'
+ * @param {string} action How to handle the response ('replace' or 'append'), default is 'replace'
+ * @param {boolean} autorefresh Whether the cause is a autorefresh or not
+ * @param {object} progressTimer A timer to be stopped when the request is done
+ * @param {object} extraHeaders Extra header entries
+ */
+ loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer, extraHeaders) {
+ var id = null;
+
+ // Default method is GET
+ if ('undefined' === typeof method) {
+ method = 'GET';
+ }
+ if ('undefined' === typeof action) {
+ action = 'replace';
+ }
+ if ('undefined' === typeof autorefresh) {
+ autorefresh = false;
+ }
+
+ this.icinga.logger.debug('Loading ', url, ' to ', $target);
+
+ // We should do better and ignore requests without target and/or id
+ if (typeof $target !== 'undefined' && $target.attr('id')) {
+ id = $target.attr('id');
+ }
+
+ // If we have a pending request for the same target...
+ if (typeof this.requests[id] !== 'undefined') {
+ // ... ignore the new request if it is already pending with the same URL. Only abort GETs, as those
+ // are the only methods that are guaranteed to return the same value
+ if (this.requests[id].url === url && method === 'GET') {
+ if (autorefresh) {
+ return false;
+ }
+
+ this.icinga.logger.debug('Request to ', url, ' is already running for ', $target);
+ return this.requests[id];
+ }
+
+ // ...or abort the former request otherwise
+ this.icinga.logger.debug(
+ 'Aborting pending request loading ',
+ url,
+ ' to ',
+ $target
+ );
+
+ this.requests[id].abort();
+ }
+
+ // Not sure whether we need this Accept-header
+ var headers = { 'X-Icinga-Accept': 'text/html' };
+
+ if (!! id) {
+ headers['X-Icinga-Container'] = id;
+ }
+
+ if (autorefresh) {
+ headers['X-Icinga-Autorefresh'] = '1';
+ }
+
+ // Ask for a new window id in case we don't already have one
+ if (this.icinga.ui.hasWindowId()) {
+ var windowId = this.icinga.ui.getWindowId();
+ var containerId = this.icinga.ui.getUniqueContainerId($target);
+ if (containerId) {
+ windowId = windowId + '_' + containerId;
+ }
+ headers['X-Icinga-WindowId'] = windowId;
+ } else {
+ headers['X-Icinga-WindowId'] = 'undefined';
+ }
+
+ if (typeof extraHeaders !== 'undefined') {
+ headers = $.extend(headers, extraHeaders);
+ }
+
+ // This is jQuery's default content type
+ var contentType = 'application/x-www-form-urlencoded; charset=UTF-8';
+
+ var isFormData = typeof window.FormData !== 'undefined' && data instanceof window.FormData;
+ if (isFormData) {
+ // Setting false is mandatory as the form's data
+ // won't be recognized by the server otherwise
+ contentType = false;
+ }
+
+ var _this = this;
+ var req = $.ajax({
+ type : method,
+ url : url,
+ data : data,
+ headers: headers,
+ context: _this,
+ contentType: contentType,
+ processData: ! isFormData
+ });
+
+ req.$target = $target;
+ req.$redirectTarget = $target;
+ req.url = url;
+ req.done(this.onResponse);
+ req.fail(this.onFailure);
+ req.always(this.onComplete);
+ req.autorefresh = autorefresh;
+ req.autosubmit = false;
+ req.scripted = false;
+ req.method = method;
+ req.action = action;
+ req.addToHistory = true;
+ req.progressTimer = progressTimer;
+
+ if (url.match(/#/)) {
+ req.forceFocus = url.split(/#/)[1];
+ }
+
+ if (id) {
+ this.requests[id] = req;
+ }
+ if (! autorefresh) {
+ setTimeout(function () {
+ // The column may have not been shown before. To make the transition
+ // delay working we have to wait for the column getting rendered
+ if (! req.autosubmit && req.state() === 'pending') {
+ req.$target.addClass('impact');
+ }
+ }, 0);
+ }
+ this.icinga.ui.refreshDebug();
+ return req;
+ },
+
+ /**
+ * Create an URL relative to the Icinga base Url, still unused
+ *
+ * @param {string} url Relative url
+ */
+ url: function (url) {
+ if (typeof url === 'undefined') {
+ return this.baseUrl;
+ }
+ return this.baseUrl + url;
+ },
+
+ stopPendingRequestsFor: function ($el) {
+ var id;
+ if (typeof $el === 'undefined' || ! (id = $el.attr('id'))) {
+ return;
+ }
+
+ if (typeof this.requests[id] !== 'undefined') {
+ this.requests[id].abort();
+ }
+ },
+
+ filterAutorefreshingContainers: function () {
+ return $(this).data('icingaRefresh') > 0 && ! $(this).is('[data-suspend-autorefresh]');
+ },
+
+ autorefresh: function () {
+ var _this = this;
+
+ $('.container').filter(this.filterAutorefreshingContainers).each(function (idx, el) {
+ var $el = $(el);
+ var id = $el.attr('id');
+
+ // Always request application-state
+ if (id !== 'application-state' && (! _this.autorefreshEnabled || _this.autorefreshSuspended)) {
+ // Continue
+ return true;
+ }
+
+ if (typeof _this.requests[id] !== 'undefined') {
+ _this.icinga.logger.debug('No refresh, request pending for ', id);
+ return;
+ }
+
+ var interval = $el.data('icingaRefresh');
+ var lastUpdate = $el.data('lastUpdate');
+
+ if (typeof interval === 'undefined' || ! interval) {
+ _this.icinga.logger.info('No interval, setting default', id);
+ interval = 10;
+ }
+
+ if (typeof lastUpdate === 'undefined' || ! lastUpdate) {
+ _this.icinga.logger.info('No lastUpdate, setting one', id);
+ $el.data('lastUpdate',(new Date()).getTime());
+ return;
+ }
+ interval = interval * 1000;
+
+ // TODO:
+ if ((lastUpdate + interval) > (new Date()).getTime()) {
+ // self.icinga.logger.info(
+ // 'Skipping refresh',
+ // id,
+ // lastUpdate,
+ // interval,
+ // (new Date()).getTime()
+ // );
+ return;
+ }
+
+ if (_this.loadUrl($el.data('icingaUrl'), $el, undefined, undefined, undefined, true) === false) {
+ _this.icinga.logger.debug(
+ 'NOT autorefreshing ' + id + ', even if ' + interval + ' ms passed. Request pending?'
+ );
+ } else {
+ _this.icinga.logger.debug(
+ 'Autorefreshing ' + id + ' ' + interval + ' ms passed'
+ );
+ }
+ el = null;
+ });
+ },
+
+ /**
+ * Disable the autorefresh mechanism
+ */
+ disableAutorefresh: function () {
+ this.autorefreshEnabled = false;
+ },
+
+ /**
+ * Enable the autorefresh mechanism
+ */
+ enableAutorefresh: function () {
+ this.autorefreshEnabled = true;
+ },
+
+ processNotificationHeader: function(req) {
+ var header = req.getResponseHeader('X-Icinga-Notification');
+ var _this = this;
+ if (! header) return false;
+ var list = header.split('&');
+ $.each(list, function(idx, el) {
+ var parts = decodeURIComponent(el).split(' ');
+ _this.createNotice(parts.shift(), parts.join(' '));
+ });
+ return true;
+ },
+
+ /**
+ * Process the X-Icinga-Redirect HTTP Response Header
+ *
+ * If the response includes the X-Icinga-Redirect header, redirects to the URL associated with the header.
+ *
+ * @param {object} req Current request
+ *
+ * @returns {boolean} Whether we're about to redirect
+ */
+ processRedirectHeader: function(req) {
+ var icinga = this.icinga,
+ redirect = req.getResponseHeader('X-Icinga-Redirect');
+
+ if (! redirect) {
+ return false;
+ }
+
+ redirect = decodeURIComponent(redirect);
+ if (redirect.match(/__SELF__/)) {
+ if (req.autorefresh) {
+ // Redirect to the current window's URL in case it's an auto-refresh request. If authenticated
+ // externally this ensures seamless re-login if the session's expired
+ redirect = redirect.replace(
+ /__SELF__/,
+ encodeURIComponent(
+ document.location.pathname + document.location.search + document.location.hash
+ )
+ );
+ } else {
+ // Redirect to the URL which required authentication. When clicking a link this ensures that we
+ // redirect to the link's URL instead of the current window's URL (see above)
+ redirect = redirect.replace(/__SELF__/, req.url);
+ }
+ } else if (redirect.match(/__BACK__/)) {
+ if (req.$redirectTarget.is('#col1')) {
+ icinga.logger.warn('Cannot navigate back. Redirect target is #col1');
+ return false;
+ }
+
+ var originUrl = req.$target.data('icingaUrl');
+
+ $(window).on('popstate.__back__', { self: this }, function (event) {
+ var _this = event.data.self;
+ var $refreshTarget = $('#col2');
+ var refreshUrl;
+
+ var hash = icinga.history.getCol2State();
+ if (hash && hash.match(/^#!/)) {
+ // TODO: These three lines are copied from history.js, I don't like this
+ var parts = hash.split(/#!/);
+
+ if (parts[1] === originUrl) {
+ // After a page load a call to back() seems not to have an effect
+ icinga.ui.layout1col();
+ } else {
+ refreshUrl = parts[1];
+ }
+ }
+
+ if (typeof refreshUrl === 'undefined' && icinga.ui.isOneColLayout()) {
+ refreshUrl = icinga.history.getCol1State();
+ $refreshTarget = $('#col1');
+ }
+
+ // loadUrl won't run this request, as due to the history handling it's already running.
+ // The important bit though is that it returns this (already running) request. This way
+ // it's possible to set `scripted = true` on the request. (`addToHistory = true` should
+ // already be the case, though it's added again just in case it's not already `true`..)
+ var req = _this.loadUrl(refreshUrl, $refreshTarget);
+ req.addToHistory = false;
+ req.scripted = true;
+
+ setTimeout(function () {
+ // TODO: Find a better solution than a hardcoded one
+ // This is still the *cheat* to get live results
+ // (in case there's a delay and a change is not instantly effective)
+ var req = _this.loadUrl(refreshUrl, $refreshTarget);
+ req.addToHistory = false;
+ req.scripted = true;
+ }, 1000);
+
+ $(window).off('popstate.__back__');
+ });
+
+ // Navigate back, no redirect desired
+ window.history.back();
+
+ return true;
+ } else if (redirect.match(/__CLOSE__/)) {
+ if (req.$redirectTarget.is('#col1')) {
+ icinga.logger.warn('Cannot close #col1');
+ return false;
+ }
+
+ // Close right column as requested
+ icinga.ui.layout1col();
+
+ // Refresh left column and produce a new history state for it
+ var $col1 = $('#col1');
+ var col1Url = icinga.history.getCol1State();
+ var refresh = this.loadUrl(col1Url, $col1);
+ refresh.scripted = true;
+
+ var _this = this;
+ setTimeout(function () {
+ // TODO: Find a better solution than a hardcoded one
+ // This is still the *cheat* to get live results
+ // (in case there's a delay and a change is not instantly effective)
+ var secondRefresh = _this.loadUrl(col1Url, $col1);
+ if (secondRefresh !== refresh) {
+ // Only change these properties if it's not still the first refresh
+ secondRefresh.addToHistory = false;
+ secondRefresh.scripted = true;
+ }
+ }, 1000);
+
+ return true;
+ }
+
+ var useHttp = req.getResponseHeader('X-Icinga-Redirect-Http');
+ if (useHttp === 'yes') {
+ window.location.replace(redirect);
+ return true;
+ }
+
+ this.redirectToUrl(redirect, req.$redirectTarget, req);
+ return true;
+ },
+
+ /**
+ * Redirect to the given url
+ *
+ * @param {string} url
+ * @param {object} $target
+ * @param {XMLHttpRequest} referrer
+ */
+ redirectToUrl: function (url, $target, referrer) {
+ var icinga = this.icinga,
+ rerenderLayout,
+ autoRefreshInterval,
+ forceFocus,
+ origin;
+
+ if (typeof referrer !== 'undefined') {
+ rerenderLayout = referrer.getResponseHeader('X-Icinga-Rerender-Layout');
+ autoRefreshInterval = referrer.getResponseHeader('X-Icinga-Refresh');
+ forceFocus = referrer.forceFocus;
+ origin = referrer.url;
+ }
+
+ icinga.logger.debug(
+ 'Got redirect for ', $target, ', URL was ' + url
+ );
+
+ if (rerenderLayout) {
+ var parts = url.split(/#!/);
+ url = parts.shift();
+ var redirectionUrl = icinga.utils.addUrlFlag(url, 'renderLayout');
+ var r = this.loadUrl(redirectionUrl, $('#layout'));
+ r.historyUrl = url;
+ if (parts.length) {
+ r.loadNext = parts;
+ } else if (!! document.location.hash) {
+ // Retain detail URL if the layout is rerendered
+ parts = document.location.hash.split('#!').splice(1);
+ if (parts.length) {
+ r.loadNext = $.grep(parts, function (url) {
+ if (url !== origin) {
+ icinga.logger.debug('Retaining detail url ' + url);
+ return true;
+ }
+
+ icinga.logger.debug('Discarding detail url ' + url + ' as it\'s the origin of the redirect');
+ return false;
+ });
+ }
+ }
+ } else {
+ if (url.match(/#!/)) {
+ var parts = url.split(/#!/);
+ icinga.ui.layout2col();
+ this.loadUrl(parts.shift(), $('#col1'));
+ this.loadUrl(parts.shift(), $('#col2'));
+ } else {
+ if ($target.attr('id') === 'col2') { // TODO: multicol
+ if (($target.data('icingaUrl') || '').split('?')[0] === url.split('?')[0]) {
+ // Don't do anything in this case
+ } else if ($('#col1').data('icingaUrl').split('?')[0] === url.split('?')[0]) {
+ icinga.ui.layout1col();
+ $target = $('#col1');
+ delete(this.requests['col2']);
+ }
+ }
+
+ var req = this.loadUrl(url, $target);
+ req.forceFocus = url === origin ? forceFocus : null;
+ req.autoRefreshInterval = autoRefreshInterval;
+ req.referrer = referrer;
+ }
+ }
+ },
+
+ /**
+ * Handle successful XHR response
+ */
+ onResponse: function (data, textStatus, req) {
+ var _this = this;
+ if (this.failureNotice !== null) {
+ if (! this.failureNotice.hasClass('fading-out')) {
+ this.failureNotice.remove();
+ }
+ this.failureNotice = null;
+ }
+
+ var target = req.getResponseHeader('X-Icinga-Container');
+ var newBody = false;
+ var oldNotifications = false;
+ if (target) {
+ if (target === 'ignore') {
+ return;
+ }
+
+ var $newTarget = this.identifyLinkTarget(target, req.$target);
+ if ($newTarget.length) {
+ // If we change the target, oncomplete will fail to clean up
+ // This fixes the problem, not using req.$target would be better
+ delete this.requests[req.$target.attr('id')];
+
+ req.$target = $newTarget;
+ req.$redirectTarget = $newTarget;
+
+ if (target === 'layout') {
+ oldNotifications = $('#notifications li').detach();
+ this.icinga.ui.layout1col();
+ newBody = true;
+ }
+ }
+ }
+
+ if (req.autorefresh && req.$target.is('[data-suspend-autorefresh]')) {
+ return;
+ }
+
+ this.icinga.logger.debug(
+ 'Got response for ', req.$target, ', URL was ' + req.url
+ );
+ this.processNotificationHeader(req);
+
+ var cssreload = req.getResponseHeader('X-Icinga-Reload-Css');
+ if (cssreload) {
+ this.icinga.ui.reloadCss();
+ }
+
+ if (req.getResponseHeader('X-Icinga-Redirect')) {
+ return;
+ }
+
+ if (req.getResponseHeader('X-Icinga-Announcements') === 'refresh') {
+ var announceReq = _this.loadUrl(_this.url('/layout/announcements'), $('#announcements'));
+ announceReq.addToHistory = false;
+ announceReq.scripted = true;
+ }
+
+ var classes;
+
+ if (target !== 'layout') {
+ var moduleName = req.getResponseHeader('X-Icinga-Module');
+ classes = $.grep(req.$target.classes(), function (el) {
+ if (el === 'icinga-module' || el.match(/^module\-/)) {
+ return false;
+ }
+ return true;
+ });
+ if (moduleName) {
+ // Lazy load module javascript (Applies only to module.js code)
+ _this.icinga.ensureModule(moduleName);
+
+ req.$target.data('icingaModule', moduleName);
+ classes.push('icinga-module');
+ classes.push('module-' + moduleName);
+ } else {
+ req.$target.removeData('icingaModule');
+ if (req.$target.attr('data-icinga-module')) {
+ req.$target.removeAttr('data-icinga-module');
+ }
+ }
+ req.$target.attr('class', classes.join(' '));
+
+ var refresh = req.autoRefreshInterval || req.getResponseHeader('X-Icinga-Refresh');
+ if (refresh) {
+ req.$target.data('icingaRefresh', refresh);
+ } else {
+ req.$target.removeData('icingaRefresh');
+ if (req.$target.attr('data-icinga-refresh')) {
+ req.$target.removeAttr('data-icinga-refresh');
+ }
+ }
+ }
+
+ var title = req.getResponseHeader('X-Icinga-Title');
+ if (title && (target === 'layout' || req.$target.is('#layout'))) {
+ this.icinga.ui.setTitle(decodeURIComponent(title));
+ } else if (title && ! req.autorefresh && req.$target.closest('.dashboard').length === 0) {
+ req.$target.data('icingaTitle', decodeURIComponent(title));
+ }
+
+ // Set a window identifier if the server asks us to do so
+ var windowId = req.getResponseHeader('X-Icinga-WindowId');
+ if (windowId) {
+ this.icinga.ui.setWindowId(windowId);
+ }
+
+ var referrer = req.referrer;
+ if (typeof referrer === 'undefined') {
+ referrer = req;
+ }
+
+ var autoSubmit = false;
+ var currentUrl = this.icinga.utils.parseUrl(req.$target.data('icingaUrl'));
+ if (referrer.method === 'POST') {
+ var newUrl = this.icinga.utils.parseUrl(req.url);
+ if (newUrl.path === currentUrl.path && this.icinga.utils.arraysEqual(newUrl.params, currentUrl.params)) {
+ autoSubmit = true;
+ }
+ }
+
+ req.$target.data('icingaUrl', req.url);
+
+ if (typeof req.progressTimer !== 'undefined') {
+ this.icinga.timer.unregister(req.progressTimer);
+ }
+
+ var contentSeparator = req.getResponseHeader('X-Icinga-Multipart-Content');
+ if (!! contentSeparator) {
+ var locationQuery = req.getResponseHeader('X-Icinga-Location-Query');
+ if (locationQuery !== null) {
+ let url = currentUrl.path + (locationQuery ? '?' + locationQuery : '');
+ if (req.autosubmit || autoSubmit) {
+ // Also update a form's action if it doesn't differ from the container's url
+ var $form = $(referrer.forceFocus).closest('form');
+ var formAction = $form.attr('action');
+ if (!! formAction) {
+ formAction = this.icinga.utils.parseUrl(formAction);
+ if (formAction.path === currentUrl.path
+ && this.icinga.utils.arraysEqual(formAction.params, currentUrl.params)
+ ) {
+ $form.attr('action', url);
+ }
+ }
+ }
+
+ req.$target.data('icingaUrl', url);
+ this.icinga.history.replaceCurrentState();
+ }
+
+ $.each(req.responseText.split(contentSeparator), function (idx, el) {
+ var match = el.match(/for=(Behavior:)?(\S+)\s+([^]*)/m);
+ if (!! match) {
+ if (match[1]) {
+ var behavior = _this.icinga.behaviors[match[2].toLowerCase()];
+ if (typeof behavior !== 'undefined' && typeof behavior.update === 'function') {
+ behavior.update(JSON.parse(match[3]));
+ } else {
+ _this.icinga.logger.warn(
+ 'Invalid behavior. Cannot update behavior "' + match[2] + '"');
+ }
+ } else {
+ var $target = $('#' + match[2]);
+ if ($target.length) {
+ var forceFocus;
+ if (req.forceFocus
+ && typeof req.forceFocus.jquery !== 'undefined'
+ && $.contains($target[0], req.forceFocus[0])
+ ) {
+ forceFocus = req.forceFocus;
+ }
+
+ _this.renderContentToContainer(
+ match[3],
+ $target,
+ 'replace',
+ req.autorefresh,
+ forceFocus,
+ req.autosubmit || autoSubmit,
+ req.scripted
+ );
+ } else {
+ _this.icinga.logger.warn(
+ 'Invalid target ID. Cannot render multipart to #' + match[2]);
+ }
+ }
+ } else {
+ _this.icinga.logger.error('Ill-formed multipart', el);
+ }
+ })
+ } else {
+ this.renderContentToContainer(
+ req.responseText,
+ req.$target,
+ req.action,
+ req.autorefresh,
+ req.forceFocus,
+ req.autosubmit || autoSubmit,
+ req.scripted
+ );
+ }
+
+ if (oldNotifications) {
+ oldNotifications.appendTo($('#notifications'));
+ }
+ if (newBody) {
+ this.icinga.ui.fixDebugVisibility().triggerWindowResize();
+ }
+ },
+
+ /**
+ * Regardless of whether a request succeeded of failed, clean up
+ */
+ onComplete: function (dataOrReq, textStatus, reqOrError) {
+ var _this = this;
+ var req;
+
+ if (typeof dataOrReq === 'object') {
+ req = dataOrReq;
+ } else {
+ req = reqOrError;
+ }
+
+ // Remove 'impact' class if there was such
+ if (req.$target.hasClass('impact')) {
+ req.$target.removeClass('impact');
+ } else {
+ var $impact = req.$target.find('.impact').first();
+ if ($impact.length) {
+ $impact.removeClass('impact');
+ }
+ }
+
+ if (req.getResponseHeader('X-Icinga-Reload-Window') === 'yes') {
+ window.location.reload();
+ return;
+ }
+
+ if (! req.autorefresh && ! req.autosubmit) {
+ // TODO: Hook for response/url?
+ var url = req.url;
+
+ if (req.$target[0].id === 'col1') {
+ this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl(url);
+ }
+
+ var $forms = $('[action="' + this.icinga.utils.parseUrl(url).path + '"]');
+ var $matches = $.merge($('[href="' + url + '"]'), $forms);
+ $matches.each(function (idx, el) {
+ var $el = $(el);
+ if ($el.closest('#menu').length) {
+ if ($el.is('form')) {
+ $('input', $el).addClass('active');
+ }
+ // Interrupt .each, only one menu item shall be active
+ return false;
+ }
+ });
+ }
+
+ // Update history when necessary
+ if (! req.autorefresh && req.addToHistory) {
+ if (req.$target.hasClass('container')) {
+ // We only want to care about top-level containers
+ if (req.$target.parent().closest('.container').length === 0) {
+ this.icinga.history.pushCurrentState();
+ }
+ } else {
+ // Request wasn't for a container, so it's usually the body
+ // or the full layout. Push request URL to history:
+ var url = typeof req.historyUrl !== 'undefined' ? req.historyUrl : req.url;
+ this.icinga.history.pushUrl(url);
+ }
+ }
+
+ req.$target.data('lastUpdate', (new Date()).getTime());
+ delete this.requests[req.$target.attr('id')];
+ this.icinga.ui.fadeNotificationsAway();
+
+ var extraUpdates = req.getResponseHeader('X-Icinga-Extra-Updates');
+ if (!! extraUpdates && req.getResponseHeader('X-Icinga-Redirect-Http') !== 'yes') {
+ $.each(extraUpdates.split(','), function (idx, el) {
+ var parts = el.trim().split(';');
+ var $target;
+ var url;
+ if (parts.length === 2) {
+ $target = $('#' + parts[0]);
+ if (! $target.length) {
+ _this.icinga.logger.warn('Invalid target ID. Cannot load extra URL', el);
+ return;
+ }
+
+ url = parts[1];
+ } else if (parts.length === 1) {
+ $target = $(parts[0]).closest(".container").not(req.$target);
+ if (! $target.length) {
+ _this.icinga.logger.warn('Invalid target ID. Cannot load extra URL', el);
+ return;
+ }
+
+ url = $target.data('icingaUrl');
+ } else {
+ _this.icinga.logger.error('Invalid extra update', el);
+ return;
+ }
+
+ _this.loadUrl(url, $target).addToHistory = false;
+ });
+ }
+
+ if (this.processRedirectHeader(req)) {
+ return;
+ }
+
+ if (typeof req.loadNext !== 'undefined' && req.loadNext.length) {
+ if ($('#col2').length) {
+ var r = this.loadUrl(req.loadNext[0], $('#col2'));
+ r.addToHistory = req.addToHistory;
+ this.icinga.ui.layout2col();
+ } else {
+ this.icinga.logger.error('Failed to load URL for #col2', req.loadNext);
+ }
+ }
+
+ // Lazy load module javascript (Applies only to module.js code)
+ this.icinga.ensureSubModules(req.$target);
+
+ req.$target.find('.container').each(function () {
+ $(this).trigger('rendered', [req.autorefresh, req.scripted, req.autosubmit]);
+ });
+ req.$target.trigger('rendered', [req.autorefresh, req.scripted, req.autosubmit]);
+
+ this.icinga.ui.refreshDebug();
+ },
+
+ /**
+ * Handle failed XHR response
+ */
+ onFailure: function (req, textStatus, errorThrown) {
+ var url = req.url;
+
+ /*
+ * Test if a manual actions comes in and autorefresh is active: Stop refreshing
+ */
+ if (req.addToHistory && ! req.autorefresh) {
+ req.$target.data('icingaRefresh', 0);
+ req.$target.data('icingaUrl', url);
+ icinga.history.pushCurrentState();
+ }
+
+ if (typeof req.progressTimer !== 'undefined') {
+ this.icinga.timer.unregister(req.progressTimer);
+ }
+
+ if (req.status > 0 && req.status < 501) {
+ this.icinga.logger.error(
+ req.status,
+ errorThrown + ':',
+ $(req.responseText).text().replace(/\s+/g, ' ').slice(0, 100)
+ );
+ this.renderContentToContainer(
+ req.responseText,
+ req.$target,
+ req.action,
+ req.autorefresh,
+ undefined,
+ req.autosubmit,
+ req.scripted
+ );
+ } else {
+ if (errorThrown === 'abort') {
+ this.icinga.logger.debug(
+ 'Request to ' + url + ' has been aborted for ',
+ req.$target
+ );
+
+ // Aborted requests should not be added to browser history
+ req.addToHistory = false;
+ } else {
+ if (this.failureNotice === null) {
+ var now = new Date();
+ var padString = this.icinga.utils.padString;
+ this.failureNotice = this.createNotice(
+ 'error',
+ 'The connection to the Icinga web server was lost at '
+ + now.getFullYear()
+ + '-' + padString(now.getMonth() + 1, 0, 2)
+ + '-' + padString(now.getDate(), 0, 2)
+ + ' ' + padString(now.getHours(), 0, 2)
+ + ':' + padString(now.getMinutes(), 0, 2)
+ + '.',
+ true
+ );
+ }
+
+ this.icinga.logger.error(
+ 'Failed to contact web server loading ',
+ url,
+ ' for ',
+ req.$target
+ );
+ }
+ }
+ },
+
+ /**
+ * Create a notification. Can be improved.
+ */
+ createNotice: function (severity, message, persist) {
+ var c = severity,
+ icon;
+ if (persist) {
+ c += ' persist';
+ }
+
+ switch (severity) {
+ case 'success':
+ icon = 'check-circle';
+ break;
+ case 'error':
+ icon = 'times';
+ break;
+ case 'warning':
+ icon = 'exclamation-triangle';
+ break;
+ case 'info':
+ icon = 'info-circle';
+ break;
+ }
+
+ var $notice = $(
+ '<li class="' + c + '">' +
+ '<i class="icon fa fa-' + icon + '"></i>' +
+ this.icinga.utils.escape(message) + '</li>'
+ ).appendTo($('#notifications'));
+
+ if (!persist) {
+ this.icinga.ui.fadeNotificationsAway();
+ }
+
+ return $notice;
+ },
+
+ /**
+ * Detect the link/form target for a given element (link, form, whatever)
+ *
+ * @param {object} $el jQuery set with the element
+ * @param {boolean} prepare Pass `false` to disable column preparation
+ */
+ getLinkTargetFor: function($el, prepare)
+ {
+ if (typeof prepare === 'undefined') {
+ prepare = true;
+ }
+
+ // If everything else fails, our target is the first column...
+ var $target = $('#col1');
+
+ // ...but usually we will use our own container...
+ var $container = $el.closest('.container');
+ if ($container.length) {
+ $target = $container;
+ }
+
+ // You can of course override the default behaviour:
+ if ($el.closest('[data-base-target]').length) {
+ var targetId = $el.closest('[data-base-target]').data('baseTarget');
+
+ $target = this.identifyLinkTarget(targetId, $el);
+ if (! $target.length) {
+ this.icinga.logger.warn('Link target "#' + targetId + '" does not exist in DOM.');
+ }
+ }
+
+ if (prepare) {
+ this.icinga.ui.prepareColumnFor($el, $target);
+ }
+
+ return $target;
+ },
+
+ /**
+ * Identify link target by the given id
+ *
+ * The id may also be one of the column aliases: `_next`, `_self` and `_main`
+ *
+ * @param {string} id
+ * @param {object} $of
+ * @return {object}
+ */
+ identifyLinkTarget: function (id, $of) {
+ var $target;
+
+ if (id === '_next') {
+ if (this.icinga.ui.hasOnlyOneColumn()) {
+ $target = $('#col1');
+ } else {
+ $target = $('#col2');
+ }
+ } else if (id === '_self') {
+ $target = $of.closest('.container');
+ } else if (id === '_main') {
+ $target = $('#col1');
+ } else {
+ $target = $('#' + id);
+ }
+
+ return $target;
+ },
+
+ /**
+ * Smoothly render given HTML to given container
+ */
+ renderContentToContainer: function (content, $container, action, autorefresh, forceFocus, autoSubmit, scripted) {
+ // Container update happens here
+ var scrollPos = false;
+ var _this = this;
+ var containerId = $container.attr('id');
+
+ var activeElementPath = false;
+ var navigationAnchor = false;
+ var focusFallback = false;
+
+ if (forceFocus && forceFocus.length) {
+ if (typeof forceFocus === 'string') {
+ navigationAnchor = forceFocus;
+ } else {
+ activeElementPath = this.icinga.utils.getCSSPath($(forceFocus));
+ }
+ } else if (document.activeElement && document.activeElement.id === 'search') {
+ activeElementPath = '#search';
+ } else if (document.activeElement
+ && document.activeElement !== document.body
+ && $.contains($container[0], document.activeElement)
+ ) {
+ // Active element in container
+ var $activeElement = $(document.activeElement);
+ var $pagination = $activeElement.closest('.pagination-control');
+ if ($pagination.length) {
+ focusFallback = {
+ 'parent': this.icinga.utils.getCSSPath($pagination),
+ 'child': '.active > a'
+ };
+ }
+ activeElementPath = this.icinga.utils.getCSSPath($activeElement);
+ }
+
+ var scrollTarget = $container;
+ if (typeof containerId !== 'undefined') {
+ if (autorefresh || autoSubmit) {
+ if ($container.css('display') === 'flex' && $container.is('.container')) {
+ var $scrollableContent = $container.children('.content');
+ scrollPos = $scrollableContent.scrollTop();
+ scrollTarget = _this.icinga.utils.getCSSPath($scrollableContent);
+ } else {
+ scrollPos = $container.scrollTop();
+ }
+ } else {
+ scrollPos = 0;
+ }
+ }
+
+ $container.trigger('beforerender', [content, action, autorefresh, scripted, autoSubmit]);
+
+ var discard = false;
+ $.each(_this.icinga.behaviors, function(name, behavior) {
+ if (behavior.renderHook) {
+ var changed = behavior.renderHook(content, $container, action, autorefresh, autoSubmit);
+ if (changed === null) {
+ discard = true;
+ } else {
+ content = changed;
+ }
+ }
+ });
+
+ $('.container', $container).each(function() {
+ _this.stopPendingRequestsFor($(this));
+ });
+
+ if (! discard) {
+ if ($container.closest('.dashboard').length) {
+ var title = $('h1', $container).first().detach();
+ $container.html(title).append(content);
+ } else if (action === 'replace') {
+ $container.html(content);
+ } else {
+ $container.append(content);
+ }
+ }
+
+ this.icinga.ui.assignUniqueContainerIds();
+
+ if (! discard && navigationAnchor) {
+ var $element = $container.find('#' + navigationAnchor);
+ if ($element.length) {
+ // data-icinga-no-scroll-on-focus is NOT designed to avoid scrolling for non-XHR requests
+ setTimeout(this.icinga.ui.focusElement.bind(this.icinga.ui), 0,
+ $element, $container, ! $element.is('[data-icinga-no-scroll-on-focus]'));
+ }
+ } else if (! activeElementPath) {
+ // Active element was not in this container
+ if (! autorefresh && ! autoSubmit && ! scripted) {
+ setTimeout(function() {
+ if (typeof $container.attr('tabindex') === 'undefined') {
+ $container.attr('tabindex', -1);
+ }
+ // Do not touch focus in case a module or component already placed it
+ if ($(document.activeElement).closest('.container').attr('id') !== containerId) {
+ _this.icinga.ui.focusElement($container);
+ }
+ }, 0);
+ }
+ } else {
+ setTimeout(function() {
+ var $activeElement = $(activeElementPath);
+
+ if ($activeElement.length && $activeElement.is(':visible')) {
+ $activeElement[0].focus({preventScroll: autorefresh || autoSubmit});
+ } else if (! autorefresh && ! autoSubmit && ! scripted) {
+ if (focusFallback) {
+ _this.icinga.ui.focusElement($(focusFallback.parent).find(focusFallback.child));
+ } else if (typeof $container.attr('tabindex') === 'undefined') {
+ $container.attr('tabindex', -1);
+ }
+ _this.icinga.ui.focusElement($container);
+ }
+ }, 0);
+ }
+
+ if (scrollPos !== false) {
+ var $scrollTarget = $(scrollTarget);
+ $scrollTarget.scrollTop(scrollPos);
+
+ // Fallback for browsers without support for focus({preventScroll: true})
+ setTimeout(function () {
+ if ($scrollTarget.scrollTop() !== scrollPos) {
+ $scrollTarget.scrollTop(scrollPos);
+ }
+ }, 0);
+ }
+
+ // Re-enable all click events (disabled as of performance reasons)
+ // $('*').off('click');
+ },
+
+ /**
+ * On shutdown we kill all pending requests
+ */
+ destroy: function() {
+ $.each(this.requests, function(id, request) {
+ request.abort();
+ });
+ this.icinga = null;
+ this.requests = {};
+ }
+
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/logger.js b/public/js/icinga/logger.js
new file mode 100644
index 0000000..471393c
--- /dev/null
+++ b/public/js/icinga/logger.js
@@ -0,0 +1,129 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Logger
+ *
+ * Well, log output. Rocket science.
+ */
+(function (Icinga) {
+
+ 'use strict';
+
+ Icinga.Logger = function (icinga) {
+
+ this.icinga = icinga;
+
+ this.logLevel = 'info';
+
+ this.logLevels = {
+ 'debug': 0,
+ 'info' : 1,
+ 'warn' : 2,
+ 'error': 3
+ };
+
+ };
+
+ Icinga.Logger.prototype = {
+
+ /**
+ * Whether the browser has a console object
+ */
+ hasConsole: function () {
+ return 'undefined' !== typeof console;
+ },
+
+ /**
+ * Raise or lower current log level
+ *
+ * Messages below this threshold will be silently discarded
+ */
+ setLevel: function (level) {
+ if ('undefined' !== typeof this.numericLevel(level)) {
+ this.logLevel = level;
+ }
+ return this;
+ },
+
+ /**
+ * Log a debug message
+ */
+ debug: function () {
+ return this.writeToConsole('debug', arguments);
+ },
+
+ /**
+ * Log an informational message
+ */
+ info: function () {
+ return this.writeToConsole('info', arguments);
+ },
+
+ /**
+ * Log a warning message
+ */
+ warn: function () {
+ return this.writeToConsole('warn', arguments);
+ },
+
+ /**
+ * Log an error message
+ */
+ error: function () {
+ return this.writeToConsole('error', arguments);
+ },
+
+ /**
+ * Write a log message with the given level to the console
+ */
+ writeToConsole: function (level, args) {
+
+ args = Array.prototype.slice.call(args);
+
+ // We want our log messages to carry precise timestamps
+ args.unshift(this.icinga.utils.timeWithMs());
+
+ if (this.hasConsole() && this.hasLogLevel(level)) {
+ if (typeof console[level] !== 'undefined') {
+ if (typeof console[level].apply === 'function') {
+ console[level].apply(console, args);
+ } else {
+ args.unshift('[' + level + ']');
+ console[level](args.join(' '));
+ }
+ } else if ('undefined' !== typeof console.log) {
+ args.unshift('[' + level + ']');
+ console.log(args.join(' '));
+ }
+ }
+ return this;
+ },
+
+ /**
+ * Return the numeric identifier for a given log level
+ */
+ numericLevel: function (level) {
+ var ret = this.logLevels[level];
+ if ('undefined' === typeof ret) {
+ throw 'Got invalid log level ' + level;
+ }
+ return ret;
+ },
+
+ /**
+ * Whether a given log level exists
+ */
+ hasLogLevel: function (level) {
+ return this.numericLevel(level) >= this.numericLevel(this.logLevel);
+ },
+
+ /**
+ * There isn't much to clean up here
+ */
+ destroy: function () {
+ this.enabled = false;
+ this.icinga = null;
+ }
+ };
+
+}(Icinga));
diff --git a/public/js/icinga/module.js b/public/js/icinga/module.js
new file mode 100644
index 0000000..2c2368e
--- /dev/null
+++ b/public/js/icinga/module.js
@@ -0,0 +1,134 @@
+/*! Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * This is how we bootstrap JS code in our modules
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Module = function (icinga, name, prototyp) {
+
+ // The Icinga instance
+ this.icinga = icinga;
+
+ // Applied event handlers
+ this.handlers = [];
+
+ // Event handlers registered by this module
+ this.registeredHandlers = [];
+
+ // The module name
+ this.name = name;
+
+ // The JS prototype for this module
+ this.prototyp = prototyp;
+
+ // Once initialized, this will be an instance of the modules prototype
+ this.object = {};
+
+ // Initialize this module
+ this.initialize();
+ };
+
+ Icinga.Module.prototype = {
+
+ initialize: function () {
+
+ if (typeof this.prototyp !== 'function') {
+ this.icinga.logger.error(
+ 'Unable to load module "' + this.name + '", constructor is missing'
+ );
+ return false;
+ }
+
+ try {
+
+ // The constructor of the modules prototype must be prepared to get an
+ // instance of Icinga.Module
+ this.object = new this.prototyp(this);
+ this.applyHandlers();
+ } catch(e) {
+ this.icinga.logger.error(
+ 'Failed to load module ' + this.name + ': ',
+ e
+ );
+
+ return false;
+ }
+
+ // That's all, the module is ready
+ this.icinga.logger.debug(
+ 'Module ' + this.name + ' has been initialized'
+ );
+
+ return true;
+ },
+
+ /**
+ * Register this modules event handlers
+ */
+ on: function (event, filter, handler) {
+ if (typeof handler === 'undefined') {
+ handler = filter;
+ filter = '.module-' + this.name;
+ } else {
+ filter = '.module-' + this.name + ' ' + filter;
+ }
+ this.registeredHandlers.push({event: event, filter: filter, handler: handler});
+
+ },
+
+ applyHandlers: function () {
+ var _this = this;
+
+ $.each(this.registeredHandlers, function (key, on) {
+ _this.bindEventHandler(
+ on.event,
+ on.filter,
+ on.handler
+ );
+ });
+ _this = null;
+
+ return this;
+ },
+
+ /**
+ * Effectively bind the given event handler
+ */
+ bindEventHandler: function (event, filter, handler) {
+ var _this = this;
+ this.icinga.logger.debug('Bound ' + filter + ' .' + event + '()');
+ this.handlers.push([event, filter, handler]);
+ $(document).on(event, filter, handler.bind(_this.object));
+ },
+
+ /**
+ * Unbind all event handlers bound by this module
+ */
+ unbindEventHandlers: function () {
+ $.each(this.handlers, function (idx, handler) {
+ $(document).off(handler[0], handler[1], handler[2]);
+ });
+ },
+
+ /**
+ * Allow to destroy and clean up this module
+ */
+ destroy: function () {
+
+ this.unbindEventHandlers();
+
+ if (typeof this.object.destroy === 'function') {
+ this.object.destroy();
+ }
+
+ this.object = null;
+ this.icinga = null;
+ this.prototyp = null;
+ }
+
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/storage.js b/public/js/icinga/storage.js
new file mode 100644
index 0000000..fa312d2
--- /dev/null
+++ b/public/js/icinga/storage.js
@@ -0,0 +1,549 @@
+/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+;(function(Icinga) {
+
+ 'use strict';
+
+ const KEY_TTL = 7776000000; // 90 days (90×24×60×60×1000)
+
+ /**
+ * Icinga.Storage
+ *
+ * localStorage access
+ *
+ * @param {string} prefix
+ */
+ Icinga.Storage = function(prefix) {
+
+ /**
+ * Prefix to use for keys
+ *
+ * @type {string}
+ */
+ this.prefix = prefix;
+
+ /**
+ * Storage backend
+ *
+ * @type {Storage}
+ */
+ this.backend = window.localStorage;
+ };
+
+ /**
+ * Callbacks for storage events on particular keys
+ *
+ * @type {{function}}
+ */
+ Icinga.Storage.subscribers = {};
+
+ /**
+ * Pass storage events to subscribers
+ *
+ * @param {StorageEvent} event
+ */
+ window.addEventListener('storage', function(event) {
+ var url = icinga.utils.parseUrl(event.url);
+ if (! url.path.startsWith(icinga.config.baseUrl)) {
+ // A localStorage is shared between all paths on the same origin.
+ // So we need to make sure it's us who made a change.
+ return;
+ }
+
+ if (typeof Icinga.Storage.subscribers[event.key] !== 'undefined') {
+ var newValue = null,
+ oldValue = null;
+ if (!! event.newValue) {
+ try {
+ newValue = JSON.parse(event.newValue);
+ } catch(error) {
+ icinga.logger.error('[Storage] Failed to parse new value (\`' + event.newValue
+ + '\`) for key "' + event.key + '". Error was: ' + error);
+ event.storageArea.removeItem(event.key);
+ return;
+ }
+ }
+ if (!! event.oldValue) {
+ try {
+ oldValue = JSON.parse(event.oldValue);
+ } catch(error) {
+ icinga.logger.warn('[Storage] Failed to parse old value (\`' + event.oldValue
+ + '\`) of key "' + event.key + '". Error was: ' + error);
+ oldValue = null;
+ }
+ }
+
+ Icinga.Storage.subscribers[event.key].forEach(function (subscriber) {
+ subscriber[0].call(subscriber[1], newValue, oldValue, event);
+ });
+ }
+ });
+
+ /**
+ * Create a new storage with `behavior.<name>` as prefix
+ *
+ * @param {string} name
+ *
+ * @returns {Icinga.Storage}
+ */
+ Icinga.Storage.BehaviorStorage = function(name) {
+ return new Icinga.Storage('behavior.' + name);
+ };
+
+ Icinga.Storage.prototype = {
+
+ /**
+ * Set the storage backend
+ *
+ * @param {Storage} backend
+ */
+ setBackend: function(backend) {
+ this.backend = backend;
+ },
+
+ /**
+ * Prefix the given key
+ *
+ * @param {string} key
+ *
+ * @returns {string}
+ */
+ prefixKey: function(key) {
+ var prefix = 'icinga.';
+ if (typeof this.prefix !== 'undefined') {
+ prefix = prefix + this.prefix + '.';
+ }
+
+ return prefix + key;
+ },
+
+ /**
+ * Store the given key-value pair
+ *
+ * @param {string} key
+ * @param {*} value
+ *
+ * @returns {void}
+ */
+ set: function(key, value) {
+ this.backend.setItem(this.prefixKey(key), JSON.stringify(value));
+ },
+
+ /**
+ * Get value for the given key
+ *
+ * @param {string} key
+ *
+ * @returns {*}
+ */
+ get: function(key) {
+ key = this.prefixKey(key);
+ var value = this.backend.getItem(key);
+
+ try {
+ return JSON.parse(value);
+ } catch(error) {
+ icinga.logger.error('[Storage] Failed to parse value (\`' + value
+ + '\`) of key "' + key + '". Error was: ' + error);
+ this.backend.removeItem(key);
+ return null;
+ }
+ },
+
+ /**
+ * Remove given key from storage
+ *
+ * @param {string} key
+ *
+ * @returns {void}
+ */
+ remove: function(key) {
+ this.backend.removeItem(this.prefixKey(key));
+ },
+
+ /**
+ * Subscribe with a callback for events on a particular key
+ *
+ * @param {string} key
+ * @param {function} callback
+ * @param {object} context
+ *
+ * @returns {void}
+ */
+ onChange: function(key, callback, context) {
+ if (this.backend !== window.localStorage) {
+ throw new Error('[Storage] Only the localStorage emits events');
+ }
+
+ var prefixedKey = this.prefixKey(key);
+
+ if (typeof Icinga.Storage.subscribers[prefixedKey] === 'undefined') {
+ Icinga.Storage.subscribers[prefixedKey] = [];
+ }
+
+ Icinga.Storage.subscribers[prefixedKey].push([callback, context]);
+ }
+ };
+
+ /**
+ * Icinga.Storage.StorageAwareMap
+ *
+ * @param {object} items
+ * @constructor
+ */
+ Icinga.Storage.StorageAwareMap = function(items) {
+
+ /**
+ * Storage object
+ *
+ * @type {Icinga.Storage}
+ */
+ this.storage = undefined;
+
+ /**
+ * Storage key
+ *
+ * @type {string}
+ */
+ this.key = undefined;
+
+ /**
+ * Event listeners for our internal events
+ *
+ * @type {{}}
+ */
+ this.eventListeners = {
+ 'add': [],
+ 'delete': []
+ };
+
+ /**
+ * The internal (real) map
+ *
+ * @type {Map<*>}
+ */
+ this.data = new Map();
+
+ // items is not passed directly because IE11 doesn't support constructor arguments
+ if (typeof items !== 'undefined' && !! items) {
+ Object.keys(items).forEach(function(key) {
+ this.data.set(key, items[key]);
+ }, this);
+ }
+ };
+
+ /**
+ * Create a new StorageAwareMap for the given storage and key
+ *
+ * @param {Icinga.Storage} storage
+ * @param {string} key
+ *
+ * @returns {Icinga.Storage.StorageAwareMap}
+ */
+ Icinga.Storage.StorageAwareMap.withStorage = function(storage, key) {
+ var items = storage.get(key);
+ if (typeof items !== 'undefined' && !! items) {
+ Object.keys(items).forEach(function(key) {
+ var value = items[key];
+
+ if (typeof value !== 'object' || typeof value['lastAccess'] === 'undefined') {
+ items[key] = {'value': value, 'lastAccess': Date.now()};
+ } else if (Date.now() - value['lastAccess'] > KEY_TTL) {
+ delete items[key];
+ }
+ }, this);
+ }
+
+ if (!! items && Object.keys(items).length) {
+ storage.set(key, items);
+ } else if (items !== null) {
+ storage.remove(key);
+ }
+
+ return (new Icinga.Storage.StorageAwareMap(items).setStorage(storage, key));
+ };
+
+ Icinga.Storage.StorageAwareMap.prototype = {
+
+ /**
+ * Bind this map to the given storage and key
+ *
+ * @param {Icinga.Storage} storage
+ * @param {string} key
+ *
+ * @returns {this}
+ */
+ setStorage: function(storage, key) {
+ this.storage = storage;
+ this.key = key;
+
+ if (storage.backend === window.localStorage) {
+ storage.onChange(key, this.onChange, this);
+ }
+
+ return this;
+ },
+
+ /**
+ * Return a boolean indicating this map got a storage
+ *
+ * @returns {boolean}
+ */
+ hasStorage: function() {
+ return typeof this.storage !== 'undefined' && typeof this.key !== 'undefined';
+ },
+
+ /**
+ * Update the storage
+ *
+ * @returns {void}
+ */
+ updateStorage: function() {
+ if (! this.hasStorage()) {
+ return;
+ }
+
+ if (this.size > 0) {
+ this.storage.set(this.key, this.toObject());
+ } else {
+ this.storage.remove(this.key);
+ }
+ },
+
+ /**
+ * Update the map
+ *
+ * @param {object} newValue
+ */
+ onChange: function(newValue) {
+ // Check for deletions first. Uses keys() to iterate over a copy
+ this.keys().forEach(function (key) {
+ if (newValue === null || typeof newValue[key] === 'undefined') {
+ var value = this.data.get(key)['value'];
+ this.data.delete(key);
+ this.trigger('delete', key, value);
+ }
+ }, this);
+
+ if (newValue === null) {
+ return;
+ }
+
+ // Now check for new entries
+ Object.keys(newValue).forEach(function(key) {
+ var known = this.data.has(key);
+ // Always override any known value as we want to keep track of all `lastAccess` changes
+ this.data.set(key, newValue[key]);
+
+ if (! known) {
+ this.trigger('add', key, newValue[key]['value']);
+ }
+ }, this);
+ },
+
+ /**
+ * Register an event handler to handle storage updates
+ *
+ * Available events are: add, delete. The callback receives the
+ * key and its value as first and second argument, respectively.
+ *
+ * @param {string} event
+ * @param {function} callback
+ * @param {object} thisArg
+ *
+ * @returns {this}
+ */
+ on: function(event, callback, thisArg) {
+ if (typeof this.eventListeners[event] === 'undefined') {
+ throw new Error('Invalid event "' + event + '"');
+ }
+
+ this.eventListeners[event].push([callback, thisArg]);
+ return this;
+ },
+
+ /**
+ * Trigger all event handlers for the given event
+ *
+ * @param {string} event
+ * @param {string} key
+ * @param {*} value
+ */
+ trigger: function(event, key, value) {
+ this.eventListeners[event].forEach(function (handler) {
+ var thisArg = handler[1];
+ if (typeof thisArg === 'undefined') {
+ thisArg = this;
+ }
+
+ handler[0].call(thisArg, key, value);
+ });
+ },
+
+ /**
+ * Return the number of key/value pairs in the map
+ *
+ * @returns {number}
+ */
+ get size() {
+ return this.data.size;
+ },
+
+ /**
+ * Set the value for the key in the map
+ *
+ * @param {string} key
+ * @param {*} value Default null
+ *
+ * @returns {this}
+ */
+ set: function(key, value) {
+ if (typeof value === 'undefined') {
+ value = null;
+ }
+
+ this.data.set(key, {'value': value, 'lastAccess': Date.now()});
+
+ this.updateStorage();
+ return this;
+ },
+
+ /**
+ * Remove all key/value pairs from the map
+ *
+ * @returns {void}
+ */
+ clear: function() {
+ this.data.clear();
+ this.updateStorage();
+ },
+
+ /**
+ * Remove the given key from the map
+ *
+ * @param {string} key
+ *
+ * @returns {boolean}
+ */
+ delete: function(key) {
+ var retVal = this.data.delete(key);
+
+ this.updateStorage();
+ return retVal;
+ },
+
+ /**
+ * Return a list of [key, value] pairs for every item in the map
+ *
+ * @returns {Array}
+ */
+ entries: function() {
+ var list = [];
+
+ if (this.size > 0) {
+ this.data.forEach(function (value, key) {
+ list.push([key, value['value']]);
+ });
+ }
+
+ return list;
+ },
+
+ /**
+ * Execute a provided function once for each item in the map, in insertion order
+ *
+ * @param {function} callback
+ * @param {object} thisArg
+ *
+ * @returns {void}
+ */
+ forEach: function(callback, thisArg) {
+ if (typeof thisArg === 'undefined') {
+ thisArg = this;
+ }
+
+ this.data.forEach(function(value, key) {
+ callback.call(thisArg, value['value'], key);
+ });
+ },
+
+ /**
+ * Return the value associated to the key, or undefined if there is none
+ *
+ * @param {string} key
+ *
+ * @returns {*}
+ */
+ get: function(key) {
+ var value = this.data.get(key)['value'];
+ this.set(key, value); // Update `lastAccess`
+
+ return value;
+ },
+
+ /**
+ * Return a boolean asserting whether a value has been associated to the key in the map
+ *
+ * @param {string} key
+ *
+ * @returns {boolean}
+ */
+ has: function(key) {
+ return this.data.has(key);
+ },
+
+ /**
+ * Return an array of keys in the map
+ *
+ * @returns {Array}
+ */
+ keys: function() {
+ var list = [];
+
+ if (this.size > 0) {
+ // .forEach() is used because IE11 doesn't support .keys()
+ this.data.forEach(function(_, key) {
+ list.push(key);
+ });
+ }
+
+ return list;
+ },
+
+ /**
+ * Return an array of values in the map
+ *
+ * @returns {Array}
+ */
+ values: function() {
+ var list = [];
+
+ if (this.size > 0) {
+ // .forEach() is used because IE11 doesn't support .values()
+ this.data.forEach(function(value) {
+ list.push(value['value']);
+ });
+ }
+
+ return list;
+ },
+
+ /**
+ * Return this map as simple object
+ *
+ * @returns {object}
+ */
+ toObject: function() {
+ var obj = {};
+
+ if (this.size > 0) {
+ this.data.forEach(function (value, key) {
+ obj[key] = value;
+ });
+ }
+
+ return obj;
+ }
+ };
+
+}(Icinga));
diff --git a/public/js/icinga/timer.js b/public/js/icinga/timer.js
new file mode 100644
index 0000000..0fea4d9
--- /dev/null
+++ b/public/js/icinga/timer.js
@@ -0,0 +1,176 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Timer
+ *
+ * Timer events are triggered once a second. Runs all reegistered callback
+ * functions and is able to preserve a desired scope.
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Timer = function (icinga) {
+
+ /**
+ * We keep a reference to the Icinga instance even if we don't need it
+ */
+ this.icinga = icinga;
+
+ /**
+ * The Interval object
+ */
+ this.ticker = null;
+
+ /**
+ * Fixed default interval is 250ms
+ */
+ this.interval = 250;
+
+ /**
+ * Our registerd observers
+ */
+ this.observers = [];
+
+ /**
+ * Counter
+ */
+ this.stepCounter = 0;
+
+ this.start = (new Date()).getTime();
+
+
+ this.lastRuntime = [];
+
+ this.isRunning = false;
+ };
+
+ Icinga.Timer.prototype = {
+
+ /**
+ * The initialization function starts our ticker
+ */
+ initialize: function () {
+ this.isRunning = true;
+
+ var _this = this;
+ var f = function () {
+ if (_this.isRunning) {
+ _this.tick();
+ setTimeout(f, _this.interval);
+ }
+ };
+ f();
+ },
+
+ /**
+ * We will trigger our tick function once a second. It will call each
+ * registered observer.
+ */
+ tick: function () {
+
+ var icinga = this.icinga;
+
+ $.each(this.observers, function (idx, observer) {
+ if (observer.isDue()) {
+ observer.run();
+ } else {
+ // Not due
+ }
+ });
+ icinga = null;
+ },
+
+ /**
+ * Register a given callback function to be run within an optional scope.
+ */
+ register: function (callback, scope, interval) {
+
+ var observer;
+
+ try {
+
+ if (typeof scope === 'undefined') {
+ observer = new Icinga.Timer.Interval(callback, interval);
+ } else {
+ observer = new Icinga.Timer.Interval(
+ callback.bind(scope),
+ interval
+ );
+ }
+
+ this.observers.push(observer);
+
+ } catch(err) {
+ this.icinga.logger.error(err);
+ }
+
+ return observer;
+ },
+
+ unregister: function (observer) {
+
+ var idx = $.inArray(observer, this.observers);
+ if (idx > -1) {
+ this.observers.splice(idx, 1);
+ }
+
+ return this;
+ },
+
+ /**
+ * Our destroy function will clean up everything. Unused right now.
+ */
+ destroy: function () {
+ this.isRunning = false;
+
+ this.icinga = null;
+ $.each(this.observers, function (idx, observer) {
+ observer.destroy();
+ });
+
+ this.observers = [];
+ }
+ };
+
+ Icinga.Timer.Interval = function (callback, interval) {
+
+ if ('undefined' === typeof interval) {
+ throw 'Timer interval is required';
+ }
+
+ if (interval < 100) {
+ throw 'Timer interval cannot be less than 100ms, got ' + interval;
+ }
+
+ this.lastRun = (new Date()).getTime();
+
+ this.interval = interval;
+
+ this.scheduledNextRun = this.lastRun + interval;
+
+ this.callback = callback;
+ };
+
+ Icinga.Timer.Interval.prototype = {
+
+ isDue: function () {
+ return this.scheduledNextRun < (new Date()).getTime();
+ },
+
+ run: function () {
+ this.lastRun = (new Date()).getTime();
+
+ while (this.scheduledNextRun < this.lastRun) {
+ this.scheduledNextRun += this.interval;
+ }
+
+ this.callback();
+ },
+
+ destroy: function () {
+ this.callback = null;
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/timezone.js b/public/js/icinga/timezone.js
new file mode 100644
index 0000000..1c2647b
--- /dev/null
+++ b/public/js/icinga/timezone.js
@@ -0,0 +1,105 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ 'use strict';
+
+ /**
+ * Get the maximum timezone offset
+ *
+ * @returns {Number}
+ */
+ Date.prototype.getStdTimezoneOffset = function() {
+ var year = new Date().getFullYear();
+ var offsetInJanuary = new Date(year, 0, 2).getTimezoneOffset();
+ var offsetInJune = new Date(year, 5, 2).getTimezoneOffset();
+
+ return Math.max(offsetInJanuary, offsetInJune);
+ };
+
+ /**
+ * Test for daylight saving time zone
+ *
+ * @returns {boolean}
+ */
+ Date.prototype.isDst = function() {
+ return this.getStdTimezoneOffset() !== this.getTimezoneOffset();
+ };
+
+ /**
+ * Write timezone information into a cookie
+ *
+ * @constructor
+ */
+ Icinga.Timezone = function() {
+ this.cookieName = 'icingaweb2-tzo';
+ };
+
+ Icinga.Timezone.prototype = {
+ /**
+ * Initialize interface method
+ */
+ initialize: function () {
+ this.writeTimezone();
+ },
+
+ destroy: function() {
+ // PASS
+ },
+
+ /**
+ * Write timezone information into cookie
+ */
+ writeTimezone: function() {
+ var date = new Date();
+ var timezoneOffset = (date.getTimezoneOffset()*60) * -1;
+ var dst = date.isDst();
+
+ if (this.readCookie(this.cookieName)) {
+ return;
+ }
+
+ this.writeCookie(this.cookieName, timezoneOffset + '-' + Number(dst), 1);
+ },
+
+ /**
+ * Write cookie data
+ *
+ * @param {String} name
+ * @param {String} value
+ * @param {Number} days
+ */
+ writeCookie: function(name, value, days) {
+ var expires = '';
+
+ if (days) {
+ var date = new Date();
+ date.setTime(date.getTime()+(days*24*60*60*1000));
+ var expires = '; expires=' + date.toGMTString();
+ }
+ document.cookie = name + '=' + value + expires + '; path=/';
+ },
+
+ /**
+ * Read cookie data
+ *
+ * @param {String} name
+ * @returns {*}
+ */
+ readCookie: function(name) {
+ var nameEq = name + '=';
+ var ca = document.cookie.split(';');
+ for(var i=0;i < ca.length;i++) {
+ var c = ca[i];
+ while (c.charAt(0)==' ') {
+ c = c.substring(1,c.length);
+ }
+ if (c.indexOf(nameEq) == 0) {
+ return c.substring(nameEq.length,c.length);
+ }
+ }
+ return null;
+ }
+ };
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js
new file mode 100644
index 0000000..b41de06
--- /dev/null
+++ b/public/js/icinga/ui.js
@@ -0,0 +1,634 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.UI
+ *
+ * Our user interface
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.UI = function (icinga) {
+
+ this.icinga = icinga;
+
+ this.currentLayout = 'default';
+
+ this.debug = false;
+
+ this.debugTimer = null;
+
+ this.timeCounterTimer = null;
+
+ // detect currentLayout
+ var classList = $('#layout').attr('class').split(/\s+/);
+ var _this = this;
+ var matched;
+ $.each(classList, function(index, item) {
+ if (null !== (matched = item.match(/^([a-z]+)-layout$/))) {
+ var layout = matched[1];
+ if (layout !== 'fullscreen') {
+ _this.currentLayout = layout;
+ // Break loop
+ return false;
+ }
+ }
+ });
+ };
+
+ Icinga.UI.prototype = {
+
+ initialize: function () {
+ $('html').removeClass('no-js').addClass('js');
+ this.enableTimeCounters();
+ this.triggerWindowResize();
+ this.fadeNotificationsAway();
+
+ $(document).on('click', '#mobile-menu-toggle', this.toggleMobileMenu);
+ $(document).on('keypress', '#search',{ self: this, type: 'key' }, this.closeMobileMenu);
+ $(document).on('mouseleave', '#sidebar', { self: this, type: 'leave' }, this.closeMobileMenu);
+ $(document).on('click', '#sidebar a', { self: this, type: 'navigate' }, this.closeMobileMenu);
+ },
+
+ fadeNotificationsAway: function() {
+ var icinga = this.icinga;
+ $('#notifications li')
+ .not('.fading-out')
+ .not('.persist')
+ .addClass('fading-out')
+ .delay(7000)
+ .fadeOut('slow',
+ function() {
+ $(this).remove();
+ });
+ },
+
+ toggleDebug: function() {
+ if (this.debug) {
+ return this.disableDebug();
+ } else {
+ return this.enableDebug();
+ }
+ },
+
+ enableDebug: function () {
+ if (this.debug === true) { return this; }
+ this.debug = true;
+ this.debugTimer = this.icinga.timer.register(
+ this.refreshDebug,
+ this,
+ 1000
+ );
+ this.fixDebugVisibility();
+
+ return this;
+ },
+
+ fixDebugVisibility: function () {
+ if (this.debug) {
+ $('#responsive-debug').css({display: 'block'});
+ } else {
+ $('#responsive-debug').css({display: 'none'});
+ }
+ return this;
+ },
+
+ disableDebug: function () {
+ if (this.debug === false) { return; }
+
+ this.debug = false;
+ this.icinga.timer.unregister(this.debugTimer);
+ this.debugTimer = null;
+ this.fixDebugVisibility();
+ return this;
+ },
+
+ reloadCss: function () {
+ var icinga = this.icinga;
+ icinga.logger.info('Reloading CSS');
+ $('link').each(function() {
+ var $oldLink = $(this);
+ if ($oldLink.hasAttr('type') && $oldLink.attr('type').indexOf('css') > -1) {
+ var $newLink = $oldLink.clone().attr(
+ 'href',
+ icinga.utils.addUrlParams(
+ $oldLink.attr('href'),
+ { id: new Date().getTime() } // Only required for Firefox to reload CSS automatically
+ )
+ ).on('load', function() {
+ $oldLink.remove();
+ $('head').trigger('css-reloaded');
+ });
+
+ $newLink.appendTo($('head'));
+ }
+ });
+ },
+
+ enableTimeCounters: function () {
+ this.timeCounterTimer = this.icinga.timer.register(
+ this.refreshTimeSince,
+ this,
+ 1000
+ );
+ return this;
+ },
+
+ disableTimeCounters: function () {
+ this.icinga.timer.unregister(this.timeCounterTimer);
+ this.timeCounterTimer = null;
+ return this;
+ },
+
+ /**
+ * Focus the given element and scroll to its position
+ *
+ * @param {string} element The name or id of the element to focus
+ * @param {object} [$container] The container containing the element
+ * @param {boolean} [scroll] Whether the viewport should be scrolled to the focused element
+ */
+ focusElement: function(element, $container, scroll) {
+ var $element = element;
+
+ if (typeof scroll === 'undefined') {
+ scroll = true;
+ }
+
+ if (typeof element === 'string') {
+ if ($container && $container.length) {
+ $element = $container.find('#' + element);
+ } else {
+ $element = $('#' + element);
+ }
+
+ if (! $element.length) {
+ // The name attribute is actually deprecated, on anchor tags,
+ // but we'll possibly handle links from another source
+ // (module etc) so that's used as a fallback
+ if ($container && $container.length) {
+ $element = $container.find('[name="' + element.replace(/'/, '\\\'') + '"]');
+ } else {
+ $element = $('[name="' + element.replace(/'/, '\\\'') + '"]');
+ }
+ }
+ }
+
+ if ($element.length) {
+ if (! this.isFocusable($element)) {
+ $element.attr('tabindex', -1);
+ }
+
+ $element[0].focus();
+
+ if (scroll && $container && $container.length) {
+ if (! $container.is('.container')) {
+ $container = $container.closest('.container');
+ }
+
+ if ($container.css('display') === 'flex' && $container.is('.container')) {
+ var $controls = $container.find('.controls');
+ var $content = $container.find('.content');
+ $content.scrollTop($element.offsetTopRelativeTo($content) - $controls.outerHeight() - (
+ $element.outerHeight(true) - $element.innerHeight()
+ ));
+ } else {
+ $container.scrollTop($element.first().position().top);
+ }
+ }
+ }
+ },
+
+ isFocusable: function ($element) {
+ return $element.is('*[tabindex], a[href], input:not([disabled]), button:not([disabled])' +
+ ', select:not([disabled]), textarea:not([disabled]), iframe, area[href], object' +
+ ', embed, *[contenteditable]');
+ },
+
+ moveToLeft: function () {
+ var col2 = this.cutContainer($('#col2'));
+ var kill = this.cutContainer($('#col1'));
+ this.pasteContainer($('#col1'), col2);
+ this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl($('#col1').data('icingaUrl'));
+ },
+
+ moveToRight: function () {
+ let col1 = document.getElementById('col1'),
+ col2 = document.getElementById('col2'),
+ col1Backup = this.cutContainer($(col1));
+
+ this.cutContainer($(col2)); // Clear col2 states
+ this.pasteContainer($(col2), col1Backup);
+ this.layout2col();
+ },
+
+ cutContainer: function ($col) {
+ var props = {
+ 'elements': $('#' + $col.attr('id') + ' > *').detach(),
+ 'data': {
+ 'data-icinga-url': $col.data('icingaUrl'),
+ 'data-icinga-title': $col.data('icingaTitle'),
+ 'data-icinga-refresh': $col.data('icingaRefresh'),
+ 'data-last-update': $col.data('lastUpdate'),
+ 'data-icinga-module': $col.data('icingaModule'),
+ 'data-icinga-container-id': $col[0].dataset.icingaContainerId
+ },
+ 'class': $col.attr('class')
+ };
+ this.icinga.loader.stopPendingRequestsFor($col);
+ $col.removeData('icingaUrl');
+ $col.removeData('icingaTitle');
+ $col.removeData('icingaRefresh');
+ $col.removeData('lastUpdate');
+ $col.removeData('icingaModule');
+ delete $col[0].dataset.icingaContainerId;
+ $col.removeAttr('class').attr('class', 'container');
+ return props;
+ },
+
+ pasteContainer: function ($col, backup) {
+ backup['elements'].appendTo($col);
+ $col.attr('class', backup['class']); // TODO: ie memleak? remove first?
+ $col.data('icingaUrl', backup['data']['data-icinga-url']);
+ $col.data('icingaTitle', backup['data']['data-icinga-title']);
+ $col.data('icingaRefresh', backup['data']['data-icinga-refresh']);
+ $col.data('lastUpdate', backup['data']['data-last-update']);
+ $col.data('icingaModule', backup['data']['data-icinga-module']);
+ $col[0].dataset.icingaContainerId = backup['data']['data-icinga-container-id'];
+ },
+
+ triggerWindowResize: function () {
+ this.onWindowResize({data: {self: this}});
+ },
+
+ /**
+ * Our window got resized, let's fix our UI
+ */
+ onWindowResize: function (event) {
+ var _this = event.data.self;
+
+ if (_this.layoutHasBeenChanged()) {
+ _this.icinga.logger.info(
+ 'Layout change detected, switching to',
+ _this.currentLayout
+ );
+ }
+
+ _this.refreshDebug();
+ },
+
+ /**
+ * Returns whether the layout is too small for more than one column
+ *
+ * @returns {boolean} True when more than one column is available
+ */
+ hasOnlyOneColumn: function () {
+ return this.currentLayout === 'poor' || this.currentLayout === 'minimal';
+ },
+
+ layoutHasBeenChanged: function () {
+
+ var layout = $('html').css('fontFamily').replace(/['",]/g, '');
+ var matched;
+
+ if (null !== (matched = layout.match(/^([a-z]+)-layout$/))) {
+ if (matched[1] === this.currentLayout &&
+ $('#layout').hasClass(layout)
+ ) {
+ return false;
+ } else {
+ $('#layout').removeClass(this.currentLayout + '-layout').addClass(layout);
+ this.currentLayout = matched[1];
+ if (this.currentLayout === 'poor' || this.currentLayout === 'minimal') {
+ this.layout1col();
+ } else if (this.icinga.initialized) {
+ // layout1col() also triggers this, that's why an else is required
+ $('#layout').trigger('layout-change');
+ }
+ return true;
+ }
+ }
+ this.icinga.logger.error(
+ 'Someone messed up our responsiveness hacks, html font-family is',
+ layout
+ );
+ return false;
+ },
+
+ /**
+ * Returns whether only one column is displayed
+ *
+ * @returns {boolean} True when only one column is displayed
+ */
+ isOneColLayout: function () {
+ return ! $('#layout').hasClass('twocols');
+ },
+
+ layout1col: function () {
+ if (this.isOneColLayout()) { return; }
+ this.icinga.logger.debug('Switching to single col');
+ $('#layout').removeClass('twocols');
+ this.closeContainer($('#col2'));
+
+ if (this.icinga.initialized) {
+ $('#layout').trigger('layout-change');
+ }
+
+ // one-column layouts never have any selection active
+ $('#col1').removeData('icinga-actiontable-former-href');
+ this.icinga.behaviors.actiontable.clearAll();
+ },
+
+ closeContainer: function($c) {
+ $c.removeData('icingaUrl');
+ $c.removeData('icingaTitle');
+ $c.removeData('icingaRefresh');
+ $c.removeData('lastUpdate');
+ $c.removeData('icingaModule');
+ delete $c[0].dataset.icingaContainerId;
+ $c.removeAttr('class').attr('class', 'container');
+ this.icinga.loader.stopPendingRequestsFor($c);
+ $c.trigger('close-column');
+ $c.html('');
+ },
+
+ layout2col: function () {
+ if (! this.isOneColLayout()) { return; }
+ this.icinga.logger.debug('Switching to double col');
+ $('#layout').addClass('twocols');
+
+ if (this.icinga.initialized) {
+ $('#layout').trigger('layout-change');
+ }
+ },
+
+ prepareColumnFor: function ($el, $target) {
+ var explicitTarget;
+
+ if ($target.attr('id') === 'col2') {
+ if ($el.closest('#col2').length) {
+ explicitTarget = $el.closest('[data-base-target]').data('baseTarget');
+ if (typeof explicitTarget !== 'undefined' && explicitTarget === '_next') {
+ this.moveToLeft();
+ }
+ } else {
+ this.layout2col();
+ }
+ } else { // if ($target.attr('id') === 'col1')
+ explicitTarget = $el.closest('[data-base-target]').data('baseTarget');
+ if (typeof explicitTarget !== 'undefined' && explicitTarget === '_main') {
+ this.layout1col();
+ }
+ }
+ },
+
+ getAvailableColumnSpace: function () {
+ return $('#main').width() / this.getDefaultFontSize();
+ },
+
+ setColumnCount: function (count) {
+ if (count === 3) {
+ $('#main > .container').css({
+ width: '33.33333%'
+ });
+ } else if (count === 2) {
+ $('#main > .container').css({
+ width: '50%'
+ });
+ } else {
+ $('#main > .container').css({
+ width: '100%'
+ });
+ }
+ },
+
+ setTitle: function (title) {
+ document.title = title;
+ return this;
+ },
+
+ getColumnCount: function () {
+ return $('#main > .container').length;
+ },
+
+ /**
+ * Assign a unique ID to each .container without such
+ *
+ * This usually applies to dashlets
+ */
+ assignUniqueContainerIds: function() {
+ var currentMax = 0;
+ $('.container').each(function() {
+ var $el = $(this);
+ var m;
+ if (!$el.attr('id')) {
+ return;
+ }
+ if (m = $el.attr('id').match(/^ciu_(\d+)$/)) {
+ if (parseInt(m[1]) > currentMax) {
+ currentMax = parseInt(m[1]);
+ }
+ }
+ });
+ $('.container').each(function() {
+ var $el = $(this);
+ if (!!$el.attr('id')) {
+ return;
+ }
+ currentMax++;
+ $el.attr('id', 'ciu_' + currentMax);
+ });
+ },
+
+ refreshDebug: function () {
+ if (! this.debug) {
+ return;
+ }
+
+ var size = this.getDefaultFontSize().toString();
+ var winWidth = $( window ).width();
+ var winHeight = $( window ).height();
+ var loading = '';
+
+ $.each(this.icinga.loader.requests, function (el, req) {
+ if (loading === '') {
+ loading = '<br />Loading:<br />';
+ }
+ loading += el + ' => ' + encodeURI(req.url);
+ });
+
+ $('#responsive-debug').html(
+ ' Time: ' +
+ this.icinga.utils.formatHHiiss(new Date()) +
+ '<br /> 1em: ' +
+ size +
+ 'px<br /> Win: ' +
+ winWidth +
+ 'x'+
+ winHeight +
+ 'px<br />' +
+ ' Layout: ' +
+ this.currentLayout +
+ loading
+ );
+ },
+
+ /**
+ * Refresh partial time counters
+ *
+ * This function runs every second.
+ */
+ refreshTimeSince: function () {
+ $('.time-ago, .time-since').each(function (idx, el) {
+ var partialTime = /(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML);
+ if (partialTime !== null) {
+ var minute = parseInt(partialTime[1], 10),
+ second = parseInt(partialTime[2], 10);
+ if (second < 59) {
+ ++second;
+ } else {
+ ++minute;
+ second = 0;
+ }
+ el.innerHTML = el.innerHTML.substr(0, partialTime.index) + minute.toString() + 'm '
+ + second.toString() + 's' + el.innerHTML.substr(partialTime.index + partialTime[0].length);
+ }
+ });
+
+ $('.time-until').each(function (idx, el) {
+ var partialTime = /(-?)(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML);
+ if (partialTime !== null) {
+ var minute = parseInt(partialTime[2], 10),
+ second = parseInt(partialTime[3], 10),
+ invert = partialTime[1];
+ if (invert.length) {
+ // Count up because partial time is negative
+ if (second < 59) {
+ ++second;
+ } else {
+ ++minute;
+ second = 0;
+ }
+ } else {
+ // Count down because partial time is positive
+ if (second === 0) {
+ if (minute === 0) {
+ // Invert counter
+ minute = 0;
+ second = 1;
+ invert = '-';
+ } else {
+ --minute;
+ second = 59;
+ }
+ } else {
+ --second;
+ }
+ }
+ el.innerHTML = el.innerHTML.substr(0, partialTime.index) + invert + minute.toString() + 'm '
+ + second.toString() + 's' + el.innerHTML.substr(partialTime.index + partialTime[0].length);
+ }
+ });
+ },
+
+ createFontSizeCalculator: function () {
+ var $el = $('<div id="fontsize-calc">&nbsp;</div>');
+ $('#layout').append($el);
+ return $el;
+ },
+
+ getDefaultFontSize: function () {
+ var $calc = $('#fontsize-calc');
+ if (! $calc.length) {
+ $calc = this.createFontSizeCalculator();
+ }
+ return $calc.width() / 1000;
+ },
+
+ /**
+ * Toggle mobile menu
+ *
+ * @param {object} e Event
+ */
+ toggleMobileMenu: function(e) {
+ $('#sidebar').toggleClass('expanded');
+ },
+
+ /**
+ * Close mobile menu when the enter key is pressed during search or the user leaves the sidebar
+ *
+ * @param {object} e Event
+ */
+ closeMobileMenu: function(e) {
+ if (e.data.self.currentLayout !== 'minimal') {
+ return;
+ }
+
+ if (e.data.type === 'key') {
+ if (e.which === 13) {
+ $('#sidebar').removeClass('expanded');
+ $(e.target)[0].blur();
+ }
+ } else {
+ $('#sidebar').removeClass('expanded');
+ }
+ },
+
+ toggleFullscreen: function () {
+ $('#layout').toggleClass('fullscreen-layout');
+ },
+
+ getUniqueContainerId: function (container) {
+ if (typeof container.jquery !== 'undefined') {
+ if (! container.length) {
+ return null;
+ }
+
+ container = container[0];
+ } else if (typeof container === 'undefined') {
+ return null;
+ }
+
+ var containerId = container.dataset.icingaContainerId || null;
+ if (containerId === null) {
+ /**
+ * Only generate an id if it's not for col1 or the menu (which are using the non-suffixed window id).
+ * This is based on the assumption that the server only knows about the menu and first column
+ * and therefore does not need to protect its ids. (As the menu is most likely part of the sidebar)
+ */
+ var col1 = document.getElementById('col1');
+ if (container.id !== 'menu' && col1 !== null && ! col1.contains(container)) {
+ containerId = this.icinga.utils.generateId(6); // Random because the content may move
+ container.dataset.icingaContainerId = containerId;
+ }
+ }
+
+ return containerId;
+ },
+
+ getWindowId: function () {
+ if (! this.hasWindowId()) {
+ return undefined;
+ }
+ return window.name.match(/^Icinga-([a-zA-Z0-9]+)$/)[1];
+ },
+
+ hasWindowId: function () {
+ var res = window.name.match(/^Icinga-([a-zA-Z0-9]+)$/);
+ return typeof res === 'object' && null !== res;
+ },
+
+ setWindowId: function (id) {
+ this.icinga.logger.debug('Setting new window id', id);
+ window.name = 'Icinga-' + id;
+ },
+
+ destroy: function () {
+ // This is gonna be hard, clean up the mess
+ this.icinga = null;
+ this.debugTimer = null;
+ this.timeCounterTimer = null;
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/utils.js b/public/js/icinga/utils.js
new file mode 100644
index 0000000..280e4f6
--- /dev/null
+++ b/public/js/icinga/utils.js
@@ -0,0 +1,582 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga utility functions
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Utils = function (icinga) {
+
+ /**
+ * Utility functions may need access to their Icinga instance
+ */
+ this.icinga = icinga;
+
+ /**
+ * We will use this to create an URL helper only once
+ */
+ this.urlHelper = null;
+ };
+
+ Icinga.Utils.prototype = {
+
+ timeWithMs: function (now) {
+
+ if (typeof now === 'undefined') {
+ now = new Date();
+ }
+
+ var ms = now.getMilliseconds() + '';
+ while (ms.length < 3) {
+ ms = '0' + ms;
+ }
+
+ return now.toLocaleTimeString() + '.' + ms;
+ },
+
+ timeShort: function (now) {
+
+ if (typeof now === 'undefined') {
+ now = new Date();
+ }
+
+ return now.toLocaleTimeString().replace(/:\d{2}$/, '');
+ },
+
+ formatHHiiss: function (date) {
+ var hours = date.getHours();
+ var minutes = date.getMinutes();
+ var seconds = date.getSeconds();
+ if (hours < 10) hours = '0' + hours;
+ if (minutes < 10) minutes = '0' + minutes;
+ if (seconds < 10) seconds = '0' + seconds;
+ return hours + ':' + minutes + ':' + seconds;
+ },
+
+ /**
+ * Format the given byte-value into a human-readable string
+ *
+ * @param {number} The amount of bytes to format
+ * @returns {string} The formatted string
+ */
+ formatBytes: function (bytes) {
+ var log2 = Math.log(bytes) / Math.LN2;
+ var pot = Math.floor(log2 / 10);
+ var unit = (['b', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB'])[pot];
+ return ((bytes / Math.pow(1024, pot)).toFixed(2)) + ' ' + unit;
+ },
+
+ /**
+ * Return whether the given element is visible in the users view
+ *
+ * Borrowed from: http://stackoverflow.com/q/487073
+ *
+ * @param {selector} element The element to check
+ * @returns {Boolean}
+ */
+ isVisible: function(element) {
+ var $element = $(element);
+ if (!$element.length) {
+ return false;
+ }
+
+ var docViewTop = $(window).scrollTop();
+ var docViewBottom = docViewTop + $(window).height();
+ var elemTop = $element.offset().top;
+ var elemBottom = elemTop + $element.height();
+
+ return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) &&
+ (elemBottom <= docViewBottom) && (elemTop >= docViewTop));
+ },
+
+ getUrlHelper: function () {
+ if (this.urlHelper === null) {
+ this.urlHelper = document.createElement('a');
+ }
+
+ return this.urlHelper;
+ },
+
+ /**
+ * Parse a given Url and return an object
+ */
+ parseUrl: function (url) {
+
+ var a = this.getUrlHelper();
+ a.href = url;
+
+ var result = {
+ source : url,
+ protocol: a.protocol.replace(':', ''),
+ host : a.hostname,
+ port : a.port,
+ query : a.search,
+ file : (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1],
+ hash : a.hash.replace('#',''),
+ path : a.pathname.replace(/^([^\/])/,'/$1'),
+ relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1],
+ segments: a.pathname.replace(/^\//,'').split('/'),
+ params : this.parseParams(a)
+ };
+ a = null;
+
+ return result;
+ },
+
+ // Local URLs only
+ addUrlParams: function (url, params) {
+ var parts = this.parseUrl(url),
+ result = parts.path,
+ newparams = parts.params;
+
+ // We overwrite existing params
+ $.each(params, function (key, value) {
+ key = encodeURIComponent(key);
+ value = typeof value !== 'string' || !! value ? encodeURIComponent(value) : null;
+
+ var found = false;
+ for (var i = 0; i < newparams.length; i++) {
+ if (newparams[i].key === key) {
+ newparams[i].value = value;
+ found = true;
+ break;
+ }
+ }
+
+ if (! found) {
+ newparams.push({ key: key, value: value });
+ }
+ });
+
+ if (newparams.length) {
+ result += '?' + this.buildQuery(newparams);
+ }
+
+ if (parts.hash.length) {
+ result += '#' + parts.hash;
+ }
+
+ return result;
+ },
+
+ // Local URLs only
+ removeUrlParams: function (url, params) {
+ var parts = this.parseUrl(url),
+ result = parts.path,
+ newparams = parts.params;
+
+ $.each(params, function (_, key) {
+ key = encodeURIComponent(key);
+
+ for (var i = 0; i < newparams.length; i++) {
+ if (newparams[i].key === key) {
+ newparams.splice(i, 1);
+ return;
+ }
+ }
+ });
+
+ if (newparams.length) {
+ result += '?' + this.buildQuery(newparams);
+ }
+
+ if (parts.hash.length) {
+ result += '#' + parts.hash;
+ }
+
+ return result;
+ },
+
+ /**
+ * Return a query string for the given params
+ *
+ * @param {Array} params
+ * @return {string}
+ */
+ buildQuery: function (params) {
+ var query = '';
+
+ for (var i = 0; i < params.length; i++) {
+ if (!! query) {
+ query += '&';
+ }
+
+ query += params[i].key;
+ switch (params[i].value) {
+ case true:
+ break;
+ case false:
+ query += '=0';
+ break;
+ case null:
+ query += '=';
+ break;
+ default:
+ query += '=' + params[i].value;
+ }
+ }
+
+ return query;
+ },
+
+ /**
+ * Parse url params
+ */
+ parseParams: function (a) {
+ var params = [],
+ segment = a.search.replace(/^\?/,'').split('&'),
+ len = segment.length,
+ i = 0,
+ key,
+ value,
+ equalPos;
+
+ for (; i < len; i++) {
+ if (! segment[i]) {
+ continue;
+ }
+
+ equalPos = segment[i].indexOf('=');
+ if (equalPos !== -1) {
+ key = segment[i].slice(0, equalPos);
+ value = segment[i].slice(equalPos + 1);
+ } else {
+ key = segment[i];
+ value = true;
+ }
+
+ params.push({ key: key, value: value });
+ }
+
+ return params;
+ },
+
+ /**
+ * Add the specified flag to the given URL
+ *
+ * @param {string} url
+ * @param {string} flag
+ *
+ * @returns {string}
+ */
+ addUrlFlag: function (url, flag) {
+ var pos = url.search(/#(?!!)/);
+
+ if (url.indexOf('?') !== -1) {
+ flag = '&' + flag;
+ } else {
+ flag = '?' + flag;
+ }
+
+ if (pos === -1) {
+ return url + flag;
+ }
+
+ return url.slice(0, pos) + flag + url.slice(pos);
+ },
+
+ /**
+ * Check whether two HTMLElements overlap
+ *
+ * @param a {HTMLElement}
+ * @param b {HTMLElement}
+ *
+ * @returns {Boolean} whether elements overlap, will return false when one
+ * element is not in the DOM
+ */
+ elementsOverlap: function(a, b)
+ {
+ // a bounds
+ var aoff = $(a).offset();
+ if (!aoff) {
+ return false;
+ }
+ var at = aoff.top;
+ var ah = a.offsetHeight || (a.getBBox && a.getBBox().height);
+ var al = aoff.left;
+ var aw = a.offsetWidth || (a.getBBox && a.getBBox().width);
+
+ // b bounds
+ var boff = $(b).offset();
+ if (!boff) {
+ return false;
+ }
+ var bt = boff.top;
+ var bh = b.offsetHeight || (b.getBBox && b.getBBox().height);
+ var bl = boff.left;
+ var bw = b.offsetWidth || (b.getBBox && b.getBBox().width);
+
+ return !(at > (bt + bh) || bt > (at + ah)) && !(bl > (al + aw) || al > (bl + bw));
+ },
+
+ /**
+ * Create a selector that can be used to fetch the element the same position in the DOM-Tree
+ *
+ * Create the path to the given element in the DOM-Tree, comparable to an X-Path. Climb the
+ * DOM tree upwards until an element with an unique ID is found, this id is used as the anchor,
+ * all other elements will be addressed by their position in the parent.
+ *
+ * @param {HTMLElement} el The element to extract the path for.
+ *
+ * @returns {Array} The path of the element, that can be passed to getElementByPath
+ */
+ getDomPath: function (el) {
+ if (! el) {
+ return [];
+ }
+ if (el.id !== '') {
+ return ['#' + el.id];
+ }
+ if (el === document.body) {
+ return ['body'];
+ }
+
+ var siblings = el.parentNode.childNodes;
+ var index = 0;
+ for (var i = 0; i < siblings.length; i ++) {
+ if (siblings[i].nodeType === 1) {
+ index ++;
+ }
+
+ if (siblings[i] === el) {
+ var p = this.getDomPath(el.parentNode);
+ p.push(':nth-child(' + (index) + ')');
+ return p;
+ }
+ }
+ },
+
+ /**
+ * Get the CSS selector to the given node
+ *
+ * @param {HTMLElement} element
+ *
+ * @returns {string}
+ */
+ getCSSPath: function(element) {
+ if (typeof element === 'undefined') {
+ throw 'Requires a element';
+ }
+
+ if (typeof element.jquery !== 'undefined') {
+ if (! element.length) {
+ throw 'Requires a element';
+ }
+
+ element = element[0];
+ }
+
+ var path = [];
+
+ while (true) {
+ let id = element.id;
+ if (typeof id !== 'undefined' && typeof id !== 'string') {
+ // Sometimes there may be a form element with the name "id"
+ id = element.getAttribute("id");
+ }
+
+ if (!! id) {
+ // Only use ids if they're truly unique
+ let results = document.querySelectorAll('* #' + this.escapeCSSSelector(id));
+ if (results.length === 1) {
+ path.push('#' + id);
+ break;
+ }
+ }
+
+ var tagName = element.tagName;
+ var parent = element.parentElement;
+
+ if (! parent) {
+ path.push(tagName.toLowerCase());
+ break;
+ }
+
+ if (parent.children.length) {
+ var index = 0;
+ do {
+ if (element.tagName === tagName) {
+ index++;
+ }
+ } while ((element = element.previousElementSibling));
+
+ path.push(tagName.toLowerCase() + ':nth-of-type(' + index + ')');
+ } else {
+ path.push(tagName.toLowerCase());
+ }
+
+ element = parent;
+ }
+
+ return path.reverse().join(' > ');
+ },
+
+ /**
+ * Escape the given string to be used in a CSS selector
+ *
+ * @param {string} selector
+ * @returns {string}
+ */
+ escapeCSSSelector: function (selector) {
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
+ return CSS.escape(selector);
+ }
+
+ return selector.replaceAll(/^(\d)/, '\\\\3$1 ');
+ },
+
+ /**
+ * Climbs up the given dom path and returns the element
+ *
+ * This is the counterpart
+ *
+ * @param path {Array} The selector
+ * @returns {HTMLElement} The corresponding element
+ */
+ getElementByDomPath: function (path) {
+ var $element;
+ $.each(path, function (i, selector) {
+ if (! $element) {
+ $element = $(selector);
+ } else {
+ $element = $element.children(selector).first();
+ if (! $element[0]) {
+ return false;
+ }
+ }
+ });
+ return $element[0];
+ },
+
+ objectKeys: Object.keys || function (obj) {
+ var keys = [];
+ $.each(obj, function (key) {
+ keys.push(key);
+ });
+ return keys;
+ },
+
+ objectsEqual: function equals(obj1, obj2) {
+ var obj1Keys = Object.keys(obj1);
+ var obj2Keys = Object.keys(obj2);
+ if (obj1Keys.length !== obj2Keys.length) {
+ return false;
+ }
+
+ return obj1Keys.concat(obj2Keys)
+ .every(function (key) {
+ return obj1[key] === obj2[key];
+ });
+ },
+
+ arraysEqual: function (array1, array2) {
+ if (array1.length !== array2.length) {
+ return false;
+ }
+
+ var value1, value2;
+ for (var i = 0; i < array1.length; i++) {
+ value1 = array1[i];
+ value2 = array2[i];
+
+ if (typeof value1 === 'object') {
+ if (typeof value2 !== 'object' || ! this.objectsEqual(value1, value2)) {
+ return false;
+ }
+ } else if (value1 !== value2) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Cleanup
+ */
+ destroy: function () {
+ this.urlHelper = null;
+ this.icinga = null;
+ },
+
+ /**
+ * Encode the parenthesis too
+ *
+ * @param str {String} A component of a URI
+ *
+ * @returns {String} Encoded component
+ */
+ fixedEncodeURIComponent: function (str) {
+ return encodeURIComponent(str).replace(/[()]/g, function(c) {
+ return '%' + c.charCodeAt(0).toString(16);
+ });
+ },
+
+ escape: function (str) {
+ return String(str).replace(
+ /[&<>"']/gm,
+ function (c) {
+ return {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ "'": '&#039;'
+ }[c];
+ }
+ );
+ },
+
+ /**
+ * Pad a string with another one
+ *
+ * @param {String} str the string to pad
+ * @param {String} padding the string to use for padding
+ * @param {Number} minLength the minimum length of the result
+ *
+ * @returns {String} the padded string
+ */
+ padString: function(str, padding, minLength) {
+ str = String(str);
+ padding = String(padding);
+ while (str.length < minLength) {
+ str = padding + str;
+ }
+ return str;
+ },
+
+ /**
+ * Shuffle a string
+ *
+ * @param {String} str The string to shuffle
+ *
+ * @returns {String} The shuffled string
+ */
+ shuffleString: function(str) {
+ var a = str.split(""),
+ n = a.length;
+
+ for(var i = n - 1; i > 0; i--) {
+ var j = Math.floor(Math.random() * (i + 1));
+ var tmp = a[i];
+ a[i] = a[j];
+ a[j] = tmp;
+ }
+ return a.join("");
+ },
+
+ /**
+ * Generate an id
+ *
+ * @param {Number} len The desired length of the id
+ *
+ * @returns {String} The id
+ */
+ generateId: function(len) {
+ return this.shuffleString('abcefghijklmnopqrstuvwxyz').substr(0, len);
+ }
+ };
+
+}(Icinga, jQuery));