summaryrefslogtreecommitdiffstats
path: root/public
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:44:46 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:44:46 +0000
commitb18bc644404e02b57635bfcc8258e85abb141146 (patch)
tree686512eacb2dba0055277ef7ec2f28695b3418ea /public
parentInitial commit. (diff)
downloadicingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.tar.xz
icingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.zip
Adding upstream version 1.1.1.upstream/1.1.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'public')
-rw-r--r--public/css/common.less405
-rw-r--r--public/css/form/schedule-service-downtime-form.less21
-rw-r--r--public/css/list/action-list.less14
-rw-r--r--public/css/list/comment-list.less50
-rw-r--r--public/css/list/downtime-list.less93
-rw-r--r--public/css/list/item-list.less154
-rw-r--r--public/css/list/list-item.less77
-rw-r--r--public/css/list/state-item-table.less201
-rw-r--r--public/css/list/state-row-item.less42
-rw-r--r--public/css/list/user-list.less26
-rw-r--r--public/css/markdown.less80
-rw-r--r--public/css/mixin/progress-bar.less217
-rw-r--r--public/css/mixin/state-badges.less31
-rw-r--r--public/css/mixins.less5
-rw-r--r--public/css/view/service-grid.less60
-rw-r--r--public/css/widget/actions.less20
-rw-r--r--public/css/widget/check-attempt.less17
-rw-r--r--public/css/widget/check-statistics.less192
-rw-r--r--public/css/widget/comment-popup.less74
-rw-r--r--public/css/widget/custom-var-table.less60
-rw-r--r--public/css/widget/donut-container.less24
-rw-r--r--public/css/widget/downtime-card.less10
-rw-r--r--public/css/widget/group-grid.less42
-rw-r--r--public/css/widget/host-state-badges.less3
-rw-r--r--public/css/widget/key-value-list.less19
-rw-r--r--public/css/widget/migrate-popup.less181
-rw-r--r--public/css/widget/monitoring-health.less136
-rw-r--r--public/css/widget/notice.less23
-rw-r--r--public/css/widget/object-features.less53
-rw-r--r--public/css/widget/object-inspection.less17
-rw-r--r--public/css/widget/object-meta-info.less95
-rw-r--r--public/css/widget/object-statistics.less44
-rw-r--r--public/css/widget/performance-data-table.less62
-rw-r--r--public/css/widget/quick-actions.less47
-rw-r--r--public/css/widget/service-state-badges.less3
-rw-r--r--public/css/widget/state-change.less128
-rw-r--r--public/css/widget/table-layout.less72
-rw-r--r--public/css/widget/tag-list.less31
-rw-r--r--public/css/widget/view-mode-switcher.less45
-rw-r--r--public/js/action-list.js788
-rw-r--r--public/js/migrate.js654
-rw-r--r--public/js/progress-bar.js110
42 files changed, 4426 insertions, 0 deletions
diff --git a/public/css/common.less b/public/css/common.less
new file mode 100644
index 0000000..4cf5cfc
--- /dev/null
+++ b/public/css/common.less
@@ -0,0 +1,405 @@
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+@exports: {
+ @iplWebAssets: "../lib/icinga/icinga-php-library";
+};
+
+& > .content.full-width {
+ padding-left: 0;
+ padding-right: 0;
+
+ .list-item,
+ .table-layout .table-row {
+ padding-left: 1em;
+ padding-right: 1em;
+ }
+}
+
+& > .content.full-height {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+.plugin-output {
+ .monospace();
+ word-break: break-word;
+}
+
+div.show-more {
+ .clearfix();
+ float: right;
+}
+
+.state-ball.state-not-available {
+ .ball-solid(@gray-light);
+ .animate(pulse 1.5s infinite both);
+}
+
+.icon-ball {
+ .ball();
+ .ball-outline(@gray);
+ text-align: center;
+
+ i:before {
+ margin-right: 0;
+ }
+}
+
+.user-ball {
+ .ball();
+ .ball-size-xl();
+ .ball-solid(@gray-semilight);
+ font-weight: bold;
+ line-height: 1.75;
+ text-transform: uppercase;
+}
+
+.usergroup-ball {
+ .ball();
+ .ball-outline(@gray);
+ .ball-size-xl();
+ line-height: 1.75;
+ text-transform: uppercase;
+}
+
+.ack-badge {
+ text-transform: uppercase;
+ line-height: 1;
+ margin-top: -.25em;
+ align-self: flex-start;
+
+ i {
+ vertical-align: baseline;
+ }
+}
+
+.controls {
+ .box-shadow(0, 0, 0, 1px, @gray-lighter);
+ flex-shrink: 0;
+ position: relative; // Required for the host meta info control
+ z-index: 1; // The content may clip, this ensures the separator is always visible
+
+ > :not(:only-child) {
+ margin-bottom: .5em;
+ }
+
+ #object-meta-info {
+ margin-bottom: 0;
+
+ .object-meta-info {
+ margin-bottom: .5em;
+ }
+ }
+
+ &.overdue,
+ &.overdue .tabs li.active a,
+ &.overdue .object-meta-info-control {
+ background-color: @gray-lighter;
+ }
+
+ .limit-control,
+ .view-mode-switcher,
+ .sort-control {
+ margin-left: .5em;
+ float: right;
+ }
+
+ .toggle-switch {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ .item-list {
+ width: 100%;
+
+ .list-item .main {
+ border-top: none;
+ }
+
+ .list-item .visual {
+ width: auto;
+ margin-top: 0;
+ }
+
+ .list-item .visual,
+ .list-item .main {
+ padding-bottom: .25em;
+ padding-top: .25em;
+ }
+ }
+
+ .search-controls .continue-with {
+ margin-right: -.5em;
+ margin-left: .5em;
+ }
+
+ .show-more {
+ margin-top: .25em;
+ }
+
+ .notice {
+ display: none;
+ }
+
+ // TODO: Remove once ipl-web v0.7.0 is required
+ &:not(.default-layout) {
+ .pagination-control {
+ float: left;
+ }
+
+ .sort-control {
+ display: flex;
+ justify-content: flex-end;
+
+ :not(.form-element) > label {
+ margin-right: 0;
+ }
+
+ .control-button {
+ margin: 0;
+ }
+ }
+
+ > :not(:only-child) {
+ margin-bottom: 0.5em;
+ }
+
+ .search-suggestions {
+ margin-bottom: 2.5em;
+ }
+
+ .search-controls {
+ clear: both;
+ display: flex;
+ min-width: 100%;
+
+ .search-bar {
+ flex: 1 1 auto;
+
+ & ~ .control-button:last-child {
+ margin-right: -.5em;
+ }
+
+ & ~ .control-button {
+ margin-left: .5em;
+ }
+ }
+ }
+ }
+}
+
+.content > h2:first-child,
+.object-detail > h2:first-child {
+ margin-top: 0;
+}
+
+.content.full-width > h2 {
+ margin-left: 1em / 1.333em; // 1em / h2 font size
+}
+
+.object-detail .plugin-output {
+ .rounded-corners(.25em);
+ background-color: @gray-lighter;
+ padding: .5em;
+}
+
+.object-detail .item-list {
+ &.action-list .list-item {
+ margin-right: -1em;
+ margin-left: -1em;
+ padding-right: 1em;
+ padding-left: 1em;
+ }
+
+ .list-item:last-of-type .caption {
+ min-height: 1.5em;
+ max-height: 3em;
+ height: auto;
+ }
+}
+
+.perfdata-wrapper {
+ svg {
+ width: 100%;
+ }
+
+ svg:not(:last-child) {
+ margin-bottom: 1em;
+ }
+}
+
+.text-center {
+ text-align: center;
+}
+
+.text-muted {
+ color: @gray;
+}
+
+.accompanying-text {
+ color: @text-color-light;
+
+ .subject {
+ color: @text-color;
+ .user-select(all);
+ }
+}
+
+.comment-detail {
+ > form,
+ > h2:not(:first-child) {
+ margin-top: 1em;
+ }
+}
+
+.downtime-detail {
+ .downtime-progress {
+ margin-bottom: 1em;
+ }
+}
+
+.footer {
+ display: flex;
+ .box-shadow(0, -1px, 0, 0, @gray-lighter);
+ color: @text-color-light;
+
+ .selection-count {
+ flex: 1 1 auto;
+
+ .selected-items {
+ font-size: 1.25em;
+
+ &.hint {
+ opacity: 0.75;
+ }
+ }
+ }
+
+ .status-bar, .selection-count {
+ font-size: .857em;
+ padding: .25em 1em;
+ line-height: 1.7;
+ }
+}
+
+.status-bar {
+ margin-left: auto;
+
+ .item-count {
+ font-size: 1.25em;
+ }
+
+ .state-badges {
+ display: inline-block;
+ margin: 0 0 0 .417em;
+ }
+}
+
+.multiselect-summary {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+
+ // Donut
+ > div:first-child {
+ height: 4em;
+ width: 4em;
+
+ > svg {
+ height: auto;
+ max-width: 100%;
+ }
+ }
+
+ > .vertical-key-value {
+ padding: 0 .5em;
+ }
+}
+
+.item-table {
+ &.table-layout {
+ &.hostgroup-table {
+ --columns: 2;
+ }
+
+ &.servicegroup-table {
+ --columns: 1;
+ }
+
+ &.user-table, // TODO: make them lists.....
+ &.usergroup-table {
+ --columns: 0;
+ }
+ }
+}
+
+.hostgroup-table,
+.servicegroup-table,
+.usergroup-table,
+.user-table {
+ .title .content > * {
+ display: block;
+ max-width: fit-content;
+ }
+
+ .object-statistics-total {
+ width: 3.75em;
+ }
+}
+
+.controls .hostgroup-table-row,
+.controls .servicegroup-table-row,
+.controls .usergroup-table-row,
+.controls .user-table-row {
+ .title .content {
+ display: inline-flex;
+ align-items: center;
+
+ > :first-child {
+ flex: 0 1 auto;
+ }
+
+ > :last-child {
+ flex: 1 1 auto;
+ }
+
+ .subject {
+ margin-right: .5em;
+ }
+ }
+
+ .vertical-key-value {
+ br {
+ display: none;
+ }
+
+ .key {
+ padding-left: .417em;
+ }
+
+ .value {
+ vertical-align: middle;
+ }
+ }
+}
+
+.history-list,
+.objectHeader {
+ .visual.small-state-change .state-change {
+ padding-top: .25em;
+ }
+}
+
+.comment-popup {
+ .comment-list .main {
+ // This is necessary to limit the visible comment lines
+ // because the popup is shown in detailed list mode only
+ .caption {
+ height: 3em;
+ }
+ }
+}
+
+form[name="form_confirm_removal"] {
+ text-align: center;
+}
diff --git a/public/css/form/schedule-service-downtime-form.less b/public/css/form/schedule-service-downtime-form.less
new file mode 100644
index 0000000..a65264d
--- /dev/null
+++ b/public/css/form/schedule-service-downtime-form.less
@@ -0,0 +1,21 @@
+.downtime-duration {
+ > label {
+ display: flex;
+ flex: 1 1 auto;
+ flex-flow: row-reverse;
+
+ input {
+ flex: 1 1 auto;
+ }
+
+ span {
+ margin-left: .5em;
+ padding: .5625em 0;
+ line-height: 1.1em;
+ }
+
+ &:not(:last-child) {
+ margin-right: 1.5em;
+ }
+ }
+}
diff --git a/public/css/list/action-list.less b/public/css/list/action-list.less
new file mode 100644
index 0000000..5bb08f4
--- /dev/null
+++ b/public/css/list/action-list.less
@@ -0,0 +1,14 @@
+.action-list {
+ [data-action-item]:hover {
+ background-color: @tr-hover-color;
+ cursor: pointer;
+ }
+
+ [data-action-item].active {
+ background-color: @tr-active-color;
+ }
+
+ &[data-icinga-multiselect-url] * {
+ user-select: none;
+ }
+}
diff --git a/public/css/list/comment-list.less b/public/css/list/comment-list.less
new file mode 100644
index 0000000..46b194d
--- /dev/null
+++ b/public/css/list/comment-list.less
@@ -0,0 +1,50 @@
+// Style
+
+// Layout
+
+.comment-list:not(.detailed) .list-item {
+ .title > i:first-child {
+ margin-right: 0;
+ }
+
+ .title > .subject + .badge,
+ .title > .badge + .subject,
+ .title > .badge:last-of-type {
+ margin-left: 0;
+ }
+
+ .title a {
+ &:not(.subject) {
+ .text-ellipsis();
+ }
+ }
+
+ .title .subject:not(:last-child) {
+ margin-left: 0;
+ }
+
+ .title .subject:nth-child(3):last-child {
+ margin-left: 0;
+ }
+}
+
+.comment-list.minimal .list-item {
+ .user-ball {
+ font-size: .857em;
+ height: 1.75em;
+ line-height: 1.5em;
+ width: 1.75em;
+ }
+}
+
+.comment-list.detailed .list-item {
+ .title > .subject:nth-child(3),
+ .title > .badge + .subject:last-child {
+ margin-left: .3em;
+ }
+
+ .caption {
+ max-height: 4.5em;
+ white-space: normal;
+ }
+}
diff --git a/public/css/list/downtime-list.less b/public/css/list/downtime-list.less
new file mode 100644
index 0000000..8768097
--- /dev/null
+++ b/public/css/list/downtime-list.less
@@ -0,0 +1,93 @@
+// Style
+
+.downtime-list .list-item,
+.downtime-detail .list-item {
+ .progress {
+ > .bar {
+ background-color: @color-ok;
+ }
+ }
+
+ .visual {
+ background-color: @gray-lighter;
+ }
+
+ .main {
+ border-top: 1px solid @gray-light;
+ }
+
+ &:first-child .main {
+ border-top: none;
+
+ .progress > .bar {
+ border-top: 1px solid @gray-light;
+ }
+ }
+
+ &.in-effect {
+ .visual {
+ background-color: @color-ok;
+ color: @text-color-on-icinga-blue;
+ }
+
+ .main {
+ padding-top: 0; // If active the progress bar represents the padding top
+ }
+ }
+}
+
+// Layout
+
+.downtime-list .list-item {
+ .caption > * {
+ display: inline;
+ }
+}
+
+.downtime-list .list-item,
+.downtime-detail .list-item {
+ .progress {
+ height: 2px;
+ margin-bottom: ~"calc(.5em - 2px)";
+ min-width: 100%;
+ position: relative;
+
+ > .bar {
+ height: 100%;
+ max-width: 100%;
+ }
+ }
+
+ &:first-child .main .progress > .bar {
+ height: ~"calc(100% + 1px)"; // +1px due to the border added exclusively for the first item
+ }
+
+ .visual {
+ justify-content: center;
+ flex-shrink: 0;
+ line-height: 1em;
+ margin-right: .5em;
+ padding: .5em .25em;
+ text-align: center;
+ width: 6em;
+
+ strong {
+ font-size: 1.5em;
+ line-height: 1em;
+ }
+ }
+}
+
+.item-list.downtime-list.minimal .list-item {
+ .visual {
+ display: block;
+ line-height: 1.5;
+ width: 8em;
+ white-space: nowrap;
+
+ strong {
+ display: inline-block;
+ font-size: 1em;
+ }
+ }
+}
diff --git a/public/css/list/item-list.less b/public/css/list/item-list.less
new file mode 100644
index 0000000..251eec3
--- /dev/null
+++ b/public/css/list/item-list.less
@@ -0,0 +1,154 @@
+// Style
+
+.item-list {
+ .load-more:hover,
+ .page-separator:hover {
+ background: none;
+ }
+
+ > .load-more a {
+ .rounded-corners(.25em);
+ background: @low-sat-blue;
+ text-align: center;
+
+ &:hover {
+ opacity: .8;
+ text-decoration: none;
+ }
+ }
+
+ > .page-separator:after {
+ content: "";
+ display: block;
+ width: 100%;
+ height: 1px;
+ background: @gray;
+ align-self: center;
+ margin-left: .25em;
+ }
+
+ > .page-separator a {
+ color: @gray;
+ font-weight: bold;
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+
+ > .page-separator + .list-item .main {
+ border-top: none;
+ }
+}
+
+// Layout
+
+.item-list .list-item {
+ &.load-more a {
+ flex: 1;
+ margin: 1.5em 0;
+ padding: .5em 0;
+ }
+}
+
+.item-list.minimal {
+ > .empty-state {
+ padding: .25em;
+ }
+
+ .list-item {
+ header {
+ max-width: 100%;
+ }
+
+ .visual {
+ width: 2.2em;
+ }
+
+ .check-attempt {
+ display: none;
+ }
+
+ .title {
+ p {
+ display: inline;
+
+ & + p {
+ margin-left: .417em;
+ }
+ }
+ }
+
+ .caption {
+ flex: 1 1 auto;
+ height: 1.5em;
+ margin-right: 1em;
+ width: 0;
+
+ .line-clamp("reset");
+ }
+
+ .caption,
+ .caption .plugin-output {
+ .text-ellipsis();
+ }
+ }
+}
+
+.item-list.detailed .list-item {
+ .title {
+ word-break: break-word;
+ -webkit-hyphens: auto;
+ -ms-hyphens: auto;
+ hyphens: auto;
+ }
+
+ .caption {
+ display: block;
+ height: auto;
+ max-height: 7.5em; /* 5 lines */
+ position: relative;
+
+ .line-clamp(4)
+ }
+}
+
+.item-list {
+ .icon-image {
+ width: 3em;
+ height: 3em;
+ text-align: center;
+ margin-top: .5em;
+ margin-left: .5em;
+ overflow: hidden;
+
+ img {
+ max-height: 100%;
+ max-width: 100%;
+ height: auto;
+ width: auto;
+ }
+ }
+
+ &.minimal {
+ .icon-image {
+ height: 2em;
+ width: 2em;
+ line-height: 2;
+ }
+ }
+}
+
+.controls .item-list:not(.detailed):not(.minimal) .list-item {
+ .plugin-output {
+ line-height: 1.5
+ }
+
+ .caption {
+ height: 2.5em;
+ }
+}
+
+.controls .item-list.minimal .icon-image {
+ margin-top: 0;
+}
diff --git a/public/css/list/list-item.less b/public/css/list/list-item.less
new file mode 100644
index 0000000..2e67e3d
--- /dev/null
+++ b/public/css/list/list-item.less
@@ -0,0 +1,77 @@
+// Style
+
+.list-item {
+ &.overdue {
+ background-color: @gray-lighter;
+ }
+
+ &.overdue header > *:not(time),
+ &.overdue .caption {
+ opacity: 0.6;
+ }
+
+ &.overdue time {
+ .rounded-corners();
+ background-color: @color-critical;
+ color: @text-color-on-icinga-blue;
+ }
+
+ .title {
+ .state-text {
+ color: @text-color;
+ text-transform: uppercase;
+ }
+ }
+
+ footer {
+ .status-icons {
+ color: @text-color-light;
+
+ .icon {
+ opacity: .5;
+ }
+ }
+ }
+}
+
+// Layout
+
+.list-item {
+ &.overdue time {
+ margin-right: -.5em;
+ padding: 0 0.5em;
+ }
+
+ .visual .check-attempt {
+ margin-top: .5em;
+ }
+
+ .caption {
+ &.plugin-output, .plugin-output {
+ font-size: 11/12em;
+ line-height: 1.5*12/11em;
+ }
+ }
+
+ footer {
+ .status-icons {
+ display: flex;
+ align-items: center;
+ }
+
+ .performance-data {
+ margin-left: auto;
+
+ .inline-pie {
+ display: inline-block;
+ line-height: 1.5*.857em;
+ height: 1em;
+ width: 1em;
+
+ &:not(:last-child) {
+ margin-right: .209em;
+ }
+ }
+ }
+ }
+}
diff --git a/public/css/list/state-item-table.less b/public/css/list/state-item-table.less
new file mode 100644
index 0000000..9c2ee62
--- /dev/null
+++ b/public/css/list/state-item-table.less
@@ -0,0 +1,201 @@
+// Style
+
+.state-item-table {
+ padding: 0;
+
+ thead {
+ th {
+ font-weight: normal;
+
+ // Border styles start
+ form {
+ padding: 0 0 0 1px;
+ border-bottom: 1px solid @gray-light;
+ background: linear-gradient(to top, @gray-light, @body-bg-color);
+
+ button {
+ background: @body-bg-color;
+ }
+ }
+ &:first-child form {
+ padding-left: 0;
+ }
+ // Border styles end
+ }
+
+ button {
+ .appearance(none);
+ border: none;
+ background: none;
+ padding: .1em .5em;
+
+ text-align: left;
+ color: @text-color-light;
+
+ > .icon {
+ opacity: 0;
+ width: 0;
+ transition: opacity .25s linear, width .25s ease;
+ }
+ &:hover .icon,
+ &:focus .icon,
+ &.active .icon {
+ opacity: 1;
+ width: 1em;
+ }
+
+ &.active {
+ font-weight: bold;
+ }
+ }
+ }
+
+ .list-item:not(:last-child) > *:not(.visual),
+ .row-item:not(:last-child) {
+ border-bottom: 1px solid @gray-light;
+ }
+}
+
+@media print {
+ .list-item.page-break-follows {
+ &:not(:last-child) > *:not(.visual) {
+ border-bottom: none;
+ }
+ }
+}
+
+// Layout
+
+table.state-item-table {
+ table-layout: fixed;
+}
+
+.state-item-table {
+ display: table;
+ width: 100%;
+ margin: 0;
+
+ thead {
+ position: sticky;
+ top: 0;
+
+ th {
+ // That's layout, yes, controls overflow when scrolling
+ padding: 1em 0 0 0;
+ background: @body-bg-color;
+ }
+
+ th button {
+ width: 100%;
+ display: inline-flex;
+ align-items: baseline;
+ justify-content: space-between;
+
+ span {
+ .text-ellipsis();
+ }
+ }
+ }
+
+ th.has-visual {
+ width: 3em;
+ }
+
+ tbody td {
+ .text-ellipsis();
+ vertical-align: top;
+ }
+
+ .list-item {
+ display: table-row;
+ }
+
+ .list-item > .col {
+ display: table-cell;
+ vertical-align: middle;
+ white-space: nowrap;
+
+ &:not(:last-child) {
+ padding-right: 1em;
+ }
+
+ &.title {
+ .text-ellipsis();
+ width: 100%;
+ }
+
+ > * {
+ display: inline-block;
+
+ &:not(:last-child) {
+ margin-right: .5em;
+ }
+ }
+ }
+
+ .list-item > *:not(.visual) {
+ padding: .5em 0;
+ }
+
+ .list-item > .visual {
+ display: table-cell;
+ padding: .5em 1em 0 0;
+ }
+
+ > .empty-state-bar,
+ > tbody > tr:first-child .empty-state-bar {
+ margin: 0 1em;
+ }
+}
+
+.content.full-width .state-item-table .list-item {
+ // The .list-item itself can't have padding because of `display:table-row`
+ &:before, &:after {
+ display: inline-block;
+ content: '\00a0';
+ width: 1em;
+ }
+}
+
+#layout.twocols table.state-item-table {
+ > thead > tr > th,
+ > tbody > tr > td {
+ &:nth-child(n+6) {
+ display: none;
+ width: 0;
+ }
+ }
+}
+
+#layout.wide-layout .item-table th.has-plugin-output {
+ width: 50em;
+}
+#layout.default-layout .item-table th.has-plugin-output {
+ width: 30em;
+}
+#layout.compact-layout .item-table th.has-plugin-output {
+ width: 10em;
+}
+
+#layout.twocols table.item-table {
+ .has-plugin-output {
+ width: auto;
+ }
+}
+
+table.item-table {
+ th.has-visual {
+ button {
+ display: inline-flex;
+ justify-content: center;
+ }
+
+ span > .icon:before {
+ margin: 0;
+ }
+ }
+
+ .visual {
+ text-align: center;
+ }
+}
diff --git a/public/css/list/state-row-item.less b/public/css/list/state-row-item.less
new file mode 100644
index 0000000..31c7e5a
--- /dev/null
+++ b/public/css/list/state-row-item.less
@@ -0,0 +1,42 @@
+// Style
+
+.row-item {
+ .plugin-output {
+ overflow: hidden;
+ .line-clamp();
+ }
+
+ .subject {
+ font-weight: bold;
+ }
+}
+
+// Layout
+
+.row-item {
+ .performance-data {
+ overflow: hidden;
+ .line-clamp(2);
+ white-space: normal;
+ margin-left: -.25em;
+
+ .inline-pie {
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ margin-left: .25em;
+ }
+ }
+
+ .has-icon-images {
+ height: 2.5em;
+ vertical-align: middle;
+
+ img {
+ max-height: 100%;
+ max-width: 100%;
+ height: auto;
+ width: auto;
+ }
+ }
+}
diff --git a/public/css/list/user-list.less b/public/css/list/user-list.less
new file mode 100644
index 0000000..078d1b9
--- /dev/null
+++ b/public/css/list/user-list.less
@@ -0,0 +1,26 @@
+// Layout
+
+.controls .user-list,
+.controls .usergroup-list {
+ .usergroup-ball,
+ .user-ball {
+ height: 1.5em;
+ width: 1.5em;
+ line-height: ~"calc(1.5em - 4px)";
+ display: block;
+ }
+
+ .title br {
+ display: none;
+ }
+
+ .list-item {
+ & > .visual {
+ padding-top: 0.25em;
+ }
+
+ & > .col {
+ padding: .25em 0;
+ }
+ }
+}
diff --git a/public/css/markdown.less b/public/css/markdown.less
new file mode 100644
index 0000000..bebede5
--- /dev/null
+++ b/public/css/markdown.less
@@ -0,0 +1,80 @@
+.markdown {
+ > p,
+ > hr,
+ > ul,
+ > ol,
+ > table,
+ > pre,
+ > blockquote,
+ li > ul,
+ li > ol {
+ margin: 1em 0 1em 0;
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+
+ p {
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ a {
+ border-bottom: 1px dotted @text-color-light;
+
+ &:hover, &:focus {
+ border-bottom: 1px solid @text-color;
+ 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;
+ border-color: @gray-light;
+ }
+ }
+}
+
+.markdown.inline {
+ img {
+ max-height: 100%;
+ vertical-align: middle;
+ }
+
+ a.with-thumbnail {
+ &, &:hover, &:focus {
+ border-bottom: none;
+ }
+ }
+}
diff --git a/public/css/mixin/progress-bar.less b/public/css/mixin/progress-bar.less
new file mode 100644
index 0000000..d15b4c3
--- /dev/null
+++ b/public/css/mixin/progress-bar.less
@@ -0,0 +1,217 @@
+.progress-bar() {
+ &.progress-bar {
+ --hPadding: 10%;
+ --duration-scale: 80%;
+
+ .above,
+ .below {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ position: relative;
+ height: ~"calc(2em + 2px)";
+ }
+
+ .below {
+ > .left {
+ position: absolute;
+ left: var(--hPadding);
+ top: 0;
+ }
+
+ > .right {
+ position: absolute;
+ left: ~"calc(var(--hPadding) + var(--duration-scale))";
+ top: 0;
+ }
+ }
+
+ .positioned {
+ position: absolute;
+ }
+
+ .bubble {
+ .rounded-corners(.25em);
+ background-color: @body-bg-color;
+ border: 1px solid;
+ border-color: @gray-light;
+ position: relative;
+ box-shadow: 0 0 1em 0 rgba(0, 0, 0, .1);
+ padding: .25em .5em;
+ text-align: center;
+ width: auto;
+ // The wrapper of .bubble is dynamically moved to the left based on the value of the progress bar
+ // This moves the center of the bubble to the beginning of the wrapper regardless of the size of the content.
+ transform: translate(-50%, 0);
+ z-index: 1;
+
+ > * {
+ position: relative;
+ z-index: 2;
+ }
+
+ &:hover {
+ z-index: 5;
+ }
+
+ &::before {
+ background-color: @body-bg-color;
+ border-bottom: 1px solid @gray-light;
+ border-right: 1px solid @gray-light;
+ content: "";
+ display: block;
+ height: 1em;
+ margin-left: -.5em;
+ transform: rotate(45deg);
+ width: 1em;
+ z-index: 1;
+
+ position: absolute;
+ bottom: -.5em;
+ left: 50%;
+ }
+
+ &.upwards::before {
+ bottom: auto;
+ top: -7/12em;
+ transform: rotate(225deg);
+ }
+
+ &.right-aligned {
+ // This is (.675em (:before placement) + .5em (half :before width)) + 1px (:before border)
+ transform: translate(~"calc(-1.175em - 1px)", 0);
+
+ &::before {
+ top: auto;
+ left: 1.175em;
+ bottom: -.5em;
+ }
+ }
+
+ &.left-aligned {
+ // entire width (moves the right border in place of the left) + (.675em (:before placement) + .5em (half :before width)) + 1px (:before border)
+ transform: translate(~"calc(-100% + 1.175em + 1px)", 0);
+
+ &::before {
+ top: auto;
+ left: auto;
+ right: .675em;
+ bottom: -.5em;
+ }
+ }
+ }
+
+ .above .positioned {
+ bottom: 0;
+ }
+
+ .below .positioned {
+ top: 0;
+ }
+
+ .vertical-key-value {
+ .key {
+ white-space: nowrap;
+ }
+
+ .value {
+ white-space: nowrap;
+ font-size: 1em;
+ line-height: 1;
+ }
+ }
+
+ .timeline {
+ @marker-gap: 1/12em;
+
+ .rounded-corners(.5em);
+ background-color: @gray-lighter;
+ height: 1em;
+ margin: 1em 0;
+ position: relative;
+ width: 100%;
+ z-index: 1;
+
+ .marker {
+ .rounded-corners(50%);
+ background-color: @gray-light;
+ height: .857em;
+ margin-left: -.857/2em;
+ width: .857em;
+ z-index: 2;
+
+ position: absolute;
+ top: @marker-gap;
+
+ &.highlighted {
+ background-color: @icinga-blue;
+ }
+
+ &.left {
+ left: var(--hPadding);
+ }
+
+ &.right {
+ left: ~"calc(var(--hPadding) + var(--duration-scale))";
+ }
+ }
+
+ .progress {
+ position: absolute;
+ left: var(--hPadding);
+ width: var(--duration-scale);
+
+ &[data-animate-progress]::before {
+ content: "";
+ display: block;
+ width: .5em + @marker-gap;
+ height: 1em + (@marker-gap * 2);
+ margin-top: -@marker-gap;
+
+ .rounded-corners(.5em);
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+
+ position: absolute;
+ left: -.5em - @marker-gap;
+ }
+
+ > .bar {
+ width: 0; // set by progress-bar.js
+ height: 1em + (@marker-gap * 2);
+ margin-top: -@marker-gap;
+ }
+
+ &::before,
+ > .bar {
+ background-color: @gray-light;
+ }
+ }
+
+ .timeline-overlay {
+ position: absolute;
+ left: ~"calc(var(--hPadding) + var(--duration-scale))";
+ width: var(--overlay-scale);
+ height: 1em + (@marker-gap * 2);
+ margin-top: -@marker-gap;
+
+ opacity: .6;
+ }
+
+ .progress > .bar,
+ .timeline-overlay {
+ display: flex;
+ justify-content: flex-end;
+
+ .now {
+ width: .25em;
+
+ border: solid @default-bg;
+ border-width: 1px 0 1px 0;
+ background-color: red;
+ }
+ }
+ }
+ }
+}
diff --git a/public/css/mixin/state-badges.less b/public/css/mixin/state-badges.less
new file mode 100644
index 0000000..4be2d07
--- /dev/null
+++ b/public/css/mixin/state-badges.less
@@ -0,0 +1,31 @@
+.state-badges() {
+ &.state-badges {
+ padding: 0;
+
+ ul {
+ padding: 0;
+ }
+
+ li {
+ display: inline-block;
+ }
+
+ li > ul > li:first-child:not(:last-child) .state-badge {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ li > ul > li:last-child:not(:first-child) .state-badge {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0;
+ }
+
+ > li:not(:last-child) {
+ margin-right: .25em;
+ }
+
+ li > ul > li + li {
+ margin-left: 1px;
+ }
+ }
+}
diff --git a/public/css/mixins.less b/public/css/mixins.less
new file mode 100644
index 0000000..326bf46
--- /dev/null
+++ b/public/css/mixins.less
@@ -0,0 +1,5 @@
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+.monospace() {
+ font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
+}
diff --git a/public/css/view/service-grid.less b/public/css/view/service-grid.less
new file mode 100644
index 0000000..41400e5
--- /dev/null
+++ b/public/css/view/service-grid.less
@@ -0,0 +1,60 @@
+.service-grid-table {
+ width: 0;
+ white-space: nowrap;
+
+ td {
+ color: @gray-light;
+ padding: 0.2em;
+ text-align: center;
+ width: 1em;
+ }
+
+ .rotate-45 {
+ height: 10em;
+
+ div {
+ .transform(translate(0.4em, 2.8em) rotate(315deg));
+ width: 1.5em;
+ }
+ }
+
+ .service-grid-table-more {
+ text-align: center;
+ a {
+ display: inline;
+ }
+ }
+}
+
+.joystick-pagination {
+ margin: 0 auto;
+ font-size: 130%;
+
+ a {
+ color: @text-color;
+ outline: none;
+
+ &:hover {
+ color: @text-color-light;
+ }
+ &:focus, &:active {
+ color: @icinga-blue;
+ }
+ }
+
+ i {
+ display: block;
+ height: 1.5em;
+ width: 1.5em;
+ }
+}
+
+.service-grid-link {
+ .bg-stateful();
+ .rounded-corners();
+
+ display: inline-block;
+ height: 1.5em;
+ vertical-align: middle;
+ width: 1.5em;
+}
diff --git a/public/css/widget/actions.less b/public/css/widget/actions.less
new file mode 100644
index 0000000..796fc38
--- /dev/null
+++ b/public/css/widget/actions.less
@@ -0,0 +1,20 @@
+.object-detail-actions a {
+ border-bottom: 1px solid @gray-light;
+ display: inline-block;
+ margin-bottom: .25em;
+
+ .text-ellipsis();
+ max-width: 32em;
+
+ &:hover {
+ border-color: @icinga-blue-light;
+ color: @icinga-blue;
+ text-decoration: none;
+ }
+}
+
+ul.object-detail-actions {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
diff --git a/public/css/widget/check-attempt.less b/public/css/widget/check-attempt.less
new file mode 100644
index 0000000..1042a08
--- /dev/null
+++ b/public/css/widget/check-attempt.less
@@ -0,0 +1,17 @@
+.check-attempt {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.check-attempt .ball {
+ background-color: @gray-light;
+
+ &:not(:last-child) {
+ margin-right: 1/6em;
+ }
+
+ &.taken {
+ background-color: @gray-semilight;
+ }
+}
diff --git a/public/css/widget/check-statistics.less b/public/css/widget/check-statistics.less
new file mode 100644
index 0000000..4bd34c2
--- /dev/null
+++ b/public/css/widget/check-statistics.less
@@ -0,0 +1,192 @@
+.check-statistics {
+ position: relative;
+ .card();
+ .progress-bar();
+
+ .check-attempt {
+ display: inline-flex;
+ }
+
+ .bubble {
+ &.top-left-aligned,
+ &.top-right-aligned {
+ &::before {
+ visibility: hidden;
+ }
+
+ svg {
+ position: absolute;
+ top: -1em;
+ width: 1em;
+ height: 1em;
+
+ .bg {
+ fill: @body-bg-color;
+ }
+
+ .border {
+ fill: @gray-light;
+ }
+ }
+ }
+
+ &.top-left-aligned {
+ transform: unset;
+ border-top-left-radius: 0;
+
+ svg {
+ left: -1px;
+ }
+ }
+
+ &.top-right-aligned {
+ transform: translate(-100%);
+ border-top-right-radius: 0;
+
+ svg {
+ right: -1px;
+ }
+ }
+ }
+
+ // ATTENTION!: `&.progress-bar {` must not be used here, seems to confuse the less parser!!!!111
+
+ &.progress-bar .timeline .progress.running {
+ &::before,
+ > .bar {
+ background: @state-ok;
+ }
+ }
+
+ &.progress-bar .check-timeline {
+ margin-top: .5em;
+ }
+ &.progress-bar .above {
+ margin-top: .5em;
+ }
+
+ .interval-line {
+ position: absolute;
+ height: 100%;
+
+ &::before {
+ position: absolute;
+ top: ~"calc(50% - .125em)";
+ display: block;
+ height: .25em;
+ width: 100%;
+ content: "";
+
+ background-color: @gray-light;
+ }
+
+ .vertical-key-value {
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, 0);
+
+ padding: 0 .2em;
+ background-color: @body-bg-color;
+ }
+
+ .start,
+ .end {
+ position: absolute;
+ top: 50%;
+ width: .25em;
+ height: 1em;
+ background-color: @gray;
+ }
+
+ .start {
+ left: 0;
+ transform: translate(-50%, -50%);
+ }
+
+ .end {
+ right: 0;
+ transform: translate(50%, -50%);
+ }
+ }
+
+ .execution-line .vertical-key-value {
+ z-index: 1;
+ }
+
+ &.check-overdue {
+ --duration-scale: 60%;
+ --overlay-scale: 20%;
+
+ .above {
+ .now {
+ position: absolute;
+ right: var(--hPadding);
+ bottom: 0;
+
+ .bubble {
+ // to move the center of the bubble to the end of the wrapper.
+ transform: translate(50%, 0);
+ }
+ }
+ }
+
+ .timeline-overlay {
+ background: linear-gradient(90deg, @gray-light 0, @color-down 2em);
+ opacity: 1;
+
+ &::after {
+ background-color: @color-down;
+ }
+ }
+ }
+
+ &.checks-disabled.progress-bar {
+ .timeline {
+ .marker {
+ &.highlighted {
+ background-color: @gray;
+ }
+ }
+ }
+ }
+
+ .checks-disabled-overlay {
+ border-radius: 0.4em;
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ background-color: ~"@{disabled-gray}20";
+ z-index: 1;
+
+ .notes {
+ color: @text-color-light;
+ margin-top: -4em;
+ text-shadow: 0 0 1px rgba(0, 0, 0, 0.25);
+ }
+ }
+}
+
+#layout.twocols &#col1,
+#layout.minimal-layout,
+#layout.poor-layout,
+#layout.twocols.compact-layout,
+#layout.twocols.default-layout {
+ .check-statistics .bubble.top-right-aligned {
+ transform: translate(-50%, 0); // default what progress-bar() defined
+ border-top-right-radius: .25em; // default what progress-bar() defined
+
+ &::before {
+ visibility: visible;
+ }
+
+ svg {
+ display: none;
+ }
+ }
+}
diff --git a/public/css/widget/comment-popup.less b/public/css/widget/comment-popup.less
new file mode 100644
index 0000000..4012697
--- /dev/null
+++ b/public/css/widget/comment-popup.less
@@ -0,0 +1,74 @@
+.comment-popup {
+ font-size: 1em / .857em; // // default font size / footer font size = 12px
+ height: 6em;
+ border-radius: 0.25em;
+ border: 1px solid;
+ border-color: @gray-light;
+ background-color: @body-bg-color;
+}
+
+.comment-wrapper {
+ position: relative;
+
+ .comment-popup {
+ position: absolute;
+ top: 2.5em;
+ left: -1.6em;
+ z-index: 1;
+ display: none;
+ width: 50em;
+ }
+
+ .comment-popup:before {
+ content: '';
+ position: absolute;
+ display: inline-block;
+ width: 1.5em;
+ height: 1.5em;
+ left: 1.5em;
+ top: ~"calc(-0.75em - 1px)";
+ border-left: 1px solid @gray-light;
+ border-top: 1px solid @gray-light;
+ background-color: @body-bg-color;
+ z-index: -1;
+ transform: rotate(45deg);
+ }
+}
+
+ul.item-list li:last-child .comment-wrapper {
+ .comment-popup {
+ top: -7em;
+ }
+
+ .comment-popup:before {
+ bottom: ~"calc(-0.75em - 1px)";
+ top: unset;
+ transform: rotate(225deg);
+ }
+}
+
+.comment-wrapper:hover .comment-popup {
+ display: block
+}
+
+#layout {
+ &.twocols {
+ .comment-popup {
+ width: 35em;
+ }
+
+ &.poor-layout,
+ &.compact-layout {
+ .comment-popup {
+ width: 25em;
+ }
+ }
+ }
+
+ &.poor-layout,
+ &.minimal-layout {
+ .comment-popup {
+ width: 25em;
+ }
+ }
+}
diff --git a/public/css/widget/custom-var-table.less b/public/css/widget/custom-var-table.less
new file mode 100644
index 0000000..46f1984
--- /dev/null
+++ b/public/css/widget/custom-var-table.less
@@ -0,0 +1,60 @@
+.custom-var-table {
+ .level-1 th {
+ padding-left: .5em;
+ }
+
+ .level-2 th {
+ padding-left: 1em;
+ }
+
+ .level-3 th {
+ padding-left: 1.5em;
+ }
+
+ .level-4 th {
+ padding-left: 2em;
+ }
+
+ .level-5 th {
+ padding-left: 2.5em;
+ }
+
+ .level-6 th {
+ padding-left: 3em;
+ }
+
+ thead th {
+ padding-left: 0;
+ text-align: left;
+ font-weight: bold;
+ font-size: 1.167em;
+
+ > span {
+ :nth-child(1),
+ :nth-child(2) {
+ display: none;
+ }
+ }
+ }
+
+ &.can-collapse thead th > span, // Icinga Web 2 < 2.12
+ &[data-can-collapse] thead th > span { // >= 2.12
+ :nth-child(1) {
+ display: none;
+ }
+
+ :nth-child(2) {
+ display: inline-block;
+ }
+ }
+
+ &.collapsed thead th > span {
+ :nth-child(1) {
+ display: inline-block;
+ }
+
+ :nth-child(2) {
+ display: none;
+ }
+ }
+}
diff --git a/public/css/widget/donut-container.less b/public/css/widget/donut-container.less
new file mode 100644
index 0000000..6fc4466
--- /dev/null
+++ b/public/css/widget/donut-container.less
@@ -0,0 +1,24 @@
+.donut-container {
+ .card();
+
+ h2 {
+ margin: 0;
+ }
+
+ .state-badges {
+ text-align: center;
+ }
+
+ &:not(:last-of-type) {
+ margin-right: 1em;
+ margin-bottom: 1em;
+ }
+
+ .donut {
+ margin: 0 auto;
+ }
+}
+
+#layout.minimal-layout .donut-container {
+ width: 100%;
+}
diff --git a/public/css/widget/downtime-card.less b/public/css/widget/downtime-card.less
new file mode 100644
index 0000000..37dd8a5
--- /dev/null
+++ b/public/css/widget/downtime-card.less
@@ -0,0 +1,10 @@
+.downtime-progress {
+ .progress-bar();
+
+ &.progress-bar .timeline .downtime-elapsed {
+ &::before,
+ > .bar {
+ background-color: @state-ok;
+ }
+ }
+}
diff --git a/public/css/widget/group-grid.less b/public/css/widget/group-grid.less
new file mode 100644
index 0000000..94b5b13
--- /dev/null
+++ b/public/css/widget/group-grid.less
@@ -0,0 +1,42 @@
+// HostGroup- and -ServiceGroupGrid styles
+
+ul.item-table.group-grid {
+ grid-template-columns: repeat(auto-fit, 15em);
+ grid-gap: 1em 2em;
+
+ .table-row {
+ margin: -.25em;
+ padding: .25em;
+ border-radius: .5em;
+ }
+
+ li.group-grid-cell {
+ .title {
+ align-items: center;
+ }
+
+ .visual {
+ margin-right: 1em;
+ }
+
+ .content {
+ line-height: 1;
+
+ a {
+ display: inline-block;
+ max-width: 10em;
+ text-align: center;
+ }
+ }
+
+ .state-badge {
+ width: 2.5em;
+ height: 2.5em;
+ line-height: 2;
+ }
+ }
+}
+
+.content.full-width ul.item-table.group-grid {
+ margin: 0 1em;
+}
diff --git a/public/css/widget/host-state-badges.less b/public/css/widget/host-state-badges.less
new file mode 100644
index 0000000..d55a45c
--- /dev/null
+++ b/public/css/widget/host-state-badges.less
@@ -0,0 +1,3 @@
+.host-state-badges {
+ .state-badges();
+}
diff --git a/public/css/widget/key-value-list.less b/public/css/widget/key-value-list.less
new file mode 100644
index 0000000..cd99f5f
--- /dev/null
+++ b/public/css/widget/key-value-list.less
@@ -0,0 +1,19 @@
+.key-value-list {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ li {
+ display: flex;
+ }
+
+ li > span {
+ padding: .25em;
+
+ &.label {
+ display: block;
+ padding-left: 0;
+ width: 12em;
+ }
+ }
+}
diff --git a/public/css/widget/migrate-popup.less b/public/css/widget/migrate-popup.less
new file mode 100644
index 0000000..8f9586b
--- /dev/null
+++ b/public/css/widget/migrate-popup.less
@@ -0,0 +1,181 @@
+#migrate-popup {
+ @transitionLength: 350ms;
+
+ display: flex;
+ min-width: 16em;
+ z-index: 1000;
+ position: fixed;
+ top: 0;
+ right: 4em;
+ pointer-events: none;
+ line-height: 1.5em;
+
+ .transform(translateY(-100%));
+ .transition(transform @transitionLength ease-in);
+
+ &.active {
+ .transform(translateY(0%));
+ .transition(transform @transitionLength ease-out);
+ }
+
+ .suggestion-area {
+ .transform(translateY(0%));
+ .transition(transform 0s linear @transitionLength);
+ }
+
+ &.active .suggestion-area {
+ .transition(transform @transitionLength ease-out);
+ }
+
+ &.minimized .suggestion-area {
+ .transform(translateY(-100%));
+ .transition(transform @transitionLength ease-in);
+ }
+
+ &.hidden .suggestion-area {
+ .transition(none);
+ }
+
+ .minimizer {
+ width: 1.25em;
+ height: 1.5em;
+ margin-left: -1px;
+ z-index: 1;
+ pointer-events: auto;
+
+ border-bottom-right-radius: 4px;
+ background-color: @body-bg-color;
+
+ .transition(none);
+
+ i:before {
+ width: 1em;
+ margin: .1em 0 0 0;
+ content: '\f102';
+ font-size: 1.25em;
+ cursor: pointer;
+ color: @gray-light;
+ }
+
+ i:hover:before {
+ color: @menu-highlight-color;
+ }
+ }
+
+ &.minimized .minimizer {
+ border-bottom-left-radius: 4px;
+ .transition(border-bottom-left-radius 0s linear @transitionLength);
+ }
+
+ &.hidden .minimizer i:before {
+ content: '\f103';
+ }
+
+ &:not(.active) .suggestion-area, &.hidden .suggestion-area {
+ box-shadow: none;
+ }
+
+ .suggestion-area {
+ display: flex;
+ flex-direction: column-reverse;
+
+ padding: .75em;
+ flex-grow: 1;
+ pointer-events: auto;
+ font-size: .75em;
+
+ background: @body-bg-color;
+ border-radius: 0 0 4px 4px;
+ box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.3);
+
+ button {
+ .link-button();
+ }
+
+ p {
+ display: none;
+ margin-bottom: .5em;
+ color: @text-color-light;
+ }
+
+ form ~ .monitoring-migration-hint,
+ .search-migration-suggestions:not(:empty) + .search-migration-hint,
+ .monitoring-migration-suggestions:not(:empty) + .monitoring-migration-hint {
+ display: block;
+ }
+
+ & > button.close {
+ margin-left: auto;
+ margin-top: 1em;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ ul {
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+ }
+
+ li {
+ margin: .5em 0;
+ display: flex;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+
+ &:first-of-type {
+ margin-top: 0;
+ }
+ }
+
+ li {
+ :not(:last-child) {
+ margin-right: .5em;
+ }
+
+ button:hover{
+ opacity: 0.8;
+ }
+
+ button[value="1"] {
+ flex-grow: 1;
+
+ color: @text-color;
+ text-decoration: underline;
+ }
+
+ button[value="0"] {
+ i:before {
+ margin: 0;
+ content: '\e804';
+ }
+ }
+ }
+
+ form {
+ width: 100%;
+
+ .control-group {
+ display: flex;
+ align-items: center;
+
+ .control-label-group {
+ margin-right: .5em;
+ }
+
+ label {
+ margin-left: auto;
+ }
+ }
+ }
+
+ .search-migration-suggestions:not(:empty) ~ form,
+ .search-migration-suggestions:not(:empty) ~ .monitoring-migration-suggestions:not(:empty) {
+ margin-bottom: .5em;
+ }
+ }
+}
diff --git a/public/css/widget/monitoring-health.less b/public/css/widget/monitoring-health.less
new file mode 100644
index 0000000..f0ed252
--- /dev/null
+++ b/public/css/widget/monitoring-health.less
@@ -0,0 +1,136 @@
+.monitoring-health {
+ max-width: 65em;
+
+ > section:not(:last-child) {
+ margin-bottom: 4em;
+ }
+
+ .vertical-key-value .value {
+ display: inline-block;
+ margin-bottom: .25em;
+ }
+
+ .check-summary {
+ padding: .5em 0;
+
+ .col {
+ padding: 0 1em .5em 1em;
+ }
+
+ .col:not(:last-child) {
+ border-right: 1px solid @gray-light;
+ }
+ }
+
+ .check-summary,
+ .instance-features {
+ .rounded-corners();
+ border: 1px solid;
+ border-color: @gray-light;
+ display: flex;
+ width: 100%;
+ }
+
+ .check-summary,
+ .col-content,
+ .icinga-info {
+ width: 100%;
+
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ }
+
+
+ .icinga-info {
+ margin-bottom: -2em;
+
+ .vertical-key-value {
+ margin-bottom: 2em;
+ }
+ }
+
+ .col {
+ flex: 1 1 auto;
+ text-align: center;
+
+ > h3 {
+ margin: 0 0 1em;
+ }
+ }
+
+ .icinga-health {
+ .rounded-corners();
+ margin-bottom: 2em;
+ padding: 1em;
+ text-align: center;
+ width: 100%;
+ font-weight: bold;
+
+ &.up {
+ border: 1px solid;
+ border-color: @color-up;
+ color: @color-up;
+ }
+
+ &.down {
+ background-color: @color-down;
+ color: @body-bg-color;
+ }
+ }
+
+ .instance-features {
+ .control-group {
+ flex: 1 1 auto;
+ margin: .5em 0;
+ padding: .5em;
+ width: 0;
+
+ display: flex;
+ align-items: center;
+ flex-direction: column-reverse;
+ justify-content: flex-end;
+
+ &:not(:last-of-type) {
+ border-right: 1px solid @gray-light;
+ }
+
+ .control-label-group {
+ font-size: 10/12em;
+ margin-right: 0;
+ margin-top: 1em;
+ padding: 0;
+ text-align: center;
+ width: auto;
+
+ label {
+ text-align: center;
+ }
+ }
+
+ .toggle-switch {
+ margin: 0;
+ }
+ }
+ }
+}
+
+#layout.minimal-layout {
+ .icinga-info {
+ .vertical-key-value {
+ width: 100%;
+ }
+ }
+
+ .instance-features {
+ flex-wrap: wrap;
+
+ .control-group {
+ width: 33%;
+
+ &:nth-child(3n) {
+ border-right: none;
+ }
+ }
+ }
+}
diff --git a/public/css/widget/notice.less b/public/css/widget/notice.less
new file mode 100644
index 0000000..7067665
--- /dev/null
+++ b/public/css/widget/notice.less
@@ -0,0 +1,23 @@
+// Style
+
+.notice {
+ @margin: 1em / 1.25;
+ @padding: .75em / 1.25;
+
+ .rounded-corners();
+ padding: @padding;
+ color: @text-color-on-icinga-blue;
+ background-color: @state-warning;
+ font-weight: bold;
+ font-size: 1.25em;
+
+ // Layout
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin: 0 @margin @margin @margin;
+
+ > span {
+ .text-ellipsis();
+ }
+}
diff --git a/public/css/widget/object-features.less b/public/css/widget/object-features.less
new file mode 100644
index 0000000..b39414d
--- /dev/null
+++ b/public/css/widget/object-features.less
@@ -0,0 +1,53 @@
+form.object-features {
+ span.description {
+ text-align: left;
+ }
+
+ .control-label-group {
+ text-align: left;
+ margin-right: 0;
+ width: @name-value-table-name-width;
+ color: @text-color-light;
+
+ label {
+ font-size: inherit;
+ }
+ }
+
+ .control-group {
+ margin-top: 0;
+ margin-bottom: 0;
+
+ &.indeterminate {
+ justify-content: flex-start;
+
+ .control-label-group {
+ flex: 0 1 auto;
+ }
+
+ select {
+ width: auto;
+ flex: 0 1 auto;
+
+ & + span.hint {
+ flex: 0 1 auto;
+ }
+ }
+ }
+ }
+
+ .toggle-switch {
+ margin-left: @table-column-padding;
+ }
+
+ select {
+ margin-right: .5em;
+ margin-left: @table-column-padding;
+
+ & + span.hint {
+ margin: .35em;
+ color: @gray-light;
+ font-style: italic;
+ }
+ }
+}
diff --git a/public/css/widget/object-inspection.less b/public/css/widget/object-inspection.less
new file mode 100644
index 0000000..60a99bf
--- /dev/null
+++ b/public/css/widget/object-inspection.less
@@ -0,0 +1,17 @@
+// Style
+
+// Layout
+
+.inspection-detail {
+ th {
+ width: 16em;
+ }
+
+ pre, td {
+ white-space: break-spaces;
+ word-break: break-word;
+ -webkit-hyphens: auto;
+ -ms-hyphens: auto;
+ hyphens: auto;
+ }
+}
diff --git a/public/css/widget/object-meta-info.less b/public/css/widget/object-meta-info.less
new file mode 100644
index 0000000..82ad044
--- /dev/null
+++ b/public/css/widget/object-meta-info.less
@@ -0,0 +1,95 @@
+// Style
+
+.object-meta-info {
+ .vertical-key-value .value {
+ font-weight: normal;
+ }
+
+ .vertical-key-value:first-child {
+ text-align: left;
+ }
+
+ .vertical-key-value:last-child {
+ text-align: right;
+ }
+}
+
+.object-meta-info-control {
+ .link-button();
+
+ .rounded-corners(0 0 .5em .5em);
+ border: 1px solid;
+ border-color: @gray-lighter;
+ border-top-width: 0;
+ background: @body-bg-color;
+}
+
+// Layout
+
+.object-meta-info {
+ display: flex;
+ justify-content: space-between;
+
+ .horizontal-key-value {
+ padding: 0;
+
+ .key {
+ width: 9em;
+ }
+ }
+
+ .vertical-key-value {
+ &:first-child {
+ .text-ellipsis();
+ margin-right: 1em;
+ }
+
+ &:last-child {
+ white-space: nowrap;
+ margin-left: 1em;
+ }
+ }
+
+ .horizontal-key-value .value,
+ .horizontal-key-value .key,
+ .vertical-key-value .value,
+ .vertical-key-value .key {
+ font-size: 16/18em;
+ line-height: 18/16; // compensate smaller font-size (1/font-size)
+ }
+}
+
+.object-meta-info-control {
+ position: absolute;
+ right: 2.25em;
+ bottom: -2.5em; // height + margin-bottom
+ height: 2em;
+ padding: .25em;
+
+ .collapse-icon,
+ .expand-icon {
+ font-size: 1.2em;
+
+ &:before {
+ margin-right: 0;
+ }
+ }
+
+ .collapse-icon {
+ display: block;
+ }
+
+ .expand-icon {
+ display: none;
+ }
+}
+
+.collapsed + .object-meta-info-control {
+ .collapse-icon {
+ display: none;
+ }
+
+ .expand-icon {
+ display: block;
+ }
+}
diff --git a/public/css/widget/object-statistics.less b/public/css/widget/object-statistics.less
new file mode 100644
index 0000000..5a9c97a
--- /dev/null
+++ b/public/css/widget/object-statistics.less
@@ -0,0 +1,44 @@
+ul.object-statistics {
+ // Reset defaults
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ display: flex;
+ align-items: center;
+
+ > li:not(:last-child) {
+ margin-right: 1em;
+ }
+}
+
+.object-statistics-graph .donut-graph {
+ height: 2em;
+ width: 2em;
+ vertical-align: middle;
+}
+
+.object-statistics-total a {
+ display: inline-block;
+ line-height: 1;
+ position: relative;
+
+ &:hover:before {
+ .rounded-corners();
+ background-color: @gray-lighter;
+ content: "";
+ display: block;
+ z-index: 1;
+
+ position: absolute;
+ bottom: -.4em;
+ left: -.4em;
+ top: -.4em;
+ right: -.4em;
+ }
+
+ .vertical-key-value {
+ position: relative;
+ z-index: 2;
+ }
+}
diff --git a/public/css/widget/performance-data-table.less b/public/css/widget/performance-data-table.less
new file mode 100644
index 0000000..26c18c8
--- /dev/null
+++ b/public/css/widget/performance-data-table.less
@@ -0,0 +1,62 @@
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+.performance-data-table {
+ width: 100%;
+ overflow-x: auto;
+ display: block;
+
+ tr:not(:last-child) {
+ border-bottom: 1px solid @gray-lighter;
+ }
+
+ td {
+ text-align: right;
+ .text-ellipsis();
+ }
+
+ th {
+ font-size: .857em;
+ font-weight: normal;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ }
+
+ thead {
+ border-bottom: 1px solid @gray-light;
+ }
+
+ th:first-child,
+ td:first-child {
+ padding-left: 0;
+ }
+
+ .title {
+ text-align: left;
+ width: 100%;
+ }
+
+ td.title {
+ font-weight: bold;
+ }
+
+ .sparkline-col {
+ min-width: 1.75em;
+ width: 1.75em;
+ padding: 2/12em 0;
+ .invalid-perfdata {
+ font-size: 1.25em;
+ vertical-align: text-bottom;
+ color: @color-warning;
+ }
+ }
+
+ .inline-pie > svg {
+ vertical-align: middle;
+ }
+
+ .no-value {
+ color: @gray-semilight;
+ text-align: center;
+ display: block;
+ }
+}
diff --git a/public/css/widget/quick-actions.less b/public/css/widget/quick-actions.less
new file mode 100644
index 0000000..bddea43
--- /dev/null
+++ b/public/css/widget/quick-actions.less
@@ -0,0 +1,47 @@
+.quick-actions {
+ display: flex;
+ flex-wrap: wrap;
+ list-style-type: none;
+ margin: 0 -.5em;
+ padding: 0;
+
+ a {
+ text-decoration: none;
+ }
+
+ a,
+ button {
+ padding: .25em;
+ .rounded-corners();
+ display: inline-flex;
+ align-items: baseline;
+
+ &:hover {
+ background: @gray-lighter;
+ }
+ }
+
+ li {
+ margin: 0 .25em .5em .25em;
+ vertical-align: middle;
+ white-space: nowrap;
+ }
+}
+
+.controls:not(.default-layout) > .quick-actions:last-child,
+.controls > .quick-actions:last-child {
+ margin-bottom: 0;
+}
+
+#layout.twocols:not(.wide-layout) {
+ .quick-actions {
+ justify-content: space-between;
+ min-width: 100%;
+ }
+}
+
+#layout.wide-layout .controls {
+ .quick-actions {
+ float: left;
+ }
+}
diff --git a/public/css/widget/service-state-badges.less b/public/css/widget/service-state-badges.less
new file mode 100644
index 0000000..8a97faa
--- /dev/null
+++ b/public/css/widget/service-state-badges.less
@@ -0,0 +1,3 @@
+.service-state-badges {
+ .state-badges();
+}
diff --git a/public/css/widget/state-change.less b/public/css/widget/state-change.less
new file mode 100644
index 0000000..adc8d42
--- /dev/null
+++ b/public/css/widget/state-change.less
@@ -0,0 +1,128 @@
+.state-change {
+ display: inline-flex;
+
+ &.reversed-state-balls {
+ // This is needed, because with ~ we can address only subsequent nodes
+ flex-direction: row-reverse;
+ }
+
+ .state-ball {
+ .box-shadow(0, 0, 0, 1px, @body-bg-color);
+ }
+
+ // Same on same
+ .state-ball ~ .state-ball {
+ &.ball-size-xs {
+ margin-left: -.05em;
+ }
+
+ &.ball-size-s {
+ margin-left: -.15em;
+ }
+
+ &.ball-size-m {
+ margin-left: -.275em;
+ }
+
+ &.ball-size-ml {
+ margin-left: -.375em;
+ }
+
+ &.ball-size-l,
+ &.ball-size-xl {
+ margin-left: -.875em;
+ }
+ }
+
+ // big left, smaller right
+ &:not(.reversed-state-balls) .ball-size-l ~ .state-ball {
+ &.ball-size-ml {
+ margin-top: .25em;
+ margin-left: -.5em;
+ margin-right: .25em;
+ }
+ }
+
+ // smaller left, big right
+ &.reversed-state-balls .ball-size-l ~ .state-ball {
+ &.ball-size-ml {
+ z-index: -1;
+ margin-top: .25em;
+ margin-right: -.5em;
+ }
+ }
+
+ .state-ball.state-ok,
+ .state-ball.state-up,
+ .state-pending {
+ &.ball-size-l,
+ &.ball-size-xl {
+ background-color: @body-bg-color;
+ }
+ }
+
+ // Avoid transparency on overlapping solid state-change state-balls
+ .state-ball.handled {
+ position: relative;
+ opacity: 1;
+
+ i {
+ position: relative;
+ z-index: 3;
+ }
+
+ &:before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ border-radius: 50%;
+ opacity: .6;
+ z-index: 2
+ }
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: -2px;
+ left: -2px;
+ right: -2px;
+ bottom: -2px;
+ border-radius: 50%;
+ background-color: @body-bg-color;
+ z-index: 1;
+ }
+
+ &.state-pending:before {
+ background-color: @color-pending;
+ }
+
+ &.state-down:before {
+ background-color: @color-down;
+ }
+
+ &.state-warning:before {
+ background-color: @color-warning;
+ }
+
+ &.state-critical:before {
+ background-color: @color-critical;
+ }
+
+ &.state-unknown:before {
+ background-color: @color-unknown;
+ }
+ }
+}
+
+.overdue .state-change .state-ball {
+ .box-shadow(0, 0, 0, 1px, @gray-lighter);
+
+ &.handled:after {
+ background-color: @gray-lighter;
+ }
+}
diff --git a/public/css/widget/table-layout.less b/public/css/widget/table-layout.less
new file mode 100644
index 0000000..f67ec0f
--- /dev/null
+++ b/public/css/widget/table-layout.less
@@ -0,0 +1,72 @@
+// HostGroup- and -ServiceGroupTable styles
+
+.item-table.table-layout {
+ --columns: 1;
+}
+
+ul.item-table.table-layout {
+ grid-template-columns: 1fr repeat(var(--columns), auto);
+
+ > li {
+ display: contents;
+
+ &:hover,
+ &.active {
+ .col, &::before, &::after {
+ // The li might get a background on hover. Though, this won't be visible
+ // as it has no box model since we apply display:contents to it.
+ background-color: inherit;
+ }
+ }
+ }
+
+ li:not(:last-of-type) {
+ .col {
+ border-bottom: 1px solid @gray-light;
+ }
+
+ .visual {
+ border-bottom: 1px solid @default-bg;
+ }
+ }
+
+ > .table-row {
+ &:not(:last-of-type) .title .visual {
+ margin-bottom: ~"calc(-.5em - 1px)";
+ }
+
+ .col {
+ padding: .5em 0;
+ }
+
+ .col:not(:last-child) {
+ padding-right: 1em;
+ }
+ }
+}
+
+.content.full-width ul.item-table.table-layout {
+ // Again, since the li has no box model, it cannot have padding. So the first
+ // and last child need to get the left and right padding respectively.
+ // But we don't want to have a border that spans to the very right or left,
+ // so pseudo elements are required. We could add empty cells instead, but
+ // that would require hard coding the width here, which I'd like to avoid.
+
+ grid-template-columns: ~"auto 1fr repeat(calc(var(--columns) + 1), auto)";
+
+ > li.table-row {
+ &::before, &::after {
+ display: inline-block;
+ content: '\00a0';
+ margin-bottom: 1px;
+ }
+
+ &::before {
+ padding-left: inherit;
+ }
+
+ &::after {
+ padding-right: inherit;
+ }
+ }
+}
diff --git a/public/css/widget/tag-list.less b/public/css/widget/tag-list.less
new file mode 100644
index 0000000..fe96691
--- /dev/null
+++ b/public/css/widget/tag-list.less
@@ -0,0 +1,31 @@
+.tag-list {
+ line-height: 1.5;
+ list-style-type: none;
+ margin: -.25em 0 0 0; // TODO: This is wrong here, if at all this must be part of wherever this widget is placed
+ padding: 0;
+
+ > li {
+ display: inline-block;
+
+ &:not(:last-child) {
+ margin-right: .417em;
+ margin-bottom: .25em; // TODO: Really? It's an inline ul, bottom margin is outer layout..
+ }
+
+ i {
+ opacity: 0.8;
+ }
+ }
+
+ > li > a {
+ .rounded-corners();
+ background-color: @gray-lighter;
+ display: block;
+ padding: .25em .5em;
+
+ &:hover {
+ background-color: @gray-light;
+ text-decoration: none;
+ }
+ }
+}
diff --git a/public/css/widget/view-mode-switcher.less b/public/css/widget/view-mode-switcher.less
new file mode 100644
index 0000000..7eda2a8
--- /dev/null
+++ b/public/css/widget/view-mode-switcher.less
@@ -0,0 +1,45 @@
+.view-mode-switcher {
+ list-style-type: none;
+ margin: 0 0 0.25em 0;
+ padding: 0;
+ display: flex;
+
+ input {
+ display: none;
+ }
+
+ label {
+ color: @control-color;
+ line-height: 1;
+ background: @low-sat-blue;
+ padding: 14/16*.25em 14/16*.5em;
+ font-size: 16/12em;
+ height: 24/16em; // desired pixel height / font-size
+ cursor: pointer;
+
+ &:first-of-type {
+ border-top-left-radius: 0.25em;
+ border-bottom-left-radius: 0.25em;
+ }
+
+ &:last-of-type {
+ border-top-right-radius: 0.25em;
+ border-bottom-right-radius: 0.25em;
+ }
+
+ &:not(:last-of-type) {
+ border-right: 1px solid @low-sat-blue-dark;
+ }
+
+ i {
+ // fix height for Chrome
+ display: block;
+ }
+ }
+
+ input[checked] + label {
+ background-color: @control-color;
+ color: @text-color-on-icinga-blue;
+ cursor: default;
+ }
+}
diff --git a/public/js/action-list.js b/public/js/action-list.js
new file mode 100644
index 0000000..69cab05
--- /dev/null
+++ b/public/js/action-list.js
@@ -0,0 +1,788 @@
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+;(function (Icinga) {
+
+ "use strict";
+
+ try {
+ var notjQuery = require('icinga/icinga-php-library/notjQuery');
+ } catch (e) {
+ console.warn('Unable to provide input enrichments. Libraries not available:', e);
+ return;
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ class ActionList extends Icinga.EventListener {
+ constructor(icinga) {
+ super(icinga);
+
+ this.on('click', '.action-list [data-action-item]:not(.page-separator), .action-list [data-action-item] a[href]', this.onClick, this);
+ this.on('close-column', '#main > #col2', this.onColumnClose, this);
+ this.on('column-moved', this.onColumnMoved, this);
+
+ this.on('rendered', '#main .container', this.onRendered, this);
+ this.on('keydown', '#body', this.onKeyDown, this);
+
+ this.on('click', '.load-more[data-no-icinga-ajax] a', this.onLoadMoreClick, this);
+ this.on('keypress', '.load-more[data-no-icinga-ajax] a', this.onKeyPress, this);
+
+ this.lastActivatedItemUrl = null;
+ this.lastTimeoutId = null;
+ this.isProcessingLoadMore = false;
+ this.activeRequests = {};
+ }
+
+ /**
+ * Parse the filter query contained in the given URL query string
+ *
+ * @param {string} queryString
+ *
+ * @returns {array}
+ */
+ parseSelectionQuery(queryString) {
+ return queryString.split('|');
+ }
+
+ /**
+ * Suspend auto refresh for the given item's container
+ *
+ * @param {Element} item
+ *
+ * @return {string} The container's id
+ */
+ suspendAutoRefresh(item) {
+ const container = item.closest('.container');
+ container.dataset.suspendAutorefresh = '';
+
+ return container.id;
+ }
+
+ /**
+ * Enable auto refresh on the given container
+ *
+ * @param {string} containerId
+ */
+ enableAutoRefresh(containerId) {
+ delete document.getElementById(containerId).dataset.suspendAutorefresh;
+ }
+
+ onClick(event) {
+ let _this = event.data.self;
+ let target = event.currentTarget;
+
+ if (target.matches('a') && (! target.matches('.subject') || event.ctrlKey || event.metaKey)) {
+ return true;
+ }
+
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ event.stopPropagation();
+
+ let item = target.closest('[data-action-item]');
+ let list = target.closest('.action-list');
+ let activeItems = _this.getActiveItems(list);
+ let toActiveItems = [],
+ toDeactivateItems = [];
+
+ const isBeingMultiSelected = list.matches('[data-icinga-multiselect-url]')
+ && (event.ctrlKey || event.metaKey || event.shiftKey);
+
+ if (isBeingMultiSelected) {
+ if (event.ctrlKey || event.metaKey) {
+ if (item.classList.contains('active')) {
+ toDeactivateItems.push(item);
+ } else {
+ toActiveItems.push(item);
+ }
+ } else {
+ document.getSelection().removeAllRanges();
+
+ let allItems = _this.getAllItems(list);
+
+ let startIndex = allItems.indexOf(item);
+ if(startIndex < 0) {
+ startIndex = 0;
+ }
+
+ let endIndex = activeItems.length ? allItems.indexOf(activeItems[0]) : 0;
+ if (startIndex > endIndex) {
+ toActiveItems = allItems.slice(endIndex, startIndex + 1);
+ } else {
+ endIndex = activeItems.length ? allItems.indexOf(activeItems[activeItems.length - 1]) : 0;
+ toActiveItems = allItems.slice(startIndex, endIndex + 1);
+ }
+
+ toDeactivateItems = activeItems.filter(item => ! toActiveItems.includes(item));
+ toActiveItems = toActiveItems.filter(item => ! activeItems.includes(item));
+ }
+ } else {
+ toDeactivateItems = activeItems;
+ toActiveItems.push(item);
+ }
+
+ if (activeItems.length === 1
+ && toActiveItems.length === 0
+ && _this.icinga.loader.getLinkTargetFor($(target)).attr('id') === 'col2'
+ ) {
+ _this.icinga.ui.layout1col();
+ _this.enableAutoRefresh('col1');
+ return;
+ }
+
+ let dashboard = list.closest('.dashboard');
+ if (dashboard) {
+ dashboard.querySelectorAll('.action-list').forEach(otherList => {
+ if (otherList !== list) {
+ toDeactivateItems.push(..._this.getAllItems(otherList));
+ }
+ })
+ }
+
+ let lastActivatedUrl = null;
+ if (toActiveItems.includes(item)) {
+ lastActivatedUrl = item.dataset.icingaDetailFilter;
+ } else if (activeItems.length > 1) {
+ lastActivatedUrl = activeItems[activeItems.length - 1] === item
+ ? activeItems[activeItems.length - 2].dataset.icingaDetailFilter
+ : activeItems[activeItems.length - 1].dataset.icingaDetailFilter;
+ }
+
+ _this.clearSelection(toDeactivateItems);
+ _this.setActive(toActiveItems);
+
+ if (! dashboard) {
+ _this.addSelectionCountToFooter(list);
+ }
+
+ _this.setLastActivatedItemUrl(lastActivatedUrl);
+ _this.loadDetailUrl(list, target.matches('a') ? target.getAttribute('href') : null);
+ }
+
+ /**
+ * Add the selection count to footer if list allow multi selection
+ *
+ * @param list
+ */
+ addSelectionCountToFooter(list) {
+ if (! list.matches('[data-icinga-multiselect-url]')) {
+ return;
+ }
+
+ let activeItemCount = this.getActiveItems(list).length;
+ let footer = list.closest('.container').querySelector('.footer');
+
+ // For items that do not have a bottom status bar like Downtimes, Comments...
+ if (footer === null) {
+ footer = notjQuery.render(
+ '<div class="footer" data-action-list-automatically-added>' +
+ '<div class="selection-count"><span class="selected-items"></span></div>' +
+ '</div>'
+ )
+
+ list.closest('.container').appendChild(footer);
+ }
+
+ let selectionCount = footer.querySelector('.selection-count');
+ if (selectionCount === null) {
+ selectionCount = notjQuery.render(
+ '<div class="selection-count"><span class="selected-items"></span></div>'
+ );
+
+ footer.prepend(selectionCount);
+ }
+
+ let selectedItems = selectionCount.querySelector('.selected-items');
+ selectedItems.innerText = activeItemCount
+ ? list.dataset.icingaMultiselectCountLabel.replace('%d', activeItemCount)
+ : list.dataset.icingaMultiselectHintLabel;
+
+ if (activeItemCount === 0) {
+ selectedItems.classList.add('hint');
+ } else {
+ selectedItems.classList.remove('hint');
+ }
+ }
+
+ /**
+ * Key navigation for .action-list
+ *
+ * - `Shift + ArrowUp|ArrowDown` = Multiselect
+ * - `ArrowUp|ArrowDown` = Select next/previous
+ * - `Ctrl|cmd + A` = Select all on currect page
+ *
+ * @param event
+ */
+ onKeyDown(event) {
+ let _this = event.data.self;
+ let list = null;
+ let pressedArrowDownKey = event.key === 'ArrowDown';
+ let pressedArrowUpKey = event.key === 'ArrowUp';
+ let focusedElement = document.activeElement;
+
+ if (
+ _this.isProcessingLoadMore
+ || ! event.key // input auto-completion is triggered
+ || (event.key.toLowerCase() !== 'a' && ! pressedArrowDownKey && ! pressedArrowUpKey)
+ ) {
+ return;
+ }
+
+ if (focusedElement && (
+ focusedElement.matches('#main > :scope')
+ || focusedElement.matches('#body'))
+ ) {
+ let activeItem = document.querySelector(
+ '#main > .container > .content > .action-list [data-action-item].active'
+ );
+ if (activeItem) {
+ list = activeItem.closest('.action-list');
+ } else {
+ list = focusedElement.querySelector('#main > .container > .content > .action-list');
+ }
+ } else if (focusedElement) {
+ list = focusedElement.closest('.content > .action-list');
+ }
+
+ if (! list) {
+ return;
+ }
+
+ let isMultiSelectableList = list.matches('[data-icinga-multiselect-url]');
+
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
+ if (! isMultiSelectableList) {
+ return;
+ }
+
+ event.preventDefault();
+ _this.selectAll(list);
+ return;
+ }
+
+ event.preventDefault();
+
+ let allItems = _this.getAllItems(list);
+ let firstListItem = allItems[0];
+ let lastListItem = allItems[allItems.length -1];
+ let activeItems = _this.getActiveItems(list);
+ let markAsLastActive = null; // initialized only if it is different from toActiveItem
+ let toActiveItem = null;
+ let wasAllSelected = activeItems.length === allItems.length;
+ let lastActivatedItem = list.querySelector(
+ `[data-icinga-detail-filter="${ _this.lastActivatedItemUrl }"]`
+ );
+
+ if (! lastActivatedItem && activeItems.length) {
+ lastActivatedItem = activeItems[activeItems.length - 1];
+ }
+
+ let directionalNextItem = _this.getDirectionalNext(lastActivatedItem, event.key);
+
+ if (activeItems.length === 0) {
+ toActiveItem = pressedArrowDownKey ? firstListItem : lastListItem;
+ // reset all on manual page refresh
+ _this.clearSelection(activeItems);
+ if (toActiveItem.classList.contains('load-more')) {
+ toActiveItem = toActiveItem.previousElementSibling;
+ }
+ } else if (isMultiSelectableList && event.shiftKey) {
+ if (activeItems.length === 1) {
+ toActiveItem = directionalNextItem;
+ } else if (wasAllSelected && (
+ (lastActivatedItem !== firstListItem && pressedArrowDownKey)
+ || (lastActivatedItem !== lastListItem && pressedArrowUpKey)
+ )) {
+ if (pressedArrowDownKey) {
+ toActiveItem = lastActivatedItem === lastListItem ? null : lastListItem;
+ } else {
+ toActiveItem = lastActivatedItem === firstListItem ? null : lastListItem;
+ }
+
+ } else if (directionalNextItem && directionalNextItem.classList.contains('active')) {
+ // deactivate last activated by down to up select
+ _this.clearSelection([lastActivatedItem]);
+ if (wasAllSelected) {
+ _this.scrollItemIntoView(lastActivatedItem, event.key);
+ }
+
+ toActiveItem = directionalNextItem;
+ } else {
+ [toActiveItem, markAsLastActive] = _this.findToActiveItem(lastActivatedItem, event.key);
+ }
+ } else {
+ toActiveItem = directionalNextItem ?? lastActivatedItem;
+
+ if (toActiveItem) {
+ if (toActiveItem.classList.contains('load-more')) {
+ clearTimeout(_this.lastTimeoutId);
+ _this.handleLoadMoreNavigate(toActiveItem, lastActivatedItem, event.key);
+ return;
+ }
+
+ _this.clearSelection(activeItems);
+ if (toActiveItem.classList.contains('page-separator')) {
+ toActiveItem = _this.getDirectionalNext(toActiveItem, event.key);
+ }
+ }
+ }
+
+ if (! toActiveItem) {
+ return;
+ }
+
+ _this.setActive(toActiveItem);
+ _this.setLastActivatedItemUrl(
+ markAsLastActive ? markAsLastActive.dataset.icingaDetailFilter : toActiveItem.dataset.icingaDetailFilter
+ );
+ _this.scrollItemIntoView(toActiveItem, event.key);
+ _this.addSelectionCountToFooter(list);
+ _this.loadDetailUrl(list);
+ }
+
+ /**
+ * Get the next list item according to the pressed key (`ArrowUp` or `ArrowDown`)
+ *
+ * @param item The list item from which we want the next item
+ * @param eventKey Pressed key (`ArrowUp` or `ArrowDown`)
+ *
+ * @returns {Element|null}
+ */
+ getDirectionalNext(item, eventKey) {
+ if (! item) {
+ return null;
+ }
+
+ return eventKey === 'ArrowUp' ? item.previousElementSibling : item.nextElementSibling;
+ }
+
+ /**
+ * Find the list item that should be activated next
+ *
+ * @param lastActivatedItem
+ * @param eventKey Pressed key (`ArrowUp` or `ArrowDown`)
+ *
+ * @returns {Element[]}
+ */
+ findToActiveItem(lastActivatedItem, eventKey) {
+ let toActiveItem;
+ let markAsLastActive;
+
+ toActiveItem = this.getDirectionalNext(lastActivatedItem, eventKey);
+
+ while (toActiveItem) {
+ if (! toActiveItem.classList.contains('active')) {
+ break;
+ }
+
+ toActiveItem = this.getDirectionalNext(toActiveItem, eventKey);
+ }
+
+ markAsLastActive = toActiveItem;
+ // if the next/previous sibling element is already active,
+ // mark the last/first active element in list as last active
+ while (markAsLastActive && this.getDirectionalNext(markAsLastActive, eventKey)) {
+ if (! this.getDirectionalNext(markAsLastActive, eventKey).classList.contains('active')) {
+ break;
+ }
+
+ markAsLastActive = this.getDirectionalNext(markAsLastActive, eventKey);
+ }
+
+ return [toActiveItem, markAsLastActive];
+ }
+
+ /**
+ * Select All list items
+ *
+ * @param list The action list
+ */
+ selectAll(list) {
+ let allItems = this.getAllItems(list);
+ let activeItems = this.getActiveItems(list);
+ this.setActive(allItems.filter(item => ! activeItems.includes(item)));
+ this.setLastActivatedItemUrl(allItems[allItems.length -1].dataset.icingaDetailFilter);
+ this.addSelectionCountToFooter(list);
+ this.loadDetailUrl(list);
+ }
+
+ /**
+ * Clear the selection by removing .active class
+ *
+ * @param selectedItems The items with class active
+ */
+ clearSelection(selectedItems) {
+ selectedItems.forEach(item => item.classList.remove('active'));
+ }
+
+ /**
+ * Set the last activated item Url
+ *
+ * @param url
+ */
+ setLastActivatedItemUrl (url) {
+ this.lastActivatedItemUrl = url;
+ }
+
+ /**
+ * Scroll the given item into view
+ *
+ * @param item Item to scroll into view
+ * @param pressedKey Pressed key (`ArrowUp` or `ArrowDown`)
+ */
+ scrollItemIntoView(item, pressedKey) {
+ item.scrollIntoView({block: "nearest"});
+ let directionalNext = this.getDirectionalNext(item, pressedKey);
+
+ if (directionalNext) {
+ directionalNext.scrollIntoView({block: "nearest"});
+ }
+ }
+
+ /**
+ * Load the detail url with selected items
+ *
+ * @param list The action list
+ * @param anchorUrl If any anchor is clicked (e.g. host in service list)
+ */
+ loadDetailUrl(list, anchorUrl = null) {
+ let url = anchorUrl;
+ let activeItems = this.getActiveItems(list);
+
+ if (url === null) {
+ if (activeItems.length > 1) {
+ url = this.createMultiSelectUrl(activeItems);
+ } else {
+ let anchor = activeItems[0].querySelector('[href]');
+ url = anchor ? anchor.getAttribute('href') : null;
+ }
+ }
+
+ if (url === null) {
+ return;
+ }
+
+ const suspendedContainer = this.suspendAutoRefresh(list);
+
+ clearTimeout(this.lastTimeoutId);
+ this.lastTimeoutId = setTimeout(() => {
+ const requestNo = this.lastTimeoutId;
+ this.activeRequests[requestNo] = suspendedContainer;
+ this.lastTimeoutId = null;
+
+ let req = this.icinga.loader.loadUrl(
+ url,
+ this.icinga.loader.getLinkTargetFor($(activeItems[0]))
+ );
+
+ req.always((_, __, errorThrown) => {
+ if (errorThrown !== 'abort') {
+ this.enableAutoRefresh(this.activeRequests[requestNo]);
+ }
+
+ delete this.activeRequests[requestNo];
+ });
+ }, 250);
+ }
+
+ /**
+ * Add .active class to given list item
+ *
+ * @param toActiveItem The list item(s)
+ */
+ setActive(toActiveItem) {
+ if (toActiveItem instanceof HTMLElement) {
+ toActiveItem.classList.add('active');
+ } else {
+ toActiveItem.forEach(item => item.classList.add('active'));
+ }
+ }
+
+ /**
+ * Get the active items from given list
+ *
+ * @param list The action list
+ *
+ * @return array
+ */
+ getActiveItems(list)
+ {
+ let items;
+ if (list.tagName.toLowerCase() === 'table') {
+ items = list.querySelectorAll(':scope > tbody > [data-action-item].active');
+ } else {
+ items = list.querySelectorAll(':scope > [data-action-item].active');
+ }
+
+ return Array.from(items);
+ }
+
+ /**
+ * Get all available items from given list
+ *
+ * @param list The action list
+ *
+ * @return array
+ */
+ getAllItems(list)
+ {
+ let items;
+ if (list.tagName.toLowerCase() === 'table') {
+ items = list.querySelectorAll(':scope > tbody > [data-action-item]');
+ } else {
+ items = list.querySelectorAll(':scope > [data-action-item]');
+ }
+
+ return Array.from(items);
+ }
+
+ /**
+ * Handle the navigation on load-more button
+ *
+ * @param loadMoreElement
+ * @param lastActivatedItem
+ * @param pressedKey Pressed key (`ArrowUp` or `ArrowDown`)
+ */
+ handleLoadMoreNavigate(loadMoreElement, lastActivatedItem, pressedKey) {
+ let req = this.loadMore(loadMoreElement.firstChild);
+ this.isProcessingLoadMore = true;
+ req.done(() => {
+ this.isProcessingLoadMore = false;
+ // list has now new items, so select the lastActivatedItem and then move forward
+ let toActiveItem = lastActivatedItem.nextElementSibling;
+ while (toActiveItem) {
+ if (toActiveItem.hasAttribute('data-action-item')) {
+ this.clearSelection([lastActivatedItem]);
+ this.setActive(toActiveItem);
+ this.setLastActivatedItemUrl(toActiveItem.dataset.icingaDetailFilter);
+ this.scrollItemIntoView(toActiveItem, pressedKey);
+ this.addSelectionCountToFooter(toActiveItem.parentElement);
+ this.loadDetailUrl(toActiveItem.parentElement);
+ return;
+ }
+
+ toActiveItem = toActiveItem.nextElementSibling;
+ }
+ });
+ }
+
+ /**
+ * Click on load-more button
+ *
+ * @param event
+ *
+ * @returns {boolean}
+ */
+ onLoadMoreClick(event) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ event.data.self.loadMore(event.target);
+
+ return false;
+ }
+
+ onKeyPress(event) {
+ if (event.key === ' ') { // space
+ event.data.self.onLoadMoreClick(event);
+ }
+ }
+
+ /**
+ * Load more list items based on the given anchor
+ *
+ * @param anchor
+ *
+ * @returns {*|{getAllResponseHeaders: function(): *|null, abort: function(*): this, setRequestHeader: function(*, *): this, readyState: number, getResponseHeader: function(*): null|*, overrideMimeType: function(*): this, statusCode: function(*): this}|jQuery|boolean}
+ */
+ loadMore(anchor) {
+ let showMore = anchor.parentElement;
+ var progressTimer = this.icinga.timer.register(function () {
+ var label = anchor.innerText;
+
+ var dots = label.substr(-3);
+ if (dots.slice(0, 1) !== '.') {
+ dots = '. ';
+ } else {
+ label = label.slice(0, -3);
+ if (dots === '...') {
+ dots = '. ';
+ } else if (dots === '.. ') {
+ dots = '...';
+ } else if (dots === '. ') {
+ dots = '.. ';
+ }
+ }
+
+ anchor.innerText = label + dots;
+ }, null, 250);
+
+ let url = anchor.getAttribute('href');
+ let req = this.icinga.loader.loadUrl(
+ // Add showCompact, we don't want controls in paged results
+ this.icinga.utils.addUrlFlag(url, 'showCompact'),
+ $(showMore.parentElement),
+ undefined,
+ undefined,
+ 'append',
+ false,
+ progressTimer
+ );
+ req.addToHistory = false;
+ req.done(function () {
+ showMore.remove();
+
+ // Set data-icinga-url to make it available for Icinga.History.getCurrentState()
+ req.$target.closest('.container').data('icingaUrl', url);
+
+ this.icinga.history.replaceCurrentState();
+ });
+
+ return req;
+ }
+
+ /**
+ * Create the detail url for multi selectable list
+ *
+ * @param items List items
+ * @param withBaseUrl Default to true
+ *
+ * @returns {string} The url
+ */
+ createMultiSelectUrl(items, withBaseUrl = true) {
+ let filters = [];
+ items.forEach(item => {
+ filters.push(item.getAttribute('data-icinga-multiselect-filter'));
+ });
+
+ let url = '?' + filters.join('|');
+
+ if (withBaseUrl) {
+ return items[0].closest('.action-list').getAttribute('data-icinga-multiselect-url') + url;
+ }
+
+ return url;
+ }
+
+ onColumnClose(event) {
+ let _this = event.data.self;
+ let list = _this.findDetailUrlActionList(document.getElementById('col1'));
+ if (list && list.matches('[data-icinga-multiselect-url], [data-icinga-detail-url]')) {
+ _this.clearSelection(_this.getActiveItems(list));
+ _this.addSelectionCountToFooter(list);
+ }
+ }
+
+ /**
+ * Find the action list using the detail url
+ *
+ * @param {Element} container
+ *
+ * @return Element|null
+ */
+ findDetailUrlActionList(container) {
+ let detailUrl = this.icinga.utils.parseUrl(
+ this.icinga.history.getCol2State().replace(/^#!/, '')
+ );
+
+ let detailItem = container.querySelector(
+ '[data-icinga-detail-filter="'
+ + detailUrl.query.replace('?', '') + '"],' +
+ '[data-icinga-multiselect-filter="'
+ + detailUrl.query.split('|', 1).toString().replace('?', '') + '"]'
+ );
+
+ return detailItem ? detailItem.parentElement : null;
+ }
+
+ /**
+ * Triggers when column is moved to left or right
+ *
+ * @param event
+ * @param sourceId The content is moved from
+ */
+ onColumnMoved(event, sourceId) {
+ let _this = event.data.self;
+
+ if (event.target.id === 'col2' && sourceId === 'col1') { // only for browser-back (col1 shifted to col2)
+ _this.clearSelection(event.target.querySelectorAll('.action-list .active'));
+ } else if (event.target.id === 'col1' && sourceId === 'col2') {
+ for (const requestNo of Object.keys(_this.activeRequests)) {
+ if (_this.activeRequests[requestNo] === sourceId) {
+ _this.enableAutoRefresh(_this.activeRequests[requestNo]);
+ _this.activeRequests[requestNo] = _this.suspendAutoRefresh(event.target);
+ }
+ }
+ }
+ }
+
+ onRendered(event, isAutoRefresh) {
+ let _this = event.data.self;
+ let container = event.target;
+ let isTopLevelContainer = container.matches('#main > :scope');
+
+ let list;
+ if (event.currentTarget !== container || Object.keys(_this.activeRequests).length) {
+ // Nested containers are not processed multiple times || still processing selection/navigation request
+ return;
+ } else if (isTopLevelContainer && container.id !== 'col1') {
+ if (isAutoRefresh) {
+ return;
+ }
+
+ // only for browser back/forward navigation
+ list = _this.findDetailUrlActionList(document.getElementById('col1'));
+ } else {
+ list = _this.findDetailUrlActionList(container);
+ }
+
+ if (list && list.matches('[data-icinga-multiselect-url], [data-icinga-detail-url]')) {
+ let detailUrl = _this.icinga.utils.parseUrl(
+ _this.icinga.history.getCol2State().replace(/^#!/, '')
+ );
+ let toActiveItems = [];
+ if (list.dataset.icingaMultiselectUrl === detailUrl.path) {
+ for (const filter of _this.parseSelectionQuery(detailUrl.query.slice(1))) {
+ let item = list.querySelector(
+ '[data-icinga-multiselect-filter="' + filter + '"]'
+ );
+
+ if (item) {
+ toActiveItems.push(item);
+ }
+ }
+ } else if (_this.matchesDetailUrl(list.dataset.icingaDetailUrl, detailUrl.path)) {
+ let item = list.querySelector(
+ '[data-icinga-detail-filter="' + detailUrl.query.slice(1) + '"]'
+ );
+
+ if (item) {
+ toActiveItems.push(item);
+ }
+ }
+
+ _this.clearSelection(_this.getAllItems(list).filter(item => !toActiveItems.includes(item)));
+ _this.setActive(toActiveItems);
+ }
+
+ if (isTopLevelContainer) {
+ let footerList = list ?? container.querySelector('.content > .action-list');
+ if (footerList) {
+ _this.addSelectionCountToFooter(footerList);
+ }
+ }
+ }
+
+ matchesDetailUrl(itemUrl, detailUrl) {
+ if (itemUrl === detailUrl) {
+ return true;
+ }
+
+ // The slash is used to avoid false positives (e.g. icingadb/hostgroup and icingadb/host)
+ return detailUrl.startsWith(itemUrl + '/');
+ }
+ }
+
+ Icinga.Behaviors.ActionList = ActionList;
+
+}(Icinga));
diff --git a/public/js/migrate.js b/public/js/migrate.js
new file mode 100644
index 0000000..dddbaed
--- /dev/null
+++ b/public/js/migrate.js
@@ -0,0 +1,654 @@
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+;(function(Icinga, $) {
+
+ 'use strict';
+
+ const ANIMATION_LENGTH = 350;
+
+ const POPUP_HTML = '<div class="icinga-module module-icingadb">\n' +
+ ' <div id="migrate-popup">\n' +
+ ' <div class="suggestion-area">\n' +
+ ' <button type="button" class="close">Don\'t show this again</button>\n' +
+ ' <ul class="search-migration-suggestions"></ul>\n' +
+ ' <p class="search-migration-hint">Miss some results? Try the link(s) below</p>\n' +
+ ' <ul class="monitoring-migration-suggestions"></ul>\n' +
+ ' <p class="monitoring-migration-hint">Preview this in Icinga DB</p>\n' +
+ ' </div>\n' +
+ ' <div class="minimizer"><i class="icon-"></i></div>\n' +
+ ' </div>\n' +
+ '</div>';
+
+ const SUGGESTION_HTML = '<li>\n' +
+ ' <button type="button" value="1"></button>\n' +
+ ' <button type="button" value="0"><i class="icon-"></i></button>\n' +
+ '</li>';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Icinga DB Migration behavior.
+ *
+ * @param icinga {Icinga} The current Icinga Object
+ */
+ class Migrate extends Icinga.EventListener {
+ constructor(icinga) {
+ super(icinga);
+
+ this.knownMigrations = {};
+ this.knownBackendSupport = {};
+ this.urlMigrationReadyState = null;
+ this.backendSupportReadyState = null;
+ this.searchMigrationReadyState = null;
+ this.backendSupportRelated = {};
+ this.$popup = null;
+
+ // Some persistence, we don't want to annoy our users too much
+ this.storage = Icinga.Storage.BehaviorStorage('icingadb.migrate');
+ this.tempStorage = Icinga.Storage.BehaviorStorage('icingadb.migrate');
+ this.tempStorage.setBackend(window.sessionStorage);
+ this.previousMigrations = {};
+
+ // We don't want to ask the server to migrate non-monitoring urls
+ this.isMonitoringUrl = new RegExp('^' + icinga.config.baseUrl + '/monitoring/');
+
+ this.on('rendered', this.onRendered, this);
+ this.on('close-column', this.onColumnClose, this);
+ this.on('click', '#migrate-popup button.close', this.onClose, this);
+ this.on('click', '#migrate-popup li button', this.onDecision, this);
+ this.on('click', '#migrate-popup .minimizer', this.onHandleClicked, this);
+ this.storage.onChange('minimized', this.onMinimized, this);
+ }
+
+ update(data) {
+ if (data !== 'bogus') {
+ return;
+ }
+
+ $.each(this.backendSupportRelated, (id, _) => {
+ let $container = $('#' + id);
+ let req = this.icinga.loader.loadUrl($container.data('icingaUrl'), $container);
+ req.addToHistory = false;
+ req.scripted = true;
+ });
+ }
+
+ onRendered(event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+
+ if (_this.tempStorage.get('closed') || $('#layout.fullscreen-layout').length) {
+ // Don't bother in case the user closed the popup or we're in fullscreen
+ return;
+ }
+
+ if (!$target.is('#main > .container')) {
+ if ($target.is('#main .container')) {
+ var attrUrl = $target.attr('data-icinga-url');
+ var dataUrl = $target.data('icingaUrl');
+ if (!! attrUrl && attrUrl !== dataUrl) {
+ // Search urls are redirected, update any migration suggestions
+ _this.prepareMigration($target);
+ return;
+ }
+ }
+
+ // We are else really only interested in top-level containers
+ return;
+ }
+
+ var $dashboard = $target.children('.dashboard');
+ if ($dashboard.length) {
+ // After a page load dashlets have no id as `renderContentToContainer()` didn't ran yet
+ _this.icinga.ui.assignUniqueContainerIds();
+
+ $target = $dashboard.children('.container');
+ }
+
+ _this.prepareMigration($target);
+ }
+
+ prepareMigration($target) {
+ let monitoringUrls = {};
+ let searchUrls = {};
+ let modules = {}
+
+ $target.each((_, container) => {
+ let $container = $(container);
+ let href = decodeURIComponent($container.data('icingaUrl'));
+ let containerId = $container.attr('id');
+
+ if (!!href) {
+ if (
+ typeof this.previousMigrations[containerId] !== 'undefined'
+ && this.previousMigrations[containerId] === href
+ ) {
+ delete this.previousMigrations[containerId];
+ } else {
+ if (href.match(this.isMonitoringUrl)) {
+ monitoringUrls[containerId] = href;
+ } else if ($container.find('[data-enrichment-type="search-bar"]').length) {
+ searchUrls[containerId] = href;
+ }
+ }
+ }
+
+ let moduleName = $container.data('icingaModule');
+ if (!! moduleName && moduleName !== 'default' && moduleName !== 'monitoring' && moduleName !== 'icingadb') {
+ modules[containerId] = moduleName;
+ }
+ });
+
+ if (Object.keys(monitoringUrls).length) {
+ this.setUrlMigrationReadyState(false);
+ this.migrateUrls(monitoringUrls, 'monitoring');
+ } else {
+ this.setUrlMigrationReadyState(null);
+ }
+
+ if (Object.keys(searchUrls).length) {
+ this.setSearchMigrationReadyState(false);
+ this.migrateUrls(searchUrls, 'search');
+ } else {
+ this.setSearchMigrationReadyState(null);
+ }
+
+ if (Object.keys(modules).length) {
+ this.setBackendSupportReadyState(false);
+ this.prepareBackendCheckboxForm(modules);
+ } else {
+ this.setBackendSupportReadyState(null);
+ }
+
+ if (
+ this.urlMigrationReadyState === null
+ && this.backendSupportReadyState === null
+ && this.searchMigrationReadyState === null
+ ) {
+ this.cleanupPopup();
+ }
+ }
+
+ onColumnClose(event) {
+ var _this = event.data.self;
+ _this.Popup().find('.suggestion-area > ul li').each(function () {
+ var $suggestion = $(this);
+ var suggestionUrl = $suggestion.data('containerUrl');
+ var $container = $('#' + $suggestion.data('containerId'));
+
+ var containerUrl = '';
+ if ($container.length) {
+ containerUrl = decodeURIComponent($container.data('icingaUrl'));
+ }
+
+ if (suggestionUrl !== containerUrl) {
+ var $newContainer = $('#main > .container').filter(function () {
+ return decodeURIComponent($(this).data('icingaUrl')) === suggestionUrl;
+ });
+ if ($newContainer.length) {
+ // Container moved
+ $suggestion.attr('id', 'suggest-' + $newContainer.attr('id'));
+ $suggestion.data('containerId', $newContainer.attr('id'));
+ }
+ }
+ });
+
+ let backendSupportRelated = { ..._this.backendSupportRelated };
+ $.each(backendSupportRelated, (id, module) => {
+ let $container = $('#' + id);
+ if (! $container.length || $container.data('icingaModule') !== module) {
+ let $newContainer = $('#main > .container').filter(function () {
+ return $(this).data('icingaModule') === module;
+ });
+ if ($newContainer.length) {
+ _this.backendSupportRelated[$newContainer.attr('id')] = module;
+ }
+
+ delete _this.backendSupportRelated[id];
+ }
+ });
+
+ _this.cleanupPopup();
+ }
+
+ onClose(event) {
+ var _this = event.data.self;
+ _this.tempStorage.set('closed', true);
+ _this.hidePopup();
+ }
+
+ onDecision(event) {
+ var _this = event.data.self;
+ var $button = $(event.target).closest('button');
+ var $suggestion = $button.parent();
+ var $container = $('#' + $suggestion.data('containerId'));
+ var containerUrl = decodeURIComponent($container.data('icingaUrl'));
+
+ if ($button.attr('value') === '1') {
+ // Yes
+ var newHref = _this.knownMigrations[containerUrl];
+ _this.icinga.loader.loadUrl(newHref, $container);
+
+ _this.previousMigrations[$suggestion.data('containerId')] = containerUrl;
+
+ if ($container.parent().is('.dashboard')) {
+ $container.find('h1 a').attr('href', _this.icinga.utils.removeUrlParams(newHref, ['showCompact']));
+ }
+ } else {
+ // No
+ _this.knownMigrations[containerUrl] = false;
+ }
+
+ if (_this.Popup().find('li').length === 1 && ! _this.Popup().find('#setAsBackendForm').length) {
+ _this.hidePopup(function () {
+ // Let the transition finish first, looks cleaner
+ $suggestion.remove();
+ });
+ } else {
+ $suggestion.remove();
+ }
+ }
+
+ onHandleClicked(event) {
+ var _this = event.data.self;
+ if (_this.togglePopup()) {
+ _this.storage.set('minimized', true);
+ } else {
+ _this.storage.remove('minimized');
+ }
+ }
+
+ onMinimized(isMinimized, oldValue) {
+ if (isMinimized && isMinimized !== oldValue && this.isShown()) {
+ this.minimizePopup();
+ }
+ }
+
+ migrateUrls(urls, type) {
+ var _this = this,
+ containerIds = [],
+ containerUrls = [];
+
+ $.each(urls, function (containerId, containerUrl) {
+ if (typeof _this.knownMigrations[containerUrl] === 'undefined') {
+ containerUrls.push(containerUrl);
+ containerIds.push(containerId);
+ }
+ });
+
+ let endpoint, changeCallback;
+ if (type === 'monitoring') {
+ endpoint = 'monitoring-url';
+ changeCallback = this.changeUrlMigrationReadyState.bind(this);
+ } else {
+ endpoint = 'search-url';
+ changeCallback = this.changeSearchMigrationReadyState.bind(this);
+ }
+
+ if (containerUrls.length) {
+ var req = $.ajax({
+ context: this,
+ type: 'post',
+ url: this.icinga.config.baseUrl + '/icingadb/migrate/' + endpoint,
+ headers: {'Accept': 'application/json'},
+ contentType: 'application/json',
+ data: JSON.stringify(containerUrls)
+ });
+
+ req.urls = urls;
+ req.suggestionType = type;
+ req.urlIndexToContainerId = containerIds;
+ req.done(this.processUrlMigrationResults);
+ req.always(() => changeCallback(true));
+ } else {
+ // All urls have already been migrated once, show popup immediately
+ this.addSuggestions(urls, type);
+ changeCallback(true);
+ }
+ }
+
+ processUrlMigrationResults(data, textStatus, req) {
+ var _this = this;
+ var result, containerId;
+
+ if (data.status === 'success') {
+ result = data.data;
+ } else { // if (data.status === 'fail')
+ result = data.data.result;
+
+ $.each(data.data.errors, function (k, error) {
+ _this.icinga.logger.error('[Migrate] Erroneous url "' + k + '": ' + error[0] + '\n' + error[1]);
+ });
+ }
+
+ $.each(result, function (i, migratedUrl) {
+ containerId = req.urlIndexToContainerId[i];
+ _this.knownMigrations[req.urls[containerId]] = migratedUrl;
+ });
+
+ this.addSuggestions(req.urls, req.suggestionType);
+ }
+
+ prepareBackendCheckboxForm(modules) {
+ let containerIds = [];
+ let moduleNames = [];
+
+ $.each(modules, (id, module) => {
+ if (typeof this.knownBackendSupport[module] === 'undefined') {
+ containerIds.push(id);
+ moduleNames.push(module);
+ }
+ });
+
+ if (moduleNames.length) {
+ let req = $.ajax({
+ context : this,
+ type : 'post',
+ url : this.icinga.config.baseUrl + '/icingadb/migrate/backend-support',
+ headers : { 'Accept': 'application/json' },
+ contentType : 'application/json',
+ data : JSON.stringify(moduleNames)
+ });
+
+ req.modules = modules;
+ req.moduleIndexToContainerId = containerIds;
+ req.done(this.processBackendSupportResults);
+ req.always(() => this.changeBackendSupportReadyState(true));
+ } else {
+ // All modules have already been checked once, show popup immediately
+ this.setupBackendCheckboxForm(modules);
+ this.changeBackendSupportReadyState(true);
+ }
+ }
+
+ processBackendSupportResults(data, textStatus, req) {
+ let result = data.data;
+
+ $.each(result, (i, state) => {
+ let containerId = req.moduleIndexToContainerId[i];
+ this.knownBackendSupport[req.modules[containerId]] = state;
+ });
+
+ this.setupBackendCheckboxForm(req.modules);
+ }
+
+ setupBackendCheckboxForm(modules) {
+ let supportedModules = {};
+
+ $.each(modules, (id, module) => {
+ if (this.knownBackendSupport[module]) {
+ supportedModules[id] = module;
+ }
+ });
+
+ if (Object.keys(supportedModules).length) {
+ this.backendSupportRelated = { ...this.backendSupportRelated, ...supportedModules };
+
+ let req = $.ajax({
+ context : this,
+ type : 'get',
+ url : this.icinga.config.baseUrl + '/icingadb/migrate/checkbox-state?showCompact'
+ });
+
+ req.done(this.setCheckboxState);
+ }
+ }
+
+ setCheckboxState(html, textStatus, req) {
+ let $form = this.Popup().find('.suggestion-area > #setAsBackendForm');
+ if (! $form.length) {
+ $form = $(html);
+ $form.attr('data-base-target', 'migrate-popup-backend-submit-blackhole');
+ $form.append('<div id="migrate-popup-backend-submit-blackhole"></div>');
+
+ this.Popup().find('.monitoring-migration-suggestions').before($form);
+ } else {
+ let $newForm = $(html);
+ $form.find('[name=backend]').prop('checked', $newForm.find('[name=backend]').is(':checked'));
+ }
+
+ this.showPopup();
+ }
+
+ addSuggestions(urls, type) {
+ var where;
+ if (type === 'monitoring') {
+ where = '.monitoring-migration-suggestions';
+ } else {
+ where = '.search-migration-suggestions';
+ }
+
+ var _this = this,
+ hasSuggestions = false,
+ $ul = this.Popup().find('.suggestion-area > ul' + where);
+ $.each(urls, function (containerId, containerUrl) {
+ // No urls for which the user clicked "No" or an error occurred and only migrated urls please
+ if (_this.knownMigrations[containerUrl] !== false && _this.knownMigrations[containerUrl] !== containerUrl) {
+ var $container = $('#' + containerId);
+
+ var $suggestion = $ul.find('li#suggest-' + containerId);
+ if ($suggestion.length) {
+ if ($suggestion.data('containerUrl') === containerUrl) {
+ // There's already a suggestion for this exact container and url
+ hasSuggestions = true;
+ return;
+ }
+
+ $suggestion.data('containerUrl', containerUrl);
+ } else {
+ $suggestion = $(SUGGESTION_HTML);
+ $suggestion.attr('id', 'suggest-' + containerId);
+ $suggestion.data('containerId', containerId);
+ $suggestion.data('containerUrl', containerUrl);
+ $ul.append($suggestion);
+ }
+
+ hasSuggestions = true;
+
+ var title;
+ if ($container.data('icingaTitle')) {
+ title = $container.data('icingaTitle').split(' :: ').slice(0, -1).join(' :: ');
+ } else if ($container.parent().is('.dashboard')) {
+ title = $container.find('h1 a').text();
+ } else {
+ title = $container.find('.tabs li.active a').text();
+ }
+
+ $suggestion.find('button:first-of-type').text(title);
+ }
+ });
+
+ if (hasSuggestions) {
+ this.showPopup();
+ if (type === 'search') {
+ this.maximizePopup();
+ }
+ }
+ }
+
+ cleanupSuggestions() {
+ var _this = this,
+ toBeRemoved = [];
+ this.Popup().find('li').each(function () {
+ var $suggestion = $(this);
+ var $container = $('#' + $suggestion.data('containerId'));
+ var containerUrl = decodeURIComponent($container.data('icingaUrl'));
+ if (
+ // Unknown url, yet
+ typeof _this.knownMigrations[containerUrl] === 'undefined'
+ // User doesn't want to migrate
+ || _this.knownMigrations[containerUrl] === false
+ // Already migrated or no migration necessary
+ || containerUrl === _this.knownMigrations[containerUrl]
+ // The container URL changed
+ || containerUrl !== $suggestion.data('containerUrl')
+ ) {
+ toBeRemoved.push($suggestion);
+ }
+ });
+
+ return toBeRemoved;
+ }
+
+ cleanupBackendForm() {
+ let $form = this.Popup().find('#setAsBackendForm');
+ if (! $form.length) {
+ return false;
+ }
+
+ let stillRelated = {};
+ $.each(this.backendSupportRelated, (id, module) => {
+ let $container = $('#' + id);
+ if ($container.length && $container.data('icingaModule') === module) {
+ stillRelated[id] = module;
+ }
+ });
+
+ this.backendSupportRelated = stillRelated;
+
+ if (Object.keys(stillRelated).length) {
+ return true;
+ }
+
+ return $form;
+ }
+
+ cleanupPopup() {
+ let toBeRemoved = this.cleanupSuggestions();
+ let hasBackendForm = this.cleanupBackendForm();
+
+ if (hasBackendForm !== true && this.Popup().find('li').length === toBeRemoved.length) {
+ this.hidePopup(() => {
+ // Let the transition finish first, looks cleaner
+ $.each(toBeRemoved, function (_, $suggestion) {
+ $suggestion.remove();
+ });
+
+ if (typeof hasBackendForm === 'object') {
+ hasBackendForm.remove();
+ }
+ });
+ } else {
+ $.each(toBeRemoved, function (_, $suggestion) {
+ $suggestion.remove();
+ });
+
+ if (typeof hasBackendForm === 'object') {
+ hasBackendForm.remove();
+ }
+
+ // Let showPopup() handle the automatic minimization in case all search suggestions have been removed
+ this.showPopup();
+ }
+ }
+
+ showPopup() {
+ var $popup = this.Popup();
+ if (this.storage.get('minimized') && ! this.forceFullyMaximized()) {
+ if (this.isShown()) {
+ this.minimizePopup();
+ } else {
+ $popup.addClass('active minimized hidden');
+ }
+ } else {
+ $popup.addClass('active');
+ }
+ }
+
+ hidePopup(after) {
+ this.Popup().removeClass('active minimized hidden');
+
+ if (typeof after === 'function') {
+ setTimeout(after, ANIMATION_LENGTH);
+ }
+ }
+
+ isShown() {
+ return this.Popup().is('.active');
+ }
+
+ minimizePopup() {
+ var $popup = this.Popup();
+ $popup.addClass('minimized');
+ setTimeout(function () {
+ $popup.addClass('hidden');
+ }, ANIMATION_LENGTH);
+ }
+
+ maximizePopup() {
+ this.Popup().removeClass('minimized hidden');
+ }
+
+ forceFullyMaximized() {
+ return this.Popup().find('.search-migration-suggestions:not(:empty)').length > 0;
+ }
+
+ togglePopup() {
+ if (this.Popup().is('.minimized')) {
+ this.maximizePopup();
+ return false;
+ } else {
+ this.minimizePopup();
+ return true;
+ }
+ }
+
+ setUrlMigrationReadyState(state) {
+ this.urlMigrationReadyState = state;
+ }
+
+ changeUrlMigrationReadyState(state) {
+ this.setUrlMigrationReadyState(state);
+
+ if (this.backendSupportReadyState !== false && this.searchMigrationReadyState !== false) {
+ this.searchMigrationReadyState = null;
+ this.backendSupportReadyState = null;
+ this.urlMigrationReadyState = null;
+ this.cleanupPopup();
+ }
+ }
+
+ setSearchMigrationReadyState(state) {
+ this.searchMigrationReadyState = state;
+ }
+
+ changeSearchMigrationReadyState(state) {
+ this.setSearchMigrationReadyState(state);
+
+ if (this.backendSupportReadyState !== false && this.urlMigrationReadyState !== false) {
+ this.searchMigrationReadyState = null;
+ this.backendSupportReadyState = null;
+ this.urlMigrationReadyState = null;
+ this.cleanupPopup();
+ }
+ }
+
+ setBackendSupportReadyState(state) {
+ this.backendSupportReadyState = state;
+ }
+
+ changeBackendSupportReadyState(state) {
+ this.setBackendSupportReadyState(state);
+
+ if (this.urlMigrationReadyState !== false && this.searchMigrationReadyState !== false) {
+ this.searchMigrationReadyState = null;
+ this.backendSupportReadyState = null;
+ this.urlMigrationReadyState = null;
+ this.cleanupPopup();
+ }
+ }
+
+ Popup() {
+ // Node.contains() is used due to `?renderLayout`
+ if (this.$popup === null || ! document.body.contains(this.$popup[0])) {
+ $('#layout').append($(POPUP_HTML));
+ this.$popup = $('#migrate-popup');
+ }
+
+ return this.$popup;
+ }
+ }
+
+ Icinga.Behaviors.Migrate = Migrate;
+
+})(Icinga, jQuery);
diff --git a/public/js/progress-bar.js b/public/js/progress-bar.js
new file mode 100644
index 0000000..be24f1c
--- /dev/null
+++ b/public/js/progress-bar.js
@@ -0,0 +1,110 @@
+(function (Icinga) {
+
+ "use strict";
+
+ class ProgressBar extends Icinga.EventListener {
+ constructor(icinga)
+ {
+ super(icinga);
+
+ /**
+ * Frame update threshold. If it reaches zero, the view is updated
+ *
+ * Currently, only every third frame is updated.
+ *
+ * @type {number}
+ */
+ this.frameUpdateThreshold = 3;
+
+ /**
+ * Threshold at which animations get smoothed out (in milliseconds)
+ *
+ * @type {number}
+ */
+ this.smoothUpdateThreshold = 250;
+
+ this.on('rendered', '#main > .container', this.onRendered, this);
+ }
+
+ onRendered(event)
+ {
+ const _this = event.data.self;
+ const container = event.target;
+
+ container.querySelectorAll('[data-animate-progress]').forEach(progress => {
+ const frequency = (
+ (Number(progress.dataset.endTime) - Number(progress.dataset.startTime)
+ ) * 1000) / progress.parentElement.offsetWidth;
+
+ _this.updateProgress(
+ now => _this.animateProgress(progress, now), frequency);
+ });
+ }
+
+ animateProgress(progress, now)
+ {
+ if (! progress.isConnected) {
+ return false; // Exit early if the node is removed from the DOM
+ }
+
+ const durationScale = 100;
+
+ const startTime = Number(progress.dataset.startTime);
+ const endTime = Number(progress.dataset.endTime);
+ const duration = endTime - startTime;
+ const end = new Date(endTime * 1000.0);
+
+ let leftNow = durationScale * (1 - (end - now) / (duration * 1000.0));
+ if (leftNow > durationScale) {
+ leftNow = durationScale;
+ } else if (leftNow < 0) {
+ leftNow = 0;
+ }
+
+ const switchAfter = Number(progress.dataset.switchAfter);
+ if (! isNaN(switchAfter)) {
+ const switchClass = progress.dataset.switchClass;
+ const switchAt = new Date((startTime * 1000.0) + (switchAfter * 1000.0));
+ if (now < switchAt) {
+ progress.classList.add(switchClass);
+ } else if (progress.classList.contains(switchClass)) {
+ progress.classList.remove(switchClass);
+ }
+ }
+
+ const bar = progress.querySelector(':scope > .bar');
+ bar.style.width = leftNow + '%';
+
+ return leftNow !== durationScale;
+ }
+
+ updateProgress(callback, frequency, now = null)
+ {
+ if (now === null) {
+ now = new Date();
+ }
+
+ if (! callback(now)) {
+ return;
+ }
+
+ if (frequency < this.smoothUpdateThreshold) {
+ let counter = this.frameUpdateThreshold;
+ const onNextFrame = timeSinceOrigin => {
+ if (--counter === 0) {
+ this.updateProgress(callback, frequency, new Date(performance.timeOrigin + timeSinceOrigin));
+ } else {
+ requestAnimationFrame(onNextFrame);
+ }
+ };
+ requestAnimationFrame(onNextFrame);
+ } else {
+ setTimeout(() => this.updateProgress(callback, frequency), frequency);
+ }
+ }
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ Icinga.Behaviors.ProgressBar = ProgressBar;
+})(Icinga);