v1.3.1: VM list node/server filtering + responsive toolbar redesign
- Add server filter dropdown (populated dynamically from loaded VMs) - Add node filter dropdown (narrows to nodes of selected server) - Both filters persisted in URL params and restored on reload - Toolbar restructured into 2 rows: filters row + actions row - Responsive breakpoints: ≤900px text labels hidden, ≤768px 2-per-row, ≤480px full-width filters, stats hidden on mobile - Snapshots toolbar migrated to inline flex (no legacy class dependency) - Update VERSION, CHANGELOG, README, WIKI to v1.3.1
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -4,6 +4,20 @@ All notable changes to PVEmanager will be documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [v1.3.1] - 2026-04-05
|
||||
|
||||
### 🔍 VM List — Node Filtering & Responsive Toolbar
|
||||
|
||||
- **Server filter** — new dropdown to filter the VM list by Proxmox server/cluster; populated dynamically from loaded VMs
|
||||
- **Node filter** — new dropdown to filter by Proxmox node; automatically narrows to nodes belonging to the selected server
|
||||
- Both filters are saved in URL params (`?server=...&node=...`) and restored on page reload
|
||||
- **Toolbar redesigned into 2 rows** — row 1: all filters (type / status / server / node / search with icon + stats); row 2: select-all checkbox, pagination, action buttons
|
||||
- **Responsive breakpoints** — at ≤900px button labels hide; at ≤768px filters wrap 2-per-row; at ≤480px each filter is full-width; stats counter hidden on mobile
|
||||
- `populateServerFilter()` and `populateNodeFilter()` helper functions populate dropdowns from loaded VM data
|
||||
- Snapshots toolbar migrated from legacy `.vm-toolbar-left/.vm-toolbar-right` classes to inline flex, unaffected by toolbar refactor
|
||||
|
||||
---
|
||||
|
||||
## [v1.3.0] - 2026-03-27
|
||||
|
||||
### 🖥️ Console — VNC & Terminal Fixes
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Modern web panel for managing Proxmox servers, virtual machines and LXC containers**
|
||||
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](LICENSE)
|
||||
[](compose.yml)
|
||||
[](backend/)
|
||||
|
||||
2
WIKI.md
2
WIKI.md
@@ -1,6 +1,6 @@
|
||||
# 📖 PVEmanager - Documentation
|
||||
|
||||
> Complete guide for installation, configuration and usage of PVEmanager v1.3.0
|
||||
> Complete guide for installation, configuration and usage of PVEmanager v1.3.1
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -41,26 +41,46 @@
|
||||
|
||||
.vm-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
/* Строка 1: фильтры */
|
||||
.vm-filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.vm-toolbar-left {
|
||||
/* Строка 2: чекбокс + пагинация + кнопки */
|
||||
.vm-actions-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.vm-toolbar-right {
|
||||
.vm-actions-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.vm-actions-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Оставляем для обратной совместимости (используется в switchVMTab) */
|
||||
.vm-toolbar-left, .vm-toolbar-right { display: contents; }
|
||||
|
||||
.vm-filter-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -394,11 +414,14 @@
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding: 0.4rem 0.625rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
min-width: 120px;
|
||||
flex: 1 1 110px;
|
||||
min-width: 100px;
|
||||
max-width: 180px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.vm-filter-select:hover {
|
||||
@@ -415,10 +438,12 @@
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
min-width: 200px;
|
||||
font-size: 0.8125rem;
|
||||
flex: 2 1 160px;
|
||||
min-width: 140px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.vm-search-input:focus {
|
||||
@@ -593,6 +618,21 @@
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Адаптивность: планшеты */
|
||||
@media (max-width: 900px) {
|
||||
.vm-toolbar {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.vm-filter-select {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Мобильная адаптация */
|
||||
@media (max-width: 768px) {
|
||||
.vm-tabs {
|
||||
@@ -607,29 +647,44 @@
|
||||
}
|
||||
|
||||
.vm-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0.75rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
}
|
||||
|
||||
.vm-toolbar-left,
|
||||
.vm-toolbar-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
.vm-filter-row {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.vm-toolbar-right {
|
||||
.vm-filter-select {
|
||||
flex: 1 1 calc(50% - 0.375rem);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.vm-search-input {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vm-stats-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vm-actions-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.vm-actions-right {
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.vm-pagination {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.vm-table th,
|
||||
.vm-table td {
|
||||
padding: 0.5rem 0.25rem;
|
||||
padding: 0.5rem 0.375rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@@ -655,14 +710,6 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.vm-row-actions {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.vm-action-btn {
|
||||
padding: 0.35rem;
|
||||
}
|
||||
|
||||
.task-progress-toast {
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
@@ -672,17 +719,13 @@
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.vm-toolbar-left {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
.vm-filter-select {
|
||||
flex: 1 1 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.vm-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vm-search input {
|
||||
width: 100%;
|
||||
.vm-actions-right .btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Скрываем ещё больше колонок */
|
||||
@@ -737,14 +780,8 @@
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="vm-toolbar">
|
||||
<div class="vm-toolbar-left">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;" title="{{ t('select_all') }}">
|
||||
<input type="checkbox" id="selectAll" class="vm-checkbox" onclick="toggleSelectAll(event)">
|
||||
<span style="color: var(--text-secondary); font-size: 0.875rem;">
|
||||
<span id="selectedCountLabel">0</span> {{ t('selected') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Строка 1: Фильтры -->
|
||||
<div class="vm-filter-row">
|
||||
<select id="typeFilter" class="vm-filter-select" onchange="filterByType(this.value)">
|
||||
<option value="">{{ t('all_types') }}</option>
|
||||
<option value="qemu">KVM (QEMU)</option>
|
||||
@@ -757,48 +794,67 @@
|
||||
<option value="stopped">{{ t('inactive') }}</option>
|
||||
</select>
|
||||
|
||||
<input type="text" id="searchInput" class="vm-search-input" placeholder="{{ t('search') }}..." oninput="searchVMs(this.value)">
|
||||
<select id="serverFilter" class="vm-filter-select" onchange="filterByServer(this.value)">
|
||||
<option value="">{{ t('all_servers') }}</option>
|
||||
</select>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; color: var(--text-muted); font-size: 0.875rem;">
|
||||
<span>{{ t('total') }}: <strong id="totalCount">0</strong></span>
|
||||
<span style="color: var(--success);"><i class="fa-solid fa-play"></i> <span id="runningCount">0</span></span>
|
||||
<span style="color: var(--text-muted);"><i class="fa-solid fa-stop"></i> <span id="stoppedCount">0</span></span>
|
||||
<select id="nodeFilter" class="vm-filter-select" onchange="filterByNode(this.value)">
|
||||
<option value="">{{ t('all_nodes') }}</option>
|
||||
</select>
|
||||
|
||||
<div style="position: relative; flex: 2 1 160px; min-width: 140px; display: flex; align-items: center;">
|
||||
<i class="fa-solid fa-magnifying-glass" style="position: absolute; left: 0.625rem; color: var(--text-muted); font-size: 0.75rem; pointer-events: none;"></i>
|
||||
<input type="text" id="searchInput" class="vm-search-input" placeholder="{{ t('search') }}..." oninput="searchVMs(this.value)" style="padding-left: 1.875rem; width: 100%;">
|
||||
</div>
|
||||
|
||||
<div class="vm-stats-row" style="display: flex; align-items: center; gap: 0.625rem; color: var(--text-muted); font-size: 0.8rem; white-space: nowrap; margin-left: auto;">
|
||||
<span>{{ t('total') }}: <strong id="totalCount" style="color: var(--text-primary);">0</strong></span>
|
||||
<span style="color: var(--success);"><i class="fa-solid fa-circle" style="font-size: 0.5rem;"></i> <span id="runningCount">0</span></span>
|
||||
<span style="color: var(--danger);"><i class="fa-solid fa-circle" style="font-size: 0.5rem;"></i> <span id="stoppedCount">0</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vm-toolbar-right">
|
||||
<div class="vm-pagination">
|
||||
<span>{{ t('per_page') }}:</span>
|
||||
<select id="pageSize" onchange="changePageSize(this.value)">
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<!-- Строка 2: Выбор + Пагинация + Кнопки -->
|
||||
<div class="vm-actions-row">
|
||||
<div class="vm-actions-left">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; white-space: nowrap;" title="{{ t('select_all') }}">
|
||||
<input type="checkbox" id="selectAll" class="vm-checkbox" onclick="toggleSelectAll(event)">
|
||||
<span style="color: var(--text-secondary); font-size: 0.8rem;">
|
||||
<span id="selectedCountLabel">0</span> {{ t('selected') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="vm-pagination">
|
||||
<span>{{ t('per_page') }}:</span>
|
||||
<select id="pageSize" onchange="changePageSize(this.value)">
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="vm-pagination-nav">
|
||||
<button class="vm-pagination-btn" id="prevPage" onclick="prevPage()" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="number" id="currentPage" value="1" min="1" style="width: 44px; text-align: center; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; padding: 0.2rem; color: var(--text-primary); font-size: 0.8rem;" onchange="goToPage(this.value)">
|
||||
<span style="color: var(--text-muted); font-size: 0.8rem;">{{ t('of') }} <span id="totalPages">1</span></span>
|
||||
<button class="vm-pagination-btn" id="nextPage" onclick="nextPage()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vm-pagination-nav">
|
||||
<button class="vm-pagination-btn" id="prevPage" onclick="prevPage()" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="number" id="currentPage" value="1" min="1" style="width: 50px; text-align: center; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; padding: 0.25rem; color: var(--text-primary);" onchange="goToPage(this.value)">
|
||||
<span style="color: var(--text-muted);">{{ t('of') }} <span id="totalPages">1</span></span>
|
||||
<button class="vm-pagination-btn" id="nextPage" onclick="nextPage()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="settings-gear" title="{{ t('settings') }}"><i class="fa-solid fa-gear"></i></button>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-secondary" onclick="syncVMs()" id="syncBtn" title="{{ t('sync_from_proxmox') }}">
|
||||
<div class="vm-actions-right">
|
||||
<button class="btn btn-secondary btn-icon" onclick="syncVMs()" id="syncBtn" title="{{ t('sync_from_proxmox') }}">
|
||||
<span id="syncIcon"><i class="fa-solid fa-rotate"></i></span>
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="showCreateVMModal()" data-permission="vms.create">{{ t('create_vm') }}</button>
|
||||
<button class="btn btn-success" onclick="showCreateLXCModal()" data-permission="vms.create"><i class="fa-solid fa-cube"></i> {{ t('create_lxc') }}</button>
|
||||
<button class="btn btn-primary" onclick="showCreateVMModal()" data-permission="vms.create"><span class="btn-text">{{ t('create_vm') }}</span><span class="btn-icon-mobile" style="display:none;"><i class="fa-solid fa-server"></i></span></button>
|
||||
<button class="btn btn-success" onclick="showCreateLXCModal()" data-permission="vms.create"><i class="fa-solid fa-cube"></i> <span class="btn-text">{{ t('create_lxc') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -850,20 +906,20 @@
|
||||
|
||||
<!-- Snapshots Tab Content -->
|
||||
<div id="snapshotsTabContent" style="display: none;">
|
||||
<div class="vm-toolbar" style="margin-bottom: 1rem;">
|
||||
<div class="vm-toolbar-left">
|
||||
<div class="vm-toolbar" style="margin-bottom: 1rem; flex-direction: row; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; flex: 1;">
|
||||
<select id="snapshotServerFilter" class="vm-filter-select" onchange="loadSnapshotsForServer(this.value)">
|
||||
<option value="">{{ t('select_server') }}</option>
|
||||
</select>
|
||||
<select id="snapshotVMFilter" class="vm-filter-select" onchange="loadSnapshotsForVM(this.value)" disabled>
|
||||
<option value="">{{ t('select_vm') }}</option>
|
||||
</select>
|
||||
<div class="search-input-wrapper" style="position: relative; flex: 1; min-width: 150px;">
|
||||
<i class="fa-solid fa-search" style="position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); color: var(--text-muted);"></i>
|
||||
<input type="text" id="snapshotSearchInput" class="vm-search-input" placeholder="{{ t('search') }}..." oninput="filterSnapshots(this.value)" style="padding-left: 2.25rem;">
|
||||
<div style="position: relative; flex: 1; min-width: 150px; display: flex; align-items: center;">
|
||||
<i class="fa-solid fa-search" style="position: absolute; left: 0.75rem; color: var(--text-muted); font-size: 0.75rem; pointer-events: none;"></i>
|
||||
<input type="text" id="snapshotSearchInput" class="vm-search-input" placeholder="{{ t('search') }}..." oninput="filterSnapshots(this.value)" style="padding-left: 2.25rem; width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-toolbar-right">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<button class="btn btn-secondary btn-icon" onclick="refreshSnapshots()" title="{{ t('refresh') }}">
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
</button>
|
||||
@@ -908,6 +964,8 @@ let sortDirection = 'asc';
|
||||
let currentTypeFilter = '';
|
||||
let currentStatusFilter = '';
|
||||
let currentSearchQuery = '';
|
||||
let currentServerFilter = '';
|
||||
let currentNodeFilter = '';
|
||||
|
||||
// URL params helper functions
|
||||
function getUrlParams() {
|
||||
@@ -947,6 +1005,18 @@ function restoreFiltersFromUrl() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) searchInput.value = search;
|
||||
|
||||
// Restore server filter
|
||||
const server = params.get('server') || '';
|
||||
currentServerFilter = server;
|
||||
const serverSelect = document.getElementById('serverFilter');
|
||||
if (serverSelect) serverSelect.value = server;
|
||||
|
||||
// Restore node filter
|
||||
const node = params.get('node') || '';
|
||||
currentNodeFilter = node;
|
||||
const nodeSelect = document.getElementById('nodeFilter');
|
||||
if (nodeSelect) nodeSelect.value = node;
|
||||
|
||||
// Restore page size
|
||||
const pageSize = params.get('pageSize');
|
||||
if (pageSize && ['25', '50', '100'].includes(pageSize)) {
|
||||
@@ -1377,6 +1447,10 @@ async function loadAllVMs() {
|
||||
|
||||
allVMs = await response.json();
|
||||
|
||||
// Populate server and node filter dropdowns
|
||||
populateServerFilter(currentServerFilter);
|
||||
populateNodeFilter(currentServerFilter);
|
||||
|
||||
// Apply filters without resetting page (page is restored from URL)
|
||||
applyFilters(false);
|
||||
} catch (error) {
|
||||
@@ -1600,6 +1674,16 @@ function applyFilters(resetPage = true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Server filter
|
||||
if (currentServerFilter && String(vm.server_id) !== currentServerFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Node filter
|
||||
if (currentNodeFilter && vm.node !== currentNodeFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (currentSearchQuery) {
|
||||
const query = currentSearchQuery.toLowerCase();
|
||||
@@ -1649,6 +1733,68 @@ function filterByStatus(status) {
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function filterByServer(serverId) {
|
||||
currentServerFilter = serverId;
|
||||
updateUrlParams({ server: serverId, page: '1' });
|
||||
// Update node filter options based on selected server
|
||||
populateNodeFilter(serverId);
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function filterByNode(node) {
|
||||
currentNodeFilter = node;
|
||||
updateUrlParams({ node: node, page: '1' });
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function populateServerFilter(selectedServerId) {
|
||||
const select = document.getElementById('serverFilter');
|
||||
if (!select) return;
|
||||
|
||||
// Collect unique servers
|
||||
const servers = new Map();
|
||||
allVMs.forEach(vm => {
|
||||
if (vm.server_id && !servers.has(vm.server_id)) {
|
||||
servers.set(vm.server_id, vm.server_name || vm.cluster || 'Server ' + vm.server_id);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by name
|
||||
const sorted = [...servers.entries()].sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
const currentVal = selectedServerId !== undefined ? selectedServerId : select.value;
|
||||
select.innerHTML = `<option value="">${t('all_servers')}</option>` +
|
||||
sorted.map(([id, name]) => `<option value="${id}">${name}</option>`).join('');
|
||||
select.value = currentVal;
|
||||
}
|
||||
|
||||
function populateNodeFilter(serverIdFilter) {
|
||||
const select = document.getElementById('nodeFilter');
|
||||
if (!select) return;
|
||||
|
||||
// Collect unique nodes, optionally filtered by selected server
|
||||
const nodes = new Set();
|
||||
allVMs.forEach(vm => {
|
||||
if (serverIdFilter && String(vm.server_id) !== serverIdFilter) return;
|
||||
if (vm.node) nodes.add(vm.node);
|
||||
});
|
||||
|
||||
const sorted = [...nodes].sort();
|
||||
|
||||
const currentVal = select.value;
|
||||
select.innerHTML = `<option value="">${t('all_nodes')}</option>` +
|
||||
sorted.map(n => `<option value="${n}">${n}</option>`).join('');
|
||||
// Restore previous value if still available
|
||||
if (nodes.has(currentVal)) {
|
||||
select.value = currentVal;
|
||||
} else if (currentNodeFilter && nodes.has(currentNodeFilter)) {
|
||||
select.value = currentNodeFilter;
|
||||
} else {
|
||||
select.value = '';
|
||||
currentNodeFilter = '';
|
||||
}
|
||||
}
|
||||
|
||||
function searchVMs(query) {
|
||||
currentSearchQuery = query;
|
||||
updateUrlParams({ search: query, page: '1' });
|
||||
@@ -3992,34 +4138,25 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
/* Snapshots toolbar responsive */
|
||||
#snapshotsTabContent .vm-toolbar {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
#snapshotsTabContent .vm-toolbar-left {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
#snapshotsTabContent .vm-toolbar-left select,
|
||||
#snapshotsTabContent .vm-toolbar-left input {
|
||||
min-width: 140px;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#snapshotsTabContent .vm-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#snapshotsTabContent .vm-toolbar-left,
|
||||
#snapshotsTabContent .vm-toolbar-right {
|
||||
#snapshotsTabContent .vm-toolbar > div:first-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#snapshotsTabContent .vm-toolbar-right {
|
||||
#snapshotsTabContent .vm-toolbar > div:last-child {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user