Skip to content

Commit c9985e1

Browse files
Fix VM widget quick rail collapse sync and overhang
1 parent 689c319 commit c9985e1

3 files changed

Lines changed: 147 additions & 5 deletions

File tree

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/dashboard.js

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,17 @@ const getDashboardFolderCardsForType = (type) => {
290290
const meta = dashboardTypeMeta(type);
291291
return $(`${meta.tbodySelector} .folder-showcase-outer`);
292292
};
293+
const getDashboardWidgetBodyForType = (type) => {
294+
const meta = dashboardTypeMeta(type);
295+
return $(meta.tbodySelector).first();
296+
};
297+
const getDashboardWidgetUpdatedRowForType = (type) => {
298+
const $tbody = getDashboardWidgetBodyForType(type);
299+
if (!$tbody.length) {
300+
return $();
301+
}
302+
return $tbody.children('tr.updated').first();
303+
};
293304
const isDashboardNodeVisible = (node) => {
294305
if (!node || !(node instanceof Element)) {
295306
return false;
@@ -308,7 +319,41 @@ const isDashboardNodeVisible = (node) => {
308319
}
309320
current = current.parentElement;
310321
}
311-
return node.getClientRects().length > 0;
322+
const rect = typeof node.getBoundingClientRect === 'function' ? node.getBoundingClientRect() : null;
323+
if (!rect) {
324+
return false;
325+
}
326+
return rect.width > 1 && rect.height > 1;
327+
};
328+
const isDashboardWidgetCollapsedForType = (type) => {
329+
const resolvedType = type === 'vm' ? 'vm' : 'docker';
330+
const $tbody = getDashboardWidgetBodyForType(resolvedType);
331+
if (!$tbody.length) {
332+
return true;
333+
}
334+
const $updatedRow = getDashboardWidgetUpdatedRowForType(resolvedType);
335+
if ($updatedRow.length) {
336+
const updatedNode = $updatedRow.get(0);
337+
const style = updatedNode ? window.getComputedStyle(updatedNode) : null;
338+
if (style && (style.display === 'none' || style.visibility === 'hidden')) {
339+
return true;
340+
}
341+
const rect = updatedNode && typeof updatedNode.getBoundingClientRect === 'function'
342+
? updatedNode.getBoundingClientRect()
343+
: null;
344+
if (rect && (rect.width < 8 || rect.height < 8)) {
345+
return true;
346+
}
347+
}
348+
const $headerRow = $tbody.children('tr').not('.updated').first();
349+
const iconClass = String(
350+
$headerRow.find('a.switch i, .switch i').first().attr('class')
351+
|| ''
352+
).toLowerCase();
353+
if (iconClass.includes('angle-down') || iconClass.includes('chevron-down')) {
354+
return true;
355+
}
356+
return false;
312357
};
313358
const getFirstVisibleDashboardFolderCardForType = (type) => {
314359
const $cards = getDashboardFolderCardsForType(type);
@@ -325,7 +370,67 @@ const getFirstVisibleDashboardFolderCardForType = (type) => {
325370
});
326371
return firstVisible;
327372
};
373+
const ensureDashboardWidgetInlineHostMountForType = (type, hostOverride = null) => {
374+
const resolvedType = type === 'vm' ? 'vm' : 'docker';
375+
const $container = resolveDashboardWidgetInlineHostForType(resolvedType);
376+
const $host = hostOverride && hostOverride.length
377+
? hostOverride
378+
: $(`.fv-dashboard-layout-inline-host[data-fv-dashboard-type="${resolvedType}"]`).first();
379+
if (!$container.length || !$host.length) {
380+
return $container;
381+
}
382+
if (!$host.parent().is($container)) {
383+
$container.prepend($host);
384+
}
385+
$container.addClass('fv-dashboard-layout-inline-container');
386+
return $container;
387+
};
328388
const hasVisibleDashboardFolderCardsForType = (type) => !!getFirstVisibleDashboardFolderCardForType(type);
389+
const syncDashboardWidgetQuickRailFitForType = (type, parentRect, offsetTop) => {
390+
const resolvedType = type === 'vm' ? 'vm' : 'docker';
391+
const $host = $(`.fv-dashboard-layout-inline-host[data-fv-dashboard-type="${resolvedType}"]`).first();
392+
if (!$host.length) {
393+
return;
394+
}
395+
const $rail = $host.children('.fv-dashboard-layout-quick-rail').first();
396+
if (!$rail.length) {
397+
return;
398+
}
399+
const availableHeight = Math.max(0, Math.floor((parentRect?.height || 0) - offsetTop - 2));
400+
if (availableHeight <= 0) {
401+
$host.css('max-height', '');
402+
$rail.css('max-height', '');
403+
$rail.removeClass('is-clamped is-compact-grid');
404+
return;
405+
}
406+
$host.css('max-height', `${availableHeight}px`);
407+
$rail.css('max-height', `${availableHeight}px`);
408+
409+
const $buttons = $rail.children('button.fv-dashboard-quick-action');
410+
const buttonCount = $buttons.length;
411+
const buttonNode = $buttons.first().get(0);
412+
const buttonStyle = buttonNode ? window.getComputedStyle(buttonNode) : null;
413+
const buttonHeight = buttonStyle
414+
? Math.max(10, Math.round(parseFloat(buttonStyle.height) || 16))
415+
: 16;
416+
const railNode = $rail.get(0);
417+
const railStyle = railNode ? window.getComputedStyle(railNode) : null;
418+
const rowGap = railStyle
419+
? Math.max(0, Math.round(parseFloat(railStyle.rowGap || railStyle.gap || '2') || 2))
420+
: 2;
421+
const singleRows = buttonCount;
422+
const singleHeight = singleRows > 0
423+
? (singleRows * buttonHeight) + (Math.max(0, singleRows - 1) * rowGap)
424+
: 0;
425+
const gridRows = Math.ceil(buttonCount / 2);
426+
const gridHeight = gridRows > 0
427+
? (gridRows * buttonHeight) + (Math.max(0, gridRows - 1) * rowGap)
428+
: 0;
429+
const useCompactGrid = availableHeight < singleHeight && availableHeight >= gridHeight;
430+
$rail.toggleClass('is-compact-grid', useCompactGrid);
431+
const requiredHeight = useCompactGrid ? gridHeight : singleHeight;
432+
$rail.toggleClass('is-clamped', requiredHeight > availableHeight);
433+
};
329434
const syncDashboardWidgetQuickRailAlignmentForType = (type) => {
330435
const resolvedType = type === 'vm' ? 'vm' : 'docker';
331436
const $host = $(`.fv-dashboard-layout-inline-host[data-fv-dashboard-type="${resolvedType}"]`).first();
@@ -337,28 +442,37 @@ const syncDashboardWidgetQuickRailAlignmentForType = (type) => {
337442
const firstVisibleCard = getFirstVisibleDashboardFolderCardForType(resolvedType);
338443
if (!parentNode || !firstVisibleCard || !isDashboardNodeVisible(parentNode)) {
339444
$host.css('top', '');
445+
$host.css('max-height', '');
446+
$host.children('.fv-dashboard-layout-quick-rail').first().css('max-height', '').removeClass('is-clamped is-compact-grid');
340447
return;
341448
}
342449
const parentRect = parentNode.getBoundingClientRect();
343450
const cardRect = firstVisibleCard.getBoundingClientRect();
344451
const offsetTop = Math.max(0, Math.round(cardRect.top - parentRect.top));
345452
$host.css('top', `${offsetTop}px`);
453+
syncDashboardWidgetQuickRailFitForType(resolvedType, parentRect, offsetTop);
346454
};
347455
const syncDashboardWidgetQuickRailVisibilityForType = (type) => {
348456
const resolvedType = type === 'vm' ? 'vm' : 'docker';
349457
const $host = $(`.fv-dashboard-layout-inline-host[data-fv-dashboard-type="${resolvedType}"]`).first();
350458
if (!$host.length) {
351459
return;
352460
}
461+
ensureDashboardWidgetInlineHostMountForType(resolvedType, $host);
353462
const hostNode = $host.get(0);
354463
const parentNode = hostNode && hostNode.parentElement ? hostNode.parentElement : null;
355464
const shouldShow = !!parentNode
356465
&& isDashboardNodeVisible(parentNode)
466+
&& isDashboardWidgetCollapsedForType(resolvedType) !== true
357467
&& hasVisibleDashboardFolderCardsForType(resolvedType);
358468
$host.toggleClass('is-hidden', !shouldShow);
359469
if (shouldShow) {
360470
syncDashboardWidgetQuickRailAlignmentForType(resolvedType);
471+
return;
361472
}
473+
$host.css('top', '');
474+
$host.css('max-height', '');
475+
$host.children('.fv-dashboard-layout-quick-rail').first().css('max-height', '').removeClass('is-clamped is-compact-grid');
362476
};
363477
const getDashboardFolderIdsForType = (type) => {
364478
const ids = [];
@@ -571,11 +685,8 @@ const ensureDashboardWidgetLayoutQuickSwitchForType = (type) => {
571685
let $host = $(hostSelector).first();
572686
if (!$host.length) {
573687
$host = $(`<div class="fv-dashboard-layout-inline-host fv-dashboard-quick-rail-host" data-fv-dashboard-type="${resolvedType}"></div>`);
574-
$container.prepend($host);
575-
} else if (!$host.parent().is($container)) {
576-
$container.prepend($host);
577688
}
578-
$container.addClass('fv-dashboard-layout-inline-container');
689+
ensureDashboardWidgetInlineHostMountForType(resolvedType, $host);
579690
if (!$host.hasClass('fv-dashboard-quick-rail-host')) {
580691
$host.addClass('fv-dashboard-quick-rail-host');
581692
}
@@ -872,6 +983,8 @@ const bindDashboardWidgetVisibilityObserverForType = (type) => {
872983
});
873984
}
874985
observer.observe(containerNode, {
986+
attributes: true,
987+
attributeFilter: ['class', 'style', 'hidden', 'aria-hidden'],
875988
childList: true,
876989
subtree: true
877990
});
@@ -895,8 +1008,12 @@ const bindDashboardQuickActionSyncHandlers = () => {
8951008
}
8961009
});
8971010
$(document).on('click.fvplusdashboardquickcollapse', 'a.switch, .switch', () => {
1011+
scheduleDashboardWidgetVisibilitySyncForType('docker', 0);
1012+
scheduleDashboardWidgetVisibilitySyncForType('vm', 0);
8981013
scheduleDashboardWidgetVisibilitySyncForType('docker', 80);
8991014
scheduleDashboardWidgetVisibilitySyncForType('vm', 80);
1015+
scheduleDashboardWidgetVisibilitySyncForType('docker', 220);
1016+
scheduleDashboardWidgetVisibilitySyncForType('vm', 220);
9001017
});
9011018
$(window).on('resize.fvplusdashboardquick orientationchange.fvplusdashboardquick', () => {
9021019
scheduleDashboardWidgetVisibilitySyncForType('docker', 0);

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/styles/dashboard.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
margin: 0;
8989
padding: 0;
9090
box-sizing: border-box;
91+
max-height: calc(100% - 2px);
92+
overflow: hidden;
9193
pointer-events: none;
9294
}
9395

@@ -100,9 +102,23 @@
100102
flex-direction: column;
101103
align-items: flex-end;
102104
gap: 2px;
105+
max-height: 100%;
106+
overflow: hidden;
103107
pointer-events: auto;
104108
}
105109

110+
.fv-dashboard-layout-quick-rail.is-compact-grid {
111+
display: grid;
112+
grid-template-columns: repeat(2, minmax(0, 16px));
113+
justify-content: end;
114+
align-content: start;
115+
}
116+
117+
.fv-dashboard-layout-quick-rail.is-clamped {
118+
overflow-y: auto;
119+
scrollbar-width: thin;
120+
}
121+
106122
.fv-dashboard-quick-action {
107123
display: inline-flex;
108124
align-items: center;
@@ -449,6 +465,10 @@ body.fvplus-performance-mode .tooltipster-content {
449465
gap: 1px;
450466
}
451467

468+
.fv-dashboard-layout-quick-rail.is-compact-grid {
469+
grid-template-columns: repeat(2, minmax(0, 14px));
470+
}
471+
452472
.fv-dashboard-quick-action {
453473
width: 14px !important;
454474
min-width: 14px !important;

tests/dashboard-layout-regression.test.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ test('dashboard runtime supports layout classes, accordion guards, and overflow
7474
assert.match(dashboardScript, /const DASHBOARD_LAYOUT_MODES = \['classic', 'fullwidth', 'accordion', 'inset', 'compactmatrix'\]/);
7575
assert.match(dashboardScript, /const ensureDashboardWidgetLayoutQuickSwitchForType = \(type\) =>/);
7676
assert.match(dashboardScript, /const resolveDashboardWidgetInlineHostForType = \(type\) =>/);
77+
assert.match(dashboardScript, /const isDashboardWidgetCollapsedForType = \(type\) =>/);
78+
assert.match(dashboardScript, /const ensureDashboardWidgetInlineHostMountForType = \(type, hostOverride = null\) =>/);
79+
assert.match(dashboardScript, /const syncDashboardWidgetQuickRailFitForType = \(type, parentRect, offsetTop\) =>/);
7780
assert.match(dashboardScript, /fv-dashboard-layout-inline-host/);
7881
assert.match(dashboardScript, /fv-dashboard-layout-quick-rail/);
7982
assert.match(dashboardScript, /ensureQuickAction\('layout-cycle'/);
@@ -113,6 +116,8 @@ test('dashboard css includes non-classic controls and overflow rendering modes',
113116
assert.match(dashboardCss, /\.fv-dashboard-layout-inline-host/);
114117
assert.match(dashboardCss, /\.fv-dashboard-layout-quick/);
115118
assert.match(dashboardCss, /\.fv-dashboard-layout-quick-rail/);
119+
assert.match(dashboardCss, /\.fv-dashboard-layout-quick-rail\.is-clamped/);
120+
assert.match(dashboardCss, /\.fv-dashboard-layout-quick-rail\.is-compact-grid/);
116121
assert.match(dashboardCss, /\.fv-dashboard-quick-action/);
117122
assert.match(dashboardCss, /tbody\.fv-dashboard-health-emphasis-enabled/);
118123
assert.match(dashboardCss, /tbody\.fv-dashboard-density-compact/);

0 commit comments

Comments
 (0)