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

feat(instances): the Network tab shows each NIC as a cable to its source

A NIC's row put its name and host device in one column, "Source",
"IPv4 unknown" and the link switch in another, and its model, MAC and
filter behind Device details; every NIC also had a "QoS limits · none"
fold and solid Edit, Add QoS and Delete buttons.

Now a row reads eth0, a cable, then the source: the cable is solid green
while the link is up and dashed grey while it is down. Under it are the
IPv4 addresses (or "No IPv4 address known") beside the link switch, and
a line of model, MAC, filter and whether bandwidth is limited. Device
details keep the type, the host device and IPv6; QoS limits show only
for a NIC that has one. The buttons are outlined. Dialogs, form names
and posts are unchanged.
This commit is contained in:
catborise 2026-11-04 09:13:43 +03:00
parent 0b2604f151
commit a06565fa0b
6 changed files with 496 additions and 455 deletions

View file

@ -289,62 +289,64 @@
{# a row per NIC in one list, as the disks; its QoS limits fold out like its device details #}
<section aria-labelledby="nics-heading">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<h2 class="h6 mb-0" id="nics-heading">{% trans "Network devices" %} <span class="text-body-secondary fw-normal">· {{ instance.config_networks|length }}</span></h2>
<h2 class="h6 mb-0" id="nics-heading">{% trans "Network devices" %} <span class="text-body-secondary fw-normal">({{ instance.config_networks|length }})</span></h2>
{% include 'add_instance_network_block.html' %}
</div>
<div class="card">
<div class="list-group list-group-flush">
{% for network in instance.config_networks %}
<article class="list-group-item py-3" aria-labelledby="nic-{{ forloop.counter0 }}">
<div class="row g-3 align-items-start small">
<div class="col-12 col-md-2">
<h3 class="h6 mb-1" id="nic-{{ forloop.counter0 }}">{% bs_icon 'ethernet' %} eth{{ forloop.counter0 }}</h3>
<div class="text-body-secondary text-break">{{ network.target|default:_("no target") }}</div>
{# the NIC as a cable from the VM to its source: solid while its link is up, dashed while down #}
<div class="d-flex flex-wrap align-items-center column-gap-3 row-gap-2">
<div class="d-flex align-items-center gap-2 me-auto text-break">
<h3 class="h6 mb-0" id="nic-{{ forloop.counter0 }}">eth{{ forloop.counter0 }}</h3>
<span class="nic-wire{% if network.state == 'up' %} nic-up{% endif %}" aria-hidden="true"></span>
<span class="fw-semibold">{{ network.nic|default:"—" }}</span>
</div>
<div class="col-12 col-md-6">
<div class="text-break"><span class="text-body-secondary">{% trans "Source" %}</span> <strong class="ms-1">{{ network.nic|default:"—" }}</strong></div>
<div class="mb-2"><span class="text-body-secondary">{% trans "IPv4" %}</span> <span class="ms-1">{{ network.ipv4|join:", "|default:_("unknown") }}</span></div>
<form action="{% url 'instances:set_link_state' instance.id %}" method="post" aria-label="{% trans "Link state form" %}">{% csrf_token %}
<input type="hidden" name="mac" value="{{ network.mac }}">
<input type="hidden" name="set_link_state" value="{{ network.state }}">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch" id="link-{{ forloop.counter0 }}"{% if network.state == 'up' %} checked{% endif %} data-submit-on-change>
<label class="form-check-label" for="link-{{ forloop.counter0 }}">{% if network.state == 'up' %}{% trans "Link up" %}{% else %}{% trans "Link down" %}{% endif %}</label>
</div>
</form>
</div>
<div class="col-12 col-md-4 d-flex flex-wrap justify-content-md-end gap-2">
<div class="d-flex flex-wrap gap-2">
{# a NIC that is a host device is changed in the XML editor #}
{% if network.type != "hostdev" %}
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#editInstanceNetwork{{ forloop.counter0 }}">{% trans "Edit" %}</button>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#editInstanceNetwork{{ forloop.counter0 }}">{% trans "Edit" %}</button>
{% endif %}
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#AddQos{{ forloop.counter0 }}">{% trans "Add QoS" %}</button>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#AddQos{{ forloop.counter0 }}">{% trans "Add QoS" %}</button>
{% if network.type != "hostdev" %}
<form action="{% url 'instances:delete_network' instance.id %}" method="post" aria-label="{% trans "Delete network device form" %}">{% csrf_token %}
<button class="btn btn-sm btn-danger" value="{{ network.mac }}" name="delete_network"
<button class="btn btn-sm btn-outline-danger" value="{{ network.mac }}" name="delete_network"
data-confirm="{% blocktrans with mac=network.mac %}Remove network card {{ mac }}? A running VM loses its connection through it at once.{% endblocktrans %}">{% trans "Delete" %}</button>
</form>
{% endif %}
</div>
</div>
<div class="d-flex flex-wrap align-items-center column-gap-4 row-gap-1 mt-2">
<span class="text-break">{% if network.ipv4 %}{{ network.ipv4|join:", " }}{% else %}<span class="text-body-secondary">{% trans "No IPv4 address known" %}</span>{% endif %}</span>
<form action="{% url 'instances:set_link_state' instance.id %}" method="post" aria-label="{% trans "Link state form" %}">{% csrf_token %}
<input type="hidden" name="mac" value="{{ network.mac }}">
<input type="hidden" name="set_link_state" value="{{ network.state }}">
<div class="form-check form-switch mb-0 small">
<input class="form-check-input" type="checkbox" role="switch" id="link-{{ forloop.counter0 }}"{% if network.state == 'up' %} checked{% endif %} data-submit-on-change>
<label class="form-check-label" for="link-{{ forloop.counter0 }}">{% if network.state == 'up' %}{% trans "Link up" %}{% else %}{% trans "Link down" %}{% endif %}</label>
</div>
</form>
</div>
<div class="small text-body-secondary text-break mt-1">{{ network.model|default:"—" }}, {{ network.mac }}{% if network.filterref %}, {% blocktrans with filter=network.filterref %}filter {{ filter }}{% endblocktrans %}{% endif %}{% if network.inbound or network.outbound %}, {% trans "bandwidth limited" %}{% endif %}</div>
{% if network.type == 'direct' %}
<p class="small text-body-secondary mt-1 mb-0">{% trans "In most configurations, macvtap does not work for host to guest network communication" %}</p>
{% endif %}
<details class="small mt-2">
<summary class="text-body-secondary">{% trans "Device details" %}</summary>
<dl class="row mb-0 mt-2" style="max-width: 32rem">
<dt class="col-4 fw-normal text-body-secondary">{% trans "Type" %}</dt><dd class="col-8 mb-1">{{ network.type }}</dd>
<dt class="col-4 fw-normal text-body-secondary">{% trans "MAC" %}</dt><dd class="col-8 mb-1 text-break">{{ network.mac }}</dd>
<dt class="col-4 fw-normal text-body-secondary">{% trans "Model" %}</dt><dd class="col-8 mb-1">{{ network.model|default:"—" }}</dd>
<dt class="col-4 fw-normal text-body-secondary">{% trans "NWFilter" %}</dt><dd class="col-8 mb-1 text-break">{{ network.filterref|default:_("None") }}</dd>
<dt class="col-4 fw-normal text-body-secondary">{% trans "Host device" %}</dt><dd class="col-8 mb-1 text-break">{{ network.target|default:_("no target") }}</dd>
<dt class="col-4 fw-normal text-body-secondary">{% trans "IPv6" %}</dt><dd class="col-8 mb-1 text-break">{{ network.ipv6|join:", "|default:_("unknown") }}</dd>
</dl>
</details>
{% if network.inbound or network.outbound %}
<details class="small">
<summary class="text-body-secondary">{% trans "QoS limits" %} · {% if network.inbound and network.outbound %}{% trans "inbound, outbound" %}{% elif network.inbound %}{% trans "inbound" %}{% elif network.outbound %}{% trans "outbound" %}{% else %}{% trans "none" %}{% endif %}</summary>
<summary class="text-body-secondary">{% trans "QoS limits" %}: {% if network.inbound and network.outbound %}{% trans "inbound, outbound" %}{% elif network.inbound %}{% trans "inbound" %}{% else %}{% trans "outbound" %}{% endif %}</summary>
{% if network.inbound %}{% include 'instances/nic_qos_form.html' with nic=forloop.counter0 direction="inbound" limit=network.inbound %}{% endif %}
{% if network.outbound %}{% include 'instances/nic_qos_form.html' with nic=forloop.counter0 direction="outbound" limit=network.outbound %}{% endif %}
<p class="text-body-secondary mt-2 mb-0">{% trans "Average and peak are in KiB/s, burst in KiB. A running VM gets a change after it is powered off and on." %}</p>
</details>
{% if network.type == 'direct' %}
<p class="small text-body-secondary border-top pt-2 mt-2 mb-0">{% bs_icon 'info-circle' %} {% trans "In most configurations, macvtap does not work for host to guest network communication" %}</p>
{% endif %}
{# the dialogs of Edit and Add QoS #}
{% if network.type != "hostdev" %}

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -230,6 +230,27 @@ button.vcpu-cell:disabled { opacity: .65; }
.snap-dot.snap-current { background: var(--bs-primary); }
.snap-dot.snap-none { border-color: var(--bs-border-color); }
/* the network tab: a NIC as a cable to its source, solid while its link is up, dashed while down */
.nic-wire {
position: relative;
flex: 0 0 3.5rem;
margin-inline: .25rem;
color: var(--bs-secondary-color);
border-top: 2px dashed currentColor;
}
.nic-wire::before, .nic-wire::after {
content: "";
position: absolute;
top: -4px;
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.nic-wire::before { left: -3px; }
.nic-wire::after { right: -3px; }
.nic-wire.nic-up { color: var(--bs-success); border-top-style: solid; }
/* a log message keeps a readable width when its table scrolls on a phone */
.log-message {
min-width: 16rem;