feat: implement VM grid view with batch selection and quick-action menu support
This commit is contained in:
parent
b125799129
commit
99ab2d06b2
23
gui_app.py
23
gui_app.py
@ -973,7 +973,28 @@ def vms():
|
|||||||
force=force,
|
force=force,
|
||||||
)
|
)
|
||||||
cache_age = int(time.time() - cache_ts) if cache_ts else None
|
cache_age = int(time.time() - cache_ts) if cache_ts else None
|
||||||
return render_template('vms.html', vms=vm_list, error=error, cache_age=cache_age)
|
|
||||||
|
# Calculate set of scheduled VMs
|
||||||
|
active_scheduled_vms = set()
|
||||||
|
with jobs_db_lock:
|
||||||
|
for job in jobs.values():
|
||||||
|
if job.get('schedule_type') and job.get('schedule_type') != 'now' and job.get('schedule_id'):
|
||||||
|
vm_names = job.get('vm_names')
|
||||||
|
if vm_names:
|
||||||
|
for vm in vm_names:
|
||||||
|
active_scheduled_vms.add(vm)
|
||||||
|
else:
|
||||||
|
vm_name = job.get('vm_name')
|
||||||
|
if vm_name:
|
||||||
|
active_scheduled_vms.add(vm_name)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'vms.html',
|
||||||
|
vms=vm_list,
|
||||||
|
error=error,
|
||||||
|
cache_age=cache_age,
|
||||||
|
scheduled_vms=list(active_scheduled_vms)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/vms')
|
@app.route('/api/vms')
|
||||||
|
|||||||
@ -313,15 +313,20 @@
|
|||||||
<button class="filter-chip" id="filter-suspended" onclick="setFilter('suspended')">
|
<button class="filter-chip" id="filter-suspended" onclick="setFilter('suspended')">
|
||||||
<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--warning); margin-right:6px; vertical-align:middle;"></span>Suspended
|
<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--warning); margin-right:6px; vertical-align:middle;"></span>Suspended
|
||||||
</button>
|
</button>
|
||||||
|
<button class="filter-chip" id="filter-unscheduled" onclick="setFilter('unscheduled')">
|
||||||
|
<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--accent-2); margin-right:6px; vertical-align:middle;"></span>Unscheduled
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VM Cards grid -->
|
<!-- VM Cards grid -->
|
||||||
<div class="vms-grid" id="vmGrid">
|
<div class="vms-grid" id="vmGrid">
|
||||||
{% for vm in vms %}
|
{% for vm in vms %}
|
||||||
|
{% set is_scheduled = vm.name in scheduled_vms %}
|
||||||
<div class="vm-card"
|
<div class="vm-card"
|
||||||
data-name="{{ vm.name|lower }}"
|
data-name="{{ vm.name|lower }}"
|
||||||
data-vmname="{{ vm.name }}"
|
data-vmname="{{ vm.name }}"
|
||||||
data-power="{{ vm.power_state }}"
|
data-power="{{ vm.power_state }}"
|
||||||
|
data-scheduled="{{ '1' if is_scheduled else '0' }}"
|
||||||
onclick="handleCardClick(this, event)">
|
onclick="handleCardClick(this, event)">
|
||||||
|
|
||||||
<!-- Selection checkbox overlay -->
|
<!-- Selection checkbox overlay -->
|
||||||
@ -353,7 +358,12 @@
|
|||||||
|
|
||||||
<div class="vm-card-header">
|
<div class="vm-card-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="vm-name">{{ vm.name }}</div>
|
<div class="vm-name">
|
||||||
|
{{ vm.name }}
|
||||||
|
{% if is_scheduled %}
|
||||||
|
<span style="display:inline-block; font-size: 11px; margin-left: 6px; color: var(--accent-2); vertical-align: middle;" title="Has active schedule">📅︎</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="vm-os">{{ vm.guest_os or 'Unknown OS' }}</div>
|
<div class="vm-os">{{ vm.guest_os or 'Unknown OS' }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% if vm.power_state == 'poweredOn' %}
|
{% if vm.power_state == 'poweredOn' %}
|
||||||
@ -455,7 +465,14 @@
|
|||||||
function filterVMs() {
|
function filterVMs() {
|
||||||
const q = document.getElementById('vmSearch').value.toLowerCase();
|
const q = document.getElementById('vmSearch').value.toLowerCase();
|
||||||
document.querySelectorAll('.vm-card').forEach(card => {
|
document.querySelectorAll('.vm-card').forEach(card => {
|
||||||
const matchFilter = activeFilter === 'all' || card.dataset.power === activeFilter;
|
let matchFilter = false;
|
||||||
|
if (activeFilter === 'all') {
|
||||||
|
matchFilter = true;
|
||||||
|
} else if (activeFilter === 'unscheduled') {
|
||||||
|
matchFilter = card.dataset.scheduled === '0';
|
||||||
|
} else {
|
||||||
|
matchFilter = card.dataset.power === activeFilter;
|
||||||
|
}
|
||||||
const matchSearch = card.dataset.name.includes(q);
|
const matchSearch = card.dataset.name.includes(q);
|
||||||
card.style.display = (matchFilter && matchSearch) ? '' : 'none';
|
card.style.display = (matchFilter && matchSearch) ? '' : 'none';
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user