1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

feat(instances): the top of a VM's page in one card

The top of a VM's page was one line of values between pipes, an icon for
the guest agent and a hidden snapshot count, then a row of big action
icons; the host links sat in a bar fixed to the bottom of the window,
over the content.

One card now holds it all (design chosen by the owner from four):
- the name with the state badge, the title under it, and where the VM
  is: its host and the host's VM list for administrators, the VM list
  for owners;
- OS details (with a connected guest agent) and Refresh;
- the guest agent's state in words and the snapshot count;
- CPU, memory, each disk with its device name and size, and the IPv4
  addresses, labelled;
- the page's sections as small labelled pills in the card's footer: one
  row from 992 px, wrapping below.

The page has no other heading: base.html's heading area is a block it
leaves out.

Each section below it (Power, Access, Resize, Snapshot, Settings, Stats,
Destroy) is a card of the same kind, so the page reads as one design
(owner's choice of two): its sub-tabs as a small underlined nav in the
card's header, its panes in the card's body without their own borders.
Buttons are of normal size in the theme's primary colour, and those that
cannot be undone (Power Cycle, Force Off, Destroy) in the danger colour.
The Stats charts lose their coloured borders. instance_tabs.js finds the
Settings tabs by their tablist role.

The bottom bar and its setting (VIEW_INSTANCE_DETAIL_BOTTOM_BAR) are
removed, and with it the CSS that kept the old Settings tabs on one line.
This commit is contained in:
catborise 2026-10-29 21:58:58 +03:00
parent d97ec5afd2
commit 935a0e9bdc
30 changed files with 327 additions and 275 deletions

View file

@ -0,0 +1,18 @@
from django.db import migrations
def remove_bottom_bar(apps, schema_editor):
"""A VM's page shows its host's links above it, always, as the host's
pages do: the setting for the bar at the bottom goes."""
AppSettings = apps.get_model("appsettings", "AppSettings")
AppSettings.objects.using(schema_editor.connection.alias).filter(key="VIEW_INSTANCE_DETAIL_BOTTOM_BAR").delete()
class Migration(migrations.Migration):
dependencies = [
("appsettings", "0022_cpu_mode_choices"),
]
operations = [
migrations.RunPython(remove_bottom_bar, migrations.RunPython.noop),
]

View file

@ -95,10 +95,13 @@ descriptions = importlib.import_module("appsettings.migrations.0021_setting_desc
class DescriptionsTestCase(TestCase):
def test_every_setting_says_what_it_does_within_its_column(self):
limit = AppSettings._meta.get_field("description").max_length
removed = {"VIEW_INSTANCE_DETAIL_BOTTOM_BAR"} # by 0023
for key, text in descriptions.DESCRIPTIONS.items():
with self.subTest(key=key):
self.assertLessEqual(len(text), limit)
self.assertEqual(AppSettings.objects.get(key=key).description, text)
if key not in removed:
self.assertEqual(AppSettings.objects.get(key=key).description, text)
self.assertFalse(AppSettings.objects.filter(key__in=removed).exists())
cpu_mode = importlib.import_module("appsettings.migrations.0022_cpu_mode_choices")

View file

@ -46,8 +46,7 @@ GROUPS = [
(_("Snapshots and jobs"), ["SNAPSHOT_FREE_SPACE_PERCENT", "SNAPSHOT_FREE_SPACE_MIN_GIB",
"WORKER_CONCURRENCY", "JOB_RETENTION_DAYS"]),
(_("Login"), ["LOGIN_FAILURE_LIMIT", "LOGIN_LOCKOUT_MINUTES"]),
(_("Lists and pages"), ["VIEW_INSTANCES_LIST_STYLE", "VIEW_INSTANCE_DETAIL_BOTTOM_BAR", "VM_DRBD_STATUS",
"LOGS_PER_PAGE"]),
(_("Lists and pages"), ["VIEW_INSTANCES_LIST_STYLE", "VM_DRBD_STATUS", "LOGS_PER_PAGE"]),
]

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% load bootstrap_icons %}
{% if request.user.is_superuser %}
<button type="button" class="btn btn-success float-end" data-bs-toggle="modal" data-bs-target="#addInstanceNetwork">
<button type="button" class="btn btn-primary float-end" data-bs-toggle="modal" data-bs-target="#addInstanceNetwork">
<span aria-hidden="true">{% bs_icon 'plus'%} </span>
</button>

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% load bootstrap_icons %}
{% if request.user.is_superuser %}
<a href="#addInstanceOwner" type="button" class="btn btn-success float-end" data-bs-toggle="modal">
<a href="#addInstanceOwner" type="button" class="btn btn-primary float-end" data-bs-toggle="modal">
<span aria-hidden="true">{% bs_icon 'plus'%} </span>
</a>

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% load bootstrap_icons %}
{% if request.user.is_superuser %}
<a href="#addvol" type="button" class="btn btn-success float-end" data-bs-toggle="modal" title="{% trans "Add Volume" %}">
<a href="#addvol" type="button" class="btn btn-primary float-end" data-bs-toggle="modal" title="{% trans "Add Volume" %}">
<span aria-hidden="true">{% bs_icon 'plus'%} </span>
</a>
@ -93,7 +93,7 @@
</div> <!-- /.modal-body -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-success" name="add_new_vol">{% trans "Add Volume" %}</button>
<button type="submit" class="btn btn-primary" name="add_new_vol">{% trans "Add Volume" %}</button>
</div>
</form>
</div>
@ -147,7 +147,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-success">{% trans "Add Volume" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Add Volume" %}</button>
</div>
</form>
</div>

View file

@ -1,28 +0,0 @@
{% load i18n %}
{% load bootstrap_icons %}
<nav class="navbar navbar-expand-sm fixed-bottom navbar-light bg-light" aria-label="Instance bottom navbar">
<div class="container">
<a class="navbar-brand" href="#">{% trans 'HOST' %}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar_bottom" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbar_bottom" class="navbar-collapse collapse">
<ul class="navbar-nav">
{% if request.user.is_superuser %}{# host pages and names are for administrators #}
<li class="nav-item">
<a class="nav-link" href="{% url 'overview' compute.id %}">
{% bs_icon 'server'%}
{{ compute.name }} {% if compute.name != compute.hostname %} - {{ compute.hostname }}{% endif %}
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'instances' compute.id %}">{% bs_icon 'laptop'%} {% trans "Instances" %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'instances:instance' instance.id %}">{% bs_icon 'device-hdd'%} {{ instance.name }}</a>
</li>
</ul>
</div>
</div>
</nav>

View file

@ -7,115 +7,92 @@
{% block title %}{% trans "Instance" %} - {{ instance.name }}{% endblock %}
{% block page_heading %}
{{ instance.name }}{% if instance.title %} ({{ instance.title }}){% endif %}
{% endblock page_heading %}
{% block page_header %}{% endblock page_header %}{# the card below has the heading #}
{% block content %}
{% include 'pleasewaitdialog.html' %}
<div>
<div>
{% vm_state instance.status as state %}
<span class="badge text-bg-{{ state.1 }}">{{ state.0 }}</span>
|
<span
{%if instance.guest_agent %}
{# the VM at a glance in one card: who and where it is, what it has, and its sections as the card's footer #}
<section class="card mb-3" aria-labelledby="vm-name">
<div class="card-body p-3">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
<div>
<div class="d-flex align-items-center gap-2">
<h1 class="h4 mb-0" id="vm-name">{{ instance.name }}</h1>
{% vm_state instance.status as state %}
<span class="badge text-bg-{{ state.1 }}">{{ state.0 }}</span>
</div>
{% if instance.title %}<p class="text-body-secondary mb-1">{{ instance.title }}</p>{% endif %}
{% include 'instances/host_link.html' %}
</div>
<div class="d-flex gap-2">
{% if instance.guest_agent_ready %}
class="badge bg-success"
title="{% trans "Guest Agent Enabled & Connected" %}"
{% else %}
class="badge bg-warning"
title="{% trans "Guest Agent Enabled but not Connected" %}"
<button type="button" class="btn btn-sm btn-secondary" onclick="get_osinfo();">{% bs_icon 'info-circle' %} {% trans "OS details" %}</button>
{% endif %}
<a class="btn btn-sm btn-secondary" href="{% url 'instances:instance' instance.id %}">{% bs_icon 'arrow-clockwise' %} {% trans "Refresh" %}</a>
</div>
</div>
<div class="d-flex flex-wrap column-gap-3 row-gap-1 small mt-2">
{% if not instance.guest_agent %}
<span>{% bs_icon 'dash-circle' %} {% trans "Guest agent off" %}</span>
{% elif instance.guest_agent_ready %}
<span><span class="text-success">{% bs_icon 'check-circle' %}</span> {% trans "Guest agent connected" %}</span>
{% else %}
class="badge bg-danger"
title="{% trans "Guest Agent Not Enabled & Not Connected" %}"
{% endif %}>{% bs_icon 'broadcast' size='1em' %}
</span>
|
{% if instance.snapshots %}
{% bs_icon 'camera'%} <i title="There are {{ instance.snapshots|length }} snapshot(s)"></i>
|
{% endif %}
{% if instance.cur_vcpu %}
{{ instance.cur_vcpu }} {% trans "VCPU" %}
{% else %}
{{ instance.vcpu }} {% trans "VCPU" %}
{% endif %}
|
{{ instance.cur_memory }} {% trans "MB" %} {% trans "RAM" %}
|
{% for disk in instance.disks %}
{{ disk.size|filesizeformat }} {% trans "Disk" %} |
{% endfor %}
{% for net in instance.networks %}
{% for ipv4 in net.ipv4 %}
{{ ipv4 }} |
{% endfor %}
{% endfor %}
{% if instance.guest_agent_ready %}
<a class="link-warning" title="{% trans 'Show Instance OS details' %}" onclick="get_osinfo();">
<span>{% bs_icon 'info-circle' %} </span>
</a>
{% endif %}
<a class="link-success" href="{% url 'instances:instance' instance.id %}" title="{% trans 'Refresh instance info' %}">
<span>{% bs_icon 'repeat'%} </span>
</a>
<span><span class="text-warning">{% bs_icon 'exclamation-circle' %}</span> {% trans "Guest agent not connected" %}</span>
{% endif %}
<span>{% bs_icon 'camera' %} {% blocktrans count counter=instance.snapshots|length %}{{ counter }} snapshot{% plural %}{{ counter }} snapshots{% endblocktrans %}</span>
</div>
<dl class="row g-2 small mb-0 mt-1">
<div class="col-6 col-lg-2">
<dt class="fw-normal text-body-secondary">{% trans "CPU" %}</dt>
<dd class="mb-0">{% firstof instance.cur_vcpu instance.vcpu %} {% trans "of" %} {{ instance.vcpu }} {% trans "vCPU" %}</dd>
</div>
<div class="col-6 col-lg-2">
<dt class="fw-normal text-body-secondary">{% trans "Memory" %}</dt>
<dd class="mb-0">{{ instance.cur_memory }} {% trans "MB" %}</dd>
</div>
<div class="col-6 col-lg-5">
<dt class="fw-normal text-body-secondary">{% trans "Disks" %}</dt>
<dd class="mb-0">{% for disk in instance.disks %}{{ disk.dev }} {{ disk.size|filesizeformat }}{% if not forloop.last %} · {% endif %}{% empty %}{% trans "None" %}{% endfor %}</dd>
</div>
<div class="col-6 col-lg-3">
<dt class="fw-normal text-body-secondary">{% trans "IPv4" %}</dt>
<dd class="mb-0 font-monospace">{{ ipv4_addresses|join:", "|default:"—" }}</dd>
</div>
</dl>
</div>
<hr>
</div>
<div class="card-footer p-2">
<ul class="nav nav-pills flex-wrap flex-lg-nowrap gap-1 small" id="navbtn" role="tablist" aria-label="{% trans "Instance actions" %}">
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap active" id="power-tab" aria-controls="power" data-bs-toggle="pill" data-bs-target="#power" type="button" role="tab" aria-selected="true">{% bs_icon 'power' %} {% trans "Power" %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap" id="access-tab" aria-controls="access" data-bs-toggle="pill" data-bs-target="#access" type="button" role="tab" aria-selected="false">{% bs_icon 'lock' %} {% trans "Access" %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap" id="resize-tab" aria-controls="resize" data-bs-toggle="pill" data-bs-target="#resize" type="button" role="tab" aria-selected="false">{% bs_icon 'arrows-angle-expand' %} {% trans "Resize" %}</button>
</li>
{% if can_snapshot %}
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap" id="snapshots-tab" aria-controls="snapshots" data-bs-toggle="pill" data-bs-target="#snapshots" type="button" role="tab" aria-selected="false">{% bs_icon 'camera' %} {% trans "Snapshot" %}</button>
</li>
{% endif %}
{% if show_settings %}
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap" id="settings-tab" aria-controls="settings" data-bs-toggle="pill" data-bs-target="#settings" type="button" role="tab" aria-selected="false">{% bs_icon 'gear-wide-connected' %} {% trans "Settings" %}</button>
</li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap" id="graphics-tab" aria-controls="graphics" data-bs-toggle="pill" data-bs-target="#graphics" type="button" role="tab" aria-selected="false">{% bs_icon 'bar-chart-line' %} {% trans "Stats" %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap" id="undefine-tab" aria-controls="undefine" data-bs-toggle="pill" data-bs-target="#undefine" type="button" role="tab" aria-selected="false">{% bs_icon 'trash' %} {% trans "Destroy" %}</button>
</li>
</ul>
</div>
</section>
<div id="job-panel" aria-live="polite"></div>
<div id="mainTabMenu">
<!-- Nav tabs -->
<ul class="nav nav-pills nav-fill" id="navbtn" role="tablist" aria-label="Instance actions">
<li class="nav-item" role="presentation">
<button class="nav-link text-center active" id="power-tab" aria-controls="power" data-bs-toggle="pill" data-bs-target="#power" type="button" role="tab" aria-selected="true">
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'power' size='2em'%} </span>
{% trans "Power" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link text-center" id="access-tab" aria-controls="access" data-bs-toggle="pill" data-bs-target="#access" type="button" role="tab" aria-selected="false">
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'lock' size='2em'%} </span>
{% trans "Access" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link text-center" id="resize-tab" aria-controls="resize" data-bs-toggle="pill" data-bs-target="#resize" type="button" role="tab" aria-selected="false">
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'arrows-angle-expand' size='2em'%} </span>
{% trans "Resize" %}
</button>
</li>
{% if can_snapshot %}
<li class="nav-item" role="presentation">
<button class="nav-link text-center" id="snapshots-tab" aria-controls="snapshots" data-bs-toggle="pill" data-bs-target="#snapshots" type="button" role="tab" aria-selected="false">
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'camera' size='2em'%} </span>
{% trans "Snapshot" %}
</button>
</li>
{% endif %}
{% if show_settings %}
<li class="nav-item" role="presentation">
<button class="nav-link text-center" id="settings-tab" aria-controls="settings" data-bs-toggle="pill" data-bs-target="#settings" type="button" role="tab" aria-selected="false">
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'gear-wide-connected' size='2em'%} </span>
{% trans "Settings" %}
</button>
</li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link text-center" id="graphics-tab" aria-controls="graphics" data-bs-toggle="pill" data-bs-target="#graphics" type="button" role="tab" aria-selected="false">
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'bar-chart-line' size='2em'%} </span>
{% trans "Stats" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link text-center" id="undefine-tab" aria-controls="undefine" data-bs-toggle="pill" data-bs-target="#undefine" type="button" role="tab" aria-selected="false">
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'trash' size='2em'%} </span>
{% trans "Destroy" %}
</button>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content" id=mainTabMenuContent>
{% include 'instances/power_tab.html' %}
@ -128,10 +105,6 @@
</div>
</div>
{% if app_settings.VIEW_INSTANCE_DETAIL_BOTTOM_BAR == 'True' %}
{% include 'bottom_bar.html' %}
{% endif %}
{% endblock %}
{% block script %}
<script src="{% static 'js/ace/ace.js' %}" type="text/javascript" charset="utf-8"></script>

View file

@ -1,7 +1,9 @@
{% load i18n %}
<div class="tab-pane mt-3" id="access" role="tabpanel" aria-label="Instance access options">
<div class="tab-pane" id="access" role="tabpanel" aria-label="Instance access options">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<div class="card">
<div class="card-header bg-transparent pb-0">
<ul class="nav nav-underline small" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#vnconsole" type="button" role="tab" aria-controls="vnconsole" aria-selected="true">
{% trans "Console" %}
@ -29,9 +31,10 @@
</li>
{% endif %}
</ul>
</div>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane border border-top-0 p-3 active" id="vnconsole">
<div class="tab-content card-body">
<div role="tabpanel" class="tab-pane active" id="vnconsole">
<p>{% blocktrans with type=instance.console_type|upper %} This action opens a new window with a {{ type }} connection to the console of the instance.{% endblocktrans %}
</p>
{% if not can_open_console %}
@ -39,17 +42,17 @@
{% else %}
{% if instance.status == 1 %}
{# one window per VM: opening it again reloads it #}
<button type="button" class="btn btn-lg btn-success float-end" id="console_button"
<button type="button" class="btn btn-primary float-end" id="console_button"
data-url="{% url 'console' compute.id instance.uuid %}" data-window="console-{{ instance.uuid }}"
onclick="window.open(this.dataset.url, this.dataset.window, 'width=850,height=650')">{% trans "Console" %}</button>
{% else %}
<button class="btn btn-lg btn-success float-end disabled">{% trans "Console" %}</button>
<button class="btn btn-primary float-end disabled">{% trans "Console" %}</button>
{% endif %}
{% endif %}
<div class="clearfix"></div>
</div>
{% if app_settings.SHOW_ACCESS_ROOT_PASSWORD == 'True' and can_change %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="rootpasswd">
<div role="tabpanel" class="tab-pane" id="rootpasswd">
<p>{% trans "Shut the VM down, or run it with its QEMU guest agent, and enter a new root password." %}</p>
<form action="{% url 'instances:rootpasswd' instance.id %}" method="post"
role="form" aria-label="Add root password to instance form">
@ -61,18 +64,18 @@
</div>
</div>
{% if instance.status == 5 or instance.status == 1 and instance.guest_agent_ready %}
<input type="submit" class="btn btn-lg btn-success float-end" name="rootpasswd"
<input type="submit" class="btn btn-primary float-end" name="rootpasswd"
value="{% trans "Reset Root Password" %}">
{% else %}
<button
class="btn btn-lg btn-success float-end disabled">{% trans "Reset Root Password" %}</button>
class="btn btn-primary float-end disabled">{% trans "Reset Root Password" %}</button>
{% endif %}
</form>
<div class="clearfix"></div>
</div>
{% endif %}
{% if app_settings.SHOW_ACCESS_SSH_KEYS == 'True' and can_change %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="sshkeys">
<div role="tabpanel" class="tab-pane" id="sshkeys">
<p>{% trans "Shut the VM down, or run it with its QEMU guest agent, and choose your public key. The key is added to root's keys." %}</p>
<form action="{% url 'instances:add_public_key' instance.id %}"method="post"
role="form" aria-label="Add public key to instance form">
@ -91,27 +94,28 @@
</div>
</div>
{% if instance.status == 5 or instance.status == 1 and instance.guest_agent_ready %}
<input type="submit" class="btn btn-lg btn-success float-end" name="addpublickey"
<input type="submit" class="btn btn-primary float-end" name="addpublickey"
value="{% trans "Add Public Key" %}">
{% else %}
<button class="btn btn-lg btn-success float-end disabled">{% trans "Add Public Key" %}</button>
<button class="btn btn-primary float-end disabled">{% trans "Add Public Key" %}</button>
{% endif %}
</form>
<div class="clearfix"></div>
</div>
{% endif %}
{% if instance.status == 1 and can_open_console %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="vdiconsole">
<div role="tabpanel" class="tab-pane" id="vdiconsole">
<p>{% trans "This action opens a remote viewer with a connection to the console of the instance." %}</p>
<div class="input-group">
<input type="text" class="input-lg disabled form-control" disabled id="vdi_url_input" />
<a href="#" class="btn btn-success" id="vdi_url">{% trans "VDI" %}</a>
<a href="#" class="btn btn-primary" id="vdi_url">{% trans "VDI" %}</a>
</div>
<p>{% trans "To download console.vv file for virt-viewer." %}</p>
<a href="{% url 'instances:getvvfile' instance.id %}" class="btn btn-lg btn-success float-end">{% trans "Get console.vv" %}</a>
<a href="{% url 'instances:getvvfile' instance.id %}" class="btn btn-primary float-end">{% trans "Get console.vv" %}</a>
<div class="clearfix"></div>
</div>
{% endif %}
</div>
</div>
</div>

View file

@ -26,7 +26,7 @@
</label>
</div>
{% endif %}
<button type="submit" class="btn btn-lg btn-success float-end" name="delete">
<button type="submit" class="btn btn-danger float-end" name="delete">
{% trans "Destroy" %}
</button>
</form>

View file

@ -1,23 +1,27 @@
{% load i18n %}
<div class="tab-pane mt-3" id="undefine" role="tabpanel">
<div class="tab-pane" id="undefine" role="tabpanel">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist" aria-label="Instance destroy menu">
<div class="card">
<div class="card-header bg-transparent pb-0">
<ul class="nav nav-underline small" role="tablist" aria-label="Instance destroy menu">
<li class="nav-item">
<a class="nav-link active" href="#destroy" aria-controls="destroy" role="tab" data-bs-toggle="tab">
{% trans "Destroy Instance" %}
</a>
</li>
</ul>
</div>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane border border-top-0 p-3 active" id="destroy">
<div class="tab-content card-body">
<div role="tabpanel" class="tab-pane active" id="destroy">
<p>{% trans 'This action starts remove instance process' %}</p>
{% if can_delete %}
<a href="{% url 'instances:destroy' instance.id %}" class="btn btn-lg btn-success float-end">{% trans "Destroy" %}</a>
<a href="{% url 'instances:destroy' instance.id %}" class="btn btn-danger float-end">{% trans "Destroy" %}</a>
{% else %}
<button class="btn btn-lg btn-success disabled float-end" name="delete">{% trans "Destroy" %}</button>
<button class="btn btn-danger disabled float-end" name="delete">{% trans "Destroy" %}</button>
{% endif %}
<div class="clearfix"></div>
</div>
</div>
</div>
</div>

View file

@ -109,7 +109,7 @@
<div class="clearfix"></div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-success" name="edit_volume">{% trans "Save" %}</button>
<button type="submit" class="btn btn-primary" name="edit_volume">{% trans "Save" %}</button>
</div><!-- /.modal-footer -->
</div> <!-- /.modal-content -->

View file

@ -0,0 +1,11 @@
{% load i18n %}
{# where the VM is: its host and the host's VM list (host pages are for administrators); its owner's VM list otherwise #}
<nav class="small" aria-label="{% trans "Location" %}">
{% if request.user.is_superuser %}
<a href="{% url 'overview' compute.id %}">{{ compute.name }}</a>
<span class="text-body-secondary mx-1" aria-hidden="true">/</span>
<a href="{% url 'instances' compute.id %}">{% trans "Instances" %}</a>
{% else %}
<a href="{% url 'instances:index' %}">{% trans "Instances" %}</a>
{% endif %}
</nav>

View file

@ -1,5 +1,5 @@
{% load i18n %}
<div class="tab-pane border border-top-0 p-3" id="osinfo" role="tabpanel">
<div class="tab-pane" id="osinfo" role="tabpanel">
<h3 class="mb-3">{% trans "Guest Info" %}</h3>
<div id="osinfo-error" class="alert alert-warning d-none" role="alert"></div>
<dl class="mx-3 row">

View file

@ -4,7 +4,9 @@
<p>{% trans "Only the owners of this instance and administrators can change its power state." %}</p>
{% else %}
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist" aria-label="Instance power actions">
<div class="card">
<div class="card-header bg-transparent pb-0">
<ul class="nav nav-underline small" role="tablist" aria-label="Instance power actions">
{% if instance.status == 1 or instance.status == 2 %}
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#poweroff" type="button" role="tab" aria-controls="poweroff" aria-selected="true">
@ -58,37 +60,38 @@
</li>
{% endif %}
</ul>
</div>
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-content card-body">
{% if instance.status == 1 or instance.status == 2 %}
<div class="tab-pane border border-top-0 p-3 active" id="poweroff" role="tabpanel" aria-labelledby="poweroff-tab">
<div class="tab-pane active" id="poweroff" role="tabpanel" aria-labelledby="poweroff-tab">
<p>{% trans "This action sends an ACPI shutdown signal to the instance." %}</p>
<form action="{% url 'instances:poweroff' instance.id %}" method="post" role="form" aria-label="Power off instance form">
{% csrf_token %}
<input type="submit" name="poweroff" class="btn btn-lg btn-success float-end" value="{% trans "Power Off" %}">
<input type="submit" name="poweroff" class="btn btn-primary float-end" value="{% trans "Power Off" %}">
<div class="clearfix"></div>
</form>
</div>
<div class="tab-pane border border-top-0 p-3" id="powercycle" role="tabpanel" aria-labelledby="powercycle-tab">
<div class="tab-pane" id="powercycle" role="tabpanel" aria-labelledby="powercycle-tab">
<p>{% trans "This action forcibly powers off and start the instance and may cause data corruption." %}</p>
<form action="{% url 'instances:powercycle' instance.id %}" method="post" role="form" aria-label="Power cycle instance form">{% csrf_token %}
<input type="submit" name="powercycle" class="btn btn-lg btn-success float-end" value="{% trans "Power Cycle" %}">
<input type="submit" name="powercycle" class="btn btn-danger float-end" value="{% trans "Power Cycle" %}">
<div class="clearfix"></div>
</form>
</div>
<div class="tab-pane border border-top-0 p-3" id="powerforce" role="tabpanel" aria-labelledby="powerforce-tab">
<div class="tab-pane" id="powerforce" role="tabpanel" aria-labelledby="powerforce-tab">
<p>{% trans "This action forcibly powers off the instance and may cause data corruption." %}</p>
<form action="{% url 'instances:force_off' instance.id %}" method="post" role="form" aria-label="Force to shutdown instance form">
{% csrf_token %}
<input type="submit" name="powerforce" class="btn btn-lg btn-success float-end" value="{% trans "Force Off" %}">
<input type="submit" name="powerforce" class="btn btn-danger float-end" value="{% trans "Force Off" %}">
<div class="clearfix"></div>
</form>
</div>
{% if request.user.is_superuser %}
<div class="tab-pane border border-top-0 p-3" id="suspend" role="tabpanel" aria-labelledby="suspend-tab">
<div class="tab-pane" id="suspend" role="tabpanel" aria-labelledby="suspend-tab">
<p>{% trans "This action suspends the instance." %}</p>
<form action="{% url 'instances:suspend' instance.id %}" method="post" role="form" aria-label="Suspend instance form">{% csrf_token %}
<input type="submit" name="suspend" class="btn btn-lg btn-success float-end" value="{% trans "Suspend" %}">
<input type="submit" name="suspend" class="btn btn-primary float-end" value="{% trans "Suspend" %}">
<div class="clearfix"></div>
</form>
</div>
@ -96,25 +99,25 @@
{% endif %}
{% if instance.status == 3 %}
{% if request.user.is_superuser %}
<div class="tab-pane border border-top-0 p-3 active" id="resume" role="tabpanel" aria-labelledby="resume-tab">
<div class="tab-pane active" id="resume" role="tabpanel" aria-labelledby="resume-tab">
<p>{% trans "This action restore the instance after suspend." %}</p>
<form action="{% url 'instances:resume' instance.id %}" method="post" role="form" aria-label="Resume instance from suspension form">{% csrf_token %}
<input type="submit" name="resume" class="btn btn-lg btn-success float-end" value="{% trans "Resume" %}">
<input type="submit" name="resume" class="btn btn-primary float-end" value="{% trans "Resume" %}">
<div class="clearfix"></div>
</form>
</div>
<div class="tab-pane border border-top-0 p-3" id="powerforce" role="tabpanel" aria-labelledby="powerforce-tab">
<div class="tab-pane" id="powerforce" role="tabpanel" aria-labelledby="powerforce-tab">
<p>{% trans "This action forcibly powers off the instance and may cause data corruption." %}</p>
<form action="{% url 'instances:force_off' instance.id %}" method="post" role="form" aria-label="Force to shutdown form">{% csrf_token %}
<input type="submit" name="powerforce" class="btn btn-lg btn-success float-end" value="{% trans "Force Off" %}">
<input type="submit" name="powerforce" class="btn btn-danger float-end" value="{% trans "Force Off" %}">
<div class="clearfix"></div>
</form>
</div>
{% else %}
<div class="tab-pane border border-top-0 p-3 active" id="resume" role="tabpanel" aria-labelledby="resume-tab">
<div class="tab-pane active" id="resume" role="tabpanel" aria-labelledby="resume-tab">
<p>{% trans "Administrator blocked your instance." %}</p>
<form action="{% url 'instances:resume' instance.id %}" method="post" role="form" aria-label="Resume instance form">{% csrf_token %}
<button class="btn btn-lg btn-success disabled float-end">{% trans "Resume" %}</button>
<button class="btn btn-primary disabled float-end">{% trans "Resume" %}</button>
<div class="clearfix"></div>
</form>
</div>
@ -122,30 +125,31 @@
{% endif %}
{% if instance.status == 4 or instance.status == 6 or instance.status == 7 %}
{# shutting down, crashed or suspended by the guest: a forced power off ends it, then it can be started #}
<div class="tab-pane border border-top-0 p-3 active" id="powerforce" role="tabpanel" aria-labelledby="powerforce-tab">
<div class="tab-pane active" id="powerforce" role="tabpanel" aria-labelledby="powerforce-tab">
<p>{% trans "This action forcibly powers off the instance and may cause data corruption." %}</p>
<form action="{% url 'instances:force_off' instance.id %}" method="post" role="form" aria-label="Force to shutdown instance form">
{% csrf_token %}
<input type="submit" name="powerforce" class="btn btn-lg btn-success float-end" value="{% trans "Force Off" %}">
<input type="submit" name="powerforce" class="btn btn-danger float-end" value="{% trans "Force Off" %}">
<div class="clearfix"></div>
</form>
</div>
{% endif %}
{% if instance.status == 5 %}
<div class="tab-pane border border-top-0 p-3 active" id="boot" role="tabpanel" aria-labelledby="boot-tab">
<div class="tab-pane active" id="boot" role="tabpanel" aria-labelledby="boot-tab">
<p>{% trans "Click on Power On button to start this instance." %}</p>
<form action="{% url 'instances:poweron' instance.id %}" method="post" role="form" aria-label="Start instance form">
{% csrf_token %}
{% if instance.is_template %}
<p>{% trans "Template instance cannot be started." %}</p>
<input type="submit" name="poweron" class="btn btn-lg btn-success float-end disabled" value="{% trans "Power On" %}">
<input type="submit" name="poweron" class="btn btn-primary float-end disabled" value="{% trans "Power On" %}">
{% else %}
<input type="submit" name="poweron" class="btn btn-lg btn-success float-end" onclick="showPleaseWaitDialog()" value="{% trans "Power On" %}">
<input type="submit" name="poweron" class="btn btn-primary float-end" onclick="showPleaseWaitDialog()" value="{% trans "Power On" %}">
{% endif %}
<div class="clearfix"></div>
</form>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>

View file

@ -1,7 +1,9 @@
{% load i18n %}
<div class="tab-pane mt-3" id="resize" role="tabpanel">
<div class="tab-pane" id="resize" role="tabpanel">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist" aria-label="Instance resize options">
<div class="card">
<div class="card-header bg-transparent pb-0">
<ul class="nav nav-underline small" role="tablist" aria-label="Instance resize options">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#resizevm_cpu" type="button" role="tab" aria-controls="resizevm_cpu" aria-selected="true">
{% trans "CPU" %}
@ -18,9 +20,10 @@
</button>
</li>
</ul>
</div>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane border border-top-0 p-3 active" id="resizevm_cpu">
<div class="tab-content card-body">
<div role="tabpanel" class="tab-pane active" id="resizevm_cpu">
{% if can_change %}
{% if instance.status == 5 or not instance.vcpus %}
<form action="{% url 'instances:resizevm_cpu' instance.id %}" method="post" role="form" aria-label="Resize instance cpu form">{% csrf_token %}
@ -47,9 +50,9 @@
</div>
{% if instance.status == 5 %}
<button type="submit" class="btn btn-lg btn-success float-end" name="resizevm_cpu">{% trans "Resize" %}</button>
<button type="submit" class="btn btn-primary float-end" name="resizevm_cpu">{% trans "Resize" %}</button>
{% else %}
<button class="btn btn-lg btn-success float-end disabled" disabled>{% trans "Resize" %}</button>
<button class="btn btn-primary float-end disabled" disabled>{% trans "Resize" %}</button>
{% endif %}
</form>
<div class="clearfix"></div>
@ -63,7 +66,7 @@
<div class="d-grid col-sm-3">
<input name="id" value="{{ id }}" hidden/>
{% if vcpu.enabled == 'yes' and vcpu.hotpluggable == "yes" %}
<button type="submit" class="btn btn-success" value="False" name="set_vcpu" title="{% trans "Disable" %}">{{ id }}</button>
<button type="submit" class="btn btn-primary" value="False" name="set_vcpu" title="{% trans "Disable" %}">{{ id }}</button>
{% elif vcpu.enabled == 'yes' and vcpu.hotpluggable == "no" %}
<button type="button" class="btn btn-info" title="{% trans "Constant" %}">{{ id }}</button>
{% else %}
@ -78,11 +81,11 @@
{% endif %}
{% else %}
{% trans "You don't have permission for resizing instance" %}
<button class="btn btn-lg btn-success float-end disabled" disabled>{% trans "Resize" %}</button>
<button class="btn btn-primary float-end disabled" disabled>{% trans "Resize" %}</button>
{% endif %}
<div class="clearfix"></div>
</div>
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="resizevm_mem">
<div role="tabpanel" class="tab-pane" id="resizevm_mem">
{% if can_change %}
<form action="{% url 'instances:resize_memory' instance.id %}" method="post" role="form" aria-label="Resize instance memory form">
{% csrf_token %}
@ -120,15 +123,15 @@
{% endif %}
</div>
</div>
<button type="submit" class="btn btn-lg btn-success float-end" name="resizevm_mem">{% trans "Resize" %}</button>
<button type="submit" class="btn btn-primary float-end" name="resizevm_mem">{% trans "Resize" %}</button>
</form>
{% else %}
{% trans "You don't have permission for resizing instance" %}
<button class="btn btn-lg btn-success float-end disabled" disabled>{% trans "Resize" %}</button>
<button class="btn btn-primary float-end disabled" disabled>{% trans "Resize" %}</button>
{% endif %}
<div class="clearfix"></div>
</div>
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="resizevm_disk">
<div role="tabpanel" class="tab-pane" id="resizevm_disk">
{% if can_change %}
<form action="{% url 'instances:resize_disk' instance.id %}" method="post" role="form" aria-label="Resize instance disk form">
{% csrf_token %}
@ -152,16 +155,17 @@
</div>
{% endfor %}
{% if instance.status == 1 or instance.status == 3 or instance.status == 5 %}
<button type="submit" class="btn btn-lg btn-success float-end" name="resizevm_disk">{% trans "Resize" %}</button>
<button type="submit" class="btn btn-primary float-end" name="resizevm_disk">{% trans "Resize" %}</button>
{% else %}
<button class="btn btn-lg btn-success float-end disabled" disabled>{% trans "Resize" %}</button>
<button class="btn btn-primary float-end disabled" disabled>{% trans "Resize" %}</button>
{% endif %}
</form>
{% else %}
{% trans "You don't have permission for resizing instance" %}
<button class="btn btn-lg btn-success float-end disabled" disabled>{% trans "Resize" %}</button>
<button class="btn btn-primary float-end disabled" disabled>{% trans "Resize" %}</button>
{% endif %}
<div class="clearfix"></div>
</div>
</div>
</div>
</div>

View file

@ -2,9 +2,11 @@
{% load django_bootstrap5 %}
{% load bootstrap_icons %}
{% load operations %}
<div class="tab-pane mt-3" id="settings" role="tabpanel">
<div class="tab-pane" id="settings" role="tabpanel">
<!-- Nav tabs -->
<ul class="nav nav-tabs vm-settings-tabs" role="tablist" aria-label="Instance settings">
<div class="card">
<div class="card-header bg-transparent pb-0">
<ul class="nav nav-underline small" role="tablist" aria-label="Instance settings">
{% if instance.guest_agent_ready %}
<li class="nav-item" role="presentation">
<button class="px-1 nav-link" data-bs-toggle="tab" id="osinfo-tab" data-bs-target="#osinfo" type="button" role="tab" aria-controls="osinfo" aria-selected="false" onclick="get_osinfo();">
@ -79,23 +81,24 @@
</li>
{% endif %}
</ul>
</div>
{% if instance.status != 5 %}
<p class="small text-muted my-2" id="next-start-note">{% trans "While the VM runs, most settings here apply at its next start." %}</p>
<p class="small text-body-secondary px-3 pt-3 mb-0" id="next-start-note">{% trans "While the VM runs, most settings here apply at its next start." %}</p>
{% endif %}
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-content card-body">
{% if instance.guest_agent_ready %}
{% include 'instances/info_tab.html' %}
{% endif %}
{% if request.user.is_superuser %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3 active" id="boot_opt">
<div role="tabpanel" class="tab-pane active" id="boot_opt">
<p class="fw-bold">{% trans 'Autostart' %}</p>
<div class="row">
<div class="col-sm-12 text-center">
<p>{% trans "Autostart your instance when host server is power on " %}</p>
{% if instance.autostart == 0 %}
<form action="{% url 'instances:set_autostart' instance.id %}" method="post" class="d-inline">{% csrf_token %}
<button type="submit" class="btn btn-success">{% trans "Enable" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Enable" %}</button>
</form>
{% else %}
<form action="{% url 'instances:unset_autostart' instance.id %}" method="post" class="d-inline">{% csrf_token %}
@ -111,7 +114,7 @@
<p>{% trans "Enable Boot Menu for your instance when it starts up " %}
{% if instance.config_bootmenu == 0 %}
<form action="{% url 'instances:set_bootmenu' instance.id %}" method="post" role="form" aria-label="Enable/disable instance boot order form">{% csrf_token %}
<input type="submit" class="btn btn-success" name="set_bootmenu" title="{% trans 'Show boot menu' %}" value="{% trans "Enable" %}">
<input type="submit" class="btn btn-primary" name="set_bootmenu" title="{% trans 'Show boot menu' %}" value="{% trans "Enable" %}">
</form>
{% else %}
<form action="{% url 'instances:unset_bootmenu' instance.id %}" method="post" role="form" aria-label="Enable/disable instance boot order form">{% csrf_token %}
@ -159,18 +162,18 @@
</div>
<div class="d-flex justify-content-center">
<div class="d-grid col-sm-6">
<input type="submit" class="btn btn-success" name="set_bootorder" value="{% trans "Apply" %}">
<input type="submit" class="btn btn-primary" name="set_bootorder" value="{% trans "Apply" %}">
</div>
</div>
</form>
{% endif %}
<div class="clearfix"></div>
</div>
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="disks">
<div role="tabpanel" class="tab-pane" id="disks">
<form action="{% url 'instances:add_cdrom' instance.id %}" method="post" role="form" aria-label="Add CD-ROM form">{% csrf_token %}
<p class="fw-bold">
{% trans "Instance Media" %}
<button class="btn btn-success float-end"
<button class="btn btn-primary float-end"
type="submit"
title="{% trans 'Add CD-ROM' %}"
{% if instance.status != 5 %} disabled {% endif %}>
@ -209,9 +212,9 @@
</select>
{% if instance.media_iso %}
<button type="submit" class="btn btn-sm btn-success" name="mount_iso" value="{{ cd.dev }}">{% trans "Mount" %}</button>
<button type="submit" class="btn btn-sm btn-primary" name="mount_iso" value="{{ cd.dev }}">{% trans "Mount" %}</button>
{% else %}
<button class="btn btn-sm btn-success disabled" disabled>{% trans "Mount" %}</button>
<button class="btn btn-sm btn-primary disabled" disabled>{% trans "Mount" %}</button>
{% endif %}
{% if instance.status == 5 %}
<button type="submit" class="btn btn-sm btn-danger float-end" formaction="{% url 'instances:detach_cdrom' instance.id cd.dev %}" formmethod="post" title="{% trans "Detach CD-ROM (remove device)" %}">
@ -239,7 +242,7 @@
</a>
<input type="text" class="form-control" value="{{ cd.image }}" disabled/>
<input type="hidden" name="path" value="{{ cd.path }}">
<button type="submit" class="btn btn-sm btn-success" value="{{ cd.dev }}" name="umount_iso">{% trans "Unmount" %}</button>
<button type="submit" class="btn btn-sm btn-primary" value="{{ cd.dev }}" name="umount_iso">{% trans "Unmount" %}</button>
</div>
</form>
{% endif %}
@ -353,7 +356,7 @@
</div>
<div class="clearfix"></div>
</div>
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="network">
<div role="tabpanel" class="tab-pane" id="network">
<div class="mb-3">
{% trans "Add a network device" %}
{% include 'add_instance_network_block.html' %}
@ -471,7 +474,7 @@
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
<button class="btn btn-success" name="change_network" title="{% trans "Apply network changes" %}">{% trans "Apply" %}</button>
<button class="btn btn-primary" name="change_network" title="{% trans "Apply network changes" %}">{% trans "Apply" %}</button>
</div>
</div>
</div>
@ -596,7 +599,7 @@
<div class="clearfix"></div>
</div>
{% if show_hostdevs %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="hostdevs">
<div role="tabpanel" class="tab-pane" id="hostdevs">
<p class="small text-body-secondary">{% trans "Devices of the host given to the VM. The VM starts only on a host that has them; it is not migrated while it has them, and its clones get none." %}</p>
{% if request.user.is_superuser %}
{# the add forms come when the tab opens: their choices scan every VM of the host #}
@ -685,7 +688,7 @@
{% endif %}
</div>
{% endif %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="migrate">
<div role="tabpanel" class="tab-pane" id="migrate">
<p>{% trans "For migration both host servers must have equal settings and OS type" %}</p>
<form action="{% url 'instances:migrate' instance.id %}" class="ms-3 form" method="post" role="form" aria-label="Migrate instance form">
{% csrf_token %}{% submission_key %}
@ -762,14 +765,14 @@
</div>
</div>
{% if computes_count != 1 %}
<button type="submit" class="btn btn-lg btn-success float-end" name="migrate" >{% trans "Migrate" %}</button>
<button type="submit" class="btn btn-primary float-end" name="migrate" >{% trans "Migrate" %}</button>
{% else %}
<button class="btn btn-lg btn-success float-end disabled" disabled>{% trans "Migrate" %}</button>
<button class="btn btn-primary float-end disabled" disabled>{% trans "Migrate" %}</button>
{% endif %}
</form>
<div class="clearfix"></div>
</div>
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="xmledit">
<div role="tabpanel" class="tab-pane" id="xmledit">
{% if instance.status != 5 %}
<p id="xml-next-start">{% trans "This is the definition for the VM's next start; a saved change applies then." %}</p>
{% endif %}
@ -778,7 +781,7 @@
<div class="col-sm-12 xmlheight">
<textarea id="editor">{{ instance.inst_xml }}</textarea>
</div>
<button type="submit" class="btn btn-lg btn-success float-end" name="change_xml">
<button type="submit" class="btn btn-primary float-end" name="change_xml">
{% trans "Change" %}
</button>
</form>
@ -798,13 +801,13 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="button" class="btn btn-success" id="xml-review-save">{% trans "Save" %}</button>
<button type="button" class="btn btn-primary" id="xml-review-save">{% trans "Save" %}</button>
</div>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="users">
<div role="tabpanel" class="tab-pane" id="users">
<div>
<div class="mb-3">
<span class="fw-bold">{% trans "Instance owners" %}</span>
@ -831,7 +834,7 @@
</div>
{% endif %}
{% if can_manage_console %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="vncsettings">
<div role="tabpanel" class="tab-pane" id="vncsettings">
<form method="post" action="{% url 'instances:update_console' instance.id %}">
{% csrf_token %}
{% if instance.status != 5 %}
@ -842,9 +845,9 @@
{% bootstrap_form console_form layout='horizontal' %}
<div class="float-end">
{% if instance.status != 5 %}
<button class="btn btn-success" disabled>{% trans "Update" %}</button>
<button class="btn btn-primary" disabled>{% trans "Update" %}</button>
{% else %}
<button type="submit" class="btn btn-success ">{% trans "Update" %}</button>
<button type="submit" class="btn btn-primary ">{% trans "Update" %}</button>
{% endif %}
</div>
</form>
@ -852,7 +855,7 @@
</div>
{% endif %}
{% if can_clone %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="clone">
<div role="tabpanel" class="tab-pane" id="clone">
<p class="fw-bold">{% trans "Create a clone" %}</p>
{% if usb_hostdevs or pci_hostdevs or other_hostdevs %}<p class="small text-body-secondary">{% trans "The clone gets none of the VM's host devices: one VM holds a device of the host." %}</p>{% endif %}
<form class="form" action="{% url 'instances:clone' instance.id %}" method="post" role="form" aria-label="Create clone form">{% csrf_token %}{% submission_key %}
@ -862,7 +865,7 @@
<div class="col-sm-6">
<div class="input-group">
<input id="clone_name" type="text" class="form-control" name="name" value="{{ instance.name }}-clone">
<button type="button" class="btn btn-success" name="guess-clone-name"
<button type="button" class="btn btn-primary" name="guess-clone-name"
onclick="guess_clone_name()">{% trans "Guess" %}</button>
</div>
</div>
@ -889,9 +892,9 @@
<div class="col-sm-6">
<div class="input-group">
<input type="text" class="form-control" name="clone-net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/>
<button type="button" class="btn btn-success" name="random-mac-{{ forloop.counter0 }}"
<button type="button" class="btn btn-primary" name="random-mac-{{ forloop.counter0 }}"
onclick="random_mac('clone-net-mac-{{ forloop.counter0 }}')">{% trans "Random" %}</button>
<button type="button" class="btn btn-success" name="guess-mac-{{ forloop.counter0 }}"
<button type="button" class="btn btn-primary" name="guess-mac-{{ forloop.counter0 }}"
onclick="guess_mac_address('#clone_name', {{ forloop.counter0 }})">{% trans "Guess" %}</button>
</div>
</div>
@ -940,16 +943,16 @@
</div>
</div>
{% if instance.status == 1 or instance.status == 3 or instance.status == 5 %}
<button type="submit" class="btn btn-lg btn-success float-end" name="clone">{% trans "Clone" %}</button>
<button type="submit" class="btn btn-primary float-end" name="clone">{% trans "Clone" %}</button>
{% else %}
<button class="btn btn-lg btn-success float-end disabled" disabled name="clone">{% trans "Clone" %}</button>
<button class="btn btn-primary float-end disabled" disabled name="clone">{% trans "Clone" %}</button>
{% endif %}
</form>
<div class="clearfix"></div>
</div>
{% endif %}
{% if can_change %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="options">
<div role="tabpanel" class="tab-pane" id="options">
<p>{% trans "To set instance template name description, shutdown the instance." %}</p>
<form action="{% url 'instances:change_options' instance.id %}" method="post" role="form" aria-label="Set instance template name description form">{% csrf_token %}
<div class="row">
@ -978,9 +981,9 @@
<div class="row">
<div class="d-grid offset-sm-3 col-sm-6">
{% if instance.status == 5 %}
<button type="submit" class="btn btn-success" name="change_options">{% trans "Change" %}</button>
<button type="submit" class="btn btn-primary" name="change_options">{% trans "Change" %}</button>
{% else %}
<button class="btn btn-success disabled" disabled name="change_options">{% trans "Change" %}</button>
<button class="btn btn-primary disabled" disabled name="change_options">{% trans "Change" %}</button>
{% endif %}
</div>
</div>
@ -999,9 +1002,9 @@
{% endfor %}
</select>
{% if instance.status == 5 %}
<button type="submit" class="btn btn-success" name="set_video_model">{% trans "Set" %}</button>
<button type="submit" class="btn btn-primary" name="set_video_model">{% trans "Set" %}</button>
{% else %}
<button class="btn btn-success disabled" disabled name="set_video_model">{% trans "Set" %}</button>
<button class="btn btn-primary disabled" disabled name="set_video_model">{% trans "Set" %}</button>
{% endif %}
</div>
</div>
@ -1019,9 +1022,9 @@
<option value="False" {% if not instance.vcpus %} selected {% endif %}>{% trans 'Disabled' %}</option>
</select>
{% if instance.status == 5 %}
<button type="submit" class="btn btn-success" name="set_vcpu_hotplug">{% trans "Set" %}</button>
<button type="submit" class="btn btn-primary" name="set_vcpu_hotplug">{% trans "Set" %}</button>
{% else %}
<button class="btn btn-success" name="set_vcpu_hotplug" disabled>{% trans "Set" %}</button>
<button class="btn btn-primary" name="set_vcpu_hotplug" disabled>{% trans "Set" %}</button>
{% endif %}
</div>
</div>
@ -1047,7 +1050,7 @@
<option value="True" {% if instance.guest_agent %} selected {% endif %}>{% trans 'Enabled' %}</option>
<option value="False" {% if not instance.guest_agent %} selected {% endif %}>{% trans 'Disabled' %}</option>
</select>
<button type="submit" class="btn btn-success" name="set_guest_agent">{% trans "Set" %}</button>
<button type="submit" class="btn btn-primary" name="set_guest_agent">{% trans "Set" %}</button>
</div>
</div>
</div>
@ -1057,4 +1060,5 @@
</div>
{% endif %}
</div>
</div>
</div>

View file

@ -3,10 +3,12 @@
{% get_current_timezone as TIME_ZONE %}
{% load bootstrap_icons %}
{% load operations %}
<div class="tab-pane mt-3" id="snapshots" role="tabpanel">
<div class="tab-pane" id="snapshots" role="tabpanel">
{% with busy=instance.snapshot_jobs held=instance.held_by_job %}
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist" aria-label="Instance snapshot menu">
<div class="card">
<div class="card-header bg-transparent pb-0">
<ul class="nav nav-underline small" role="tablist" aria-label="Instance snapshot menu">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#takesnapshot" type="button" role="tab" aria-controls="takesnapshot" aria-selected="true">
{% trans "Take Snapshot - Internal" %}
@ -23,14 +25,15 @@
</button>
</li>
</ul>
</div>
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-content card-body">
{% if busy %}
<div class="alert alert-warning mt-3" role="status">
{% trans "A disk job is running on this VM; snapshot actions are unavailable until it ends." %}
</div>
{% endif %}
<div role="tabpanel" class="tab-pane border border-top-0 p-3 active" id="takesnapshot">
<div role="tabpanel" class="tab-pane active" id="takesnapshot">
{% if instance.status != 5 %}
<p>{% trans "With running machine, internal snapshots may take more than an hour, depending on how much memory has on your instance and how large the disk is." %} </p>
<p class="text-danger">{% trans "The VM is paused while its memory is saved." %}</p>
@ -44,15 +47,15 @@
<span class="input-group-text">|</span>
<input type="text" class="form-control form-control-lg" name="description" placeholder="{% trans "Snapshot Description" %}" maxlength="45">
{% if instance.external_snapshots or busy or held %}
<button type="submit" class="btn btn-lg btn-success float-end" name="snapshot" disabled>{% trans "Take Snapshot" %}</button>
<button type="submit" class="btn btn-primary float-end" name="snapshot" disabled>{% trans "Take Snapshot" %}</button>
{% else %}
<button type="submit" class="btn btn-lg btn-success float-end" name="snapshot">{% trans "Take Snapshot" %}</button>
<button type="submit" class="btn btn-primary float-end" name="snapshot">{% trans "Take Snapshot" %}</button>
{% endif %}
</div>
</form>
<div class="clearfix"></div>
</div> <!-- Tab pane - takesnapshot -->
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="takeextsnapshot">
<div role="tabpanel" class="tab-pane" id="takeextsnapshot">
<p>{% trans "Create an external snapshot. Each disk keeps its state in a new file next to it." %}</p>
<form action="{% url 'instances:create_external_snapshot' instance.id %}" method="post" role="form" aria-label="Create external snapshot form">
{% csrf_token %}{% submission_key %}
@ -61,9 +64,9 @@
<span class="input-group-text">|</span>
<input type="text" class="form-control form-control-lg" name="description" placeholder="{% trans "Snapshot Description" %}" maxlength="45">
{% if instance.internal_snapshots or busy or held %}
<button type="submit" class="btn btn-lg btn-success float-end" name="snapshot" disabled>{% trans "Take Snapshot" %}</button>
<button type="submit" class="btn btn-primary float-end" name="snapshot" disabled>{% trans "Take Snapshot" %}</button>
{% else %}
<button type="submit" class="btn btn-lg btn-success float-end" name="snapshot">{% trans "Take Snapshot" %}</button>
<button type="submit" class="btn btn-primary float-end" name="snapshot">{% trans "Take Snapshot" %}</button>
{% endif %}
</div>
<div class="form-check mb-3">
@ -83,7 +86,7 @@
</form>
<div class="clearfix"></div>
</div> <!--tab pane takeextsnapshot-->
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="managesnapshot">
<div role="tabpanel" class="tab-pane" id="managesnapshot">
{% if instance.internal_snapshots or instance.external_snapshots %}
<p>{% trans "Choose a snapshot for restore/delete" %}</p>
{% if instance.internal_snapshots %}
@ -188,5 +191,6 @@
{% endif %}
</div> <!--tab pane managesnapshot-->
</div> <!-- Tab content-->
</div>
{% endwith %}
</div> <!-- Tab panel-->

View file

@ -1,8 +1,10 @@
{% load i18n %}
{% load bootstrap_icons %}
<div class="tab-pane mt-3" id="graphics" role="tabpanel" >
<div class="tab-pane" id="graphics" role="tabpanel" >
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist" aria-label="Instance graphs and logs menu">
<div class="card">
<div class="card-header bg-transparent pb-0">
<ul class="nav nav-underline small" role="tablist" aria-label="Instance graphs and logs menu">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#graphs" type="button" role="tab" aria-controls="graphs" aria-selected="true">
{% trans "Real Time" %}
@ -14,10 +16,11 @@
</button>
</li>
</ul>
</div>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane border border-top-0 p-3 active" id="graphs">
<div class="mb-1 card border-success">
<div class="tab-content card-body">
<div role="tabpanel" class="tab-pane active" id="graphs">
<div class="mb-3 card">
<div class="card-header">
<h5 class="card-title">{% bs_icon 'arrow-right' %}
{% trans "CPU Usage" %}
@ -27,7 +30,7 @@
<div class="chart position-relative"><canvas id="cpuChart" role="img" aria-label="{% trans "CPU Usage" %}"></canvas></div>
</div>
</div>
<div class="mb-1 card border-danger">
<div class="mb-3 card">
<div class="card-header">
<h5 class="card-title">{% bs_icon 'arrow-right' %}
{% trans "Memory Usage" %}
@ -38,7 +41,7 @@
</div>
</div>
{% for net in instance.networks %}
<div class="mb-1 card border-info">
<div class="mb-3 card">
<div class="card-header">
<h5 class="card-title">{% bs_icon 'arrow-right' %}
{% trans "Bandwidth Device" %}: eth{{ forloop.counter0 }}
@ -50,7 +53,7 @@
</div>
{% endfor %}
{% for disk in instance.disks %}
<div class="mb-1 card border-warning">
<div class="mb-3 card">
<div class="card-header">
<h5 class="card-title">{% bs_icon 'arrow-right' %}
{% trans "Disk I/O device" %}: {{ disk.dev }}
@ -63,7 +66,7 @@
{% endfor %}
<div class="clearfix"></div>
</div>
<div role="tabpanel" class="tab-pane border border-top-0 p-3" id="logs">
<div role="tabpanel" class="tab-pane" id="logs">
<div class="table-responsive">
<table class="table table-striped align-middle" id="logs_table">
<thead>
@ -84,6 +87,7 @@
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<script>
function update_logs_table() {

View file

@ -34,7 +34,7 @@ SUPERUSER_ONLY = {"vcpu hotplug", "video model", "guest agent"}
class PageControls:
def setUp(self):
AppSettings.objects.filter(
key__in=["SHOW_ACCESS_ROOT_PASSWORD", "SHOW_ACCESS_SSH_KEYS", "VIEW_INSTANCE_DETAIL_BOTTOM_BAR"]
key__in=["SHOW_ACCESS_ROOT_PASSWORD", "SHOW_ACCESS_SSH_KEYS"]
).update(value="True")
self.compute = Compute.objects.create(
name="ctl-compute", hostname="127.0.0.1:1", login="root", password="", type=1
@ -124,6 +124,24 @@ class PageControlsTestCase(PageControls, TestCase):
)
self.assertEqual(result.returncode, 0, result.stderr)
def test_the_header_names_the_vm_its_place_and_resources(self):
"""Host links for administrators, the VM list for its owner; the
guest agent's state in words; each disk with its device name."""
disks = [{"dev": "vda", "size": 2 << 30}, {"dev": "vdb", "size": 1 << 30}]
with patch.object(Instance, "disks", new_callable=PropertyMock, return_value=disks), \
patch.object(Instance, "guest_agent", new_callable=PropertyMock, return_value=True), \
patch.object(Instance, "guest_agent_ready", new_callable=PropertyMock, return_value=False):
self.controls(get_user_model().objects.create_superuser("hdr-admin", "a@example.com", "x"))
admin_page = lxml_html.fromstring(self.last_page)
self.controls(self.user("hdr-owner", owner={}))
owner_page = lxml_html.fromstring(self.last_page)
links = "//nav[@aria-label='Location']/a"
self.assertEqual([a.text_content().strip() for a in admin_page.xpath(links)], ["ctl-compute", "Instances"])
self.assertEqual([a.text_content().strip() for a in owner_page.xpath(links)], ["Instances"])
self.assertIn("Guest agent not connected", self.last_page)
self.assertIn("vda 2.0\xa0GB · vdb 1.0\xa0GB", self.last_page)
self.assertEqual(owner_page.xpath("normalize-space(//dt[.='IPv4']/following-sibling::dd)"), "—")
def test_superuser_sees_every_control(self):
superuser = get_user_model().objects.create_superuser("ctl_super", "ctl_super@example.com", "x")
self.assertEqual(

View file

@ -142,7 +142,8 @@ class SnapshotTabTestCase(TestCase):
self.assertEqual(
flags, sorted([libvirt.VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL, libvirt.VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL])
)
self.assertEqual(doc.xpath("//i[contains(@title, 'snapshot(s)')]/@title"), ["There are 2 snapshot(s)"])
self.assertEqual(doc.xpath("normalize-space(//span[contains(., ' snapshots')][not(*[contains(., ' snapshots')])])"),
"2 snapshots")
def test_a_failed_job_query_still_renders_the_tab(self):
# the actions themselves check for jobs again and refuse if that fails

View file

@ -249,7 +249,7 @@ def _instance_page(request, pk, instance, compute):
if can_manage_console:
console_form = ConsoleForm(instance=instance, can_set_listener=request.user.is_superuser)
console_listener_addresses = settings.QEMU_CONSOLE_LISTENER_ADDRESSES
bottom_bar = app_settings.VIEW_INSTANCE_DETAIL_BOTTOM_BAR
ipv4_addresses = [address for net in instance.networks for address in net["ipv4"] or []]
try:
userinstance = UserInstance.objects.get(
instance__compute_id=compute.id,

View file

@ -102,14 +102,3 @@ th[aria-sort="ascending"]:not([data-sort-method="none"])::after {
.host-nav .breadcrumb-item + .breadcrumb-item::before {
padding-right: var(--bs-breadcrumb-item-padding-x);
}
/* a VM's settings tabs (instances/settings_tab.html) on one line in every
theme: the themes' tab font sizes and spacing differ by up to a tab */
.vm-settings-tabs {
font-size: .875rem;
}
.vm-settings-tabs .nav-link {
padding-left: .75rem;
padding-right: .75rem;
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrows-angle-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707"/>
</svg>

After

Width:  |  Height:  |  Size: 492 B

View file

@ -0,0 +1,8 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart-line" viewBox="0 0 16 16">
<path d="M11 2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12h.5a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1H1v-3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3h1V7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7h1zm1 12h2V2h-2zm-3 0V7H7v7zm-5 0v-3H2v3z"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

View file

@ -23,7 +23,7 @@ function restoreInstanceTab(doc, hash, show, loadInfo) {
}
}
var settings = doc.getElementById("settings");
var tabs = settings ? settings.querySelector(".nav-tabs") : null;
var tabs = settings ? settings.querySelector("[role=tablist]") : null;
if (tabs && !tabs.querySelector(".nav-link.active")) {
var first = tabs.querySelector(".nav-link:not(#osinfo-tab)");
if (first) {

View file

@ -41,6 +41,7 @@
{% endif %}
<div role="main" class="container">
{% block page_header %}{# a page with a heading of its own leaves it out #}
<div>
<div>
<div class="float-end">
@ -53,6 +54,7 @@
</h3>
</div>
</div>
{% endblock page_header %}
<div id="messages">{% bootstrap_messages %}</div>
{% block content %}{% endblock %}
</div>

View file

@ -21,7 +21,7 @@ function* walk(node) {
}
function matches(node, selector) {
if (selector === ".nav-tabs") return node.classList.has("nav-tabs");
if (selector === "[role=tablist]") return node.attrs.role === "tablist";
if (selector === ".nav-link.active") return node.classList.has("nav-link") && node.classList.has("active");
if (selector === ".nav-link:not(#osinfo-tab)") return node.classList.has("nav-link") && node.id !== "osinfo-tab";
if (selector === "#osinfo-tab") return node.id === "osinfo-tab";
@ -40,7 +40,7 @@ function link(target, extra = []) {
// The VM page: top-level panes, Settings with its own tabs and panes.
function page(settingsLinks) {
const settings = el("settings", ["tab-pane"], [
el("", ["nav-tabs"], settingsLinks),
el("", ["nav", "nav-underline"], settingsLinks, {role: "tablist"}),
el("", ["tab-content"], [el("osinfo", ["tab-pane"]), el("options", ["tab-pane"]), el("vncsettings", ["tab-pane"])]),
]);
const root = el("", [], [el("", ["tab-content"], [el("power", ["tab-pane", "active"]), el("access", ["tab-pane"]), settings])]);