Merge pull request #3638 from Icinga/feature/js-collapsible-containers

Persistent Collapsible Containers
This commit is contained in:
Johannes Meyer 2019-07-18 07:52:56 +02:00 committed by GitHub
commit 6cd94080bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 882 additions and 25 deletions

View File

@ -833,6 +833,18 @@
"css": "th-list", "css": "th-list",
"code": 61449, "code": 61449,
"src": "mfglabs" "src": "mfglabs"
},
{
"uid": "63b3012c8cbe3654ba5bea598235aa3a",
"css": "angle-double-up",
"code": 61698,
"src": "fontawesome"
},
{
"uid": "dfec4ffa849d8594c2e4b86f6320b8a6",
"css": "angle-double-down",
"code": 61699,
"src": "fontawesome"
} }
] ]
} }

View File

@ -135,5 +135,7 @@
.icon-th-list:before { content: '\f009'; } /* '' */ .icon-th-list:before { content: '\f009'; } /* '' */
.icon-th-thumb-empty:before { content: '\f00b'; } /* '' */ .icon-th-thumb-empty:before { content: '\f00b'; } /* '' */
.icon-github-circled:before { content: '\f09b'; } /* '' */ .icon-github-circled:before { content: '\f09b'; } /* '' */
.icon-angle-double-up:before { content: '\f102'; } /* '' */
.icon-angle-double-down:before { content: '\f103'; } /* '' */
.icon-history:before { content: '\f1da'; } /* '' */ .icon-history:before { content: '\f1da'; } /* '' */
.icon-binoculars:before { content: '\f1e5'; } /* '' */ .icon-binoculars:before { content: '\f1e5'; } /* '' */

File diff suppressed because one or more lines are too long

View File

@ -135,5 +135,7 @@
.icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-angle-double-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-angle-double-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }

View File

@ -146,5 +146,7 @@
.icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-angle-double-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-angle-double-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }

View File

@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: 'ifont'; font-family: 'ifont';
src: url('../font/ifont.eot?15561604'); src: url('../font/ifont.eot?42867337');
src: url('../font/ifont.eot?15561604#iefix') format('embedded-opentype'), src: url('../font/ifont.eot?42867337#iefix') format('embedded-opentype'),
url('../font/ifont.woff2?15561604') format('woff2'), url('../font/ifont.woff2?42867337') format('woff2'),
url('../font/ifont.woff?15561604') format('woff'), url('../font/ifont.woff?42867337') format('woff'),
url('../font/ifont.ttf?15561604') format('truetype'), url('../font/ifont.ttf?42867337') format('truetype'),
url('../font/ifont.svg?15561604#ifont') format('svg'); url('../font/ifont.svg?42867337#ifont') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'ifont'; font-family: 'ifont';
src: url('../font/ifont.svg?15561604#ifont') format('svg'); src: url('../font/ifont.svg?42867337#ifont') format('svg');
} }
} }
*/ */
@ -191,5 +191,7 @@
.icon-th-list:before { content: '\f009'; } /* '' */ .icon-th-list:before { content: '\f009'; } /* '' */
.icon-th-thumb-empty:before { content: '\f00b'; } /* '' */ .icon-th-thumb-empty:before { content: '\f00b'; } /* '' */
.icon-github-circled:before { content: '\f09b'; } /* '' */ .icon-github-circled:before { content: '\f09b'; } /* '' */
.icon-angle-double-up:before { content: '\f102'; } /* '' */
.icon-angle-double-down:before { content: '\f103'; } /* '' */
.icon-history:before { content: '\f1da'; } /* '' */ .icon-history:before { content: '\f1da'; } /* '' */
.icon-binoculars:before { content: '\f1e5'; } /* '' */ .icon-binoculars:before { content: '\f1e5'; } /* '' */

View File

@ -229,11 +229,11 @@ body {
} }
@font-face { @font-face {
font-family: 'ifont'; font-family: 'ifont';
src: url('./font/ifont.eot?97051739'); src: url('./font/ifont.eot?11177247');
src: url('./font/ifont.eot?97051739#iefix') format('embedded-opentype'), src: url('./font/ifont.eot?11177247#iefix') format('embedded-opentype'),
url('./font/ifont.woff?97051739') format('woff'), url('./font/ifont.woff?11177247') format('woff'),
url('./font/ifont.ttf?97051739') format('truetype'), url('./font/ifont.ttf?11177247') format('truetype'),
url('./font/ifont.svg?97051739#ifont') format('svg'); url('./font/ifont.svg?11177247#ifont') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -502,6 +502,8 @@ body {
<div class="the-icons span3" title="Code: 0xf09b"><i class="demo-icon icon-github-circled">&#xf09b;</i> <span class="i-name">icon-github-circled</span><span class="i-code">0xf09b</span></div> <div class="the-icons span3" title="Code: 0xf09b"><i class="demo-icon icon-github-circled">&#xf09b;</i> <span class="i-name">icon-github-circled</span><span class="i-code">0xf09b</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xf102"><i class="demo-icon icon-angle-double-up">&#xf102;</i> <span class="i-name">icon-angle-double-up</span><span class="i-code">0xf102</span></div>
<div class="the-icons span3" title="Code: 0xf103"><i class="demo-icon icon-angle-double-down">&#xf103;</i> <span class="i-name">icon-angle-double-down</span><span class="i-code">0xf103</span></div>
<div class="the-icons span3" title="Code: 0xf1da"><i class="demo-icon icon-history">&#xf1da;</i> <span class="i-name">icon-history</span><span class="i-code">0xf1da</span></div> <div class="the-icons span3" title="Code: 0xf1da"><i class="demo-icon icon-history">&#xf1da;</i> <span class="i-name">icon-history</span><span class="i-code">0xf1da</span></div>
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div> <div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
</div> </div>

View File

@ -278,6 +278,10 @@
<glyph glyph-name="github-circled" unicode="&#xf09b;" d="M429 779q116 0 215-58t156-156 57-215q0-140-82-252t-211-155q-15-3-22 4t-7 17q0 1 0 43t0 75q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 67-44 115 21 51-4 114-16 5-46-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-47 7q-25-63-5-114-44-48-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-21-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l6-13q7-21 24-34t37-17 39-3 31 1l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58z m-267-616q2 4-3 7-6 1-8-1-1-4 4-7 5-3 7 1z m18-19q4 3-1 9-6 5-9 2-4-3 1-9 5-6 9-2z m16-25q6 4 0 11-4 7-9 3-5-3 0-10t9-4z m24-23q4 4-2 10-7 7-11 2-5-5 2-11 6-6 11-1z m32-14q1 6-8 9-8 2-10-4t7-9q8-3 11 4z m35-3q0 7-10 6-9 0-9-6 0-7 10-6 9 0 9 6z m32 5q-1 7-10 5-9-1-8-8t10-4 8 7z" horiz-adv-x="857.1" /> <glyph glyph-name="github-circled" unicode="&#xf09b;" d="M429 779q116 0 215-58t156-156 57-215q0-140-82-252t-211-155q-15-3-22 4t-7 17q0 1 0 43t0 75q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 67-44 115 21 51-4 114-16 5-46-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-47 7q-25-63-5-114-44-48-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-21-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l6-13q7-21 24-34t37-17 39-3 31 1l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58z m-267-616q2 4-3 7-6 1-8-1-1-4 4-7 5-3 7 1z m18-19q4 3-1 9-6 5-9 2-4-3 1-9 5-6 9-2z m16-25q6 4 0 11-4 7-9 3-5-3 0-10t9-4z m24-23q4 4-2 10-7 7-11 2-5-5 2-11 6-6 11-1z m32-14q1 6-8 9-8 2-10-4t7-9q8-3 11 4z m35-3q0 7-10 6-9 0-9-6 0-7 10-6 9 0 9 6z m32 5q-1 7-10 5-9-1-8-8t10-4 8 7z" horiz-adv-x="857.1" />
<glyph glyph-name="angle-double-up" unicode="&#xf102;" d="M600 118q0-7-6-13l-28-28q-5-5-12-5t-13 5l-220 219-219-219q-5-5-13-5t-12 5l-28 28q-6 6-6 13t6 13l260 260q5 5 12 5t13-5l260-260q6-6 6-13z m0 214q0-7-6-13l-28-28q-5-5-12-5t-13 5l-220 220-219-220q-5-5-13-5t-12 5l-28 28q-6 6-6 13t6 13l260 260q5 6 12 6t13-6l260-260q6-6 6-13z" horiz-adv-x="642.9" />
<glyph glyph-name="angle-double-down" unicode="&#xf103;" d="M600 368q0-7-6-13l-260-260q-5-6-13-6t-12 6l-260 260q-6 6-6 13t6 13l28 28q5 5 12 5t13-5l219-220 220 220q5 5 13 5t12-5l28-28q6-6 6-13z m0 214q0-7-6-13l-260-260q-5-5-13-5t-12 5l-260 260q-6 6-6 13t6 13l28 28q5 6 12 6t13-6l219-219 220 219q5 6 13 6t12-6l28-28q6-6 6-13z" horiz-adv-x="642.9" />
<glyph glyph-name="history" unicode="&#xf1da;" d="M857 350q0-87-34-166t-91-137-137-92-166-34q-96 0-183 41t-147 114q-4 6-4 13t5 11l76 77q6 5 14 5 9-1 13-7 41-53 100-82t126-29q58 0 110 23t92 61 61 91 22 111-22 111-61 91-92 61-110 23q-55 0-105-20t-90-57l77-77q17-16 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l72-72q60 57 137 88t159 31q87 0 166-34t137-92 91-137 34-166z m-357 161v-250q0-8-5-13t-13-5h-178q-8 0-13 5t-5 13v35q0 8 5 13t13 5h125v197q0 8 5 13t12 5h36q8 0 13-5t5-13z" horiz-adv-x="857.1" /> <glyph glyph-name="history" unicode="&#xf1da;" d="M857 350q0-87-34-166t-91-137-137-92-166-34q-96 0-183 41t-147 114q-4 6-4 13t5 11l76 77q6 5 14 5 9-1 13-7 41-53 100-82t126-29q58 0 110 23t92 61 61 91 22 111-22 111-61 91-92 61-110 23q-55 0-105-20t-90-57l77-77q17-16 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l72-72q60 57 137 88t159 31q87 0 166-34t137-92 91-137 34-166z m-357 161v-250q0-8-5-13t-13-5h-178q-8 0-13 5t-5 13v35q0 8 5 13t13 5h125v197q0 8 5 13t12 5h36q8 0 13-5t5-13z" horiz-adv-x="857.1" />
<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" /> <glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -76,6 +76,12 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml';
} }
}()); }());
</script> </script>
<div id="collapsible-control-ghost" class="collapsible-control">
<button>
<?= $this->icon('angle-double-down', $this->translate('Expand'), ['class' => 'expand-icon']) ?>
<?= $this->icon('angle-double-up', $this->translate('Collapse'), ['class' => 'collapse-icon']) ?>
</button>
</div>
<!--[if lt IE 10]> <!--[if lt IE 10]>
<iframe id="fileupload-frame-target" name="fileupload-frame-target"></iframe> <iframe id="fileupload-frame-target" name="fileupload-frame-target"></iframe>
<![endif]--> <![endif]-->

View File

@ -13,6 +13,7 @@ class JavaScript
'js/helpers.js', 'js/helpers.js',
'js/icinga.js', 'js/icinga.js',
'js/icinga/logger.js', 'js/icinga/logger.js',
'js/icinga/storage.js',
'js/icinga/utils.js', 'js/icinga/utils.js',
'js/icinga/ui.js', 'js/icinga/ui.js',
'js/icinga/timer.js', 'js/icinga/timer.js',
@ -24,6 +25,7 @@ class JavaScript
'js/icinga/timezone.js', 'js/icinga/timezone.js',
'js/icinga/behavior/application-state.js', 'js/icinga/behavior/application-state.js',
'js/icinga/behavior/autofocus.js', 'js/icinga/behavior/autofocus.js',
'js/icinga/behavior/collapsible.js',
'js/icinga/behavior/detach.js', 'js/icinga/behavior/detach.js',
'js/icinga/behavior/tooltip.js', 'js/icinga/behavior/tooltip.js',
'js/icinga/behavior/sparkline.js', 'js/icinga/behavior/sparkline.js',

View File

@ -161,13 +161,6 @@ a:hover > .icon-cancel {
background-color: @tr-hover-color; background-color: @tr-hover-color;
cursor: pointer; cursor: pointer;
} }
caption {
border-top: 1px solid @gray-light;
caption-side: bottom;
font-style: italic;
text-align: right;
}
} }
.name-value-table { .name-value-table {
@ -233,3 +226,85 @@ a:hover > .icon-cancel {
width: 100%; width: 100%;
} }
} }
// Collapsible Control
#collapsible-control-ghost {
display: none;
}
.collapsible-control {
position: relative;
button {
.rounded-corners(50%);
background: @gray-lighter;
color: @gray;
width: 2em;
height: 2em;
z-index: 1;
position: absolute;
border: none;
padding: 0;
bottom: -1em;
right: .25em;
-webkit-box-shadow: 0 0 1/3em rgba(0,0,0,.3);
-moz-box-shadow: 0 0 1/3em rgba(0,0,0,.3);
box-shadow: 0 0 1/3em rgba(0,0,0,.3);
&:hover:before {
content: "";
display: block;
position: absolute;
top: -1/6em;
right: -1/6em;
bottom: -1/6em;
left: -1/6em;
background: fade(@text-color, 10);
.rounded-corners(50%);
}
}
button i:before {
margin-right: 0;
}
}
.collapsible.can-collapse:not(.collapsed) + .collapsible-control button {
> i.expand-icon {
display: none;
}
> i.collapse-icon {
display: inline;
}
}
.collapsible.collapsed + .collapsible-control button {
> i.expand-icon {
display: inline;
}
> i.collapse-icon {
display: none;
}
}
// Collapsibles
.collapsible.collapsed {
position: relative;
overflow: hidden;
&:before {
content: "";
display: block;
height: 2em;
background: linear-gradient(rgba(255,255,255,0), white);
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
}

View File

@ -0,0 +1,215 @@
/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
;(function(Icinga, $) {
'use strict';
Icinga.Behaviors = Icinga.Behaviors || {};
/**
* Behavior for collapsible containers.
*
* @param icinga Icinga The current Icinga Object
*/
var Collapsible = function(icinga) {
Icinga.EventListener.call(this, icinga);
this.on('layout-change', this.onLayoutChange, this);
this.on('rendered', '.container', this.onRendered, this);
this.on('click', '.collapsible + .collapsible-control', this.onControlClicked, this);
this.icinga = icinga;
this.defaultVisibleRows = 2;
this.defaultVisibleHeight = 36;
this.state = new Icinga.Storage.StorageAwareMap.withStorage(
Icinga.Storage.BehaviorStorage('collapsible'),
'expanded'
)
.on('add', this.onExpand, this)
.on('delete', this.onCollapse, this);
};
Collapsible.prototype = new Icinga.EventListener();
/**
* Initializes all collapsibles. Triggered on rendering of a container.
*
* @param event Event The `onRender` event triggered by the rendered container
*/
Collapsible.prototype.onRendered = function(event) {
var _this = event.data.self;
$('.collapsible:not(.can-collapse)', event.currentTarget).each(function() {
var $collapsible = $(this);
// Assumes that any newly rendered elements are expanded
if (_this.canCollapse($collapsible)) {
$collapsible.after($('#collapsible-control-ghost').clone().removeAttr('id'));
$collapsible.addClass('can-collapse');
if (! _this.state.has(_this.icinga.utils.getCSSPath($collapsible))) {
_this.collapse($collapsible);
}
}
});
};
/**
* Updates all collapsibles.
*
* @param event Event The `layout-change` event triggered by window resizing or column changes
*/
Collapsible.prototype.onLayoutChange = function(event) {
var _this = event.data.self;
$('.collapsible').each(function() {
var $collapsible = $(this);
var collapsiblePath = _this.icinga.utils.getCSSPath($collapsible);
if ($collapsible.is('.can-collapse')) {
if (! _this.canCollapse($collapsible)) {
$collapsible.next('.collapsible-control').remove();
$collapsible.removeClass('can-collapse');
_this.expand($collapsible);
}
} else if (_this.canCollapse($collapsible)) {
// It's expanded but shouldn't
$collapsible.after($('#collapsible-control-ghost').clone().removeAttr('id'));
$collapsible.addClass('can-collapse');
if (! _this.state.has(collapsiblePath)) {
_this.collapse($collapsible);
}
}
});
};
/**
* A collapsible got expanded in another window, try to apply this here as well
*
* @param {string} collapsiblePath
*/
Collapsible.prototype.onExpand = function(collapsiblePath) {
var $collapsible = $(collapsiblePath);
if ($collapsible.length && $collapsible.is('.can-collapse')) {
this.expand($collapsible);
}
};
/**
* A collapsible got collapsed in another window, try to apply this here as well
*
* @param {string} collapsiblePath
*/
Collapsible.prototype.onCollapse = function(collapsiblePath) {
var $collapsible = $(collapsiblePath);
if ($collapsible.length && this.canCollapse($collapsible)) {
this.collapse($collapsible);
}
};
/**
* Event handler for toggling collapsibles. Switches the collapsed state of the respective container.
*
* @param event Event The `onClick` event triggered by the clicked collapsible-control element
*/
Collapsible.prototype.onControlClicked = function(event) {
var _this = event.data.self;
var $target = $(event.currentTarget);
var $collapsible = $target.prev('.collapsible');
if (! $collapsible.length) {
_this.icinga.logger.error('[Collapsible] Collapsible control has no associated .collapsible: ', $target);
} else {
var collapsiblePath = _this.icinga.utils.getCSSPath($collapsible);
if (_this.state.has(collapsiblePath)) {
_this.state.delete(collapsiblePath);
_this.collapse($collapsible);
} else {
_this.state.set(collapsiblePath);
_this.expand($collapsible);
}
}
};
/**
* Return an appropriate row element selector
*
* @param $collapsible jQuery The given collapsible container element
*
* @returns {string}
*/
Collapsible.prototype.getRowSelector = function($collapsible) {
if ($collapsible.is('table')) {
return '> tbody > tr';
} else if ($collapsible.is('ul, ol')) {
return '> li';
}
return '';
};
/**
* Check whether the given collapsible needs to collapse
*
* @param $collapsible jQuery The given collapsible container element
*
* @returns {boolean}
*/
Collapsible.prototype.canCollapse = function($collapsible) {
var rowSelector = this.getRowSelector($collapsible);
if (!! rowSelector) {
return $(rowSelector, $collapsible).length > ($collapsible.data('visibleRows') || this.defaultVisibleRows);
} else {
var actualHeight = $collapsible[0].scrollHeight;
var maxHeight = $collapsible.data('visibleHeight') || this.defaultVisibleHeight;
if (actualHeight <= maxHeight) {
return false;
}
// Although the height seems larger than what it should be, make sure it's not just a small fraction
// i.e. more than 12 pixel and at least 10% difference
return actualHeight - maxHeight > 12 && actualHeight / maxHeight >= 1.1;
}
};
/**
* Collapse the given collapsible
*
* @param $collapsible jQuery The given collapsible container element
*/
Collapsible.prototype.collapse = function($collapsible) {
$collapsible.addClass('collapsed');
var rowSelector = this.getRowSelector($collapsible);
if (!! rowSelector) {
var $rows = $(rowSelector, $collapsible).slice(0, $collapsible.data('visibleRows') || this.defaultVisibleRows);
var totalHeight = $rows.offset().top - $collapsible.offset().top;
$rows.outerHeight(function(_, height) {
totalHeight += height;
});
$collapsible.css({display: 'block', height: totalHeight});
} else {
$collapsible.css({display: 'block', height: $collapsible.data('visibleHeight') || this.defaultVisibleHeight});
}
};
/**
* Expand the given collapsible
*
* @param $collapsible jQuery The given collapsible container element
*/
Collapsible.prototype.expand = function($collapsible) {
$collapsible.removeClass('collapsed');
$collapsible.css({display: '', height: ''});
};
Icinga.Behaviors.Collapsible = Collapsible;
})(Icinga, jQuery);

526
public/js/icinga/storage.js Normal file
View File

@ -0,0 +1,526 @@
/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
;(function(Icinga) {
'use strict';
const KEY_TTL = 7776000000; // 90 days (90×24×60×60×1000)
/**
* Icinga.Storage
*
* localStorage access
*
* @param {string} prefix
*/
Icinga.Storage = function(prefix) {
/**
* Prefix to use for keys
*
* @type {string}
*/
this.prefix = prefix;
};
/**
* Callbacks for storage events on particular keys
*
* @type {{function}}
*/
Icinga.Storage.subscribers = {};
/**
* Pass storage events to subscribers
*
* @param {StorageEvent} event
*/
window.addEventListener('storage', function(event) {
var url = icinga.utils.parseUrl(event.url);
if (! url.path.substring(0, icinga.config.baseUrl.length) === icinga.config.baseUrl) {
// A localStorage is shared between all paths on the same origin.
// So we need to make sure it's us who made a change.
return;
}
if (typeof Icinga.Storage.subscribers[event.key] !== 'undefined') {
var newValue = null,
oldValue = null;
if (!! event.newValue) {
try {
newValue = JSON.parse(event.newValue);
} catch(error) {
icinga.logger.error('[Storage] Failed to parse new value (\`' + event.newValue
+ '\`) for key "' + event.key + '". Error was: ' + error);
event.storageArea.removeItem(event.key);
return;
}
}
if (!! event.oldValue) {
try {
oldValue = JSON.parse(event.oldValue);
} catch(error) {
icinga.logger.warn('[Storage] Failed to parse old value (\`' + event.oldValue
+ '\`) of key "' + event.key + '". Error was: ' + error);
oldValue = null;
}
}
Icinga.Storage.subscribers[event.key].forEach(function (subscriber) {
subscriber[0].call(subscriber[1], newValue, oldValue, event);
});
}
});
/**
* Create a new storage with `behavior.<name>` as prefix
*
* @param {string} name
*
* @returns {Icinga.Storage}
*/
Icinga.Storage.BehaviorStorage = function(name) {
return new Icinga.Storage('behavior.' + name);
};
Icinga.Storage.prototype = {
/**
* Prefix the given key
*
* @param {string} key
*
* @returns {string}
*/
prefixKey: function(key) {
var prefix = 'icinga.';
if (typeof this.prefix !== 'undefined') {
prefix = prefix + this.prefix + '.';
}
return prefix + key;
},
/**
* Store the given key-value pair
*
* @param {string} key
* @param {*} value
*
* @returns {void}
*/
set: function(key, value) {
window.localStorage.setItem(this.prefixKey(key), JSON.stringify(value));
},
/**
* Get value for the given key
*
* @param {string} key
*
* @returns {*}
*/
get: function(key) {
key = this.prefixKey(key);
var value = window.localStorage.getItem(key);
try {
return JSON.parse(value);
} catch(error) {
icinga.logger.error('[Storage] Failed to parse value (\`' + value
+ '\`) of key "' + key + '". Error was: ' + error);
window.localStorage.removeItem(key);
return null;
}
},
/**
* Remove given key from storage
*
* @param {string} key
*
* @returns {void}
*/
remove: function(key) {
window.localStorage.removeItem(this.prefixKey(key));
},
/**
* Subscribe with a callback for events on a particular key
*
* @param {string} key
* @param {function} callback
* @param {object} context
*
* @returns {void}
*/
onChange: function(key, callback, context) {
var prefixedKey = this.prefixKey(key);
if (typeof Icinga.Storage.subscribers[prefixedKey] === 'undefined') {
Icinga.Storage.subscribers[prefixedKey] = [];
}
Icinga.Storage.subscribers[prefixedKey].push([callback, context]);
}
};
/**
* Icinga.Storage.StorageAwareMap
*
* @param {object} items
* @constructor
*/
Icinga.Storage.StorageAwareMap = function(items) {
/**
* Storage object
*
* @type {Icinga.Storage}
*/
this.storage = undefined;
/**
* Storage key
*
* @type {string}
*/
this.key = undefined;
/**
* Event listeners for our internal events
*
* @type {{}}
*/
this.eventListeners = {
'add': [],
'delete': []
};
/**
* The internal (real) map
*
* @type {Map<*>}
*/
this.data = new Map();
// items is not passed directly because IE11 doesn't support constructor arguments
if (typeof items !== 'undefined' && !! items) {
Object.keys(items).forEach(function(key) {
this.data.set(key, items[key]);
}, this);
}
};
/**
* Create a new StorageAwareMap for the given storage and key
*
* @param {Icinga.Storage} storage
* @param {string} key
*
* @returns {Icinga.Storage.StorageAwareMap}
*/
Icinga.Storage.StorageAwareMap.withStorage = function(storage, key) {
var items = storage.get(key);
if (typeof items !== 'undefined' && !! items) {
Object.keys(items).forEach(function(key) {
var value = items[key];
if (typeof value !== 'object' || typeof value['lastAccess'] === 'undefined') {
items[key] = {'value': value, 'lastAccess': Date.now()};
} else if (Date.now() - value['lastAccess'] > KEY_TTL) {
delete items[key];
}
}, this);
}
if (!! items && items.length) {
storage.set(key, items);
} else if(items !== null) {
storage.remove(key);
}
return (new Icinga.Storage.StorageAwareMap(items).setStorage(storage, key));
};
Icinga.Storage.StorageAwareMap.prototype = {
/**
* Bind this map to the given storage and key
*
* @param {Icinga.Storage} storage
* @param {string} key
*
* @returns {this}
*/
setStorage: function(storage, key) {
this.storage = storage;
this.key = key;
storage.onChange(key, this.onChange, this);
return this;
},
/**
* Return a boolean indicating this map got a storage
*
* @returns {boolean}
*/
hasStorage: function() {
return typeof this.storage !== 'undefined' && typeof this.key !== 'undefined';
},
/**
* Update the storage
*
* @returns {void}
*/
updateStorage: function() {
if (! this.hasStorage()) {
return;
}
if (this.size > 0) {
this.storage.set(this.key, this.toObject());
} else {
this.storage.remove(this.key);
}
},
/**
* Update the map
*
* @param {object} newValue
*/
onChange: function(newValue) {
// Check for deletions first. Uses keys() to iterate over a copy
this.keys().forEach(function (key) {
if (newValue === null || typeof newValue[key] === 'undefined') {
var value = this.data.get(key)['value'];
this.data.delete(key);
this.trigger('delete', key, value);
}
}, this);
if (newValue === null) {
return;
}
// Now check for new entries
Object.keys(newValue).forEach(function(key) {
var known = this.data.has(key);
// Always override any known value as we want to keep track of all `lastAccess` changes
this.data.set(key, newValue[key]);
if (! known) {
this.trigger('add', key, newValue[key]['value']);
}
}, this);
},
/**
* Register an event handler to handle storage updates
*
* Available events are: add, delete. The callback receives the
* key and its value as first and second argument, respectively.
*
* @param {string} event
* @param {function} callback
* @param {object} thisArg
*
* @returns {this}
*/
on: function(event, callback, thisArg) {
if (typeof this.eventListeners[event] === 'undefined') {
throw new Error('Invalid event "' + event + '"');
}
this.eventListeners[event].push([callback, thisArg]);
return this;
},
/**
* Trigger all event handlers for the given event
*
* @param {string} event
* @param {string} key
* @param {*} value
*/
trigger: function(event, key, value) {
this.eventListeners[event].forEach(function (handler) {
var thisArg = handler[1];
if (typeof thisArg === 'undefined') {
thisArg = this;
}
handler[0].call(thisArg, key, value);
});
},
/**
* Return the number of key/value pairs in the map
*
* @returns {number}
*/
get size() {
return this.data.size;
},
/**
* Set the value for the key in the map
*
* @param {string} key
* @param {*} value Default null
*
* @returns {this}
*/
set: function(key, value) {
if (typeof value === 'undefined') {
value = null;
}
this.data.set(key, {'value': value, 'lastAccess': Date.now()});
this.updateStorage();
return this;
},
/**
* Remove all key/value pairs from the map
*
* @returns {void}
*/
clear: function() {
this.data.clear();
this.updateStorage();
},
/**
* Remove the given key from the map
*
* @param {string} key
*
* @returns {boolean}
*/
delete: function(key) {
var retVal = this.data.delete(key);
this.updateStorage();
return retVal;
},
/**
* Return a list of [key, value] pairs for every item in the map
*
* @returns {Array}
*/
entries: function() {
var list = [];
if (this.size > 0) {
this.data.forEach(function (value, key) {
list.push([key, value['value']]);
});
}
return list;
},
/**
* Execute a provided function once for each item in the map, in insertion order
*
* @param {function} callback
* @param {object} thisArg
*
* @returns {void}
*/
forEach: function(callback, thisArg) {
if (typeof thisArg === 'undefined') {
thisArg = this;
}
this.data.forEach(function(value, key) {
callback.call(thisArg, value['value'], key);
});
},
/**
* Return the value associated to the key, or undefined if there is none
*
* @param {string} key
*
* @returns {*}
*/
get: function(key) {
var value = this.data.get(key)['value'];
this.set(key, value); // Update `lastAccess`
return value;
},
/**
* Return a boolean asserting whether a value has been associated to the key in the map
*
* @param {string} key
*
* @returns {boolean}
*/
has: function(key) {
return this.data.has(key);
},
/**
* Return an array of keys in the map
*
* @returns {Array}
*/
keys: function() {
var list = [];
if (this.size > 0) {
// .forEach() is used because IE11 doesn't support .keys()
this.data.forEach(function(_, key) {
list.push(key);
});
}
return list;
},
/**
* Return an array of values in the map
*
* @returns {Array}
*/
values: function() {
var list = [];
if (this.size > 0) {
// .forEach() is used because IE11 doesn't support .values()
this.data.forEach(function(value) {
list.push(value['value']);
});
}
return list;
},
/**
* Return this map as simple object
*
* @returns {object}
*/
toObject: function() {
var obj = {};
if (this.size > 0) {
this.data.forEach(function (value, key) {
obj[key] = value;
});
}
return obj;
}
};
}(Icinga));

View File

@ -268,6 +268,9 @@
this.currentLayout = matched[1]; this.currentLayout = matched[1];
if (this.currentLayout === 'poor' || this.currentLayout === 'minimal') { if (this.currentLayout === 'poor' || this.currentLayout === 'minimal') {
this.layout1col(); this.layout1col();
} else {
// layout1col() also triggers this, that's why an else is required
$('#layout').trigger('layout-change');
} }
return true; return true;
} }
@ -293,6 +296,7 @@
this.icinga.logger.debug('Switching to single col'); this.icinga.logger.debug('Switching to single col');
$('#layout').removeClass('twocols'); $('#layout').removeClass('twocols');
this.closeContainer($('#col2')); this.closeContainer($('#col2'));
$('#layout').trigger('layout-change');
// one-column layouts never have any selection active // one-column layouts never have any selection active
$('#col1').removeData('icinga-actiontable-former-href'); $('#col1').removeData('icinga-actiontable-former-href');
this.icinga.behaviors.actiontable.clearAll(); this.icinga.behaviors.actiontable.clearAll();
@ -315,6 +319,7 @@
this.icinga.logger.debug('Switching to double col'); this.icinga.logger.debug('Switching to double col');
$('#layout').addClass('twocols'); $('#layout').addClass('twocols');
this.fixControls(); this.fixControls();
$('#layout').trigger('layout-change');
}, },
getAvailableColumnSpace: function () { getAvailableColumnSpace: function () {