mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 19:23:31 +00:00
feat: improve UI responsiveness and fix table scrolling
This commit is contained in:
+72
-175
@@ -149,10 +149,9 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Charts toggle button styling */
|
||||
.charts-mode #toggle-charts-button {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
/* Charts mode indicator */
|
||||
.charts-mode {
|
||||
/* Chart mode styles handled by checkbox state */
|
||||
}
|
||||
|
||||
/* Enhanced metric cell layouts */
|
||||
@@ -201,117 +200,6 @@
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ALL first cells are sticky on mobile */
|
||||
#main-table td:first-child,
|
||||
#main-table th:first-child {
|
||||
position: sticky !important;
|
||||
left: -2px; /* Shift left to cover gap */
|
||||
z-index: 1;
|
||||
padding-left: 10px; /* Add extra padding to compensate */
|
||||
}
|
||||
|
||||
/* Guest row first cells */
|
||||
#main-table tbody tr:not(.node-header) td:first-child {
|
||||
background: white;
|
||||
background-clip: padding-box;
|
||||
box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.dark #main-table tbody tr:not(.node-header) td:first-child {
|
||||
background: rgb(31 41 55);
|
||||
background-clip: padding-box;
|
||||
box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
#main-table tbody tr:not(.node-header):hover td:first-child {
|
||||
background: rgb(249 250 251);
|
||||
}
|
||||
.dark #main-table tbody tr:not(.node-header):hover td:first-child {
|
||||
background: rgb(55 65 81);
|
||||
}
|
||||
|
||||
/* Table header first cell */
|
||||
#main-table thead th:first-child {
|
||||
background: rgb(243 244 246);
|
||||
background-clip: padding-box;
|
||||
z-index: 11;
|
||||
box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.dark #main-table thead th:first-child {
|
||||
background: rgb(55 65 81);
|
||||
background-clip: padding-box;
|
||||
box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Node header first cell styling */
|
||||
#main-table tr.node-header td:first-child {
|
||||
z-index: 2;
|
||||
background: rgb(229 231 235) !important;
|
||||
background-clip: padding-box;
|
||||
box-shadow: 2px 0 4px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.dark #main-table tr.node-header td:first-child {
|
||||
background: rgb(75 85 99) !important;
|
||||
background-clip: padding-box;
|
||||
box-shadow: 2px 0 4px -2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Ensure full coverage on sticky cells */
|
||||
#main-table td:first-child,
|
||||
#main-table th:first-child {
|
||||
min-width: 140px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Solid edge on sticky columns to prevent bleed-through */
|
||||
#main-table td:first-child::before,
|
||||
#main-table th:first-child::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -20px; /* Extended further left */
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
background: inherit;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Additional left coverage with after element */
|
||||
#main-table td:first-child::after,
|
||||
#main-table th:first-child::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -5px;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
background: inherit;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Additional left edge coverage */
|
||||
#main-table td:first-child,
|
||||
#main-table th:first-child {
|
||||
border-left: 2px solid;
|
||||
}
|
||||
#main-table tbody tr:not(.node-header) td:first-child {
|
||||
border-left-color: white;
|
||||
}
|
||||
.dark #main-table tbody tr:not(.node-header) td:first-child {
|
||||
border-left-color: rgb(31 41 55);
|
||||
}
|
||||
#main-table tr.node-header td:first-child {
|
||||
border-left-color: rgb(229 231 235);
|
||||
}
|
||||
.dark #main-table tr.node-header td:first-child {
|
||||
border-left-color: rgb(75 85 99);
|
||||
}
|
||||
#main-table thead th:first-child {
|
||||
border-left-color: rgb(243 244 246);
|
||||
}
|
||||
.dark #main-table thead th:first-child {
|
||||
border-left-color: rgb(55 65 81);
|
||||
}
|
||||
|
||||
/* Remove the old node header mobile handling since we're using individual cells now */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -344,7 +232,7 @@
|
||||
</svg>
|
||||
<span id="diagnostics-badge" class="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full hidden animate-pulse"></span>
|
||||
</a>
|
||||
<button id="theme-toggle-button" type="button" class="p-1 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none" aria-label="Toggle theme">
|
||||
<button id="theme-toggle-button" type="button" class="p-1 h-11 w-11 sm:h-auto sm:w-auto rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none" aria-label="Toggle theme">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 block dark:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
@@ -382,84 +270,93 @@
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Loading node summary...</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-filter flex flex-col gap-3 mb-3 p-2 bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<div class="dashboard-filter-controls flex flex-wrap items-center gap-2">
|
||||
<input type="text" id="dashboard-search" placeholder="Search Name, ID, Node (use ',' for OR)" aria-label="Search virtual machines and containers" class="flex-grow min-w-[200px] p-1 px-2 h-7 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none" />
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="reset-filters-button" title="Reset Filters & Sort (Esc)" class="p-1.5 sm:p-1 h-9 w-9 sm:h-7 sm:w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
|
||||
</button>
|
||||
<button id="toggle-charts-button" title="Toggle Charts View" aria-label="Toggle between charts and metrics view" class="p-1.5 sm:p-1 h-9 w-9 sm:h-7 sm:w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto">
|
||||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||||
<line x1="6" y1="20" x2="6" y2="14"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="toggle-thresholds-button" title="Toggle Threshold Filters" aria-label="Toggle threshold filters panel" class="relative p-1.5 sm:p-1 h-9 w-9 sm:h-7 sm:w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
|
||||
<span class="relative inline-block">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto">
|
||||
<line x1="4" y1="21" x2="4" y2="14"></line>
|
||||
<line x1="4" y1="10" x2="4" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12" y2="3"></line>
|
||||
<line x1="20" y1="21" x2="20" y2="16"></line>
|
||||
<line x1="20" y1="12" x2="20" y2="3"></line>
|
||||
<line x1="1" y1="14" x2="7" y2="14"></line>
|
||||
<line x1="9" y1="8" x2="15" y2="8"></line>
|
||||
<line x1="17" y1="16" x2="23" y2="16"></line>
|
||||
<div class="dashboard-filter flex flex-col gap-2 mb-3 p-2 bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<div class="filter-controls-wrapper flex flex-wrap items-center gap-2">
|
||||
<input type="text" id="dashboard-search" placeholder="Search Name, ID, Node (use ',' for OR)" aria-label="Search virtual machines and containers" class="flex-1 min-w-[150px] max-w-full p-1 px-2 h-7 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none" />
|
||||
<button id="reset-filters-button" title="Reset Filters & Sort (Esc)" class="flex items-center justify-center p-1 h-11 w-11 sm:h-7 sm:w-7 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors flex-shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class=""><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 font-medium hidden sm:inline">View:</span>
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden flex-shrink-0">
|
||||
<label class="relative">
|
||||
<input type="checkbox" id="toggle-charts-checkbox" class="hidden peer">
|
||||
<span class="flex items-center justify-center gap-1 px-3 py-2 sm:px-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked:bg-blue-100 dark:peer-checked:bg-blue-800/50 peer-checked:text-blue-700 dark:peer-checked:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto] transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden">
|
||||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||||
<line x1="6" y1="20" x2="6" y2="14"></line>
|
||||
</svg>
|
||||
<span id="threshold-count-badge" class="absolute -top-1.5 -right-1.5 inline-flex items-center justify-center px-1 py-0.5 text-[9px] font-bold leading-none text-white transform translate-x-1/2 -translate-y-1/2 bg-blue-600 rounded-full hidden">0</span>
|
||||
</span>
|
||||
</button>
|
||||
<span class="hidden md:inline">Charts</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="relative">
|
||||
<input type="checkbox" id="toggle-thresholds-checkbox" class="hidden peer">
|
||||
<span class="flex items-center justify-center gap-1 px-3 py-2 sm:px-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked:bg-blue-100 dark:peer-checked:bg-blue-800/50 peer-checked:text-blue-700 dark:peer-checked:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto] transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden">
|
||||
<line x1="4" y1="21" x2="4" y2="14"></line>
|
||||
<line x1="4" y1="10" x2="4" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12" y2="3"></line>
|
||||
<line x1="20" y1="21" x2="20" y2="16"></line>
|
||||
<line x1="20" y1="12" x2="20" y2="3"></line>
|
||||
<line x1="1" y1="14" x2="7" y2="14"></line>
|
||||
<line x1="9" y1="8" x2="15" y2="8"></line>
|
||||
<line x1="17" y1="16" x2="23" y2="16"></line>
|
||||
</svg>
|
||||
<span class="hidden md:inline">Thresholds</span>
|
||||
<span id="threshold-count-badge" class="absolute -top-1 -right-1 inline-flex items-center justify-center px-1 py-0.5 text-[9px] font-bold leading-none text-white bg-blue-600 rounded-full hidden">0</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-toggles-wrapper flex items-center gap-4 flex-wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden">
|
||||
<div class="h-4 w-px bg-gray-300 dark:bg-gray-600 hidden lg:block flex-shrink-0"></div>
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 font-medium hidden lg:inline">Display:</span>
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden flex-shrink-0">
|
||||
<input type="radio" id="group-grouped" name="group-filter" value="grouped" class="hidden peer/grouped" checked>
|
||||
<label for="group-grouped" class="flex items-center justify-center gap-1 px-3 py-1 sm:px-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/grouped:bg-blue-100 dark:peer-checked/grouped:bg-blue-800/50 peer-checked/grouped:text-blue-700 dark:peer-checked/grouped:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="group-grouped" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/grouped:bg-blue-100 dark:peer-checked/grouped:bg-blue-800/50 peer-checked/grouped:text-blue-700 dark:peer-checked/grouped:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><path d="M4 4h4v4H4zM10 4h4v4h-4zM16 4h4v4h-4zM4 10h4v4H4zM10 10h4v4h-4zM16 10h4v4h-4zM4 16h4v4H4zM10 16h4v4h-4zM16 16h4v4h-4z"/></svg>
|
||||
<span class="hidden md:inline">Grouped</span>
|
||||
</label>
|
||||
<input type="radio" id="group-list" name="group-filter" value="list" class="hidden peer/list">
|
||||
<label for="group-list" class="flex items-center justify-center gap-1 px-3 py-1 sm:px-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/list:bg-blue-100 dark:peer-checked/list:bg-blue-800/50 peer-checked/list:text-blue-700 dark:peer-checked/list:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="group-list" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/list:bg-blue-100 dark:peer-checked/list:bg-blue-800/50 peer-checked/list:text-blue-700 dark:peer-checked/list:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>
|
||||
<span class="hidden md:inline">List</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 pl-2">
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden">
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden flex-shrink-0">
|
||||
<input type="radio" id="filter-all" name="type-filter" value="all" class="hidden peer/all" checked>
|
||||
<label for="filter-all" class="px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/all:bg-blue-100 dark:peer-checked/all:bg-blue-800/50 peer-checked/all:text-blue-700 dark:peer-checked/all:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="filter-all" class="flex items-center justify-center px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/all:bg-blue-100 dark:peer-checked/all:bg-blue-800/50 peer-checked/all:text-blue-700 dark:peer-checked/all:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<span class="inline">All</span>
|
||||
</label>
|
||||
<input type="radio" id="filter-vm" name="type-filter" value="vm" class="hidden peer/vm">
|
||||
<label for="filter-vm" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/vm:bg-blue-100 dark:peer-checked/vm:bg-blue-800/50 peer-checked/vm:text-blue-700 dark:peer-checked/vm:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="filter-vm" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/vm:bg-blue-100 dark:peer-checked/vm:bg-blue-800/50 peer-checked/vm:text-blue-700 dark:peer-checked/vm:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><rect width="20" height="8" x="2" y="2" rx="2" ry="2"/><rect width="20" height="8" x="2" y="14" rx="2" ry="2"/><line x1="6" x2="6.01" y1="6" y2="6"/><line x1="6" x2="6.01" y1="18" y2="18"/></svg>
|
||||
<span class="hidden md:inline">VM</span>
|
||||
</label>
|
||||
<input type="radio" id="filter-lxc" name="type-filter" value="lxc" class="hidden peer/lxc">
|
||||
<label for="filter-lxc" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/lxc:bg-green-100 dark:peer-checked/lxc:bg-green-800/50 peer-checked/lxc:text-green-700 dark:peer-checked/lxc:text-green-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="filter-lxc" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/lxc:bg-green-100 dark:peer-checked/lxc:bg-green-800/50 peer-checked/lxc:text-green-700 dark:peer-checked/lxc:text-green-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" x2="12" y1="22.08" y2="12"/></svg>
|
||||
<span class="hidden md:inline">LXC</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 pl-2">
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden">
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden flex-shrink-0">
|
||||
<input type="radio" id="filter-status-all" name="status-filter" value="all" class="hidden peer/status-all" checked>
|
||||
<label for="filter-status-all" class="px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/status-all:bg-blue-100 dark:peer-checked/status-all:bg-blue-800/50 peer-checked/status-all:text-blue-700 dark:peer-checked/status-all:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="filter-status-all" class="flex items-center justify-center px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/status-all:bg-blue-100 dark:peer-checked/status-all:bg-blue-800/50 peer-checked/status-all:text-blue-700 dark:peer-checked/status-all:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<span class="inline">All</span>
|
||||
</label>
|
||||
<input type="radio" id="filter-status-running" name="status-filter" value="running" class="hidden peer/status-running">
|
||||
<label for="filter-status-running" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/status-running:bg-green-100 dark:peer-checked/status-running:bg-green-800/50 peer-checked/status-running:text-green-700 dark:peer-checked/status-running:text-green-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="filter-status-running" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/status-running:bg-green-100 dark:peer-checked/status-running:bg-green-800/50 peer-checked/status-running:text-green-700 dark:peer-checked/status-running:text-green-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
<span class="hidden md:inline">Running</span>
|
||||
</label>
|
||||
<input type="radio" id="filter-status-stopped" name="status-filter" value="stopped" class="hidden peer/status-stopped">
|
||||
<label for="filter-status-stopped" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/status-stopped:bg-gray-100 dark:peer-checked/status-stopped:bg-gray-600/50 peer-checked/status-stopped:text-gray-700 dark:peer-checked/status-stopped:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="filter-status-stopped" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/status-stopped:bg-gray-100 dark:peer-checked/status-stopped:bg-gray-600/50 peer-checked/status-stopped:text-gray-700 dark:peer-checked/status-stopped:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/></svg>
|
||||
<span class="hidden md:inline">Stopped</span>
|
||||
</label>
|
||||
@@ -489,7 +386,7 @@
|
||||
</colgroup>
|
||||
<thead class="sticky top-0 z-10 bg-gray-100 dark:bg-gray-800">
|
||||
<tr class="text-[10px] sm:text-xs font-medium tracking-wider text-left text-gray-600 uppercase bg-gray-100 dark:bg-gray-700 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
|
||||
<th class="sortable p-1 px-2" data-sort="name" style="width: var(--name-col-width, 150px);">Name</th>
|
||||
<th class="sticky left-0 bg-gray-100 dark:bg-gray-800 z-20 sortable p-1 px-2" data-sort="name" style="width: var(--name-col-width, 150px);">Name</th>
|
||||
<th class="p-1 px-2" style="width: 50px;">Type</th>
|
||||
<th class="sortable p-1 px-2" data-sort="id" style="width: 50px;">ID</th>
|
||||
<th class="sortable p-1 px-2" data-sort="uptime" style="width: var(--uptime-col-width, 80px);" title="Uptime">Up</th>
|
||||
@@ -593,26 +490,26 @@
|
||||
<p id="backups-loading-message" class="text-gray-500 dark:text-gray-400 p-4 text-center mb-2">Loading backup status overview...</p>
|
||||
<div class="backups-filter flex flex-col md:flex-row justify-between items-stretch md:items-center gap-3 mb-3 p-2 bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<div class="dashboard-filter-controls flex-grow flex items-center gap-2">
|
||||
<input type="text" id="backups-search" placeholder="Search Name, ID, Node (use ',' for OR)" class="flex-grow p-1 px-2 h-7 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none" />
|
||||
<button id="reset-backups-filters-button" title="Reset Filters & Sort (Esc)" class="p-1 h-7 w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
|
||||
<input type="text" id="backups-search" placeholder="Search Name, ID, Node (use ',' for OR)" class="flex-grow min-w-[200px] max-w-full p-1 px-2 h-7 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none" />
|
||||
<button id="reset-backups-filters-button" title="Reset Filters & Sort (Esc)" class="flex items-center justify-center p-1 h-11 w-11 sm:h-7 sm:w-7 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter-toggles-wrapper flex items-center gap-4 flex-wrap">
|
||||
<div class="filter-toggles-wrapper flex items-center gap-3 sm:gap-4 flex-wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden">
|
||||
<input type="radio" id="backups-filter-type-all" name="backups-type-filter" value="all" class="hidden peer/btype-all" checked>
|
||||
<label for="backups-filter-type-all" class="px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/btype-all:bg-blue-100 dark:peer-checked/btype-all:bg-blue-800/50 peer-checked/btype-all:text-blue-700 dark:peer-checked/btype-all:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-type-all" class="flex items-center justify-center px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/btype-all:bg-blue-100 dark:peer-checked/btype-all:bg-blue-800/50 peer-checked/btype-all:text-blue-700 dark:peer-checked/btype-all:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<span class="hidden md:inline">All</span>
|
||||
<span class="inline md:hidden">All</span>
|
||||
</label>
|
||||
<input type="radio" id="backups-filter-type-vm" name="backups-type-filter" value="vm" class="hidden peer/btype-vm">
|
||||
<label for="backups-filter-type-vm" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/btype-vm:bg-blue-100 dark:peer-checked/btype-vm:bg-blue-800/50 peer-checked/btype-vm:text-blue-700 dark:peer-checked/btype-vm:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-type-vm" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/btype-vm:bg-blue-100 dark:peer-checked/btype-vm:bg-blue-800/50 peer-checked/btype-vm:text-blue-700 dark:peer-checked/btype-vm:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><rect width="20" height="8" x="2" y="2" rx="2" ry="2"/><rect width="20" height="8" x="2" y="14" rx="2" ry="2"/><line x1="6" x2="6.01" y1="6" y2="6"/><line x1="6" x2="6.01" y1="18" y2="18"/></svg>
|
||||
<span class="hidden md:inline">VM</span>
|
||||
</label>
|
||||
<input type="radio" id="backups-filter-type-ct" name="backups-type-filter" value="lxc" class="hidden peer/btype-ct">
|
||||
<label for="backups-filter-type-ct" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/btype-ct:bg-green-100 dark:peer-checked/btype-ct:bg-green-800/50 peer-checked/btype-ct:text-green-700 dark:peer-checked/btype-ct:text-green-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-type-ct" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/btype-ct:bg-green-100 dark:peer-checked/btype-ct:bg-green-800/50 peer-checked/btype-ct:text-green-700 dark:peer-checked/btype-ct:text-green-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" x2="12" y1="22.08" y2="12"/></svg>
|
||||
<span class="hidden md:inline">LXC</span>
|
||||
</label>
|
||||
@@ -621,32 +518,32 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden">
|
||||
<input type="radio" id="backups-filter-status-all" name="backups-status-filter" value="all" class="hidden peer/bhealth-all" checked>
|
||||
<label for="backups-filter-status-all" class="px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/bhealth-all:bg-blue-100 dark:peer-checked/bhealth-all:bg-blue-800/50 peer-checked/bhealth-all:text-blue-700 dark:peer-checked/bhealth-all:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-status-all" class="flex items-center justify-center px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/bhealth-all:bg-blue-100 dark:peer-checked/bhealth-all:bg-blue-800/50 peer-checked/bhealth-all:text-blue-700 dark:peer-checked/bhealth-all:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<span class="hidden md:inline">All</span>
|
||||
<span class="inline md:hidden">All</span>
|
||||
</label>
|
||||
<input type="radio" id="backups-filter-status-ok" name="backups-status-filter" value="ok" class="hidden peer/bhealth-ok">
|
||||
<label for="backups-filter-status-ok" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-ok:bg-green-100 dark:peer-checked/bhealth-ok:bg-green-800/50 peer-checked/bhealth-ok:text-green-700 dark:peer-checked/bhealth-ok:text-green-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-status-ok" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-ok:bg-green-100 dark:peer-checked/bhealth-ok:bg-green-800/50 peer-checked/bhealth-ok:text-green-700 dark:peer-checked/bhealth-ok:text-green-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden text-green-600 dark:text-green-400"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
<span class="hidden md:inline">OK</span>
|
||||
</label>
|
||||
<input type="radio" id="backups-filter-status-stale" name="backups-status-filter" value="stale" class="hidden peer/bhealth-stale">
|
||||
<label for="backups-filter-status-stale" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-stale:bg-yellow-100 dark:peer-checked/bhealth-stale:bg-yellow-800/50 peer-checked/bhealth-stale:text-yellow-700 dark:peer-checked/bhealth-stale:text-yellow-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-status-stale" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-stale:bg-yellow-100 dark:peer-checked/bhealth-stale:bg-yellow-800/50 peer-checked/bhealth-stale:text-yellow-700 dark:peer-checked/bhealth-stale:text-yellow-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden text-yellow-600 dark:text-yellow-400"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
<span class="hidden md:inline">Stale</span>
|
||||
</label>
|
||||
<input type="radio" id="backups-filter-status-old" name="backups-status-filter" value="old" class="hidden peer/bhealth-old">
|
||||
<label for="backups-filter-status-old" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-old:bg-orange-100 dark:peer-checked/bhealth-old:bg-orange-800/50 peer-checked/bhealth-old:text-orange-700 dark:peer-checked/bhealth-old:text-orange-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-status-old" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-old:bg-orange-100 dark:peer-checked/bhealth-old:bg-orange-800/50 peer-checked/bhealth-old:text-orange-700 dark:peer-checked/bhealth-old:text-orange-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden text-orange-600 dark:text-orange-400"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>
|
||||
<span class="hidden md:inline">Old</span>
|
||||
</label>
|
||||
<input type="radio" id="backups-filter-status-failed" name="backups-status-filter" value="failed" class="hidden peer/bhealth-failed">
|
||||
<label for="backups-filter-status-failed" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-failed:bg-red-100 dark:peer-checked/bhealth-failed:bg-red-800/50 peer-checked/bhealth-failed:text-red-700 dark:peer-checked/bhealth-failed:text-red-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-status-failed" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-failed:bg-red-100 dark:peer-checked/bhealth-failed:bg-red-800/50 peer-checked/bhealth-failed:text-red-700 dark:peer-checked/bhealth-failed:text-red-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden text-red-600 dark:text-red-400"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
<span class="hidden md:inline">Failed</span>
|
||||
</label>
|
||||
<input type="radio" id="backups-filter-status-none" name="backups-status-filter" value="none" class="hidden peer/bhealth-none">
|
||||
<label for="backups-filter-status-none" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-none:bg-gray-100 dark:peer-checked/bhealth-none:bg-gray-600/50 peer-checked/bhealth-none:text-gray-700 dark:peer-checked/bhealth-none:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
|
||||
<label for="backups-filter-status-none" class="flex items-center justify-center gap-1 px-2 py-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/bhealth-none:bg-gray-100 dark:peer-checked/bhealth-none:bg-gray-600/50 peer-checked/bhealth-none:text-gray-700 dark:peer-checked/bhealth-none:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none min-h-[44px] sm:min-h-[auto]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden text-gray-400 dark:text-gray-500"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/></svg>
|
||||
<span class="hidden md:inline">None</span>
|
||||
</label>
|
||||
@@ -662,7 +559,7 @@
|
||||
<table id="backups-overview-table" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="sticky top-0 z-10 bg-gray-100 dark:bg-gray-800">
|
||||
<tr class="text-xs font-medium tracking-wider text-left text-gray-600 uppercase bg-gray-100 dark:bg-gray-700 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
|
||||
<th scope="col" class="sortable p-1 px-2 cursor-pointer select-none whitespace-nowrap" data-sort="guestName">Guest Name</th>
|
||||
<th scope="col" class="sticky left-0 bg-gray-100 dark:bg-gray-800 z-20 sortable p-1 px-2 cursor-pointer select-none whitespace-nowrap" data-sort="guestName">Guest Name</th>
|
||||
<th scope="col" class="sortable p-1 px-2 cursor-pointer select-none whitespace-nowrap" data-sort="guestId">ID</th>
|
||||
<th scope="col" class="sortable p-1 px-2 cursor-pointer select-none whitespace-nowrap" data-sort="guestType">Type</th>
|
||||
<th scope="col" class="sortable p-1 px-2 cursor-pointer select-none whitespace-nowrap" data-sort="node">Node</th>
|
||||
|
||||
+17
-10
@@ -254,7 +254,7 @@ PulseApp.ui.backups = (() => {
|
||||
sevenDayDots += '</div>';
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="p-1 px-2 whitespace-nowrap font-medium text-gray-900 dark:text-gray-100" title="${guestStatus.guestName}">${guestStatus.guestName}</td>
|
||||
<td class="sticky left-0 bg-white dark:bg-gray-800 z-10 p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0 text-gray-900 dark:text-gray-100" title="${guestStatus.guestName}">${guestStatus.guestName}</td>
|
||||
<td class="p-1 px-2 text-gray-500 dark:text-gray-400">${guestStatus.guestId}</td>
|
||||
<td class="p-1 px-2">${typeIcon}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-500 dark:text-gray-400">${guestStatus.node}</td>
|
||||
@@ -299,6 +299,11 @@ PulseApp.ui.backups = (() => {
|
||||
console.error("UI elements for Backups tab not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the scrollable container
|
||||
const scrollableContainer = PulseApp.utils.getScrollableParent(tableBody) ||
|
||||
tableContainer.closest('.overflow-x-auto') ||
|
||||
tableContainer;
|
||||
|
||||
const { allGuests, initialDataReceived, tasksByGuest, snapshotsByGuest, dayBoundaries, threeDaysAgo, sevenDaysAgo } = _getInitialBackupData();
|
||||
|
||||
@@ -364,15 +369,16 @@ PulseApp.ui.backups = (() => {
|
||||
const sortStateBackups = PulseApp.state.getSortState('backups');
|
||||
const sortedBackupStatus = PulseApp.utils.sortData(filteredBackupStatus, sortStateBackups.column, sortStateBackups.direction, 'backups');
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
if (sortedBackupStatus.length > 0) {
|
||||
sortedBackupStatus.forEach(guestStatus => {
|
||||
const row = _renderBackupTableRow(guestStatus);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
noDataMsg.classList.add('hidden');
|
||||
tableContainer.classList.remove('hidden');
|
||||
} else {
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
tableBody.innerHTML = '';
|
||||
if (sortedBackupStatus.length > 0) {
|
||||
sortedBackupStatus.forEach(guestStatus => {
|
||||
const row = _renderBackupTableRow(guestStatus);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
noDataMsg.classList.add('hidden');
|
||||
tableContainer.classList.remove('hidden');
|
||||
} else {
|
||||
tableContainer.classList.add('hidden');
|
||||
let emptyMessage = "No backup information found for any guests.";
|
||||
if (backupStatusByGuest.length > 0 && filteredBackupStatus.length === 0) { // Data exists, but filters hide all
|
||||
@@ -393,6 +399,7 @@ PulseApp.ui.backups = (() => {
|
||||
noDataMsg.textContent = emptyMessage;
|
||||
noDataMsg.classList.remove('hidden');
|
||||
}
|
||||
}); // End of preserveScrollPosition
|
||||
|
||||
const backupsSortColumn = sortStateBackups.column;
|
||||
const backupsHeader = document.querySelector(`#backups-overview-table th[data-sort="${backupsSortColumn}"]`);
|
||||
|
||||
@@ -312,18 +312,13 @@ PulseApp.ui.common = (() => {
|
||||
function generateNodeGroupHeaderCellHTML(text, colspan, cellTag = 'td') {
|
||||
const baseClasses = 'py-0.5 px-2 text-left font-medium text-xs sm:text-sm text-gray-700 dark:text-gray-300';
|
||||
|
||||
// On mobile, create individual cells so first one can be sticky
|
||||
if (window.innerWidth < 768) {
|
||||
let html = `<${cellTag} class="${baseClasses} bg-gray-200 dark:bg-gray-700">${text}</${cellTag}>`;
|
||||
// Add empty cells for remaining columns
|
||||
for (let i = 1; i < colspan; i++) {
|
||||
html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700"></${cellTag}>`;
|
||||
}
|
||||
return html;
|
||||
// Always create individual cells so first one can be sticky
|
||||
let html = `<${cellTag} class="sticky left-0 bg-gray-200 dark:bg-gray-700 z-10 ${baseClasses}">${text}</${cellTag}>`;
|
||||
// Add empty cells for remaining columns
|
||||
for (let i = 1; i < colspan; i++) {
|
||||
html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700"></${cellTag}>`;
|
||||
}
|
||||
|
||||
// Desktop: use colspan
|
||||
return `<${cellTag} colspan="${colspan}" class="${baseClasses} bg-gray-200 dark:bg-gray-700 node-header-cell">${text}</${cellTag}>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -67,9 +67,9 @@ PulseApp.ui.dashboard = (() => {
|
||||
}
|
||||
|
||||
// Initialize charts toggle
|
||||
const chartsToggleButton = document.getElementById('toggle-charts-button');
|
||||
if (chartsToggleButton) {
|
||||
chartsToggleButton.addEventListener('click', toggleChartsMode);
|
||||
const chartsToggleCheckbox = document.getElementById('toggle-charts-checkbox');
|
||||
if (chartsToggleCheckbox) {
|
||||
chartsToggleCheckbox.addEventListener('change', toggleChartsMode);
|
||||
}
|
||||
|
||||
// Initialize mobile scroll indicators
|
||||
@@ -524,18 +524,21 @@ PulseApp.ui.dashboard = (() => {
|
||||
} else {
|
||||
row.className = 'border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50';
|
||||
}
|
||||
|
||||
|
||||
// Update specific cells that might have changed
|
||||
const cells = row.querySelectorAll('td');
|
||||
|
||||
// Ensure name cell keeps sticky styling even after row class updates
|
||||
if (cells[0]) {
|
||||
cells[0].className = 'sticky left-0 bg-white dark:bg-gray-800 z-10 p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0';
|
||||
}
|
||||
if (cells.length >= 10) {
|
||||
// Cell order: name(0), type(1), id(2), uptime(3), cpu(4), memory(5), disk(6), diskread(7), diskwrite(8), netin(9), netout(10)
|
||||
|
||||
// Update name (cell 0)
|
||||
const nameCell = cells[0];
|
||||
|
||||
if (nameCell.textContent !== guest.name) {
|
||||
nameCell.textContent = guest.name;
|
||||
nameCell.title = guest.name;
|
||||
// Update name (cell 0) content only (styling handled above)
|
||||
if (cells[0].textContent !== guest.name) {
|
||||
cells[0].textContent = guest.name;
|
||||
cells[0].title = guest.name;
|
||||
}
|
||||
|
||||
// Ensure ID cell (2) has proper classes
|
||||
@@ -682,6 +685,11 @@ PulseApp.ui.dashboard = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the scrollable container
|
||||
const scrollableContainer = PulseApp.utils.getScrollableParent(tableBodyEl) ||
|
||||
document.querySelector('.table-container') ||
|
||||
tableBodyEl.closest('.overflow-x-auto');
|
||||
|
||||
// Show loading skeleton if no data yet
|
||||
const currentData = PulseApp.state.get('dashboardData');
|
||||
if (!currentData || currentData.length === 0) {
|
||||
@@ -746,50 +754,56 @@ PulseApp.ui.dashboard = (() => {
|
||||
visibleCount = sortedData.length;
|
||||
sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase()));
|
||||
} else if (needsFullRebuild) {
|
||||
// Full rebuild for normal rendering
|
||||
if (groupByNode) {
|
||||
const groupRenderResult = _renderGroupedByNode(tableBodyEl, sortedData, createGuestRow);
|
||||
visibleCount = groupRenderResult.visibleCount;
|
||||
visibleNodes = groupRenderResult.visibleNodes;
|
||||
} else {
|
||||
PulseApp.utils.renderTableBody(tableBodyEl, sortedData, createGuestRow, "No matching guests found.", 11);
|
||||
visibleCount = sortedData.length;
|
||||
sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase()));
|
||||
}
|
||||
// Full rebuild for normal rendering with scroll preservation
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
if (groupByNode) {
|
||||
const groupRenderResult = _renderGroupedByNode(tableBodyEl, sortedData, createGuestRow);
|
||||
visibleCount = groupRenderResult.visibleCount;
|
||||
visibleNodes = groupRenderResult.visibleNodes;
|
||||
} else {
|
||||
PulseApp.utils.renderTableBody(tableBodyEl, sortedData, createGuestRow, "No matching guests found.", 11);
|
||||
visibleCount = sortedData.length;
|
||||
sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase()));
|
||||
}
|
||||
});
|
||||
previousGroupByNode = groupByNode;
|
||||
} else {
|
||||
// Incremental update using DOM diffing
|
||||
const result = _updateTableIncremental(tableBodyEl, sortedData, createGuestRow, groupByNode);
|
||||
visibleCount = result.visibleCount;
|
||||
visibleNodes = result.visibleNodes;
|
||||
// Incremental update using DOM diffing with scroll preservation
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
const result = _updateTableIncremental(tableBodyEl, sortedData, createGuestRow, groupByNode);
|
||||
visibleCount = result.visibleCount;
|
||||
visibleNodes = result.visibleNodes;
|
||||
});
|
||||
}
|
||||
|
||||
previousTableData = sortedData;
|
||||
|
||||
if (visibleCount === 0 && tableBodyEl) {
|
||||
const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : [];
|
||||
const activeThresholds = Object.entries(thresholdState).filter(([_, state]) => state.value > 0);
|
||||
const thresholdTexts = activeThresholds.map(([key, state]) => {
|
||||
return `${PulseApp.utils.getReadableThresholdName(key)}>=${PulseApp.utils.formatThresholdValue(key, state.value)}`;
|
||||
});
|
||||
|
||||
const hasFilters = filterGuestType !== FILTER_ALL || filterStatus !== FILTER_ALL || textSearchTerms.length > 0 || activeThresholds.length > 0;
|
||||
|
||||
if (PulseApp.ui.emptyStates) {
|
||||
const context = {
|
||||
filterType: filterGuestType,
|
||||
filterStatus: filterStatus,
|
||||
searchTerms: textSearchTerms,
|
||||
thresholds: thresholdTexts
|
||||
};
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : [];
|
||||
const activeThresholds = Object.entries(thresholdState).filter(([_, state]) => state.value > 0);
|
||||
const thresholdTexts = activeThresholds.map(([key, state]) => {
|
||||
return `${PulseApp.utils.getReadableThresholdName(key)}>=${PulseApp.utils.formatThresholdValue(key, state.value)}`;
|
||||
});
|
||||
|
||||
const emptyType = hasFilters ? 'no-results' : 'no-guests';
|
||||
tableBodyEl.innerHTML = PulseApp.ui.emptyStates.createTableEmptyState(emptyType, context, 11);
|
||||
} else {
|
||||
// Fallback to simple message
|
||||
let message = hasFilters ? "No guests match the current filters." : "No guests found.";
|
||||
tableBodyEl.innerHTML = `<tr><td colspan="11" class="p-4 text-center text-gray-500 dark:text-gray-400">${message}</td></tr>`;
|
||||
}
|
||||
const hasFilters = filterGuestType !== FILTER_ALL || filterStatus !== FILTER_ALL || textSearchTerms.length > 0 || activeThresholds.length > 0;
|
||||
|
||||
if (PulseApp.ui.emptyStates) {
|
||||
const context = {
|
||||
filterType: filterGuestType,
|
||||
filterStatus: filterStatus,
|
||||
searchTerms: textSearchTerms,
|
||||
thresholds: thresholdTexts
|
||||
};
|
||||
|
||||
const emptyType = hasFilters ? 'no-results' : 'no-guests';
|
||||
tableBodyEl.innerHTML = PulseApp.ui.emptyStates.createTableEmptyState(emptyType, context, 11);
|
||||
} else {
|
||||
// Fallback to simple message
|
||||
let message = hasFilters ? "No guests match the current filters." : "No guests found.";
|
||||
tableBodyEl.innerHTML = `<tr><td colspan="11" class="p-4 text-center text-gray-500 dark:text-gray-400">${message}</td></tr>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateDashboardStatusMessage(statusElementEl, visibleCount, visibleNodes, groupByNode, filterGuestType, filterStatus, searchInput, thresholdState);
|
||||
@@ -943,7 +957,7 @@ PulseApp.ui.dashboard = (() => {
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0" title="${guest.name}">${guest.name}</td>
|
||||
<td class="sticky left-0 bg-white dark:bg-gray-800 z-10 p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0" title="${guest.name}">${guest.name}</td>
|
||||
<td class="p-1 px-2">${typeIcon}</td>
|
||||
<td class="p-1 px-2">${guest.id}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis">${uptimeDisplay}</td>
|
||||
@@ -980,16 +994,13 @@ PulseApp.ui.dashboard = (() => {
|
||||
|
||||
function toggleChartsMode() {
|
||||
const mainContainer = document.getElementById('main');
|
||||
const button = document.getElementById('toggle-charts-button');
|
||||
const checkbox = document.getElementById('toggle-charts-checkbox');
|
||||
const label = checkbox ? checkbox.parentElement : null;
|
||||
|
||||
if (mainContainer.classList.contains('charts-mode')) {
|
||||
// Switch to metrics mode
|
||||
mainContainer.classList.remove('charts-mode');
|
||||
button.title = 'Toggle Charts View';
|
||||
} else {
|
||||
if (checkbox && checkbox.checked) {
|
||||
// Switch to charts mode
|
||||
mainContainer.classList.add('charts-mode');
|
||||
button.title = 'Toggle Metrics View';
|
||||
if (label) label.title = 'Toggle Metrics View';
|
||||
|
||||
// Immediately render charts when switching to charts mode
|
||||
if (PulseApp.charts) {
|
||||
@@ -997,6 +1008,10 @@ PulseApp.ui.dashboard = (() => {
|
||||
PulseApp.charts.updateAllCharts();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Switch to metrics mode
|
||||
mainContainer.classList.remove('charts-mode');
|
||||
if (label) label.title = 'Toggle Charts View';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ PulseApp.ui.nodes = (() => {
|
||||
<span class="capitalize">${statusText}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap font-medium text-gray-900 dark:text-gray-100" title="${node.node || 'N/A'}">${node.node || 'N/A'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0 text-gray-900 dark:text-gray-100" title="${node.node || 'N/A'}">${node.node || 'N/A'}</td>
|
||||
<td class="p-1 px-2 min-w-[200px]">${cpuBarHTML}</td>
|
||||
<td class="p-1 px-2 min-w-[200px]">${memoryBarHTML}</td>
|
||||
<td class="p-1 px-2 min-w-[200px]">${diskBarHTML}</td>
|
||||
@@ -147,7 +147,13 @@ PulseApp.ui.nodes = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = ''; // Clear previous content
|
||||
// Find the scrollable container
|
||||
const scrollableContainer = PulseApp.utils.getScrollableParent(container) ||
|
||||
container.closest('.overflow-x-auto') ||
|
||||
container.parentElement;
|
||||
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
container.innerHTML = ''; // Clear previous content
|
||||
|
||||
const numNodes = nodes.length;
|
||||
const isMobile = window.innerWidth < 640; // sm breakpoint
|
||||
@@ -197,6 +203,7 @@ PulseApp.ui.nodes = (() => {
|
||||
});
|
||||
container.appendChild(gridDiv);
|
||||
}
|
||||
}); // End of preserveScrollPosition
|
||||
}
|
||||
|
||||
function createCondensedNodeCard(node) {
|
||||
@@ -246,7 +253,14 @@ PulseApp.ui.nodes = (() => {
|
||||
console.log('[Nodes] Node table not found - using summary cards display instead');
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Find the scrollable container
|
||||
const scrollableContainer = PulseApp.utils.getScrollableParent(tbody) ||
|
||||
tbody.closest('.overflow-x-auto') ||
|
||||
tbody.parentElement;
|
||||
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="p-4 text-center text-gray-500 dark:text-gray-400">No nodes found or data unavailable</td></tr>';
|
||||
@@ -290,6 +304,7 @@ PulseApp.ui.nodes = (() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
}); // End of preserveScrollPosition
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
+17
-2
@@ -367,9 +367,15 @@ PulseApp.ui.pbs = (() => {
|
||||
const table = tableBody.closest('table');
|
||||
const tableId = table?.id;
|
||||
const isShowMoreExpanded = tableId ? expandedShowMoreState.has(tableId) : false;
|
||||
|
||||
// Find the scrollable container
|
||||
const scrollableContainer = PulseApp.utils.getScrollableParent(tableBody) ||
|
||||
parentSectionElement.closest('.overflow-x-auto') ||
|
||||
parentSectionElement;
|
||||
|
||||
// Use global expanded state instead of scanning DOM
|
||||
tableBody.innerHTML = '';
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
const tasks = fullTasksArray || [];
|
||||
|
||||
@@ -478,11 +484,19 @@ PulseApp.ui.pbs = (() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}); // End of preserveScrollPosition
|
||||
}
|
||||
|
||||
const _populateDsTableBody = (dsTableBody, datastores, statusText, showDetails) => {
|
||||
if (!dsTableBody) return;
|
||||
dsTableBody.innerHTML = '';
|
||||
|
||||
// Find the scrollable container
|
||||
const scrollableContainer = PulseApp.utils.getScrollableParent(dsTableBody) ||
|
||||
dsTableBody.closest('.overflow-x-auto') ||
|
||||
dsTableBody.parentElement;
|
||||
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
dsTableBody.innerHTML = '';
|
||||
|
||||
if (showDetails && datastores) {
|
||||
if (datastores.length === 0) {
|
||||
@@ -566,6 +580,7 @@ PulseApp.ui.pbs = (() => {
|
||||
cell.className = `px-4 py-4 ${CSS_CLASSES.TEXT_SM} ${CSS_CLASSES.TEXT_GRAY_400} text-center`;
|
||||
cell.textContent = statusText;
|
||||
}
|
||||
}); // End of preserveScrollPosition
|
||||
};
|
||||
|
||||
const _populateInstanceTaskSections = (detailsContainer, instanceId, pbsInstance, statusText, showDetails) => {
|
||||
|
||||
@@ -115,8 +115,16 @@ PulseApp.ui.storage = (() => {
|
||||
function updateStorageInfo() {
|
||||
const contentDiv = document.getElementById('storage-info-content');
|
||||
if (!contentDiv) return;
|
||||
contentDiv.innerHTML = '';
|
||||
contentDiv.className = '';
|
||||
|
||||
// Find the scrollable container
|
||||
const scrollableContainer = PulseApp.utils.getScrollableParent(contentDiv) ||
|
||||
contentDiv.closest('.overflow-x-auto') ||
|
||||
contentDiv.parentElement;
|
||||
|
||||
// Preserve scroll position while updating
|
||||
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
|
||||
contentDiv.innerHTML = '';
|
||||
contentDiv.className = '';
|
||||
|
||||
const nodes = PulseApp.state.get('nodesData') || [];
|
||||
|
||||
@@ -200,6 +208,7 @@ PulseApp.ui.storage = (() => {
|
||||
table.appendChild(thead);
|
||||
table.appendChild(tbody);
|
||||
contentDiv.appendChild(table);
|
||||
}); // End of preserveScrollPosition
|
||||
}
|
||||
|
||||
function _createStorageRow(store) {
|
||||
@@ -222,7 +231,7 @@ PulseApp.ui.storage = (() => {
|
||||
const contentBadges = getContentBadgesHTML(store.content);
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-900 dark:text-gray-100 font-medium">${store.storage || 'N/A'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0 text-gray-900 dark:text-gray-100">${store.storage || 'N/A'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300 text-xs flex items-center">${contentBadges}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300">${store.type || 'N/A'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap storage-tooltip-trigger cursor-default" data-tooltip="${sharedIconTooltip}">${sharedIcon}</td>
|
||||
|
||||
@@ -10,7 +10,7 @@ PulseApp.ui.thresholds = (() => {
|
||||
|
||||
function init() {
|
||||
thresholdRow = document.getElementById('threshold-slider-row');
|
||||
toggleThresholdsButton = document.getElementById('toggle-thresholds-button');
|
||||
toggleThresholdsButton = document.getElementById('toggle-thresholds-checkbox');
|
||||
thresholdBadge = document.getElementById('threshold-count-badge');
|
||||
|
||||
sliders = {
|
||||
@@ -30,12 +30,12 @@ PulseApp.ui.thresholds = (() => {
|
||||
updateThresholdRowVisibility();
|
||||
|
||||
if (toggleThresholdsButton) {
|
||||
toggleThresholdsButton.addEventListener('click', () => {
|
||||
PulseApp.state.set('isThresholdRowVisible', !PulseApp.state.get('isThresholdRowVisible'));
|
||||
toggleThresholdsButton.addEventListener('change', () => {
|
||||
PulseApp.state.set('isThresholdRowVisible', toggleThresholdsButton.checked);
|
||||
updateThresholdRowVisibility();
|
||||
});
|
||||
} else {
|
||||
console.warn('#toggle-thresholds-button not found.');
|
||||
console.warn('#toggle-thresholds-checkbox not found.');
|
||||
}
|
||||
|
||||
_setupSliderListeners();
|
||||
@@ -126,10 +126,8 @@ PulseApp.ui.thresholds = (() => {
|
||||
if (thresholdRow) {
|
||||
thresholdRow.classList.toggle('hidden', !isVisible);
|
||||
if (toggleThresholdsButton) {
|
||||
toggleThresholdsButton.classList.toggle('bg-blue-100', isVisible);
|
||||
toggleThresholdsButton.classList.toggle('dark:bg-blue-800/50', isVisible);
|
||||
toggleThresholdsButton.classList.toggle('text-blue-700', isVisible);
|
||||
toggleThresholdsButton.classList.toggle('dark:text-blue-300', isVisible);
|
||||
// Update checkbox state to match visibility
|
||||
toggleThresholdsButton.checked = isVisible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-1
@@ -285,6 +285,43 @@ PulseApp.utils = (() => {
|
||||
// Debounced version for resize events
|
||||
const updateProgressBarTextsDebounced = debounce(updateProgressBarTexts, 100);
|
||||
|
||||
// Scroll position preservation for table updates
|
||||
function preserveScrollPosition(element, updateFn) {
|
||||
if (!element) {
|
||||
updateFn();
|
||||
return;
|
||||
}
|
||||
|
||||
// Save current scroll positions
|
||||
const scrollLeft = element.scrollLeft;
|
||||
const scrollTop = element.scrollTop;
|
||||
|
||||
// Execute the update function
|
||||
updateFn();
|
||||
|
||||
// Restore scroll positions after a microtask to ensure DOM is updated
|
||||
Promise.resolve().then(() => {
|
||||
element.scrollLeft = scrollLeft;
|
||||
element.scrollTop = scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
// Get the scrollable parent of a table
|
||||
function getScrollableParent(element) {
|
||||
if (!element) return null;
|
||||
|
||||
let parent = element.parentElement;
|
||||
while (parent) {
|
||||
const style = window.getComputedStyle(parent);
|
||||
if (style.overflowX === 'auto' || style.overflowX === 'scroll' ||
|
||||
style.overflowY === 'auto' || style.overflowY === 'scroll') {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return the public API for this module
|
||||
return {
|
||||
sanitizeForId: (str) => str.replace(/[^a-zA-Z0-9-]/g, '-'),
|
||||
@@ -303,6 +340,8 @@ PulseApp.utils = (() => {
|
||||
renderTableBody,
|
||||
debounce,
|
||||
updateProgressBarTexts,
|
||||
updateProgressBarTextsDebounced
|
||||
updateProgressBarTextsDebounced,
|
||||
preserveScrollPosition,
|
||||
getScrollableParent
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user